Weiche Trennstriche auflösen, Courier-OCR-Ebenen nicht als Code setzen
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
a21d900e6b
commit
217d1ee09b
2 changed files with 49 additions and 7 deletions
25
pdf2md.py
25
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
|
# pymupdf4llm entfernt jeden Bindestrich am Zeilenende ("Militär-" + Zeile
|
||||||
# "Geheimdienst" -> "MilitärGeheimdienst"). Für Großbuchstaben und
|
# "Geheimdienst" -> "MilitärGeheimdienst"). Für Großbuchstaben und
|
||||||
# Konjunktionen ("Arbeits- und") markieren wir den Strich vorher, sodass er
|
# Konjunktionen ("Arbeits- und") markieren wir den Strich vorher, sodass er
|
||||||
# erhalten bleibt. Echte Silbentrennung (Kleinbuchstabe) bleibt unverändert.
|
# 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 = "\ue000" # Strich behalten, ohne Leerzeichen anschließen
|
||||||
KEEP_HYPHEN_SPACE = "\ue001" # Strich und Leerzeichen behalten
|
KEEP_HYPHEN_SPACE = "\ue001" # Strich und Leerzeichen behalten
|
||||||
|
|
||||||
|
|
||||||
|
SOFT_HYPHENS = ("\u00ad", "¬")
|
||||||
|
MONOSPACE_FLAG = 8
|
||||||
|
|
||||||
|
|
||||||
def _mark_line_end_hyphens(spans: list[dict]) -> list[dict]:
|
def _mark_line_end_hyphens(spans: list[dict]) -> list[dict]:
|
||||||
marked = []
|
marked = []
|
||||||
for i, span in enumerate(spans):
|
for i, span in enumerate(spans):
|
||||||
nxt = spans[i + 1] if i + 1 < len(spans) else None
|
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()
|
text = span["text"].rstrip()
|
||||||
if (
|
line_end = nxt is not None and (span.get("block"), span.get("line")) != (
|
||||||
nxt is not None
|
nxt.get("block"), nxt.get("line")
|
||||||
and text.endswith("-")
|
)
|
||||||
and (span.get("block"), span.get("line"))
|
if line_end and text.endswith(SOFT_HYPHENS):
|
||||||
!= (nxt.get("block"), nxt.get("line"))
|
span = {**span, "text": text[:-1] + "-"}
|
||||||
):
|
elif line_end and text.endswith("-"):
|
||||||
word = FIRST_WORD.match(nxt["text"].lstrip())
|
word = FIRST_WORD.match(nxt["text"].lstrip())
|
||||||
if word and word.group(0).lower() in CONJUNCTIONS:
|
if word and word.group(0).lower() in CONJUNCTIONS:
|
||||||
span = {**span, "text": text + KEEP_HYPHEN_SPACE}
|
span = {**span, "text": text + KEEP_HYPHEN_SPACE}
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,37 @@ def test_pymupdf4llm_patch_keeps_compound_hyphen():
|
||||||
assert output.strip() == "Der Militär-Geheimdienst wusste es. Arbeits- und Kriegergeschrei."
|
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 -----------------------------------------------------------------
|
# --- Überschriften -----------------------------------------------------------------
|
||||||
|
|
||||||
def test_heading_without_space_and_bold_is_cleaned():
|
def test_heading_without_space_and_bold_is_cleaned():
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue