7.7 KiB
In-Browser WASM Rendering — Reality Check & Implementation Plan
Status: Review-only. No code changed. Verified by reading the actual WASM facade + build config.
DECISION (deferred): Do not build WASM rendering now. Revisit after Phase 3 delivers the real PDF→display-list renderer — at which point the WASM path renders real content for free, and the transport/worker wiring (Tier 1) can be added once, correctly. The only thing that would flip this to "now" is a hard near-term offline/privacy requirement (documents must render in-browser), which would justify the PDFium→WASM route (Option A) despite its cost. WASM is a transport/optimization, not a feature, and does not advance the Adobe-grade content-editing goal (that's Phase 3).
TL;DR
- Your feature-flag / separate-command idea is correct — React keeps working because only the render transport swaps. Easy to build (Tier 1 below).
- But the current WASM engine renders a mock page, not the real PDF.
loadDocumentdoesn't parse the document;renderPagedraws a hardcoded fake page. So a "WASM render mode" today would show fake pages. - Making WASM render the real PDF in-browser is one of the two biggest efforts in the whole project — it needs either PDFium-compiled-to-WASM or finishing the Phase 3 custom renderer. That's a strategic decision, not a wiring task.
1. What actually exists today (verified)
| Piece | State | Evidence |
|---|---|---|
WASM build target pdfengine_wasm |
✅ Real | wasm/CMakeLists.txt:44 — compiles wasm_engine.cpp + pdf_engine_facade.cpp, exports _loadDocument/_renderPage/_getPageTextJson/... |
Exported C functions + JS glue (pdfengine.mjs/.wasm) |
✅ Real | build_wasm.ps1 builds + smoke-tests + copies to public/ |
WasmRasterizer (display-list → RGBA pixels, CPU) |
✅ Real | facade replays a display list into the output buffer |
| PDFium in WASM | ❌ OFF | CMakePresets.json wasm preset: PDFENGINE_WITH_PDFIUM: OFF |
loadDocument parses the real PDF |
❌ Mock | pdf_engine_facade.cpp:289 — counts pages by scanning for /Type /Page, builds a WasmMockDocument |
renderPage renders real content |
❌ Mock | draws hardcoded fillRect border/grid + "Page N" via WasmMockPage |
getPageTextJson |
❌ Mock | returns mock text |
| Frontend uses WASM for rendering | ❌ Dormant | wasmLoader.loadEngine() only console.logs; WasmInspector not mounted; viewer renders server-side via /render |
So: the plumbing (Emscripten build, exports, rasterizer, JS bridge) is real and done. The content (PDF parsing + real rendering) is a mock, because the only two things that can turn a real PDF into pixels — PDFium, or the engine's own content-stream renderer — are respectively disabled and unbuilt (Phase 3).
2. Your "separate command" idea — yes, it works (and it's the right pattern)
A feature-flagged render mode is exactly how the roadmap intends it ("Feature-flagged. Falls back to server-side render if WASM not loaded").
- Add
VITE_RENDER_MODE=wasmand a scriptnpm run dev:wasm. - Abstract rendering behind a
RenderSourceinterface:ServerRenderSource(today's/render) andWasmRenderSource. - React is unaffected — the UI, tools, overlays, inspector all stay identical; only where the page bitmap comes from changes.
- Auto-fallback to server if WASM fails to load. No risk to the default path.
This part is genuinely small and safe to build.
3. Implementation plan — two tiers
Tier 1 — WASM transport + Web Worker + feature flag (buildable now)
Completes the roadmap's "React: WASM integration + Web Worker" item and validates the facade. Output is still the mock page until Tier 2, but the entire in-browser pipeline becomes real, off-main-thread, and flag-controlled.
Steps:
- Build + validate the existing WASM — run
scripts/build_wasm.ps1(requires EMSDK installed), confirmwasm/pdfengine.test.mjspasses, i.e. the real.wasmactually loads/executes (vs the JS mock fallback). RenderSourceabstraction in the frontend — interfacerenderPage(docRef, pageIndex, zoom) → bitmap; implementationsserver(current) andwasm.- Web Worker — run the WASM module in a Worker;
postMessage({pageIndex, scale})→ transfer back anImageData/ArrayBuffer. Keeps the main thread free (the roadmap's "no main-thread blocking"). - Load doc bytes into the worker once —
fetchDocumentBytes→ copy into WASM heap →loadDocumenthandle cached per doc. - Feature flag +
npm run dev:wasm+ graceful fallback to server if the worker/WASM is unavailable. - Wire
CanvasLayerto draw from whichever source returns the bitmap.
Effort: ~3–5 days. Risk: low (default server path untouched). Caveat: renders the mock page — honest, but not yet useful for real docs.
Tier 2 — REAL in-browser PDF rendering (the strategic decision)
To render the actual PDF in-browser, pick one:
| Option | What it is | Effort | Trade-offs |
|---|---|---|---|
| A. PDFium → WASM | Compile PDFium with Emscripten; facade calls FPDF_RenderPageBitmap (same engine as server) |
Very large (PDFium Emscripten port; multi-MB .wasm; heavy build) |
✅ Full fidelity, matches server exactly · ❌ huge payload + build complexity. Reference: pdfium.js exists |
| B. Finish the custom renderer (= Phase 3) | Build PDF parse + content-stream interpreter + display-list builder; WasmRasterizer already does display-list→pixels |
Months (this is Phase 3: content-stream analyser, text/path/image operators, Skia/CPU rasterizer) | ✅ Aligns with the custom-engine vision; smaller .wasm; no PDFium · ❌ the single hardest module in the project |
| C. Don't (keep server render primary) | Leave WASM as offline/preview mock; ship real rendering only via server | none | ✅ zero cost; every feature already works server-side · ❌ no in-browser/offline real rendering |
Key reframe: WASM rendering is a performance / offline transport, not a feature. The server render path already delivers every editing capability at full fidelity. So Tier 2 is strategic/optional, not blocking anything.
4. Recommendation
- Build Tier 1 now — it's small, safe, completes the "React WASM + Web Worker" roadmap item, validates the facade, and gives you the
npm run dev:wasmmode you described. Be clear-eyed that it renders the mock page until Tier 2. - Decide Tier 2 deliberately, don't drift into it. If in-browser/offline real rendering is a near-term product need → Option A (PDFium-WASM) is the faster route to fidelity. If the custom-engine is the long-term goal → Option B is Phase 3 and should be planned as such. If neither is urgent → Option C (server stays primary) is perfectly legitimate.
- Either way, fix the mislabel: the roadmap's "C++ Engine → WASM Facade — Done" is true only for the plumbing; the facade renders a mock. Reword to "WASM facade + rasterizer done; real in-browser rendering pending PDFium-WASM or Phase 3 custom renderer."
5. Effort & decision summary
| Work | Buildable now? | Effort | Blocks features? |
|---|---|---|---|
Tier 1: transport + Web Worker + flag (dev:wasm) |
✅ yes | ~3–5 days | no |
| Tier 2A: PDFium → WASM (real render) | needs decision | very large | no (optimization) |
| Tier 2B: custom renderer (real render) | = Phase 3 | months | no (optimization) |
| Tier 2C: don't, keep server primary | n/a | none | no |
Prereq for any WASM build: EMSDK (Emscripten) installed + on PATH (build_wasm.ps1 looks for it).