diff --git a/CLAUDE.md b/CLAUDE.md index 353b813..53f8adc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,7 +1,7 @@ # CLAUDE.md PDF → Markdown → EPUB für den PocketBook-Reader (digitale und gescannte PDFs). -Aktuelles Programm: `pdf2md.py` (Einzeldatei; Name bleibt fest, keine Versionsnummer im Dateinamen). v1–v4 liegen in `History/` (Historie, nicht ändern). +Aktuelles Programm: `pdf2md.py` (Einzeldatei; Name bleibt fest, keine Versionsnummer im Dateinamen). Frühere Stände (v1–v4) nur noch in der Git-Historie. ## Befehle diff --git a/History/pdf2md_v1.py b/History/pdf2md_v1.py deleted file mode 100644 index b462733..0000000 --- a/History/pdf2md_v1.py +++ /dev/null @@ -1,146 +0,0 @@ -#!/usr/bin/env python3 -"""PDF nach Markdown konvertieren und weiche Zeilenumbrüche entfernen.""" - -import argparse -import re -import sys -from pathlib import Path - -import pymupdf4llm - - -HEADING = re.compile(r"^\s{0,3}#{1,6}\s+") -LIST_ITEM = re.compile(r"^\s{0,3}(?:[-+*]|\d+[.)])\s+") -BLOCKQUOTE = re.compile(r"^\s{0,3}>\s?") -FENCE = re.compile(r"^\s{0,3}(`{3,}|~{3,})") -RULE = re.compile(r"^\s{0,3}(?:-\s*){3,}$|^\s{0,3}(?:\*\s*){3,}$") -IMAGE = re.compile(r"^\s*!\[.*?\]\(.*\)\s*$") -HTML_BLOCK = re.compile(r"^\s* bool: - """Zeilen, die nicht mit dem vorherigen Fließtext verbunden werden.""" - return bool( - HEADING.match(line) - or LIST_ITEM.match(line) - or BLOCKQUOTE.match(line) - or RULE.match(line) - or IMAGE.match(line) - or HTML_BLOCK.match(line) - or line.lstrip().startswith("|") - or line.startswith((" ", "\t")) - ) - - -def normalize_markdown(markdown: str) -> str: - result = [] - paragraph = [] - in_fence = False - fence_char = "" - fence_length = 0 - - def flush_paragraph(): - if paragraph: - result.append(" ".join(paragraph)) - paragraph.clear() - - for original in markdown.replace("\r\n", "\n").replace("\r", "\n").split("\n"): - line = original.rstrip() - marker = FENCE.match(line) - - if in_fence: - result.append(line) - if marker: - token = marker.group(1) - if token[0] == fence_char and len(token) >= fence_length: - in_fence = False - continue - - if marker: - flush_paragraph() - result.append(line) - in_fence = True - fence_char = marker.group(1)[0] - fence_length = len(marker.group(1)) - continue - - if not line.strip(): - flush_paragraph() - if result and result[-1] != "": - result.append("") - continue - - if is_structural(line): - flush_paragraph() - result.append(line) - continue - - paragraph.append(line.strip()) - - flush_paragraph() - - return "\n".join(result).strip() + "\n" - - -def main() -> int: - parser = argparse.ArgumentParser( - description="PDF in Markdown umwandeln; Zeilen innerhalb von Absätzen verbinden." - ) - parser.add_argument("pdf", type=Path, help="Eingabe-PDF") - parser.add_argument( - "markdown", - type=Path, - nargs="?", - help="Ausgabe-Datei; Standard: gleicher Name mit .md", - ) - parser.add_argument( - "--ocr-language", - default="deu", - help="Tesseract-Sprachcode bei erforderlicher OCR (Standard: deu)", - ) - parser.add_argument( - "--no-ocr", - action="store_true", - help="OCR abschalten, wenn die PDF sicher durchsuchbaren Text enthält", - ) - parser.add_argument( - "--keep-headers", - action="store_true", - help="Wiederkehrende Seitenköpfe in die Ausgabe übernehmen", - ) - parser.add_argument( - "--keep-footers", - action="store_true", - help="Wiederkehrende Seitenfüße in die Ausgabe übernehmen", - ) - args = parser.parse_args() - - source = args.pdf.expanduser() - target = (args.markdown or source.with_suffix(".md")).expanduser() - - if not source.is_file(): - parser.error(f"Eingabedatei nicht gefunden: {source}") - if source.resolve() == target.resolve(): - parser.error("Eingabe- und Ausgabedatei müssen verschieden sein.") - - try: - markdown = pymupdf4llm.to_markdown( - str(source), - header=args.keep_headers, - footer=args.keep_footers, - use_ocr=not args.no_ocr, - ocr_language=args.ocr_language, - ) - target.parent.mkdir(parents=True, exist_ok=True) - target.write_text(normalize_markdown(markdown), encoding="utf-8") - except Exception as exc: - print(f"Fehler bei der Konvertierung: {exc}", file=sys.stderr) - return 1 - - print(f"Markdown geschrieben: {target}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) - diff --git a/History/pdf2md_v2.py b/History/pdf2md_v2.py deleted file mode 100644 index ca5d53f..0000000 --- a/History/pdf2md_v2.py +++ /dev/null @@ -1,181 +0,0 @@ -#!/usr/bin/env python3 -"""PDF nach Markdown konvertieren und Absätze anhand von Satzenden verbinden.""" - -import argparse -import re -import sys -from pathlib import Path - -import pymupdf4llm -from pymupdf4llm.ocr import tesseract_api - - -HEADING = re.compile(r"^\s{0,3}#{1,6}\s+") -LIST_ITEM = re.compile(r"^\s{0,3}(?:[-+*]|\d+[.)])\s+") -BLOCKQUOTE = re.compile(r"^\s{0,3}>\s?") -FENCE = re.compile(r"^\s{0,3}(`{3,}|~{3,})") -RULE = re.compile(r"^\s{0,3}(?:-\s*){3,}$|^\s{0,3}(?:\*\s*){3,}$") -IMAGE = re.compile(r"^\s*!\[.*?\]\(.*\)\s*$") -HTML_BLOCK = re.compile( - r"^\s* bool: - return bool(SENTENCE_END.search(text.rstrip())) - - -def is_structural(line: str) -> bool: - """Markdown-Zeilen, die nicht in Fließtext aufgehen sollen.""" - return bool( - LIST_ITEM.match(line) - or BLOCKQUOTE.match(line) - or RULE.match(line) - or IMAGE.match(line) - or HTML_BLOCK.match(line) - or line.lstrip().startswith("|") - or line.startswith((" ", "\t")) - ) - - -def normalize_markdown(markdown: str) -> str: - result: list[str] = [] - paragraph: list[str] = [] - - in_fence = False - fence_char = "" - fence_length = 0 - - def add_blank_line() -> None: - if result and result[-1] != "": - result.append("") - - def flush_paragraph() -> None: - if paragraph: - result.append(" ".join(paragraph)) - paragraph.clear() - - for original in markdown.replace("\r\n", "\n").replace("\r", "\n").split("\n"): - line = original.rstrip() - marker = FENCE.match(line) - - if in_fence: - result.append(line) - if marker: - token = marker.group(1) - if token[0] == fence_char and len(token) >= fence_length: - in_fence = False - continue - - if marker: - flush_paragraph() - add_blank_line() - result.append(line) - in_fence = True - fence_char = marker.group(1)[0] - fence_length = len(marker.group(1)) - continue - - if HEADING.match(line): - flush_paragraph() - add_blank_line() - result.append(line) - result.append("") - continue - - if is_structural(line): - flush_paragraph() - result.append(line) - continue - - if not line.strip(): - # Eine Leerzeile ist nur nach einem Satzende eine Absatzgrenze. - # Andernfalls bleibt paragraph bestehen und die nächste - # Textzeile wird mit einem Leerzeichen angehängt. - if paragraph and ends_sentence(paragraph[-1]): - flush_paragraph() - add_blank_line() - elif not paragraph: - add_blank_line() - continue - - paragraph.append(line.strip()) - - flush_paragraph() - return "\n".join(result).strip() + "\n" - - -def main() -> int: - parser = argparse.ArgumentParser( - description=( - "PDF in Markdown umwandeln; Absätze nur nach Satzende trennen." - ) - ) - parser.add_argument("pdf", type=Path, help="Eingabe-PDF") - parser.add_argument( - "markdown", - type=Path, - nargs="?", - help="Ausgabe-Datei; Standard: gleicher Name mit .md", - ) - parser.add_argument( - "--ocr-language", - default="deu", - help="Tesseract-Sprachcode bei OCR (Standard: deu)", - ) - parser.add_argument( - "--no-ocr", - action="store_true", - help="OCR deaktivieren", - ) - parser.add_argument( - "--keep-headers", - action="store_true", - help="Wiederkehrende Seitenköpfe übernehmen", - ) - parser.add_argument( - "--keep-footers", - action="store_true", - help="Wiederkehrende Seitenfüße übernehmen", - ) - args = parser.parse_args() - - source = args.pdf.expanduser() - target = (args.markdown or source.with_suffix(".md")).expanduser() - - if not source.is_file(): - parser.error(f"Eingabedatei nicht gefunden: {source}") - - if source.resolve() == target.resolve(): - parser.error("Eingabe- und Ausgabedatei müssen verschieden sein.") - - try: - markdown = pymupdf4llm.to_markdown( - str(source), - header=args.keep_headers, - footer=args.keep_footers, - use_ocr=not args.no_ocr, - ocr_language=args.ocr_language, - ocr_function=( - tesseract_api.exec_ocr if not args.no_ocr else None - ), - ) - - target.parent.mkdir(parents=True, exist_ok=True) - target.write_text(normalize_markdown(markdown), encoding="utf-8") - - except Exception as exc: - print(f"Fehler bei der Konvertierung: {exc}", file=sys.stderr) - return 1 - - print(f"Markdown geschrieben: {target}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) - diff --git a/History/pdf2md_v3.py b/History/pdf2md_v3.py deleted file mode 100644 index 1bf2bd3..0000000 --- a/History/pdf2md_v3.py +++ /dev/null @@ -1,249 +0,0 @@ -#!/usr/bin/env python3 -"""PDF nach Markdown konvertieren und Fließtext nach Satzenden gliedern.""" - -import argparse -import re -import sys -from pathlib import Path - -import pymupdf4llm -from pymupdf4llm.ocr import tesseract_api - - -HEADING = re.compile(r"^\s{0,3}#{1,6}\s+\S") - -# Punkt, Fragezeichen, Ausrufezeichen oder Auslassungspunkte; -# danach dürfen schließende Anführungszeichen/Klammern folgen. -SENTENCE_END = re.compile(r"""[.!?…]+[”’"'»›)\]]*$""") - -FENCE = re.compile(r"^\s{0,3}(`{3,}|~{3,})") -LIST_ITEM = re.compile(r"^\s{0,3}(?:[-+*]|\d+[.)])\s+") -BLOCKQUOTE = re.compile(r"^\s{0,3}>\s?") -RULE = re.compile(r"^\s{0,3}(?:-\s*){3,}$|^\s{0,3}(?:\*\s*){3,}$") -IMAGE = re.compile(r"^\s*!\[.*?\]\(.*\)\s*$") - -# Beispiel: -# **==> picture [426 x 480] intentionally omitted <==** -OMITTED_IMAGE = re.compile( - r"^\s*(?:\*\*|__)?\s*==>\s*" - r"(?:picture|image|bild)\b.*?" - r"(?:intentionally omitted|omitted|ausgelassen)\s*" - r"<==\s*(?:\*\*|__)?\s*$", - re.IGNORECASE, -) - -CONTENTS_TITLE = re.compile( - r"^\s*(?:#{1,6}\s+)?" - r"(?:inhaltsverzeichnis|inhalt|contents|table of contents)" - r"\s*$", - re.IGNORECASE, -) - -# Inhaltsverzeichnis mit Füllpunkten oder Abstand vor einer Seitenzahl: -# Kapitel 1 .......... 12 -# Kapitel 1 12 -# Kapitel 1 xiv -TOC_WITH_PAGE = re.compile( - r"^\s*(?:[-*]\s+)?\S.+?" - r"(?:\.{2,}|\s{2,})" - r"(?:\d+|[ivxlcdm]+)\s*$", - re.IGNORECASE, -) - -# Einträge ohne Seitenzahl, sofern bereits ein Inhaltsverzeichnis erkannt wurde: -# 1 Einleitung -# 1.2 Vorgeschichte -# Kapitel 3 – Die Reise -TOC_NUMBERED = re.compile( - r"^\s*(?:[-*]\s+)?" - r"(?:\d+(?:\.\d+)*\.?\s+|Kapitel\s+\d+\b|Chapter\s+\d+\b)" - r".+", - re.IGNORECASE, -) - -HTML_BLOCK = re.compile( - r"^\s* bool: - return bool(SENTENCE_END.search(text.rstrip())) - - -def is_other_structure(line: str) -> bool: - """Markdown-Strukturen, die nicht zu Fließtext verbunden werden.""" - return bool( - LIST_ITEM.match(line) - or BLOCKQUOTE.match(line) - or RULE.match(line) - or IMAGE.match(line) - or HTML_BLOCK.match(line) - or line.lstrip().startswith("|") - or line.startswith((" ", "\t")) - ) - - -def normalize_markdown(markdown: str) -> str: - result: list[str] = [] - paragraph: list[str] = [] - - in_fence = False - fence_char = "" - fence_length = 0 - in_contents = False - - def blank_line() -> None: - if result and result[-1] != "": - result.append("") - - def flush_paragraph() -> None: - if paragraph: - result.append(" ".join(paragraph)) - paragraph.clear() - - def standalone(line: str) -> None: - flush_paragraph() - blank_line() - result.append(line) - result.append("") - - lines = markdown.replace("\r\n", "\n").replace("\r", "\n").split("\n") - - for original in lines: - line = original.rstrip() - marker = FENCE.match(line) - - if in_fence: - result.append(line) - if marker: - token = marker.group(1) - if token[0] == fence_char and len(token) >= fence_length: - in_fence = False - continue - - if marker: - flush_paragraph() - blank_line() - result.append(line) - in_fence = True - fence_char = marker.group(1)[0] - fence_length = len(marker.group(1)) - continue - - if not line.strip(): - # Leerzeile nur dann als Absatzgrenze behandeln, - # wenn der bisherige Fließtext mit einem Satzende endet. - if paragraph and ends_sentence(paragraph[-1]): - flush_paragraph() - blank_line() - elif not paragraph: - blank_line() - continue - - if CONTENTS_TITLE.match(line): - standalone(line) - in_contents = True - continue - - if HEADING.match(line): - standalone(line) - in_contents = False - continue - - if OMITTED_IMAGE.match(line): - standalone(line) - continue - - if TOC_WITH_PAGE.match(line): - standalone(line) - continue - - if in_contents and TOC_NUMBERED.match(line): - standalone(line) - continue - - if in_contents and ends_sentence(line): - # Erster normaler Satz nach einem erkannten Inhaltsverzeichnis. - in_contents = False - - if is_other_structure(line): - flush_paragraph() - result.append(line) - continue - - # Einfache Zeilenumbrüche und nicht bestätigte Absatzgrenzen - # werden beim späteren Flush durch Leerzeichen ersetzt. - paragraph.append(line.strip()) - - flush_paragraph() - return "\n".join(result).strip() + "\n" - - -def main() -> int: - parser = argparse.ArgumentParser( - description="PDF in Markdown umwandeln; Fließtext nur nach Satzende trennen." - ) - parser.add_argument("pdf", type=Path, help="Eingabe-PDF") - parser.add_argument( - "markdown", - type=Path, - nargs="?", - help="Ausgabe-Datei; Standard: gleicher Name mit .md", - ) - parser.add_argument( - "--ocr-language", - default="deu", - help="Tesseract-Sprachcode bei OCR (Standard: deu)", - ) - parser.add_argument( - "--no-ocr", - action="store_true", - help="OCR deaktivieren", - ) - parser.add_argument( - "--keep-headers", - action="store_true", - help="Wiederkehrende Seitenköpfe übernehmen", - ) - parser.add_argument( - "--keep-footers", - action="store_true", - help="Wiederkehrende Seitenfüße übernehmen", - ) - args = parser.parse_args() - - source = args.pdf.expanduser() - target = (args.markdown or source.with_suffix(".md")).expanduser() - - if not source.is_file(): - parser.error(f"Eingabedatei nicht gefunden: {source}") - if source.resolve() == target.resolve(): - parser.error("Eingabe- und Ausgabedatei müssen verschieden sein.") - - try: - markdown = pymupdf4llm.to_markdown( - str(source), - header=args.keep_headers, - footer=args.keep_footers, - use_ocr=not args.no_ocr, - ocr_language=args.ocr_language, - ocr_function=( - tesseract_api.exec_ocr if not args.no_ocr else None - ), - ) - - target.parent.mkdir(parents=True, exist_ok=True) - target.write_text(normalize_markdown(markdown), encoding="utf-8") - - except Exception as exc: - print(f"Fehler bei der Konvertierung: {exc}", file=sys.stderr) - return 1 - - print(f"Markdown geschrieben: {target}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) - diff --git a/History/pdf2md_v4.py b/History/pdf2md_v4.py deleted file mode 100644 index 7a921ad..0000000 --- a/History/pdf2md_v4.py +++ /dev/null @@ -1,269 +0,0 @@ -#!/usr/bin/env python3 -"""PDF nach Markdown konvertieren, Absätze gliedern und optional Bilder speichern.""" - -import argparse -import re -import sys -from pathlib import Path - -import pymupdf4llm -from pymupdf4llm.ocr import tesseract_api - - -HEADING = re.compile(r"^\s{0,3}#{1,6}\s+\S") - -# Satzzeichen, optional gefolgt von schließenden Anführungszeichen/Klammern. -SENTENCE_END = re.compile(r"""[.!?…]+[”’"'»›)\]]*$""") - -FENCE = re.compile(r"^\s{0,3}(`{3,}|~{3,})") -LIST_ITEM = re.compile(r"^\s{0,3}(?:[-+*]|\d+[.)])\s+") -BLOCKQUOTE = re.compile(r"^\s{0,3}>\s?") -RULE = re.compile(r"^\s{0,3}(?:-\s*){3,}$|^\s{0,3}(?:\*\s*){3,}$") -IMAGE = re.compile(r"^\s*!\[.*?\]\(.*\)\s*$") - -# Beispiel: **==> picture [426 x 480] intentionally omitted <==** -OMITTED_IMAGE = re.compile( - r"^\s*(?:\*\*|__)?\s*==>\s*" - r"(?:picture|image|bild)\b.*?" - r"(?:intentionally omitted|omitted|ausgelassen)\s*" - r"<==\s*(?:\*\*|__)?\s*$", - re.IGNORECASE, -) - -CONTENTS_TITLE = re.compile( - r"^\s*(?:#{1,6}\s+)?" - r"(?:inhaltsverzeichnis|inhalt|contents|table of contents)" - r"\s*$", - re.IGNORECASE, -) - -TOC_WITH_PAGE = re.compile( - r"^\s*(?:[-*]\s+)?\S.+?" - r"(?:\.{2,}|\s{2,})" - r"(?:\d+|[ivxlcdm]+)\s*$", - re.IGNORECASE, -) - -TOC_NUMBERED = re.compile( - r"^\s*(?:[-*]\s+)?" - r"(?:\d+(?:\.\d+)*\.?\s+|Kapitel\s+\d+\b|Chapter\s+\d+\b)" - r".+", - re.IGNORECASE, -) - -HTML_BLOCK = re.compile( - r"^\s* bool: - return bool(SENTENCE_END.search(text.rstrip())) - - -def is_other_structure(line: str) -> bool: - """Markdown-Strukturen, die nicht zu Fließtext verbunden werden.""" - return bool( - LIST_ITEM.match(line) - or BLOCKQUOTE.match(line) - or RULE.match(line) - or IMAGE.match(line) - or HTML_BLOCK.match(line) - or line.lstrip().startswith("|") - or line.startswith((" ", "\t")) - ) - - -def normalize_markdown(markdown: str) -> str: - result: list[str] = [] - paragraph: list[str] = [] - - in_fence = False - fence_char = "" - fence_length = 0 - in_contents = False - - def blank_line() -> None: - if result and result[-1] != "": - result.append("") - - def flush_paragraph() -> None: - if paragraph: - result.append(" ".join(paragraph)) - paragraph.clear() - - def standalone(line: str) -> None: - flush_paragraph() - blank_line() - result.append(line) - result.append("") - - lines = markdown.replace("\r\n", "\n").replace("\r", "\n").split("\n") - - for original in lines: - line = original.rstrip() - marker = FENCE.match(line) - - if in_fence: - result.append(line) - if marker: - token = marker.group(1) - if token[0] == fence_char and len(token) >= fence_length: - in_fence = False - continue - - if marker: - flush_paragraph() - blank_line() - result.append(line) - in_fence = True - fence_char = marker.group(1)[0] - fence_length = len(marker.group(1)) - continue - - if not line.strip(): - if paragraph and ends_sentence(paragraph[-1]): - flush_paragraph() - blank_line() - elif not paragraph: - blank_line() - continue - - if CONTENTS_TITLE.match(line): - standalone(line) - in_contents = True - continue - - if HEADING.match(line): - standalone(line) - in_contents = False - continue - - if OMITTED_IMAGE.match(line): - standalone(line) - continue - - if TOC_WITH_PAGE.match(line): - standalone(line) - continue - - if in_contents and TOC_NUMBERED.match(line): - standalone(line) - continue - - if in_contents and ends_sentence(line): - in_contents = False - - if is_other_structure(line): - flush_paragraph() - result.append(line) - continue - - paragraph.append(line.strip()) - - flush_paragraph() - return "\n".join(result).strip() + "\n" - - -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 - - return markdown.replace( - absolute_dir + "/", - relative_dir + "/", - ) - - -def main() -> int: - parser = argparse.ArgumentParser( - description=( - "PDF in Markdown umwandeln; Fließtext nach Satzenden gliedern " - "und optional Grafiken speichern." - ) - ) - parser.add_argument("pdf", type=Path, help="Eingabe-PDF") - parser.add_argument( - "markdown", - type=Path, - nargs="?", - help="Ausgabe-Datei; Standard: gleicher Name mit .md", - ) - parser.add_argument( - "--ocr-language", - default="deu", - help="Tesseract-Sprachcode bei OCR (Standard: deu)", - ) - parser.add_argument( - "--no-ocr", - action="store_true", - help="OCR deaktivieren", - ) - parser.add_argument( - "--images", - action="store_true", - help="Grafiken speichern und in Markdown verlinken", - ) - parser.add_argument( - "--keep-headers", - action="store_true", - help="Wiederkehrende Seitenköpfe übernehmen", - ) - parser.add_argument( - "--keep-footers", - action="store_true", - help="Wiederkehrende Seitenfüße übernehmen", - ) - args = parser.parse_args() - - source = args.pdf.expanduser() - target = (args.markdown or source.with_suffix(".md")).expanduser() - - if not source.is_file(): - parser.error(f"Eingabedatei nicht gefunden: {source}") - - if source.resolve() == target.resolve(): - parser.error("Eingabe- und Ausgabedatei müssen verschieden sein.") - - image_dir = target.parent / f"{target.stem}_images" - - try: - target.parent.mkdir(parents=True, exist_ok=True) - - if args.images: - image_dir.mkdir(parents=True, exist_ok=True) - - markdown = pymupdf4llm.to_markdown( - str(source), - header=args.keep_headers, - footer=args.keep_footers, - use_ocr=not args.no_ocr, - ocr_language=args.ocr_language, - ocr_function=( - tesseract_api.exec_ocr if not args.no_ocr else None - ), - write_images=args.images, - image_path=str(image_dir.resolve()) if args.images else "", - image_format="png", - ) - - if args.images: - markdown = make_image_links_relative(markdown, image_dir) - - target.write_text(normalize_markdown(markdown), encoding="utf-8") - - except Exception as exc: - print(f"Fehler bei der Konvertierung: {exc}", file=sys.stderr) - return 1 - - print(f"Markdown geschrieben: {target}") - if args.images: - print(f"Bilder gespeichert in: {image_dir}") - - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) - diff --git a/Info/Mit_welchem_Programmen_kann_ich_eine_PDF-Datei_am_elegantesten_in_eine_moderne_EPUB-Datei_fuer_meinen_Pocketbook-Reader_umwandeln_und_die_Bilder_und_die_Ueberschriften_uebernehmen.md b/Info/Mit_welchem_Programmen_kann_ich_eine_PDF-Datei_am_elegantesten_in_eine_moderne_EPUB-Datei_fuer_meinen_Pocketbook-Reader_umwandeln_und_die_Bilder_und_die_Ueberschriften_uebernehmen.md deleted file mode 100644 index 94c7c65..0000000 --- a/Info/Mit_welchem_Programmen_kann_ich_eine_PDF-Datei_am_elegantesten_in_eine_moderne_EPUB-Datei_fuer_meinen_Pocketbook-Reader_umwandeln_und_die_Bilder_und_die_Ueberschriften_uebernehmen.md +++ /dev/null @@ -1,1470 +0,0 @@ - - -# Ubuntu 24.04: Mit welchem Programmen kann ich eine PDF-Datei am elegantesten in eine moderne EPUB-Datei für meinen Pocketbook-Reader umwandeln und die Bilder und die Überschriften übernehmen ? - -Für Ubuntu 24.04 würde ich mit **Calibre** beginnen und das Ergebnis anschließend in Sigil nachbearbeiten. So bekommst du ein EPUB mit anpassbarer Schriftgröße, eingebundenen Bildern und einem brauchbaren Inhaltsverzeichnis. Wie gut Überschriften und Bilder übernommen werden, hängt allerdings stark vom Aufbau der PDF ab: PDF speichert vor allem Seitenpositionen, nicht zuverlässig die logische Buchstruktur. Gerade mehrspaltige Seiten, Tabellen und vektorbasierte Grafiken bereiten Calibre Probleme.[^1_1][^1_2] - -## Welches Programm wofür? - -| Programm | Aufgabe | Empfehlung | -| :-- | :-- | :-- | -| Calibre | PDF in ein mitfließendes EPUB konvertieren; Bilder extrahieren und Kapitelüberschriften versuchsweise erkennen | Der einfachste erste Versuch. [^1_3][^1_1] | -| Sigil | Überschriften, Bilder und Inhaltsverzeichnis im fertigen EPUB kontrollieren und korrigieren | Für ein wirklich sauberes Ergebnis. Sigil unterstützt EPUB 3 und mehrstufige Inhaltsverzeichnisse. [^1_2] | -| OCRmyPDF | Gescannte, nicht durchsuchbare PDFs mit einer Textebene versehen | Nur als vorgeschalteter Schritt bei Scans; OCR stellt die ursprüngliche Überschriftenstruktur nicht automatisch wieder her. [^1_4][^1_1] | - -## Praktischer Ablauf - -1. Prüfe zuerst, ob du Text in der PDF markieren und kopieren kannst. Falls nicht, installiere die Texterkennung und erstelle eine durchsuchbare Arbeitskopie:[^1_4][^1_5] - -```bash -sudo apt install ocrmypdf tesseract-ocr-deu -ocrmypdf -l deu --deskew "buch.pdf" "buch-ocr.pdf" -``` - -2. Öffne die ursprüngliche PDF beziehungsweise bei einem Scan `buch-ocr.pdf` in Calibre. Wähle „Bücher konvertieren“ → Ausgabeformat „EPUB“. Prüfe insbesondere „Strukturerkennung“ und „Inhaltsverzeichnis“; Calibre kann unformatierte Kapitelüberschriften heuristisch als Überschriften markieren. Vergleiche das Resultat anschließend mit der PDF, statt die Erkennung ungeprüft zu übernehmen.[^1_6][^1_1] -3. Öffne das EPUB in Sigil. Korrigiere falsch erkannte Überschriften, fehlplatzierte Bilder und das Inhaltsverzeichnis. Das ist der Schritt, der aus einer bloßen Konvertierung ein angenehm lesbares E-Book macht.[^1_2] - -Für einen ersten Versuch auf der Kommandozeile reicht auch: - -```bash -sudo apt install calibre -ebook-convert "buch.pdf" "buch.epub" -``` - -Calibres `ebook-convert` nimmt Eingabe- und Ausgabedatei als erste beiden Argumente. Wenn Bilder wichtig sind, verwende nicht die Option `--no-images`: Sie unterbindet deren Extraktion.[^1_3] - -„Modern“ muss nicht zwingend EPUB 3 heißen: Calibre weist darauf hin, dass EPUB 2 breiter kompatibel ist. PocketBook unterstützt EPUB 3 bei dokumentierten Modellen ebenfalls; ohne genaue Modellangabe würde ich zunächst das erzeugte EPUB auf deinem Reader testen und EPUB 3 nur gezielt wählen, wenn du es brauchst.[^1_7][^1_8] - -**Wichtige Grenze:** Ist die PDF ein aufwendig gesetztes Magazin, ein Fachbuch mit vielen Tabellen oder überwiegend ein Seitenscan, gibt es meist keine elegante Ein-Klick-Umwandlung *mit zugleich* frei skalierbarem Text und originalgetreuem Layout. In solchen Fällen ist die EPUB-Originaldatei, falls erhältlich, die bessere Ausgangsbasis; andernfalls ist Nacharbeit unvermeidlich.[^1_1] - -[^1_10][^1_11][^1_12][^1_13][^1_14][^1_15][^1_16][^1_17][^1_18][^1_19][^1_20][^1_21][^1_22][^1_23][^1_24][^1_25][^1_26][^1_27][^1_28][^1_29][^1_30][^1_31][^1_32][^1_33][^1_34][^1_35][^1_36][^1_37][^1_38][^1_39][^1_40][^1_9] - -
⁂
- -[^1_1]: https://manual.calibre-ebook.com/conversion.html - -[^1_2]: https://sigil-ebook.com/sigil/ - -[^1_3]: https://manual.calibre-ebook.com/generated/en/ebook-convert.html - -[^1_4]: https://ocrmypdf.readthedocs.io/en/latest/introduction.html - -[^1_5]: https://ocrmypdf.readthedocs.io/en/latest/cookbook.html - -[^1_6]: https://manual.calibre-ebook.com/de/conversion.html - -[^1_7]: https://manual.calibre-ebook.com/de/generated/de/ebook-convert.html - -[^1_8]: https://support.pocketbook-int.com/fw/627/ww/6.5.2307/manual/User_Manual_Touch_Lux_4_EN.pdf - -[^1_9]: https://lehrerfortbildung-bw.de/st_digital/medienwerkstatt/office/text/ebook/erstellung/calibre_edit.html - -[^1_10]: https://play.google.com/store/apps/details?id=com.enzodev.convertirpdfaepub\&hl=de - -[^1_11]: https://tools.pdf24.org/de/pdf-in-epub - -[^1_12]: https://www.techspot.com/downloads/5411-calibre.html - -[^1_13]: https://www.chip.de/ratgeber/software/pdf-in-epub-umwandeln-so-geht-s_40a9a185-7250-4c01-a053-1bf50582d9ce.html - -[^1_14]: https://online2pdf.com/de/pdf-in-epub-umwandeln - -[^1_15]: https://mobisystems.com/de-de/mobipdf/features/pdf-to-epub - -[^1_16]: https://apps.apple.com/de/app/konvertieren-von-pdf-in-epub/id1477775676 - -[^1_17]: https://convertio.co/de/epub-pdf/ - -[^1_18]: https://www.reddit.com/r/Calibre/comments/1hc2utk/converting_pdf_to_epub/ - -[^1_19]: https://forum.ubuntuusers.de/topic/pdf-in-epub-umwandeln/ - -[^1_20]: https://tinkerpete.wordpress.com/2010/11/26/tipps-zum-konvertieren-von-pdf-zu-epub-mit-calibre/ - -[^1_21]: https://www.pdftodo.com/de/PDFToePub - -[^1_22]: https://www.tutonaut.de/anleitung-pdf-in-epub-konvertieren/ - -[^1_23]: https://ocrmypdf.readthedocs.io/en/latest/advanced.html - -[^1_24]: https://manual.calibre-ebook.com/calibre.pdf - -[^1_25]: https://manual.calibre-ebook.com/et/generated/et/ebook-convert.html - -[^1_26]: https://ocrmypdf.readthedocs.io/en/latest/batch.html - -[^1_27]: https://ocrmypdf.readthedocs.io/en/stable/apiref.html - -[^1_28]: https://ocrmypdf.readthedocs.io/\_/downloads/en/v9.7.2/pdf/ - -[^1_29]: https://manual.calibre-ebook.com/faq.html - -[^1_30]: https://ocrmypdf.readthedocs.io/\_/downloads/en/v11.1.1/pdf/ - -[^1_31]: https://ocrmypdf.readthedocs.io/en/latest/releasenotes/version03.html - -[^1_32]: https://ocrmypdf.readthedocs.io/\_/downloads/en/v15.2.0/pdf/ - -[^1_33]: https://manual.calibre-ebook.com/fr/generated/fr/ebook-convert.html - -[^1_34]: https://manual.calibre-ebook.com/sv/generated/sv/ebook-convert.html - -[^1_35]: https://support.pocketbook-int.com/fw/632_HZO/ww/6.0.926/rn/ww/RN_PB_Touch_HD_3_EN_6.0.pdf - -[^1_36]: https://support.pocketbook-int.com/fw/632_HZO/ww/6.11.1796/manual/ww/User_Manual_Touch_HD_3_EN.pdf - -[^1_37]: https://support.pocketbook-int.com/fw/629/u/6.8.2659/manual/User_Manual_Verse_EN.pdf - -[^1_38]: https://support.pocketbook-int.com/fw/700/u/6.7.584/manual/ww/User_Manual_Era_EN.pdf - -[^1_39]: https://support.pocketbook-int.com/fw/700/u/6.8.3713/manual/User_Manual_Era_EN.pdf - -[^1_40]: https://support.pocketbook-int.com/fw/740/u/6.1.1069/rn/ww/RN_PB_InkPad_3_EN.pdf - - ---- - -# Mit welche Programmen kann ich dann das EPUB wie editieren ? - -Für dein bereits konvertiertes EPUB ist der **integrierte Editor von Calibre** der bequemste Einstieg: Er zeigt die Textdateien, Bilder und CSS-Dateien des Buchs sowie eine Live-Vorschau. Wenn du vor allem Überschriften und das Inhaltsverzeichnis systematisch überarbeiten willst, ist Sigil eine gute Alternative. Beide bearbeiten das EPUB selbst – du musst nicht erneut von PDF konvertieren.[^2_1][^2_2] - -## So editierst du es in Calibre - -1. Lege zuerst eine Kopie der EPUB-Datei an. Füge das Buch dann in Calibre hinzu, klicke es mit der rechten Maustaste an und wähle „Buch bearbeiten“. -2. Öffne links eine der Textdateien. Der Buchtext liegt dort als HTML/XHTML; Änderungen siehst du rechts in der Vorschau. Die Formatierung steckt überwiegend in den CSS-Dateien.[^2_1] -``` -1. Korrigiere Überschriften im Text als echte Überschriften, etwa `

