diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md new file mode 100644 index 0000000..dbea778 --- /dev/null +++ b/BEDIENUNGSANLEITUNG.md @@ -0,0 +1,236 @@ +# Bedienungsanleitung: pdf2md_v5.py – PDF → Markdown → EPUB + +`pdf2md_v5.py` wandelt PDF-Bücher in sauberes Markdown um: echte Absätze, Überschriften mit +richtiger Ebene, Bilder und ohne Seitenzahlen. Daraus entsteht auf Wunsch direkt ein EPUB für +den PocketBook-Reader. Das funktioniert mit digitalen PDFs (Text lässt sich markieren) ebenso wie +mit gescannten PDFs (nur Seitenbilder, der Text wird per OCR erkannt). + +Es gibt zwei Arbeitswege: + +- **Weg A:** PDF → EPUB in einem Schritt, wenn das Ergebnis ohne Nacharbeit passt. +- **Weg B:** PDF → Markdown → von Hand bearbeiten → EPUB, wenn du Überschriften, Text oder + Bilder noch korrigieren willst. + +--- + +## 1. Installation und Prüfung + +Einmalig unter Ubuntu 24.04: + +```bash +sudo apt install tesseract-ocr tesseract-ocr-deu pandoc +cd ~/Downloads/ljutsch +pip install -r requirements.txt +``` + +- `tesseract-ocr` + `tesseract-ocr-deu` braucht man nur für gescannte PDFs. +- `pandoc` braucht man nur für das EPUB. + +Prüfen, ob alles da ist: + +```bash +tesseract --list-langs # muss "deu" enthalten +pandoc --version # Version 3.x +python3 pdf2md_v5.py --help # zeigt alle Optionen +``` + +--- + +## 2. Weg A – PDF → EPUB in einem Schritt + +```bash +cd ~/Downloads/ljutsch +python3 pdf2md_v5.py --epub Operation_Ljutsch_Band_1_-_Reinhard_Otto_Kranz.pdf Output/band1.md +``` + +Das Programm meldet zuerst, was es in der PDF gefunden hat: + +``` +706 Seiten: 705 Text, 1 Scan/Bild, 0 mit OCR-Ebene, 0 leer → 0 Seiten werden per OCR gelesen +Hinweis: 1 Bildseite(n) ohne Text bleiben ohne OCR (Fotos/Cover oder --ocr never). + [1/15] Seiten 1–50 (Text) fertig + ... +``` + +| Begriff | Bedeutung | +| :-- | :-- | +| Text | Seiten mit echter Textebene, keine OCR nötig | +| Scan/Bild | Seiten, die nur aus einem Bild bestehen (Scan, Foto, Cover) | +| mit OCR-Ebene | Scans, die schon unsichtbaren Text enthalten (z. B. von `ocrmypdf`) | +| „Hinweis: … Bildseite(n)“ | Bildseiten, die bewusst *nicht* per OCR gelesen werden, meist das Cover | + +Danach liegen in `Output/`: + +| Datei | Inhalt | +| :-- | :-- | +| `band1.md` | das Buch als Markdown | +| `band1_images/` | Bilder als JPEG, dazu `cover.jpg` (Seite 1 der PDF) | +| `band1.epub` | das fertige E-Book (Titel und Autor aus der PDF) | + +Ein Band mit ca. 700 Seiten dauert etwa anderthalb Minuten. + +--- + +## 3. Weg B – Markdown von Hand bearbeiten, dann EPUB + +### Schritt 1: Markdown erzeugen + +Wie bei Weg A, also **mit** `--epub`. So entstehen gleich das Cover (`cover.jpg`) und ein erstes +EPUB zum Vergleichen: + +```bash +cd ~/Downloads/ljutsch +python3 pdf2md_v5.py --epub Operation_Ljutsch_Band_1_-_Reinhard_Otto_Kranz.pdf Output/band1.md +``` + +Wenn du kein Cover und kein Probe-EPUB brauchst, reicht `--images` statt `--epub`. + +### Schritt 2: `band1.md` bearbeiten + +Öffne die Datei in einem beliebigen Texteditor und halte dich an diese Regeln: + +| Element | Schreibweise | Wirkung im EPUB | +| :-- | :-- | :-- | +| Teil | `## Teil I: Das Vermächtnis` | eigener Abschnitt im Inhaltsverzeichnis | +| Kapitel | `### 1 Der Notar` | eigenes Kapitel (eigene Datei im EPUB), unter dem Teil eingeordnet | +| Absatz | durch **eine Leerzeile** vom nächsten getrennt | neuer Absatz | +| Zeilenumbruch innerhalb eines Absatzes | einfacher Umbruch | wird zu einem Leerzeichen | +| Bild | `![](band1_images/…jpg)` auf eigener Zeile | Bild an dieser Stelle | +| kursiv / fett | `_Wort_` / `**Wort**` | kursiv / fett | + +Wichtig: + +- Der Ordner `band1_images/` muss **neben** `band1.md` liegen bleiben. Benennst du die + Markdown-Datei um, ändern sich die Bildpfade nicht, die Bilder bleiben gültig. +- Bildzeilen nicht verändern, nur löschen, wenn das Bild weg soll. +- Nach `#` immer ein Leerzeichen: `## Titel`, nicht `##Titel`. + +Beispiel: Eine falsch erkannte Überschrift korrigieren. + +Vorher (ein Satz, der als Überschrift erkannt wurde, und ein Kapiteltitel ohne Überschrift): + +```markdown +## Er ging nach Hause. + +**4 Flucht aufs Land** + +Am nächsten Morgen ... +``` + +Nachher: + +```markdown +Er ging nach Hause. + +### 4 Flucht aufs Land + +Am nächsten Morgen ... +``` + +### Schritt 3: EPUB aus dem bearbeiteten Markdown erzeugen + +Im Ordner der Markdown-Datei starten (wichtig für die Bilder): + +```bash +cd ~/Downloads/ljutsch/Output +pandoc band1.md \ + --from markdown-yaml_metadata_block --to epub3 \ + --output band1.epub \ + --resource-path . \ + --split-level=3 --toc-depth=3 \ + --metadata title="Operation Ljutsch" \ + --metadata author="Reinhard Otto Kranz" \ + --metadata lang=de \ + --epub-cover-image band1_images/cover.jpg +``` + +Das sind dieselben Einstellungen, die `pdf2md_v5.py --epub` verwendet. Titel und Autor trägst du +selbst ein. Ohne Cover lässt du die letzte Zeile weg und entfernst den `\` am Ende der Zeile davor. + +Schritt 2 und 3 kannst du beliebig oft wiederholen. **Achtung:** Schritt 1 erneut auszuführen +überschreibt deine Änderungen in `band1.md`. Leg dir vorher eine Kopie an oder nimm einen anderen +Dateinamen. + +--- + +## 4. Gescannte PDFs + +Bei Scans liest das Programm den Text per OCR (Tesseract). Der Aufruf ist derselbe: + +```bash +python3 pdf2md_v5.py --epub scan.pdf Output/scan.md +``` + +| Situation | Option | +| :-- | :-- | +| Buch in Deutsch (Standard) | – (`--ocr-language deu`) | +| 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` | +| keine OCR | `--ocr never` oder `--no-ocr` | + +So entscheidet das Programm von selbst (`--ocr auto`): + +- **Scanseiten** werden per OCR gelesen. Richtwert: ca. 1 Sekunde pro Seite mit 4 Prozessen, + 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. +- **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“. + +Kolumnentitel und Seitenzahlen gedruckter Bücher werden automatisch entfernt, getrennte Wörter +(„Geheim-|dienst“) zusammengefügt. + +--- + +## 5. Alle Optionen + +| Option | Bedeutung | +| :-- | :-- | +| `pdf` | Eingabe-PDF | +| `markdown` | Ausgabe-Datei; ohne Angabe: gleicher Name wie die PDF mit `.md` | +| `--ocr auto\|always\|never` | OCR-Entscheidung (Standard `auto`, siehe Abschnitt 4) | +| `--no-ocr` | Kurzform für `--ocr never` | +| `--ocr-language` | Tesseract-Sprache(n), z. B. `deu+eng` (Standard `deu`) | +| `--ocr-dpi` | Auflösung für die OCR (Standard 300) | +| `--jobs N` | parallele OCR-Prozesse (Standard 4) | +| `--images` | Bilder in `_images/` speichern und im Markdown verlinken | +| `--image-format jpg\|png` | Bildformat (Standard `jpg`; `png` verlustfrei, aber ca. 3× größer) | +| `--epub` | zusätzlich EPUB erzeugen (schaltet `--images` ein) | +| `--epub-file PFAD` | anderer Pfad für das EPUB | +| `--lang` | Sprache im EPUB (Standard `de`) | +| `--keep-headers` | Kolumnentitel/Seitenköpfe nicht entfernen | +| `--keep-footers` | Seitenzahlen/Seitenfüße nicht entfernen | + +--- + +## 6. Ergebnis prüfen + +1. **Überschriften auflisten.** So sieht man schnell, ob Teile und Kapitel stimmen: + ```bash + 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. +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. +4. Für Feinarbeit direkt am EPUB eignen sich **Sigil** oder der **Calibre-Editor** („Buch bearbeiten“). + +--- + +## 7. Häufige Probleme + +| Meldung / Problem | Lösung | +| :-- | :-- | +| `Tesseract-Sprachdaten fehlen: deu – installieren mit: …` | den angezeigten Befehl ausführen, z. B. `sudo apt install tesseract-ocr-deu` | +| `Tesseract nicht gefunden` | `sudo apt install tesseract-ocr tesseract-ocr-deu` | +| `pandoc nicht gefunden` | `sudo apt install pandoc` | +| 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) | +| 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 070a1af..115aa02 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,9 @@ pip install -r requirements.txt # pymupdf4llm, pymupdf-layout, pytest ## Verwendung +Ausführliche Schritt-für-Schritt-Anleitung, auch für den Weg „Markdown von Hand bearbeiten, +dann EPUB“: **[BEDIENUNGSANLEITUNG.md](BEDIENUNGSANLEITUNG.md)**. + ```bash # digitales oder gescanntes PDF -> Markdown (OCR wird automatisch entschieden) python3 pdf2md_v5.py buch.pdf buch.md diff --git a/pdf2md_v5.py b/pdf2md_v5.py index 5c134f3..2a1aef6 100644 --- a/pdf2md_v5.py +++ b/pdf2md_v5.py @@ -867,15 +867,25 @@ def extract_markdown(source: Path, plan: list[str], options: dict, jobs: int) -> return "\n\n".join(parts) -def make_image_links_relative(markdown: str, image_dir: Path) -> str: - """Absolute Bildpfade durch Pfade relativ zur Markdown-Datei ersetzen.""" - absolute_dir = image_dir.resolve().as_posix() - relative_dir = image_dir.name +IMAGE_LINK = re.compile(r"(!\[[^\]]*\]\()([^)\s]+)\)") - return markdown.replace( - absolute_dir + "/", - relative_dir + "/", - ) + +def make_image_links_relative(markdown: str, image_dir: Path) -> str: + """Bildpfade relativ zur Markdown-Datei machen ("_images/datei.jpg"). + + pymupdf4llm schreibt den Pfad relativ zum Arbeitsverzeichnis, wenn der + Bilderordner darunter liegt (z. B. "Output/x_images/…"), sonst absolut. + """ + image_dir = image_dir.resolve() + + def fix(match: re.Match) -> str: + path = Path(match.group(2)) + full = path if path.is_absolute() else Path.cwd() / path + if full.resolve().parent != image_dir: + return match.group(0) + return f"{match.group(1)}{image_dir.name}/{path.name})" + + return IMAGE_LINK.sub(fix, markdown) # =========================================================================== diff --git a/tests/test_e2e.py b/tests/test_e2e.py index 65ce0a9..9cbed2b 100644 --- a/tests/test_e2e.py +++ b/tests/test_e2e.py @@ -13,12 +13,13 @@ from conftest import ROOT, TITLE, needs_pandoc, needs_tesseract SCRIPT = ROOT / "pdf2md_v5.py" -def run(*args) -> subprocess.CompletedProcess: +def run(*args, cwd=None) -> subprocess.CompletedProcess: done = subprocess.run( [sys.executable, str(SCRIPT), *map(str, args)], capture_output=True, text=True, timeout=600, + cwd=cwd, ) assert done.returncode == 0, done.stderr return done @@ -128,3 +129,14 @@ def test_png_on_request(pdfs, tmp_path): run(pdfs["digital"], out, "--images", "--image-format", "png") md = out.read_text(encoding="utf-8") assert re.search(r"!\[\]\(png_images/[^)]+\.png\)", md) + + +def test_image_links_relative_to_markdown_with_relative_output(pdfs, tmp_path): + # Aufruf wie "python3 pdf2md_v5.py --images buch.pdf Output/buch.md" aus dem Projektordner + run(pdfs["digital"], "Output/buch.md", "--images", cwd=tmp_path) + md = (tmp_path / "Output" / "buch.md").read_text(encoding="utf-8") + links = re.findall(r"!\[\]\(([^)]+)\)", md) + assert links + for link in links: + assert link.startswith("buch_images/"), link + assert (tmp_path / "Output" / link).is_file() diff --git a/tests/test_normalize.py b/tests/test_normalize.py index 48ef565..001e9a2 100644 --- a/tests/test_normalize.py +++ b/tests/test_normalize.py @@ -1,5 +1,7 @@ """Unit-Tests für die Markdown-Nachbearbeitung (ohne PDF, schnell).""" +from pathlib import Path + import pymupdf import pytest @@ -263,3 +265,17 @@ def test_split_by_indent_needs_sentence_end(): def test_demoted_heading_after_closed_paragraph_starts_new_paragraph(): md = "Erster Absatz.\n## Sie verabschiedeten sich.\n" assert norm(md) == "Erster Absatz.\n\nSie verabschiedeten sich.\n" + + +def test_make_image_links_relative(tmp_path, monkeypatch): + image_dir = tmp_path / "Output" / "buch_images" + image_dir.mkdir(parents=True) + monkeypatch.chdir(tmp_path) + md = ( + f"![](Output/buch_images/a.jpg)\n\n![]({image_dir}/b.jpg)\n\n" + "![](https://example.org/c.jpg)\n" + ) + assert p.make_image_links_relative(md, Path("Output/buch_images")) == ( + "![](buch_images/a.jpg)\n\n![](buch_images/b.jpg)\n\n" + "![](https://example.org/c.jpg)\n" + )