Files
docview/tests/compare_link_borders.py
2026-09-21 13:41:40 +09:00

97 lines
6.1 KiB
Python

#!/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()