"""Headless renderer for visual answers (SVG / HTML / canvas / three.js) with objective checks. render(kind, source, png_path) opens the page in headless Chrome (the installed browser, via playwright) at 1024x768, waits for it to settle and returns a report: errors JavaScript exceptions and console errors (messages) failed network requests that failed (e.g. a wrong library URL) blank the screenshot is (almost) a single colour overflow the page is wider than the viewport (horizontal scroll) animated two screenshots 0.7 s apart differ (for animation tasks) coverage share of the viewport that is not background colour parse_error for SVG: the XML does not parse The screenshot is saved to png_path. Used by the visual benchmark and by the render-check-repair module. """ import io import os import re import tempfile import xml.dom.minidom import numpy as np from PIL import Image from playwright.sync_api import sync_playwright W, H = 1024, 768 TMP = os.path.join(tempfile.gettempdir(), "lfm3_render") os.makedirs(TMP, exist_ok=True) SVG_PAGE = """
__SVG__
""" def _stats(png): a = np.asarray(Image.open(io.BytesIO(png)).convert("RGB")).astype(np.int16) flat = a.reshape(-1, 3) vals, counts = np.unique(flat[::7], axis=0, return_counts=True) bg = vals[counts.argmax()] diff = np.abs(a - bg).sum(axis=2) > 24 return a, float(diff.mean()) def extract(text, kind): if text is None: return None blocks = re.findall(r"```(\w*)\s*\n(.*?)```", text, re.S) for lang, body in reversed(blocks): if kind == "svg" and ("", text, re.S) return m.group(0) if m else None m = re.search(r"|", text, re.S | re.I) return m.group(0) if m else None class Renderer: def __init__(self, channel="chrome"): self.pw = sync_playwright().start() self.browser = self.pw.chromium.launch(channel=channel, headless=True, args=["--use-angle=swiftshader", "--enable-unsafe-swiftshader"]) def close(self): self.browser.close() self.pw.stop() def render(self, kind, source, png_path, wait_ms=1500): rep = {"errors": [], "failed": [], "parse_error": None} if kind == "svg": try: xml.dom.minidom.parseString(source.encode("utf-8")) except Exception as e: rep["parse_error"] = str(e)[:200] html = SVG_PAGE.replace("__SVG__", source) else: html = source path = os.path.join(TMP, f"page_{os.getpid()}_{abs(hash(png_path)) % 10**8}.html") with open(path, "w", encoding="utf-8") as f: f.write(html) page = self.browser.new_page(viewport={"width": W, "height": H}) page.on("pageerror", lambda e: rep["errors"].append(f"Uncaught {e.name}: {e.message}"[:300])) page.on("console", lambda m: rep["errors"].append(("console.error: " + m.text)[:300]) if m.type == "error" else None) page.on("requestfailed", lambda r: rep["failed"].append(r.url[:200])) try: page.goto("file:///" + path.replace("\\", "/"), wait_until="load", timeout=20000) page.wait_for_timeout(wait_ms) png1 = page.screenshot() page.wait_for_timeout(700) png2 = page.screenshot() rep["overflow"] = bool(page.evaluate("document.documentElement.scrollWidth > window.innerWidth + 2")) rep["page_height"] = int(page.evaluate("document.documentElement.scrollHeight")) except Exception as e: rep["errors"].append(f"page failed to load: {str(e)[:200]}") png1 = png2 = page.screenshot() if not page.is_closed() else None rep["overflow"], rep["page_height"] = False, 0 finally: page.close() try: os.remove(path) except OSError: pass if png1 is None: rep.update(blank=True, coverage=0.0, animated=False) return rep with open(png_path, "wb") as f: f.write(png2) a1, cov = _stats(png1) a2, _ = _stats(png2) rep["coverage"] = round(cov, 4) rep["blank"] = cov < 0.003 rep["animated"] = bool((np.abs(a1 - a2).sum(axis=2) > 24).mean() > 0.0005) return rep if __name__ == "__main__": import json import sys r = Renderer() kind, src, out = sys.argv[1], open(sys.argv[2], encoding="utf-8").read(), sys.argv[3] print(json.dumps(r.render(kind, src, out), ensure_ascii=False)) r.close()