added missing subcommands (pdf, epub, build-all); upped version to 2.0

This commit is contained in:
eclipse 2025-10-28 22:40:22 +01:00
parent c7efbf2c3f
commit aef9f19db1
10 changed files with 304 additions and 475 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@
output/
.venv/
__pycache__/
dist/

414
build.py
View File

@ -1,414 +0,0 @@
#!/usr/bin/env python
from pathlib import Path
import subprocess
from enum import Enum
from shutil import which
from typing import Annotated
import logging
import typer
import yaml
from pypdf import PdfReader, PdfWriter
# start logger
logger = logging.getLogger(__name__)
# define directories
base_dir = Path.cwd()
config_dir = base_dir / "config"
content_dir = base_dir / "content"
cover_dir = base_dir / "cover"
output_dir = base_dir / "output"
minizine_dir = base_dir / "promo" / "minizine"
# define executables and default options
pandoc = which("pandoc")
if not pandoc:
logger.critical("pandoc is not installed.")
exit(1)
pandoc_opts = []
typst = which("typst")
if not typst:
logger.critical("typst is not installed.")
exit(1)
typst_opts = ["compile", "--root", "."]
# helper class for available output formats
class Format(str, Enum):
PDF = "pdf"
PNG = "png"
# instantiate typer app
app = typer.Typer(
no_args_is_help=True,
help="Build script to make a professionally typeset book from markdown source files that can either be read on screen (PDF or EPUB) or be used in a project with Tredition.",
context_settings={"help_option_names": ["-h", "--help"]}
)
# ##################
# INTERNAL FUNCTIONS
# ##################
def _get_input_files(target_format: str) -> list:
# read filenames from file if such a file exists
list_file = config_dir / f"source-files.{target_format}.txt"
# read file if it exists
if list_file.is_file():
with open(list_file, "r") as file:
return [line.strip() for line in file if line[0] != "#"]
# use all files otherwise; ignore tredition flag
else:
paths = content_dir.glob('**/*.md')
return list(map(str, sorted(paths)))
def _run_command(executable: str, infiles: list, ex_opts: list = [], outfile: str = ""):
# put command together
outfile = [outfile] if outfile else []
command = [executable] + ex_opts + infiles + outfile
logger.debug(f"Executing command {command}")
# run shell command
try:
stdout = subprocess.check_output(command, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
logger.critical(f"Command failed with return code {e.returncode}.")
logger.critical(f"Command output: {e.output}")
raise typer.Exit(code=e.returncode)
else:
logger.debug(f"Command finished with return code 0.")
logger.debug(f"Command output: {stdout}")
def _clean_dir(path: Path):
for file in path.iterdir():
# if file: delete
if file.is_file():
file.unlink()
# if dir: delete recursively
else:
# clean dir recursively
_clean_dir(file)
# delete dir unless it's the output_dir
if file != output_dir:
file.rmdir()
def _add_presuffix(path: Path|str, presuffix: str) -> Path:
# cast past to Path
if isinstance(path, str):
path = Path(path)
# add leading dot if nexessary
if presuffix[0] != ".":
presuffix = "." + presuffix
return path.parent / f"{path.stem}{presuffix}{path.suffix}"
def _merge_pdf_with_covers(frontcover: str, content: str, backcover: str, keep_coverless: bool=False):
pdflist = []
# check if files exist
for filename in [frontcover, content, backcover]:
if Path(filename).is_file():
pdflist.append(filename)
# merge files wwith pypdf
merger = PdfWriter()
for pdf in pdflist:
merger.append(pdf)
# write merged file
outfile = str(_add_presuffix(content, ".with-covers")) if keep_coverless else content
merger.write(outfile)
merger.close()
def _get_data_from_yaml(inputfile: str, key: str) -> str|list|dict:
with open(inputfile, "r") as file:
data = yaml.load(file, Loader=yaml.Loader)
return data.get(key)
# #############
# CLI COMMANDS
# #############
@app.command()
def pdf(
tredition: Annotated[
bool,
typer.Option("--tredition", "-t", help="Build PDFs in accordance with Tredition's requirements for print projects. Will produce a content file without covers and the book's envelope including the spine.")
] = False,
keep_coverless: Annotated[
bool,
typer.Option("--keep-coverless", "-k", help="Do not delete the content-only PDF file after merging it with the covers (does not apply if 'tredition' is set).")
] = False
):
"""
Build the printed book's PDF version (usually a single file containing front cover, content, and back cover).
"""
logger.info("Building PDF.")
# define executable
executable, executable_opts = pandoc, pandoc_opts
# add pandoc defaults file to options
defaults_file = str(config_dir / f"pandoc-defaults.{Format.PDF}.yaml")
if Path(defaults_file).is_file():
executable_opts.append("--defaults")
executable_opts.append(defaults_file)
else:
logger.critical(f"Pandoc defaults file {defaults_file} not found.")
raise typer.Exit(code=1)
# get input files
input_files = _get_input_files(Format.PDF.value)
# if tredition flag is set: remove empty first and/or last page
if not tredition:
for i in (0, -1):
if input_files[i].endswith("leere seite.md"):
del input_files[i]
# get output filename (to be used later)
outfile = _get_data_from_yaml(defaults_file, "output-file")
if not outfile:
logger.error("Could not get output filename from defaults file.")
typer.Exit(2)
# add presuffix ".tredition" to output filename if flag is set
if tredition:
outfile = str(_add_presuffix(outfile, ".tredition"))
executable_opts.append("--output")
executable_opts.append(outfile)
# make PDf
_run_command(executable, ex_opts=executable_opts, infiles=input_files)
# make PDF covers
cover(tredition=tredition, format=Format.PDF, frontcover=not tredition, backcover=not tredition, envelope=tredition)
# add front-/backcover to PDF unless tredition flag is set
if not tredition:
_merge_pdf_with_covers(str(output_dir / "frontcover.pdf"), outfile, str(output_dir / "backcover.pdf"), keep_coverless)
# check metadata -> try veraPDF
@app.command()
def epub(
tredition: Annotated[
bool,
typer.Option("--tredition", "-t", help="Build files in accordance with Tredition's requirements for ebook projects. Will produce a main EPUB file without covers and a separate PDF with the cover page.")
] = False,
validate: Annotated[
bool,
typer.Option("--validate", "-V", help="Validate completed EPUB file (requires 'epubcheck' executable in path).")
] = False
):
"""
Build the book's EPUB version (usually a single EPUB file with front cover).
"""
logger.info("Building EPUB.")
# define pandoc defaults file
defaults_file = str(config_dir / "pandoc-defaults.epub.yaml")
# define executable and options
executable, executable_opts = pandoc, pandoc_opts
executable_opts.append("--defaults")
executable_opts.append(defaults_file)
# make frontcover
format = Format.PDF if tredition else Format.PNG
cover(format, tredition, backcover=False, envelope=False)
# add cover to pandoc options (no-tredition only)
if not tredition:
executable_opts.append("--epub-cover-image")
executable_opts.append(str(output_dir / "frontcover.png"))
# get input files
input_files = _get_input_files("epub")
# get output filename
outfile = _get_data_from_yaml(defaults_file, "output-file")
# modify output filename (tredition only)
if tredition:
outfile = str(_add_presuffix(outfile, ".tredition"))
executable_opts.append("--output")
executable_opts.append(outfile)
# put together command and run it
_run_command(executable, ex_opts=executable_opts, infiles=input_files)
# validate epub
if validate and which("epubcheck"):
_run_command(which("epubcheck"), infiles=[outfile])
@app.command()
def cover(
format: Annotated[
Format,
typer.Option("--format", "-f", help="Set output format.")
] = Format.PDF,
tredition: Annotated[
bool,
typer.Option("--tredition", "-t", help="Build covers in accordance with Tredition's requirements. Produces a PDF with the ebook cover and a PDF with the printed book's envelope.")
] = False,
frontcover: Annotated[
bool,
typer.Option("--frontcover", "-c", help="Include the front cover when building.")
] = True,
backcover: Annotated[
bool,
typer.Option("--backcover", "-b", help="Include the back cover when building.")
] = False,
envelope: Annotated[
bool,
typer.Option("--envelope", "-e", help="Include the printed book's envelope when building.")
] = False,
all_covers: Annotated[
bool,
typer.Option("--all", "-a", help="Build all covers in all (sensible) formats.")
] = False
):
"""
Build only the cover(s).
"""
logger.info("Building cover(s).")
if all_covers:
envelope, frontcover, backcover = True, True, True
tredition = True
format = Format.PDF
# define executable and options
executable, executable_opts = typst, typst_opts
# check for nonsensical flag combination
if tredition and format == Format.PNG:
logger.warning("PNG can't be used as output format in combination with --tredition; setting format to PDF.")
format = Format.PDF
# select files
if tredition:
paths = cover_dir.glob("*tredition*.typ")
else:
paths = [path for path in cover_dir.glob("*.typ") if not "tredition" in path.name]
# remove input files with unset flags
if not envelope:
paths = [path for path in paths if not "envelope" in path.name]
if not frontcover:
paths = [path for path in paths if not "frontcover" in path.name]
if not backcover:
paths = [path for path in paths if not "backcover" in path.name]
# iterate over files
for path in paths:
# get input file name
input_files = [str(path)]
# set output file name
output_file = str(output_dir / f"{path.stem}.{format.value}")
# run command
_run_command(executable, infiles=input_files, outfile=output_file, ex_opts=executable_opts)
# call function again if flag "all" is set (but don't recurse)
if all_covers:
cover(Format.PDF, tredition=False, frontcover=True, backcover=True, envelope=False, all_covers=False)
cover(Format.PNG, tredition=False, frontcover=True, backcover=True, envelope=False, all_covers=False)
@app.command()
def minizine(
format: Annotated[
Format,
typer.Option("--format", "-f", help="Set output format.")
] = Format.PDF
):
"""
Build the minizine for the book.
"""
logger.debug("Building Minizine.")
# define executable and options
executable, executable_opts = typst, typst_opts
# iterate over input files
for path in minizine_dir.glob("*.typ"):
# get input file name
input_files = [str(path)]
# set output file name
output_file = str(output_dir / f"{path.stem}.{format.value}")
# run command
_run_command(executable, infiles=input_files, outfile=output_file, ex_opts=executable_opts)
@app.command("build-all")
def build_all():
"""
Build every available target, with and without Tredition specs (where applicable).
"""
logger.debug("Building everything.")
clean()
minizine(format=Format.PDF)
minizine(format=Format.PNG)
cover(all_covers=True)
epub()
epub(tredition=True)
pdf()
pdf(tredition=True)
@app.command()
def clean():
"""
Remove all files from output directory.
"""
logger.info("Cleaning output directory.")
_clean_dir(output_dir)
@app.callback()
def main(
log_level: Annotated[
str,
typer.Option("--log-level", "-l", help="Set log level.")
] = "INFO",
outdir: Annotated[
str,
typer.Option("--output-dir", "-o", help="Specify output directory.")
] = "output/"
):
# set user log level
if not getattr(logging, log_level.upper()):
log_level = "INFO"
# configure logger
logging.basicConfig(
level=getattr(logging, log_level.upper()),
format="{asctime} {levelname}: {message}",
style="{",
datefmt="%Y-%m-%d %H:%M:%S"
)
logger.debug("Logger configured.")
# set output directory
p = Path(outdir).expanduser()
globals()["output_dir"] = p if p.is_absolute() else base_dir / p
od = globals()["output_dir"]
logger.debug(f"Output directory is {od}")
od.mkdir(parents=True, exist_ok=True)
if __name__ == "__main__":
app()

View File

@ -1,12 +1,10 @@
[tool.poetry]
name = "bookbuild"
version = "0.1.0"
version = "0.2.0"
description = "Build script for my book projects"
authors = ["Tobias Radloff <mail@tobias-radloff.de>"]
license = "GPLv3"
packages = [
{ include = "bookbuild", from = "src" }
]
packages = [{ include = "bookbuild", from = "src" }]
[tool.poetry.dependencies]
python = "^3.10"
@ -22,4 +20,3 @@ bookbuild = 'bookbuild.main:app'
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

26
src/bookbuild/all.py Normal file
View File

@ -0,0 +1,26 @@
import typer
from .defaults import LOGGER, Format
from .clean import clean
from .minizine import minizine
from .cover import cover
from .epub import epub
from .pdf import pdf
# instantiate typer app
all_app = typer.Typer()
@all_app.command("build-all")
def build_all():
"""
Build every available target, with and without Tredition specs (where applicable).
"""
LOGGER.debug("Building everything.")
clean()
minizine(format=Format.PDF)
minizine(format=Format.PNG)
cover(all_covers=True)
epub()
epub(tredition=True)
pdf()
pdf(tredition=True)

View File

@ -46,16 +46,7 @@ def cover(
"""
Build only the cover(s).
"""
# make sure input_dir exists and is not empty
input_dir = absolutify(input_dir)
if not input_dir.exists():
LOGGER.error(f"Input directory {input_dir} does not exist.")
return
elif not next(input_dir.glob("*"), None):
LOGGER.error(f"Input directory {input_dir} is empty.")
return
# prepare building all covers
# all_covers requires a specific combination of parameters
if all_covers:
envelope, frontcover, backcover = True, True, True
tredition = True
@ -69,9 +60,10 @@ def cover(
LOGGER.info(f"Building cover(s) as {format.upper()} file(s).")
# get typst executable
typst = EXECUTABLES["typst"]
typst = PROGRAMS["typst"]
# select files
input_dir = Path(input_dir)
if tredition:
paths = input_dir.glob("*tredition*.typ")
else:

View File

@ -6,8 +6,11 @@ from pathlib import Path
from shutil import which
from typing import Annotated
import yaml
from pypdf import PdfWriter
import typer
# start logger
LOGGER = logging.getLogger(__name__)
@ -16,7 +19,6 @@ class Format(str, Enum):
PDF = "pdf"
PNG = "png"
# directory defaults
BASE_DIR = Path.cwd()
CONFIG_DIR = BASE_DIR / "config"
@ -29,17 +31,93 @@ OUTPUT_DIR = BASE_DIR / "output"
LOG_LEVEL = "INFO"
# default executables and options
EXECUTABLES = {
"pandoc": {"executable": "", "options": []},
"typst": {"executable": "", "options": ["compile", "--root", "."]}
# list of shell programs and options
PROGRAMS = {
"pandoc": {"executable": None, "options": []},
"typst": {"executable": None, "options": ["compile", "--root", "."]},
}
OPTIONAL_PROGRAMS = {
"epubcheck": {"executable": None, "options": []}
}
# ##################
# INTERNAL FUNCTIONS
# ##################
def absolutify(path: str) -> Path:
"""
Takes a path in string form and returns a Path object which is either the original path (if it was absolute) or relative to the current working directory.
"""
p = Path(path).expanduser()
return p if p.is_absolute() else BASE_DIR / p
def add_presuffix(path: Path|str, presuffix: str) -> Path:
# cast past to Path
if isinstance(path, str):
path = Path(path)
# add leading dot if nexessary
if presuffix[0] != ".":
presuffix = "." + presuffix
return path.parent / f"{path.stem}{presuffix}{path.suffix}"
def get_data_from_yaml(input_file: str, key: str) -> str|list|dict:
with open(input_file, "r") as file:
data = yaml.load(file, Loader=yaml.Loader)
return data.get(key)
def __get_executable(cmd: str, optional: bool = False) -> str|None:
"""
Returns the path to a given shell command. If the command doesn't exist in path, the function exits unless optional is True, in which case the return value is None.
"""
exe = which(cmd)
if not exe:
if not optional:
LOGGER.critical(f"Required program {cmd} is not installed!")
raise typer.Exit()
else:
LOGGER.warning(f"Optional program {cmd} is not installed!")
return None
return exe
def get_input_files(target_format: str) -> list:
# read filenames from file if such a file exists
list_file = CONFIG_DIR / f"source-files.{target_format}.txt"
if list_file.is_file():
with open(list_file, "r") as file:
return [line.strip() for line in file if line[0] != "#"]
# use all files otherwise; ignore tredition flag
else:
paths = CONTENT_DIR.glob('**/*.md')
return list(map(str, sorted(paths)))
def merge_pdf_with_covers(frontcover: str, content: str, backcover: str, keep_coverless: bool=False):
pdflist = []
# check if files exist
for filename in [frontcover, content, backcover]:
if Path(filename).is_file():
pdflist.append(filename)
# merge files wwith pypdf
merger = PdfWriter()
for pdf in pdflist:
merger.append(pdf)
# write merged file
outfile = str(add_presuffix(content, ".with-covers")) if keep_coverless else content
merger.write(outfile)
merger.close()
def run_command(executable: str, infiles: list, ex_opts: list = [], outfile: str = ""):
"""
Runs a given executable as a shell command using the given options, list of input filenames, and optional output filename.
@ -60,33 +138,13 @@ def run_command(executable: str, infiles: list, ex_opts: list = [], outfile: str
LOGGER.debug(f"run_command(): {executable} finished with return code 0 and this output:\n{stdout.decode('utf-8')}")
def get_executable(cmd: str) -> str:
"""
Returns the path to shell command `cmd` or exits.
"""
exe = which(cmd)
if not exe:
LOGGER.critical(f"{cmd} is not installed!")
typer.Exit(1)
return exe
def absolutify(path: str) -> Path:
"""
Takes a path in string form and returns a Path object which is either the original path (if it was absolute) or relative to the current working directory.
"""
p = Path(path).expanduser()
return p if p.is_absolute() else BASE_DIR / p
# ##############
# TYPER CALLBACK
# ##############
# ##################
# CALLBACK FUNCTIONS
# ##################
# callback function for the main typer app
def callback(
ctx: typer.Context,
def main_callback(
log_level: Annotated[
str,
typer.Option("--log-level", "-l", help="Set log level.")
@ -119,6 +177,25 @@ def callback(
LOGGER.debug(f"output_dir is {OUTPUT_DIR}")
# set paths to executables
for k, v in EXECUTABLES.items():
v["executable"] = get_executable(k)
for k, v in PROGRAMS.items():
v["executable"] = __get_executable(k)
for k, v in OPTIONAL_PROGRAMS.items():
v["executable"] = __get_executable(k)
def input_dir_callback(input_dir: str) -> str:
"""
Validates the argument input_dir which is used by several commands.
"""
input_dir = absolutify(input_dir)
# make sure dir exists
if not input_dir.exists():
raise typer.BadParameter(f"Input directory {input_dir} does not exist.")
# make sure dir isn't empty
if not next(input_dir.glob("*"), None):
raise typer.BadParameter(f"Input directory {input_dir} is empty.")
# OK
return str(input_dir)

74
src/bookbuild/epub.py Normal file
View File

@ -0,0 +1,74 @@
from typing import Annotated
import typer
from .defaults import *
from .cover import cover
# instantiate typer app
epub_app = typer.Typer()
# ##############
# TYPER COMMANDS
# ##############
@epub_app.command()
def epub(
tredition: Annotated[
bool,
typer.Option("--tredition", "-t", help="Build files in accordance with Tredition's requirements for ebook projects. Will produce a main EPUB file without covers and a separate PDF with the cover page.")
] = False,
validate: Annotated[
bool,
typer.Option("--validate", "-V", help="Validate completed EPUB file (requires 'epubcheck' executable in path).")
] = False
):
"""
Build the book's EPUB version (usually a single EPUB file with front cover).
"""
LOGGER.info("Building EPUB.")
# define pandoc defaults file
defaults_file = CONFIG_DIR / "pandoc-defaults.epub.yaml"
if not defaults_file.exists():
LOGGER.critical(f"Defaults file {defaults_file} not found.")
raise typer.Exit()
# define executable and options
pandoc = PROGRAMS["pandoc"]
pandoc["options"].append("--defaults")
pandoc["options"].append(str(defaults_file))
# make frontcover
format = Format.PDF if tredition else Format.PNG
cover(format, tredition=tredition, backcover=False, envelope=False)
# add cover to pandoc options (no-tredition only)
if not tredition:
pandoc["options"].append("--epub-cover-image")
pandoc["options"].append(str(OUTPUT_DIR / "frontcover.png"))
# get input files
input_files = get_input_files("epub")
# get output filename
outfile = get_data_from_yaml(defaults_file, "output-file")
if not outfile:
LOGGER.error("Could not get output filename from defaults file.")
raise typer.Exit()
# modify output filename (tredition only)
if tredition:
outfile = str(add_presuffix(outfile, ".tredition"))
pandoc["options"].append("--output")
pandoc["options"].append(outfile)
# put together command and run it
run_command(pandoc["executable"], ex_opts=pandoc["options"], infiles=input_files)
# validate epub
if validate:
epubcheck = OPTIONAL_PROGRAMS["epubcheck"]
if epubcheck["executable"]:
run_command(epubcheck["executable"], ex_opts=epubcheck["options"], infiles=[outfile])

View File

@ -3,12 +3,12 @@ from pathlib import Path
import typer
from .defaults import callback
from .defaults import main_callback
# instantiate typer app
app = typer.Typer(
no_args_is_help=True,
callback=callback,
callback=main_callback,
context_settings={"help_option_names": ["-h", "--help"]},
help="Build script to make a professionally typeset book from markdown source files that can either be read on screen (PDF or EPUB) or be used in a project with Tredition."
)
@ -20,6 +20,12 @@ from .minizine import minizine_app
app.add_typer(minizine_app)
from .cover import cover_app
app.add_typer(cover_app)
from .epub import epub_app
app.add_typer(epub_app)
from .pdf import pdf_app
app.add_typer(pdf_app)
from .all import all_app
app.add_typer(all_app)
# Make CLI runnable from source tree with python src/package
if not __package__:

View File

@ -22,7 +22,12 @@ def minizine(
] = Format.PDF,
input_dir: Annotated[
str,
typer.Option("--input-dir", "-i", help=f"Specify input directory (either absolute or relative to {str(BASE_DIR)}).")
typer.Option(
"--input-dir",
"-i",
callback=input_dir_callback,
help=f"Specify input directory (either absolute or relative to {str(BASE_DIR)})."
)
] = str(MINIZINE_DIR)
):
"""
@ -30,20 +35,11 @@ def minizine(
"""
LOGGER.info(f"Building minizine as a {format.upper()} file.")
# make sure input_dir exists and is not empty
input_dir = absolutify(input_dir)
if not input_dir.exists():
LOGGER.error(f"Input directory {input_dir} does not exist.")
return
elif not next(input_dir.glob("*"), None):
LOGGER.error(f"Input directory {input_dir} is empty.")
return
# get typst executable
typst = EXECUTABLES["typst"]
typst = PROGRAMS["typst"]
# get input files
input_paths = input_dir.glob("*.typ")
input_paths = Path(input_dir).glob("*.typ")
# run command on each input file
was_empty = True

74
src/bookbuild/pdf.py Normal file
View File

@ -0,0 +1,74 @@
from typing import Annotated
import typer
from .defaults import *
from .cover import cover
# instantiate typer app
pdf_app = typer.Typer()
# ##############
# TYPER COMMANDS
# ##############
@pdf_app.command()
def pdf(
tredition: Annotated[
bool,
typer.Option("--tredition", "-t", help="Build PDFs in accordance with Tredition's requirements for print projects. Will produce a content file without covers and the book's envelope including the spine.")
] = False,
keep_coverless: Annotated[
bool,
typer.Option("--keep-coverless", "-k", help="Do not delete the content-only PDF file after merging it with the covers (does not apply if 'tredition' is set).")
] = False
):
"""
Build the printed book's PDF version (usually a single file containing front cover, content, and back cover).
"""
LOGGER.info("Building PDF.")
# define pandoc defaults file
defaults_file = CONFIG_DIR / f"pandoc-defaults.{Format.PDF}.yaml"
if not defaults_file.exists():
LOGGER.critical(f"Defaults file {defaults_file} not found.")
raise typer.Exit()
# define executable
pandoc = PROGRAMS["pandoc"]
pandoc["options"].append("--defaults")
pandoc["options"].append(str(defaults_file))
# get input files
input_files = get_input_files(Format.PDF.value)
# if tredition flag is set: remove empty first and/or last page
if not tredition:
for i in (0, -1):
if input_files[i].endswith("leere seite.md"):
del input_files[i]
# get output filename (to be used later)
outfile = get_data_from_yaml(str(defaults_file), "output-file")
if not outfile:
LOGGER.error("Could not get output filename from defaults file.")
raise typer.Exit()
# add presuffix ".tredition" to output filename if flag is set
if tredition:
outfile = str(add_presuffix(outfile, ".tredition"))
pandoc["options"].append("--output")
pandoc["options"].append(outfile)
# make PDf
run_command(pandoc["executable"], ex_opts=pandoc["options"], infiles=input_files)
# make PDF covers
cover(tredition=tredition, format=Format.PDF, frontcover=not tredition, backcover=not tredition, envelope=tredition)
# add front-/backcover to PDF unless tredition flag is set
if not tredition:
merge_pdf_with_covers(str(OUTPUT_DIR / "frontcover.pdf"), outfile, str(OUTPUT_DIR / "backcover.pdf"), keep_coverless)
# check metadata -> try veraPDF