Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch pdfrw to pypdf #169

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
92 changes: 30 additions & 62 deletions dungeonsheets/fill_pdf_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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):
Expand Down
Binary file modified dungeonsheets/forms/blank-personality-sheet-default.pdf
Binary file not shown.
12 changes: 10 additions & 2 deletions dungeonsheets/make_sheets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
1 change: 0 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading