From 217d1ee09b72b6423b0cebbfec7bf78153bbaf06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dieter=20Schl=C3=BCter?= Date: Fri, 2 Oct 2026 13:51:50 +0200 Subject: [PATCH] =?UTF-8?q?Weiche=20Trennstriche=20aufl=C3=B6sen,=20Courie?= =?UTF-8?q?r-OCR-Ebenen=20nicht=20als=20Code=20setzen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Zeilenende mit U+00AD oder "¬" wird wie Silbentrennung behandelt ("wach­ senden" -> "wachsenden") - Unsichtbarer Text (OCR-Ebene) verliert das Monospace-Flag, sonst setzt pymupdf4llm jedes Wort in Backticks - Tests für beide Fälle und für sichtbaren Monospace-Text Co-Authored-By: Claude Opus 5.5 --- pdf2md.py | 25 ++++++++++++++++++------- tests/test_normalize.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/pdf2md.py b/pdf2md.py index 2a1aef6..29bd0ee 100644 --- a/pdf2md.py +++ b/pdf2md.py @@ -472,28 +472,39 @@ def load_outline(doc: pymupdf.Document) -> dict[str, int]: # =========================================================================== -# Korrektur für pymupdf4llm: Bindestrich-Komposita am Zeilenende +# Korrektur für pymupdf4llm: Trennstriche am Zeilenende, OCR-Ebenen # =========================================================================== # pymupdf4llm entfernt jeden Bindestrich am Zeilenende ("Militär-" + Zeile # "Geheimdienst" -> "MilitärGeheimdienst"). Für Großbuchstaben und # Konjunktionen ("Arbeits- und") markieren wir den Strich vorher, sodass er # erhalten bleibt. Echte Silbentrennung (Kleinbuchstabe) bleibt unverändert. +# Weiche Trennstriche (U+00AD) und "¬" kennt pymupdf4llm nicht ("wach­ senden"); +# sie trennen immer innerhalb eines Wortes und werden zu "-", das dann entfällt. +# +# Unsichtbarer Text (OCR-Ebene eines Scans) ist oft in Courier gesetzt; +# pymupdf4llm hielte ihn sonst für Programmcode und setzte jedes Wort in `…`. KEEP_HYPHEN = "\ue000" # Strich behalten, ohne Leerzeichen anschließen KEEP_HYPHEN_SPACE = "\ue001" # Strich und Leerzeichen behalten +SOFT_HYPHENS = ("\u00ad", "¬") +MONOSPACE_FLAG = 8 + + def _mark_line_end_hyphens(spans: list[dict]) -> list[dict]: marked = [] for i, span in enumerate(spans): nxt = spans[i + 1] if i + 1 < len(spans) else None + if not span.get("char_flags", VISIBLE_TEXT) & VISIBLE_TEXT: + span = {**span, "flags": span.get("flags", 0) & ~MONOSPACE_FLAG} text = span["text"].rstrip() - if ( - nxt is not None - and text.endswith("-") - and (span.get("block"), span.get("line")) - != (nxt.get("block"), nxt.get("line")) - ): + line_end = nxt is not None and (span.get("block"), span.get("line")) != ( + nxt.get("block"), nxt.get("line") + ) + if line_end and text.endswith(SOFT_HYPHENS): + span = {**span, "text": text[:-1] + "-"} + elif line_end and text.endswith("-"): word = FIRST_WORD.match(nxt["text"].lstrip()) if word and word.group(0).lower() in CONJUNCTIONS: span = {**span, "text": text + KEEP_HYPHEN_SPACE} diff --git a/tests/test_normalize.py b/tests/test_normalize.py index 0098d45..747bf12 100644 --- a/tests/test_normalize.py +++ b/tests/test_normalize.py @@ -74,6 +74,37 @@ def test_pymupdf4llm_patch_keeps_compound_hyphen(): assert output.strip() == "Der Militär-Geheimdienst wusste es. Arbeits- und Kriegergeschrei." +def test_pymupdf4llm_patch_resolves_soft_hyphens(): + # Weicher Trennstrich (U+00AD) bzw. "¬" am Zeilenende, auch vor Großbuchstaben + spans = [ + {"text": "einer wach­", "flags": 0, "char_flags": 16, "font": "x", "block": 0, "line": 0}, + {"text": "senden Demo¬", "flags": 0, "char_flags": 16, "font": "x", "block": 0, "line": 1}, + {"text": "kratie und Lebens­ ", "flags": 0, "char_flags": 16, "font": "x", "block": 0, "line": 2}, + {"text": "Läufe.", "flags": 0, "char_flags": 16, "font": "x", "block": 0, "line": 3}, + ] + output, _ = p.document_layout.get_styled_text(spans) + assert output.strip() == "einer wachsenden Demokratie und LebensLäufe." + + +def test_pymupdf4llm_patch_ignores_monospace_of_invisible_text(): + # OCR-Ebene in Courier: unsichtbar, aber mit Monospace-Flag + spans = [ + {"text": "Nature reioicej", "flags": 8, "char_flags": 0, "alpha": 0, "font": "Courier", + "block": 0, "line": 0}, + {"text": "of many frendes", "flags": 8, "char_flags": 0, "alpha": 0, "font": "Courier", + "block": 0, "line": 1}, + ] + output, _ = p.document_layout.get_styled_text(spans) + assert output.strip() == "Nature reioicej of many frendes" + + +def test_visible_monospace_stays_code(): + span = {"text": "print()", "flags": 8, "char_flags": int(p.VISIBLE_TEXT), "font": "Courier", + "block": 0, "line": 0} + output, _ = p.document_layout.get_styled_text([span]) + assert output.strip() == "`print()`" + + # --- Überschriften ----------------------------------------------------------------- def test_heading_without_space_and_bold_is_cleaned():