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).
70 lines
3.0 KiB
TypeScript
70 lines
3.0 KiB
TypeScript
/**
|
|
* 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)`);
|
|
});
|
|
});
|