feat: Bild-Routing, Logo/Symbol/Fragment-Filter, Produktprogramm-Bilder

Bild-Verarbeitung der Anleitungen (lib/pdf_extract.py) erweitert:

- Routing nach Kapitel-Typ: generelle Kapitel (Steuerung) -> assets/shared/
  images/<subdir>, projektbezogene (Betrieb/Wartung) -> projekte/fortna/images.
  Fotos (.jpg) IMMER projektspezifisch, unabhaengig vom Kapitel.
- Benennung aus Bildunterschrift: <Stichwort>_NN.png (z.B. Anlage_Uebersicht_01);
  Fotos img_NNN.jpg, SW-Skizzen >10KB sketch_NNN.png, Rest img_NNN.png.
- Logos: nur wiederkehrende (>=3x) breite Randgrafiken gelten als Logo -> alle
  Aufloesungen nach assets/shared/logos/logo_<n>_<breite>px, nicht inline.
- Wertlose Fragmente (flach <=55px, breit, SW = gerenderte Kapitelnummern wie
  "5.2.6") werden verworfen.
- Ziel-Bildordner werden vor jedem Lauf geleert.

extract_products.py: Produktprogramm-Bilder -> assets/shared/images/
produktprogramm/{omniflo,ils,cpc,geruest,trolley} (generelle Produktbilder).

.gitignore: extrahierte Bildmassen ausgeschlossen, nur kuratierte symbols/ +
logos/ versioniert.

