From fa900505351df5548ca0a1683c00c39e98060673 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dieter=20Schl=C3=BCter?= Date: Fri, 2 Oct 2026 15:20:08 +0200 Subject: [PATCH] Bessere Standardwerte: EPUB-Sprache, Titel/Autor, Prozesszahl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - --lang ohne Angabe aus der ersten OCR-Sprache ableiten (eng -> en) - Fehlen Titel/Autor in den PDF-Metadaten: aus dem Dateinamen nach dem Muster "Autor_-_Titel" übernehmen, sonst Dateiname mit Leerzeichen - --jobs: halbe Kernzahl, höchstens 8 (bisher 4) - Tests (tests/test_defaults.py), README und Bedienungsanleitung Co-Authored-By: Claude Opus 5.5 --- BEDIENUNGSANLEITUNG.md | 15 ++++++++----- README.md | 9 ++++---- pdf2md.py | 48 ++++++++++++++++++++++++++++++++++----- tests/test_defaults.py | 51 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 108 insertions(+), 15 deletions(-) create mode 100644 tests/test_defaults.py diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md index 088c461..af9695b 100644 --- a/BEDIENUNGSANLEITUNG.md +++ b/BEDIENUNGSANLEITUNG.md @@ -65,10 +65,15 @@ Danach liegen in `Output/`: | :-- | :-- | | `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) | +| `band1.epub` | das fertige E-Book (Titel und Autor aus der PDF, siehe unten) | Ein Band mit ca. 700 Seiten dauert etwa anderthalb Minuten. +**Titel und Autor im EPUB** stammen aus den Metadaten der PDF. Fehlen sie, wertet das Programm +den Dateinamen nach dem Muster `Autor_-_Titel.pdf` aus: `Katja_Adler_-_Rolle_rückwärts.pdf` ergibt +Titel „Rolle rückwärts“ und Autor „Katja Adler“. Ohne ` - ` im Namen wird der ganze Dateiname +(mit Leerzeichen statt `_`) zum Titel. Es lohnt sich also, PDFs vorher passend umzubenennen. + --- ## 3. Weg B – Markdown von Hand bearbeiten, dann EPUB @@ -166,9 +171,9 @@ python3 pdf2md.py --epub scan.pdf Output/scan.md | :-- | :-- | | 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) | +| mehr oder weniger parallele OCR-Prozesse | `--jobs N` (Standard: halbe Kernzahl, höchstens 8) | | OCR erzwingen, auch für vorhandene OCR-Ebenen | `--ocr always` | -| englisches Buch | `--ocr-language eng --lang en` (Sprachpaket `tesseract-ocr-eng` nötig) | +| englisches Buch | `--ocr-language eng` (Sprachpaket `tesseract-ocr-eng` nötig; EPUB-Sprache wird automatisch `en`) | | keine OCR | `--ocr never` oder `--no-ocr` | So entscheidet das Programm von selbst (`--ocr auto`): @@ -199,12 +204,12 @@ Kolumnentitel und Seitenzahlen gedruckter Bücher werden automatisch entfernt, g | `--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) | +| `--jobs N` | parallele OCR-Prozesse (Standard: halbe Kernzahl, höchstens 8) | | `--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`) | +| `--lang` | Sprache im EPUB (Standard: aus `--ocr-language` abgeleitet, `deu`→`de`, `eng`→`en`; sonst `de`) | | `--keep-headers` | Kolumnentitel/Seitenköpfe nicht entfernen | | `--keep-footers` | Seitenzahlen/Seitenfüße nicht entfernen | diff --git a/README.md b/README.md index 6a1005c..93e43e1 100644 --- a/README.md +++ b/README.md @@ -27,14 +27,15 @@ dann EPUB“: **[BEDIENUNGSANLEITUNG.md](BEDIENUNGSANLEITUNG.md)**. # digitales oder gescanntes PDF -> Markdown (OCR wird automatisch entschieden) python3 pdf2md.py buch.pdf buch.md -# zusätzlich Bilder und EPUB (Titel/Autor aus den PDF-Metadaten, Seite 1 als Cover) +# zusätzlich Bilder und EPUB (Titel/Autor aus den PDF-Metadaten bzw. dem Dateinamen +# "Autor_-_Titel.pdf", Seite 1 als Cover) 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 +python3 pdf2md.py --epub --ocr-language eng --ocr always scan.pdf scan.md # EPUB-Sprache: en ``` | Option | Bedeutung | @@ -42,11 +43,11 @@ python3 pdf2md.py --epub --lang en --ocr-language eng --ocr always scan.pdf scan | `--ocr auto` (Standard) | Scanseiten per OCR lesen; vorhandene, gerade OCR-Ebenen übernehmen, schiefe neu erkennen; reine Bildseiten in digitalen PDFs (Fotos, Cover) nur nach erfolgreicher Probe-OCR | | `--ocr always` / `--ocr never` (`--no-ocr`) | alle Seiten neu erkennen / keine OCR | | `--ocr-language`, `--ocr-dpi` | Tesseract-Sprache(n) (`deu`), Auflösung (300) | -| `--jobs N` | parallele OCR-Prozesse (Standard 4); reine Textdokumente laufen immer sequenziell | +| `--jobs N` | parallele OCR-Prozesse (Standard: halbe Kernzahl, höchstens 8); reine Textdokumente laufen immer sequenziell | | `--images` | Grafiken in `_images/` speichern und verlinken | | `--image-format jpg\|png` | Bildformat (Standard jpg; png verlustfrei, aber ca. 3× größer) | | `--epub`, `--epub-file PFAD` | EPUB 3 mit pandoc erzeugen (schaltet `--images` ein) | -| `--lang` | Sprache im EPUB (`de`) | +| `--lang` | Sprache im EPUB (Standard: aus `--ocr-language`, `deu`→`de`, `eng`→`en`) | | `--keep-headers`, `--keep-footers` | Kolumnentitel bzw. Seitenzahlen nicht entfernen | ## Was die Nachbearbeitung macht diff --git a/pdf2md.py b/pdf2md.py index a848de4..7f81ff0 100644 --- a/pdf2md.py +++ b/pdf2md.py @@ -1057,6 +1057,39 @@ def heading_depth(markdown: str) -> int: return min(max(levels, default=1), 3) +# Tesseract-Sprachcode -> Sprachcode im EPUB (ISO 639-1) +EPUB_LANGUAGES = { + "deu": "de", "deu_latf": "de", "frk": "de", "eng": "en", "fra": "fr", + "spa": "es", "ita": "it", "nld": "nl", "por": "pt", "pol": "pl", + "ces": "cs", "dan": "da", "swe": "sv", "nor": "no", "fin": "fi", + "rus": "ru", "ukr": "uk", "lat": "la", "ell": "el", "tur": "tr", +} + + +def epub_language(ocr_language: str) -> str: + """EPUB-Sprache aus der (ersten) OCR-Sprache: "eng+deu" -> "en".""" + first = ocr_language.split("+")[0].strip().lower() + return EPUB_LANGUAGES.get(first, "de") + + +def default_jobs() -> int: + """Halbe Kernzahl, höchstens 8: Jeder Prozess lädt das Layout-Modell.""" + return max(1, min(8, (os.cpu_count() or 2) // 2)) + + +def title_author_from_filename(stem: str) -> tuple[str, str]: + """Titel und Autor aus einem Dateinamen nach dem Muster "Autor_-_Titel". + + "Katja_Adler_-_Rolle_rückwärts" -> ("Rolle rückwärts", "Katja Adler"); + ohne " - " ist der ganze Name der Titel. + """ + name = re.sub(r"\s+", " ", stem.replace("_", " ")).strip() + author, sep, title = name.partition(" - ") + if sep and author.strip() and title.strip(): + return title.strip(), author.strip() + return name, "" + + def write_epub( markdown_path: Path, epub_path: Path, @@ -1142,8 +1175,9 @@ def main() -> int: parser.add_argument( "--jobs", type=int, - default=min(4, os.cpu_count() or 1), - help="Anzahl paralleler Prozesse (Standard: %(default)s)", + default=default_jobs(), + help="Anzahl paralleler OCR-Prozesse (Standard: halbe Kernzahl, " + "höchstens 8; hier %(default)s)", ) parser.add_argument( "--images", @@ -1169,8 +1203,8 @@ def main() -> int: ) parser.add_argument( "--lang", - default="de", - help="Sprache im EPUB (Standard: de)", + help="Sprache im EPUB, z. B. en (Standard: aus --ocr-language, " + "deu -> de, eng -> en)", ) parser.add_argument( "--keep-headers", @@ -1183,6 +1217,7 @@ def main() -> int: help="Seitenfüße/Seitenzahlen übernehmen", ) args = parser.parse_args() + args.lang = args.lang or epub_language(args.ocr_language) source = args.pdf.expanduser() target = (args.markdown or source.with_suffix(".md")).expanduser() @@ -1206,8 +1241,9 @@ def main() -> int: outline = load_outline(doc) uris = load_link_uris(doc) page_count = doc.page_count - title = (doc.metadata or {}).get("title") or source.stem - author = (doc.metadata or {}).get("author") or "" + file_title, file_author = title_author_from_filename(source.stem) + title = ((doc.metadata or {}).get("title") or "").strip() or file_title + author = ((doc.metadata or {}).get("author") or "").strip() or file_author counts = Counter(kinds) print( diff --git a/tests/test_defaults.py b/tests/test_defaults.py new file mode 100644 index 0000000..848fd26 --- /dev/null +++ b/tests/test_defaults.py @@ -0,0 +1,51 @@ +"""Standardwerte: EPUB-Sprache, Prozesszahl, Titel/Autor aus dem Dateinamen.""" + +import zipfile + +import pymupdf +import pytest + +import pdf2md as p +from conftest import needs_pandoc +from test_e2e import run + + +@pytest.mark.parametrize( + "ocr_language, expected", + [("deu", "de"), ("eng", "en"), ("eng+deu", "en"), ("fra", "fr"), ("xyz", "de")], +) +def test_epub_language_follows_ocr_language(ocr_language, expected): + assert p.epub_language(ocr_language) == expected + + +@pytest.mark.parametrize("cores, expected", [(None, 1), (1, 1), (4, 2), (24, 8)]) +def test_default_jobs(monkeypatch, cores, expected): + monkeypatch.setattr(p.os, "cpu_count", lambda: cores) + assert p.default_jobs() == expected + + +@pytest.mark.parametrize( + "stem, expected", + [ + ("Katja_Adler_-_Rolle_rückwärts_DDR", ("Rolle rückwärts DDR", "Katja Adler")), + ("Zensur_Netzwerk_Deutschland_de-report-de", ("Zensur Netzwerk Deutschland de-report-de", "")), + ("A_-_B_-_C", ("B - C", "A")), + ], +) +def test_title_author_from_filename(stem, expected): + assert p.title_author_from_filename(stem) == expected + + +@needs_pandoc +def test_epub_without_metadata_uses_filename_and_ocr_language(pdfs, tmp_path): + doc = pymupdf.open(pdfs["digital"]) + doc.set_metadata({}) + source = tmp_path / "Max_Muster_-_Ein_Buch.pdf" + doc.save(source) + + run(source, tmp_path / "buch.md", "--epub", "--ocr-language", "eng") + with zipfile.ZipFile(tmp_path / "buch.epub") as z: + opf = z.read("EPUB/content.opf").decode() + assert ">Ein Buch<" in opf + assert ">Max Muster<" in opf + assert "en" in opf