wave E stage 2: renderSvg goes native-first - resvg in-process
Visual-maker SVGs never execute script, so a browser added nothing but a process. resvg rasterizes in-process now (white background and fitTo width mirror the old Chrome-page semantics; svgViewport unchanged, quirks included for parity). The old Chrome path stays verbatim as the fallback for exotic-SVG parse failures - it was the only path before, so nothing regresses. The resvg binary (4MB per platform, one per build) rides the standalone via the same tracing pattern. Gates 1519/1519; native path pinned against real SVGs (intrinsic size, viewBox-only 900 default, opts override, system-font text).
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Wave E stage 2 (owner 2026-10-10): renderSvg is NATIVE FIRST — resvg
|
||||
* rasterizes in-process; the old Chrome path stays as fallback for
|
||||
* exotic-SVG parse failures. These tests pin the native path against real
|
||||
* SVGs: attribute-sized, viewBox-only (the 900px default), text-bearing
|
||||
* (system fonts), and the canonical single-IDAT PNG form.
|
||||
*/
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import { realHeadless } from "../src/agent/subagents/headless.ts";
|
||||
|
||||
function pngFacts(buf: Buffer): { w: number; h: number; idats: number } {
|
||||
assert.equal(buf.subarray(0, 8).toString("hex"), "89504e470d0a1a0a", "PNG signature");
|
||||
const w = buf.readUInt32BE(16);
|
||||
const h = buf.readUInt32BE(20);
|
||||
let off = 8;
|
||||
let idats = 0;
|
||||
while (off < buf.length) {
|
||||
const len = buf.readUInt32BE(off);
|
||||
const type = buf.subarray(off + 4, off + 8).toString("ascii");
|
||||
if (type === "IDAT") idats++;
|
||||
off += 12 + len;
|
||||
}
|
||||
return { w, h, idats };
|
||||
}
|
||||
|
||||
describe("native renderSvg (resvg in-process, browser fallback)", () => {
|
||||
it("renders an attribute-sized SVG at its intrinsic size", async () => {
|
||||
const svg =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="240" height="160"><rect width="240" height="160" fill="#3355aa"/></svg>';
|
||||
const png = await realHeadless.renderSvg(svg);
|
||||
const f = pngFacts(png);
|
||||
assert.equal(f.w, 240);
|
||||
assert.equal(f.h, 160);
|
||||
assert.equal(f.idats, 1, "single-IDAT canonical PNG");
|
||||
});
|
||||
|
||||
it("viewBox-only SVG gets the 900px-wide default", async () => {
|
||||
// No width attr anywhere (svgViewport grabs the FIRST width= in the
|
||||
// document — children included — a pre-existing quirk kept for
|
||||
// parity; use a circle so the default branch is exercised).
|
||||
const svg =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 200"><circle cx="150" cy="100" r="90" fill="#aa5533"/></svg>';
|
||||
const png = await realHeadless.renderSvg(svg);
|
||||
const f = pngFacts(png);
|
||||
assert.equal(f.w, 900, "svgViewport default width");
|
||||
assert.equal(f.h, 600, "aspect preserved from viewBox");
|
||||
});
|
||||
|
||||
it("width/height opts override for viewBox-only SVGs", async () => {
|
||||
const svg =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 100"><rect width="400" height="100" fill="#33aa55"/></svg>';
|
||||
const png = await realHeadless.renderSvg(svg, { width: 500, height: 125 });
|
||||
const f = pngFacts(png);
|
||||
assert.equal(f.w, 500);
|
||||
});
|
||||
|
||||
it("text-bearing SVG renders with system fonts (non-trivial output)", async () => {
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="300" height="100">
|
||||
<rect width="300" height="100" fill="#ffffff"/>
|
||||
<text x="150" y="60" text-anchor="middle" font-family="Helvetica, Arial, sans-serif" font-size="32" fill="#111111">Harness</text>
|
||||
</svg>`;
|
||||
const png = await realHeadless.renderSvg(svg);
|
||||
const f = pngFacts(png);
|
||||
assert.equal(f.w, 300);
|
||||
// A blank 300x100 white PNG is ~300 bytes; glyphs push it well past.
|
||||
assert.ok(png.length > 1000, `text rendered (png ${png.length} bytes > 1000)`);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user