feat: Symbole zentralisieren + Fragment-Ueberschriften filtern
Perceptual-Hash-basierte Symbol-/Duplikaterkennung in lib/pdf_extract.py: - average_hash() (aHash) + Hamming-Distanz statt exaktem Byte-Hash: erkennt visuell gleiche Bilder trotz unterschiedlicher Skalierung/Kompression - Symbole (klein <=260px UND mehrfach vorkommend, z.B. Warndreiecke) werden EINMAL zentral nach assets/shared/symbols/symbol_<hash>.png abgelegt und im RST als .. image:: (12mm, zentriert) auf die eine Quelle referenziert - dokumentuebergreifend (registry). Ein Warndreieck: 1x gespeichert statt 58x - grossformatige Inhaltsbild-Duplikate ebenfalls ueber aHash zusammengefuehrt - Fragment-"Ueberschriften" (POS, li., re., "bar, max. 10 bar") unterdrueckt - Altbestand assets/shared/symbols/img_*.png (durcheinander benannt) entfernt Wirkung: omniflo images/ 83 -> 62, 14 zentrale Symbole statt zig Duplikate. Voller PDF-Build sauber (lualatex exit 0, 719 Seiten). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 227 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 183 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
@@ -50,6 +50,15 @@ sauberere `.rst` erzeugt:
|
|||||||
vorhanden – der folgenden „Abbildung N: …"-Zeile als Caption eingebaut. (625 figure-Refs,
|
vorhanden – der folgenden „Abbildung N: …"-Zeile als Caption eingebaut. (625 figure-Refs,
|
||||||
0 fehlende Bilddateien.) Duplikate werden per MD5 entfernt, Titelseiten-Deko und
|
0 fehlende Bilddateien.) Duplikate werden per MD5 entfernt, Titelseiten-Deko und
|
||||||
Vollseiten-Hintergründe herausgefiltert.
|
Vollseiten-Hintergründe herausgefiltert.
|
||||||
|
- **Symbole/Warnzeichen zentral:** Kleine (≤ 260 px), mehrfach vorkommende Bilder (Warndreiecke,
|
||||||
|
Piktogramme) werden per Perceptual-Hash (aHash + Hamming-Distanz ≤ 6) erkannt, **einmal** zentral
|
||||||
|
nach `assets/shared/symbols/symbol_<hash>.png` abgelegt und im RST als `.. image::` (12 mm,
|
||||||
|
zentriert) auf die eine Quelle verwiesen – dokumentübergreifend. Vorher lag dasselbe Warndreieck
|
||||||
|
z. B. 8× als `img_003/018/020/025/026/031/035…` in einem einzigen Kapitel. Auch großformatige
|
||||||
|
Inhaltsbild-Duplikate werden über den aHash zusammengeführt (nur einmal abgelegt, mehrfach
|
||||||
|
referenziert). Wirkung: omniflo-`images/` von 83 → 62, Warndreieck 1× statt 58× gespeichert.
|
||||||
|
- **Fragment-„Überschriften" unterdrückt:** Bildlegenden/Tabellenzellen mit führender Nummer
|
||||||
|
(„POS", „li.", „re.", „bar, max. 10 bar") werden nicht mehr als Überschrift gesetzt.
|
||||||
- **Fließtext zu Absätzen zusammengeführt:** PDF-Zeilenumbrüche werden nicht mehr zu je einem
|
- **Fließtext zu Absätzen zusammengeführt:** PDF-Zeilenumbrüche werden nicht mehr zu je einem
|
||||||
Absatz; zusammenhängende Zeilen bilden einen Absatz, Satzende beendet ihn.
|
Absatz; zusammenhängende Zeilen bilden einen Absatz, Satzende beendet ihn.
|
||||||
- **Aufzählungen als RST-Listen:** einzeln stehende Bullet-Zeichen werden mit der Folgezeile
|
- **Aufzählungen als RST-Listen:** einzeln stehende Bullet-Zeichen werden mit der Folgezeile
|
||||||
|
|||||||
@@ -87,7 +87,8 @@ CHAPTER_DOC_TITLE = {
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def convert_manual(item: dict, source_root: Path, chapters_dir: Path,
|
def convert_manual(item: dict, source_root: Path, chapters_dir: Path,
|
||||||
dry_run: bool) -> None:
|
dry_run: bool, symbols_dir: Path | None = None,
|
||||||
|
symbol_registry: dict | None = None) -> None:
|
||||||
"""Konvertiert eine Hauptanleitung nach chapters/<kapitel>/."""
|
"""Konvertiert eine Hauptanleitung nach chapters/<kapitel>/."""
|
||||||
src_pdf = source_root / item["rel_path"]
|
src_pdf = source_root / item["rel_path"]
|
||||||
chapter_dir = chapters_dir / item["chapter"]
|
chapter_dir = chapters_dir / item["chapter"]
|
||||||
@@ -118,8 +119,9 @@ def convert_manual(item: dict, source_root: Path, chapters_dir: Path,
|
|||||||
# Text + Bilder positionsbasiert extrahieren (verwoben in Lesereihenfolge,
|
# Text + Bilder positionsbasiert extrahieren (verwoben in Lesereihenfolge,
|
||||||
# PDF-Ueberschriftennummern entfernt, Bilder als .. figure:: referenziert).
|
# PDF-Ueberschriftennummern entfernt, Bilder als .. figure:: referenziert).
|
||||||
doc_title = CHAPTER_DOC_TITLE.get(item["chapter"], slugify(item["filename"]))
|
doc_title = CHAPTER_DOC_TITLE.get(item["chapter"], slugify(item["filename"]))
|
||||||
blocks, img_count = pdf_extract.extract_manual(src_pdf, rst_path, img_dir,
|
blocks, img_count = pdf_extract.extract_manual(
|
||||||
title=doc_title)
|
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",
|
log.info(" %d Textbloecke, %d Bilder (referenziert) -> %s",
|
||||||
blocks, img_count, rst_path.name)
|
blocks, img_count, rst_path.name)
|
||||||
|
|
||||||
@@ -268,8 +270,17 @@ def main() -> None:
|
|||||||
if args.only in (None, "convert"):
|
if args.only in (None, "convert"):
|
||||||
conv = [i for i in items if i["action"] == "convert"]
|
conv = [i for i in items if i["action"] == "convert"]
|
||||||
log.info("--- %d Hauptanleitungen konvertieren ---", len(conv))
|
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"
|
||||||
|
symbol_registry: dict = {}
|
||||||
for it in conv:
|
for it in conv:
|
||||||
convert_manual(it, source_root, chapters_dir, dry_run)
|
convert_manual(it, source_root, chapters_dir, dry_run,
|
||||||
|
symbols_dir=symbols_dir, symbol_registry=symbol_registry)
|
||||||
|
if not dry_run:
|
||||||
|
log.info("Symbole zentral: %d eindeutige -> %s",
|
||||||
|
len(symbol_registry), symbols_dir)
|
||||||
|
|
||||||
# 2. Verzeichnislisten
|
# 2. Verzeichnislisten
|
||||||
if args.only in (None, "list"):
|
if args.only in (None, "list"):
|
||||||
|
|||||||
@@ -22,12 +22,47 @@ Oeffentliche Funktion: extract_manual(pdf_path, rst_path, img_dir) -> (blocks, i
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import io
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Schwellwerte fuer die Symbol-/Icon-Erkennung
|
||||||
|
SYMBOL_MAX_EDGE = 260 # Bilder <= dieser Kantenlaenge gelten als Symbol-Kandidat
|
||||||
|
SYMBOL_MIN_COUNT = 2 # ... wenn dasselbe Bild mind. so oft vorkommt
|
||||||
|
AHASH_THRESHOLD = 6 # max. Hamming-Distanz, ab der zwei aHashes als gleich gelten
|
||||||
|
|
||||||
|
|
||||||
|
def average_hash(image_bytes: bytes, size: int = 8) -> int:
|
||||||
|
"""Perceptual Average-Hash (aHash) eines Bildes als Integer.
|
||||||
|
|
||||||
|
Graustufen -> size x size -> Bit je Pixel (>= Mittelwert). Zwei visuell
|
||||||
|
aehnliche Bilder (auch bei leicht anderer Skalierung/Kompression, wie
|
||||||
|
mehrfach eingebettete Warnsymbole aus Word) haben eine kleine Hamming-
|
||||||
|
Distanz. Robuster als ein exakter Byte-/Pixel-Hash.
|
||||||
|
"""
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
try:
|
||||||
|
im = Image.open(io.BytesIO(image_bytes)).convert("L").resize((size, size))
|
||||||
|
px = list(im.getdata())
|
||||||
|
avg = sum(px) / len(px)
|
||||||
|
bits = 0
|
||||||
|
for i, p in enumerate(px):
|
||||||
|
if p >= avg:
|
||||||
|
bits |= (1 << i)
|
||||||
|
return bits
|
||||||
|
except Exception:
|
||||||
|
# Fallback: Byte-Hash als Integer, wenn PIL das Bild nicht oeffnen kann
|
||||||
|
return int(hashlib.md5(image_bytes).hexdigest()[:16], 16)
|
||||||
|
|
||||||
|
|
||||||
|
def hamming(a: int, b: int) -> int:
|
||||||
|
"""Hamming-Distanz zweier Integer-Hashes (Anzahl unterschiedlicher Bits)."""
|
||||||
|
return bin(a ^ b).count("1")
|
||||||
|
|
||||||
RST_H = {1: "=", 2: "-", 3: "~", 4: "^", 5: '"'}
|
RST_H = {1: "=", 2: "-", 3: "~", 4: "^", 5: '"'}
|
||||||
|
|
||||||
# Symbolfont-Zeichen (Private Use Area) auf Unicode abbilden bzw. entfernen.
|
# Symbolfont-Zeichen (Private Use Area) auf Unicode abbilden bzw. entfernen.
|
||||||
@@ -80,14 +115,28 @@ def _strip_heading_number(num: str, rest: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def extract_manual(pdf_path: Path, rst_path: Path, img_dir: Path,
|
def extract_manual(pdf_path: Path, rst_path: Path, img_dir: Path,
|
||||||
title: str | None = None) -> tuple[int, int]:
|
title: str | None = None,
|
||||||
|
symbols_dir: Path | None = None,
|
||||||
|
symbol_registry: dict | None = None) -> tuple[int, int]:
|
||||||
"""Extrahiert Text + Bilder eines Anleitungs-PDF in eine .rst-Datei.
|
"""Extrahiert Text + Bilder eines Anleitungs-PDF in eine .rst-Datei.
|
||||||
|
|
||||||
|
Symbole/Icons (kleine, mehrfach vorkommende Bilder wie Warndreiecke) werden
|
||||||
|
NICHT pro Vorkommen dupliziert, sondern einmal zentral nach symbols_dir
|
||||||
|
(assets/shared/symbols) als symbol_<hash>.png abgelegt und im RST als
|
||||||
|
`.. image::` mit relativem Pfad referenziert. symbol_registry (dict
|
||||||
|
visual_hash -> relativer Pfad) wird dokumentuebergreifend wiederverwendet.
|
||||||
|
|
||||||
|
Inhaltsbild-Duplikate (gleicher visueller Hash) innerhalb des Dokuments
|
||||||
|
werden ebenfalls nur einmal abgelegt und mehrfach referenziert.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(anzahl_textbloecke, anzahl_bilder)
|
(anzahl_textbloecke, anzahl_bilder)
|
||||||
"""
|
"""
|
||||||
import fitz
|
import fitz
|
||||||
|
|
||||||
|
if symbol_registry is None:
|
||||||
|
symbol_registry = {}
|
||||||
|
|
||||||
doc = fitz.open(str(pdf_path))
|
doc = fitz.open(str(pdf_path))
|
||||||
img_dir.mkdir(parents=True, exist_ok=True)
|
img_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
@@ -108,11 +157,19 @@ def extract_manual(pdf_path: Path, rst_path: Path, img_dir: Path,
|
|||||||
and ("datum" in low or "kapitel" in low)):
|
and ("datum" in low or "kapitel" in low)):
|
||||||
skip_pages.add(pn)
|
skip_pages.add(pn)
|
||||||
|
|
||||||
# --- Elemente (Text + Bild) pro Seite in Lesereihenfolge sammeln ---
|
# --- Pass 1: Text- und Bild-Elemente sammeln (Bilder nur puffern) ---
|
||||||
# element: {"kind": "text"|"image", "y": float, "page": int, ...}
|
# Bilder werden zunaechst nicht geschrieben, sondern per Perceptual-Hash
|
||||||
|
# (aHash) einem Cluster zugeordnet. Visuell aehnliche Bilder (gleiches Symbol
|
||||||
|
# in anderer Skalierung) landen im selben Cluster. Erst nach dem Scan steht
|
||||||
|
# fest, welches Cluster mehrfach vorkommt (Symbol-Kriterium).
|
||||||
elements: list[dict] = []
|
elements: list[dict] = []
|
||||||
seen_hashes: set[str] = set()
|
clusters: list[dict] = [] # [{ahash, data, ext, w, h, count}]
|
||||||
img_counter = 0
|
|
||||||
|
def _find_cluster(ah: int) -> int:
|
||||||
|
for ci, c in enumerate(clusters):
|
||||||
|
if hamming(ah, c["ahash"]) <= AHASH_THRESHOLD:
|
||||||
|
return ci
|
||||||
|
return -1
|
||||||
|
|
||||||
for pn in range(len(doc)):
|
for pn in range(len(doc)):
|
||||||
if pn in skip_pages:
|
if pn in skip_pages:
|
||||||
@@ -148,8 +205,7 @@ def extract_manual(pdf_path: Path, rst_path: Path, img_dir: Path,
|
|||||||
"text": text, "size": round(max_size, 1), "bold": bold,
|
"text": text, "size": round(max_size, 1), "bold": bold,
|
||||||
})
|
})
|
||||||
|
|
||||||
# Bilder mit Position. Titelseiten (erste 2 Seiten) enthalten nur
|
# Bilder mit Position. Titelseiten (erste 2 Seiten) = nur Logo/Deko.
|
||||||
# Logo-/Deko-Bilder -> ueberspringen.
|
|
||||||
if pn < 2:
|
if pn < 2:
|
||||||
continue
|
continue
|
||||||
for im in page.get_image_info(xrefs=True):
|
for im in page.get_image_info(xrefs=True):
|
||||||
@@ -161,7 +217,7 @@ def extract_manual(pdf_path: Path, rst_path: Path, img_dir: Path,
|
|||||||
# Vollseiten-Deko / Hintergrundbilder ueberspringen
|
# Vollseiten-Deko / Hintergrundbilder ueberspringen
|
||||||
if (b[2] - b[0]) > pw * 0.95 and (b[3] - b[1]) > ph * 0.95:
|
if (b[2] - b[0]) > pw * 0.95 and (b[3] - b[1]) > ph * 0.95:
|
||||||
continue
|
continue
|
||||||
if w < 40 or h < 40: # Icons
|
if w < 40 or h < 40: # winzige Icons
|
||||||
continue
|
continue
|
||||||
base = doc.extract_image(xref)
|
base = doc.extract_image(xref)
|
||||||
if not base:
|
if not base:
|
||||||
@@ -169,20 +225,69 @@ def extract_manual(pdf_path: Path, rst_path: Path, img_dir: Path,
|
|||||||
data = base["image"]
|
data = base["image"]
|
||||||
if len(data) < 3000:
|
if len(data) < 3000:
|
||||||
continue
|
continue
|
||||||
md5 = hashlib.md5(data).hexdigest()
|
ah = average_hash(data)
|
||||||
if md5 in seen_hashes:
|
ci = _find_cluster(ah)
|
||||||
continue # exaktes Duplikat (wiederkehrendes Logo/Symbol)
|
if ci < 0:
|
||||||
seen_hashes.add(md5)
|
clusters.append({"ahash": ah, "data": data,
|
||||||
img_counter += 1
|
"ext": base.get("ext", "png"),
|
||||||
ext = base.get("ext", "png")
|
"w": w, "h": h, "count": 1})
|
||||||
fname = f"img_{img_counter:03d}.{ext}"
|
ci = len(clusters) - 1
|
||||||
(img_dir / fname).write_bytes(data)
|
else:
|
||||||
|
clusters[ci]["count"] += 1
|
||||||
elements.append({
|
elements.append({
|
||||||
"kind": "image", "y": b[1], "page": pn + 1, "file": fname,
|
"kind": "image", "y": b[1], "page": pn + 1, "cluster": ci,
|
||||||
})
|
})
|
||||||
|
|
||||||
doc.close()
|
doc.close()
|
||||||
|
|
||||||
|
# --- Pass 2: entscheiden, wohin jedes Bild-Cluster geschrieben wird ---
|
||||||
|
# Symbol: klein UND mehrfach vorkommend -> zentral assets/shared/symbols.
|
||||||
|
# Sonst (inkl. mehrfach genutzte Inhaltsbilder): einmal in Kapitel-images.
|
||||||
|
if symbols_dir is None:
|
||||||
|
symbols_dir = img_dir.parent.parent.parent / "assets" / "shared" / "symbols"
|
||||||
|
img_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# relativer Pfad-Praefix vom RST (liegt in img_dir.parent) zu symbols_dir
|
||||||
|
try:
|
||||||
|
sym_rel_prefix = Path(
|
||||||
|
__import__("os").path.relpath(symbols_dir, img_dir.parent)
|
||||||
|
).as_posix()
|
||||||
|
except ValueError:
|
||||||
|
sym_rel_prefix = symbols_dir.as_posix()
|
||||||
|
|
||||||
|
cluster_target: list[str] = [""] * len(clusters) # ci -> RST-Bildpfad
|
||||||
|
cluster_is_symbol: list[bool] = [False] * len(clusters)
|
||||||
|
local_counter = 0
|
||||||
|
for ci, c in enumerate(clusters):
|
||||||
|
is_symbol = (max(c["w"], c["h"]) <= SYMBOL_MAX_EDGE
|
||||||
|
and c["count"] >= SYMBOL_MIN_COUNT)
|
||||||
|
cluster_is_symbol[ci] = is_symbol
|
||||||
|
if is_symbol:
|
||||||
|
# Dokumentuebergreifende Registry per aHash-Naehe durchsuchen.
|
||||||
|
match = None
|
||||||
|
for reg_ah, reg_file in symbol_registry.items():
|
||||||
|
if hamming(c["ahash"], reg_ah) <= AHASH_THRESHOLD:
|
||||||
|
match = reg_file
|
||||||
|
break
|
||||||
|
if match is None:
|
||||||
|
symbols_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
match = f"symbol_{c['ahash'] & 0xffffffff:08x}.{c['ext']}"
|
||||||
|
(symbols_dir / match).write_bytes(c["data"])
|
||||||
|
symbol_registry[c["ahash"]] = match
|
||||||
|
cluster_target[ci] = f"{sym_rel_prefix}/{match}"
|
||||||
|
else:
|
||||||
|
local_counter += 1
|
||||||
|
fname = f"img_{local_counter:03d}.{c['ext']}"
|
||||||
|
(img_dir / fname).write_bytes(c["data"])
|
||||||
|
cluster_target[ci] = f"images/{fname}"
|
||||||
|
|
||||||
|
# Bild-Elemente mit Zielpfad versehen
|
||||||
|
for e in elements:
|
||||||
|
if e["kind"] == "image":
|
||||||
|
e["path"] = cluster_target[e["cluster"]]
|
||||||
|
e["is_symbol"] = cluster_is_symbol[e["cluster"]]
|
||||||
|
img_counter = local_counter
|
||||||
|
|
||||||
# --- pro Seite nach Y-Position sortieren (Lesereihenfolge) ---
|
# --- pro Seite nach Y-Position sortieren (Lesereihenfolge) ---
|
||||||
elements.sort(key=lambda e: (e["page"], e["y"]))
|
elements.sort(key=lambda e: (e["page"], e["y"]))
|
||||||
|
|
||||||
@@ -233,6 +338,19 @@ def extract_manual(pdf_path: Path, rst_path: Path, img_dir: Path,
|
|||||||
for idx, e in enumerate(merged):
|
for idx, e in enumerate(merged):
|
||||||
if e["kind"] == "image":
|
if e["kind"] == "image":
|
||||||
flush_para()
|
flush_para()
|
||||||
|
path = e.get("path", "")
|
||||||
|
if not path:
|
||||||
|
continue
|
||||||
|
if e.get("is_symbol"):
|
||||||
|
# Symbol/Warnzeichen: kleine, zentrierte Grafik ohne Caption-Box,
|
||||||
|
# verweist auf die eine zentrale Quelle in assets/shared/symbols.
|
||||||
|
out.append("")
|
||||||
|
out.append(f".. image:: {path}")
|
||||||
|
out.append(" :align: center")
|
||||||
|
out.append(" :height: 12mm")
|
||||||
|
out.append("")
|
||||||
|
blocks += 1
|
||||||
|
continue
|
||||||
caption = ""
|
caption = ""
|
||||||
if idx + 1 < n and merged[idx + 1]["kind"] == "text":
|
if idx + 1 < n and merged[idx + 1]["kind"] == "text":
|
||||||
cm = RE_CAPTION.match(merged[idx + 1]["text"])
|
cm = RE_CAPTION.match(merged[idx + 1]["text"])
|
||||||
@@ -240,7 +358,7 @@ def extract_manual(pdf_path: Path, rst_path: Path, img_dir: Path,
|
|||||||
caption = merged[idx + 1]["text"]
|
caption = merged[idx + 1]["text"]
|
||||||
merged[idx + 1]["_consumed"] = True
|
merged[idx + 1]["_consumed"] = True
|
||||||
out.append("")
|
out.append("")
|
||||||
out.append(f".. figure:: images/{e['file']}")
|
out.append(f".. figure:: {path}")
|
||||||
out.append(" :align: center")
|
out.append(" :align: center")
|
||||||
out.append(" :width: 80%")
|
out.append(" :width: 80%")
|
||||||
if caption:
|
if caption:
|
||||||
@@ -255,11 +373,22 @@ def extract_manual(pdf_path: Path, rst_path: Path, img_dir: Path,
|
|||||||
|
|
||||||
text = e["text"]
|
text = e["text"]
|
||||||
hm = RE_HEADING.match(text)
|
hm = RE_HEADING.match(text)
|
||||||
# Ueberschrift NUR mit fuehrender PDF-Gliederungsnummer akzeptieren.
|
# Ueberschrift NUR mit fuehrender PDF-Gliederungsnummer akzeptieren und
|
||||||
|
# offensichtliche Fragmente/Bildlegenden ausschliessen (sonst werden
|
||||||
|
# Zellen wie "POS", "li.", "re.", "bar, max. 10 bar" zu Ueberschriften).
|
||||||
is_heading = False
|
is_heading = False
|
||||||
if hm and len(hm.group(2)) < 120:
|
if hm and len(hm.group(2)) < 120:
|
||||||
num, rest = hm.group(1), hm.group(2)
|
num, rest = hm.group(1), hm.group(2)
|
||||||
if not re.match(r"^(mm|cm|m|kg|x|l|h|min|s)\b", rest, re.I):
|
rest_stripped = rest.strip()
|
||||||
|
is_frag = (
|
||||||
|
len(rest_stripped) < 4 # zu kurz ("POS", "li.")
|
||||||
|
or rest_stripped.endswith((",",)) # abgeschnittene Zelle
|
||||||
|
or re.match(r"^(mm|cm|m|kg|x|l|h|min|s|bar|POS|li|re|Pos)\b",
|
||||||
|
rest_stripped, re.I)
|
||||||
|
or re.match(r"^(li|re|oben|unten|vorne|hinten|links|rechts)\.?$",
|
||||||
|
rest_stripped, re.I)
|
||||||
|
)
|
||||||
|
if not is_frag:
|
||||||
is_heading = True
|
is_heading = True
|
||||||
|
|
||||||
if is_heading:
|
if is_heading:
|
||||||
|
|||||||