Skip to content

Read, write and draw with a document's own fonts - #93

Merged
bobsingor merged 17 commits into
embedpdf/mainfrom
embedpdf/feature/fonts
Nov 1, 2026
Merged

bobsingor merged 17 commits into
embedpdf/mainfrom
embedpdf/feature/fonts

Conversation

@bobsingor

Copy link
Copy Markdown
Contributor

Text can be written, and shown while it's typed, in the fonts a document already holds, and the engine reads a document's fonts the way a browser needs them. Everything a font decision rests on (which program a write uses, what it covers, what its licence allows) is read here; the TypeScript engine decides with it.

A document's fonts. EPDFFontFaces_Load lists every font face the document holds, from wherever a drawing can use one (page, form XObject, pattern, soft mask, Type 3 and appearance resources, the form's and fields' /DR), each with its programs: object number, format, embedded whole or as a subset, PostScript name, licence, Unicode cmap, SHA-256 and line metrics. A program's bytes, the code points it covers and, for a Type 1, bare CFF or cmap-less program, an OpenType display program a browser can load (tables realigned when a producer wrote them unpadded). On layers the walk is incremental (a commit or abort forgets only the places it touched) and EPDFDoc_GetFontsFingerprint says when the faces changed.

What a write would use. EPDFDoc_ResolveFonts resolves runs the way a write does, without writing: the font each run is drawn in, its fallbacks in order, and the characters no font has. A run can name the exact document program a write would (programs), so two programs that describe themselves alike are told apart. Characters an appearance draws with no font are collected per write.

Writing with them. EPDFAnnot_SetDefaultAppearanceDocumentFont puts a whole, editable document program in a box's /DA (fields, free text, redaction labels), and the appearance draws with that program instead of embedding another; a simple /DA font writes new text through its program's cmap. A rich text style may carry its exact program (never written to /RC or /DS, which name families): the body's becomes the /DA font, a run that restates the body's face keeps it, another face is found by its family. The registered-font licence hole in /DA is closed.

