From 243347868022574890eb5a6f5dc09182d377ce59 Mon Sep 17 00:00:00 2001 From: Rob Date: Thu, 16 Apr 2026 16:18:59 +0200 Subject: [PATCH] In PDF Javascript gespeicherte Feldoptionen verwenden MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Kosten-pro-Tag-Berechnung und die Drop-Down-Optionen im Anwesenheitsfeld werden per Javsacript gemacht. JS funktioniert aber in pyumpdf nicht, darum werden die korrekten Optionen aus dem JS ausgelesen und dieses 1:1 in den Feldern gesetzt. Mittels Schalter in der config können auch wieder die Rohwerte aus dem Excel gesetzt werden. (PDF muss dann von Hand geöffnet und gespeichert werden damit die entgültigen passenden Werte enthalten sind). --- config.json | 3 +- schema.json | 5 +++ src/pdf_filler.py | 66 ++++++++++++++++++++++++++++++++++++++++ tests/test_regression.py | 8 ++--- 4 files changed, 77 insertions(+), 5 deletions(-) diff --git a/config.json b/config.json index 9f5c1e2..ac84ca1 100644 --- a/config.json +++ b/config.json @@ -29,5 +29,6 @@ "pdf_field_ort": "unterschrift.ort", "pdf_field_erstellungsdatum": "unterschrift.datum", "pdf_pauschale_prefix": "pauschale_5", - "pauschale_present_value": "1" + "pauschale_present_value": "1", + "pdf_use_display_values": true } diff --git a/schema.json b/schema.json index da5a5f8..e84b79f 100644 --- a/schema.json +++ b/schema.json @@ -80,6 +80,11 @@ "pauschale_value": { "type": "string", "description": "Wert für das pauschale.N Feld wenn anwesend" + }, + "pdf_use_display_values": { + "type": "boolean", + "description": "true = Dropdown-Anzeigewerte ins PDF schreiben (z.B. 'X = anwesend'); false = rohe Export-Werte (z.B. 'X')", + "default": true } } } diff --git a/src/pdf_filler.py b/src/pdf_filler.py index 2ca82da..17accc1 100644 --- a/src/pdf_filler.py +++ b/src/pdf_filler.py @@ -91,6 +91,51 @@ def build_field_dict( return fields +def _load_opt_mappings(reader: PdfReader) -> Dict[str, Dict[str, str]]: + """Return export→display maps for every choice field in the template. + + For a field named ``anwesend.0`` with /Opt ``[["X", "X = anwesend"], ...]`` + the result contains ``{"anwesend": {"X": "X = anwesend", ...}}``. + Multiple indexed fields (e.g. ``anwesend.0``…``anwesend.22``) share the + same base name and therefore the same mapping. + """ + opt_maps: Dict[str, Dict[str, str]] = {} + fields = reader.get_fields() or {} + for name, field in fields.items(): + if str(field.get("/FT", "")) != "/Ch": + continue + base = name.split(".")[0] + if base in opt_maps: + continue # already recorded from an earlier indexed copy + raw_opts = field.get("/Opt", []) + mapping: Dict[str, str] = {} + for opt in raw_opts: + if isinstance(opt, list) and len(opt) == 2: + mapping[str(opt[0])] = str(opt[1]) + else: + s = str(opt) + mapping[s] = s + if mapping: + opt_maps[base] = mapping + return opt_maps + + +def _apply_opt_mappings( + fields: Dict[str, str], opt_maps: Dict[str, Dict[str, str]] +) -> None: + """Replace export values with display values in *fields* in-place. + + Iterates over every field whose base name exists in *opt_maps* and + substitutes the stored export value for its display counterpart. + Fields whose value is not found in the map are left unchanged. + """ + for field_name, value in list(fields.items()): + base = field_name.split(".")[0] + mapping = opt_maps.get(base) + if mapping and value in mapping: + fields[field_name] = mapping[value] + + def _update_widget_visibility(writer: PdfWriter, fields: Dict[str, str]) -> None: """Un-hide widget annotations for ist.N and plan.N fields that have a value. @@ -175,6 +220,27 @@ def fill_pdf( f"PDF-Template konnte nicht gelesen werden: {exc}" ) from exc + # Optionally replace export values with display values for choice fields. + # Controlled by the config key ``pdf_use_display_values`` (default: True). + # When True, pymupdf/viewers render e.g. "X = anwesend" instead of "X". + if config.get("pdf_use_display_values", True): + opt_maps = _load_opt_mappings(reader) + + # Prefill every choice field that our data logic didn't touch so that + # its template-default export value also gets mapped to a display value. + # This covers e.g. pauschale_25.N (never set by us, always "0" in template) + # and anwesend.N / pauschale_5.N for rows beyond the data entries. + for field_name, field in (reader.get_fields() or {}).items(): + if field_name in fields: + continue + if str(field.get("/FT", "")) != "/Ch": + continue + export_val = str(field.get("/V", "")) + if export_val: + fields[field_name] = export_val + + _apply_opt_mappings(fields, opt_maps) + writer = PdfWriter() writer.clone_document_from_reader(reader) diff --git a/tests/test_regression.py b/tests/test_regression.py index 27491d1..5f408fc 100644 --- a/tests/test_regression.py +++ b/tests/test_regression.py @@ -15,12 +15,12 @@ A fixture is discovered automatically when: Rendering --------- -Both reference and generated PDFs are rendered to images using pymupdf -(no browser required). The images are then compared pixel-by-pixel. +Both reference and generated PDFs are rendered to images using pymupdf. +The images are then compared pixel-by-pixel. Similarity threshold -------------------- -SIMILARITY_THRESHOLD (default 95 %) — fraction of pixels whose R, G, B channels +SIMILARITY_THRESHOLD (100%) — fraction of pixels whose R, G, B channels each differ by at most PIXEL_TOLERANCE (default 5) between reference and generated PDF. Diff images @@ -47,7 +47,7 @@ from PIL import Image # Constants # --------------------------------------------------------------------------- FIXTURES_DIR = Path(__file__).parent / "fixtures" -SIMILARITY_THRESHOLD = 0.95 # minimum fraction of matching pixels per page +SIMILARITY_THRESHOLD = 1.00 # minimum fraction of matching pixels per page PIXEL_TOLERANCE = 5 # max per-channel delta still counted as "same" RENDER_DPI = 150 # rendering resolution for PDF → image