#!/usr/bin/env python3 """Read-only link annotation evidence through the production sandboxed worker. Poppler is an independent comparison, not an all-features correctness oracle. Run after building pdf-worker-evidence. Requires Pillow and Poppler utils. """ import argparse import hashlib import json from pathlib import Path import subprocess import time from PIL import Image, ImageChops, ImageStat ROOT = Path(__file__).resolve().parent.parent def sha(path): with path.open('rb') as source: return hashlib.file_digest(source, 'sha256').hexdigest() def stats(a, b): assert a.size == b.size diff = ImageChops.difference(a, b) pixels = list(diff.getdata()) return {'size': list(a.size), 'pixels': len(pixels), 'equal_pixels': sum(max(p) == 0 for p in pixels), 'pixels_max_delta_gt_16': sum(max(p) > 16 for p in pixels), 'mean_absolute_channel_error': sum(ImageStat.Stat(diff).mean) / 3} def region(rect, rotation, margin=6): points = [] for x in [rect[0], rect[2]]: for y in [rect[1], rect[3]]: x0, y0 = (x - 20) * 2, (450 - y) * 2 points.append([(x0, y0), (840 - y0, x0), (1020 - x0, 840 - y0), (y0, 1020 - x0)][rotation // 90]) return (min(p[0] for p in points) - margin, min(p[1] for p in points) - margin, max(p[0] for p in points) + margin, max(p[1] for p in points) + margin) def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--build-dir', type=Path, default=ROOT / 'build') parser.add_argument('--output', type=Path, default=ROOT / 'tests/results/link-borders') args = parser.parse_args() out = args.output.resolve(); out.mkdir(parents=True, exist_ok=True) build = args.build_dir.resolve(); helper = build / 'pdf-worker-evidence' fixture = ROOT / 'tests/fixtures/pdf/link-borders/styles.pdf' before = sha(fixture) start = time.monotonic() subprocess.run([str(helper), str(fixture), str(out / 'worker')], check=True, timeout=60) elapsed = time.monotonic() - start reference = subprocess.run(['/usr/bin/pdftoppm', '-cropbox', '-r', '144', '-png', str(fixture), str(out / 'poppler')], capture_output=True, check=True, timeout=60) (out / 'poppler-stderr.txt').write_bytes(reference.stderr) manifest = json.loads((fixture.parent / 'manifest.json').read_text())['fixtures'][0] common = {'default', 'solid-rgb', 'dashed-border', 'dashed-bs-odd', 'dashed-default', 'underline', 'bs-overrides-border', 'zero-width', 'transparent', 'hidden', 'no-view', 'print-flag-visible'} records = [] for index, rotation in enumerate([0, 90, 180, 270], 1): a = Image.open(out / f'worker/page-{index}.png').convert('RGB') b = Image.open(out / f'poppler-{index}.png').convert('RGB') diff = ImageChops.difference(a, b); diff.save(out / f'difference-{index}.png') comparisons = [] for case in manifest['annotations']: bounds = region(case['rect'], rotation) row = {'name': case['name'], 'reference_supported': case['name'] in common, **stats(a.crop(bounds), b.crop(bounds))} if case['name'] in common: # At 144dpi the grid-aligned borders should match except small # independent rasterizer antialias/color-rounding differences. assert row['mean_absolute_channel_error'] < 3, row comparisons.append(row) records.append({'page': index, 'rotation': rotation, 'whole_page': stats(a, b), 'annotations': comparisons}) navigation = ROOT / 'tests/fixtures/pdf/navigation.pdf' navigation_before = sha(navigation) subprocess.run([str(helper), str(navigation), str(out / 'navigation-worker')], check=True, timeout=60) subprocess.run(['/usr/bin/pdftoppm', '-cropbox', '-f', '1', '-singlefile', '-r', '144', '-png', str(navigation), str(out / 'navigation-poppler')], capture_output=True, check=True, timeout=60) a = Image.open(out / 'navigation-worker/page-1.png').convert('RGB') b = Image.open(out / 'navigation-poppler.png').convert('RGB') # The exact region where the original default link border was absent. navigation_comparison = stats(a.crop((18, 368, 282, 400)), b.crop((18, 368, 282, 400))) assert navigation_comparison['mean_absolute_channel_error'] < 3, navigation_comparison assert before == sha(fixture) and navigation_before == sha(navigation) result = {'transport': 'production WorkerProcess, sandboxed PDFWorker and FontBroker; no direct PDFium exporter', 'worker_sha256': sha(build / 'docview-pdf-worker'), 'exporter_sha256': sha(helper), 'qpdf_version': subprocess.check_output(['qpdf', '--version'], text=True).splitlines()[0], 'poppler_version': subprocess.run(['/usr/bin/pdftoppm', '-v'], capture_output=True, text=True).stderr.splitlines()[0], 'fixture_sha256': before, 'source_unchanged': True, 'four_page_export_seconds': elapsed, 'reference_status': 'independent Poppler comparison; manually inspect images; not a golden-image acceptance gate', 'reference_limitations': manifest['reference_limitations'], 'known_limitations': ['NoZoom/NoRotate geometry for generated links, saved normal APs, and native Text icons is covered separately by tests/results/pdf-annotation-flags.', 'Other annotation types without a supplied AP retain PDFium generation behavior; this corpus does not certify their NoZoom handling.', 'Text annotation icon appearance is viewer-specific and is not a link border defect.', 'This closes tested standard link border cases, not the entire G-RENDER gate.'], 'pages': records, 'navigation_fixture_sha256': navigation_before, 'navigation_default_link_region': navigation_comparison} (out / 'comparison.json').write_text(json.dumps(result, indent=2) + '\n') print(json.dumps({'source_unchanged': True, 'four_page_export_seconds': elapsed, 'standard_case_comparisons': len(common) * 4, 'navigation_default_link_region': navigation_comparison}, indent=2)) if __name__ == '__main__': main()