Row {i:03}
' for i in range(286) ) + "# /// script # requires-python = ">=3.11" # dependencies = ["pymupdf==1.28.2"] # /// """Exercise the real CLI and a PDF reader, including ActualText and Type3 glyphs. Run after building Moli: uv run scripts/check-native-pdf.py --moli target/debug/moli Unlike lopdf's basic extractor, MuPDF interprets ActualText, reversed text and colored Type3 glyphs. Keep these reader checks separate from the Rust structural and plain-text regressions. Artifacts and observed process costs are retained. """ import argparse import base64 import json from pathlib import Path import subprocess import time import pymupdf ROOT = Path(__file__).resolve().parents[1] def data_url(data, media_type): return f"data:{media_type};base64," + base64.b64encode(data).decode() def capture(binary, output, name, html): path = output / f"{name}.pdf" started = time.perf_counter() peak_kib = 0 with path.open("wb") as target: process = subprocess.Popen( [str(binary), "fetch", "--font", "--layout", "--wait-until", "load", "--log-level", "error", "--http-no-proxy", "*", "--dump", "pdf", data_url(html.encode(), "text/html")], stdout=target, stderr=subprocess.PIPE, ) while process.poll() is None: if time.perf_counter() - started > 120: process.kill() process.communicate() raise TimeoutError(f"{name} exceeded 120 seconds") try: for line in Path(f"/proc/{process.pid}/status").read_text().splitlines(): if line.startswith("VmHWM:"): peak_kib = max(peak_kib, int(line.split()[1])) except FileNotFoundError: pass time.sleep(0.005) stderr = process.communicate()[1].decode(errors="replace") if process.returncode: raise RuntimeError(f"{name} failed: {stderr}") seconds = time.perf_counter() - started document = pymupdf.open(path) text = " ".join(" ".join(page.get_text().split()) for page in document) (output / f"{name}.txt").write_text(text) document[0].get_pixmap(matrix=pymupdf.Matrix(1.5, 1.5)).save(output / f"{name}.png") info = {"pages": len(document), "pdf_bytes": path.stat().st_size, "seconds": round(seconds, 4), "peak_rss_kib_sampled": peak_kib, "fonts": sorted({font[3] for page in document for font in page.get_fonts()}), "image_count": sum(len(page.get_images()) for page in document)} return text, info def check_button_backgrounds(binary, output, html): screenshot = output / "backgrounds-screenshot.png" result = subprocess.run( [str(binary), "fetch", "--font", "--layout", "--wait-until", "load", "--log-level", "error", "--http-no-proxy", "*", "--dump", "screenshot", data_url(html.encode(), "text/html")], capture_output=True, check=True, timeout=120, ) screenshot.write_bytes(result.stdout) reference = pymupdf.Pixmap(screenshot) with pymupdf.open(output / "backgrounds.pdf") as document: page = document[0] # CLI PDF fits the viewport width inside the default 1 cm margins. margin = 72 / 2.54 scale = (page.rect.width - 2 * margin) / reference.width clip = pymupdf.Rect(margin, margin, page.rect.width - margin, margin + reference.height * scale) rendered = page.get_pixmap(matrix=pymupdf.Matrix(1 / scale, 1 / scale), clip=clip) rendered.save(output / "backgrounds-rendered.png") for x, y in [(110, 112), (320, 112), (460, 112)]: expected = reference.pixel(x, y)[:3] actual = rendered.pixel(round(margin / scale + x - rendered.x), round(margin / scale + y - rendered.y))[:3] assert min(expected) < 180, ("missing screenshot background", x, y, expected) assert max(abs(a - b) for a, b in zip(actual, expected)) <= 3, ( "PDF button background differs from screenshot", x, y, actual, expected ) def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--moli", type=Path, default=ROOT / "target/debug/moli") parser.add_argument("--output-dir", type=Path, default=ROOT / "target/native-pdf-check") args = parser.parse_args() args.output_dir.mkdir(parents=True, exist_ok=True) font = data_url((ROOT / "moli-paint/tests/fixtures/moli-pdf.ttf").read_bytes(), "font/ttf") cjk = data_url((ROOT / "moli-layout/tests/fixtures/moli-cjk.ttf").read_bytes(), "font/ttf") style = f"""""" results = {} text, results["minimal"] = capture(args.moli.resolve(), args.output_dir, "minimal", style + "
Font Text 123 中文
office fi ffi á
Second page a
abc אבג xyz
Color ★
Weight 650
Italic
""" text, results["shaping"] = capture(args.moli.resolve(), args.output_dir, "shaping", html) assert text == " ".join(expected), text html = style + """Opacity text
Shadow text once
Filtered text once
Hidden opacity
Hidden mask
""" text, results["effects"] = capture(args.moli.resolve(), args.output_dir, "effects", html) for phrase in ["Opacity text", "Shadow text once", "Filtered text once"]: assert text.count(phrase) == 1, text assert "Hidden" not in text, text # Exceeds the full-document screenshot CSS dimension limit. There is no # full-document pixel allocation in this path, even at the default viewport. html = style + 'Row {i:03}
' for i in range(286) ) + "