|
def html_to_png( |
|
html: str, |
|
width: int, |
|
height: int, |
|
*, |
|
scale: float = 2.0, |
|
time_budget_ms: int = 4000, |
|
timeout_s: float = 120.0, |
|
sandbox: bool = True, |
|
gl: str = "software", |
|
) -> bytes: |
|
"""Rasterize standalone chart HTML to PNG with an installed headless browser. |
|
|
|
The current adapter supports the Chromium family |
|
(Chrome, Chromium, Edge, and chrome-headless-shell). Pure mechanism (no |
|
Figure), so it is testable without numpy. `scale` is the device-pixel |
|
ratio (2 = retina-crisp). |
|
|
|
`gl` picks the WebGL backend: "software" (default) pins SwiftShader for |
|
deterministic pixels on any machine (including GPU-less CI); "hardware" |
|
lets Chromium use the real GPU — much faster on large direct-mode payloads, |
|
at the cost of driver-dependent rasterization.""" |
|
width = _positive_pixel_count(width, "PNG width") |
|
height = _positive_pixel_count(height, "PNG height") |
|
scale = _positive_finite_float(scale, "PNG scale") |
|
time_budget_ms = _positive_pixel_count(time_budget_ms, "PNG time_budget_ms") |
|
timeout_s = _positive_finite_float(timeout_s, "PNG timeout_s") |
|
sandbox = _bool_option(sandbox, "PNG sandbox") |
|
gl = _gl_option(gl) |
|
exe = find_browser() |
|
if exe is None: |
|
raise RuntimeError( |
|
"browser PNG export needs a supported Chrome/Chromium/Edge executable " |
|
f"and none was found. Set ${_BROWSER_ENV} to its executable path " |
|
"or install a supported browser. HTML export (to_html) needs nothing extra." |
|
) |
|
with tempfile.TemporaryDirectory() as td: |
|
page = Path(td) / "chart.html" |
|
page.write_text(html, encoding="utf-8") |
|
shot = Path(td) / "out.png" |
|
gl_flags = ( |
|
["--use-angle=swiftshader", "--enable-unsafe-swiftshader"] if gl == "software" else [] |
|
) |
|
args = [ |
|
exe, |
|
"--headless=new", |
|
"--disable-dev-shm-usage", |
|
"--hide-scrollbars", |
|
*gl_flags, |
|
f"--force-device-scale-factor={scale}", |
|
f"--window-size={int(width)},{int(height)}", |
|
f"--virtual-time-budget={int(time_budget_ms)}", |
|
f"--screenshot={shot}", |
|
page.as_uri(), |
|
] |
|
if not sandbox: |
|
args.insert(2, "--no-sandbox") |
|
proc = subprocess.run( |
|
args, |
|
capture_output=True, |
|
text=True, |
|
timeout=timeout_s, |
|
) |
|
if not shot.exists(): |
|
first_tail = (proc.stderr or "")[-500:] |
|
if sandbox: |
|
retry_args = list(args) |
|
retry_args.insert(2, "--no-sandbox") |
|
proc = subprocess.run( |
|
retry_args, |
|
capture_output=True, |
|
text=True, |
|
timeout=timeout_s, |
|
) |
|
if not shot.exists(): |
|
tail = (proc.stderr or "")[-500:] |
|
if sandbox: |
|
tail = f"sandboxed launch failed: {first_tail}\nno-sandbox retry failed: {tail}" |
Summary
The public security policy says browser export is sandboxed by default and disabling it is an explicit caller opt-out. Both Chromium paths actually retry unsandboxed automatically and silently when a sandboxed launch fails. The repository's own audit records that mismatch and separately records missing Cargo/Bun advisory scanning, while
make check-securityonly runs source-level export tests.Evidence
SECURITY.mddescribessandbox=Falseas the explicit opt-out:xy/SECURITY.md
Lines 23 to 35 in 99eda6d
html_to_png(..., sandbox=True)automatically inserts--no-sandboxon retry:xy/python/xy/export.py
Lines 548 to 625 in 99eda6d
sandbox=False:xy/python/xy/export.py
Lines 1034 to 1051 in 99eda6d
xy/spec/process/security-audit-2026-07-06.md
Lines 255 to 275 in 99eda6d
cargo audit/cargo denyis pending:xy/spec/process/security-audit-2026-07-06.md
Lines 195 to 208 in 99eda6d
pip-auditevidence and an unrun Bun audit, not continuous gates:xy/spec/process/security-audit-2026-07-06.md
Lines 210 to 220 in 99eda6d
xy/spec/process/security-audit-2026-07-06.md
Lines 248 to 251 in 99eda6d
make check-securityinvokes only the HTML/client test group:xy/Makefile
Lines 85 to 86 in 99eda6d
Acceptance criteria
sandbox=Truefails closed; any no-sandbox fallback requires an explicit caller option and is observable in logs/warnings.SECURITY.mdstate the exact enforced behavior and isolation requirements.make check-security(or a clearly named companion) exposes the repeatable local checks; point-in-time audit results are labeled as historical evidence.