Inhaltsverzeichnis als Tabelle erhalten, Doku nach Probeläufen ergänzt
- normalize_markdown: Tabellenzeilen im Inhaltsverzeichnis unverändert übernehmen; der Verzeichnis-Modus endet mit der Tabelle (sonst zerfiel die Tabelle und die folgende Überschrift "Vorwort" ging verloren) - Test für Verzeichnis als Tabelle - README/Bedienungsanleitung: weiche Trennstriche, Courier-OCR-Ebenen, englische Bücher, --ocr always für alte Scans, Grenzen aus den Probeläufen mit fremden PDFs - TODO.md: bekannte Schwächen (Zahlen als Überschrift, römische Seitenzahlen, gesperrte Überschriften) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
217d1ee09b
commit
f9dfa3a52e
5 changed files with 87 additions and 16 deletions
|
|
@ -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 |
|
||||
|
|
|
|||
44
README.md
44
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
|
||||
|
|
|
|||
11
TODO.md
11
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.
|
||||
|
|
|
|||
18
pdf2md.py
18
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):
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue