"""Compare what Loupe reads from a page's pixels with what the browser computed, block by block. Pages are collected in truth mode (collect_page.cjs ... truth). The audit never reads those truth fields; this script does, to report how far font size, line height and contrast are from the browser's values. Usage: validate_page_measures.py [more directories] """ import json import re import sys from pathlib import Path sys.stdout.reconfigure(encoding="utf-8", errors="replace") PACKAGE = Path(__file__).resolve().parent sys.path.insert(0, str(PACKAGE)) import numpy from PIL import Image import block_measure def colour(text, behind=None): """An rgb() or rgba() string as RGB; a translucent colour is composited over the colour behind it.""" numbers = [float(value) for value in re.findall(r"[\d.]+", text)] alpha = numbers[3] if len(numbers) > 3 and behind is not None else 1.0 return [alpha * numbers[channel] + (1 - alpha) * (behind[channel] if behind else 0.0) for channel in range(3)] def page_errors(directory, maps): layout = json.loads((directory / "layout.json").read_text(encoding="utf-8")) with Image.open(directory / "page.png") as image: pixels = numpy.asarray(image.convert("RGB")) errors = {"font_px": [], "line_height_px": [], "contrast": []} for text in layout["texts"]: truth = text["truth"] measured = block_measure.measure_block(pixels, text["box"], maps) errors["font_px"].append(abs(measured["font_px"] / truth["font_px"] - 1)) if measured["lines"] > 1 and truth["line_height_px"] is not None: errors["line_height_px"].append(abs(measured["line_height_px"] / truth["line_height_px"] - 1)) behind = colour(truth["background"]) declared = block_measure.contrast_ratio(behind, colour(truth["color"], behind)) errors["contrast"].append(abs(measured["contrast"] / declared - 1)) return errors def summary(values): values = numpy.asarray(values) return {"blocks": int(len(values)), "median_relative_error": float(numpy.median(values)), "within_10_percent": float((values <= 0.10).mean()), "within_20_percent": float((values <= 0.20).mean())} def main(): maps = block_measure.load_maps() pooled = {"font_px": [], "line_height_px": [], "contrast": []} report = {"pages": {}} for argument in sys.argv[1:]: directory = Path(argument) errors = page_errors(directory, maps) report["pages"][directory.name] = {kind: summary(values) for kind, values in errors.items() if values} for kind, values in errors.items(): pooled[kind].extend(values) report["pooled"] = {kind: summary(values) for kind, values in pooled.items() if values} (PACKAGE / "page_measure_validation.json").write_text(json.dumps(report, indent=1), encoding="utf-8") for name, entry in {**report["pages"], "POOLED": report["pooled"]}.items(): print(name, {kind: (value["blocks"], round(value["median_relative_error"], 3), round(value["within_10_percent"], 2)) for kind, value in entry.items()}) if __name__ == "__main__": main()