File size: 5,336 Bytes
cd61333
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
"""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 = """<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;background:#fff}

#c{width:1024px;height:768px;display:flex;align-items:center;justify-content:center}

#c>svg{max-width:1024px;max-height:768px;width:100%;height:100%}</style></head><body><div id="c">__SVG__</div></body></html>"""


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 ("<svg" in body):
            return body[body.index("<svg"):].strip()
        if kind == "html" and ("<html" in body.lower() or "<body" in body.lower() or "<canvas" in body or "<script" in body):
            return body.strip()
    if kind == "svg":
        m = re.search(r"<svg.*</svg>", text, re.S)
        return m.group(0) if m else None
    m = re.search(r"<!doctype html.*</html>|<html.*</html>", 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()