#!/usr/bin/env python3 """Compare the original extended corpus with independent Poppler rendering. No pixel metric is interpreted as a calibrated compatibility acceptance score. """ from pathlib import Path import argparse import hashlib import json import os import subprocess import sys from PIL import Image, ImageChops, ImageDraw, ImageStat ROOT = Path(__file__).resolve().parent.parent parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--build-dir", type=Path, default=ROOT / os.environ.get("DOCVIEW_BUILD_DIR", "build")) parser.add_argument("--output", type=Path, default=ROOT / "tests/results/pdf-extended") parser.add_argument("--pdfium-library", type=Path, default=ROOT / ".deps/pdfium/lib/libpdfium.so", help="Selected worker library to identify in the evidence record.") args = parser.parse_args() BUILD = args.build_dir.resolve() CORPUS = ROOT / "tests/fixtures/pdf/extended" OUT = args.output.resolve() OUT.mkdir(parents=True, exist_ok=True) # The runtime wrapper can carry a private Fontconfig setup. Prefer an explicit # system installation so Standard-14 fallback uses that installation's fonts. def poppler_binary(name): return os.environ.get("DOCVIEW_" + name.upper(), str(Path("/usr/bin") / name) if (Path("/usr/bin") / name).is_file() else name) PDFTOPPM, PDFTOTEXT, PDFFONTS = (poppler_binary(name) for name in ("pdftoppm", "pdftotext", "pdffonts")) manifest = json.loads((CORPUS / "manifest.json").read_text()) helper = BUILD / "pdf-worker-evidence" if not helper.is_file(): raise SystemExit("Build the pdf-worker-evidence target before comparison.") report = {"reference_status": "Independent Poppler comparison of synthetic source; not a human-approved golden image or full compatibility acceptance", "platform": sys.platform, "transport": "production sandboxed PDF worker and FontBroker", "worker_sha256": hashlib.sha256((BUILD / "docview-pdf-worker").read_bytes()).hexdigest(), "broker_sha256": hashlib.sha256((BUILD / "libdocview_font_broker.a").read_bytes()).hexdigest(), "resolution_dpi": 144, "pdfium_adapter_sha256": hashlib.sha256((BUILD / "libdocview_pdf.a").read_bytes()).hexdigest(), "pdfium_library_sha256": hashlib.sha256(args.pdfium_library.resolve().read_bytes()).hexdigest(), "poppler_version": subprocess.run([PDFTOPPM, "-v"], text=True, capture_output=True).stderr.splitlines()[0], "poppler_binary": PDFTOPPM, "documents": []} report["fontconfig_matches"] = {family: subprocess.check_output(["fc-match", family], text=True).strip() for family in ["Helvetica", "Times-Roman", "Courier"]} overview = [] for specification in manifest["documents"]: source = CORPUS / specification["file"] if hashlib.sha256(source.read_bytes()).hexdigest() != specification["sha256"]: raise SystemExit("Fixture hash differs from source manifest: " + source.name) target = OUT / source.stem (target / "pdfium").mkdir(parents=True, exist_ok=True) (target / "poppler").mkdir(exist_ok=True) subprocess.run([str(helper), str(source), str(target / "pdfium")], check=True) subprocess.run([PDFTOPPM, "-cropbox", "-r", "144", "-png", str(source), str(target / "poppler/page")], check=True) subprocess.run([PDFTOTEXT, "-layout", str(source), str(target / "poppler/text.txt")], check=True) fonts = subprocess.check_output([PDFFONTS, str(source)], text=True) (target / "fonts.txt").write_text(fonts) doc = {"file": specification["file"], "fixture_sha256": hashlib.sha256(source.read_bytes()).hexdigest(), "pages": []} for index in range(1, specification["page_count"] + 1): a = Image.open(target / "pdfium" / f"page-{index}.png").convert("RGB") b = Image.open(target / "poppler" / f"page-{index}.png").convert("RGB") same_size = a.size == b.size page = {"page": index, "pdfium_size": list(a.size), "poppler_size": list(b.size), "dimensions_match": same_size} expected_size = specification["expected"].get("page_sizes_pt") if expected_size: page["expected_size_px"] = [v * 2 for v in expected_size[index - 1]] elif specification["file"] == "page-geometry.pdf": geometry = specification["expected"]["pages"][index - 1] page["expected_size_px"] = [v * 2 for v in geometry["display_size_pt"]] left, bottom, right, top = geometry["crop_box_pt"] w, h = right - left, top - bottom checks = [] for marker in geometry["markers_pdf_coordinates"]: x, y = marker["x"] - left, marker["y"] - bottom coordinates = {0: (x, h - y), 90: (y, x), 180: (w - x, y), 270: (h - y, w - x)}[geometry["rotation"]] point = tuple(int(v * 2) for v in coordinates) actual_a, actual_b = a.getpixel(point), b.getpixel(point) expected = marker["rgb"] checks.append({"point_px": point, "expected_rgb": expected, "pdfium_rgb": actual_a, "poppler_rgb": actual_b, "both_match_source_color": max(abs(actual_a[k] - expected[k]) for k in range(3)) <= 2 and max(abs(actual_b[k] - expected[k]) for k in range(3)) <= 2}) page["source_geometry_marker_checks"] = checks page["dimensions_match_source"] = list(a.size) == page.get("expected_size_px") and list(b.size) == page.get("expected_size_px") if specification["file"] == "graphics-compositing.pdf": probes = [] for probe in specification["expected"]["alpha_probes"]: x, y = probe["point_pt"] point = (x * 2, (760 - y) * 2) actual_a, actual_b, expected = a.getpixel(point), b.getpixel(point), probe["expected_rgb"] probes.append({"point_px": point, "region": probe["region"], "expected_rgb": expected, "pdfium_rgb": actual_a, "poppler_rgb": actual_b, "both_match_source_color": max(abs(actual_a[k] - expected[k]) for k in range(3)) <= 2 and max(abs(actual_b[k] - expected[k]) for k in range(3)) <= 2}) page["source_alpha_checks"] = probes if same_size: difference = ImageChops.difference(a, b) difference.save(target / f"difference-{index}.png") histogram = ImageChops.lighter(ImageChops.lighter(difference.getchannel("R"), difference.getchannel("G")), difference.getchannel("B")).histogram() page["equal_pixels"] = histogram[0] page["pixels_max_delta_gt_16"] = sum(histogram[17:]) page["mean_absolute_channel_error"] = sum(ImageStat.Stat(difference).mean) / 3 else: difference = Image.new("RGB", a.size, "#ff8080") tile = Image.new("RGB", (1200, 490), "#eeeeee"); draw = ImageDraw.Draw(tile) for column, (title, picture) in enumerate([(source.stem + f" p{index}: PDFium", a), ("Poppler independent renderer", b), ("Absolute RGB difference", difference)]): draw.text((column * 400 + 10, 10), title, fill="black") preview = picture.copy(); preview.thumbnail((385, 450)); tile.paste(preview, (column * 400 + (400 - preview.width) // 2, 33)) tile.save(target / f"comparison-{index}.png") overview.append(tile) doc["pages"].append(page) if specification["file"] == "japanese-layout.pdf": doc["japanese_text_extracted_by_poppler"] = "日本語の文字配置と埋込みフォント" in (target / "poppler/text.txt").read_text() meta = json.loads((target / "pdfium/metadata.json").read_text()) doc["japanese_search_matches_pdfium"] = len(meta.get("japaneseSearch", {}).get("matches", [])) doc["font_listing"] = fonts report["documents"].append(doc) sheet = Image.new("RGB", (1200, len(overview) * 490), "white") for index, tile in enumerate(overview): sheet.paste(tile, (0, index * 490)) sheet.save(OUT / "overview.png") (OUT / "comparison.json").write_text(json.dumps(report, indent=2, ensure_ascii=False) + "\n") summary = {"documents": len(report["documents"]), "pages": len(overview), "all_dimensions_match_source": all(page["dimensions_match_source"] for doc in report["documents"] for page in doc["pages"]), "all_geometry_markers_match_source": all(check["both_match_source_color"] for doc in report["documents"] for page in doc["pages"] for check in page.get("source_geometry_marker_checks", [])), "all_alpha_probes_match_source": all(check["both_match_source_color"] for doc in report["documents"] for page in doc["pages"] for check in page.get("source_alpha_checks", [])), "japanese_text_and_search": all(doc.get("japanese_text_extracted_by_poppler", True) and doc.get("japanese_search_matches_pdfium", 1) > 0 for doc in report["documents"]), "report": str(OUT / "comparison.json")} print(json.dumps(summary, indent=2)) if not all(summary[key] for key in ["all_dimensions_match_source", "all_geometry_markers_match_source", "all_alpha_probes_match_source", "japanese_text_and_search"]): raise SystemExit("A deterministic source assertion failed; see comparison.json")