"""PDF → images PNG 300 dpi avec cache par hash SHA256. Applique optionnellement un deskew automatique (redressement) sur chaque page pour corriger le biais d'inclinaison des scans. Voir pipeline/deskew.py. """ import hashlib import os from pathlib import Path from pdf2image import convert_from_path from PIL import Image from .deskew import deskew_image, MIN_ANGLE_DEG DEFAULT_DPI = 300 CACHE_ROOT = Path(".cache/images") def pdf_hash(pdf_path: str) -> str: """Hash SHA256 court du contenu PDF.""" h = hashlib.sha256() with open(pdf_path, "rb") as f: for chunk in iter(lambda: f.read(65536), b""): h.update(chunk) return h.hexdigest()[:16] def pdf_to_images(pdf_path: str, dpi: int = DEFAULT_DPI, cache_root: Path = CACHE_ROOT, deskew: bool = True) -> list[Path]: """Convertit un PDF en PNG 300 dpi. Retourne la liste des chemins (1 par page). Le cache est indexé par hash du PDF : un PDF inchangé n'est jamais reconverti. Avec `deskew=True` (défaut), chaque page est redressée si son angle de skew dépasse le seuil défini dans `pipeline.deskew.MIN_ANGLE_DEG` (0.3°). L'angle appliqué est persisté dans un fichier `.skew` à côté (pour audit). """ cache_root = Path(cache_root) h = pdf_hash(pdf_path) out_dir = cache_root / h out_dir.mkdir(parents=True, exist_ok=True) # Le glob est strict pour ne pas attraper les crops intermédiaires # (page_XX_recodage.png, etc.) existing = sorted(p for p in out_dir.glob("page_*.png") if p.stem.replace("page_", "").isdigit()) if existing: return existing pages = convert_from_path(pdf_path, dpi) paths = [] for i, img in enumerate(pages, 1): if deskew: img, applied = deskew_image(img) if applied != 0.0: # Trace d'audit : on note l'angle corrigé (out_dir / f"page_{i:02d}.skew").write_text(f"{applied:.3f}\n") p = out_dir / f"page_{i:02d}.png" img.save(p, "PNG", optimize=True) paths.append(p) return paths def load_image(path: Path) -> Image.Image: return Image.open(path)