fix(cv): protect revisions and measure rendering readiness

This commit is contained in:
cesnimda
2026-10-07 07:56:35 +02:00
parent ac6d78289d
commit 046dc304aa
175 changed files with 16252 additions and 41 deletions
+117
View File
@@ -0,0 +1,117 @@
"""Compare recorded real live-preview frames with native exported PDFs.
Requires pdfplumber, pypdfium2 and Pillow (available in Codex's bundled runtime).
Run the cv-readiness browser corpus first; this separate gate fails honestly on
material differences and never modifies the application or its PDF output.
"""
import argparse
import json
import re
from pathlib import Path
import pdfplumber
import pypdfium2
from PIL import Image, ImageDraw
MM_PER_POINT = 25.4 / 72
POINTS_PER_PIXEL = 72 / 96
POSITION_TOLERANCE_MM = 2
PAGE_TOLERANCE_MM = 0.5
FONT_TOLERANCE_PT = 0.5
COLOR_TOLERANCE = 1 / 255
def measure(root: Path, fixture: dict) -> dict:
directory = root / fixture["fixture"]
settings, preview = fixture["settings"], fixture["measured"]
width_mm, height_mm = (215.9, 279.4) if settings.get("pageSize") == "letter" else (210, 297)
failures, pdf_tokens, pages, fonts = [], [], [], set()
with pdfplumber.open(directory / "export.pdf") as pdf:
if len(pdf.pages) != fixture["previewPages"]:
failures.append(f"page count: preview {fixture['previewPages']}, PDF {len(pdf.pages)}")
for number, page in enumerate(pdf.pages):
dimensions = [page.width * MM_PER_POINT, page.height * MM_PER_POINT]
if any(abs(actual - expected) > PAGE_TOLERANCE_MM for actual, expected in zip(dimensions, [width_mm, height_mm])):
failures.append(f"page {number + 1}: unexpected physical dimensions {dimensions}")
text = page.extract_text() or ""
if not text.strip():
failures.append(f"page {number + 1}: blank page")
if "HIDDEN-MARKER" in text:
failures.append(f"page {number + 1}: hidden section leaked")
for word in page.extract_words(extra_attrs=["fontname", "size", "non_stroking_color"]):
fonts.add(word["fontname"])
token = re.fullmatch(r"[SEBPR]\d{2}", word["text"])
if token:
pdf_tokens.append({"token": token[0], "page": number + 1, "x": word["x0"], "y": word["top"], "fontSize": word["size"], "font": word["fontname"], "color": word["non_stroking_color"]})
margin_mm = float(preview["effectiveMargin"].strip().removesuffix("mm"))
outside = [c for c in page.chars if c["text"].strip() and (c["top"] * MM_PER_POINT < margin_mm - 1 or c["bottom"] * MM_PER_POINT > height_mm - margin_mm + 1)]
if outside:
failures.append(f"page {number + 1}: {len(outside)} glyphs outside vertical safe area")
pages.append({"page": number + 1, "dimensionsMm": dimensions, "text": text, "glyphBoundsPt": [min((c["x0"] for c in page.chars), default=None), min((c["top"] for c in page.chars), default=None), max((c["x1"] for c in page.chars), default=None), max((c["bottom"] for c in page.chars), default=None)]})
remaining = list(pdf_tokens)
comparisons = []
height_px = height_mm * 96 / 25.4
for token in preview["tokens"]:
match = next((candidate for candidate in remaining if candidate["token"] == token["token"]), None)
if match is None:
failures.append(f"missing PDF marker {token['token']}")
continue
remaining.remove(match)
preview_page = int(token["y"] // height_px) + 1
dx = abs(token["x"] * POINTS_PER_PIXEL - match["x"]) * MM_PER_POINT
dy = abs((token["y"] % height_px) * POINTS_PER_PIXEL - match["y"]) * MM_PER_POINT
font_delta = abs(float(token["fontSize"].removesuffix("px")) * POINTS_PER_PIXEL - match["fontSize"])
preview_color = [int(value) / 255 for value in re.findall(r"\d+", token["color"])[:3]]
pdf_color = match["color"]
color_delta = max(abs(a - b) for a, b in zip(preview_color, pdf_color)) if isinstance(pdf_color, (list, tuple)) and len(pdf_color) == 3 else None
comparisons.append({"token": token["token"], "previewPage": preview_page, "pdfPage": match["page"], "deltaXmm": dx, "deltaYmm": dy, "fontDeltaPt": font_delta, "colorDelta": color_delta})
if preview_page != match["page"]:
failures.append(f"{token['token']}: preview page {preview_page}, PDF page {match['page']}")
elif max(dx, dy) > POSITION_TOLERANCE_MM:
failures.append(f"{token['token']}: position delta x={dx:.2f}mm y={dy:.2f}mm")
if font_delta > FONT_TOLERANCE_PT:
failures.append(f"{token['token']}: font-size delta {font_delta:.2f}pt")
if color_delta is not None and color_delta > COLOR_TOLERANCE:
failures.append(f"{token['token']}: RGB component delta {color_delta:.4f}")
if remaining:
failures.append(f"extra PDF markers: {[word['token'] for word in remaining]}")
# Visual evidence uses actual exported bytes, rendered independently of Chromium.
document = pypdfium2.PdfDocument(str(directory / "export.pdf"))
for index in range(len(document)):
pdf_page = document[index]
bitmap = pdf_page.render(scale=96 / 72)
image = bitmap.to_pil()
image.save(directory / f"pdf-{index + 1}.png")
preview_path = directory / f"preview-{index + 1}.png"
if preview_path.exists():
preview_image = Image.open(preview_path).convert("RGB")
side_by_side = Image.new("RGB", (preview_image.width + image.width, max(preview_image.height, image.height) + 28), "#ffffff")
side_by_side.paste(preview_image, (0, 28))
side_by_side.paste(image, (preview_image.width, 28))
draw = ImageDraw.Draw(side_by_side)
draw.text((8, 6), "Live preview", fill="black")
draw.text((preview_image.width + 8, 6), "Native exported PDF", fill="black")
side_by_side.save(directory / f"comparison-{index + 1}.png")
bitmap.close()
pdf_page.close()
document.close()
return {"fixture": fixture["fixture"], "pass": not failures, "failures": failures, "previewPages": fixture["previewPages"], "pdfPages": len(pages), "fontNames": sorted(fonts), "comparisons": comparisons, "pages": pages}
def main():
parser = argparse.ArgumentParser()
parser.add_argument("directory", type=Path)
args = parser.parse_args()
inputs = json.loads((args.directory / "measurements.json").read_text(encoding="utf-8"))
results = [measure(args.directory, fixture) for fixture in inputs["fixtures"]]
report = {"tolerances": {"pageDimensionMm": PAGE_TOLERANCE_MM, "positionMm": POSITION_TOLERANCE_MM, "fontPt": FONT_TOLERANCE_PT, "rgbComponent": COLOR_TOLERANCE, "pageCountAndMembership": "exact"}, "browserVersion": inputs["browserVersion"], "passed": sum(result["pass"] for result in results), "total": len(results), "fixtures": results}
(args.directory / "fidelity-results.json").write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
for result in results:
print(f"{'PASS' if result['pass'] else 'FAIL'} {result['fixture']}: preview={result['previewPages']} PDF={result['pdfPages']}; {len(result['failures'])} differences")
raise SystemExit(0 if report["passed"] == report["total"] else 1)
if __name__ == "__main__":
main()