""" Trennt Doppelseiten-Scans in Einzelseiten, bevor OCR und Docling sie lesen. Bei den meisten Titeln im Kafka-Korpus liegen zwei Buchseiten auf einer PDF-Seite (Querformat). Tesseract liest dann stellenweise quer über den Falz, Doclings Layout-Modell trennt Absätze an der Seitenmitte. Hier wird jede Querformat-Seite in zwei Seiten zugeschnitten: Der Falz wird im Bild gesucht, als die senkrechte Spalte in der Seitenmitte mit den wenigsten Hell-Dunkel-Wechseln (Text erzeugt viele, Innenrand und Falzschatten kaum welche). Bildinhalt und Textschicht bleiben unverändert, nur der sichtbare Seitenbereich (MediaBox/CropBox) wird gesetzt — Docling und OCRmyPDF halten sich daran. Neben jedes getrennte PDF wird .pages.json geschrieben: zu jeder neuen Seite die Seite im Original-PDF (ab 0) und die Hälfte ("L", "R" oder "" für ungeteilt). docling_loader.py rechnet Seitenangaben damit auf das Original zurück. Benutzt von prepare_corpus.py; läuft in der Docling-Umgebung (pypdfium2, numpy). """ import json from pathlib import Path import numpy as np import pypdfium2 as pdfium from pypdf import PdfReader, PdfWriter from pypdf.generic import RectangleObject LANDSCAPE_RATIO = 1.15 # Breite/Höhe, ab der eine Seite als Doppelseite gilt SEARCH_BAND = (0.35, 0.65) # Falz wird in diesem Bereich der Seitenbreite gesucht RENDER_SCALE = 0.5 # grobes Vorschaubild genügt für die Falzsuche def find_gutter(page: pdfium.PdfPage) -> float: """Lage des Falzes als Anteil der Seitenbreite (0..1).""" gray = np.asarray(page.render(scale=RENDER_SCALE).to_pil().convert("L"), dtype=np.int16) h, w = gray.shape band = gray[int(h * 0.08):int(h * 0.92)] # Ober- und Unterkante meiden transitions = (np.abs(np.diff(band, axis=0)) > 40).sum(axis=0).astype(float) k = max(3, w // 100) smooth = np.convolve(transitions, np.ones(k) / k, mode="same") lo, hi = int(w * SEARCH_BAND[0]), int(w * SEARCH_BAND[1]) window = smooth[lo:hi] # Mitte des längsten Abschnitts nahe dem Minimum: der leere Streifen zwischen den Seiten quiet = window <= window.min() + 0.15 * (np.median(window) - window.min()) best, start, run = (0, 0), None, [] for i, q in enumerate(list(quiet) + [False]): if q and start is None: start = i elif not q and start is not None: best = max(best, (i - start, start)) start = None length, first = best return (lo + first + length / 2) / w def split_pdf(src: Path, dst: Path) -> list[dict]: """Schreibt das getrennte PDF nach dst und liefert die Seitenzuordnung.""" reader = PdfReader(src) preview = pdfium.PdfDocument(str(src)) writer = PdfWriter() mapping = [] for i, page in enumerate(reader.pages): box = page.mediabox left, bottom, right, top = (float(v) for v in (box.left, box.bottom, box.right, box.top)) rotated = int(page.get("/Rotate", 0) or 0) % 180 != 0 if rotated or (right - left) <= LANDSCAPE_RATIO * (top - bottom): writer.add_page(page) mapping.append({"original_page": i, "half": ""}) continue x = left + find_gutter(preview[i]) * (right - left) for half, (x0, x1) in (("L", (left, x)), ("R", (x, right))): new = writer.add_page(page) rect = RectangleObject([x0, bottom, x1, top]) new.mediabox = rect new.cropbox = rect mapping.append({"original_page": i, "half": half, "gutter_x": round(x, 1)}) dst.parent.mkdir(parents=True, exist_ok=True) with open(dst, "wb") as f: writer.write(f) pages_json(dst).write_text(json.dumps(mapping, indent=1), encoding="utf-8") return mapping def pages_json(pdf: Path) -> Path: return pdf.with_name(pdf.stem + ".pages.json") def is_spread_pdf(pdf: Path, min_share: float = 0.5) -> bool: """Liegt bei mindestens min_share der Seiten Querformat vor?""" pages = PdfReader(pdf).pages land = sum(float(p.mediabox.width) > LANDSCAPE_RATIO * float(p.mediabox.height) for p in pages) return land >= min_share * len(pages) def bounded_page_texts(pdf: Path) -> list[str]: """Text jeder Seite, auf den sichtbaren Bereich begrenzt (pypdf ignoriert den Zuschnitt).""" doc = pdfium.PdfDocument(str(pdf)) texts = [] for page in doc: left, bottom, right, top = page.get_cropbox() texts.append(page.get_textpage().get_text_bounded(left, bottom, right, top)) return texts