Bessere Standardwerte: EPUB-Sprache, Titel/Autor, Prozesszahl

- --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 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-10-02 15:20:08 +02:00
commit fa90050535
4 changed files with 108 additions and 15 deletions

View file

@ -65,10 +65,15 @@ Danach liegen in `Output/`:
| :-- | :-- | | :-- | :-- |
| `band1.md` | das Buch als Markdown | | `band1.md` | das Buch als Markdown |
| `band1_images/` | Bilder als JPEG, dazu `cover.jpg` (Seite 1 der PDF) | | `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. 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 ## 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`) | | Buch in Deutsch (Standard) | – (`--ocr-language deu`) |
| Deutsch und Englisch gemischt | `--ocr-language deu+eng` (Sprachpaket `tesseract-ocr-eng` nötig) | | 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` | | 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` | | keine OCR | `--ocr never` oder `--no-ocr` |
So entscheidet das Programm von selbst (`--ocr auto`): 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` | | `--no-ocr` | Kurzform für `--ocr never` |
| `--ocr-language` | Tesseract-Sprache(n), z. B. `deu+eng` (Standard `deu`) | | `--ocr-language` | Tesseract-Sprache(n), z. B. `deu+eng` (Standard `deu`) |
| `--ocr-dpi` | Auflösung für die OCR (Standard 300) | | `--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 `<name>_images/` speichern und im Markdown verlinken | | `--images` | Bilder in `<name>_images/` speichern und im Markdown verlinken |
| `--image-format jpg\|png` | Bildformat (Standard `jpg`; `png` verlustfrei, aber ca. 3× größer) | | `--image-format jpg\|png` | Bildformat (Standard `jpg`; `png` verlustfrei, aber ca. 3× größer) |
| `--epub` | zusätzlich EPUB erzeugen (schaltet `--images` ein) | | `--epub` | zusätzlich EPUB erzeugen (schaltet `--images` ein) |
| `--epub-file PFAD` | anderer Pfad für das EPUB | | `--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-headers` | Kolumnentitel/Seitenköpfe nicht entfernen |
| `--keep-footers` | Seitenzahlen/Seitenfüße nicht entfernen | | `--keep-footers` | Seitenzahlen/Seitenfüße nicht entfernen |

View file

@ -27,14 +27,15 @@ dann EPUB“: **[BEDIENUNGSANLEITUNG.md](BEDIENUNGSANLEITUNG.md)**.
# digitales oder gescanntes PDF -> Markdown (OCR wird automatisch entschieden) # digitales oder gescanntes PDF -> Markdown (OCR wird automatisch entschieden)
python3 pdf2md.py buch.pdf buch.md 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 python3 pdf2md.py --epub buch.pdf buch.md
# Scan in Deutsch und Englisch, 8 parallele OCR-Prozesse # Scan in Deutsch und Englisch, 8 parallele OCR-Prozesse
python3 pdf2md.py --ocr-language deu+eng --jobs 8 scan.pdf scan.md python3 pdf2md.py --ocr-language deu+eng --jobs 8 scan.pdf scan.md
# englisches Buch; alter Scan mit schlechter OCR-Ebene neu erkennen # 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 | | 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 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 always` / `--ocr never` (`--no-ocr`) | alle Seiten neu erkennen / keine OCR |
| `--ocr-language`, `--ocr-dpi` | Tesseract-Sprache(n) (`deu`), Auflösung (300) | | `--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 `<name>_images/` speichern und verlinken | | `--images` | Grafiken in `<name>_images/` speichern und verlinken |
| `--image-format jpg\|png` | Bildformat (Standard jpg; png verlustfrei, aber ca. 3× größer) | | `--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) | | `--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 | | `--keep-headers`, `--keep-footers` | Kolumnentitel bzw. Seitenzahlen nicht entfernen |
## Was die Nachbearbeitung macht ## Was die Nachbearbeitung macht

View file

@ -1057,6 +1057,39 @@ def heading_depth(markdown: str) -> int:
return min(max(levels, default=1), 3) 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( def write_epub(
markdown_path: Path, markdown_path: Path,
epub_path: Path, epub_path: Path,
@ -1142,8 +1175,9 @@ def main() -> int:
parser.add_argument( parser.add_argument(
"--jobs", "--jobs",
type=int, type=int,
default=min(4, os.cpu_count() or 1), default=default_jobs(),
help="Anzahl paralleler Prozesse (Standard: %(default)s)", help="Anzahl paralleler OCR-Prozesse (Standard: halbe Kernzahl, "
"höchstens 8; hier %(default)s)",
) )
parser.add_argument( parser.add_argument(
"--images", "--images",
@ -1169,8 +1203,8 @@ def main() -> int:
) )
parser.add_argument( parser.add_argument(
"--lang", "--lang",
default="de", help="Sprache im EPUB, z. B. en (Standard: aus --ocr-language, "
help="Sprache im EPUB (Standard: de)", "deu -> de, eng -> en)",
) )
parser.add_argument( parser.add_argument(
"--keep-headers", "--keep-headers",
@ -1183,6 +1217,7 @@ def main() -> int:
help="Seitenfüße/Seitenzahlen übernehmen", help="Seitenfüße/Seitenzahlen übernehmen",
) )
args = parser.parse_args() args = parser.parse_args()
args.lang = args.lang or epub_language(args.ocr_language)
source = args.pdf.expanduser() source = args.pdf.expanduser()
target = (args.markdown or source.with_suffix(".md")).expanduser() target = (args.markdown or source.with_suffix(".md")).expanduser()
@ -1206,8 +1241,9 @@ def main() -> int:
outline = load_outline(doc) outline = load_outline(doc)
uris = load_link_uris(doc) uris = load_link_uris(doc)
page_count = doc.page_count page_count = doc.page_count
title = (doc.metadata or {}).get("title") or source.stem file_title, file_author = title_author_from_filename(source.stem)
author = (doc.metadata or {}).get("author") or "" 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) counts = Counter(kinds)
print( print(

51
tests/test_defaults.py Normal file
View file

@ -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 "<dc:language>en</dc:language>" in opf