Repository navigation
Upgrade create_pdf action with themes, visual layout, and unicode safety - #286
Conversation
AlanAAG
commented
May 27, 2026
- Replace deprecated FPDF/HTMLMixin pattern with direct write_html on FPDF 2.8
- Add gradient banner header rendered from the first H1 heading
- Add 5 colour themes: default, corporate, minimal, warm, forest
- Add tag_styles for h1-h5 with colour hierarchy, code blocks with background fill, and page number footer
- Add unicode sanitizer (_CHAR_MAP) to prevent latin-1 crashes on em dashes, smart quotes, currency symbols, and other common characters
- Add optional subtitle and page_numbers input fields
- Return pages and size_bytes in success payload for output verification
- Add parallelizable=False to prevent concurrent file write corruption
- Remove FPDF from requirement list (was silently skipped by executor heuristic)
- Add tiered error handling: PermissionError, OSError, and generic Exception with actionable messages instead of a single bare except
- Auto-create missing parent directories instead of crashing
- Validate .pdf extension upfront
- Replace deprecated FPDF/HTMLMixin pattern with direct write_html on FPDF 2.8 - Add gradient banner header rendered from the first H1 heading - Add 5 colour themes: default, corporate, minimal, warm, forest - Add tag_styles for h1-h5 with colour hierarchy, code blocks with background fill, and page number footer - Add unicode sanitizer (_CHAR_MAP) to prevent latin-1 crashes on em dashes, smart quotes, currency symbols, and other common characters - Add optional subtitle and page_numbers input fields - Return pages and size_bytes in success payload for output verification - Add parallelizable=False to prevent concurrent file write corruption - Remove FPDF from requirement list (was silently skipped by executor heuristic) - Add tiered error handling: PermissionError, OSError, and generic Exception with actionable messages instead of a single bare except - Auto-create missing parent directories instead of crashing - Validate .pdf extension upfront
| ops_done += 1 | ||
|
|
||
| # ── fill_field (AcroForm via pypdf) ─────────────────────── | ||
| elif op_type == "fill_field": |
There was a problem hiding this comment.
fill_field ops are silently skipped in the main loop and lose per-op accounting on post-process failure
In the main operation loop here, fill_field is a no-op:
elif op_type == "fill_field":
# Defer all fill_field ops to after PyMuPDF saves
# (pypdf needs to open the saved file)
# We flag these for post-processing below
pass # handled in post-processing stepTwo problems with this:
-
No
ops_doneincrement here.ops_doneonly gets bumped inside the post-processing loop , so if afill_fieldop succeeds, the count is right - but if the outer post-processtryfails wholesale, everyfill_fieldop in the batch is dropped without per-op accounting:except Exception as e: warnings.append(f"AcroForm fill failed: {type(exc).__name__}: {e}.")
The caller sees one aggregate
"AcroForm fill failed: PdfReadError: …"warning and has no idea which fields were attempted, which succeeded before the failure, or which were skipped. -
No early validation in the main loop. A user passing a
fill_fieldop withoutfield_namewon't be told about it until post-processing - and if post-processing crashes before reaching that op, the missing-field warning is never emitted at all.
Suggested fix: validate up-front in the main loop, then do per-op try/except inside the post-process loop so each failed field gets its own warning.
# In the main loop — fail fast on shape problems before we even reach pypdf
elif op_type == "fill_field":
if not str(op.get("field_name", "")).strip():
warnings.append(f"{op_tag}: 'field_name' is required.")
# actual fill is deferred — see post-processing block below
# In the post-processing block, replace the outer try/except with per-op:
if acroform_ops:
try:
reader = pypdf.PdfReader(abs_output)
writer = pypdf.PdfWriter()
writer.append(reader)
existing_fields = reader.get_fields() or {}
except Exception as e:
for j, _ in enumerate(acroform_ops):
warnings.append(f"op[fill_field {j}]: could not open PDF for AcroForm: {type(e).__name__}: {e}.")
else:
for j, op in enumerate(acroform_ops):
op_tag = f"op[fill_field {j}]"
field_name = str(op.get("field_name", "")).strip()
value = str(op.get("value", ""))
if not field_name:
continue # already warned in main loop
if field_name not in existing_fields:
warnings.append(
f"{op_tag}: field '{field_name}' not found in AcroForm. "
f"Available: {list(existing_fields.keys())[:10]}."
)
continue
try:
for page_obj in writer.pages:
writer.update_page_form_field_values(page_obj, {field_name: value})
ops_done += 1
except Exception as e:
warnings.append(f"{op_tag}: {type(e).__name__}: {e}.")
try:
with open(abs_output, "wb") as f:
writer.write(f)
except Exception as e:
warnings.append(f"AcroForm write failed: {type(e).__name__}: {e}.")Now the caller can distinguish:
-
"couldn't open the PDF at all" (all ops failed with the same upstream cause)
-
"field X wasn't in the form" (that specific op, ops continue)
-
"writing the result to disk failed" (separate from the per-field operations)
-
versus the current behaviour where any of those three failure modes collapses to one opaque aggregate warning.
ahmad-ajmal
left a comment
There was a problem hiding this comment.
This is a small quality of life change - other than that the PR is perfect.