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