pdf2md/pdf2md_v1.py

146 lines
4.1 KiB
Python
Raw Normal View History

#!/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*</?(?:table|tr|td|th|div|pre|p|h[1-6])\b", re.I)
def is_structural(line: str) -> 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())