Bedienungsanleitung ergänzt, Bildpfade relativ zur Markdown-Datei

- BEDIENUNGSANLEITUNG.md: Installation, Weg A (PDF -> EPUB), Weg B (Markdown
  bearbeiten, dann pandoc), Scans, Optionen, Prüfen, häufige Probleme;
  Verweis in README.md
- Fix: pymupdf4llm schreibt Bildpfade relativ zum Arbeitsverzeichnis
  ("Output/x_images/…"), wenn der Bilderordner darunter liegt. Die Links
  zeigen jetzt immer relativ zur Markdown-Datei; Regressionstests ergänzt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-10-02 01:23:36 +02:00
commit 85a01ac5c7
5 changed files with 285 additions and 8 deletions

236
BEDIENUNGSANLEITUNG.md Normal file
View file

@ -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 `<name>_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 |

View file

@ -17,6 +17,9 @@ pip install -r requirements.txt # pymupdf4llm, pymupdf-layout, pytest
## Verwendung ## Verwendung
Ausführliche Schritt-für-Schritt-Anleitung, auch für den Weg „Markdown von Hand bearbeiten,
dann EPUB“: **[BEDIENUNGSANLEITUNG.md](BEDIENUNGSANLEITUNG.md)**.
```bash ```bash
# digitales oder gescanntes PDF -> Markdown (OCR wird automatisch entschieden) # digitales oder gescanntes PDF -> Markdown (OCR wird automatisch entschieden)
python3 pdf2md_v5.py buch.pdf buch.md python3 pdf2md_v5.py buch.pdf buch.md

View file

@ -867,15 +867,25 @@ def extract_markdown(source: Path, plan: list[str], options: dict, jobs: int) ->
return "\n\n".join(parts) return "\n\n".join(parts)
def make_image_links_relative(markdown: str, image_dir: Path) -> str: IMAGE_LINK = re.compile(r"(!\[[^\]]*\]\()([^)\s]+)\)")
"""Absolute Bildpfade durch Pfade relativ zur Markdown-Datei ersetzen."""
absolute_dir = image_dir.resolve().as_posix()
relative_dir = image_dir.name
return markdown.replace(
absolute_dir + "/", def make_image_links_relative(markdown: str, image_dir: Path) -> str:
relative_dir + "/", """Bildpfade relativ zur Markdown-Datei machen ("<name>_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)
# =========================================================================== # ===========================================================================

View file

@ -13,12 +13,13 @@ from conftest import ROOT, TITLE, needs_pandoc, needs_tesseract
SCRIPT = ROOT / "pdf2md_v5.py" SCRIPT = ROOT / "pdf2md_v5.py"
def run(*args) -> subprocess.CompletedProcess: def run(*args, cwd=None) -> subprocess.CompletedProcess:
done = subprocess.run( done = subprocess.run(
[sys.executable, str(SCRIPT), *map(str, args)], [sys.executable, str(SCRIPT), *map(str, args)],
capture_output=True, capture_output=True,
text=True, text=True,
timeout=600, timeout=600,
cwd=cwd,
) )
assert done.returncode == 0, done.stderr assert done.returncode == 0, done.stderr
return done return done
@ -128,3 +129,14 @@ def test_png_on_request(pdfs, tmp_path):
run(pdfs["digital"], out, "--images", "--image-format", "png") run(pdfs["digital"], out, "--images", "--image-format", "png")
md = out.read_text(encoding="utf-8") md = out.read_text(encoding="utf-8")
assert re.search(r"!\[\]\(png_images/[^)]+\.png\)", md) 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()

View file

@ -1,5 +1,7 @@
"""Unit-Tests für die Markdown-Nachbearbeitung (ohne PDF, schnell).""" """Unit-Tests für die Markdown-Nachbearbeitung (ohne PDF, schnell)."""
from pathlib import Path
import pymupdf import pymupdf
import pytest import pytest
@ -263,3 +265,17 @@ def test_split_by_indent_needs_sentence_end():
def test_demoted_heading_after_closed_paragraph_starts_new_paragraph(): def test_demoted_heading_after_closed_paragraph_starts_new_paragraph():
md = "Erster Absatz.\n## Sie verabschiedeten sich.\n" md = "Erster Absatz.\n## Sie verabschiedeten sich.\n"
assert norm(md) == "Erster Absatz.\n\nSie 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"
)