fix: AcroForm custom fonts fall back to a substitute font in Acrobat - #1789
Open
KaiPressmar wants to merge 2 commits into
Open
fix: AcroForm custom fonts fall back to a substitute font in Acrobat#1789KaiPressmar wants to merge 2 commits into
KaiPressmar wants to merge 2 commits into
Conversation
pdfkit's initForm() always sets NeedAppearances, so a reader may need to regenerate a form field's appearance from its plain-text value at any time. Doing that requires resolving arbitrary characters to glyphs through the font referenced in the AcroForm's /DR and /DA resources. For a custom embedded font, that resource previously reused the same font object used in content streams: a subsetted Type0 font under /Encoding /Identity-H. Identity-H has no character encoding a reader can resolve on its own, and pdfkit's subsetter drops the font's cmap table entirely, since content streams address glyphs directly by ID. A reader regenerating a field's appearance therefore has no way to map the field's text to a glyph in that font, and silently falls back to a substitute font -- reproducible in Adobe Acrobat/Reader, though not in every viewer. Embed a second, dedicated font object for AcroForm use instead: the complete, un-subsetted font program under a standard /Encoding /WinAnsiEncoding, so a reader can look up any character itself. This font is not a subset, so it does not use the subset-tag BaseFont naming convention (a six-uppercase-letter prefix meaning "an arbitrary subset of the font named after the +"); reusing that tag previously gave both font objects an identical BaseFont name despite differing font programs, which caused Acrobat's "embedded font could not be extracted" warning during testing. The same FontFile2 stream was also missing the required /Length1 entry (the uncompressed program length), which triggered the same warning on its own before the naming was fixed. Fixes foliojs#1096
KaiPressmar
force-pushed
the
acroform-font-fix-1096
branch
from
September 5, 2026 10:17
0d5d247 to
ee50422
Compare
KaiPressmar
added a commit
to KaiPressmar/pdfkit
that referenced
this pull request
Sep 5, 2026
…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.
KaiPressmar
added a commit
to KaiPressmar/pdfkit
that referenced
this pull request
Sep 5, 2026
…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.
The AcroForm font this method builds must give a viewer a way to resolve field text to a glyph on its own, without relying on pdfkit's own subsetting. The previous fix satisfied that for TrueType sources by embedding a simple font with /Encoding /WinAnsiEncoding built from `this.font.stream.buffer`, but that buffer is the font's original file bytes -- for a WOFF/WOFF2 source, a compressed container rather than a valid standalone font program, so Acrobat still reported the font as one it "could not be extracted" for any font actually loaded from a .woff2 file (as most web-sourced fonts are). Route the font program through fontkit's ordinary subset encoder instead, including every glyph so the reader can still resolve any character. That encoder always normalizes its input into a valid TrueType/CFF program, but also always produces CID-keyed, cmap-less output -- incompatible with a simple font's /Encoding /WinAnsiEncoding. Rebuild the AcroForm font as a composite (Type0/CID) font instead, addressed through a custom WinAnsi-code-to-glyph-id CMap built from the font's own cmap (replacing the usual /Identity-H, which only a content-stream author who already knows the glyph ids can use) -- this works uniformly for both TrueType and CFF sources. Add a CFF-flavored regression test (tests/fonts/Montserrat-Bold.otf) alongside the existing TrueType one, since the CFF/CIDFontType0 path was never exercised by the original fix. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
KaiPressmar
added a commit
to KaiPressmar/pdfkit
that referenced
this pull request
Sep 5, 2026
…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>
Member
|
1- This behavior should be opt in. This increase the file size significantly |
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 kind of change does this PR introduce?
Bug fix. Fixes #1096 (also relevant to #1335).
A custom embedded font applied to an AcroForm text field renders in the wrong font in some readers — reported here in Adobe Acrobat/Reader — even though the same font renders correctly for ordinary page text.
Root cause
initForm()always setsNeedAppearances: true, so a reader may regenerate a form field's appearance from its plain-text value at any time. Doing that requires resolving arbitrary characters to glyphs through the font referenced in the AcroForm's/DRand/DAresources.For a custom embedded font, that resource was reusing the exact same font object used in content streams. I inspected the raw PDF bytes pdfkit produces and confirmed:
/DR /Fontentry and the page's content-stream font are literally the same object: a subsettedType0font under/Encoding /Identity-H.cmaptable at all (head, hhea, loca, maxp, cvt, prep, glyf, hmtx, fpgm— nocmap). That's fine for content streams, since pdfkit addresses glyphs directly by glyph ID there, but it leaves a reader with no way to map a form field's text to a glyph in that font when regenerating its appearance, andIdentity-Hisn't a standard character encoding it can fall back on either. It silently substitutes another font instead.The fix
EmbeddedFontnow embeds a second, dedicated font object the first time it's needed for AcroForm use, holding every glyph so a reader can resolve any character a user later types into the field, not just the ones already drawn elsewhere in the document.acroform.jsnow asks each font for this dedicated reference;StandardFonthas no such distinction, so it falls back to its existing.ref()(the 14 standard fonts aren't embedded and already useWinAnsiEncoding, so they were never affected).The dedicated font is a composite (Type0/CID) font, addressed through a custom WinAnsi-code-to-glyph-id CMap rather than a simple font under
/Encoding /WinAnsiEncoding(the initial version of this PR). That redesign came from a second real-world bug, found after this PR was first opened and after real Acrobat testing against only a synthetic.ttftest fixture had passed:this.font.stream.bufferdirectly, assuming it was always a valid, standalone TrueType or CFF program. For a font whose source file is.woff/.woff2— which is how most web-sourced fonts, and both fonts that surfaced this in production, actually arrive — that buffer is the original compressed container, not a font program a viewer can load at all. Acrobat reported it as a font it "could not be extracted", the same failure this PR set out to fix.cmap-less output for both TrueType and CFF sources — incompatible with a simple font's/Encoding /WinAnsiEncoding, which needs named, non-CID glyph access.cmapwould otherwise have supplied. It works uniformly whether the source font is TrueType or CFF, and whether it came from a.ttf/.otffile or a.woff/.woff2one.Two pitfalls worth flagging for review, both found and fixed while testing against real Acrobat:
BaseFontnaming convention (the six-uppercase-letter prefix meaning "an arbitrary subset of the font named after the +"), since it isn't a subset in pdfkit's usual sense. Reusing it gave both font objects an identicalBaseFontname despite differing font programs, which made Acrobat report the embedded font as one it "could not be extracted".FontFile2/FontFile3stream needs its required length entry (/Length1forFontFile2, spec 9.9 Table 127); pdfkit's existing embedding for content-stream fonts doesn't set it either, but that only surfaced as a problem once this stream was something Acrobat's forms engine actually tried to load and validate on its own.Demo
Attached: a before/after PDF pair (same custom-font AcroForm field, built with pdfkit as it was before this fix and with this fix applied) plus screenshots of both opened in Adobe Acrobat/Reader. Before: the field falls back to a substitute font. After: it renders in the embedded font, no warnings on open.
Verification
yarn test:unit— all 438 tests pass, including two regression tests intests/unit/acroform.spec.js: one against a TrueType fixture, one against a CFF-flavored (OpenType/CFF) fixture, since the CFF/CIDFontType0code path is a separate path through this fix and wasn't exercised by the first version of it.yarn lint/yarn format— clean..woff2files (both TrueType- and CFF-flavored), which is what caught the second bug described above.Checklist:
Credit to @r4tz52 for the original report.
cc @blikblum, since you looked at this issue before — flagging for review whenever you have a chance.