Skip to content

feat: add an embedFonts option for AcroForm fields - #1789

Merged
blikblum merged 1 commit into
foliojs:masterfrom
KaiPressmar:acroform-font-fix-1096
Sep 14, 2026
Merged

feat: add an embedFonts option for AcroForm fields#1789
blikblum merged 1 commit 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, 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() sets NeedAppearances, 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 /DR and /DA resources reused the font pdfkit embeds for content streams: a subsetted Type0 font under /Encoding /Identity-H whose program has no cmap table. 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 /DR and /DA:

doc.font('fonts/MyFont.ttf');
doc.initForm({ embedFonts: true });
  • Off by default. The copy holds the whole font, roughly +80 KB per font for Roboto. Without the option, output is byte-identical to master.
  • No AcroForm knowledge in EmbeddedFont. It offers completeRef(), a complete, text-addressable embedding of itself; acroform.js decides when a form needs one. Standard fonts have no completeRef() and keep using ref().
  • Composite font with a custom CMap. The copy is a Type0 font addressed through a WinAnsiEncoding-to-glyph CMap rather than a simple font with /Encoding /WinAnsiEncoding: fontkit's subset encoder emits no cmap or 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/.woff2 sources work, since their raw bytes are a compressed container rather than a font program.
  • Shared program embedding. embed() and embedComplete() now build the font program and descriptor through one embedProgram(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. /Length1 is set on its FontFile2, 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: true produces.)

image

Verification

  • yarn test passes. tests/unit/acroform.spec.js covers 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 lint and yarn prettier are clean.
  • Checked in Adobe Acrobat with TrueType and CFF-flavored fonts loaded from .ttf/.otf and .woff2.

Checklist:

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

Credit to @r4tz52 for the original report.

@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
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

KaiPressmar added a commit to KaiPressmar/pdfkit that referenced this pull request Sep 11, 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 and
foliojs#1790 are under review; rebuild
and recommit js/ if this branch is rebased onto a newer upstream master.
@KaiPressmar
KaiPressmar force-pushed the acroform-font-fix-1096 branch from 56f3d12 to 490720d Compare September 11, 2026 11:39
KaiPressmar added a commit to KaiPressmar/pdfkit that referenced this pull request Sep 11, 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 and
foliojs#1790 are under review; rebuild
and recommit js/ if this branch is rebased onto a newer upstream master.
@KaiPressmar

Copy link
Copy Markdown
Contributor Author

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 initForm({ embedCompleteFonts: true }), off by default. I checked that the default path is byte-identical to master (identical output apart from the random /ID), and as a result the object numbers the existing init standard fonts test pins reverted to master's.

2. Dependency direction. EmbeddedFont no longer mentions AcroForm anywhere. It offers completeRef() — "a complete, text-addressable embedding of me" — and acroform.js decides when a form needs one. While there I also deduplicated: embed() and embedComplete() each built the same font program, descriptor and flags, which is now one embedProgram(subset, complete).

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 this.subset in ascending order and fontkit's renumbering gives each glyph its original id, so content streams keep addressing by glyph id and both dictionaries can reference one program. But widths and unicode are indexed by that id and embed() writes both out positionally, so they then span the whole font rather than the glyphs actually drawn. The resulting /W array and /ToUnicode CMap cost more than the subset program they remove:

document no option shared program two programs
no page text 3,373 83,640 87,436
1 page of text 10,729 99,834 93,008
10 pages of text 34,247 123,326 116,392

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 initForm — after that the subset's renumbering is already fixed — so it would mean two code paths for a saving that is usually negative. The program/descriptor deduplication in point 2 is the part of that work I kept.

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.

@blikblum

Copy link
Copy Markdown
Member

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.
@KaiPressmar
KaiPressmar force-pushed the acroform-font-fix-1096 branch from 490720d to cd32a19 Compare September 12, 2026 21:53
@KaiPressmar KaiPressmar changed the title fix: AcroForm custom fonts fall back to a substitute font in Acrobat feat: add an embedFonts option for AcroForm fields Sep 12, 2026
@KaiPressmar

Copy link
Copy Markdown
Contributor Author

Thanks! Rebased onto master and renamed the option to embedFonts:

doc.initForm({ embedFonts: true });

I also updated the PR description, which still described the earlier always-on version.

KaiPressmar added a commit to KaiPressmar/pdfkit that referenced this pull request Sep 12, 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
upstream master.
@blikblum

Copy link
Copy Markdown
Member

Many thanks

@blikblum
blikblum merged commit d05b18f into foliojs:master Sep 14, 2026
3 checks passed
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