WebAssembly
DynDyn.Pdf's fully-managed engine — the same DynDyn.Pdf.Core / DynDyn.Pdf.Engine assemblies
that back the CLI, the library, and the server —
also runs compiled to WebAssembly, inside a browser tab. A PDF dropped into the page is converted
to HTML entirely client-side. The file is never uploaded.
This is the same conversion, not a lookalike: no separate code path, no reduced feature set beyond what a browser sandbox mechanically cannot do (see What it cannot do, below). A document converted in the browser and the same document converted by the CLI or the server, with the same options, produce the same HTML.
How it's loaded
The browser build (DynDyn.Pdf.Wasm) is a headless Blazor WebAssembly host — it renders no
UI. It exposes exactly one JS-callable entry point:
DotNet.invokeMethodAsync('DynDyn.Pdf.Wasm', 'ConvertAsync', bytes, request)
where bytes is the PDF as a Uint8Array and request is a plain object matching the same
option contract described below. The bundle is loaded as a normal <script> tag
(wasm/_framework/blazor.webassembly.js, autostart="false") and started with
Blazor.start({ loadBootResource }); there is deliberately no Blazor JS module initializer, since
one would resolve its imports against the host page's URL rather than the bundle's own
/wasm/ path and hang the boot promise.
Because DynDyn.Pdf.Wasm is not referenced by the server project (a ProjectReference would force
every build of DynDyn.Pdf.Web to acquire the wasm-tools SDK workload) and is not part of
the main solution, it is published separately and copied into the server's wwwroot/wasm/ as a
build artifact:
pwsh scripts/publish-wasm.ps1
A deployment that never runs this script simply doesn't have wasm/_framework/blazor.boot.json
present, and the demo page falls back to server-side conversion only.
Boot cost
The bundle is several megabytes of brotli-compressed WebAssembly + IL (the current published
build totals roughly 6 MB across _framework/*.br before decompression) — a one-time download per
tab, cached by the browser afterward. This is the tradeoff for zero-upload conversion: bandwidth
and boot latency instead of a network round trip per document. The demo page's own boot timeout is
generous (180 seconds) precisely because that first download can be slow on a cold cache.
Options
The browser entry point accepts the same option surface as the server upload and the CLI — pages,
line grouping, semantic tags, decorative-text detection, style extraction, DPI, raster fallback,
password, font-embedding policy, and the DoS-boundary limits (MaxPages, MaxPagePixels,
MaxEstimatedModelBytes, timeout) — all as strings, mirroring how a multipart form field arrives,
so "where the conversion runs" never changes "what settings it runs with". Unset/blank/unparsable
values fall back to the same ConversionOptions defaults every other entry point uses.
Two fields are fixed by the browser environment rather than left to the caller:
MaxDegreeOfParallelism = 1— this WASM build runs single-threaded.MaxImageDimension = 2048(below the4096library default) — keeps a single tab's memory footprint in check.
What it can do
- Convert an ordinary PDF to a single self-contained HTML file (
SingleFileOutputWriter) with the full managed pipeline: clip-aware text extraction, glyph-outline font synthesis, vector backgrounds, semantic structure, decorative-text detection, style extraction. - Open password-protected and encrypted PDFs. The standard security handler's key-derivation
and content decryption (RC4 and AES-CBC, both the legacy revision 2–4 algorithms and revision 6)
are implemented as hand-written, pure-managed
Md5/AesCiphertypes inDynDyn.Pdf.Parsing/Encryption/, used unconditionally on every platform rather than delegating toSystem.Security.Cryptography. That BCL path does not exist under Blazor WebAssembly — .NET's crypto layer there backs onto the browser's SubtleCrypto, which does not expose MD5 at all and only exposes AES asynchronously — so before this fix an encrypted PDF converted in-browser silently returned incomplete output. It is fixed today: the same decryption code runs identically on the CLI, the server, and in the browser.
What it cannot do
- No native engine, no raster backgrounds. A browser sandbox cannot load pdfium; the WASM host
constructs its
DocumentConverterwith noIPageRasterizerFactory, so a page that would need a raster background degrades to its vector background with a warning instead — the same graceful fallback the CLI and server use when the native library is simply absent from the build. There is no--engine nativeequivalent in the browser. - No multi-document batching. One
ConvertAsynccall, one document, staged to a per-call temp directory and cleaned up afterward — there is no browser equivalent ofBatchConverter. - No filesystem access beyond the call. The uploaded bytes are written to WASM's in-memory
MEMFS-backed temp directory only for the duration of the conversion and deleted in a
finallyblock; nothing persists between calls or across a page reload.
Privacy
The whole point of this delivery form: the document never leaves the machine. No network
request carries the PDF's bytes anywhere — conversion happens entirely inside the tab's WASM
sandbox, and the resulting HTML is handed back to the calling page as an in-memory string (the demo
wraps it in a Blob URL for preview/download). This is a genuine architectural property, not a
policy promise: there is no code path in WasmConversionApi.ConvertAsync that performs a fetch or
XHR with the input bytes.
Related: server.md documents the same options on the multipart-upload path when
you do want server-side conversion; library.md documents the underlying
ConversionOptions/ConversionResult types this entry point maps onto; security.md
covers the DoS-boundary limits this entry point enforces more strictly than the library default
(never lets a limit be disabled, only resized) because a browser tab has no operator to raise them
back up.