Repository navigation
Read, write and draw with a document's own fonts - #93
Merged
Merged
Conversation
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.
This was referenced Nov 1, 2026
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.
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_Loadlists 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) andEPDFDoc_GetFontsFingerprintsays when the faces changed.What a write would use.
EPDFDoc_ResolveFontsresolves 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_SetDefaultAppearanceDocumentFontputs 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 exactprogram(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.gnandBUILD.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 --strictandcheck-c-apipass. 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.