- Erkennung: epub:type/role pagebreak (aria-label, title, Text) sowie Seitenanker-IDs page_47, page37, seite-12 und p17 (nur leere a/span) - Ausgabe: visible = \[S. 123\], comment = <!-- S. 123 -->, none (Standard) - keine Marker in Code; Marker in Überschriften als Absatz davor; Layout-Tabellen-Überschriften behalten ihre Seitenmarke - Bericht: layout.page_markers; interne data-e2m-*-Attribute nie im HTML Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
451 lines
29 KiB
Markdown
451 lines
29 KiB
Markdown
# epub2md
|
||
|
||
Kommandozeilenwerkzeug, das **DRM-freie EPUB-Dateien** (EPUB 2 und EPUB 3) lokal und offline in eine
|
||
strukturierte Sammlung **kapitelweiser Markdown-Dateien** umwandelt – mit extrahierten Medien,
|
||
YAML-Metadaten, Inhaltsverzeichnis, maschinenlesbarem Manifest und einem Konvertierungsbericht.
|
||
|
||
Gedacht für die Weiterverarbeitung mit Markdown-Renderern, Pandoc, Obsidian/Zettelkasten, Git,
|
||
RAG-/Embedding-Pipelines, Volltextsuche und lokale LLMs.
|
||
|
||
Eine Schritt-für-Schritt-Anleitung für Anwender steht in der [BEDIENUNGSANLEITUNG.md](BEDIENUNGSANLEITUNG.md).
|
||
|
||
Das Projekt enthält zwei Programme:
|
||
|
||
| Programm | Zweck |
|
||
| --- | --- |
|
||
| `epub2md.py` | EPUB → Verzeichnis mit Kapitel-Markdown, Medien, Metadaten und Bericht |
|
||
| `epub2md_merge.py` | Ausgabeverzeichnis → **eine** vollständige Markdown-Datei des Buches |
|
||
|
||
## Schnellstart
|
||
|
||
```bash
|
||
pip install -r requirements.txt
|
||
python3 epub2md.py Beispiele/buch.epub output/buch # Kapitel-Dateien erzeugen
|
||
python3 epub2md_merge.py output/buch # → output/buch/buch.md (Gesamtdatei)
|
||
```
|
||
|
||
## Grenzen
|
||
|
||
- **Kein DRM:** Verschlüsselte EPUBs (`META-INF/encryption.xml` mit verschlüsselten Inhalten) werden mit
|
||
Exit-Code 2 abgelehnt. Es wird kein Kopierschutz umgangen. Reine Font-Obfuscation ist erlaubt.
|
||
- **Keine OCR:** Gescannte Bücher (Seiten als Bilder) liefern nur Bildreferenzen, keinen Text.
|
||
- **Formeln als Bild/SVG** werden nie in TeX „geraten“. Sie bleiben als Bild/SVG erhalten und werden im
|
||
Bericht gemeldet. TeX entsteht nur aus echtem MathML, TeX-Annotationen oder TeX im `alt`-Text.
|
||
- **Layout-Heuristiken** (z. B. Überschriften, die als Tabellen gesetzt sind) sind konservativ und
|
||
erfassen nicht jedes Verlagslayout.
|
||
|
||
## Installation (Ubuntu 24.04)
|
||
|
||
```bash
|
||
sudo apt install python3 python3-venv
|
||
python3 -m venv .venv
|
||
source .venv/bin/activate
|
||
pip install -r requirements.txt # Laufzeit: nur lxml
|
||
pip install -r requirements-dev.txt # Tests: pytest, PyYAML
|
||
```
|
||
|
||
Laufzeitabhängigkeit ist ausschließlich **lxml** (sicheres XML/HTML-Parsing). Alles andere ist
|
||
Standardbibliothek. Pandoc, Calibre, Java oder Node.js werden **nicht** benötigt. Pandoc ist nur für
|
||
die optionale Weiterverarbeitung der Ausgabe nützlich.
|
||
|
||
## Aufruf
|
||
|
||
```bash
|
||
python3 epub2md.py INPUT.epub OUTPUT_VERZEICHNIS [Optionen]
|
||
# alternativ
|
||
python3 -m epub2md INPUT.epub OUTPUT_VERZEICHNIS [Optionen]
|
||
```
|
||
|
||
Beispiele:
|
||
|
||
```bash
|
||
# Programmierbuch mit Standardoptionen
|
||
python3 epub2md.py \
|
||
Beispiele/programmierbuch.epub \
|
||
output/programmierbuch \
|
||
--format gfm \
|
||
--math auto \
|
||
--footnotes end-of-chapter \
|
||
--extract-media \
|
||
--frontmatter \
|
||
--toc \
|
||
--verbose
|
||
|
||
# Roman mit Teilen (h1) und Kapiteln (h2): ein File pro Kapitel
|
||
python3 epub2md.py roman.epub output/roman --chapter-level 2
|
||
|
||
# Nur analysieren, nichts schreiben
|
||
python3 epub2md.py buch.epub output/buch --dry-run
|
||
|
||
# Erneut konvertieren und frühere Ausgabe ersetzen
|
||
python3 epub2md.py buch.epub output/buch --force --report berichte/buch.json
|
||
```
|
||
|
||
## Optionen
|
||
|
||
| Option | Standard | Wirkung |
|
||
| --- | --- | --- |
|
||
| `--format {gfm,commonmark}` | `gfm` | `gfm`: Pipe-Tabellen, `~~durchgestrichen~~`, `[^fn]`-Fußnoten. `commonmark`: Tabellen als HTML, `<del>`, Fußnoten als Anker-Links |
|
||
| `--extract-media` / `--no-extract-media` | an | Medien nach `media/` extrahieren. Ohne Extraktion stehen Bildreferenzen als Text `[Bild: alt – Pfad]` da (keine kaputten Links) |
|
||
| `--copy-original` / `--no-copy-original` | aus | Original-EPUB nach `source/` kopieren |
|
||
| `--chapter-level N` | `1` | Überschriftenebene (1–6), ab der eine neue Kapiteldatei beginnt |
|
||
| `--toc` / `--no-toc` | an | `README.md` mit Inhaltsverzeichnis, Statistik und Reproduktionsbefehl |
|
||
| `--frontmatter` / `--no-frontmatter` | an | YAML-Frontmatter in jeder Kapiteldatei |
|
||
| `--footnotes {keep-links,end-of-chapter,end-of-book,inline}` | `end-of-chapter` | siehe [Fußnoten](#fußnoten-modi) |
|
||
| `--math {auto,tex,mathml,image}` | `auto` | siehe [Mathematik](#math-modi) |
|
||
| `--images {relative,flat,original-path}` | `relative` | `relative`: `media/images/0001-name.ext`; `flat`: `media/name.ext`; `original-path`: `media/<Pfad im EPUB>` |
|
||
| `--slug-language {de,en,none}` | `de` | Dateinamen: `de` ä→ae, ß→ss; `en` ä→a, ß→ss; `none` Unicode bleibt |
|
||
| `--code-language SPRACHE` | – | Sprachlabel für Codeblöcke ohne erkannte Sprache (z. B. `python`, `c`; Aliase wie `py` erlaubt). Erkannte Labels haben Vorrang; im Bericht `detected_by: "option"` |
|
||
| `--keep-wrap-markers` | aus | Druck-Umbruchzeichen `↪`/`➥` in Codeblöcken stehen lassen. Standard: Fortsetzungszeilen werden wieder zur ursprünglichen Codezeile zusammengefügt (Anzahl im Bericht: `wrap_lines_joined`) |
|
||
| `--boxes {callout,quote,html,plain}` | `callout` | Infokästen (Tipp, Hinweis, Warnung, Exkurs, Merke …): `callout` = `> [!TIP]` mit fettem Titel (GitHub-Alerts, Obsidian-Callouts; anderswo ein Zitatblock), `quote` = Zitatblock mit fettem Titel, `html` = `<div class="box box-tip">`, `plain` = normaler Text wie bisher |
|
||
| `--styles {semantic,html}` | `semantic` | `semantic`: nur bedeutungstragende Gestaltung (Hintergrundmarkierung → `<mark>`). `html`: zusätzlich Farbe, Hintergrund, Schriftgröße, Schriftart, Kapitälchen, Ausrichtung als `<span style>`/`<div style>` und fremdsprachige Passagen als `<span lang>`, jeweils nur, wo sie vom normalen Text des Buches abweichen. Obsidian, VS Code und Pandoc (HTML/PDF/EPUB) zeigen das an, GitHub nicht |
|
||
| `--page-markers {none,visible,comment}` | `none` | Seitenzahlen der Druckausgabe an der Stelle des Seitenwechsels: `visible` = `\[S. 123\]` im Text (zum Zitieren), `comment` = `<!-- S. 123 -->` (unsichtbar, aber durchsuchbar), `none` = weglassen |
|
||
| `--css-emphasis` / `--no-css-emphasis` | an | Fett, kursiv, unterstrichen, durchgestrichen, hoch-/tiefgestellt auch übernehmen, wenn das Buch sie nur per CSS-Klasse setzt (z. B. `span.italic`). Einzelwörter in Monospace-Schrift werden zu Inline-Code; per CSS ausgeblendete Elemente (`display:none`) entfallen. Statistik im Bericht unter `css` |
|
||
| `--keep-invisible-chars` | aus | Nullbreiten-Zeichen und weiche Trennstriche im Fließtext behalten. Standard: entfernen, weil sie Suche und RAG stören (Anzahl im Bericht: `text_cleanup`). Code bleibt immer unverändert |
|
||
| `--keep-html` | aus | Unbekannte/nicht sicher konvertierbare Elemente als bereinigtes HTML erhalten (sonst: Text übernommen + Eintrag in `diagnostics/unsupported-elements.md`) |
|
||
| `--strict` | aus | Abbruch bei kritischen Fehlern (fehlende/unlesbare Spine-Dokumente, unsichere ZIP-Pfade, ungültige Spine-Verweise) |
|
||
| `--force` | aus | Bestehendes Ausgabeverzeichnis ersetzen – siehe [Sicherheit](#sicherheit-und-fehlerbehandlung) |
|
||
| `--verbose`, `-v` | aus | Verarbeitungsschritte und Warnungen auf stderr |
|
||
| `--dry-run` | aus | EPUB analysieren, geplante Kapiteldateien anzeigen, **nichts** schreiben (auch kein `--report`) |
|
||
| `--report DATEI.json` | – | Bericht zusätzlich an diesen Ort schreiben. `OUTPUT/conversion-report.json` entsteht immer. Eine bestehende Datei `DATEI.json` wird ersetzt; Namen, die epub2md selbst im Ausgabeverzeichnis erzeugt (z. B. `manifest.json`), werden abgelehnt (Exit 1). Kann der Bericht nicht geschrieben werden: Exit 1, die Ausgabe ist trotzdem vollständig |
|
||
| `--version` | – | Programmversion |
|
||
|
||
### Rückgabecodes
|
||
|
||
| Code | Bedeutung |
|
||
| --- | --- |
|
||
| 0 | Erfolgreich, ggf. mit nicht-kritischen Warnungen |
|
||
| 1 | Ungültige Parameter, Eingabedatei fehlt/nicht lesbar, Ausgabeverzeichnis belegt oder nicht beschreibbar (Schreibfehler) |
|
||
| 2 | ZIP-/Container-Fehler, DRM, unsichere ZIP-Pfade (mit `--strict`) |
|
||
| 3 | OPF-/Spine-/XHTML-Fehler (z. B. leere Spine; mit `--strict` auch einzelne defekte Dokumente) |
|
||
| 4 | Unerwarteter interner Fehler (Traceback mit `--verbose`) |
|
||
|
||
## Ausgabestruktur
|
||
|
||
```text
|
||
output/mein-buch/
|
||
├── README.md # Titel, Metadaten, Inhaltsverzeichnis, Statistik, Reproduktion
|
||
├── metadata.yaml # Buchmetadaten (nur vorhandene Felder, nichts erfunden)
|
||
├── manifest.json # Kapitel mit Anker-Listen, Wortzahl, Code/Bild/Formel-Zählern – für RAG
|
||
├── conversion-report.json # vollständiger Bericht (Optionen, SHA-256, Warnungen, Formeln, Links …)
|
||
├── source/mein-buch.epub # nur mit --copy-original
|
||
├── media/
|
||
│ ├── images/0001-cover.jpg # stabile, kollisionsfreie Namen; jede Quelle genau einmal
|
||
│ ├── audio/ video/ other/ # Audio, Video, Schriften, CSS …
|
||
├── chapters/
|
||
│ ├── 001-cover.md
|
||
│ ├── 002-vorwort.md
|
||
│ ├── 003-1-einleitung.md
|
||
│ ├── …
|
||
│ └── 999-anmerkungen.md # nur bei --footnotes end-of-book
|
||
├── diagnostics/
|
||
│ ├── warnings.md # Warnungen/Hinweise gruppiert, für Menschen
|
||
│ └── unsupported-elements.md
|
||
└── mein-buch.md # Gesamtdatei, erst nach epub2md_merge.py
|
||
```
|
||
|
||
- Dateinamen: fortlaufend dreistellig nummeriert (`001-…`) plus Slug aus dem Kapiteltitel. Gleiche Titel
|
||
bekommen eindeutige Namen, weil die Nummer immer Teil des Namens ist.
|
||
- Kapitel-Frontmatter (YAML, `---`-Begrenzer): `book_title`, `book_author`, `language`,
|
||
`chapter_number`, `chapter_title`, `source_epub`, `source_href` (+ `source_hrefs`, wenn ein Kapitel aus
|
||
mehreren XHTML-Dateien besteht), `epub_type`, `conversion_status` (`converted` oder
|
||
`converted-with-warnings`).
|
||
- Spezialabschnitte (Glossar, Literatur, Sach-/Personenregister, Anhang, Endnoten) erhalten eigene
|
||
Dateien und werden in `README.md` unter „Besondere Abschnitte“ verlinkt.
|
||
- Nicht-lineare Spine-Dokumente (`linear="no"`, z. B. Bild-Zoomseiten) bleiben in Spine-Reihenfolge
|
||
erhalten und sind im Inhaltsverzeichnis als *Zusatzseite* markiert.
|
||
|
||
## Wie Kapitel gebildet werden
|
||
|
||
1. Die **OPF-Spine** bestimmt die Lesereihenfolge (nicht Dateinamen, nicht das Inhaltsverzeichnis).
|
||
2. Überschriften `h1`…`hN` (N = `--chapter-level`) und semantische Abschnitte (`epub:type`/`role`:
|
||
chapter, part, appendix, glossary, bibliography, index, preface, …) beginnen neue Kapiteldateien,
|
||
auch mitten in einer XHTML-Datei.
|
||
3. Eine XHTML-Datei, die nur mit einer tieferen Überschrift beginnt (z. B. eine Datei pro Abschnitt
|
||
`1.2`), wird an das laufende Kapitel angehängt.
|
||
4. Dateien ohne Überschrift (Cover, Titelei, Widmung) werden eigene Kapitel. Ihr Titel kommt aus
|
||
NAV/NCX, `<title>` oder `epub:type`. Fehlt all das, entscheidet der Inhalt:
|
||
- nur ein Bild am Buchanfang → „Cover“
|
||
- Buchtitel und Autorenname → „Titelseite“
|
||
- ©/ISBN/„urheberrechtlich“ → „Impressum“
|
||
- sonst die ersten acht Wörter
|
||
- erst als letzte Notlösung der Dateiname
|
||
5. Ein HTML-Inhaltsverzeichnis wird übersprungen, wenn NAV/NCX vorhanden ist (keine doppelte Ausgabe).
|
||
Links darauf zeigen auf `README.md`.
|
||
6. Verzeichnisse erhalten immer eine eigene Datei, auch unterhalb von `--chapter-level`, z. B. ein
|
||
`## Quellen` im Anhang. Das gilt nur für Überschriften, die genau einem Verzeichnisnamen entsprechen:
|
||
Literatur(verzeichnis), Quellen(verzeichnis), Bibliografie, References, Glossar, Stichwortverzeichnis,
|
||
Sach-/Personen-/Namensregister, Register, Index. Die nächste Überschrift gleicher oder höherer Ebene
|
||
beendet den Abschnitt. Innerhalb eines Kapitels, das selbst ein Verzeichnis ist, wird nicht weiter
|
||
geteilt.
|
||
7. Überschriftenebenen werden je Datei verschoben: Der Kapiteltitel ist `#`, alle weiteren beginnen bei `##`.
|
||
|
||
Verlagsspezifische Heuristiken: Layout-Tabellen mit Klassen wie `Head1`/`Title_document` werden zu
|
||
Überschriften. `<pre>` mit `<br/>`-Zeilen und `div.*programlisting* > span.pre` werden zu Codeblöcken.
|
||
Literaturlisten mit eigenen Labels (`[BryL06]`) werden nicht doppelt nummeriert.
|
||
|
||
**Tipp:** Hat ein Buch Teile als `h1` und Kapitel als `h2` (typisch bei Romanen), erzeugt
|
||
`--chapter-level 2` eine Datei pro Kapitel. Mit dem Standard `1` entsteht eine Datei pro Teil.
|
||
Ist eine Kapiteldatei größer als 30 000 Wörter und hat sie mindestens zwei `##`-Unterabschnitte, meldet
|
||
epub2md eine Warnung `chapter-large` mit der passenden `--chapter-level`-Empfehlung.
|
||
|
||
## Math-Modi
|
||
|
||
| Modus | MathML | TeX in `alt`/Annotation | SVG-/Bildformel ohne TeX |
|
||
| --- | --- | --- | --- |
|
||
| `auto` | konservativ → TeX `$…$` / `$$…$$`; bei Unklarheit MathML behalten | TeX `$…$` / `$$…$$` | Bild + Warnung |
|
||
| `tex` | wie auto, aber Begrenzer `\(…\)` / `\[…\]` (nie `$`) | TeX `\(…\)` / `\[…\]` | Bild + Warnung |
|
||
| `mathml` | MathML als eingebettetes XML (Namespace normalisiert) | Bild (TeX bleibt im Alt-Text) | Bild |
|
||
| `image` | `altimg`-Bild falls vorhanden, sonst MathML + Warnung | Bild | Bild |
|
||
|
||
- Die MathML→TeX-Umsetzung unterstützt `mi, mn, mo, mtext, mspace, mrow, mstyle, mfrac, msqrt, mroot,
|
||
msup, msub, msubsup, mover, munder, munderover, mfenced, mtable/mtr/mtd, semantics, maction`.
|
||
Bei allem anderen (z. B. `menclose`, `mmultiscripts`, `mlabeledtr`) bleibt das MathML erhalten. Lieber
|
||
MathML als falsches TeX.
|
||
- TeX-Annotationen (`<annotation encoding="application/x-tex">`) haben Vorrang.
|
||
- Im `alt`-Text doppelt escapte Zeichen (`&amp;` → `&`) werden repariert.
|
||
- Gleichungsnummern wie `(3.7)` bleiben als Text sichtbar, Gleichungsanker als `<a id>` erhalten.
|
||
- Formelbilder, die durch TeX ersetzt wurden, werden nicht extrahiert. Die Anzahl steht im Bericht
|
||
(`formula_images_replaced_by_tex`).
|
||
- **Qualität hängt vom EPUB ab:** Echtes MathML/TeX liefert semantische Formeln. Reine SVG-/Bitmap-Formeln
|
||
bleiben Bilder.
|
||
|
||
## Fußnoten-Modi
|
||
|
||
Erkannt werden `epub:type="noteref|footnote|endnote|rearnote"`, `role="doc-noteref|doc-footnote|doc-endnote"`,
|
||
Klassen wie `footnote`/`endnote` und Links auf solche Ziele. Rücklinks innerhalb der Note werden entfernt.
|
||
Die Note erscheint nicht zusätzlich im Fließtext.
|
||
|
||
| Modus | Ergebnis |
|
||
| --- | --- |
|
||
| `end-of-chapter` | `Text[^fn-001]` und `[^fn-001]: …` am Kapitelende. Mehrere Absätze, Code, Formeln und Links bleiben erhalten. Mehrfachverweise nutzen dieselbe Definition |
|
||
| `end-of-book` | `chapters/999-anmerkungen.md` mit Ankern. Verweise `<sup>[1](999-anmerkungen.md#fn-001)</sup>`, Rücklinks `↩` zu jeder Verweisstelle |
|
||
| `inline` | `Text (Anmerkung: …)`. Noten mit Blockinhalt (Code, Listen, Tabellen) fallen auf `end-of-chapter` zurück (Hinweis im Bericht) |
|
||
| `keep-links` | Originalstruktur: Note bleibt an ihrer Stelle, Verweise sind normale Links auf Anker |
|
||
|
||
Mit `--format commonmark` werden Fußnoten als Anker-Links mit Abschnitt „Anmerkungen“ ausgegeben,
|
||
weil CommonMark keine `[^…]`-Syntax kennt.
|
||
|
||
## Qualitätshinweise
|
||
|
||
- **Formelbasierte EPUBs:** Am besten sind EPUBs mit MathML oder TeX im `alt`-Text (z. B. Springer).
|
||
- **SVG-Formeln** bleiben SVG (nie gerastert) und werden im Bericht als Formel markiert.
|
||
- **Layouttabellen / komplexe Tabellen** (`colspan`, `rowspan`, Blockinhalte in Zellen, verschachtelte
|
||
Tabellen, senkrechte Striche `|` in Code, Formeln oder Link-Zielen – GFM würde die Zeile dort in
|
||
zusätzliche Zellen zerlegen) bleiben als bereinigtes HTML im Markdown erhalten, mit Warnung. Einfache Tabellen werden
|
||
GFM-Pipe-Tabellen. Einzellige Layouttabellen werden als normaler Text ausgegeben. Innerhalb solcher
|
||
HTML-Tabellen rendern Markdown-Programme kein Markdown. Formeln stehen dort deshalb als MathML oder als
|
||
Formelbild, Hervorhebungen als HTML-Tags, Fußnotenverweise als HTML-Links (auch bei `--footnotes inline`).
|
||
- **Syntax-Highlighting:** Farb-Spans, `<strong>`/`<em>` in Listings und Zeilennummern werden entfernt.
|
||
Einrückung, Tabs, `<`, `>`, `&`, `$`, `|`, Backslashes und Backticks bleiben exakt erhalten. Die Sprache
|
||
kommt aus Klassen/Attributen (`language-python`, `data-lang`, …) oder starken Inhaltssignalen
|
||
(`#include`, `def …:`, Shebang, …). Ist sie unsicher, bleibt das Label leer. Bei einsprachigen
|
||
Büchern kann `--code-language python` (o. Ä.) alle übrigen Blöcke kennzeichnen. Achtung: Das betrifft
|
||
auch Programmausgaben und Shell-Befehle im Buch.
|
||
- **CSS-Formatierung:** Viele Verlage setzen Hervorhebungen nur über Stylesheets. epub2md wertet die
|
||
CSS-Dateien des Buches aus: Klassen-, Element- und ID-Selektoren, Nachfahren- und Kind-Kombinatoren,
|
||
Spezifität und `style`-Attribute. `@media`-Blöcke und Selektoren mit Pseudoklassen werden ignoriert.
|
||
Direkt aneinanderstoßende gleiche Hervorhebungen werden zusammengefasst (`*x**y*` → `*xy*`).
|
||
- **Farben und Schriften:** Markdown kennt keine Farben, Schriftarten oder -größen. Mit `--styles html`
|
||
bleiben sie als HTML-Stil erhalten. Übernommen wird nur, was vom Grundstil abweicht (häufigste Werte aller
|
||
Absätze; Schriftgröße erst ab ±15 %). Abgesetzte Formeln und Bildabsätze bekommen keine Ausrichtung. Werte
|
||
mit `url(...)`, `expression` usw. werden verworfen.
|
||
- **Seitenzahlen (`--page-markers`):** Erkannt werden `epub:type="pagebreak"`/`role="doc-pagebreak"`
|
||
(Nummer aus `aria-label`, `title` oder Text) und Seitenanker mit IDs wie `page_47`, `page37`, `seite-12`
|
||
oder – nur bei leeren `<a>`/`<span>` – `p17`. In Codeblöcken und Inline-Code stehen keine Marker; ein
|
||
Seitenwechsel in einer Überschrift steht als eigener Absatz davor. Die Anzahl steht im Bericht unter
|
||
`layout.page_markers`.
|
||
- **Infokästen:**
|
||
- **Erkennung:** `aside`/`epub:type` (sidebar, tip, warning …), Klassennamen (box, kasten, tipp, hinweis,
|
||
warnung, achtung, wichtig, merke, exkurs …) oder ein CSS-Rahmen bzw. -Hintergrund um kurzen Fließtext.
|
||
Folgen von Tipp-Absätzen ohne Rahmen werden zu einem Kasten zusammengefasst.
|
||
- **Titel und Symbole:** Kastenüberschriften werden zum fetten Titel und tauchen damit nicht mehr in der
|
||
Gliederung auf. Dekorative Symbolbilder entfallen.
|
||
- **Typ:** Er ergibt sich aus Klasse, Titel oder erstem Wort: TIP, IMPORTANT, WARNING, CAUTION, sonst NOTE.
|
||
- **Ausnahmen:** Umrahmte Codelistings und Bildrahmen gelten nicht als Kasten.
|
||
- **Szenenwechsel und Verse:**
|
||
- Leere Absätze zwischen zwei Textabsätzen werden zu `* * *`; mehrere hintereinander ergeben einen
|
||
Wechsel. Ebenso werden Ornament-Absätze wie `***` oder `⁂` zu `* * *`.
|
||
- Leere `div`s und Absätze, die nur Anker enthalten, gelten als Layout und nicht als Wechsel.
|
||
- Gedichte (`poem`, `verse`, `gedicht` … bzw. `z3998:poem`) werden zum Zitatblock mit einer Zeile pro Vers
|
||
und einer Leerzeile zwischen Strophen.
|
||
- Anzahl im Bericht unter `layout`.
|
||
- **Gescannte Texte:** nur Bilder, kein Text (keine OCR).
|
||
- **DRM:** wird erkannt und abgelehnt.
|
||
- **Register:** Buchstabengruppen werden zu `##`-Überschriften, Untereinträge erhalten `– `. Interne
|
||
Registerlinks werden auf Kapiteldatei + Anker umgeschrieben. Seitenzahlen bleiben als Text sichtbar.
|
||
|
||
## Gesamtdatei erzeugen: `epub2md_merge.py`
|
||
|
||
Fügt die Kapiteldateien einer epub2md-Ausgabe in Lesereihenfolge (aus `manifest.json`) zu **einer**
|
||
Markdown-Datei zusammen. Die Datei heißt wie das Original-EPUB, aber mit der Endung `.md`.
|
||
|
||
```bash
|
||
python3 epub2md_merge.py output/programmierhandbuch
|
||
# → output/programmierhandbuch/<Original-Name>.md
|
||
|
||
python3 epub2md_merge.py output/programmierhandbuch -o buecher/handbuch.md --toc --force
|
||
# alternativ: python3 -m epub2md.merge …
|
||
```
|
||
|
||
Eingabe ist ein von `epub2md.py` erzeugtes Verzeichnis. Die Reihenfolge stammt aus `manifest.json`,
|
||
der Dateiname aus `conversion-report.json`. Fehlen diese Dateien, werden `chapters/*.md` alphabetisch
|
||
verwendet und das Verzeichnis gibt den Namen vor.
|
||
|
||
| Option | Wirkung |
|
||
| --- | --- |
|
||
| `-o`, `--output DATEI.md` | anderer Zielort; Medienpfade werden relativ zum Ziel umgeschrieben |
|
||
| `--frontmatter` / `--no-frontmatter` | ein YAML-Kopf mit Buchmetadaten (`title`, `author`, `lang`, …) für Pandoc (Standard: an) |
|
||
| `--toc` | Inhaltsverzeichnis mit Sprunglinks auf die Kapitel einfügen |
|
||
| `--force` | bestehende Zieldatei überschreiben (sonst Abbruch mit Exit 1) |
|
||
| `--version` | Programmversion |
|
||
|
||
Beim Zusammenfügen passiert Folgendes:
|
||
|
||
- Die Frontmatter der einzelnen Kapitel wird entfernt.
|
||
- Jedes Kapitel erhält einen Startanker (`<a id="003-kapitelname"></a>`).
|
||
- Links zwischen Kapiteln (`003-x.md#anker`) werden zu Sprunglinks in der Gesamtdatei.
|
||
- Anker, die in mehreren Kapiteln vorkommen (z. B. `Sec1` in jedem Kapitel eines Springer-Buchs), werden
|
||
ab dem zweiten Vorkommen in `c012-Sec1` umbenannt, samt aller Links darauf.
|
||
- Medienpfade (`../media/…`) werden auf den Ort der Gesamtdatei umgeschrieben.
|
||
- Fußnotendefinitionen, die in mehreren Kapiteln stehen, erscheinen nur einmal.
|
||
- Codeblöcke und Inline-Code werden nie verändert.
|
||
- Kapitelüberschriften bleiben `#`, jedes Kapitel beginnt also mit einer H1. Pandoc behandelt sie als
|
||
Kapitel.
|
||
- Der Buch-Kopf enthält nur Felder, die im EPUB vorhanden sind (`title`, `subtitle`, `author`, `lang`,
|
||
`publisher`, `date`, `identifier`, `rights`, `description`).
|
||
|
||
Am Ende werden alle Links der Gesamtdatei geprüft. Ein Link auf einen nicht vorhandenen Anker zeigt auf
|
||
den Kapitelanfang und erzeugt eine Warnung auf stderr. Die Zusammenfassung nennt Kapitelanzahl,
|
||
umbenannte Anker, entfernte doppelte Fußnoten und ungültige Links.
|
||
|
||
Rückgabecodes: `0` Erfolg, `1` Verzeichnis ungültig/leer, Kapiteldatei fehlt oder Zieldatei existiert,
|
||
`4` unerwarteter interner Fehler.
|
||
|
||
## Weiterverarbeitung mit Pandoc
|
||
|
||
Am einfachsten geht das über die Gesamtdatei aus `epub2md_merge.py`. Sie hat einen Buch-Kopf und
|
||
konsistente Anker:
|
||
|
||
```bash
|
||
cd output/programmierbuch # Medienpfade sind relativ zur Gesamtdatei
|
||
pandoc programmierbuch.md --toc -s -o programmierbuch.html
|
||
pandoc programmierbuch.md --toc -o programmierbuch.epub
|
||
|
||
cd output/mathematikbuch
|
||
pandoc mathematikbuch.md --mathml -s -o mathematikbuch.html
|
||
# PDF über den Zwischenschritt HTML (siehe unten)
|
||
pandoc mathematikbuch.html --toc --pdf-engine=xelatex -o mathematikbuch.pdf
|
||
```
|
||
|
||
**PDF immer über HTML erzeugen.** Beim direkten Weg Markdown → LaTeX verwirft Pandoc allen rohen HTML-Code.
|
||
Dabei gehen HTML-Tabellen samt Inhalt verloren, und `<a id>`-Sprungziele fehlen, sodass interne Links im PDF
|
||
ins Leere zeigen. Liest Pandoc dagegen die HTML-Fassung, macht es daraus echte Tabellen und Sprungziele.
|
||
Getestet mit dem C-Buch (405 Seiten, 34 Tabellen, 728 von 755 internen Links funktionieren; die übrigen
|
||
stehen in Codeblöcken innerhalb von Tabellen). Die `README.md` jedes konvertierten Buches enthält den
|
||
passenden Befehl.
|
||
|
||
Alternativ direkt aus den Kapiteldateien:
|
||
|
||
```bash
|
||
pandoc output/programmierbuch/chapters/*.md \
|
||
--metadata-file=output/programmierbuch/metadata.yaml \
|
||
-o programmierbuch.html
|
||
```
|
||
|
||
- Für HTML mit Formeln `--mathml`, `--mathjax` oder `--katex` verwenden. Pandocs Standard-HTML kann
|
||
Matrizen nicht in Unicode umsetzen, meldet „Could not convert TeX math“ und belässt das TeX.
|
||
- Ausgaben mit `--math tex` (`\(…\)`) brauchen `-f markdown+tex_math_single_backslash`.
|
||
- Das YAML-Frontmatter jeder Kapiteldatei wird von Pandoc als Metadaten gelesen. Wer die Kapiteldateien
|
||
direkt zusammen verarbeitet, sollte mit `--no-frontmatter` konvertieren oder die Gesamtdatei verwenden.
|
||
- Einzelne TeX-Befehle aus EPUBs (z. B. `\O` oder `@`-Konstrukte) kennt Pandocs Formelparser nicht. Er
|
||
belässt sie als TeX, mit der Warnung „Could not convert TeX math“.
|
||
|
||
## Bericht und Diagnose
|
||
|
||
`conversion-report.json` enthält u. a. Programmversion, Zeitstempel, Eingabedatei mit SHA-256, EPUB-Version,
|
||
Optionen, Metadaten, Kapitel mit Statistik, alle Medien, Codeblöcke (Sprache + Erkennungsart),
|
||
Formeln (Herkunft + Darstellung), Fußnoten (Modus, erkannt/referenziert/ausgegeben), Spezialabschnitte,
|
||
Warnungen, nicht-fatale Fehler, nicht aufgelöste interne Links, nicht unterstützte Elemente,
|
||
übersprungene Dokumente und das Ergebnis eines **Linkchecks über die erzeugte Ausgabe**. Dabei wird
|
||
jeder relative Link und jedes Bild auf Zieldatei und Anker geprüft.
|
||
|
||
`status` ist `success`, `success-with-warnings` oder `success-with-errors`. Bei Warnungen wird nie
|
||
Verlustfreiheit behauptet (`lossless: false`).
|
||
|
||
## Sicherheit und Fehlerbehandlung
|
||
|
||
- Das EPUB wird **nie** pauschal entpackt. Einträge werden einzeln gelesen. Absolute Pfade und `..`
|
||
werden ignoriert und gemeldet (mit `--strict`: Abbruch). Alle Ausgabedateien werden vor dem Schreiben
|
||
gegen das Zielverzeichnis geprüft.
|
||
- Schutz gegen ZIP-Bomben: Größenlimit je Eintrag und Prüfung des Kompressionsverhältnisses.
|
||
- XML wird mit lxml **ohne DTD-Laden, ohne Entity-Auflösung und ohne Netzwerk** geparst (kein XXE).
|
||
HTML-Entities (` ` …) werden vorab in Zeichenreferenzen umgesetzt.
|
||
- Ungültiges UTF-8 wird mit Ersatzzeichen gelesen und als Warnung gemeldet.
|
||
- Keine Netzwerkzugriffe, keine Shell-Aufrufe, keine Telemetrie.
|
||
- Die Ausgabe entsteht in einem temporären Verzeichnis neben dem Ziel und wird erst am Ende an ihren Platz
|
||
verschoben. Bei Fehlern wird das temporäre Verzeichnis gelöscht, und es bleibt keine halbe Ausgabe zurück.
|
||
- **Bestehendes Ausgabeverzeichnis:** Ist es nicht leer, bricht das Programm ab (Exit 1). `--force` ersetzt
|
||
es nur, wenn es eine frühere epub2md-Ausgabe ist (`conversion-report.json`/`manifest.json` vorhanden),
|
||
nicht das Arbeits- oder Home-Verzeichnis ist und die Eingabedatei nicht darin liegt.
|
||
|
||
## Bekannte Einschränkungen
|
||
|
||
- Titel überschriftsloser Seiten werden aus dem Inhalt abgeleitet (Cover, Titelseite, Impressum, erste
|
||
Wörter). Das ist eine Heuristik und trifft nicht jede Seite.
|
||
- Aneinanderstoßende Feld-Spans ohne Leerraum (z. B. Autor | Titel | Reihe in Springer-Kapitelköpfen)
|
||
erhalten ein trennendes Leerzeichen. Das gilt nur bei unterschiedlichen Klassen und wenn rechts ein
|
||
Großbuchstabe oder eine Ziffer folgt. Andere direkt aneinanderstoßende Inline-Elemente bleiben
|
||
unverändert, damit Initialen und Wortteile nicht zerrissen werden.
|
||
- Fehlerhafte Einrückungen der Vorlage werden zeichengetreu übernommen. Druck-Fortsetzungszeichen
|
||
(`↪`/`➥`) am Zeilenanfang werden dagegen standardmäßig aufgelöst (siehe `--keep-wrap-markers`).
|
||
- Formelbilder ohne TeX-Alternativtext bleiben Bilder.
|
||
- Audio/Video sind implementiert, aber nur mit synthetischen Daten geprüft. Bildbände sind mit einem
|
||
realen Buch geprüft (434 Bilder, 246 Figuren, ausführliche Alternativtexte).
|
||
- Rein per CSS erzeugte Inhalte (`::before`/`content:`, z. B. automatische Schrittnummern) werden nicht
|
||
übernommen, weil CSS nicht ausgewertet wird.
|
||
|
||
## Architektur
|
||
|
||
```text
|
||
epub2md.py Einstiegspunkt
|
||
epub2md_merge.py Einstiegspunkt: Gesamtdatei aus einer Ausgabe erzeugen (epub2md/merge.py)
|
||
epub2md/
|
||
├── cli.py argparse, Exit-Codes
|
||
├── pipeline.py Ablaufsteuerung, Schreiben (atomar), Bericht/Manifest
|
||
├── epub_reader.py ZIP-Sicherheit, DRM-Check, container.xml, OPF, NAV, NCX, Guide
|
||
├── normalize.py Laden der Spine-Dokumente, Layout-Heuristiken, Seitenumbrüche, HTML-TOC
|
||
├── footnotes.py Erkennung/Auslagerung von Fuß- und Endnoten
|
||
├── chapters.py Kapitelbildung (Spine + Überschriften + epub:type), Klassifikation
|
||
├── references.py Ziel-IDs → Kapiteldatei + Anker, Link-Auflösung
|
||
├── html_converter.py rekursiver XHTML→Markdown-Renderer (Blöcke, Inline, Tabellen, Listen, Bilder)
|
||
├── codeblocks.py Codeerkennung, exakte Textextraktion, Spracherkennung, Fences
|
||
├── mathml.py konservative MathML→TeX-Konvertierung, TeX-Erkennung
|
||
├── media.py Medienregistry mit stabilen Namen
|
||
├── linkcheck.py Validierung aller relativen Links/Anker der Ausgabe
|
||
├── merge.py Gesamtdatei: Reihenfolge, Anker-Umbenennung, Pfade, Fußnoten-Deduplizierung
|
||
├── writer.py README, metadata.yaml, Frontmatter, Diagnose-Markdown
|
||
├── diagnostics.py zentrale Sammlung von Warnungen/Fehlern
|
||
├── models.py Dataclasses (Kapitel, Medien, Noten, Metadaten …)
|
||
└── utils.py Slugs, YAML-Emitter, Pfadhelfer
|
||
```
|
||
|
||
Statt BeautifulSoup wird direkt lxml verwendet: Es ist für die XHTML-Bäume ausreichend, schneller und
|
||
ermöglicht die sichere Parser-Konfiguration (XXE). Die Konvertierung ist deterministisch. Gleiche
|
||
Eingabe und Optionen ergeben identische Kapiteldateien.
|
||
|
||
## Tests
|
||
|
||
```bash
|
||
pip install -r requirements-dev.txt
|
||
python3 -m pytest -q
|
||
ruff check epub2md tests && mypy epub2md --ignore-missing-imports # optional, falls installiert
|
||
```
|
||
|
||
Die Tests erzeugen synthetische Minimal-EPUBs (EPUB 2/3, NAV/NCX, Code, Tabellen, MathML, SVG,
|
||
Fußnoten, Glossar/Literatur/Register, unsichere ZIP-Pfade, DRM, XXE, defekte Container, Kodierung) und
|
||
prüfen die tatsächliche CLI-Ausgabe. `tests/test_merge.py` prüft die Gesamtdatei: Reihenfolge,
|
||
Anker-Umbenennung, Pfade, Fußnoten, unveränderter Code und Überschreibschutz. Liegen EPUBs unter `Beispiele/`, werden sie zusätzlich auf
|
||
Struktur-Invarianten geprüft: keine kaputten Links, balancierte Code-Fences, gültiges YAML, alle Medien
|
||
vorhanden. Dabei werden keine Buchtexte verglichen. `Beispiele/` ist per `.gitignore` vom Repository
|
||
ausgeschlossen.
|