"""PDF 頁 → PNG 影像(pymupdf,deterministic、全程本地)。 供 vision fallback 使用:文字抽取失敗(needs_ocr)的掃描/圖片型 PDF 逐頁 render 成影像後,交給 from_vision.py 以本地 vision 模型判讀。render 本身不含任何 LLM, 維持「deterministic 工具做結構、LLM 只做語意」的分工(AGENTS.md §3 稽核原則)。 可獨立執行:python tools/convert/to_image.py --out DIR [--dpi 150] """ import argparse import pathlib import fitz # pymupdf # ponytail: 預設 150 DPI 是「可讀性 vs 影像大小/推論成本」的折衷;掃描件文字偏小時 # 可調高。升級路徑:依頁面尺寸/內容自適應 DPI。 DEFAULT_DPI = 150 def render_pdf_pages(src, out_dir, dpi=DEFAULT_DPI, prefix="page"): """將 src 每頁 render 成 PNG,回傳影像路徑列表(依頁序)。""" src = pathlib.Path(src) out_dir = pathlib.Path(out_dir) out_dir.mkdir(parents=True, exist_ok=True) paths = [] doc = fitz.open(str(src)) try: for i, page in enumerate(doc, 1): pix = page.get_pixmap(dpi=dpi) p = out_dir / f"{prefix}{i}.png" pix.save(str(p)) paths.append(p) finally: doc.close() return paths def main(argv=None): ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("src") ap.add_argument("--out", required=True, help="影像輸出目錄") ap.add_argument("--dpi", type=int, default=DEFAULT_DPI) a = ap.parse_args(argv) paths = render_pdf_pages(a.src, a.out, a.dpi) print(f"{len(paths)} 頁 → {a.out}") if __name__ == "__main__": main()