diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md index f87b9a7..b4d25c5 100644 --- a/BEDIENUNGSANLEITUNG.md +++ b/BEDIENUNGSANLEITUNG.md @@ -167,6 +167,7 @@ python3 pdf2md.py --epub scan.pdf Output/scan.md | Deutsch und Englisch gemischt | `--ocr-language deu+eng` (Sprachpaket `tesseract-ocr-eng` nötig) | | schneller auf vielen Kernen | `--jobs 8` (Standard 4) | | OCR erzwingen, auch für vorhandene OCR-Ebenen | `--ocr always` | +| englisches Buch | `--ocr-language eng --lang en` (Sprachpaket `tesseract-ocr-eng` nötig) | | keine OCR | `--ocr never` oder `--no-ocr` | So entscheidet das Programm von selbst (`--ocr auto`): @@ -175,7 +176,9 @@ So entscheidet das Programm von selbst (`--ocr auto`): also ca. 5 Minuten für 300 Seiten. - **Scans mit vorhandener OCR-Ebene** (z. B. von `ocrmypdf`): Eine gerade Ebene wird übernommen, das geht sehr schnell. Eine schiefe Ebene (Scan nicht entzerrt) wird neu erkannt, - weil ihre Lesereihenfolge oft falsch ist. + weil ihre Lesereihenfolge oft falsch ist. Alte OCR-Ebenen (z. B. aus Digitalisaten älterer + Bücher) sind oft schlecht: Kolumnentitel werden zu Überschriften, Buchstaben sind falsch + erkannt. Dann mit `--ocr always` neu erkennen lassen. - **Reine Bildseiten in einem sonst digitalen PDF** (Fotos, Cover) werden nur übernommen, wenn eine Probe-OCR echten Fließtext liefert. Sonst bleiben sie Bild, und das Programm meldet einen „Hinweis“. @@ -213,7 +216,9 @@ Kolumnentitel und Seitenzahlen gedruckter Bücher werden automatisch entfernt, g grep -n "^#" Output/band1.md ``` Erwartet: `## Teil …` und darunter `### 1 …`, `### 2 …` usw. Ganze Sätze als Überschrift - (endend mit `.` oder `«`) sind Erkennungsfehler, die du in Weg B korrigierst. + (endend mit `.` oder `«`) sind Erkennungsfehler, die du in Weg B korrigierst. Die Ebenen + kommen aus der Gliederung (Lesezeichen) der PDF; ist sie flach, stehen alle Kapitel auf + einer Ebene. 2. **Stichproben** am Anfang, in der Mitte und am Ende des Markdowns mit der PDF vergleichen: Fehlen Absätze, sind Absätze zusammengeklebt, stehen Bilder an der richtigen Stelle? 3. **EPUB auf dem PocketBook testen:** Inhaltsverzeichnis, Kapitelsprünge, Bilder, Cover. @@ -231,6 +236,11 @@ Kolumnentitel und Seitenzahlen gedruckter Bücher werden automatisch entfernt, g | Bilder fehlen im EPUB (Weg B) | pandoc im Ordner der `.md` starten (`cd Output`) bzw. `--resource-path` auf diesen Ordner setzen; der `_images`-Ordner muss daneben liegen | | Gescannter Text fehlt, Meldung „Hinweis: … Bildseite(n)“ | die Seite wurde für ein Foto gehalten → `--ocr always` | | Satz als Überschrift oder Kapitel ohne Überschrift | in Weg B von Hand korrigieren (Abschnitt 3) | +| Überschrift besteht nur aus einer Zahl (`## 90`, `## XX`) | meist Fußnotenzeichen oder Seitenzahl → in Weg B löschen | +| Buchstaben fehlen („Offzielle“, „treff ch“) | Ligatur (fi, fl) ohne Textzuordnung in der PDF; von Hand korrigieren | +| Überschrift gesperrt („M e i n k l e i nes Stück“) | Buchstaben einzeln gesetzt in der PDF; von Hand korrigieren | +| Formeln erscheinen als Bilder | bei Mathebüchern normal; dafür ist das Programm nicht gebaut | +| Alter Scan: falsche Überschriften, viele Lesefehler | `--ocr always` (vorhandene OCR-Ebene ignorieren) | | Auf Scans fehlen Leerzeichen („deutetin“) | OCR-Fehler; höhere Auflösung versuchen (`--ocr-dpi 400`) oder von Hand korrigieren | | EPUB zu groß | `--image-format jpg` (Standard) verwenden; PNG ist ca. 3× größer | | Meine Änderungen in der `.md` sind weg | Schritt 1 wurde erneut ausgeführt → vorher Kopie anlegen | diff --git a/README.md b/README.md index 1e5eb3d..175584a 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,13 @@ # ljutsch – PDF → Markdown → EPUB -Werkzeug, um PDF-Bücher (hier: *Operation Ljutsch*, Band 1 und 2) in sauberes Markdown -und anschließend in ein EPUB für den PocketBook-Reader umzuwandeln – mit Überschriften, -Bildern und echten Absätzen. Funktioniert für **digitale** PDFs (Textebene vorhanden) -ebenso wie für **gescannte** PDFs (nur Bilder, per OCR) und für Scans, die bereits eine -OCR-Ebene haben (z. B. von `ocrmypdf`). +Werkzeug, um PDF-Bücher in sauberes Markdown und anschließend in ein EPUB für den +PocketBook-Reader umzuwandeln – mit Überschriften, Bildern und echten Absätzen. Funktioniert +für **digitale** PDFs (Textebene vorhanden) ebenso wie für **gescannte** PDFs (nur Bilder, +per OCR) und für Scans, die bereits eine OCR-Ebene haben (z. B. von `ocrmypdf`). + +Entwickelt an *Operation Ljutsch* (Band 1 und 2), außerdem erprobt an einem deutschen +Sachbuch, einem englischen Handbuch, einem Mathematikbuch, einem Bericht mit Fußnoten und +einem englischen Scan von 1893 (Ergebnisse siehe [Grenzen](#grenzen)). ## Installation (Ubuntu 24.04) @@ -29,6 +32,9 @@ python3 pdf2md.py --epub buch.pdf buch.md # Scan in Deutsch und Englisch, 8 parallele OCR-Prozesse python3 pdf2md.py --ocr-language deu+eng --jobs 8 scan.pdf scan.md + +# englisches Buch; alter Scan mit schlechter OCR-Ebene neu erkennen +python3 pdf2md.py --epub --lang en --ocr-language eng --ocr always scan.pdf scan.md ``` | Option | Bedeutung | @@ -49,10 +55,13 @@ python3 pdf2md.py --ocr-language deu+eng --jobs 8 scan.pdf scan.md (`.` `!` `?` `…`, auch mit `«` `“` `»` usw. dahinter). Auf Scans werden Absätze am Einzug erkannt. - **Silbentrennung:** `Geheim-|dienst` → `Geheimdienst`, `Militär-|Geheimdienst` bleibt mit - Bindestrich, `Arbeits- und` bleibt. + Bindestrich, `Arbeits- und` bleibt. Weiche Trennstriche (U+00AD) und `¬` am Zeilenende + werden ebenfalls aufgelöst (`wach­|senden` → `wachsenden`). - **Überschriften:** Ebenen aus den PDF-Lesezeichen (Teile `##`, Kapitel `###`); Satzreste, die fälschlich als Überschrift erkannt wurden, kommen zurück in den Text. -- **Inhaltsverzeichnis:** zusammengeklebte Einträge werden getrennt. +- **Inhaltsverzeichnis:** zusammengeklebte Einträge werden getrennt; liegt das Verzeichnis + als Tabelle vor, bleibt die Tabelle erhalten. +- **OCR-Ebenen** in Courier werden nicht als Programmcode (`` `…` ``) gesetzt. - **Seitenzahlen, Kolumnentitel, Text in Fotos** werden entfernt. ## Tests @@ -66,12 +75,21 @@ synthetisch erzeugt. ## Grenzen -- Die Ergebnisqualität hängt von pymupdf4llm/pymupdf-layout ab. Zwei Schwächen der - Bibliothek gleicht `pdf2md.py` per Patch aus (Bindestriche am Zeilenende, Absätze in - Scan-Blöcken). Eine weitere ist bekannt: Bei Schriften mit Ligaturen (ff, fi) mit - übergroßer Glyphenbox kann pymupdf4llm einzelne Zeilen verlieren. Bei den Ljutsch-Bänden - tritt das nicht auf (geprüft per Wortabgleich PDF ↔ Markdown). -- OCR-Fehler (z. B. fehlende Leerzeichen) werden nicht korrigiert. +- Die Ergebnisqualität hängt von pymupdf4llm/pymupdf-layout ab. Einige Schwächen der + Bibliothek gleicht `pdf2md.py` per Patch aus (Bindestriche und weiche Trennstriche am + Zeilenende, Absätze in Scan-Blöcken, OCR-Ebenen in Courier). Eine weitere ist bekannt: + Bei Schriften mit Ligaturen (ff, fi) mit übergroßer Glyphenbox kann pymupdf4llm einzelne + Zeilen verlieren. Bei den Ljutsch-Bänden tritt das nicht auf (geprüft per Wortabgleich + PDF ↔ Markdown). +- OCR-Fehler (z. B. fehlende Leerzeichen) werden nicht korrigiert. Bei alten Scans mit + mitgelieferter OCR-Ebene liefert `--ocr always` meist deutlich besseren Text. +- **Mathematische Formeln** werden als Bilder übernommen; für Mathebücher ist ein anderes + Werkzeug besser geeignet (z. B. Marker, Nougat). +- **Was am PDF liegt, bleibt:** fehlende Ligaturen ohne Textzuordnung („Offzielle“), + gesperrt gesetzte Überschriften („M e i n k l e i nes …“), flache PDF-Gliederungen + (alle Kapitel auf einer Ebene). +- Reine Zahlen können als Überschrift erscheinen (Fußnotenzeichen, römische Seitenzahlen). + Weitere bekannte Schwächen: [TODO.md](TODO.md). - Bilder werden als JPEG (Qualität 95) gespeichert; EPUB Band 1 ca. 5 MB. ## Dateien diff --git a/TODO.md b/TODO.md index 23588e6..81e8f2f 100644 --- a/TODO.md +++ b/TODO.md @@ -61,3 +61,14 @@ wieder zusammengesetzt = Gesamt-Markdown. ausführen; Titel, Autor und Cover automatisch übernehmen. - JPEG-Qualität einstellbar machen (z. B. `--jpeg-quality 85`), damit das EPUB noch kleiner wird. - Remote für das Git-Repo einrichten (z. B. GitHub), damit die Commits gesichert sind. + +## 3. Bekannte Schwächen (aus Probeläufen mit fremden PDFs) + +- Reine Zahlen als Überschrift: pymupdf4llm macht Fußnotenzeichen zu Überschriften + („## 90“ im Zensur-Report). Nicht pauschal entfernen – im DDR-Buch sind „## 1“, „## 2“ + echte Kapitelnummern. Möglicher Ansatz: Zahl-Überschrift nur behalten, wenn direkt eine + Überschrift folgt. +- Römische Seitenzahlen („## XX“, „## XXIV“) werden nicht als Seitenzahl erkannt; Vorsicht + wegen echter Kapitel „I.“, „II.“. +- Gesperrte Überschriften („M e i n k l e i nes Stück D D R“) bleiben zerrissen. +- Alte Scans mit eigener OCR-Ebene: `--ocr always` liefert meist besseren Text. diff --git a/pdf2md.py b/pdf2md.py index 29bd0ee..c60685a 100644 --- a/pdf2md.py +++ b/pdf2md.py @@ -303,6 +303,7 @@ def normalize_markdown( fence_length = 0 in_contents = False contents_seen: set[str] = set() # Titel, die im Inhaltsverzeichnis standen + contents_table = False # Inhaltsverzeichnis liegt als Markdown-Tabelle vor headings_seen: set[str] = set() def paragraph_open() -> bool: @@ -375,6 +376,7 @@ def normalize_markdown( if CONTENTS_TITLE.match(text): heading(len(match.group(1)) if match else 2, text) in_contents = True + contents_table = False continue if match: @@ -408,7 +410,21 @@ def normalize_markdown( continue if in_contents: - if ends_sentence(text) and len(text) > 80: + if stripped.startswith("|"): + # Verzeichnis als Tabelle: Zeilen unverändert übernehmen, + # sonst zerfällt die Tabelle in einzelne Absätze. + if not contents_table: + flush_paragraph() + blank_line() + contents_table = True + for cell in stripped.strip("|").split("|"): + contents_seen.add(norm_title(cell)) + result.append(line) + continue + if contents_table: + in_contents = False # Tabelle zu Ende: Fließtext folgt + blank_line() + elif ends_sentence(text) and len(text) > 80: in_contents = False # erster Fließtext nach dem Verzeichnis else: for entry in split_contents_line(stripped, titles): diff --git a/tests/test_normalize.py b/tests/test_normalize.py index 747bf12..7097ad4 100644 --- a/tests/test_normalize.py +++ b/tests/test_normalize.py @@ -217,6 +217,22 @@ def test_contents_block_keeps_entries_and_headings_follow(): ) +def test_contents_table_stays_table_and_contents_end_after_it(): + outline = {"vorwort": 1, "der staat": 1} + md = ( + "## Inhaltsverzeichnis\n\n" + "|Vorwort|3|\n|---|---|\n|Der Staat|19|\n\n" + "Dieser Bericht steht unter CC BY-SA 4.0 (Lizenz)\n\n" + "Vorwort\n\nDer Text beginnt hier.\n" + ) + assert norm(md, outline) == ( + "## Inhaltsverzeichnis\n\n" + "|Vorwort|3|\n|---|---|\n|Der Staat|19|\n\n" + "Dieser Bericht steht unter CC BY-SA 4.0 (Lizenz)\n\n" + "# Vorwort\n\nDer Text beginnt hier.\n" + ) + + def test_heading_formatted_contents_entries_stay_plain(): outline = {"1 der notar": 3} md = "## Inhaltsverzeichnis\n\n## 1 Der Notar\n\n## 1 Der Notar\n\nText.\n"