Add Windows PDFium system-renderer fallback for render_pdf_page/region - #195
Open
dancarloni wants to merge 10 commits into
Open
dancarloni wants to merge 10 commits into
dancarloni wants to merge 10 commits into
Conversation
render_pdf_page and render_pdf_region have never had a Windows fallback: pdfjsRendererPolicy(), systemRenderPage/Region, and runSystemCommand's spawn whitelist were all darwin-only, so any host whose native @napi-rs/canvas binding is blocked or unavailable on win32 gets "No PDF page renderer is available in this host" with no recourse. This is exactly the shape of Claude Desktop's embedded Electron UtilityProcess, which already has a proven, deliberate native-canvas block with its own crash latch (see nativeCanvasBlockRemediation and the embedded-host detection in pdfjs-subprocess.js) and no fallback to catch it on Windows. Add a Windows counterpart to the macOS qlmanage/sips fallback, using Google Chromium's official PDFium prebuilt library (via bblanchon/pdfium-binaries) called through koffi FFI, with PNG encoding done in pure JavaScript (server/png-encoder.js) since PDFium's C API renders to a raw bitmap and has no image encoder. Isolation is hybrid rather than a single mechanism, because the two plausible designs each fail differently in exactly one of the two hosts that matter: - Ordinary Windows hosts (Cursor, a plain CLI run): the PDFium render runs in a disposable child process, spawned through the same runSystemCommand sandboxing already proven for qlmanage/sips on darwin (whitelisted command, shell:false, strict argument-shape validation, a hard timeout, one system-render child at a time). A hang or crash in pdfium.dll only takes down that child. - Claude Desktop's embedded Electron UtilityProcess: relaunching process.execPath as a child process is not reliable there (its process.execPath is the Electron/Claude binary, not a plain node.exe; see the selectPdfjsIsolationMode comment in pdfjs-subprocess.js documenting the same class of failure for the PDF.js render worker itself). The render instead runs on a dedicated worker_threads.Worker, terminated on the same deadline, mirroring the pattern already proven in this codebase for the QPDF WebAssembly decryption worker and for pdf-lib mutations inside this exact host. pdfjsRendererPolicy() now returns native_with_system_fallback on win32 when the vendored pdfium.dll exists for the host's architecture, mirroring the darwin check; output-schemas.js's renderer enum gains "windows-pdfium". Vendors the official win-x64 and win-arm64 PDFium builds (bblanchon/pdfium-binaries tag chromium/8044, pdfium 155.0.8044.0, built without V8/XFA) under vendor/pdfium/, verified against pinned SHA-256 hashes in sources.lock.json and reproducible via scripts/vendor-pdfium-runtime.mjs. Every bundled component (PDFium itself plus FreeType, libpng, zlib, lcms, libjpeg-turbo, OpenJPEG, ICU, Abseil, LLVM libc, simdutf, fast_float, agg23) is permissively licensed (BSD/MIT/zlib/Apache-2.0/FTL/Unicode) with no copyleft component, all compatible with this project's MIT license; see vendor/pdfium/README.md and runtime.provenance.json for the component-by-license breakdown. Adds koffi as a dependency (prebuilt per-platform bindings, no compilation, consistent with this project's existing constraints) and test/pdfium-runtime-artifact.test.js, which binds the committed DLLs to their pinned hashes and (gated to win32, since koffi cannot load a Windows PE binary elsewhere) actually loads pdfium.dll and renders a page. .github/workflows/windows-render.yml and its probe script now also exercise this path on the existing windows-latest workflow_dispatch job, including the scenario this bug report was actually about: the embedded native-canvas block in force with the system renderer available. Regenerates the npm license provenance record for koffi's locked packages and the layout-occurrence-oracle.v1.json fixture, both stale after this change touched package.json/package-lock.json and output-schemas.js; fixes test/mcp-contract.test.js's share-mirror file count/list (it only matched *.js, missing the new *.mjs file) and its pinned tool-contract hash (moved by the legitimate renderer enum addition); adds renderer_label to two hand-built system_command test fixtures in test/pdfjs-subprocess-boundary.test.js that predate that field. Mirrors all server-side changes into pdf-toolkit-mcp-share/ per this project's existing convention. Does not touch the macOS qlmanage/sips renderer path, its error messages' default wording, or its tests: the shared error-message helpers now take an optional rendererLabel (default "macOS", unused by every existing call site) purely to word Windows failures correctly without duplicating them. Not yet done: vendor/pdfium/ and koffi are not wired into scripts/build-mcpb.mjs's MCPB packaging or the CycloneDX SBOM, so a built .mcpb or share ZIP does not yet ship the DLLs — the existing staged-vendor-inventory assertion in build-mcpb.mjs will fail loudly rather than ship an incomplete extension until that follow-up lands. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HK9BXJsKMD4rzNAkJpR7J
package-for-friend.js derives the share package's package.json dependencies from a hardcoded list rather than copying package.json verbatim, and koffi was missing from it: verifyShareLock() then found pdf-toolkit-mcp-share/package-lock.json's root record disagreeing with the derived manifest (the lock has koffi, the derived manifest didn't), failing npm run test:contract:share with "package-lock.json is stale at packages[\"\"].dependencies". Caught by the real CI run on PR #1 once GitHub Actions was enabled on the fork (this repo's git history has no CI runner, so this is the first time this specific script has actually executed against this change) — both matrix legs (node 20.19, node 22.12) failed identically at the same step for the same reason, with the "Test suite" step passing cleanly on both (2814/2814, no environment-specific noise, on real hardware). Verified locally with `npm run test:contract:share` before pushing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HK9BXJsKMD4rzNAkJpR7J
- validatePdfiumRenderRequest compared realpath(args[0]) against the pinned expectedDllPath, but args[0] is already checked with strict string equality against that same constant a few lines above, and lstat(args[0]) already rejects a symlinked DLL path. If the checkout itself is reached through a symlinked path component (a real possibility: bind mounts, npm workspace layouts, /tmp on macOS), realpath() resolves to a different string than the literal constant and a correctly-pinned request is wrongly rejected. Drop the redundant realpath comparison; the whitelist is unweakened by the two checks that remain. - The embedded-host PDFium render path (runPdfiumWorkerRender, a worker_threads.Worker) never enforced the one-system-renderer-at-a-time cap that the subprocess path already enforces via systemChildren.size in pdfjs-subprocess.js, so concurrent renders inside the Electron host had no bound. Add the same guard, reusing the existing system_renderer_concurrency_limit reason. Mirrored into pdf-toolkit-mcp-share/. Verified with the targeted test files (pdfjs-subprocess-boundary, pdfjs-worker-contract, render-pdf-page, mcp-contract): no new failures beyond the one pre-existing, environment-specific flake already documented on this PR (process-group tracking under this sandbox's root container). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HK9BXJsKMD4rzNAkJpR7J
- Create scripts/pdfium-runtime.mjs: exports runtime assets, files list, and verification function - Create scripts/pdfium-sbom.mjs: derives CycloneDX SBOM components from provenance - Update scripts/build-mcpb.mjs: stage PDFium DLLs and koffi packages per target platform - Update package-for-friend.js: include PDFium components in share package SBOM - Verify PDFium runtime against provenance during build validation - Platform-conditional staging: Windows targets only receive pdfium.dll and koffi - Reproducible builds: byte-identical verification across isolated builds Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HK9BXJsKMD4rzNAkJpR7J
The share package contract test creates an isolated build root and copies only specific script sources to it. With the new pdfium-runtime.mjs and pdfium-sbom.mjs scripts now imported by package-for-friend.js, they must be included in ISOLATED_PACKAGER_SCRIPT_SOURCES so the test can create a complete package build in an isolated checkout. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HK9BXJsKMD4rzNAkJpR7J
The share package contract test creates an isolated build root with only the necessary files for packaging. Since package-for-friend.js now imports pdfium-runtime.mjs which reads vendor/pdfium/runtime.provenance.json, and since pdfium-sbom.mjs references license files in vendor/pdfium/licenses/, those files must be copied to the isolated build root just like the QPDF runtime files are. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HK9BXJsKMD4rzNAkJpR7J
The share contract test derives the expected SBOM component count from the files actually shipped in the archive. The function expectedNativeSbomShape only accounted for QPDF WASM runtime components, but now also includes PDFium runtime components (runtime + bundled libraries + build recipe). Update the function to read PDFium's runtime.provenance.json and count its components. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HK9BXJsKMD4rzNAkJpR7J
PDFium is a Windows-only runtime included in the MCPB platform-specific packaging, not in the cross-platform share package for Cursor. The share package contains only QPDF WASM components, not PDFium. Remove: - PDFIUM_SBOM_DEPENDENCIES from share package SBOM dependencies - PDFIUM_SBOM_COMPONENTS from share package SBOM components - PDFium component validation from validateCycloneDxSbom - PDFium provenance/licenses from test build root population - PDFium component counting from test SBOM shape calculation The PDFIUM imports and exports remain for use by build-mcpb.mjs. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HK9BXJsKMD4rzNAkJpR7J
…oading pdfium-runtime.mjs reads vendor/pdfium/runtime.provenance.json at module import time (top-level code), so the file must exist in the isolated build root even though PDFium runtime files are not included in the share package. Copy only the provenance file (not the runtime directory or licenses) to allow package-for-friend.js to import and load the module successfully. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HK9BXJsKMD4rzNAkJpR7J
… list build-mcpb.mjs now imports it, which is part of build-agent-plugin.mjs's static import closure. test/plugin-freshness-coverage.test.js caught the gap: the freshness gate would have missed changes to it and reported a stale published plugin as fresh. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HK9BXJsKMD4rzNAkJpR7J
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical and moderate findings remain in Windows compatibility, rendering isolation, correctness, and MCPB/SBOM packaging.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds a Windows PDFium fallback for PDF page and region rendering when native canvas is unavailable.
Changes:
- Adds PDFium/koffi rendering, hybrid isolation, and pure-JavaScript PNG encoding.
- Vendors Windows runtimes and updates packaging, SBOM, provenance, licensing, and dependencies.
- Adds mirrored share-package updates, contract changes, artifact tests, and Windows CI coverage.
File summaries
| File | Reviewed changes / final comments |
|---|---|
vendor/pdfium/sources.lock.json |
Pins PDFium sources and hashes. |
vendor/pdfium/runtime.provenance.json |
Runtime provenance; nit (3 votes): stale integration status and incorrect share ZIP claim. |
vendor/pdfium/README.md |
Runtime documentation; nit (2 votes): documented generation does not match the promotion script. |
vendor/pdfium/licenses/zlib.txt |
Bundled license notice. |
vendor/pdfium/licenses/simdutf.txt |
Bundled license notice. |
vendor/pdfium/licenses/pdfium.txt |
Bundled license notice. |
vendor/pdfium/licenses/pdfium-binaries-build-scripts.txt |
Bundled license notice. |
vendor/pdfium/licenses/manifest.json |
License manifest. |
vendor/pdfium/licenses/llvm-libc.txt |
Bundled license notice. |
vendor/pdfium/licenses/libpng.txt |
Bundled license notice. |
vendor/pdfium/licenses/libopenjpeg.txt |
Bundled license notice. |
vendor/pdfium/licenses/libjpeg_turbo.md |
Bundled license notice. |
vendor/pdfium/licenses/libjpeg_turbo.ijg |
Bundled license notice. |
vendor/pdfium/licenses/lcms.txt |
Bundled license notice. |
vendor/pdfium/licenses/freetype.txt |
Bundled license notice. |
vendor/pdfium/licenses/fast_float.txt |
Bundled license notice. |
vendor/pdfium/licenses/agg23.txt |
Bundled license notice. |
vendor/pdfium/licenses/abseil.txt |
Bundled license notice. |
vendor/npm-licenses/npm-license-provenance.json |
Regenerated npm license provenance. |
test/pdfjs-subprocess-boundary.test.js |
Updates system-command fixtures. |
test/pdfium-runtime-artifact.test.js |
Verifies runtime hashes and Windows loading/rendering. |
test/mcp-contract.test.js |
Updates share-package contract data. |
test/fixtures/eval/extraction/phase1/layout-occurrence-oracle.v1.json |
Regenerated evaluation fixture. |
server/png-encoder.js |
Pure-JavaScript PNG encoding; nit (1 vote): add decoded pixel validation. |
server/pdfjs-worker.js |
Windows rendering and isolation; critical (1 vote): worker termination cannot contain native crashes; nits (2 and 3 votes): subprocess and region paths lack CI coverage. |
server/pdfjs-subprocess.js |
Windows command validation and subprocess support. |
server/pdfium-render-host.mjs |
PDFium FFI rendering; moderate (1 vote): pass page rotation to PDFium. |
server/output-schemas.js |
Adds the Windows renderer enum; critical (1 vote): update trajectory-grader renderer allowlists. |
server/index.js |
Enables Windows fallback selection; critical (2 votes): update the Windows renderer-policy test assertion. |
scripts/vendor-pdfium-runtime.mjs |
Promotes pinned runtime assets; moderate (1 vote): rebuild runtime and license directories from scratch. |
scripts/test-share-contract.mjs |
Share-package contract support. |
scripts/plugin-shipped-paths.mjs |
Shipped-path inventory support. |
scripts/pdfium-sbom.mjs |
PDFium SBOM generation; moderate findings (1, 2, 1, and 1 votes): fix notice URLs and hashes, add DLL references, and use valid SPDX identifiers. |
scripts/pdfium-runtime.mjs |
Defines and verifies PDFium runtime assets. |
scripts/build-mcpb.mjs |
Stages Windows runtime assets; moderate (3 votes): include PDFium license notices and synchronize inventory/hash checks. |
pdf-toolkit-mcp-share/server/png-encoder.js |
Mirrored PNG encoder. |
pdf-toolkit-mcp-share/server/pdfjs-worker.js |
Mirrored Windows rendering worker. |
pdf-toolkit-mcp-share/server/pdfjs-subprocess.js |
Mirrored subprocess support. |
pdf-toolkit-mcp-share/server/pdfium-render-host.mjs |
Mirrored PDFium rendering host. |
pdf-toolkit-mcp-share/server/output-schemas.js |
Mirrored output schema updates. |
pdf-toolkit-mcp-share/server/index.js |
Mirrored renderer policy updates. |
pdf-toolkit-mcp-share/package.json |
Mirrored package metadata. |
package.json |
Adds koffi dependency. |
package-lock.json |
Locks dependency updates. |
package-for-friend.js |
Packaging and SBOM integration; moderate (3 votes): add PDFium records to the generated MCPB SBOM. |
.github/workflows/windows-render.yml |
Adds Windows rendering workflow coverage. |
.github/workflows/windows-render-probe.mjs |
Adds embedded-host rendering probe. |
.gitattributes |
Tracks vendored runtime assets appropriately. |
Review details
Files not reviewed (1)
- pdf-toolkit-mcp-share/package-lock.json: Generated file
Suppressed comments (6)
scripts/pdfium-sbom.mjs:113
- Even after the notices are staged, this reference drops the
licenses/directory: the files live atvendor/pdfium/licenses/<name>, so every CycloneDX license URL currently points to a nonexistent path. Includelicenses/in the URL so SBOM consumers can resolve the notices.
const assetPath = `vendor/pdfium/${notice.file}`;
scripts/pdfium-sbom.mjs:162
- The PDFium runtime component has no hash or distribution reference for either architecture's
pdfium.dll, even though the provenance record carries both digests. The generated SBOM therefore cannot bind the shipped native payload to the reviewed bytes, unlike the existing QPDF runtime component; add architecture-specific DLL references and hashes.
return {
type: "library",
"bom-ref": RUNTIME_BOM_REF,
group: "pdf-tools",
name: "pdfium-runtime",
version,
scope: "required",
description:
`Official Google Chromium PDFium prebuilt binaries from bblanchon/pdfium-binaries, `
+ `shipping Windows platform binaries (win-x64 and win-arm64) at ${RUNTIME_DIRECTORY}/ `
+ `in the MCPB for Windows targets only.`,
purl: `pkg:generic/pdf-tools/pdfium-runtime@${version}`
scripts/pdfium-sbom.mjs:200
PDFIUM_SBOM_COMPONENTScopies each provenancespdxvalue directly into a CycloneDX license expression, but the new provenance usesAGGfor agg23 and the repository's pinned SPDX identifier record contains noAGGidentifier. Once this graph is included, the component and composite runtime license emit an invalid SPDX expression; use a valid SPDX identifier or an explicitLicenseRef-*representation and validate it.
return bundledAndRecipe.map(notice => ({
type: "library",
"bom-ref": `urn:pdf-tools:pdfium-bundled:${notice.component}`,
name: notice.component,
version: notice.component.includes(" ") ? notice.component.split(" ").pop() : "embedded",
scope: "required",
description: `${notice.component}; statically linked into ${RUNTIME_DIRECTORY}/pdfium.dll (Windows platforms only).`,
licenses: [{ expression: notice.spdx }],
externalReferences: [
noticeReference(provenance, notice),
],
scripts/vendor-pdfium-runtime.mjs:81
- The promotion script writes into existing runtime and license directories without clearing them first. If a future pinned release removes a DLL or notice, rerunning this generator leaves the old file behind and
writeLicenseManifest()records it as shipped, so the promoted tree is no longer an exact representation of the pinned archives. Rebuild the promoted directories from scratch before copying the new assets, as the QPDF promotion script does.
const runtimeDir = path.join(VENDOR_DIR, "runtime", asset.arch);
await mkdir(runtimeDir, { recursive: true });
await writeFile(path.join(runtimeDir, "pdfium.dll"), await readFile(dllPath));
server/pdfium-render-host.mjs:105
- The PDFium render call hard-codes its
rotateargument to0, whilesystemRenderPageWindows()sizes and crops the bitmap from PDF.js's already-rotated page view. PDFium's render API takes page rotation explicitly, so a page with/Rotateis rendered in unrotated page space and region crops can be misplaced or distorted. Pass the page rotation (or an equivalent transform) to PDFium and cover a rotated page in the Windows system-renderer path.
fn.FPDFBitmap_FillRect(bitmap, 0, 0, widthPx, heightPx, 0xffffffff);
fn.FPDF_RenderPageBitmap(bitmap, page, 0, 0, widthPx, heightPx, 0, FPDF_ANNOT);
server/png-encoder.js:81
- The new encoder's current runtime check only verifies the PNG signature; the Windows artifact test does not decode the image or verify pixel values. A BGRA channel, row-order, or stride regression could therefore pass the Windows gate while returning a corrupted render. Add a focused test with known RGBA bytes and assert decoded dimensions and pixels.
const raw = Buffer.alloc((stride + 1) * height);
const rgbaBuffer = Buffer.isBuffer(rgba) ? rgba : Buffer.from(rgba);
for (let row = 0; row < height; row += 1) {
const rawOffset = row * (stride + 1);
raw[rawOffset] = 0; // filter type: None
- Files reviewed: 28/52 changed files
- Comments generated: 8
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if (forced) return "system"; | ||
| if (disabled || process.platform !== "darwin") return "native"; | ||
| if (disabled || !available) return "native"; | ||
| return "native_with_system_fallback"; |
| rendered_height_px: integer, | ||
| scale: number, | ||
| renderer: enumString(["native-canvas", "macos-sips", "macos-quicklook"]), | ||
| renderer: enumString(["native-canvas", "macos-sips", "macos-quicklook", "windows-pdfium"]), |
Comment on lines
+2128
to
+2130
| const worker = new Worker(PDFIUM_RENDER_HOST_URL, { | ||
| workerData: { pdf_tools_worker: "pdfium_render", dllPath, heightPx, outputPngPath, sourcePdfPath, widthPx }, | ||
| }); |
Comment on lines
+20
to
+26
| PDFIUM_RUNTIME_FILES, | ||
| } from "./scripts/pdfium-runtime.mjs"; | ||
| import { | ||
| PDFIUM_RUNTIME_COMPONENT_BOM_REF, | ||
| PDFIUM_SBOM_COMPONENTS, | ||
| PDFIUM_SBOM_DEPENDENCIES, | ||
| } from "./scripts/pdfium-sbom.mjs"; |
| if (target.os !== "win32") return; // Only Windows targets get PDFium | ||
| const archDir = target.cpu === "x64" ? "win-x64" : "win-arm64"; | ||
| const dllPath = `${PDFIUM_RUNTIME_DIRECTORY}/${archDir}/pdfium.dll`; | ||
| copyRegularFile(dllPath, dllPath, stagingDir); |
Comment on lines
+124
to
+129
| // For shipped notices, include the hash | ||
| return { | ||
| type: "license", | ||
| url: assetPath, | ||
| comment: `${notice.component} (${notice.spdx})`, | ||
| hashes: [{ alg: "SHA-256", content: notice.sha256 }], |
Comment on lines
+2108
to
+2112
| async function runPdfiumSubprocessRender({ dllPath, sourcePdfPath, widthPx, heightPx, outputPngPath }) { | ||
| await runSystemCommand(process.execPath, [ | ||
| PDFIUM_RENDER_HOST_PATH, | ||
| dllPath, | ||
| sourcePdfPath, |
Comment on lines
+2242
to
+2246
| async function systemRenderRegionWindows(bytes, password, options) { | ||
| const page = await systemRenderPageWindows(bytes, password, { | ||
| page: options.page, | ||
| max_dimension_px: null, | ||
| renderer_policy: "system", |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
render_pdf_pageandrender_pdf_regionhave never had a Windows fallback:pdfjsRendererPolicy(),systemRenderPage/Region, andrunSystemCommand's spawn whitelist were all darwin-only, so any host whose native@napi-rs/canvasbinding is blocked or unavailable on win32 gets"No PDF page renderer is available in this host"with no recourse — including Claude Desktop's embedded Electron UtilityProcess, which already has a proven, deliberate native-canvas block with its own crash latch and no fallback to catch it on Windows.bblanchon/pdfium-binaries) called throughkoffiFFI, with PNG encoding done in pure JavaScript (server/png-encoder.js) since PDFium's C API renders to a raw bitmap and has no image encoder.runSystemCommandsandboxing already proven for qlmanage/sips on darwin (whitelisted command,shell:false, strict argument-shape validation, a hard timeout, one system-render child at a time).process.execPathas a child process is not reliable there (itsprocess.execPathis the Electron/Claude binary, not a plainnode.exe). The render instead runs on a dedicatedworker_threads.Worker, terminated on the same deadline, mirroring the pattern already proven in this codebase for the QPDF WebAssembly decryption worker and for pdf-lib mutations inside this exact host.pdfjsRendererPolicy()now returnsnative_with_system_fallbackon win32 when the vendoredpdfium.dllexists for the host's architecture;output-schemas.js's renderer enum gains"windows-pdfium".bblanchon/pdfium-binariestagchromium/8044, PDFium 155.0.8044.0, built without V8/XFA) undervendor/pdfium/, verified against pinned SHA-256 hashes insources.lock.jsonand reproducible viascripts/vendor-pdfium-runtime.mjs. Every bundled component is permissively licensed (BSD/MIT/zlib/Apache-2.0/FTL/Unicode), no copyleft, all compatible with this project's MIT license — seevendor/pdfium/README.mdandruntime.provenance.json.koffias a dependency (prebuilt per-platform bindings, no compilation) andtest/pdfium-runtime-artifact.test.js, which binds the committed DLLs to their pinned hashes and (gated to win32) actually loadspdfium.dlland renders a page..github/workflows/windows-render.ymland its probe script now also exercise this path on the existingwindows-latestworkflow_dispatchjob, including the scenario this fallback was built for: the embedded native-canvas block in force with the system renderer available.layout-occurrence-oracle.v1.jsonfixture (both stale after this change touchedpackage.json/package-lock.jsonandoutput-schemas.js); fixestest/mcp-contract.test.js's share-mirror file count/list and pinned tool-contract hash; addsrenderer_labelto two hand-builtsystem_commandtest fixtures. Mirrors all server-side changes intopdf-toolkit-mcp-share/per this project's existing convention.rendererLabel(default"macOS", unused by every existing call site).vendor/pdfium/andkoffiintoscripts/build-mcpb.mjs's per-target staging (stagePdfiumDllsForTarget,installKoffiForTarget, win32 targets only), extends the staged-vendor-inventory assertion and its provenance check (verifyPdfiumRuntime), and updates the CycloneDX SBOM (scripts/pdfium-sbom.mjs) so a built.mcpbnow actually ships the DLLs and platform-specific koffi bindings for win32-x64/arm64. The Cursor share ZIP intentionally excludes PDFium (Windows-only, MCPB-specific).Test plan
npm test— clean except for pre-existing, environment-specific failures unrelated to this change (confirmed against a clean baseline checkout of the same base commit): process-group/chmod-dependent and network-dependent tests that don't behave correctly in a sandboxed CI-like container running as root.npm run build:mcpb— reproducible, byte-identical build; staged MCPB verified to containvendor/pdfium/runtime/win-{x64,arm64}/pdfium.dlland the platform-specifickoffipackages.npm run smoke:mcpb— passes against the built artifact.npm run test:contract:share— passes; share package correctly excludes the Windows-only PDFium runtime.test/pdfium-runtime-artifact.test.js— 6/7 pass on Linux; the 7th (realpdfium.dllload + render) is gated to win32 and will run for real onwindows-latest.render_pdf_page/render_pdf_regionrun on a real Windows host — requested from the PR author; not yet performed.This PR has already been developed, tested, and verified on a fork (dancarloni#1), where it is green on CI and mergeable. This request proposes it as an upstream contribution, as a draft pending review from the Open Document Alliance maintainers.