wave E stage 1: pdf_render goes native - pdfjs paints, Skia hosts

The browser was never the PDF engine - pdfjs always painted the pages;
Chrome only hosted the canvas tab. Now pdfjs-dist v6's own Node
integration (@napi-rs/canvas, its optionalDependency) hosts in-process:
same paint code, Skia canvas, zero browsers, zero temp files, no base64
hop. The whole pdf-reader runs browser-free - the Chrome gate on
pdf_render is gone.

The Skia binary (27MB per platform, exactly one per build - npm installs
only the building machine's) rides the standalone via the same
outputFileTracingIncludes pattern as pdfjs-dist. Verified: builds green
(1515), native render pinned against the real fixture (canonical
single-IDAT PNGs, dpi/72 arithmetic, page-range skipping), and the
canvas loads + paints from inside .next/standalone.

Stages 2-3 (resvg for SVG, the --browser-service demo helper) follow.
This commit is contained in:
amosblomqvist
2026-10-11 06:49:13 +03:00
parent 0af005d343
commit 951c7704b9
4 changed files with 150 additions and 101 deletions
+20 -26
View File
@@ -173,9 +173,9 @@ export async function runPdfReaderAgent(
if (openError) {
return { path: pdfPath, question: input.question, brief: `PDF failed: ${openError}`, pagesRead: [], pagesConsulted: [] };
}
const chromeReason = headless.unavailableReason();
// Chrome missing does NOT fail the run: text-side tools still work; the
// doctrine degrades to extract+search (render calls return an error note).
// No Chrome gate (Wave E, 2026-10-10): pdf_render is native now
// (pdfjs paints into @napi-rs/canvas in-process) — the whole pdf-reader
// runs browser-free; text tools always worked without one.
const tools = createPdfTools(pdfPath);
const toolDeclarations = Object.values(tools);
@@ -280,30 +280,24 @@ export async function runPdfReaderAgent(
} else if (call.name === "pdf_render") {
const spec = String(args.pages ?? "1");
const dpi = numOr(args.dpi, 150);
if (chromeReason) {
toolResults.push(
toolText(call.id, call.name, `ERROR: rendering unavailable — ${chromeReason} Read via pdf_extract instead.`),
);
const wanted = specNumbers(spec, RENDER_BATCH_CAP);
opts.onActivity?.(`pdf_render: ${spec} @${dpi}dpi`);
const pages = await headless.renderPdfPages(pdfPath, wanted, { dpi });
for (const p of pages) {
consulted.add(p.page);
read.add(p.page);
}
if (pages.length === 0) {
toolResults.push(toolText(call.id, call.name, "ERROR: no pages rendered (spec matched nothing?)"));
} else {
const wanted = specNumbers(spec, RENDER_BATCH_CAP);
opts.onActivity?.(`pdf_render: ${spec} @${dpi}dpi`);
const pages = await headless.renderPdfPages(pdfPath, wanted, { dpi });
for (const p of pages) {
consulted.add(p.page);
read.add(p.page);
}
if (pages.length === 0) {
toolResults.push(toolText(call.id, call.name, "ERROR: no pages rendered (spec matched nothing?)"));
} else {
renderedImages = pages;
toolResults.push(
toolText(
call.id,
call.name,
`Rendered ${pages.length} page(s) at ${dpi} DPI — the images follow below. Read them carefully (equations, diagrams, layout).`,
),
);
}
renderedImages = pages;
toolResults.push(
toolText(
call.id,
call.name,
`Rendered ${pages.length} page(s) at ${dpi} DPI — the images follow below. Read them carefully (equations, diagrams, layout).`,
),
);
}
} else {
toolResults.push(toolText(call.id, call.name, `ERROR: unknown tool "${call.name}"`));