feat: implemented hit testing, form field, content stream and base security hardening
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rendering-regression harness for the PDFium engine.
|
||||
|
||||
Renders every page (capped) of every PDF in the committed corpus and compares it
|
||||
to a frozen baseline using SSIM. The baseline is the engine's own output at the
|
||||
moment it was approved — so a regression here means *this* build renders
|
||||
differently from the blessed build, not that it disagrees with Acrobat.
|
||||
|
||||
python tests/regression/run.py --update # (re)generate the frozen baseline
|
||||
python tests/regression/run.py # check current renders vs baseline
|
||||
|
||||
Exit code is non-zero if any page regresses below the SSIM threshold, so it
|
||||
drops straight into CI.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import io
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from ssim import ssim # noqa: E402
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
DEFAULT_CORPUS = ROOT / "corpus"
|
||||
DEFAULT_BASELINE = Path(__file__).resolve().parent / "baseline"
|
||||
# corpus subdirs that are *not* part of the frozen baseline (large/downloaded).
|
||||
EXCLUDE_DIRS = {"fuzz"}
|
||||
|
||||
|
||||
def _import_engine():
|
||||
# The compiled extension lives in gateway/ after scripts/build_cpp.ps1.
|
||||
sys.path.insert(0, str(ROOT / "gateway"))
|
||||
import pdfengine # noqa: PLC0415
|
||||
|
||||
return pdfengine
|
||||
|
||||
|
||||
def _render_gray(page, dpi: int) -> np.ndarray:
|
||||
png = page.render(dpi).data
|
||||
img = Image.open(io.BytesIO(png)).convert("L")
|
||||
return np.asarray(img)
|
||||
|
||||
|
||||
def _corpus_pdfs(corpus: Path):
|
||||
for p in sorted(corpus.rglob("*.pdf")):
|
||||
if any(part in EXCLUDE_DIRS for part in p.relative_to(corpus).parts):
|
||||
continue
|
||||
yield p
|
||||
|
||||
|
||||
def _key(rel: Path, page_index: int) -> str:
|
||||
return f"{rel.as_posix().replace('/', '__')}__p{page_index}"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("--update", action="store_true", help="write the frozen baseline instead of checking")
|
||||
ap.add_argument("--sweep", action="store_true",
|
||||
help="render-stability sweep over a large corpus (no baseline); "
|
||||
"passes as long as the engine never crashes")
|
||||
ap.add_argument("--corpus", type=Path, default=DEFAULT_CORPUS)
|
||||
ap.add_argument("--baseline", type=Path, default=DEFAULT_BASELINE)
|
||||
ap.add_argument("--dpi", type=int, default=72)
|
||||
ap.add_argument("--max-pages", type=int, default=2, help="pages rendered per document")
|
||||
ap.add_argument("--threshold", type=float, default=0.990, help="min SSIM to pass")
|
||||
args = ap.parse_args()
|
||||
|
||||
pdfengine = _import_engine()
|
||||
args.baseline.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
failures: list[tuple[str, float]] = []
|
||||
checked = skipped = updated = rendered = 0
|
||||
|
||||
for pdf in _corpus_pdfs(args.corpus):
|
||||
rel = pdf.relative_to(args.corpus)
|
||||
try:
|
||||
doc = pdfengine.PdfDocument.load_from_memory(pdf.read_bytes(), "")
|
||||
n = min(doc.page_count, args.max_pages)
|
||||
except Exception as exc: # encrypted / intentionally-malformed fixtures
|
||||
print(f" skip {rel} ({exc})")
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
for i in range(n):
|
||||
try:
|
||||
cur = _render_gray(doc.get_page(i), args.dpi)
|
||||
except Exception as exc:
|
||||
print(f" skip {rel} p{i} (render: {exc})")
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
if args.sweep:
|
||||
# Surviving the render is the whole test; a crash kills the process.
|
||||
rendered += 1
|
||||
continue
|
||||
|
||||
ref_path = args.baseline / f"{_key(rel, i)}.png"
|
||||
if args.update:
|
||||
Image.fromarray(cur).save(ref_path)
|
||||
updated += 1
|
||||
continue
|
||||
|
||||
if not ref_path.exists():
|
||||
print(f" NEW {rel} p{i} (no baseline — run --update)")
|
||||
failures.append((f"{rel} p{i}", -1.0))
|
||||
continue
|
||||
|
||||
ref = np.asarray(Image.open(ref_path).convert("L"))
|
||||
score = ssim(cur, ref)
|
||||
checked += 1
|
||||
mark = "ok " if score >= args.threshold else "FAIL "
|
||||
if score < args.threshold:
|
||||
failures.append((f"{rel} p{i}", score))
|
||||
print(f" {mark} {rel} p{i} SSIM={score:.4f}")
|
||||
|
||||
print()
|
||||
if args.sweep:
|
||||
print(f"Sweep complete: {rendered} pages rendered, {skipped} skipped (graceful). "
|
||||
f"No crash — engine is render-stable over this corpus.")
|
||||
return 0
|
||||
if args.update:
|
||||
print(f"Baseline updated: {updated} images written to {args.baseline}")
|
||||
return 0
|
||||
|
||||
print(f"Checked {checked} pages, skipped {skipped}, {len(failures)} regression(s).")
|
||||
for name, score in failures:
|
||||
tag = "missing baseline" if score < 0 else f"SSIM={score:.4f}"
|
||||
print(f" REGRESSION: {name} ({tag})")
|
||||
return 1 if failures else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user