Skip to content

fix: AcroForm custom fonts fall back to a substitute font in Acrobat - #1789

Open
KaiPressmar wants to merge 2 commits into
foliojs:masterfrom
KaiPressmar:acroform-font-fix-1096
Open

fix: AcroForm custom fonts fall back to a substitute font in Acrobat#1789
KaiPressmar wants to merge 2 commits into
foliojs:masterfrom
KaiPressmar:acroform-font-fix-1096

Conversation

@KaiPressmar

@KaiPressmar KaiPressmar commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

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 sets NeedAppearances: 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 /DR and /DA resources.

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:

  • The AcroForm's /DR /Font entry and the page's content-stream font are literally the same object: a subsetted Type0 font under /Encoding /Identity-H.
  • That embedded, subsetted TrueType program has no cmap table at all (head, hhea, loca, maxp, cvt, prep, glyf, hmtx, fpgm — no cmap). 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, and Identity-H isn't a standard character encoding it can fall back on either. It silently substitutes another font instead.

The fix

EmbeddedFont now 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.js now asks each font for this dedicated reference; StandardFont has no such distinction, so it falls back to its existing .ref() (the 14 standard fonts aren't embedded and already use WinAnsiEncoding, 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 .ttf test fixture had passed:

  • The original fix embedded this.font.stream.buffer directly, 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.
  • Routing the font program through fontkit's ordinary subset encoder instead (including every glyph, so nothing is missing) fixes that: the encoder always normalizes any source format into a valid TrueType/CFF program. But that encoder also always produces CID-keyed, cmap-less output for both TrueType and CFF sources — incompatible with a simple font's /Encoding /WinAnsiEncoding, which needs named, non-CID glyph access.
  • The composite-font-plus-custom-CMap design resolves both constraints at once: it only ever needs the subset encoder's actual output shape (CID-keyed access to any glyph by id), and the CMap gives a reader the character encoding a simple font's built-in cmap would otherwise have supplied. It works uniformly whether the source font is TrueType or CFF, and whether it came from a .ttf/.otf file or a .woff/.woff2 one.

Two pitfalls worth flagging for review, both found and fixed while testing against real Acrobat:

  • This new font object must not reuse pdfkit's subset-tag BaseFont naming 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 identical BaseFont name despite differing font programs, which made Acrobat report the embedded font as one it "could not be extracted".
  • The FontFile2/FontFile3 stream needs its required length entry (/Length1 for FontFile2, 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.

image

Verification

  • yarn test:unit — all 438 tests pass, including two regression tests in tests/unit/acroform.spec.js: one against a TrueType fixture, one against a CFF-flavored (OpenType/CFF) fixture, since the CFF/CIDFontType0 code path is a separate path through this fix and wasn't exercised by the first version of it.
  • yarn lint / yarn format — clean.
  • Manually verified against Adobe Acrobat with a generated form: before the fix, the field falls back to a substitute font; with the fix, it renders in the embedded font with no warnings on open. Also verified against real production fonts loaded from .woff2 files (both TrueType- and CFF-flavored), which is what caught the second bug described above.

Checklist:

  • Unit Tests
  • Documentation
  • Update CHANGELOG.md
  • Ready to be merged

Credit to @r4tz52 for the original report.

cc @blikblum, since you looked at this issue before — flagging for review whenever you have a chance.

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
KaiPressmar force-pushed the acroform-font-fix-1096 branch from 0d5d247 to ee50422 Compare September 5, 2026 10:17
@KaiPressmar KaiPressmar changed the title fix: give AcroForm a font readers can resolve field text against (#1096) fix: give AcroForm a font readers can resolve field text against Sep 5, 2026
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.
@KaiPressmar KaiPressmar changed the title fix: give AcroForm a font readers can resolve field text against fix: AcroForm custom fonts fall back to a substitute font in Acrobat Sep 5, 2026
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>
@blikblum

blikblum commented Sep 5, 2026

Copy link
Copy Markdown
Member

1- This behavior should be opt in. This increase the file size significantly
2- The handling should not be done in EmbeddedFont class, i.e., EmbeddedFont should not know about AcroForm. It should be the other way around
3- Investigate the possibility of using only the complete font for both AcroForm and TextStream, so the subset can be discarded ib this context

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Setting font-family for AcroFrom not working properly

2 participants