Voller PDF-Build sauber (lualatex exit 0, 589 Seiten, 638 Bild-Refs, 0 fehlend).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michael Stangl
2026-09-23 14:35:19 +02:00
co-authored by Claude Opus 4.8
parent bbfe17ec37
commit 3ebfe06139
11 changed files with 356 additions and 72 deletions
+80 -23
View File
@@ -81,6 +81,32 @@ CHAPTER_DOC_TITLE = {
"06_steuerung/loading_loop": "Funktionsbeschreibung Steuerung Loading Loop",
}
# Kapitel-Typ: "general" -> Bilder sind wiederverwendbar (Maschinenelemente,
# Steuerungs-Funktionsbeschreibungen) -> assets/shared
# "project" -> projektspezifisch (Betrieb/Wartung der Anlage)
# -> projekte/fortna/images
# Fotos (.jpg) landen unabhaengig davon IMMER projektspezifisch (siehe
# convert_manual: Routing ueber Klassifizierung im Aufrufer wird durch
# pdf_extract je Bild nicht getrennt - daher legen wir generelle Kapitel nach
# assets, projektbezogene nach projekte/fortna). Feinsteuerung .jpg->projekt
# erfolgt zusaetzlich unten.
CHAPTER_TYPE = {
"04_betrieb/betriebsanleitung": "project",
"05_wartung/wartungsanleitung": "project",
"06_steuerung/ils": "general",
"06_steuerung/omniflo": "general",
"06_steuerung/loading_loop": "general",
}
# kurzer Unterordner-Name je Kapitel fuer die Bildablage
CHAPTER_IMG_SUBDIR = {
"04_betrieb/betriebsanleitung": "betrieb",
"05_wartung/wartungsanleitung": "wartung",
"06_steuerung/ils": "steuerung_ils",
"06_steuerung/omniflo": "steuerung_omniflo",
"06_steuerung/loading_loop": "steuerung_loading_loop",
}
# ---------------------------------------------------------------------------
# action="convert": Volltext + Bilder
@@ -88,18 +114,38 @@ CHAPTER_DOC_TITLE = {
def convert_manual(item: dict, source_root: Path, chapters_dir: Path,
dry_run: bool, symbols_dir: Path | None = None,
symbol_registry: dict | None = None) -> None:
"""Konvertiert eine Hauptanleitung nach chapters/<kapitel>/."""
symbol_registry: dict | None = None,
logos_dir: Path | None = None,
logo_registry: dict | None = None,
project_dir: Path | None = None,
assets_dir: Path | None = None) -> None:
"""Konvertiert eine Hauptanleitung nach chapters/<kapitel>/.
Bild-Routing je Kapitel:
general -> assets/shared/images/<subdir>
project -> projekte/fortna/images/<subdir>
Fotos (.jpg) gehen unabhaengig davon IMMER nach projekte/fortna/images/<subdir>.
"""
chapter = item["chapter"]
src_pdf = source_root / item["rel_path"]
chapter_dir = chapters_dir / item["chapter"]
chapter_dir = chapters_dir / chapter
stem = slugify(item["filename"])
rst_path = chapter_dir / f"{stem}.de.rst"
img_dir = chapter_dir / "images"
log.info("CONVERT %s", item["chapter"])
subdir = CHAPTER_IMG_SUBDIR.get(chapter, slugify(chapter))
ctype = CHAPTER_TYPE.get(chapter, "project")
assets_base = assets_dir or (chapters_dir.parent / "assets" / "shared" / "images")
project_base = (project_dir or (chapters_dir.parent / "projekte" / "fortna")) / "images"
# generelle Grafiken -> assets, projektbezogene -> projekt; Fotos immer projekt
image_dest = (assets_base if ctype == "general" else project_base) / subdir
photo_dest = project_base / subdir
log.info("CONVERT %s (%s)", chapter, ctype)
log.info(" Quelle: %s (%d MB)", item["rel_path"],
item["size_bytes"] // (1024 * 1024))
log.info(" -> %s", rst_path.relative_to(chapters_dir))
log.info(" Bilder -> %s (Fotos -> %s)",
image_dest.relative_to(chapters_dir.parent),
photo_dest.relative_to(chapters_dir.parent))
if dry_run:
return
@@ -108,21 +154,21 @@ def convert_manual(item: dict, source_root: Path, chapters_dir: Path,
log.error(" Quelle fehlt: %s", src_pdf)
return
# Zielordner vorbereiten: vorhandene RST/Bilder ersetzen (aus einem Guss)
# Zielordner vorbereiten und ALLE vorher existierenden Bilder darin loeschen.
chapter_dir.mkdir(parents=True, exist_ok=True)
if img_dir.exists():
shutil.rmtree(img_dir)
# Alte .de.rst im Zielordner entfernen (Testbestand)
for d in (image_dest, photo_dest, chapter_dir / "images"):
if d.exists():
shutil.rmtree(d)
for old in chapter_dir.glob("*.de.rst"):
old.unlink()
# 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"]))
doc_title = CHAPTER_DOC_TITLE.get(chapter, slugify(item["filename"]))
blocks, img_count = pdf_extract.extract_manual(
src_pdf, rst_path, img_dir, title=doc_title,
symbols_dir=symbols_dir, symbol_registry=symbol_registry)
log.info(" %d Textbloecke, %d Bilder (referenziert) -> %s",
src_pdf, rst_path, chapter_dir / "images", title=doc_title,
symbols_dir=symbols_dir, symbol_registry=symbol_registry,
logos_dir=logos_dir, logo_registry=logo_registry,
image_dest=image_dest, photo_dest=photo_dest)
log.info(" %d Textbloecke, %d Bilder -> %s",
blocks, img_count, rst_path.name)
@@ -270,17 +316,28 @@ def main() -> None:
if args.only in (None, "convert"):
conv = [i for i in items if i["action"] == "convert"]
log.info("--- %d Hauptanleitungen konvertieren ---", len(conv))
# Symbole dokumentuebergreifend zentral in assets/shared/symbols/ ablegen
# und nur einmal referenzieren (Warndreiecke etc. tauchen in mehreren
# Anleitungen auf). Registry ueber alle Dokumente hinweg teilen.
symbols_dir = chapters_dir.parent / "assets" / "shared" / "symbols"
# Symbole + Logos dokumentuebergreifend zentral in assets/shared ablegen
# und nur einmal referenzieren. Registries ueber alle Dokumente teilen.
base = chapters_dir.parent
symbols_dir = base / "assets" / "shared" / "symbols"
logos_dir = base / "assets" / "shared" / "logos"
assets_img_dir = base / "assets" / "shared" / "images"
project_dir = base / "projekte" / "fortna"
symbol_registry: dict = {}
logo_registry: dict = {}
# Vor dem Lauf zentrale Ziel-Bildordner leeren (frischer Stand).
if not dry_run:
for d in (symbols_dir, logos_dir, assets_img_dir, project_dir / "images"):
if d.exists():
shutil.rmtree(d)
for it in conv:
convert_manual(it, source_root, chapters_dir, dry_run,
symbols_dir=symbols_dir, symbol_registry=symbol_registry)
symbols_dir=symbols_dir, symbol_registry=symbol_registry,
logos_dir=logos_dir, logo_registry=logo_registry,
project_dir=project_dir, assets_dir=assets_img_dir)
if not dry_run:
log.info("Symbole zentral: %d eindeutige -> %s",
len(symbol_registry), symbols_dir)
log.info("Symbole zentral: %d | Logos: %d -> assets/shared/",
len(symbol_registry), len(logo_registry))
# 2. Verzeichnislisten
if args.only in (None, "list"):