feat(export): client-side DOCX → PDF export for Editor V2 (R&D) - #3919
Draft
gpardhivvarma wants to merge 5 commits into
Draft
feat(export): client-side DOCX → PDF export for Editor V2 (R&D)#3919gpardhivvarma wants to merge 5 commits into
gpardhivvarma wants to merge 5 commits into
Conversation
Add `superdoc.export({ exportType: ['pdf'], triggerDownload: true })` — a fully
client-side, WASM-free PDF export. SuperDoc V2 already renders a pixel-accurate
paginated DOM; the exporter redraws those pages into a PDF with pdf-lib, so the
result is WYSIWYG with the editor: selectable text, embedded/subset fonts and
clickable links.
Core (packages/superdoc):
- core/export/pdf-export.ts — DOM→PDF: word-anchored + width-matched text,
per-glyph font fallback (DejaVu symbols + lazy Noto SC for CJK), URI + GoTo
link annotations, table/shading/border replay, images (canvas→PNG), true-vector
SVG (raster fallback), scroll-to-paint for virtualized pages, zoom reset.
- core/export/font-extract.ts — deobfuscate the DOCX's embedded .odttf fonts and
embed them byte-exact.
- core/export/field-resolve.ts — parse header/footer PAGE/NUMPAGES fields and draw
real page numbers (SuperDoc's layout leaves them blank).
- Wire into SuperDoc.export(); add ExportParams.pdfOptions; class names read via
@superdoc/dom-contract where the contract covers them.
- tests/consumer-typecheck fixture pinning the public export({pdf}) shape.
pdf-export-poc/: standalone deployable POC on published superdoc@2.7.0, validated
against the calibre torture-test + 6 more fixtures. FINDINGS.md covers the full
R&D, incl. why Typst was rejected and why RTL/complex-script shaping is the one
gap that needs a WASM shaper.
Note for CI: run `pnpm install` to refresh the lockfile for pdf-lib /
@pdf-lib/fontkit; fonts are bundled in the POC for now (CDN/LFS for production).
The SVG vectorizer used a single uniform scale (width/viewBox), so an SVG with a square viewBox rendered at a non-square size — e.g. a footnote separator with a `0 0 100 100` viewBox rendered 312×1 — was drawn as a large filled square (a black box over the footnote). drawSvgPath only supports uniform scaling, so when the x/y scales differ (>2%) fall back to rasterizing, which reproduces the element at its exact rendered size.
Add `mode: 'word' | 'pixel'` to the PDF exporter. 'pixel' rasterizes each page via SVG <foreignObject> (the browser's own paint pipeline) with the DOCX's embedded fonts re-declared as data-URI @font-face inside the SVG, plus an invisible selectable-text + link overlay. Verified pixel-identical to the editor (0 of 3.4M px differ) on the calibre torture-test and a Hebrew RTL doc, which also makes RTL/Arabic shaping exact. 'word' (vector) stays the default. Also fix four systematic editor-parity bugs in vector mode, found by localized cluster diffing against live editor screenshots: - floating images that paint in front of text are drawn after the text pass (was: before, so text bled through them) - bold Hebrew keeps its weight via a weight-aware DejaVu Sans Bold fallback - RTL header/footer page-number fields get a level-1 bidi visual reorder - table border strokes are inset by half their width (CSS box-model), so they land pixel-exact instead of ~1.5px off Grows the public surface: pdfOptions.mode on ExportParams, pinned in the consumer-typecheck fixture. POC updated with a mode picker; FINDINGS/README document the parity proof and methodology.
…-pdf-export # Conflicts: # packages/superdoc/package.json
…ixel The pixel-mode invisible selectable-text layer drew every token with the primary font only, which lacks Hebrew/CJK glyphs — so those embedded as .notdef with no ToUnicode and copied out as nothing (English worked, Hebrew didn't). Draw the invisible layer per-character at each glyph's measured visual position using the same covering-font fallback as the visible path (DejaVu for Hebrew), via a new opacity parameter on drawWord. Selection now aligns with the page image and copy yields real characters; bidi-aware apps paste RTL in correct order. POC: default to pixel mode and drop the word/pixel picker (pixel is the high-fidelity path). `?mode=word` still forces vector for comparison.
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.
What
Adds a fully client-side, WASM-free PDF export to Editor V2 — the exact API from the brief:
Live demo: https://pdf-export-poc.vercel.app (open the bundled calibre doc or Open .docx… your own; then Export PDF)
How
V2 already renders a pixel-accurate paginated DOM. The exporter redraws those pages into a PDF with pdf-lib (no server, no WASM), so output is WYSIWYG with the editor: selectable text, embedded/subset fonts, clickable links.
.odttf) and embedded byte-exact; non-embedded families use bundled substitutes, with per-glyph fallback (DejaVu symbols + a lazily-loaded Noto Sans SC for CJK)./URI+ in-document/GoTo(TOC) annotations.PAGE/NUMPAGESfields are parsed from the DOCX and drawn with real numbers (V2's layout leaves them blank).pdf-lib + fontkit are dynamically imported (only load on a PDF export).
Files
packages/superdoc/src/core/export/{pdf-export,font-extract,field-resolve}.ts— the exporter + DOCX font/field parsing.SuperDoc.export()wired forexportType: ['pdf'];ExportParams.pdfOptionsadded; DOM classes read via@superdoc/dom-contractwhere the contract covers them.tests/consumer-typecheck/src/export-pdf-params.ts— pins the publicexport({pdf})shape.pdf-export-poc/— standalone deployable POC (publishedsuperdoc@2.7.0), validated against the calibre torture-test + 6 fixtures.pdf-export-poc/FINDINGS.mdhas the full R&D write-up.R&D findings (see FINDINGS.md)
Notes for reviewers / follow-ups
pnpm installto refresh the lockfile for the newpdf-lib/@pdf-lib/fontkitdeps.@superdoc/fonts/@superdoc/font-system(which already loads the embedded bytes).doc.export.toPdf()in@superdoc/document-api.build:superdoc+check:publicin CI.