One text engine. Rich text is laid out with the Unicode bidi algorithm (ICU's ubidi, already in the build), fonts are matched per grapheme cluster in tiers with a last-resort font, and form fields and redaction labels are laid out with the same engine on CPVT's geometry. New field appearances are form XObjects.

Text extraction. /ActualText characters inside form XObjects are boxed in page space, and /ActualText holding marks or inner spaces reads as written.

Merge with main. One conflict, in CPDF_TextPage::CloseTempLine(): upstream's O(n) space collapse is kept and the /ActualText reading (BidiTextOf) sits on top of it. BUILD.gn and BUILD.gn.orig, committed by mistake while the build patches were applied, are restored and removed.

Checks. macOS: unit 1312 pass; embedder 1702 pass, and the 30 failures are the known macOS pixel ones (the merge fixed 6 more). check-fork-tags --strict and check-c-api pass. The Linux unit and embedder tests are running locally; this waits for them before merging.

Before a release. The upstream freshness gate fails on main itself: 39 upstream commits since S14, the oldest 4.8 days old. A release needs the S15 sync first, or an override reason.

Index every font face a document holds (form, page, form XObject, pattern,
soft mask, Type 3 and appearance resources), with each program's format,
embedding, permission, Unicode map, SHA-256 and line metrics, cached per
program stream. Resolve which font a write would draw each run in, without
writing. An annotation's default appearance can name a program the document
holds whole, and its appearance draws with that program. Report characters
drawn with no font. Read registered fonts' programs, coverage and fallback
order. New text no longer draws with a registered preview-and-print font
that was not authorized for editing.
A document font that is Type 1, bare CFF or a TrueType without a Unicode
cmap gets a display program: an OpenType font with the program's outlines
(read unscaled and unhinted, written as CFF charstrings), a Unicode cmap
holding exactly the characters the engine writes with the font through
the PDF's encoding, and its /Widths advances. Built once per program
stream, the same bytes every time. EPDFFontFaces_GetProgramDisplayHash
names what a browser loads for a program, EPDFDoc_GetFontDisplayProgramData
returns it, and coverage reports the display program's characters.
CFX_BidiParagraph resolves a paragraph's levels with ICU's ubidi, sets a
line's trailing whitespace to the paragraph's level and orders a line's
runs. The rich layout splits items by level and shapes them in its
direction, orders each line's runs, and takes a paragraph's direction from
its dir or, for form fields, from its first strong character. A line's
trailing spaces hang past its end, a right-to-left paragraph's indent is on
its right, and a justified line no longer stretches the spaces it ends with.
A cluster goes to the first fallback, in the order for its style, that has
all of it. Fallbacks are ranked by a tier the caller gives, then style,
then the order added (EPDFFont_AddFallbackFont takes the tier). A
registered last-resort font (EPDFFont_SetLastResortFont) draws a character
no font has as its box; it is embedded like a fallback and the character
is still reported. Registered and document fonts keep their coverage as
code point ranges.
ArrangeField lays a field's text out with CPVT's geometry (word breaks,
line metrics, fitted sizes, alignment, combs) over the shared engine's
clusters: a font per cluster with the last resort, shaping, bidi and whole
supplementary characters. The forms generator and redaction labels write
through it with one annotation font map, saved or on the fly. The map is no
longer a CPVT font map, and layout fonts carry no ToUnicode. A test holds
the layout to CPVT across the forms parity matrix.
ProcessMarkedContent() boxed and placed /ActualText characters with the
text object's rect and position, which are in the space of the form that
draws it. Text in a form drawn anywhere but the page's origin, such as a
flattened form field, was selected in the wrong place. The rect is now
split in the object's space and each character's slice, and its origin,
taken through the form's matrix.
CloseTempLine() cut a line's bidi segments at every mark and space, also
inside an /ActualText span, and gave a right-to-left line's segments in
visual order, each kept forward: pointed Hebrew and Arabic with harakat
came out scrambled, and a right-to-left span of two words had them
swapped. Inside one span, a mark now takes the direction of the character
before it and characters between two of the same direction take theirs
(UAX #9 W1 and N1), so the span is one segment; its leading and trailing
neutrals keep their own.
Add shared sfnt writing and alignment helpers. When an embedded Unicode-mapped font has misaligned tables, preserve its original program while providing browsers with a realigned display program and hash.
A /DA font over a program that can author text (an sfnt with a Unicode cmap whose licence allows editing) lays new text out with that program's own cmap, as composite /DA fonts already did and as Acrobat does: a simple font's 8-bit encoding no longer sends characters its program has (Hebrew in a Mac Roman Adobe Hebrew) to a fallback.
Add an API to fingerprint document fonts and cache per-place font walks for layered documents. Invalidate affected walks when transactions end, and cover incremental updates, aborts, and plain documents with tests.
A style's program (its stream's object number) joins the face request, so the rich text writer and EPDFDoc_ResolveFonts draw with that program even when another describes itself alike. The body's program becomes the /DA font; a run that restates the body's face keeps it, another face is found by its family.
BUILD.gn and BUILD.gn.orig were committed while shared-library.patch was applied: restore BUILD.gn as the base has it and drop the patch backup.
A display program depends on how a font dictionary reads its program (encoding, /Widths, flags), so two dictionaries sharing one Type 1 or bare CFF stream got one hash and the first one's advances. Display programs are now kept per stream and dictionary, by SHA-256, and EPDFDoc_GetFontDisplayProgramData and EPDFDoc_GetFontDisplayProgramCoverage (was EPDFDoc_GetFontProgramCoverage) take the display hash instead of the stream's object number.
bobsingor added a commit to embedpdf/embed-pdf-viewer that referenced this pull request Nov 1, 2026
A document font is everything the PDF draws under one name: copies of one font that editors described differently are one entry, and two fonts described alike keep their own names. Free text in a document font draws in that font's own program, where it fell back to Helvetica. Display programs are served by hash, so one program two font dictionaries read differently is two programs with their own widths. Needs the runtime from embedpdf/runtime#93.
EPDFFontResolution_GetProgramDisplayHash gives a resolved run's program by its display hash, as the run's own font dictionary reads it. The object number can't tell apart two dictionaries that read one stream differently, so two fields over a shared CFF program both previewed with one dictionary's widths.
Document the font code's upstream touchpoints, test coverage, and CPVT line-breaking dependency in the fork ledger. Clarify the coordinate space used for synthetic ActualText and distinguish glyph outlines from generated OpenType fonts.
@bobsingor
bobsingor merged commit dae847b into embedpdf/main Nov 1, 2026
1 check passed
@bobsingor
bobsingor deleted the embedpdf/feature/fonts branch November 1, 2026 13:49
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.

1 participant