From 64cf32d43c911e63ed7f869c24ec84e40200d983 Mon Sep 17 00:00:00 2001 From: eclipse Date: Tue, 28 Oct 2025 22:40:22 +0100 Subject: [PATCH] added missing subcommands (pdf, epub, build-all); upped version to 2.0 --- .gitignore | 1 + build.py | 414 -------------------------------------- pyproject.toml | 7 +- src/bookbuild/all.py | 26 +++ src/bookbuild/cover.py | 14 +- src/bookbuild/defaults.py | 139 ++++++++++--- src/bookbuild/epub.py | 74 +++++++ src/bookbuild/main.py | 10 +- src/bookbuild/minizine.py | 20 +- src/bookbuild/pdf.py | 74 +++++++ 10 files changed, 304 insertions(+), 475 deletions(-) delete mode 100755 build.py create mode 100644 src/bookbuild/all.py create mode 100644 src/bookbuild/epub.py create mode 100644 src/bookbuild/pdf.py diff --git a/.gitignore b/.gitignore index 82e4936..04ec0c5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ output/ .venv/ __pycache__/ +dist/ diff --git a/build.py b/build.py deleted file mode 100755 index 0dd402f..0000000 --- a/build.py +++ /dev/null @@ -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() - diff --git a/pyproject.toml b/pyproject.toml index 8291955..c674fce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 "] 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" - diff --git a/src/bookbuild/all.py b/src/bookbuild/all.py new file mode 100644 index 0000000..95bf6e4 --- /dev/null +++ b/src/bookbuild/all.py @@ -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) diff --git a/src/bookbuild/cover.py b/src/bookbuild/cover.py index 4683adc..6ae2db3 100644 --- a/src/bookbuild/cover.py +++ b/src/bookbuild/cover.py @@ -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: diff --git a/src/bookbuild/defaults.py b/src/bookbuild/defaults.py index 6ff8c7f..c725857 100644 --- a/src/bookbuild/defaults.py +++ b/src/bookbuild/defaults.py @@ -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) \ No newline at end of file diff --git a/src/bookbuild/epub.py b/src/bookbuild/epub.py new file mode 100644 index 0000000..01e88ae --- /dev/null +++ b/src/bookbuild/epub.py @@ -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]) diff --git a/src/bookbuild/main.py b/src/bookbuild/main.py index baf9219..dae4db9 100644 --- a/src/bookbuild/main.py +++ b/src/bookbuild/main.py @@ -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__: diff --git a/src/bookbuild/minizine.py b/src/bookbuild/minizine.py index e4c68f1..aa5f7c7 100644 --- a/src/bookbuild/minizine.py +++ b/src/bookbuild/minizine.py @@ -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 diff --git a/src/bookbuild/pdf.py b/src/bookbuild/pdf.py new file mode 100644 index 0000000..11d4259 --- /dev/null +++ b/src/bookbuild/pdf.py @@ -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