Kapitel 1

` für Kapitel und `

Unterkapitel

` für Unterabschnitte. Ein bloß fett formatierter Absatz ist für ein automatisch erzeugtes Inhaltsverzeichnis nicht dasselbe.[^2_3] -``` - -1. Prüfe die Bilddateien links im Dateibrowser und ihre Position im Text. Calibre lässt dich HTML-, CSS- und Bilddateien innerhalb des Buchs bearbeiten.[^2_1] -2. Öffne „Werkzeuge“ → „Inhaltsverzeichnis“ → „Inhaltsverzeichnis bearbeiten“. Dort kannst du Einträge umbenennen, verschieben und ihre Sprungziele korrigieren. Speichere anschließend das Buch und teste es auf dem PocketBook.[^2_1] - -Ein Beispiel für eine nach der PDF-Konvertierung falsch erkannte Überschrift: - -```html -

Kapitel 3 – Die Reise

-``` - -Ändere sie, sofern sie tatsächlich ein Kapiteltitel ist, zu: - -```html -

Kapitel 3 – Die Reise

-``` - -Damit ist die Struktur im EPUB ausdrücklich ausgezeichnet; danach kannst du das Inhaltsverzeichnis passend dazu aufbauen.[^2_3] - -## Wann Sigil sinnvoller ist - -Sigil ist ein eigenständiger EPUB-Editor für Linux. Er unterstützt EPUB 2 und EPUB 3, den Import von Bildern und Stylesheets sowie die Erzeugung eines mehrstufigen Inhaltsverzeichnisses aus Überschriften. Ich würde ihn nehmen, wenn du viele Kapitel systematisch bereinigen oder die EPUB-Struktur genauer kontrollieren möchtest.[^2_2][^2_4] - -Unter Ubuntu kannst du zunächst die Paketversionen verwenden: - -```bash -sudo apt install calibre sigil -``` - -Calibre ist in den Ubuntu-Paketquellen verfügbar; dessen Projekt empfiehlt für die jeweils aktuelle Version allerdings seine eigene Linux-Installation, weil Distributionspakete veraltet sein können.[^2_5][^2_6] - -Für dein konkretes Ziel würde ich mit Calibre anfangen und nur bei umfangreicher Strukturarbeit zu Sigil wechseln. Gerade bei einer aus PDF erzeugten Datei lohnt sich zuerst eine Stichprobe an drei Stellen: Anfang, Mitte und Ende – dort erkennst du schnell, ob Überschriften, Bildzuordnung und Lesereihenfolge grundsätzlich stimmen. - -[^2_10][^2_11][^2_12][^2_13][^2_14][^2_15][^2_16][^2_7][^2_8][^2_9] - -
⁂
- -[^2_1]: https://manual.calibre-ebook.com/edit.html - -[^2_2]: https://sigil-ebook.com/sigil/ - -[^2_3]: https://manual.calibre-ebook.com/de/polish.html - -[^2_4]: https://daisy.org/guidance/info-help/guidance-training/content-creation/using-sigil-to-create-epub-files/ - -[^2_5]: https://wiki.ubuntuusers.de/Calibre/ - -[^2_6]: https://calibre-ebook.com/download_linux - -[^2_7]: https://flathub.org/en/apps/com.sigil_ebook.Sigil - -[^2_8]: https://www.vpl.ca/guides/inspiration-lab-sigil/getting-started-sigil - -[^2_9]: https://lwn.net/Articles/1054751/ - -[^2_10]: https://github.com/Sigil-Ebook-Editor - -[^2_11]: https://davidgerard.co.uk/blockchain/2020/11/05/calibre-epub-and-epubcheck-the-curse-of-editing-xhtml/ - -[^2_12]: https://linuxcapable.com/how-to-install-calibre-on-ubuntu-linux/ - -[^2_13]: https://ossmalta.eu/how-to-install-sigil-2-3-1-in-ubuntu-24-04-linux-mint-22/ - -[^2_14]: https://pclosmag.com/html/Issues/201302/page09.html - -[^2_15]: https://ubuntu.dirkschmidtke.de/en/portfolio/calibre/ - -[^2_16]: https://www.reddit.com/r/Calibre/comments/cqa1s7/i_edit_an_epub_with_calibre_editerwhen_i_inport_a/ - - ---- - -# Ubuntu 24.04: Wie kann ich eine PDF-Datei in eine Markdown-Datei umwandeln und dabei alle Überschriften und Absätze so erhalten wie in der PDF-Datei, aber einfache Zeilenumbrüche sollen durch Leerzeichen ersetzt werden, doppelte Zeilenumbrüche (d. h. wahre Absätze) sollen erhalten bleiben. Schreibe ein Python-Commandline-Progamm, das die Konvertierung vornimmt. - -Für deinen Zweck würde ich die PDF zunächst mit PyMuPDF4LLM als strukturiertes Markdown auslesen und danach nur die Zeilenumbrüche innerhalb erkannter Textabsätze zusammenziehen. Das Programm unten lässt Leerzeilen als Absatzgrenzen stehen und schützt Markdown-Überschriften, Listen, Tabellen und Codeblöcke davor, versehentlich mit Fließtext verschmolzen zu werden. PyMuPDF4LLM unterstützt Layoutanalyse und kann Überschriften aus dem Dokument ableiten; eine perfekte Rekonstruktion ist bei PDFs aber nicht garantiert.[^3_1][^3_2][^3_3] - -## Installation unter Ubuntu 24.04 - -Installiere das Python-Paket in einer virtuellen Umgebung. Für gescannte PDFs kommen Tesseract und das deutsche Sprachpaket hinzu; bei PDFs mit auswählbarem Text brauchst du sie normalerweise nicht. PyMuPDF4LLM kann OCR bei Bedarf automatisch einsetzen.[^3_1] - -```bash -sudo apt install python3-venv tesseract-ocr tesseract-ocr-deu - -python3 -m venv ~/venvs/pdf2md -~/venvs/pdf2md/bin/pip install --upgrade pip -~/venvs/pdf2md/bin/pip install pymupdf4llm -``` - - -## Das Kommandozeilenprogramm - -Speichere Folgendes als `pdf2md.py`: - -```python -#!/usr/bin/env python3 -"""PDF nach Markdown konvertieren und weiche Zeilenumbrüche entfernen.""" - -import argparse -import re -import sys -from pathlib import Path - -import pymupdf4llm - - -HEADING = re.compile(r"^\s{0,3}#{1,6}\s+") -LIST_ITEM = re.compile(r"^\s{0,3}(?:[-+*]|\d+[.)])\s+") -BLOCKQUOTE = re.compile(r"^\s{0,3}>\s?") -FENCE = re.compile(r"^\s{0,3}(`{3,}|~{3,})") -RULE = re.compile(r"^\s{0,3}(?:-\s*){3,}$|^\s{0,3}(?:\*\s*){3,}$") -IMAGE = re.compile(r"^\s*!\[.*?\]\(.*\)\s*$") -HTML_BLOCK = re.compile(r"^\s* bool: - """Zeilen, die nicht mit dem vorherigen Fließtext verbunden werden.""" - return bool( - HEADING.match(line) - or LIST_ITEM.match(line) - or BLOCKQUOTE.match(line) - or RULE.match(line) - or IMAGE.match(line) - or HTML_BLOCK.match(line) - or line.lstrip().startswith("|") - or line.startswith((" ", "\t")) - ) - - -def normalize_markdown(markdown: str) -> str: - result = [] - paragraph = [] - in_fence = False - fence_char = "" - fence_length = 0 - - def flush_paragraph(): - if paragraph: - result.append(" ".join(paragraph)) - paragraph.clear() - - for original in markdown.replace("\r\n", "\n").replace("\r", "\n").split("\n"): - line = original.rstrip() - marker = FENCE.match(line) - - if in_fence: - result.append(line) - if marker: - token = marker.group(1) - if token[^3_0] == fence_char and len(token) >= fence_length: - in_fence = False - continue - - if marker: - flush_paragraph() - result.append(line) - in_fence = True - fence_char = marker.group(1)[^3_0] - fence_length = len(marker.group(1)) - continue - - if not line.strip(): - flush_paragraph() - if result and result[-1] != "": - result.append("") - continue - - if is_structural(line): - flush_paragraph() - result.append(line) - continue - - paragraph.append(line.strip()) - - flush_paragraph() - - return "\n".join(result).strip() + "\n" - - -def main() -> int: - parser = argparse.ArgumentParser( - description="PDF in Markdown umwandeln; Zeilen innerhalb von Absätzen verbinden." - ) - parser.add_argument("pdf", type=Path, help="Eingabe-PDF") - parser.add_argument( - "markdown", - type=Path, - nargs="?", - help="Ausgabe-Datei; Standard: gleicher Name mit .md", - ) - parser.add_argument( - "--ocr-language", - default="deu", - help="Tesseract-Sprachcode bei erforderlicher OCR (Standard: deu)", - ) - parser.add_argument( - "--no-ocr", - action="store_true", - help="OCR abschalten, wenn die PDF sicher durchsuchbaren Text enthält", - ) - parser.add_argument( - "--keep-headers", - action="store_true", - help="Wiederkehrende Seitenköpfe in die Ausgabe übernehmen", - ) - parser.add_argument( - "--keep-footers", - action="store_true", - help="Wiederkehrende Seitenfüße in die Ausgabe übernehmen", - ) - args = parser.parse_args() - - source = args.pdf.expanduser() - target = (args.markdown or source.with_suffix(".md")).expanduser() - - if not source.is_file(): - parser.error(f"Eingabedatei nicht gefunden: {source}") - if source.resolve() == target.resolve(): - parser.error("Eingabe- und Ausgabedatei müssen verschieden sein.") - - try: - markdown = pymupdf4llm.to_markdown( - str(source), - header=args.keep_headers, - footer=args.keep_footers, - use_ocr=not args.no_ocr, - ocr_language=args.ocr_language, - ) - target.parent.mkdir(parents=True, exist_ok=True) - target.write_text(normalize_markdown(markdown), encoding="utf-8") - except Exception as exc: - print(f"Fehler bei der Konvertierung: {exc}", file=sys.stderr) - return 1 - - print(f"Markdown geschrieben: {target}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) -``` - -PyMuPDF4LLM stellt die hier verwendeten Optionen für Markdown-Ausgabe, Seitenkopf/-fuß und OCR-Sprache bereit. Das Weglassen wiederkehrender Kopf- und Fußzeilen ist standardmäßig aktiviert, damit beispielsweise Seitenzahlen nicht zwischen zwei Textabsätzen landen.[^3_2][^3_1] - -## Aufruf und Ergebnis - -```bash -~/venvs/pdf2md/bin/python pdf2md.py "mein-buch.pdf" "mein-buch.md" -``` - -Für eine deutsch-englische gescannte PDF: - -```bash -~/venvs/pdf2md/bin/python pdf2md.py \ - --ocr-language deu+eng "mein-buch.pdf" "mein-buch.md" -``` - -Aus diesem erkannten Markdown-Abschnitt: - -```text -## Einleitung - -Dies ist die erste Zeile -desselben Absatzes. - -Dies ist ein neuer -Absatz. -``` - -macht das Programm: - -```text -## Einleitung - -Dies ist die erste Zeile desselben Absatzes. - -Dies ist ein neuer Absatz. -``` - -Die entscheidende Einschränkung: Das Programm erhält *erkannte* Absatzgrenzen. Wenn die PDF beziehungsweise die Extraktion zwei echte Absätze ohne Leerzeile ausgibt, kann die reine Zeilenumbruchregel sie nicht sicher unterscheiden. Ebenso können Überschriften bei uneinheitlicher Typografie falsch erkannt werden; mehrspaltige Seiten oder Tabellen solltest du stichprobenartig mit dem Original vergleichen.[^3_3][^3_2] - -[^3_10][^3_11][^3_12][^3_13][^3_14][^3_15][^3_16][^3_17][^3_18][^3_4][^3_5][^3_6][^3_7][^3_8][^3_9] - -
⁂
- -[^3_1]: https://pymupdf.readthedocs.io/en/latest/pymupdf4llm/ - -[^3_2]: https://pymupdf.readthedocs.io/en/latest/pymupdf4llm/api.html - -[^3_3]: https://pymupdf.readthedocs.io/en/latest/recipes-text.html - -[^3_4]: https://pymupdf.readthedocs.io/en/latest/app1.html - -[^3_5]: https://pymupdf.io/blog/pdf-native-vs-vision-models-gemini-3 - -[^3_6]: https://pymupdf.readthedocs.io/ - -[^3_7]: https://www.nutrient.io/blog/extract-text-from-pdf-pymupdf/ - -[^3_8]: https://github.com/pymupdf/PyMuPDF4LLM - -[^3_9]: https://deepwiki.com/pymupdf/pymupdf4llm/9-configuration-reference - -[^3_10]: https://github.com/pymupdf/pymupdf4llm/tree/main/src/helpers/chunking - -[^3_11]: https://deepwiki.com/pymupdf/pymupdf4llm/2.4-output-formats-and-page-chunking - -[^3_12]: https://github.com/pymupdf/PyMuPDF/discussions/1512 - -[^3_13]: https://github.com/pymupdf/PyMuPDF/discussions/2396 - -[^3_14]: https://deepwiki.com/pymupdf/PyMuPDF/2.2-text-extraction-and-processing - -[^3_15]: https://documentation.help/PyMuPDF/app2.html - -[^3_16]: tools.ebook_conversion - -[^3_17]: projects.textbook.operations_research_trading - -[^3_18]: https://www.perplexity.ai/search/2748e552-fbd6-492f-88ca-8fb325447e38 - - ---- - -# python pdf2md.py Operation_Ljutsch_Band_1-\_Reinhard_Otto_Kranz.pdf Operation_Ljutsch_Band_1-\_Reinhard_Otto_Kranz.md - -Fehler bei der Konvertierung: 'RapidOCR' object has no attribute 'text_detector' - -Der Fehler kommt aus der OCR-Komponente, nicht aus der Funktion, die Zeilenumbrüche zusammenzieht: PyMuPDF4LLM versucht offenbar, RapidOCR aufzurufen, dessen installiertes Objekt die erwartete Eigenschaft `text_detector` nicht hat. Die automatische OCR-Auswahl bevorzugt sogar eine Kombination aus RapidOCR und Tesseract, wenn beide verfügbar sind.[^4_1][^4_2] - -## Schnellster Versuch - -Wenn du in der PDF Text mit der Maus markieren und kopieren kannst, starte das vorhandene Skript zunächst ohne OCR: - -```bash -python pdf2md.py --no-ocr \ - Operation_Ljutsch_Band_1-_Reinhard_Otto_Kranz.pdf \ - Operation_Ljutsch_Band_1-_Reinhard_Otto_Kranz.md -``` - -In meinem Skript setzt `--no-ocr` den Parameter `use_ocr=False`. Das umgeht den fehlerhaften OCR-Pfad. Prüfe anschließend, ob die Markdown-Datei tatsächlich Text enthält.[^4_3] - -## Falls die PDF ein Scan ist - -Dann brauchst du OCR. Ändere in `pdf2md.py` den Importbereich von - -```python -import pymupdf4llm -``` - -zu - -```python -import pymupdf4llm -from pymupdf4llm.ocr import tesseract_api -``` - -und ergänze im Aufruf von `pymupdf4llm.to_markdown(...)` diese Zeile: - -```python -ocr_function=tesseract_api.exec_ocr if not args.no_ocr else None, -``` - -Der betreffende Abschnitt sieht danach so aus: - -```python -markdown = pymupdf4llm.to_markdown( - str(source), - header=args.keep_headers, - footer=args.keep_footers, - use_ocr=not args.no_ocr, - ocr_language=args.ocr_language, - ocr_function=tesseract_api.exec_ocr if not args.no_ocr else None, -) -``` - -Damit wählst du ausdrücklich das Tesseract-Plugin statt der automatischen Auswahl mit RapidOCR. PyMuPDF4LLM dokumentiert `ocr_function` genau für die Auswahl eines bestimmten OCR-Plugins.[^4_1] - -Stelle sicher, dass Tesseract und die deutschen Sprachdaten installiert sind, und starte denselben Befehl danach erneut: - -```bash -sudo apt install tesseract-ocr tesseract-ocr-deu - -python pdf2md.py \ - Operation_Ljutsch_Band_1-_Reinhard_Otto_Kranz.pdf \ - Operation_Ljutsch_Band_1-_Reinhard_Otto_Kranz.md -``` - -**Meine Empfehlung:** Probiere zuerst `--no-ocr`. Nur wenn dadurch Text fehlt, ändere das Skript auf das explizite Tesseract-Plugin. Das behebt gezielt den gemeldeten RapidOCR-Fehler, ohne an der Absatzbehandlung etwas zu verändern.[^4_2][^4_1] - -[^4_10][^4_11][^4_12][^4_13][^4_14][^4_15][^4_4][^4_5][^4_6][^4_7][^4_8][^4_9] - -
⁂
- -[^4_1]: https://pymupdf.readthedocs.io/en/latest/pymupdf4llm/ocr-plugins.html - -[^4_2]: https://github.com/pymupdf/pymupdf4llm/issues/398 - -[^4_3]: https://pymupdf.readthedocs.io/en/latest/pymupdf4llm/api.html - -[^4_4]: https://pymupdf.io/blog/hybrid-ocr-in-pymupdf4llm - -[^4_5]: https://artifex.com/blog/building-a-multimodal-llm-application-with-pymupdf4llm - -[^4_6]: https://pypi.org/project/pymupdf4llm/ - -[^4_7]: https://data.safetycli.com/packages/pypi/pymupdf/changelog - -[^4_8]: https://www.llamaindex.ai/glossary/tesseract-ocr-python - -[^4_9]: https://github.com/pymupdf/pymupdf4llm/blob/main/CHANGES.md - -[^4_10]: https://github.com/pymupdf/PyMuPDF/issues/4833 - -[^4_11]: https://buildmedia.readthedocs.org/media/pdf/pymupdf/latest/pymupdf.pdf - -[^4_12]: https://stackoverflow.com/questions/72175112/how-to-data-extract-from-unstructured-pdfs-using-pymupdf-in-python - -[^4_13]: https://dev.to/aairom/using-doclings-ocr-features-with-rapidocr-29hd - -[^4_14]: https://github.com/pymupdf/pymupdf4llm - -[^4_15]: https://pymupdf.readthedocs.io/en/latest/pymupdf4llm/ - - ---- - -# Verbessere das Programm: Absätze müssen immer mit einem Satzzeichen enden, das das Ende eines Satzes markiert oder das Ende eines Satzes mit Anführungszeichen markiert ('z. B. .' , '?' , '!' oder diese Zeichen gefolgt von einem beendenden Anführungszeichen). Wenn am Ende eines vermeintlichen Absatzes kein solches Zeichen oder eine solche Zeichenfolge steht, handelt es sich NICHT um ein Absatzende. Dann sollen die Zeichen bis zur nächsten Zeile durch ein Leerzeichen ersetzt werden und der Absatz fortgesetzt werden. Nur Überschriften sind auch ohne Satzende-Zeichen(-folge) erlaubt. - -Ja. Die Absatzlogik lässt sich so ändern, dass eine Leerzeile nur dann einen neuen Absatz erzeugt, wenn der bisherige Text mit `.`, `?`, `!` oder `…` endet – gegebenenfalls gefolgt von schließenden Anführungszeichen. Andernfalls verbindet das Programm den Text über die Leerzeile hinweg mit einem Leerzeichen. **Überschriften** bleiben auch ohne Satzzeichen eigenständige Blöcke. - -Ersetze `pdf2md.py` vollständig durch diese Version. Sie enthält außerdem die Tesseract-Auswahl aus der vorherigen Fehlerbehebung: - -```python -#!/usr/bin/env python3 -"""PDF nach Markdown konvertieren und Absätze anhand von Satzenden verbinden.""" - -import argparse -import re -import sys -from pathlib import Path - -import pymupdf4llm -from pymupdf4llm.ocr import tesseract_api - - -HEADING = re.compile(r"^\s{0,3}#{1,6}\s+") -LIST_ITEM = re.compile(r"^\s{0,3}(?:[-+*]|\d+[.)])\s+") -BLOCKQUOTE = re.compile(r"^\s{0,3}>\s?") -FENCE = re.compile(r"^\s{0,3}(`{3,}|~{3,})") -RULE = re.compile(r"^\s{0,3}(?:-\s*){3,}$|^\s{0,3}(?:\*\s*){3,}$") -IMAGE = re.compile(r"^\s*!\[.*?\]\(.*\)\s*$") -HTML_BLOCK = re.compile( - r"^\s* bool: - return bool(SENTENCE_END.search(text.rstrip())) - - -def is_structural(line: str) -> bool: - """Markdown-Zeilen, die nicht in Fließtext aufgehen sollen.""" - return bool( - LIST_ITEM.match(line) - or BLOCKQUOTE.match(line) - or RULE.match(line) - or IMAGE.match(line) - or HTML_BLOCK.match(line) - or line.lstrip().startswith("|") - or line.startswith((" ", "\t")) - ) - - -def normalize_markdown(markdown: str) -> str: - result: list[str] = [] - paragraph: list[str] = [] - - in_fence = False - fence_char = "" - fence_length = 0 - - def add_blank_line() -> None: - if result and result[-1] != "": - result.append("") - - def flush_paragraph() -> None: - if paragraph: - result.append(" ".join(paragraph)) - paragraph.clear() - - for original in markdown.replace("\r\n", "\n").replace("\r", "\n").split("\n"): - line = original.rstrip() - marker = FENCE.match(line) - - if in_fence: - result.append(line) - if marker: - token = marker.group(1) - if token[0] == fence_char and len(token) >= fence_length: - in_fence = False - continue - - if marker: - flush_paragraph() - add_blank_line() - result.append(line) - in_fence = True - fence_char = marker.group(1)[0] - fence_length = len(marker.group(1)) - continue - - if HEADING.match(line): - flush_paragraph() - add_blank_line() - result.append(line) - result.append("") - continue - - if is_structural(line): - flush_paragraph() - result.append(line) - continue - - if not line.strip(): - # Eine Leerzeile ist nur nach einem Satzende eine Absatzgrenze. - # Andernfalls bleibt paragraph bestehen und die nächste - # Textzeile wird mit einem Leerzeichen angehängt. - if paragraph and ends_sentence(paragraph[-1]): - flush_paragraph() - add_blank_line() - elif not paragraph: - add_blank_line() - continue - - paragraph.append(line.strip()) - - flush_paragraph() - return "\n".join(result).strip() + "\n" - - -def main() -> int: - parser = argparse.ArgumentParser( - description=( - "PDF in Markdown umwandeln; Absätze nur nach Satzende trennen." - ) - ) - parser.add_argument("pdf", type=Path, help="Eingabe-PDF") - parser.add_argument( - "markdown", - type=Path, - nargs="?", - help="Ausgabe-Datei; Standard: gleicher Name mit .md", - ) - parser.add_argument( - "--ocr-language", - default="deu", - help="Tesseract-Sprachcode bei OCR (Standard: deu)", - ) - parser.add_argument( - "--no-ocr", - action="store_true", - help="OCR deaktivieren", - ) - parser.add_argument( - "--keep-headers", - action="store_true", - help="Wiederkehrende Seitenköpfe übernehmen", - ) - parser.add_argument( - "--keep-footers", - action="store_true", - help="Wiederkehrende Seitenfüße übernehmen", - ) - args = parser.parse_args() - - source = args.pdf.expanduser() - target = (args.markdown or source.with_suffix(".md")).expanduser() - - if not source.is_file(): - parser.error(f"Eingabedatei nicht gefunden: {source}") - - if source.resolve() == target.resolve(): - parser.error("Eingabe- und Ausgabedatei müssen verschieden sein.") - - try: - markdown = pymupdf4llm.to_markdown( - str(source), - header=args.keep_headers, - footer=args.keep_footers, - use_ocr=not args.no_ocr, - ocr_language=args.ocr_language, - ocr_function=( - tesseract_api.exec_ocr if not args.no_ocr else None - ), - ) - - target.parent.mkdir(parents=True, exist_ok=True) - target.write_text(normalize_markdown(markdown), encoding="utf-8") - - except Exception as exc: - print(f"Fehler bei der Konvertierung: {exc}", file=sys.stderr) - return 1 - - print(f"Markdown geschrieben: {target}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) -``` - -Beispiel für die neue Regel: - -```text -Dies ist ein vermeintlicher Absatz - -der tatsächlich noch weitergeht. - -Hier endet ein Satz. - -Hier beginnt ein neuer Absatz. -``` - -wird zu: - -```text -Dies ist ein vermeintlicher Absatz der tatsächlich noch weitergeht. - -Hier endet ein Satz. - -Hier beginnt ein neuer Absatz. -``` - -Du kannst es wie bisher aufrufen: - -```bash -python pdf2md.py --no-ocr \ - Operation_Ljutsch_Band_1-_Reinhard_Otto_Kranz.pdf \ - Operation_Ljutsch_Band_1-_Reinhard_Otto_Kranz.md -``` - -Die Regel ist bewusst strikt: Eine echte Absatzgrenze nach einem Doppelpunkt oder eine abbrechende Dialogzeile ohne Schlusssatzzeichen wird ebenfalls zusammengezogen. Umgekehrt zählt auch ein Punkt in einer Abkürzung am Zeilenende als Satzende, *wenn dort zugleich eine Leerzeile erkannt wurde*. Listen, Tabellen, Bilder und Codeblöcke bleiben aus Sicherheitsgründen eigene Markdown-Strukturen und werden nicht nach dieser Satzzeichenregel zusammengezogen. - ---- - -# Verbessere das Programm: Absätze müssen immer mit einem Satzzeichen enden, das das Ende eines Satzes markiert oder das Ende eines Satzes mit Anführungszeichen markiert ('z. B. .' , '?' , '!' oder diese Zeichen gefolgt von einem beendenden Anführungszeichen). Wenn am Ende eines vermeintlichen Absatzes kein solches Zeichen oder eine solche Zeichenfolge steht, handelt es sich NICHT um ein Absatzende. Dann sollen die Zeichen bis zur nächsten Zeile durch ein Leerzeichen ersetzt werden und der Absatz fortgesetzt werden. Nur Überschriften, Zeilen des Inhaltsverzeichnisses oder Hinweise auf ausgelassene Bilder (z. B. '**==> picture [426 x 480] intentionally omitted \<==**' ) sind auch ohne Satzende-Zeichen(-folge) erlaubt. - -Die zusätzliche Regel ist: Eine Zeile des Inhaltsverzeichnisses und ein Platzhalter für ein ausgelassenes Bild dürfen ebenso wie eine Überschrift ohne Satzzeichen enden. Sie bleiben eigenständige Markdown-Blöcke; gewöhnlicher Fließtext wird dagegen über vermeintliche Absatzgrenzen hinweg verbunden, bis ein zulässiges Satzende erreicht ist. - -Ersetze `pdf2md.py` durch diese vollständige Fassung: - -```python -#!/usr/bin/env python3 -"""PDF nach Markdown konvertieren und Fließtext nach Satzenden gliedern.""" - -import argparse -import re -import sys -from pathlib import Path - -import pymupdf4llm -from pymupdf4llm.ocr import tesseract_api - - -HEADING = re.compile(r"^\s{0,3}#{1,6}\s+\S") - -# Punkt, Fragezeichen, Ausrufezeichen oder Auslassungspunkte; -# danach dürfen schließende Anführungszeichen/Klammern folgen. -SENTENCE_END = re.compile(r"""[.!?…]+[”’"'»›)\]]*$""") - -FENCE = re.compile(r"^\s{0,3}(`{3,}|~{3,})") -LIST_ITEM = re.compile(r"^\s{0,3}(?:[-+*]|\d+[.)])\s+") -BLOCKQUOTE = re.compile(r"^\s{0,3}>\s?") -RULE = re.compile(r"^\s{0,3}(?:-\s*){3,}$|^\s{0,3}(?:\*\s*){3,}$") -IMAGE = re.compile(r"^\s*!\[.*?\]\(.*\)\s*$") - -# Beispiel: -# **==> picture [426 x 480] intentionally omitted <==** -OMITTED_IMAGE = re.compile( - r"^\s*(?:\*\*|__)?\s*==>\s*" - r"(?:picture|image|bild)\b.*?" - r"(?:intentionally omitted|omitted|ausgelassen)\s*" - r"<==\s*(?:\*\*|__)?\s*$", - re.IGNORECASE, -) - -CONTENTS_TITLE = re.compile( - r"^\s*(?:#{1,6}\s+)?" - r"(?:inhaltsverzeichnis|inhalt|contents|table of contents)" - r"\s*$", - re.IGNORECASE, -) - -# Inhaltsverzeichnis mit Füllpunkten oder Abstand vor einer Seitenzahl: -# Kapitel 1 .......... 12 -# Kapitel 1 12 -# Kapitel 1 xiv -TOC_WITH_PAGE = re.compile( - r"^\s*(?:[-*]\s+)?\S.+?" - r"(?:\.{2,}|\s{2,})" - r"(?:\d+|[ivxlcdm]+)\s*$", - re.IGNORECASE, -) - -# Einträge ohne Seitenzahl, sofern bereits ein Inhaltsverzeichnis erkannt wurde: -# 1 Einleitung -# 1.2 Vorgeschichte -# Kapitel 3 – Die Reise -TOC_NUMBERED = re.compile( - r"^\s*(?:[-*]\s+)?" - r"(?:\d+(?:\.\d+)*\.?\s+|Kapitel\s+\d+\b|Chapter\s+\d+\b)" - r".+", - re.IGNORECASE, -) - -HTML_BLOCK = re.compile( - r"^\s* bool: - return bool(SENTENCE_END.search(text.rstrip())) - - -def is_other_structure(line: str) -> bool: - """Markdown-Strukturen, die nicht zu Fließtext verbunden werden.""" - return bool( - LIST_ITEM.match(line) - or BLOCKQUOTE.match(line) - or RULE.match(line) - or IMAGE.match(line) - or HTML_BLOCK.match(line) - or line.lstrip().startswith("|") - or line.startswith((" ", "\t")) - ) - - -def normalize_markdown(markdown: str) -> str: - result: list[str] = [] - paragraph: list[str] = [] - - in_fence = False - fence_char = "" - fence_length = 0 - in_contents = False - - def blank_line() -> None: - if result and result[-1] != "": - result.append("") - - def flush_paragraph() -> None: - if paragraph: - result.append(" ".join(paragraph)) - paragraph.clear() - - def standalone(line: str) -> None: - flush_paragraph() - blank_line() - result.append(line) - result.append("") - - lines = markdown.replace("\r\n", "\n").replace("\r", "\n").split("\n") - - for original in lines: - line = original.rstrip() - marker = FENCE.match(line) - - if in_fence: - result.append(line) - if marker: - token = marker.group(1) - if token[0] == fence_char and len(token) >= fence_length: - in_fence = False - continue - - if marker: - flush_paragraph() - blank_line() - result.append(line) - in_fence = True - fence_char = marker.group(1)[0] - fence_length = len(marker.group(1)) - continue - - if not line.strip(): - # Leerzeile nur dann als Absatzgrenze behandeln, - # wenn der bisherige Fließtext mit einem Satzende endet. - if paragraph and ends_sentence(paragraph[-1]): - flush_paragraph() - blank_line() - elif not paragraph: - blank_line() - continue - - if CONTENTS_TITLE.match(line): - standalone(line) - in_contents = True - continue - - if HEADING.match(line): - standalone(line) - in_contents = False - continue - - if OMITTED_IMAGE.match(line): - standalone(line) - continue - - if TOC_WITH_PAGE.match(line): - standalone(line) - continue - - if in_contents and TOC_NUMBERED.match(line): - standalone(line) - continue - - if in_contents and ends_sentence(line): - # Erster normaler Satz nach einem erkannten Inhaltsverzeichnis. - in_contents = False - - if is_other_structure(line): - flush_paragraph() - result.append(line) - continue - - # Einfache Zeilenumbrüche und nicht bestätigte Absatzgrenzen - # werden beim späteren Flush durch Leerzeichen ersetzt. - paragraph.append(line.strip()) - - flush_paragraph() - return "\n".join(result).strip() + "\n" - - -def main() -> int: - parser = argparse.ArgumentParser( - description="PDF in Markdown umwandeln; Fließtext nur nach Satzende trennen." - ) - parser.add_argument("pdf", type=Path, help="Eingabe-PDF") - parser.add_argument( - "markdown", - type=Path, - nargs="?", - help="Ausgabe-Datei; Standard: gleicher Name mit .md", - ) - parser.add_argument( - "--ocr-language", - default="deu", - help="Tesseract-Sprachcode bei OCR (Standard: deu)", - ) - parser.add_argument( - "--no-ocr", - action="store_true", - help="OCR deaktivieren", - ) - parser.add_argument( - "--keep-headers", - action="store_true", - help="Wiederkehrende Seitenköpfe übernehmen", - ) - parser.add_argument( - "--keep-footers", - action="store_true", - help="Wiederkehrende Seitenfüße übernehmen", - ) - args = parser.parse_args() - - source = args.pdf.expanduser() - target = (args.markdown or source.with_suffix(".md")).expanduser() - - if not source.is_file(): - parser.error(f"Eingabedatei nicht gefunden: {source}") - if source.resolve() == target.resolve(): - parser.error("Eingabe- und Ausgabedatei müssen verschieden sein.") - - try: - markdown = pymupdf4llm.to_markdown( - str(source), - header=args.keep_headers, - footer=args.keep_footers, - use_ocr=not args.no_ocr, - ocr_language=args.ocr_language, - ocr_function=( - tesseract_api.exec_ocr if not args.no_ocr else None - ), - ) - - target.parent.mkdir(parents=True, exist_ok=True) - target.write_text(normalize_markdown(markdown), encoding="utf-8") - - except Exception as exc: - print(f"Fehler bei der Konvertierung: {exc}", file=sys.stderr) - return 1 - - print(f"Markdown geschrieben: {target}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) -``` - -Aufruf für eine PDF mit kopierbarem Text: - -```bash -python pdf2md.py --no-ocr \ - Operation_Ljutsch_Band_1-_Reinhard_Otto_Kranz.pdf \ - Operation_Ljutsch_Band_1-_Reinhard_Otto_Kranz.md -``` - -Beispielsweise bleibt diese Bildzeile jetzt allein stehen, obwohl sie keinen Satz abschließt: - -```markdown -**==> picture [426 x 480] intentionally omitted <==** -``` - -Auch `Kapitel 1 .......... 12` wird als eigenständige Inhaltsverzeichniszeile behandelt. Einträge *ohne* Seitenzahl erkennt das Programm nur im Abschnitt nach einer erkannten Überschrift „Inhaltsverzeichnis“, „Inhalt“, „Contents“ oder „Table of Contents“. - -Eine Grenze bleibt: Wenn die PDF-Extraktion eine Überschrift oder Inhaltsverzeichniszeile nicht als solche erkennbar ausgibt, lässt sich ihr Typ aus einem einfachen Zeilenumbruch nicht zuverlässig ableiten. Die neue Regel schützt die genannten Muster; bei ungewöhnlichem Buchsatz müssen wir deren Erkennung gegebenenfalls anhand eines tatsächlichen Auszugs deiner erzeugten Markdown-Datei ergänzen. - ---- - -# Ist es kompliziert, auch die Grafiken in die MD-Datei einzufügen ? - -Nein, für dein bestehendes Programm ist das eine überschaubare Änderung. PyMuPDF4LLM kann Grafiken als Bilddateien speichern und an ihrer Stelle Markdown-Verweise erzeugen. Das ist für deine Datei praktischer, als die Bilddaten direkt in die `.md`-Datei einzubetten: Du erhältst eine lesbare Markdown-Datei und einen zugehörigen Bilderordner.[^7_1] - -## Änderungen am Programm - -Ergänze bei den Argumenten in `main()`: - -```python -parser.add_argument( - "--images", - action="store_true", - help="Grafiken als Dateien speichern und in Markdown verlinken", -) -``` - -Füge unmittelbar *vor* dem Aufruf von `pymupdf4llm.to_markdown(...)` Folgendes ein: - -```python -image_dir = target.parent / f"{target.stem}_images" - -if args.images: - image_dir.mkdir(parents=True, exist_ok=True) -``` - -Ergänze im Aufruf von `to_markdown(...)` diese Parameter: - -```python -write_images=args.images, -image_path=str(image_dir) if args.images else "", -image_format="png", -``` - -Da `image_path` hier ein absoluter oder zum aktuellen Arbeitsverzeichnis relativer Dateipfad sein kann, sollten die erzeugten Verweise anschließend relativ zum Speicherort der Markdown-Datei sein. Füge deshalb direkt *nach* `to_markdown(...)`, aber *vor* `normalize_markdown(markdown)`, ein: - -```python -if args.images: - markdown = markdown.replace( - str(image_dir.resolve()) + "/", - image_dir.name + "/", - ).replace( - str(image_dir) + "/", - image_dir.name + "/", - ) -``` - -Dein bisheriger Schreibaufruf bleibt unverändert: - -```python -target.write_text(normalize_markdown(markdown), encoding="utf-8") -``` - -`normalize_markdown()` erkennt bereits Markdown-Bildzeilen wie `![...](...)` als eigenständige Struktur. Sie werden daher nicht mit dem nächsten Absatz verschmolzen.[^7_1] - -## Aufruf - -```bash -python pdf2md.py --no-ocr --images \ - Operation_Ljutsch_Band_1-_Reinhard_Otto_Kranz.pdf \ - Operation_Ljutsch_Band_1-_Reinhard_Otto_Kranz.md -``` - -Danach liegen die `.md`-Datei und ein Ordner namens `Operation_Ljutsch_Band_1-_Reinhard_Otto_Kranz_images` nebeneinander. Beim Verschieben solltest du beide zusammen mitnehmen. - -Eine wichtige Einschränkung: Die Bilder stehen nicht *als Bilddaten* in der Markdown-Datei, sondern als Verweise, etwa `![...](Operation_Ljutsch_Band_1-_Reinhard_Otto_Kranz_images/...)`. PyMuPDF4LLM kann auch Vektorgrafiken als Bildbereiche ausgeben; eingebetteter Text innerhalb solcher Bereiche kann je nach Erkennung Teil des Bildes werden. Vergleiche deshalb bei einigen Seiten die Position von Grafik und Bildunterschrift mit der PDF.[^7_1] - -[^7_2][^7_3] - -
⁂
- -[^7_1]: https://pymupdf.readthedocs.io/en/latest/pymupdf4llm/api.html - -[^7_2]: https://pymupdf.readthedocs.io/en/latest/pymupdf4llm/ - -[^7_3]: https://pymupdf.readthedocs.io/en/latest/pymupdf4llm/ocr-plugins.html - - ---- - -# Zeige das geänderte Skript vollständig. - -Hier ist das vollständige Skript mit `--images`. Es speichert extrahierte Grafiken in einem Ordner neben der Markdown-Datei und setzt relative Bildverweise, sodass du Datei und Bilderordner gemeinsam verschieben kannst. PyMuPDF4LLM bietet dafür die Optionen `write_images`, `image_path` und `image_format`.[^8_1] - -```python -#!/usr/bin/env python3 -"""PDF nach Markdown konvertieren, Absätze gliedern und optional Bilder speichern.""" - -import argparse -import re -import sys -from pathlib import Path - -import pymupdf4llm -from pymupdf4llm.ocr import tesseract_api - - -HEADING = re.compile(r"^\s{0,3}#{1,6}\s+\S") - -# Satzzeichen, optional gefolgt von schließenden Anführungszeichen/Klammern. -SENTENCE_END = re.compile(r"""[.!?…]+[”’"'»›)\]]*$""") - -FENCE = re.compile(r"^\s{0,3}(`{3,}|~{3,})") -LIST_ITEM = re.compile(r"^\s{0,3}(?:[-+*]|\d+[.)])\s+") -BLOCKQUOTE = re.compile(r"^\s{0,3}>\s?") -RULE = re.compile(r"^\s{0,3}(?:-\s*){3,}$|^\s{0,3}(?:\*\s*){3,}$") -IMAGE = re.compile(r"^\s*!\[.*?\]\(.*\)\s*$") - -# Beispiel: **==> picture [426 x 480] intentionally omitted <==** -OMITTED_IMAGE = re.compile( - r"^\s*(?:\*\*|__)?\s*==>\s*" - r"(?:picture|image|bild)\b.*?" - r"(?:intentionally omitted|omitted|ausgelassen)\s*" - r"<==\s*(?:\*\*|__)?\s*$", - re.IGNORECASE, -) - -CONTENTS_TITLE = re.compile( - r"^\s*(?:#{1,6}\s+)?" - r"(?:inhaltsverzeichnis|inhalt|contents|table of contents)" - r"\s*$", - re.IGNORECASE, -) - -TOC_WITH_PAGE = re.compile( - r"^\s*(?:[-*]\s+)?\S.+?" - r"(?:\.{2,}|\s{2,})" - r"(?:\d+|[ivxlcdm]+)\s*$", - re.IGNORECASE, -) - -TOC_NUMBERED = re.compile( - r"^\s*(?:[-*]\s+)?" - r"(?:\d+(?:\.\d+)*\.?\s+|Kapitel\s+\d+\b|Chapter\s+\d+\b)" - r".+", - re.IGNORECASE, -) - -HTML_BLOCK = re.compile( - r"^\s* bool: - return bool(SENTENCE_END.search(text.rstrip())) - - -def is_other_structure(line: str) -> bool: - """Markdown-Strukturen, die nicht zu Fließtext verbunden werden.""" - return bool( - LIST_ITEM.match(line) - or BLOCKQUOTE.match(line) - or RULE.match(line) - or IMAGE.match(line) - or HTML_BLOCK.match(line) - or line.lstrip().startswith("|") - or line.startswith((" ", "\t")) - ) - - -def normalize_markdown(markdown: str) -> str: - result: list[str] = [] - paragraph: list[str] = [] - - in_fence = False - fence_char = "" - fence_length = 0 - in_contents = False - - def blank_line() -> None: - if result and result[-1] != "": - result.append("") - - def flush_paragraph() -> None: - if paragraph: - result.append(" ".join(paragraph)) - paragraph.clear() - - def standalone(line: str) -> None: - flush_paragraph() - blank_line() - result.append(line) - result.append("") - - lines = markdown.replace("\r\n", "\n").replace("\r", "\n").split("\n") - - for original in lines: - line = original.rstrip() - marker = FENCE.match(line) - - if in_fence: - result.append(line) - if marker: - token = marker.group(1) - if token[^8_0] == fence_char and len(token) >= fence_length: - in_fence = False - continue - - if marker: - flush_paragraph() - blank_line() - result.append(line) - in_fence = True - fence_char = marker.group(1)[^8_0] - fence_length = len(marker.group(1)) - continue - - if not line.strip(): - if paragraph and ends_sentence(paragraph[-1]): - flush_paragraph() - blank_line() - elif not paragraph: - blank_line() - continue - - if CONTENTS_TITLE.match(line): - standalone(line) - in_contents = True - continue - - if HEADING.match(line): - standalone(line) - in_contents = False - continue - - if OMITTED_IMAGE.match(line): - standalone(line) - continue - - if TOC_WITH_PAGE.match(line): - standalone(line) - continue - - if in_contents and TOC_NUMBERED.match(line): - standalone(line) - continue - - if in_contents and ends_sentence(line): - in_contents = False - - if is_other_structure(line): - flush_paragraph() - result.append(line) - continue - - paragraph.append(line.strip()) - - flush_paragraph() - return "\n".join(result).strip() + "\n" - - -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 - - return markdown.replace( - absolute_dir + "/", - relative_dir + "/", - ) - - -def main() -> int: - parser = argparse.ArgumentParser( - description=( - "PDF in Markdown umwandeln; Fließtext nach Satzenden gliedern " - "und optional Grafiken speichern." - ) - ) - parser.add_argument("pdf", type=Path, help="Eingabe-PDF") - parser.add_argument( - "markdown", - type=Path, - nargs="?", - help="Ausgabe-Datei; Standard: gleicher Name mit .md", - ) - parser.add_argument( - "--ocr-language", - default="deu", - help="Tesseract-Sprachcode bei OCR (Standard: deu)", - ) - parser.add_argument( - "--no-ocr", - action="store_true", - help="OCR deaktivieren", - ) - parser.add_argument( - "--images", - action="store_true", - help="Grafiken speichern und in Markdown verlinken", - ) - parser.add_argument( - "--keep-headers", - action="store_true", - help="Wiederkehrende Seitenköpfe übernehmen", - ) - parser.add_argument( - "--keep-footers", - action="store_true", - help="Wiederkehrende Seitenfüße übernehmen", - ) - args = parser.parse_args() - - source = args.pdf.expanduser() - target = (args.markdown or source.with_suffix(".md")).expanduser() - - if not source.is_file(): - parser.error(f"Eingabedatei nicht gefunden: {source}") - - if source.resolve() == target.resolve(): - parser.error("Eingabe- und Ausgabedatei müssen verschieden sein.") - - image_dir = target.parent / f"{target.stem}_images" - - try: - target.parent.mkdir(parents=True, exist_ok=True) - - if args.images: - image_dir.mkdir(parents=True, exist_ok=True) - - markdown = pymupdf4llm.to_markdown( - str(source), - header=args.keep_headers, - footer=args.keep_footers, - use_ocr=not args.no_ocr, - ocr_language=args.ocr_language, - ocr_function=( - tesseract_api.exec_ocr if not args.no_ocr else None - ), - write_images=args.images, - image_path=str(image_dir.resolve()) if args.images else "", - image_format="png", - ) - - if args.images: - markdown = make_image_links_relative(markdown, image_dir) - - target.write_text(normalize_markdown(markdown), encoding="utf-8") - - except Exception as exc: - print(f"Fehler bei der Konvertierung: {exc}", file=sys.stderr) - return 1 - - print(f"Markdown geschrieben: {target}") - if args.images: - print(f"Bilder gespeichert in: {image_dir}") - - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) -``` - -Für deine PDF mit kopierbarem Text und Grafiken: - -```bash -python pdf2md.py --no-ocr --images \ - Operation_Ljutsch_Band_1-_Reinhard_Otto_Kranz.pdf \ - Operation_Ljutsch_Band_1-_Reinhard_Otto_Kranz.md -``` - -Falls die PDF stattdessen ein Scan ist, lasse `--no-ocr` weg. Die Grafiken stehen dann als Verweise in der `.md`-Datei; die eigentlichen Bilddateien liegen im gleichnamigen `_images`-Ordner. [^8_1] - -
⁂
- -[^8_1]: https://pymupdf.readthedocs.io/en/latest/pymupdf4llm/api.html - - ---- - -# Das Inhaltsverzeichnis in der MD-Datei sieht so aus: - -\#\#Inhaltsverzeichnis -Titel Vorwort: Berlin 2015 Teil I: Das Vermächtnis -1 Der Notar -2 Ein Brief – eine Liste – ein Vermächtnis -3 Die Grauen nehmen die Spur auf 4 Flucht aufs Land -5 Oie begegnet den Grauen -6 Ulms Elysium -7 Feindlich-freundliche Kooperation -8 Fischzug der Erinnerungen -9 Brand-Jagd Teil II: Verbündete -10 Samuel Nussbaum – Kryptologe -11 Nussbaums Talente -12 Dollar-Depot -13 Katharina -14 Reisevorbereitungen -15 Rudolfs CoCom-Geheimnis Teil III: Lettische Geschichten: Der Kreml-Flug – Operation Prelomlenie (Lichtbrechung) 16 Zweimal Riga 17 Hermannis Visvaldis 18 Auf dem Letten-Land 19 Kreml-Flug-Dirigent Oberst a.D. Valentin Kiparsky 20 Lettische Erzählungen 21 Nacht-Kampf mit den Grauen 22 Flucht zur Insel Ruhnu 23 Helsinki-Törn Teil IV: Russische Capricen 24 Operation Abendlicht – der Kreml-Flug Plan C 25 Helsinki-Positionen 26 Sankt Petersburg 27 Professor Kusnezow – Seismograph einer wirtschaftlichen Implosion 28 Unruhe 29 Sankt Petersburg – Moskau 30 Petrows Erzählungen – die politischen Hintergründe der Europäischen Perestroika 31 Die Dienste stochern im Nebel Der Autor Vorschau Band Zwei Impressum neobooks - -Woran liegt das ? - -Das liegt hauptsächlich an der **Absatzregel des Skripts**, nicht daran, dass die Einträge in der PDF zwingend so zusammenstehen. - -Die erste Zeile `##Inhaltsverzeichnis` wird von unserem Programm nicht als Überschrift erkannt: Die bisherige Erkennung erwartet ein Leerzeichen nach `##`, also `## Inhaltsverzeichnis`. Auch die spezielle Erkennung des Inhaltsverzeichnis-Titels erwartet dieses Leerzeichen. Damit wird `in_contents` nicht aktiviert. - -Danach greift genau die Regel, die wir für Fließtext eingebaut haben: Zeilen ohne Satzende werden über Zeilenumbrüche und Leerzeilen hinweg mit Leerzeichen verbunden. Inhaltsverzeichniseinträge wie `1 Der Notar` enden normalerweise nicht mit `.`, `?` oder `!`. Sie werden deshalb zu einem einzigen langen Absatz zusammengezogen. In deinem Ergebnis sieht man das besonders deutlich bei `3 Die Grauen nehmen die Spur auf 4 Flucht aufs Land`. - -Es gibt noch einen zweiten Punkt: Die bisherige Inhaltsverzeichniserkennung berücksichtigt vor allem nummerierte Einträge. Unnummerierte Zeilen wie `Titel`, `Vorwort`, `Teil I: Das Vermächtnis`, `Der Autor` und `Impressum` brauchen dort ebenfalls eine eigene Behandlung. - -Die unmittelbare Korrektur für die Überschrift wäre: - -```markdown -## Inhaltsverzeichnis -``` - -Das allein genügt für dein Beispiel aber noch nicht zuverlässig. Das Programm muss nach dieser Überschrift in einen eigenen Inhaltsverzeichnis-Modus wechseln und dort **jede erkannte Eintragszeile als eigene Zeile erhalten**, statt die Satzende-Regel für Fließtext anzuwenden. Falls PyMuPDF4LLM bereits mehrere Einträge *innerhalb derselben ausgegebenen Zeile* zusammengefügt hat, muss man zusätzlich anhand der PDF-Positionen beziehungsweise der Eintragsnummern trennen; aus dem fertigen Text ist die ursprüngliche Grenze nicht immer eindeutig rekonstruierbar. - diff --git a/README.md b/README.md index 6cfa4c9..1e5eb3d 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,4 @@ synthetisch erzeugt. ## Dateien - `pdf2md.py` – aktuelle Version -- `History/pdf2md_v1.py` … `History/pdf2md_v4.py` – frühere Entwicklungsstände (siehe `Info/`) -- `Info/` – Recherche und Entstehungsgeschichte der Skripte - `tests/` – pytest-Suite