Files
pdf/docs/wasm-rendering-plan.md
T
2026-06-10 21:51:43 +05:30

7.7 KiB
Raw Blame History

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. loadDocument doesn't parse the document; renderPage draws 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=wasm and a script npm run dev:wasm.
  • Abstract rendering behind a RenderSource interface: ServerRenderSource (today's /render) and WasmRenderSource.
  • 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:

  1. Build + validate the existing WASM — run scripts/build_wasm.ps1 (requires EMSDK installed), confirm wasm/pdfengine.test.mjs passes, i.e. the real .wasm actually loads/executes (vs the JS mock fallback).
  2. RenderSource abstraction in the frontend — interface renderPage(docRef, pageIndex, zoom) → bitmap; implementations server (current) and wasm.
  3. Web Worker — run the WASM module in a Worker; postMessage({pageIndex, scale}) → transfer back an ImageData/ArrayBuffer. Keeps the main thread free (the roadmap's "no main-thread blocking").
  4. Load doc bytes into the worker oncefetchDocumentBytes → copy into WASM heap → loadDocument handle cached per doc.
  5. Feature flag + npm run dev:wasm + graceful fallback to server if the worker/WASM is unavailable.
  6. Wire CanvasLayer to draw from whichever source returns the bitmap.

Effort: ~35 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

  1. 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:wasm mode you described. Be clear-eyed that it renders the mock page until Tier 2.
  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.
  3. 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 ~35 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).