feat: add an embedFonts option for AcroForm fields - #1789
Conversation
0d5d247 to
ee50422
Compare
…ls (fork-only) Not for upstream: this lets a package manager build js/pdfkit.js automatically when this branch is installed directly from GitHub as a dependency, since pdfkit has no committed build output and prepublishOnly only runs on npm publish, not on a git checkout. Both hooks are set because Yarn Classic v1 does not reliably run "prepare" for nested git dependencies (only "postinstall"), while npm relies on "prepare" for the same purpose. Consumed by the Plan monorepo while foliojs#1789 is under review.
…rk-only) Not for upstream: this lets Yarn install this branch directly from GitHub without a build step, since pdfkit has no committed build output and Yarn Classic v1 does not reliably run "prepare"/"postinstall" for a nested git dependency's own devDependencies (rollup couldn't be found when that was tried instead). Consumed by the Plan monorepo while foliojs#1789 is under review; rebuild and recommit js/ if this branch is rebased onto a newer fix.
…rk-only) Not for upstream: this lets Yarn install this branch directly from GitHub without a build step, since pdfkit has no committed build output and Yarn Classic v1 does not reliably run "prepare"/"postinstall" for a nested git dependency's own devDependencies. Consumed by the Plan monorepo while foliojs#1789 is under review; rebuild and recommit js/ if this branch is rebased onto a newer fix. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
1- This behavior should be opt in. This increase the file size significantly |
…rk-only) Not for upstream: this lets Yarn install this branch directly from GitHub without a build step, since pdfkit has no committed build output and Yarn Classic v1 does not reliably run "prepare"/"postinstall" for a nested git dependency's own devDependencies. Consumed by the Plan monorepo while foliojs#1789 and foliojs#1790 are under review; rebuild and recommit js/ if this branch is rebased onto a newer upstream master.
56f3d12 to
490720d
Compare
…rk-only) Not for upstream: this lets Yarn install this branch directly from GitHub without a build step, since pdfkit has no committed build output and Yarn Classic v1 does not reliably run "prepare"/"postinstall" for a nested git dependency's own devDependencies. Consumed by the Plan monorepo while foliojs#1789 and foliojs#1790 are under review; rebuild and recommit js/ if this branch is rebased onto a newer upstream master.
|
Thanks — reworked for all three points. Squashed to one commit, since the rework touched most of the original diff. 1. Opt-in. You were right about the cost, and by more than I had assumed: on a 5-field form with Roboto, 4.1 KB → 84.3 KB, roughly +80 KB per font. It is now 2. Dependency direction. 3. Sharing one program. I implemented it, measured it, and backed it out, because it turns out to be a net loss. Reporting the detail in case it saves you the same detour. It works mechanically: include every glyph in
So it only wins when the document draws no text at all, and otherwise loses about 7 KB. It is also only available when nothing has been drawn before I have not yet worked through how iText, QuestPDF and ReportLab handle this, as you suggested on #1096 — happy to do that if the shape of the opt-in is still in question. |
|
Great work. Needs rebase Please just ask AI for different option names for embedCompleteFonts. Is ok but if we get a shorter yet descriptive name the better |
`NeedAppearances` asks the reader to regenerate a field's appearance from its plain-text value, which it can only do with a font it can resolve that text against on its own. The font pdfkit embeds for its own content streams is subsetted and addressed by glyph id, so readers such as Acrobat fall back to a substitute font for the field. Fixes foliojs#1096. `initForm({ embedFonts: true })` embeds a complete, character-addressable copy of each custom font used in a field, as a composite font under a WinAnsiEncoding-to-glyph CMap (a simple font with `/Encoding /WinAnsiEncoding` cannot be used: fontkit's subset encoder emits no cmap or glyph-name tables, and its CFF output is always CID-keyed). It costs roughly the size of the font file per font, so it is off by default. Without it, output is byte-identical to before. EmbeddedFont knows nothing about AcroForm: it offers `completeRef()`, a complete, text-addressable embedding of itself, and acroform.js decides when a form needs one. `embed()` and `embedComplete()` now share one `embedProgram()` for the font program and descriptor they both build.
490720d to
cd32a19
Compare
|
Thanks! Rebased onto master and renamed the option to doc.initForm({ embedFonts: true });I also updated the PR description, which still described the earlier always-on version. |
…rk-only) Not for upstream: this lets Yarn install this branch directly from GitHub without a build step, since pdfkit has no committed build output and Yarn Classic v1 does not reliably run "prepare"/"postinstall" for a nested git dependency's own devDependencies. Consumed by the Plan monorepo while foliojs#1789 is under review; rebuild and recommit js/ if this branch is rebased onto a newer upstream master.
|
Many thanks |
What kind of change does this PR introduce?
Bug fix, opt-in. Fixes #1096 (also relevant to #1335).
A custom embedded font applied to an AcroForm text field renders in a substitute font in readers that regenerate the field's appearance, such as Adobe Acrobat/Reader, even though the same font renders correctly for page text.
Root cause
initForm()setsNeedAppearances, which asks the reader to regenerate a field's appearance from its plain-text value. That needs a font the reader can resolve text against on its own. The AcroForm's/DRand/DAresources reused the font pdfkit embeds for content streams: a subsetted Type0 font under/Encoding /Identity-Hwhose program has nocmaptable. That is fine for content streams, which address glyphs by id, but gives a reader no character encoding for field text, so it substitutes another font.The change
initForm({ embedFonts: true })embeds a complete, character-addressable copy of each custom font used in a field and references it from/DRand/DA:EmbeddedFont. It offerscompleteRef(), a complete, text-addressable embedding of itself;acroform.jsdecides when a form needs one. Standard fonts have nocompleteRef()and keep usingref()./Encoding /WinAnsiEncoding: fontkit's subset encoder emits nocmapor glyph-name tables and always produces CID-keyed CFF, so a simple font cannot address its glyphs. Going through the subset encoder (with every glyph included) is also what makes.woff/.woff2sources work, since their raw bytes are a compressed container rather than a font program.embed()andembedComplete()now build the font program and descriptor through oneembedProgram(subset, complete). A complete program gets no subset tag in its name and no/CIDSet; Acrobat rejects two different programs under the same tagged name./Length1is set on itsFontFile2, which Acrobat needs to load it.Using a single complete program for both page text and the form was investigated and measured; it costs more than it saves (details in this comment).
Demo
Before/after in Adobe Acrobat/Reader, same custom-font field. Before: the field falls back to a substitute font. After: it renders in the embedded font, no warnings on open. (The attached PDFs predate the option; the "after" behaviour is what
embedFonts: trueproduces.)Verification
yarn testpasses.tests/unit/acroform.spec.jscovers the default (the form reuses the content-stream font), a TrueType font with the option, and a CFF-flavored OpenType font with the option.yarn lintandyarn prettierare clean..ttf/.otfand.woff2.Checklist:
Credit to @r4tz52 for the original report.