diff --git a/README.rst b/README.rst index 6307e253..d81deb07 100644 --- a/README.rst +++ b/README.rst @@ -64,12 +64,8 @@ Optional External dependencies generate the PDF spell pages (optional). If **pdftk** is available, it will be used for pdf generation. If not, -a fallback python library (pdfrw) will be used. This has some -limitations: - -- Produces v1.3 PDF files -- Not able to flatten PDF forms -- Will produce separate character-sheets, spell-lists and spell-books. +a fallback python library (pypdf) will be used. This has the +limitation that it is not able to flatten PDF forms. Different linux distributions have different names for packages. While pdftk is available in Debian and derivatives as **pdftk**, the package diff --git a/dungeonsheets/fill_pdf_template.py b/dungeonsheets/fill_pdf_template.py index 23be8966..bcd88fe5 100644 --- a/dungeonsheets/fill_pdf_template.py +++ b/dungeonsheets/fill_pdf_template.py @@ -3,8 +3,8 @@ import subprocess import warnings -import pdfrw from fdfgen import forge_fdf +from pypdf import PdfWriter, PdfReader from dungeonsheets.forms import mod_str @@ -405,7 +405,7 @@ def make_pdf(fields: dict, src_pdf: str, basename: str, flatten: bool = False): src_pdf : Path to the PDF that will serve as the template. basename : - The path of the destination PDF without the file extensions. The + The basename of the destination PDF without the file extensions. The resulting pdf will be {basename}.pdf flatten : If truthy, the PDF will be collapsed so it is no longer @@ -415,71 +415,39 @@ def make_pdf(fields: dict, src_pdf: str, basename: str, flatten: bool = False): try: _make_pdf_pdftk(fields, src_pdf, basename, flatten) except FileNotFoundError: - # pdftk could not run, so alert the user and use pdfrw + # pdftk could not run, so alert the user and use pypdf warnings.warn( f"Could not run `{PDFTK_CMD}`, using fallback; forcing `--editable`.", RuntimeWarning, ) - _make_pdf_pdfrw(fields, src_pdf, basename, flatten) - - -def _make_pdf_pdfrw(fields: dict, src_pdf: str, basename: str, flatten: bool = False): - """Backup make_pdf function in case pdftk is not available.""" - template = pdfrw.PdfReader(src_pdf) - # Different types of PDF fields - BUTTON = "/Btn" - # Names for entries in PDF annotation list - # DEFAULT_VALUE = "/DV" - # APPEARANCE = "/MK" - FIELD = "/T" - # PROPS = "/P" - TYPE = "/FT" - # FLAGS = "/Ff" - # SUBTYPE = "/Subtype" - # ALL_KEYS = [ - # "/DV", - # "/F", - # "/FT", - # "/Ff", - # "/MK", - # "/P", - # "/Rect", - # "/Subtype", - # "/T", - # "/Type", - # ] - annots = template.pages[0]["/Annots"] - # Update each annotation if it's in the requested dictionary - for annot in annots: - this_field = annot[FIELD][1:-1] - # Check if the field has a new value passed - if this_field in fields.keys(): - val = fields[this_field] - # Convert integers to strings - if isinstance(val, int): - val = str(val) - log.debug( - f"Set field '{this_field}' " - f"({annot[TYPE]}) " - f"to `{val}` ({val.__class__}) " - f"in file '{basename}.pdf'" + _make_pdf_pypdf(fields, src_pdf, basename, flatten=flatten) + + +def _make_pdf_pypdf(fields: dict, src_pdf: str, basename: str, flatten: bool = False): + """ + Writes the dictionary values to the pdf. Supports text and checkboxes. + Does so by updating each individual annotation with the contents of the fiels. + + """ + + writer = PdfWriter() + reader = PdfReader(src_pdf) + form_fields = reader.get_fields() + writer.append(reader) + + for key in fields.keys(): + if key in form_fields: + if fields[key] == "Yes": + fields[key] = r"/Yes" + if fields[key] == "Off": + fields[key] = r"/Off" + writer.update_page_form_field_values( + writer.pages[0], {key: fields[key]}, + auto_regenerate=False, ) - # Prepare a PDF dictionary based on the fields properties - if annot[TYPE] == BUTTON: - # Radio buttons require special appearance streams - if val == CHECKBOX_ON: - val = bytes(val, "utf-8") - pdf_dict = pdfrw.PdfDict(V=val, AS=val) - else: - continue - else: - # All other widget types - pdf_dict = pdfrw.PdfDict(V=val) - annot.update(pdf_dict) - else: - log.debug(f"Skipping unused field '{this_field}' in file '{basename}.pdf'") - # Now write the PDF to the new pdf file - pdfrw.PdfWriter().write(f"{basename}.pdf", template) + + with open(f"{basename}.pdf", "wb") as output_stream: + writer.write(output_stream) def _make_pdf_pdftk(fields, src_pdf, basename, flatten=False): diff --git a/dungeonsheets/forms/blank-personality-sheet-default.pdf b/dungeonsheets/forms/blank-personality-sheet-default.pdf index 57c94818..1e8e6c36 100644 Binary files a/dungeonsheets/forms/blank-personality-sheet-default.pdf and b/dungeonsheets/forms/blank-personality-sheet-default.pdf differ diff --git a/dungeonsheets/make_sheets.py b/dungeonsheets/make_sheets.py index b7e2f844..d4b4a98a 100644 --- a/dungeonsheets/make_sheets.py +++ b/dungeonsheets/make_sheets.py @@ -12,6 +12,7 @@ from itertools import product from multiprocessing import Pool, cpu_count from pathlib import Path +from pypdf import PdfReader, PdfWriter from typing import Union, Sequence, Optional, List from dungeonsheets import ( @@ -666,9 +667,16 @@ def merge_pdfs(src_filenames, dest_filename, clean_up=False): subprocess.call(popenargs) except FileNotFoundError: warnings.warn( - f"Could not run `{PDFTK_CMD}`; skipping file concatenation.", RuntimeWarning + f"Could not run `{PDFTK_CMD}`, using fallback.", RuntimeWarning ) - else: + + merger = PdfWriter() + for pdf in src_filenames: + merger.append(pdf) + merger.set_need_appearances_writer(True) + merger.write(dest_filename) + merger.close() + finally: # Remove temporary files if clean_up: for sheet in src_filenames: diff --git a/pyproject.toml b/pyproject.toml index 6e4710b4..fdad426c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ "Topic :: Games/Entertainment :: Role-Playing", ] keywords = ["D&D", "character", "sheets"] -dependencies = ["certifi", "fdfgen", "npyscreen", "jinja2", "sphinx", "pdfrw", "EbookLib", "reportlab", "docutils", "pypdf"] +dependencies = ["certifi", "fdfgen", "npyscreen", "jinja2", "sphinx", "EbookLib", "reportlab", "docutils", "pypdf"] [project.optional-dependencies] dev = ["pytest", "pytest-cov", "coverall", "flake8", "black"] diff --git a/requirements.txt b/requirements.txt index 1c9a6135..0fe13b05 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,6 @@ fdfgen>=0.16 npyscreen~=4.10.5 jinja2~=3.1.2 sphinx~=7.2.6 -pdfrw~=0.4 EbookLib~=0.18 reportlab~=4.0.8 setuptools~=69.0.3