Managed vs Native (pdfium) — fidelity, performance, prod-readiness
Head-to-head of the two build flavors converting the same PDFs to HTML, each page scored against
the pdfium page render (ground truth) with bench/fidelity_bench.py. Both flavors run over an
identical 39-document representative sample (text-heavy, chart, photo-cover, multi-language + a few
pdf.js edge cases), sampling the cover page + 10 body pages per document — 408 pages compared.
Fidelity
| metric | Managed (SVG + live text) | Native (pdfium raster fallback + live text) |
|---|---|---|
| mean SSIM | 0.850 | 0.859 |
| median SSIM | 0.871 | 0.877 |
| mean structural cosine (photo/AA-robust) | 0.958 | 0.978 |
| confirmed suspects (SSIM<0.72 and coverage off) | 5 | 2 |
| pages where managed ≥ native | 392 / 408 (96 %) | — |
| pages where native beats managed by >0.10 SSIM | 11 / 408 (3 %) | — |
The two flavors are within ~1 % mean SSIM, and managed equals or beats native on 96 % of pages. Native's advantage is concentrated on the 3 % of pages that are design-heavy graphics — full-bleed marketing covers and infographic spreads — where pdfium's raster is more robust than the managed SVG path.
Where native wins (the gap)
All the >0.10-SSIM wins are graphics-heavy pages. The design-heavy-cover defects surfaced here:
Nestlé p1 / p12 — a JP2 cover image whose ICC colour box tripped the managed JPX decoder → dropped → near-blank. FIXED (decode the bare jp2c codestream on the colour-box failure); the cover now renders. This also fixed every other JP2-with-ICC-box image in the corpus.
Orange Polska p1 — a black marketing cover whose subset WOFF emitted empty glyf outlines for
a/e/y(RAPORT ZINTEGROWANY→R PORT ZINT GROW N). FIXED (#21): the font ships only a platform-0 Unicode cmap; adopt it asWindowsUnicodeCMapwhen there is no Windows(3,1)so glyph lookup no longer falls to the buggy post-table name search. Cover now renders in full.Explicit
/Maskstencil covers (Arcadis, Aalberts, Aegon, Galp…) — full-bleed / hexagon-clipped photo covers whose base image is decodable but carries an explicit stencil/Mask(anImageMask, CCITT 1-bit). Every managed decode path bailed on the stencil → the whole image dropped to a blank page. FIXED (20d21f0): decode the stencil to an alpha plane (inverted vs a soft mask) and apply it as a luminance mask / baked RGBA. This was the dominant cause of managed blank pages — 78 pages across 9 docs — now resolved.
On text, tables and charts — the body of an ESEF financial report — the two flavors are indistinguishable (managed's #20 / #18 / soft-mask / JPX / #21 / stencil-mask fixes closed the confirmed defects).
SSIM under-scores managed's faithful vector photos (pdfium and Blink rasterize JPEGs differently), so the raw-SSIM gap overstates the real difference. The coverage-ratio + structural-cosine columns and the confirmed-suspect count are the fair signal.
Performance (convert, 30 pages, -f pages)
| document | managed | native |
|---|---|---|
| SAP 2024 (text) | 1.2 s | 1.4 s |
| Continental 2024 (charts) | 4.5 s | 4.3 s |
| Novo Nordisk 2024 | 1.1 s | 1.1 s |
Essentially identical. The native raster fallback only adds pdfium-render time on the specific pages it rasters; everywhere else both take the same managed extraction path.
Deployment
| managed | native | |
|---|---|---|
| pdfium native binaries | 0 (native-free) | 12 |
| output size on disk | 90 MB | 160 MB |
| runs on | any .NET 10 target, no native libs | needs the pdfium binaries per platform |
| output | pure inline SVG + live selectable HTML text | same, but hard pages are an inline raster PNG with live text on top |
Managed is native-free — deploys anywhere .NET 10 runs, no per-platform native libraries, and every page stays machine-readable vector + live text (the ESEF requirement).
Verdict — is managed production-ready?
For ESEF / born-digital financial reports (text, tables, charts, photos): yes. Managed is fidelity-equivalent to native (mean SSIM within 1 %, ≥ native on 96 % of pages, identical performance), fully machine-readable, and native-free. The confirmed fidelity defects surfaced by the bench in this area were fixed this cycle (chart strokes #20, soft-masked images #18 + Merlin/Kofola).
The remaining gap is design-heavy marketing covers (~1–3 % of pages), where managed occasionally drops a complex cover image or subset-font glyph that native's pdfium raster handles.
The pure-managed flavor reproduces page content as vector SVG with live text. Where a page contains content it cannot reproduce faithfully, it emits a per-page warning identifying the page and the feature, and keeps the vector background — it never silently degrades. The native flavor additionally carries an independent renderer (pdfium) that can substitute a raster background for such pages; text stays live HTML in both flavors. Because that rescue depends on a second, independent PDF implementation, it is inherently native-only: a managed renderer would be driven by the same extraction that flagged the page, and would reproduce the same gap.
Font-embedding policy parity (#45)
Both engines build glyph outlines themselves rather than delegating rendering to pdfium/Skia — the
native engine reads FPDFFont_GetGlyphPath per captured code point into the same PathCommand shape
the managed engine emits from the vendored parser, and both feed the result into the same shared, engine-agnostic
decision logic (DynDyn.Pdf.Core.Fonts.FontEmbeddingRights / FontSubstitution, DynDyn.Pdf.Fonts'
vendored OFL Liberation faces). There is no pdfium-vs-managed architectural fork here: FontEmbeddingPolicy.SubstituteFree
now behaves identically on both engines for a restricted-embedding font (fsType Restricted
License / No Subsetting / Bitmap-Only) — the source's glyph advances are kept and every glyph is
redrawn from the bundled OFL substitute, gated the same way behind ConversionOptions.EmbeddingRightsAttestation.
PdfToHtmlConverter.BuildFontAssets (native) was ported line-for-line from DocumentConverter.BuildFontAssets
(managed) to use the full FontEmbeddingRights.Decide overload instead of the old Strict-only Skip check.
Fixed (#193): the native FontRegistry used to build a FontSpec only for a font it had captured an
embedded program from — a PDF font dictionary with no FontFile/FontFile2/FontFile3 at all
(e.g. a bare reference to a standard-14 font) never reached the font pipeline, so
FontSpec.NoEmbeddedProgram was hardcoded false on the native path and SubstituteFree's "never
embedded to begin with" branch could never fire there. FontRegistry.GetOrRegister now asks pdfium
directly (FPDFFont_GetIsEmbedded) rather than inferring embedding from whether font-data bytes were
readable, and registers an accumulator for every font it sees — embedded or not — so
FontSpec.NoEmbeddedProgram carries the real signal and SubstituteFree behaves identically on both
engines for this case too. See FontRegistryNonEmbeddedTests in DynDyn.Tests.Native for the
end-to-end regression (a real pdfium round trip through a bare /BaseFont /Helvetica reference).