fix: saubere PDF-Extraktion der Fortna-Anleitungen (lib/pdf_extract.py)

Neues Modul lib/pdf_extract.py fuer die Volltext-Konvertierung der 5
Hauptanleitungen, behebt die Qualitaetsprobleme der ersten Version:

- Doppelte Nummerierung entfernt: PDF-Gliederungsnummer wird nur fuer die
  Ueberschriften-Ebene ausgewertet, im RST-Text weggelassen (Sphinx nummeriert)
- Bilder positionsbasiert (bbox) extrahiert und als .. figure:: mit Caption
  an der richtigen Textstelle referenziert (625 Refs, 0 fehlende Bilder)
- Fliesstext zu Absaetzen zusammengefuehrt statt je PDF-Zeile ein Absatz
- Aufzaehlungen als RST-Listen (Bullet-only-Zeilen mit Folgezeile gemergt,
  zusammengelaufene Bullets aufgesplittet)
- Aenderungshistorie-/Dokumentinfo-Tabellen und TOC-Seiten uebersprungen
- Symbolfont-Zeichen (PUA) auf Unicode gemappt

fortna_convert_chapters.py: nutzt pdf_extract statt extract_products, lesbare
Dokumenttitel je Kapitel, Verzeichnislisten fuellen Kapitel ohne DE-Quelle mit
vorhandenen Fremdsprachen (Ersatzteilliste EN).
build_index.py / conf.py: Fremd-Altdateien aus dem Build ausgeschlossen.
Alte Test-.rst aus fruehen convert_to_rst.py-Laeufen entfernt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michael Stangl
2026-09-23 13:44:09 +02:00
co-authored by Claude Opus 4.8
parent e7aa3b4e4b
commit c912b1e9d5
5 changed files with 372 additions and 14 deletions
+29 -12
View File
@@ -34,8 +34,9 @@ import sys
from collections import defaultdict
from pathlib import Path
# extract_products stellt die robuste PDF-Extraktion bereit (gleiches lib/-Verzeichnis)
import extract_products as ep
# pdf_extract stellt die positionsbasierte Text+Bild-Extraktion bereit
# (verwebt Text und Bilder, entfernt PDF-Ueberschriftennummern).
import pdf_extract
log = logging.getLogger(__name__)
@@ -71,6 +72,16 @@ def load_inventory(path: Path) -> dict:
return json.load(f)
# Lesbarer Dokumenttitel je Zielkapitel (statt PDF-Dateiname als Ueberschrift).
CHAPTER_DOC_TITLE = {
"04_betrieb/betriebsanleitung": "Betriebsanleitung",
"05_wartung/wartungsanleitung": "Wartungsanleitung",
"06_steuerung/ils": "Funktionsbeschreibung Steuerung ILS",
"06_steuerung/omniflo": "Funktionsbeschreibung Steuerung OMNIFLO",
"06_steuerung/loading_loop": "Funktionsbeschreibung Steuerung Loading Loop",
}
# ---------------------------------------------------------------------------
# action="convert": Volltext + Bilder
# ---------------------------------------------------------------------------
@@ -104,14 +115,13 @@ def convert_manual(item: dict, source_root: Path, chapters_dir: Path,
for old in chapter_dir.glob("*.de.rst"):
old.unlink()
# Bilder extrahieren (extract_products entfernt kleine Icons < 5KB automatisch)
img_count = ep.extract_images(src_pdf, img_dir)
dup = ep.dedup_images(img_dir)
log.info(" %d Bilder (%d Duplikate entfernt)", img_count, dup)
# Text nach RST extrahieren (positionsbasiert, Kopf-/Fusszeilen gefiltert)
blocks = ep.extract_text_to_rst(src_pdf, rst_path)
log.info(" %d Textbloecke -> %s", blocks, rst_path.name)
# Text + Bilder positionsbasiert extrahieren (verwoben in Lesereihenfolge,
# PDF-Ueberschriftennummern entfernt, Bilder als .. figure:: referenziert).
doc_title = CHAPTER_DOC_TITLE.get(item["chapter"], slugify(item["filename"]))
blocks, img_count = pdf_extract.extract_manual(src_pdf, rst_path, img_dir,
title=doc_title)
log.info(" %d Textbloecke, %d Bilder (referenziert) -> %s",
blocks, img_count, rst_path.name)
# ---------------------------------------------------------------------------
@@ -254,7 +264,7 @@ def main() -> None:
items = [i for i in inv["items"] if i["lang"] == args.lang and i["chapter"]]
# 1. Volltext-Konvertierung
# 1. Volltext-Konvertierung (nur Zielsprache)
if args.only in (None, "convert"):
conv = [i for i in items if i["action"] == "convert"]
log.info("--- %d Hauptanleitungen konvertieren ---", len(conv))
@@ -263,7 +273,14 @@ def main() -> None:
# 2. Verzeichnislisten
if args.only in (None, "list"):
list_items = [i for i in items if i["action"] == "list"]
# Zielsprache bevorzugen; Kapitel, die in der Zielsprache KEINE list-Datei
# haben (z.B. Ersatzteilliste nur EN), mit den vorhandenen Fremdsprachen
# auffuellen, damit das Kapitel nicht leer bleibt.
all_list = [i for i in inv["items"]
if i["chapter"] and i["action"] == "list"]
chapters_with_lang = {i["chapter"] for i in all_list if i["lang"] == args.lang}
list_items = [i for i in all_list
if i["lang"] == args.lang or i["chapter"] not in chapters_with_lang]
by_chapter: dict[str, list[dict]] = defaultdict(list)
for it in list_items:
by_chapter[it["chapter"]].append(it)