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:
@@ -61,6 +61,9 @@ const nextConfig: NextConfig = {
|
|||||||
// npm installs only the current platform's binary, so this glob
|
// npm installs only the current platform's binary, so this glob
|
||||||
// resolves to exactly one Skia binary per build.
|
// resolves to exactly one Skia binary per build.
|
||||||
"node_modules/@napi-rs/canvas-*/**/*",
|
"node_modules/@napi-rs/canvas-*/**/*",
|
||||||
|
// Wave E stage 2: resvg rasterizes visual-maker SVGs natively; same
|
||||||
|
// per-platform-binary story as the canvas above (one per build).
|
||||||
|
"node_modules/@resvg/resvg-js-*/**/*",
|
||||||
"node_modules/@earendil-works/pi-ai/dist/providers/data/**/*",
|
"node_modules/@earendil-works/pi-ai/dist/providers/data/**/*",
|
||||||
"node_modules/@earendil-works/pi-agent-core/dist/**/*.json",
|
"node_modules/@earendil-works/pi-agent-core/dist/**/*.json",
|
||||||
],
|
],
|
||||||
@@ -73,6 +76,7 @@ const nextConfig: NextConfig = {
|
|||||||
"@earendil-works/pi-agent-core",
|
"@earendil-works/pi-agent-core",
|
||||||
"pdfjs-dist", // sources v0: bundling breaks its node fake-worker path
|
"pdfjs-dist", // sources v0: bundling breaks its node fake-worker path
|
||||||
"@napi-rs/canvas", // native binding — bundling breaks the .node require
|
"@napi-rs/canvas", // native binding — bundling breaks the .node require
|
||||||
|
"@resvg/resvg-js", // native binding — same
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Generated
+228
@@ -13,6 +13,7 @@
|
|||||||
"@earendil-works/pi-ai": "1.0.2",
|
"@earendil-works/pi-ai": "1.0.2",
|
||||||
"@mozilla/readability": "^0.6.0",
|
"@mozilla/readability": "^0.6.0",
|
||||||
"@radix-ui/react-use-controllable-state": "^1.2.6",
|
"@radix-ui/react-use-controllable-state": "^1.2.6",
|
||||||
|
"@resvg/resvg-js": "^2.6.2",
|
||||||
"@rive-app/react-webgl2": "^4.32.1",
|
"@rive-app/react-webgl2": "^4.32.1",
|
||||||
"@streamdown/cjk": "^1.0.3",
|
"@streamdown/cjk": "^1.0.3",
|
||||||
"@streamdown/code": "^1.1.1",
|
"@streamdown/code": "^1.1.1",
|
||||||
@@ -4454,6 +4455,233 @@
|
|||||||
"integrity": "sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==",
|
"integrity": "sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@resvg/resvg-js": {
|
||||||
|
"version": "2.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@resvg/resvg-js/-/resvg-js-2.6.2.tgz",
|
||||||
|
"integrity": "sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==",
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@resvg/resvg-js-android-arm-eabi": "2.6.2",
|
||||||
|
"@resvg/resvg-js-android-arm64": "2.6.2",
|
||||||
|
"@resvg/resvg-js-darwin-arm64": "2.6.2",
|
||||||
|
"@resvg/resvg-js-darwin-x64": "2.6.2",
|
||||||
|
"@resvg/resvg-js-linux-arm-gnueabihf": "2.6.2",
|
||||||
|
"@resvg/resvg-js-linux-arm64-gnu": "2.6.2",
|
||||||
|
"@resvg/resvg-js-linux-arm64-musl": "2.6.2",
|
||||||
|
"@resvg/resvg-js-linux-x64-gnu": "2.6.2",
|
||||||
|
"@resvg/resvg-js-linux-x64-musl": "2.6.2",
|
||||||
|
"@resvg/resvg-js-win32-arm64-msvc": "2.6.2",
|
||||||
|
"@resvg/resvg-js-win32-ia32-msvc": "2.6.2",
|
||||||
|
"@resvg/resvg-js-win32-x64-msvc": "2.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@resvg/resvg-js-android-arm-eabi": {
|
||||||
|
"version": "2.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm-eabi/-/resvg-js-android-arm-eabi-2.6.2.tgz",
|
||||||
|
"integrity": "sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@resvg/resvg-js-android-arm64": {
|
||||||
|
"version": "2.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm64/-/resvg-js-android-arm64-2.6.2.tgz",
|
||||||
|
"integrity": "sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@resvg/resvg-js-darwin-arm64": {
|
||||||
|
"version": "2.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-arm64/-/resvg-js-darwin-arm64-2.6.2.tgz",
|
||||||
|
"integrity": "sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@resvg/resvg-js-darwin-x64": {
|
||||||
|
"version": "2.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-x64/-/resvg-js-darwin-x64-2.6.2.tgz",
|
||||||
|
"integrity": "sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@resvg/resvg-js-linux-arm-gnueabihf": {
|
||||||
|
"version": "2.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm-gnueabihf/-/resvg-js-linux-arm-gnueabihf-2.6.2.tgz",
|
||||||
|
"integrity": "sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@resvg/resvg-js-linux-arm64-gnu": {
|
||||||
|
"version": "2.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-gnu/-/resvg-js-linux-arm64-gnu-2.6.2.tgz",
|
||||||
|
"integrity": "sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@resvg/resvg-js-linux-arm64-musl": {
|
||||||
|
"version": "2.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-musl/-/resvg-js-linux-arm64-musl-2.6.2.tgz",
|
||||||
|
"integrity": "sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@resvg/resvg-js-linux-x64-gnu": {
|
||||||
|
"version": "2.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-gnu/-/resvg-js-linux-x64-gnu-2.6.2.tgz",
|
||||||
|
"integrity": "sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"libc": [
|
||||||
|
"glibc"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@resvg/resvg-js-linux-x64-musl": {
|
||||||
|
"version": "2.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-musl/-/resvg-js-linux-x64-musl-2.6.2.tgz",
|
||||||
|
"integrity": "sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"libc": [
|
||||||
|
"musl"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@resvg/resvg-js-win32-arm64-msvc": {
|
||||||
|
"version": "2.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-arm64-msvc/-/resvg-js-win32-arm64-msvc-2.6.2.tgz",
|
||||||
|
"integrity": "sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@resvg/resvg-js-win32-ia32-msvc": {
|
||||||
|
"version": "2.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-ia32-msvc/-/resvg-js-win32-ia32-msvc-2.6.2.tgz",
|
||||||
|
"integrity": "sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@resvg/resvg-js-win32-x64-msvc": {
|
||||||
|
"version": "2.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-x64-msvc/-/resvg-js-win32-x64-msvc-2.6.2.tgz",
|
||||||
|
"integrity": "sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rive-app/react-webgl2": {
|
"node_modules/@rive-app/react-webgl2": {
|
||||||
"version": "4.32.1",
|
"version": "4.32.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rive-app/react-webgl2/-/react-webgl2-4.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rive-app/react-webgl2/-/react-webgl2-4.32.1.tgz",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
"@earendil-works/pi-ai": "1.0.2",
|
"@earendil-works/pi-ai": "1.0.2",
|
||||||
"@mozilla/readability": "^0.6.0",
|
"@mozilla/readability": "^0.6.0",
|
||||||
"@radix-ui/react-use-controllable-state": "^1.2.6",
|
"@radix-ui/react-use-controllable-state": "^1.2.6",
|
||||||
|
"@resvg/resvg-js": "^2.6.2",
|
||||||
"@rive-app/react-webgl2": "^4.32.1",
|
"@rive-app/react-webgl2": "^4.32.1",
|
||||||
"@streamdown/cjk": "^1.0.3",
|
"@streamdown/cjk": "^1.0.3",
|
||||||
"@streamdown/code": "^1.1.1",
|
"@streamdown/code": "^1.1.1",
|
||||||
|
|||||||
@@ -261,6 +261,26 @@ function crc32(buf: Buffer): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function renderSvg(svg: string, opts: RenderSvgOptions = {}): Promise<Buffer> {
|
async function renderSvg(svg: string, opts: RenderSvgOptions = {}): Promise<Buffer> {
|
||||||
|
// NATIVE FIRST (Wave E stage 2, owner 2026-10-10): resvg rasterizes
|
||||||
|
// static SVG in-process — visual-maker SVGs never execute script, so a
|
||||||
|
// browser adds nothing but a process. White background mirrors the old
|
||||||
|
// Chrome page (html,body{background:#fff}); fitTo width mirrors
|
||||||
|
// svgViewport semantics (attrs win, viewBox-only gets the 900 default,
|
||||||
|
// opts override). The browser path below stays as the fallback for
|
||||||
|
// exotic-SVG features resvg cannot parse — failures there used to be
|
||||||
|
// the ONLY path, so nothing regresses.
|
||||||
|
const { width } = svgViewport(svg, opts);
|
||||||
|
try {
|
||||||
|
const { Resvg } = await import("@resvg/resvg-js");
|
||||||
|
const resvg = new Resvg(svg, {
|
||||||
|
fitTo: { mode: "width", value: width },
|
||||||
|
font: { loadSystemFonts: true },
|
||||||
|
background: "#ffffff",
|
||||||
|
});
|
||||||
|
return providerSafePng(resvg.render().asPng());
|
||||||
|
} catch {
|
||||||
|
/* fall through to the browser path */
|
||||||
|
}
|
||||||
const dir = tempDir("lh-visual-");
|
const dir = tempDir("lh-visual-");
|
||||||
const file = path.join(dir, "figure.html");
|
const file = path.join(dir, "figure.html");
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
|
|||||||
@@ -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