pdf-codec 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -175,11 +175,11 @@ The package is layered from generic primitives outward to the codec itself:
175
175
  - **`src/util/`** — two small, independently-duplicated copies of logic that lives elsewhere in the family for a reason narrow enough not to warrant a shared dependency: `base64.ts` (isomorphic base64 ⇄ `Uint8Array`, a verbatim copy of `odf.js`'s own `src/util/base64.ts`, replacing a dependency this codec used to have on `ooxml.js` purely for this one helper pair) and `abort.ts` (`throwIfAborted`, a four-line signal-check helper called at every page loop boundary in `write.ts`/`read.ts` — there is no `await` point in this package's synchronous reader/writer pipeline for cancellation to hook into implicitly, so every long-running loop checks explicitly instead; a duplicate of `documents.js`'s own `src/ports/abort.ts`, which stays there since other, non-PDF consumers still depend on it in that repository).
176
176
  - **`src/crypto/`** — MD5, SHA-256/384/512, RC4, and AES-CBC, hand-written with zero local imports, exactly like `src/bytes/`. Not a preference: ISO 32000-1's own key-derivation algorithms name MD5 and RC4 directly, neither of which any platform crypto API this package could portably reach still offers, and `crypto.subtle` is asynchronous where this codec's read path is synchronous end to end. Reaching for `node:crypto` would put a Node builtin inside a `src/` tree that deliberately has none and is built with `platform: 'neutral'`, breaking the browser bundle its downstream consumer depends on. Each module cites the specification it implements (RFC 1321, FIPS 180-4, FIPS 197) and is tested against that specification's own published conformance vectors. MD5 and RC4 are cryptographically broken and are here solely to *read* files that already exist and whose format mandates them.
177
177
  - **The codec itself, importing only `math-types`/`formula`/`bytes`/`image`/`crypto` (no OOXML or ODF knowledge at all):**
178
- - **Write**: `objects.ts` (the `PdfObject` discriminated union), `afm-widths.ts`/`encoding.ts`/`winansi.ts`/`fonts.ts` (standard-14 metrics, WinAnsi encoding, family resolution), `font-registry.ts` (the source-document → caller-supplied → vendored-substitute → standard-14 resolution port sitting in front of `resolveStandardFont`, plus `resolveFaceWithRegistry`, the one step both the measurer and the writer resolve through so they can never disagree about which face a `LayoutFont` means), `measure.ts`/`text-layout.ts` (greedy line-wrapping, measured against either a standard-14 face's AFM widths plus a per-family correction or a resolved face's own real `hmtx` advances — never both, see [Fidelity](#fidelity)), `matrix.ts`, `content-write.ts` (`LayoutItem[]` → content-stream operators, with text branching on whether its font resolved to a standard-14 face shown as a WinAnsi byte string or an embedded one shown as Identity-H 2-byte CIDs), `write.ts` (the full object graph, classic cross-reference table, trailer, and — when `WritePdfOptions.formulas` is non-empty — one embedded math composite font group, plus one embedded text font group per face `WritePdfOptions.fonts` resolved, each allocated once in a fixed sorted order and shared across pages).
179
- - **sfnt font tables**: `sfnt.ts` (a bounds-checked sfnt table-directory reader, big-endian primitive readers, and the `hasBytes` range check every table parser below pre-checks with), `cmap-table.ts` (Unicode → glyph ID, formats 4/12/6), `hmtx-table.ts` (per-glyph advance widths), `font-tables.ts` (`head`/`maxp`/`OS/2`/`post`/`name` — design grid, glyph count, vertical metrics and style bits, italic angle and underline geometry, PostScript and family names), `glyf.ts` (the `loca` offset index, per-glyph headers, a composite glyph's own component records — a composite refers to its base letter and combining marks by glyph ID, and those references nest, so subsetting one safely means taking the transitive closure over this walk — and `glyphInkBounds`, a glyph's own tight ink box, read straight out of a simple glyph's header where the format already states it and unioned from a composite's own transformed, placed components where it does not), and `math-table.ts` (the OpenType `MATH` table's constants, glyph-info, and variants subtables). Every one of these parsers degrades to `undefined` on a missing or truncated table rather than throwing: the vendored fonts are trusted, but a font extracted from an arbitrary source document is not.
180
- - **sfnt subsetting**: `sfnt-subset.ts` (a TrueType-outline glyph subsetter: Unicode code points → glyph IDs through `cmap-table.ts`, the transitive closure over `glyf.ts`'s composite-component walk, then a rebuilt sfnt container carrying only those glyphs' outlines). **Glyph IDs are preserved, never renumbered** — an unused ID below the highest used one survives as an empty `loca` entry rather than being squeezed out — which keeps every composite's own component references correct inside bytes copied verbatim, keeps a caller's already-resolved glyph IDs valid against the subset, and makes CID == GID trivially true for the embedded program (a `/CIDFontType2` then needs only `/CIDToGIDMap /Identity`). The output rebuilds `head`/`hhea`/`maxp`/`loca`/`glyf`/`hmtx` (with `indexToLocFormat` forced long, one always-legal code path), copies the hinting programs (`cvt `/`fpgm`/`prep`) verbatim, stubs `post` as a version 3.0 "no glyph names" header, and omits `cmap`/`name`/`OS/2`/`GSUB`/`GPOS`/`kern` — none of which an embedded `CIDFontType2` program is read through (ISO 32000-1 9.9). The honest cost of preserving IDs: `loca` and `hmtx` stay proportional to the highest used glyph ID rather than to the number of glyphs kept, so for a document touching one glyph near the end of a large font's glyph order, most of the (already much smaller) output is those two index tables rather than outline data. Applies to `glyf`-flavoured fonts only; a CFF-flavoured one returns `undefined`, the same scope boundary [Fidelity](#fidelity) states for the embedded math font.
178
+ - **Write**: `objects.ts` (the `PdfObject` discriminated union), `afm-widths.ts`/`encoding.ts`/`winansi.ts`/`fonts.ts` (standard-14 metrics, WinAnsi encoding, family resolution), `font-registry.ts` (the source-document → caller-supplied → vendored-substitute → standard-14 resolution port sitting in front of `resolveStandardFont`, plus `resolveFaceWithRegistry`, the one step both the measurer and the writer resolve through so they can never disagree about which face a `LayoutFont` means), `measure.ts`/`text-layout.ts` (greedy line-wrapping, measured against either a standard-14 face's AFM widths plus a per-family correction or a resolved face's own real `hmtx` advances — never both, see [Fidelity](#fidelity)), `matrix.ts`, `content-write.ts` (`LayoutItem[]` → content-stream operators, with text branching on whether its font resolved to a standard-14 face shown as a WinAnsi byte string with `Tj` or an embedded one shown as Identity-H 2-byte CIDs, split at that face's own pair-kerning adjustments into a `TJ` array where it has any to apply and shown as a single unsplit `Tj` string where it does not), `write.ts` (the full object graph, classic cross-reference table, trailer, and — when `WritePdfOptions.formulas` is non-empty — one embedded math composite font group, plus one embedded text font group per face `WritePdfOptions.fonts` resolved, each allocated once in a fixed sorted order and shared across pages).
179
+ - **sfnt font tables**: `sfnt.ts` (a bounds-checked sfnt table-directory reader, big-endian primitive readers, and the `hasBytes` range check every table parser below pre-checks with), `cmap-table.ts` (Unicode → glyph ID, formats 4/12/6), `hmtx-table.ts` (per-glyph advance widths), `font-tables.ts` (`head`/`maxp`/`OS/2`/`post`/`name` — design grid, glyph count, vertical metrics and style bits, italic angle and underline geometry, PostScript and family names), `glyf.ts` (the `loca` offset index, per-glyph headers, a composite glyph's own component records — a composite refers to its base letter and combining marks by glyph ID, and those references nest, so subsetting one safely means taking the transitive closure over this walk — and `glyphInkBounds`, a glyph's own tight ink box, read straight out of a simple glyph's header where the format already states it and unioned from a composite's own transformed, placed components where it does not), and `math-table.ts` (the OpenType `MATH` table's constants, glyph-info, and variants subtables). `ot-layout-common.ts` holds the two Common Table Formats every OpenType Layout table indexes glyphs through — Coverage and ClassDef — stored as sorted glyph ranges and searched by bisection rather than expanded into a glyph-keyed map, since six bytes of a format 2 record can legitimately declare a 65536-glyph range and expanding every range of every subtable turns a small untrusted font into a large allocation. `gpos-table.ts` reads a face's own `GPOS` table for exactly one thing: how much the font wants the advance of glyph A adjusted when glyph B follows it. It resolves the `kern` feature through the ScriptList (rather than sweeping the FeatureList for every feature tagged `kern`, which is what would apply a font's Cyrillic or Greek kerning lookups to Latin text), handles both PairPos subtable formats and the LookupType 9 Extension indirection — all three are real code paths, since Carlito reaches its kerning only through Extension-wrapped format 2 subtables while Caladea uses LookupType 2 directly and mixes formats inside one lookup — and reads only the first glyph's XAdvance, the one field a horizontal left-to-right run's next glyph position can depend on. Mark attachment, cursive joining, and contextual positioning have no consumer in a codec that positions glyphs itself, so they are not parsed. Every one of these parsers degrades to `undefined` on a missing or truncated table rather than throwing: the vendored fonts are trusted, but a font extracted from an arbitrary source document is not.
180
+ - **sfnt subsetting**: `sfnt-subset.ts` (a TrueType-outline glyph subsetter: Unicode code points → glyph IDs through `cmap-table.ts`, the transitive closure over `glyf.ts`'s composite-component walk, then a rebuilt sfnt container carrying only those glyphs' outlines). **Glyph IDs are preserved, never renumbered** — an unused ID below the highest used one survives as an empty `loca` entry rather than being squeezed out — which keeps every composite's own component references correct inside bytes copied verbatim, keeps a caller's already-resolved glyph IDs valid against the subset, and makes CID == GID trivially true for the embedded program (a `/CIDFontType2` then needs only `/CIDToGIDMap /Identity`). The output rebuilds `head`/`hhea`/`maxp`/`loca`/`glyf`/`hmtx` (with `indexToLocFormat` forced long, one always-legal code path), copies the hinting programs (`cvt `/`fpgm`/`prep`) verbatim, stubs `post` as a version 3.0 "no glyph names" header, and omits `cmap`/`name`/`OS/2`/`GSUB`/`GPOS`/`kern` — none of which an embedded `CIDFontType2` program is read through (ISO 32000-1 9.9). Dropping `GPOS` costs the document no kerning even though its pairs are now genuinely applied: the adjustments are resolved at write time and written into the page's own `TJ` array, so a consumer reads them off the content stream rather than out of the font program. The honest cost of preserving IDs: `loca` and `hmtx` stay proportional to the highest used glyph ID rather than to the number of glyphs kept, so for a document touching one glyph near the end of a large font's glyph order, most of the (already much smaller) output is those two index tables rather than outline data. Applies to `glyf`-flavoured fonts only; a CFF-flavoured one returns `undefined`, the same scope boundary [Fidelity](#fidelity) states for the embedded math font.
181
181
  - **Embedded math font**: `math-font.ts` (parses and caches the vendored STIX Two Math font once per process, exposing a size-specific `MathFontMetrics` implementation and a points-in/points-out stretchy-glyph entry point), `math-stretch.ts` (the OpenType MATH two-stage stretching model over that parsed `MathVariants` data: pick the smallest pre-built variant that reaches the target, else assemble from repeated parts with every seam overlapped inside both sides' own declared connector lengths -- unit-agnostic, so it works in design units or points alike), `math-font-write.ts` (builds the `/Type0`/`/CIDFontType0`/`/FontDescriptor`/`/FontFile3`/ToUnicode object group), `math-content-write.ts` (a `PositionedFormula[]` → PDF content-stream bytes, Identity-H 2-byte CIDs for text-showing, `re`/`m`/`l` operators for rules and the radical hook, and one glyph-ID-addressed text object per placement of a stretched construction, wrapped in an `/ActualText` marked-content span). See [Fidelity](#fidelity) for the CFF-full-embed (not glyph-subsetted) simplification this makes.
182
- - **Embedded text faces**: `embedded-font.ts` (parses and caches one TrueType-outline face's `cmap`/`hmtx`/`hhea`/`head`/`OS/2`/`post`/`name` metrics, and `encodeForShowEmbedded`, the single code path both measurement and text-showing go through — the same reason `winansi.ts`'s `encodeForShow` exists, since encoding and measuring separately lets the two disagree about which characters resolved to which glyph and silently desyncs a computed wrap point from the drawn line). Every geometry field it exposes is converted into PDF's 1000-units-per-em glyph space (ISO 32000-1 9.8.1), which a font's own design grid frequently is not: STIX Two Math is drawn on a 1000-unit em so that factor is an identity for the math font, but Carlito is drawn on a 2048-unit em, where getting the conversion wrong is silent rather than loud — the font simply renders with roughly twice its intended metrics and nothing anywhere reports an error. The serif flag a `/FontDescriptor` needs is read off the face's own PANOSE classification rather than guessed from its family name. `embedded-font-write.ts` builds the `/Type0`/`/CIDFontType2`/`/FontDescriptor`/`/FontFile2`/ToUnicode object group, with `/CIDToGIDMap /Identity` written explicitly (stating outright the CID == GID invariant `sfnt-subset.ts`'s GID-preserving design guarantees) and `/Length1` set to the **uncompressed** subset length — the single most commonly mis-set key in TrueType embedding, since the obvious-looking value, the stream's own `/Length`, is silently accepted by lenient readers and rejected by strict ones. Its subset tag (`ABCDEF+Carlito-Regular`, six uppercase letters per 9.6.4) is a CRC32 over the face's PostScript name and its exact ascending glyph-ID list, so identical input yields byte-identical output and two subsets of one face carrying different glyphs can never be mistaken for one another.
182
+ - **Embedded text faces**: `embedded-font.ts` (parses and caches one TrueType-outline face's `cmap`/`hmtx`/`hhea`/`head`/`OS/2`/`post`/`name` metrics and its own `GPOS` pair kerning, and `encodeForShowEmbedded`, the single code path both measurement and text-showing go through — the same reason `winansi.ts`'s `encodeForShow` exists, since encoding and measuring separately lets the two disagree about which characters resolved to which glyph and silently desyncs a computed wrap point from the drawn line). Pair kerning is applied inside that same one path, for that same one reason: one pass over one glyph sequence produces both the run's width and the list of adjustments `content-write.ts` positions its glyphs by, so a measurement that included kerning while the page drew unkerned glyphs (or the reverse) is not a state this can reach. Kerning is looked up between the glyphs that will actually be shown, `.notdef` included, matching the same "measure what will be drawn, not what was asked for" rule the missing-glyph handling already follows. Every geometry field it exposes is converted into PDF's 1000-units-per-em glyph space (ISO 32000-1 9.8.1), which a font's own design grid frequently is not: STIX Two Math is drawn on a 1000-unit em so that factor is an identity for the math font, but Carlito is drawn on a 2048-unit em, where getting the conversion wrong is silent rather than loud — the font simply renders with roughly twice its intended metrics and nothing anywhere reports an error. The serif flag a `/FontDescriptor` needs is read off the face's own PANOSE classification rather than guessed from its family name. `embedded-font-write.ts` builds the `/Type0`/`/CIDFontType2`/`/FontDescriptor`/`/FontFile2`/ToUnicode object group, with `/CIDToGIDMap /Identity` written explicitly (stating outright the CID == GID invariant `sfnt-subset.ts`'s GID-preserving design guarantees) and `/Length1` set to the **uncompressed** subset length — the single most commonly mis-set key in TrueType embedding, since the obvious-looking value, the stream's own `/Length`, is silently accepted by lenient readers and rejected by strict ones. Its subset tag (`ABCDEF+Carlito-Regular`, six uppercase letters per 9.6.4) is a CRC32 over the face's PostScript name and its exact ascending glyph-ID list, so identical input yields byte-identical output and two subsets of one face carrying different glyphs can never be mistaken for one another.
183
183
  - **ToUnicode CMaps**: `tounicode.ts`, shared by both embedded-font writers above rather than duplicated in each — a character code → Unicode code point mapping written as a bfchar CMap (9.10.3), with supplementary-plane code points encoded as genuine UTF-16BE surrogate pairs and entries emitted in blocks of at most 100, the limit the CMap syntax sets and one a subsetted text face routinely exceeds. A glyph with no code point to map back to (a stretchy construction's own unencoded pieces) is dropped from the CMap rather than mapped to a stand-in that would extract as the wrong character; the `/ActualText` span around such a construction carries its real text instead.
184
184
  - **CFF reading**: `cff.ts` holds the two container structures every CFF program is built out of — the `INDEX` and the `DICT` — shared by the two readers built on it rather than hand-rolled twice. `cff-bounds.ts` is a Type 2 charstring interpreter that computes each glyph's own tight ink bounding box: a CFF glyph, unlike a TrueType one, stores no bounding box anywhere, so the only way to know what area it covers is to run its outline program. It is a path *walker*, not a rasteriser — it tracks the current point through every path-construction operator and solves each cubic's real extrema from the roots of its own derivative, so a bound it reports is genuinely tight rather than a control-point hull. Hint operators are decoded only far enough to know how many bytes a following `hintmask` consumes. Verified against the vendored STIX Two Math font's whole 5,543-glyph repertoire: every glyph matches fontTools' own `BoundsPen` to within 0.01 design units, and the union of all 5,543 computed boxes lands exactly on the font's own `head` table `FontBBox`. Out of scope, each reported as `undefined` rather than guessed at: a CID-keyed program (its local subroutines live per-FD behind `FDArray`/`FDSelect`), `endchar` in its four-argument seac-like form (which needs the charset and Standard Encoding to resolve into two other glyphs), and the arithmetic/storage/conditional escaped operators. `cff-probe.ts` reads a bare CFF program's header, Name INDEX, and just enough of its Top DICT to detect the `ROS` operator (the escaped `12 30` whose presence *is* the definition of a CID-keyed font, CFF 1.0 Appendix H). It exists to make the embedding path **refuse** such a font rather than mis-embed it: a CID-keyed CFF carries its own charset mapping CIDs onto glyph indices, so CID == GID does not hold, and showing text through Identity-H against one anyway produces no error anywhere — the file is structurally valid, every reader accepts it, and the page simply renders the wrong glyphs. Not wired into any write path yet; it is the guard a later source-embedded-font phase needs. It correctly reports the vendored STIX Two Math font's own `CFF ` table as *not* CID-keyed, which is what makes `math-font-write.ts`'s existing embedding sound.
185
185
  - **Read**: `lexer.ts`/`parse.ts` (byte tokenizer and tokens → `PdfObject`), `filters.ts`/`predictors.ts` (Flate/LZW/ASCII85/ASCIIHex/RunLength/CCITTFax, TIFF/PNG predictors), `xref.ts`/`document.ts` (classic and cross-reference-stream resolution, object streams, `/Prev` chains, linear-scan recovery, the page tree with attribute inheritance), `encrypt.ts` (the standard security handler: `/Encrypt` parsing, empty-user-password key derivation and `/U` verification, per-object keys, and the string/stream decryption `document.ts` applies transparently as each indirect object is fetched — see [Gotchas](#gotchas-and-quirks)), `content-read.ts`/`interpret.ts` (the content-stream tokenizer and graphics/text state machine, including form-XObject recursion and general vector-path tracking — see [Gotchas](#gotchas-and-quirks)), `cmap.ts`/`font-style.ts`/`font-read.ts` (`/ToUnicode` CMaps, font-dictionary resolution), `images-read.ts` (Image XObjects → PNG/JPEG bytes), `read.ts` (`readPdf`, assembling all of the above into a `LayoutDocument`).
@@ -216,7 +216,9 @@ Dependency direction is strictly downward and checkable: `math-types`/`formula`/
216
216
  - **An ink box is genuinely tight, which for a math font means it is often *larger* than the nominal metrics, not smaller.** The "ink is a fraction of the nominal extent" intuition holds for text-like glyphs (a full stop, a parenthesis, an `x`) and fails for the extension pieces, display-size operators, and pre-built large variants a math font is full of: over a tenth of STIX Two Math's repertoire draws above its own nominal ascent, and another tenth below its nominal descent, reaching 2.6 em up and 1.6 em down at the extremes. Sizing those from `ascentPerEm` under-reports them exactly as badly as it over-reports a full stop, which is the whole reason the per-glyph measurement exists.
217
217
  - **A glyph's ink descent is negative where its lowest ink sits above the baseline.** `inkDescentPt` follows `descentPerEm`'s own sign convention (ink below the baseline is a positive descent), so a superscript-height glyph honestly reports a negative descent rather than a clamped zero. A consumer that needs a box which never crosses the baseline clamps at its own layer, where it can see what the box is for.
218
218
  - **An embedded `CIDFontType2` program needs no `cmap` table of its own, and `sfnt-subset.ts`'s output doesn't carry one — this is a property of the spec, not an oversight this package works around.** `cmap` maps a character code to a glyph ID for a *simple* font; a `Type0` composite font never asks the embedded font program to do that lookup at all. Character code → CID goes through the `Type0` font's own `/Encoding` (Identity-H here, so CID == character code by construction for the 2-byte codes this package writes), and CID → GID goes through `/CIDToGIDMap` (`/Identity` here, matching `sfnt-subset.ts`'s own GID-preserving design). Both steps happen inside the PDF's own object graph, entirely before the embedded font program is ever consulted — ISO 32000-1 9.7.4.2. This is exactly why the subset output can safely omit `cmap` alongside `name`/`OS/2`/`GSUB`/`GPOS`/`kern` (see Architecture above): none of the five is on the code-path a `CIDFontType2` reader actually walks.
219
- - **No GPOS/GSUB/kern support: an embedded face's own kerning pairs, ligature substitutions, and other OpenType layout features are never read, subsetted, or applied.** `sfnt-subset.ts` strips `GSUB`/`GPOS`/`kern` from its output entirely, and nothing upstream of that `measure.ts`'s line-wrapping, `content-write.ts`'s glyph placement ever consults them either, subset or not. Every embedded run is placed glyph-by-glyph at that glyph's own bare `hmtx` advance width, with no per-pair kerning adjustment. See the acceptance-bar paragraph under [Fidelity](#fidelity) for what this bounds and doesn't bound.
219
+ - **`GPOS` pair kerning is read and applied for an embedded face; no other OpenType layout feature is.** An embedded run's glyphs are placed at their own `hmtx` advances adjusted by the font's own pair kerning, written into the page as a real `TJ` array (see the gotcha below). `GSUB` is a different matter and is genuinely not supported: ligature substitution, contextual alternates, and small caps are never applied, so a face's `fi` ligature is drawn as two separate glyphs. The legacy `kern` table is not read either, and nothing is lost by that for the vendored families neither Carlito nor Caladea ships one in any face; both carry all of their real pair kerning in `GPOS`.
220
+ - **A kerned run is shown with `TJ`, and the sign of a `TJ` number is the opposite of the adjustment it expresses.** ISO 32000-1 9.4.3 defines a number in a `TJ` array as being SUBTRACTED from the current horizontal coordinate, in thousandths of a unit of text space — so a positive number moves the next glyph closer, and a pair the font tightens by 43.457 glyph-space units is written as `+43.457`, not `-43.457`. `content-write.ts` negates the advance delta at exactly that one point, and `interpret.ts`'s own `TJ` handling is the reader half of the same convention, so a page written here and read back through this package's own parser recovers the positions it was written with (`write-embedded-font.test.ts` asserts that round trip specifically, since it is what settles the direction empirically rather than by argument from the specification alone). A run whose adjacent pairs the face kerns nothing about is still shown as one unsplit hex string with `Tj`, byte for byte what this package emitted before kerning existed — only a genuinely kerned run pays for an array.
221
+ - **Kerning applies to whole shown strings, so a wrap decision does not see a pair straddling the boundary between two separately-measured words.** `text-layout.ts` decides where a line breaks by summing separately-measured word and whitespace atoms, and a pair spanning one of those boundaries (Caladea kerns a comma or a full stop against a following space by -30 design units; Carlito kerns nothing against a space at all) is therefore not counted at that one decision point. Everything downstream of the decision is exact: a fragment's reported width and the glyphs actually drawn for it both come from measuring that whole fragment in one call, so the width a line reports is the width the page draws. Making the wrap decision itself exact would mean widening the `TextMeasurer` port with a cross-string pair-adjustment method — a public API change for a sub-point difference that, for both vendored families, only ever errs towards breaking a line early rather than overrunning a column.
220
222
  - **`font-substitutes.ts` maps both `Calibri` and `Calibri Light` onto the same, ordinary-weight Carlito face.** Carlito ships only one weight per style axis (regular/bold/italic/bolditalic) — there is no distinct Light design to embed — so `Calibri Light` substitutes to standard Carlito rather than a genuinely lighter face. An honest, documented approximation (see that file's own top-of-file comment), not a faithful weight match: a caller relying on Calibri Light's visibly thinner strokes will not see them, only its width metrics.
221
223
  - **`cff-probe.ts`'s CID-keyed CFF guard exists for a source-embedded-font phase this package hasn't built yet — it is not wired into any write path today, and today's embedding never needs it.** Every face this package currently embeds — the vendored Carlito/Caladea substitutes, and any caller-supplied face via `sourceFonts`/`fonts` — is `glyf`-flavoured TrueType, and `sfnt-subset.ts` already refuses (returns `undefined` for) anything that isn't before `cff-probe.ts` would ever run against it. The guard is what a future phase embedding a real, subsetted CFF program (rather than the whole-table CFF embed `math-font-write.ts` already does for STIX Two Math) will need before it can trust CID == GID against an arbitrary caller-supplied font: a CID-keyed CFF carries its own CID → glyph-index charset, so that identity does not hold for one, and nothing about the file signals the mismatch to a reader — it just renders the wrong glyphs.
222
224
 
@@ -252,7 +254,7 @@ Dependency direction is strictly downward and checkable: `math-types`/`formula`/
252
254
 
253
255
  **Supply a `FontRegistry` (`createFontRegistry()`) and Calibri and Cambria stop being an approximation at all.** The vendored Carlito and Caladea faces are the real, metric-compatible TrueType families those two names substitute for, resolved automatically with no caller configuration needed. Aptos still has no vendored substitute — it falls back to the same width-corrected standard-14 approximation as any other unlisted family, registry or not. The mechanism: a resolved face is measured at its own real `hmtx` advances (never the width-correction table, which is not consulted at all on that path — applying both would silently draw text narrower than it was measured and overrun its column), underlined at its own `post` geometry, subsetted to the glyphs the document actually uses, and embedded as a real `/Type0` + `/CIDFontType2` + `/FontFile2` group. A caller's own `fonts`/`sourceFonts` extend the same resolution to any TrueType-outline face. The honest remaining limits: only TrueType (`glyf`) outlines can be embedded, since `sfnt-subset.ts` rebuilds outline tables rather than re-encoding CFF charstrings (a resolved face with no `glyf` throws rather than silently falling back to a different font than the caller asked for); a character the face has no glyph for is drawn as `.notdef` and reported through `onMissingGlyph` rather than substituted; and which of a font's three competing vertical-metric sets drives line height is a caller-chosen policy, not a settled fact (see `VerticalMetricPolicy` above).
254
256
 
255
- **The acceptance bar for embedded-font fidelity is deliberately "no page-count drift on a real corpus", not "line-identical".** Every glyph in an embedded run is placed at its own bare `hmtx` advance width and nothing elsethere is no pair-kerning adjustment anywhere in this package's measurement or content-stream writing path (see [Gotchas](#gotchas-and-quirks)). Reaching genuine line-identical parity with the original authoring application would additionally need a GPOS pair-kerning reader on top of the outline subsetter this package already has, a materially larger, separate undertaking this phase deliberately did not attempt. What the current bar does guarantee: a real document measured and drawn through the same resolved face's own advances will not silently reflow onto a different number of pages the way a width-corrected standard-14 substitute occasionally can.
257
+ **The acceptance bar for embedded-font fidelity is deliberately "no page-count drift on a real corpus", not "line-identical".** An embedded run is placed at the face's own `hmtx` advances adjusted by the face's own `GPOS` pair kerning measured and drawn from one shared computation, so the two cannot disagree which is a genuine step towards what the original authoring application would produce rather than an approximation of it. What still separates this from line-identical output is everything else an OpenType shaping engine does: `GSUB` ligatures and contextual alternates are never applied, and kerning is applied within each shown string rather than across the whitespace boundaries a wrap decision measures separately (see [Gotchas](#gotchas-and-quirks) for both). What the bar does guarantee: a real document measured and drawn through the same resolved face's own advances will not silently reflow onto a different number of pages the way a width-corrected standard-14 substitute occasionally can.
256
258
 
257
259
  **The one exception is math-formula rendering (`WritePdfOptions.formulas`): this genuinely embeds a real, hand-parsed font.** Real box-model glyph runs are shown through the embedded STIX Two Math font with genuine per-glyph metrics (advance width, italic correction, top-accent attachment) and font-wide layout constants (axis height, fraction/radical rule thickness and gaps, script shift amounts) parsed directly from that font's own `MATH` table — not approximated or hand-tuned. Stretchy constructions are real too: a `MathVariants` variant or part assembly is resolved, measured against actual glyph outlines, and drawn by glyph ID. See [Gotchas](#gotchas-and-quirks) for the exact boundary of what this package's own font parsing does and doesn't cover (the CFF-full-embed simplification, and what a stretched construction costs in ToUnicode terms).
258
260
 
@@ -33,27 +33,48 @@ function writeUnderline(writer, item, widthPt, underline) {
33
33
  writer.writeAscii("f\n");
34
34
  writer.writeAscii("Q\n");
35
35
  }
36
- function writeShowTextBlock(writer, item, resourceName, scalePercent, codes) {
36
+ function writeShowTextBlock(writer, item, resourceName, scalePercent, show) {
37
37
  writer.writeAscii("BT\n");
38
38
  writer.writeAscii(`/${resourceName} ${require_serialize.formatNumber(item.sizePt)} Tf\n`);
39
39
  writer.writeAscii(`${require_serialize.formatNumber(scalePercent)} Tz\n`);
40
40
  writeRgbOperator(writer, item.color, "rg");
41
41
  writeMatrixOperator(writer, anchorMatrix(item.xPt, item.yPt, item.rotationDeg), "Tm");
42
- require_serialize.writeObject(writer, require_objects.pdfHexString(codes));
43
- writer.writeAscii(" Tj\n");
42
+ require_serialize.writeObject(writer, show.operand);
43
+ writer.writeAscii(` ${show.operator}\n`);
44
44
  writer.writeAscii("ET\n");
45
45
  }
46
46
  function writeStandardText(writer, item, standardName, resourceName, measurer, substitutions) {
47
47
  const encoded = require_afm_widths.encodeForShow(item.text, standardName);
48
48
  substitutions.push(...encoded.substitutions);
49
49
  const scale = measurer.horizontalScaleFor(item.font);
50
- writeShowTextBlock(writer, item, resourceName, scale * 100, encoded.codes);
50
+ writeShowTextBlock(writer, item, resourceName, scale * 100, {
51
+ operand: require_objects.pdfHexString(encoded.codes),
52
+ operator: "Tj"
53
+ });
51
54
  if (item.underline === true) writeUnderline(writer, item, item.widthPt ?? encoded.width1000 / GLYPH_SPACE_UNITS_PER_EM * item.sizePt * scale, measurer.underlineAtSize(item.font, item.sizePt));
52
55
  }
56
+ function embeddedShowOperand(encoded) {
57
+ if (encoded.kerns.length === 0) return {
58
+ operand: require_objects.pdfHexString(encoded.codes),
59
+ operator: "Tj"
60
+ };
61
+ const items = [];
62
+ let runStart = 0;
63
+ for (const kern of encoded.kerns) {
64
+ items.push(require_objects.pdfHexString(encoded.codes.subarray(runStart, kern.codeOffset)));
65
+ items.push(require_objects.pdfNum(-kern.adjustment1000));
66
+ runStart = kern.codeOffset;
67
+ }
68
+ items.push(require_objects.pdfHexString(encoded.codes.subarray(runStart)));
69
+ return {
70
+ operand: require_objects.pdfArray(items),
71
+ operator: "TJ"
72
+ };
73
+ }
53
74
  function writeEmbeddedText(writer, item, face, resourceName, missingGlyphs) {
54
75
  const encoded = require_embedded_font.encodeForShowEmbedded(item.text, face);
55
76
  missingGlyphs.push(...encoded.substitutions);
56
- writeShowTextBlock(writer, item, resourceName, EMBEDDED_HORIZONTAL_SCALE_PERCENT, encoded.codes);
77
+ writeShowTextBlock(writer, item, resourceName, EMBEDDED_HORIZONTAL_SCALE_PERCENT, embeddedShowOperand(encoded));
57
78
  if (item.underline === true) writeUnderline(writer, item, item.widthPt ?? encoded.width1000 / GLYPH_SPACE_UNITS_PER_EM * item.sizePt, {
58
79
  offsetPt: face.metrics.underlinePositionGlyphSpace / GLYPH_SPACE_UNITS_PER_EM * item.sizePt,
59
80
  thicknessPt: face.metrics.underlineThicknessGlyphSpace / GLYPH_SPACE_UNITS_PER_EM * item.sizePt
@@ -1,5 +1,5 @@
1
1
  import { r as StandardFontName } from "./afm-widths-DyDq56Ph.cjs";
2
- import { r as EmbeddedFaceSubstitution, t as EmbeddedFace } from "./embedded-font-CLedn-mT.cjs";
2
+ import { r as EmbeddedFaceSubstitution, t as EmbeddedFace } from "./embedded-font-BU6Jgcof.cjs";
3
3
  import { TextMeasurer } from "./measure.cjs";
4
4
  import { WinAnsiSubstitution } from "./winansi.cjs";
5
5
  import { LayoutFont, LayoutItem } from "document-schema.js";
@@ -1,5 +1,5 @@
1
1
  import { r as StandardFontName } from "./afm-widths-DyDq56Ph.js";
2
- import { r as EmbeddedFaceSubstitution, t as EmbeddedFace } from "./embedded-font-CbqkKgBl.js";
2
+ import { r as EmbeddedFaceSubstitution, t as EmbeddedFace } from "./embedded-font-fREdbxnr.js";
3
3
  import { TextMeasurer } from "./measure.js";
4
4
  import { WinAnsiSubstitution } from "./winansi.js";
5
5
  import { LayoutFont, LayoutItem } from "document-schema.js";
@@ -1,6 +1,6 @@
1
1
  import { o as encodeForShow } from "./afm-widths-Dxucrg7D.js";
2
2
  import { ByteWriter } from "./bytes/writer.js";
3
- import { pdfHexString } from "./objects.js";
3
+ import { pdfArray, pdfHexString, pdfNum } from "./objects.js";
4
4
  import { BEZIER_KAPPA, multiplyMatrices, rotationMatrix, scaleMatrix, translationMatrix } from "./matrix.js";
5
5
  import { encodeForShowEmbedded } from "./embedded-font.js";
6
6
  import { formatNumber, writeObject } from "./serialize.js";
@@ -32,27 +32,48 @@ function writeUnderline(writer, item, widthPt, underline) {
32
32
  writer.writeAscii("f\n");
33
33
  writer.writeAscii("Q\n");
34
34
  }
35
- function writeShowTextBlock(writer, item, resourceName, scalePercent, codes) {
35
+ function writeShowTextBlock(writer, item, resourceName, scalePercent, show) {
36
36
  writer.writeAscii("BT\n");
37
37
  writer.writeAscii(`/${resourceName} ${formatNumber(item.sizePt)} Tf\n`);
38
38
  writer.writeAscii(`${formatNumber(scalePercent)} Tz\n`);
39
39
  writeRgbOperator(writer, item.color, "rg");
40
40
  writeMatrixOperator(writer, anchorMatrix(item.xPt, item.yPt, item.rotationDeg), "Tm");
41
- writeObject(writer, pdfHexString(codes));
42
- writer.writeAscii(" Tj\n");
41
+ writeObject(writer, show.operand);
42
+ writer.writeAscii(` ${show.operator}\n`);
43
43
  writer.writeAscii("ET\n");
44
44
  }
45
45
  function writeStandardText(writer, item, standardName, resourceName, measurer, substitutions) {
46
46
  const encoded = encodeForShow(item.text, standardName);
47
47
  substitutions.push(...encoded.substitutions);
48
48
  const scale = measurer.horizontalScaleFor(item.font);
49
- writeShowTextBlock(writer, item, resourceName, scale * 100, encoded.codes);
49
+ writeShowTextBlock(writer, item, resourceName, scale * 100, {
50
+ operand: pdfHexString(encoded.codes),
51
+ operator: "Tj"
52
+ });
50
53
  if (item.underline === true) writeUnderline(writer, item, item.widthPt ?? encoded.width1000 / GLYPH_SPACE_UNITS_PER_EM * item.sizePt * scale, measurer.underlineAtSize(item.font, item.sizePt));
51
54
  }
55
+ function embeddedShowOperand(encoded) {
56
+ if (encoded.kerns.length === 0) return {
57
+ operand: pdfHexString(encoded.codes),
58
+ operator: "Tj"
59
+ };
60
+ const items = [];
61
+ let runStart = 0;
62
+ for (const kern of encoded.kerns) {
63
+ items.push(pdfHexString(encoded.codes.subarray(runStart, kern.codeOffset)));
64
+ items.push(pdfNum(-kern.adjustment1000));
65
+ runStart = kern.codeOffset;
66
+ }
67
+ items.push(pdfHexString(encoded.codes.subarray(runStart)));
68
+ return {
69
+ operand: pdfArray(items),
70
+ operator: "TJ"
71
+ };
72
+ }
52
73
  function writeEmbeddedText(writer, item, face, resourceName, missingGlyphs) {
53
74
  const encoded = encodeForShowEmbedded(item.text, face);
54
75
  missingGlyphs.push(...encoded.substitutions);
55
- writeShowTextBlock(writer, item, resourceName, EMBEDDED_HORIZONTAL_SCALE_PERCENT, encoded.codes);
76
+ writeShowTextBlock(writer, item, resourceName, EMBEDDED_HORIZONTAL_SCALE_PERCENT, embeddedShowOperand(encoded));
56
77
  if (item.underline === true) writeUnderline(writer, item, item.widthPt ?? encoded.width1000 / GLYPH_SPACE_UNITS_PER_EM * item.sizePt, {
57
78
  offsetPt: face.metrics.underlinePositionGlyphSpace / GLYPH_SPACE_UNITS_PER_EM * item.sizePt,
58
79
  thicknessPt: face.metrics.underlineThicknessGlyphSpace / GLYPH_SPACE_UNITS_PER_EM * item.sizePt
@@ -26,17 +26,23 @@ interface EmbeddedFace {
26
26
  readonly metrics: EmbeddedFaceMetrics;
27
27
  glyphId(codePoint: number): number | undefined;
28
28
  glyphSpaceWidth(glyphId: number): number;
29
+ kernGlyphSpace(leftGlyphId: number, rightGlyphId: number): number;
29
30
  }
30
31
  declare function loadEmbeddedFace(font: SfntFont): EmbeddedFace | undefined;
31
32
  interface EmbeddedFaceSubstitution {
32
33
  readonly from: string;
33
34
  }
35
+ interface EmbeddedKern {
36
+ readonly codeOffset: number;
37
+ readonly adjustment1000: number;
38
+ }
34
39
  interface EmbeddedShow {
35
40
  readonly codes: Uint8Array<ArrayBuffer>;
36
41
  readonly width1000: number;
37
42
  readonly substitutions: readonly EmbeddedFaceSubstitution[];
43
+ readonly kerns: readonly EmbeddedKern[];
38
44
  }
39
45
  declare function encodeForShowEmbedded(text: string, face: EmbeddedFace): EmbeddedShow;
40
46
  declare function collectEmbeddedGlyphs(texts: Iterable<string>, face: EmbeddedFace): ReadonlyMap<number, number>;
41
47
  //#endregion
42
- export { NOTDEF_GLYPH_ID as a, loadEmbeddedFace as c, EmbeddedShow as i, EmbeddedFaceMetrics as n, collectEmbeddedGlyphs as o, EmbeddedFaceSubstitution as r, encodeForShowEmbedded as s, EmbeddedFace as t };
48
+ export { EmbeddedShow as a, encodeForShowEmbedded as c, EmbeddedKern as i, loadEmbeddedFace as l, EmbeddedFaceMetrics as n, NOTDEF_GLYPH_ID as o, EmbeddedFaceSubstitution as r, collectEmbeddedGlyphs as s, EmbeddedFace as t };
@@ -26,17 +26,23 @@ interface EmbeddedFace {
26
26
  readonly metrics: EmbeddedFaceMetrics;
27
27
  glyphId(codePoint: number): number | undefined;
28
28
  glyphSpaceWidth(glyphId: number): number;
29
+ kernGlyphSpace(leftGlyphId: number, rightGlyphId: number): number;
29
30
  }
30
31
  declare function loadEmbeddedFace(font: SfntFont): EmbeddedFace | undefined;
31
32
  interface EmbeddedFaceSubstitution {
32
33
  readonly from: string;
33
34
  }
35
+ interface EmbeddedKern {
36
+ readonly codeOffset: number;
37
+ readonly adjustment1000: number;
38
+ }
34
39
  interface EmbeddedShow {
35
40
  readonly codes: Uint8Array<ArrayBuffer>;
36
41
  readonly width1000: number;
37
42
  readonly substitutions: readonly EmbeddedFaceSubstitution[];
43
+ readonly kerns: readonly EmbeddedKern[];
38
44
  }
39
45
  declare function encodeForShowEmbedded(text: string, face: EmbeddedFace): EmbeddedShow;
40
46
  declare function collectEmbeddedGlyphs(texts: Iterable<string>, face: EmbeddedFace): ReadonlyMap<number, number>;
41
47
  //#endregion
42
- export { NOTDEF_GLYPH_ID as a, loadEmbeddedFace as c, EmbeddedShow as i, EmbeddedFaceMetrics as n, collectEmbeddedGlyphs as o, EmbeddedFaceSubstitution as r, encodeForShowEmbedded as s, EmbeddedFace as t };
48
+ export { EmbeddedShow as a, encodeForShowEmbedded as c, EmbeddedKern as i, loadEmbeddedFace as l, EmbeddedFaceMetrics as n, NOTDEF_GLYPH_ID as o, EmbeddedFaceSubstitution as r, collectEmbeddedGlyphs as s, EmbeddedFace as t };
@@ -1,5 +1,5 @@
1
1
  import { PdfDict, PdfObject } from "./objects.cjs";
2
- import { t as EmbeddedFace } from "./embedded-font-CLedn-mT.cjs";
2
+ import { t as EmbeddedFace } from "./embedded-font-BU6Jgcof.cjs";
3
3
  import { SfntSubsetResult } from "./sfnt-subset.cjs";
4
4
  //#region src/embedded-font-write.d.ts
5
5
  declare function embeddedSubsetTag(postScriptName: string, glyphIds: readonly number[]): string;
@@ -1,5 +1,5 @@
1
1
  import { PdfDict, PdfObject } from "./objects.js";
2
- import { t as EmbeddedFace } from "./embedded-font-CbqkKgBl.js";
2
+ import { t as EmbeddedFace } from "./embedded-font-fREdbxnr.js";
3
3
  import { SfntSubsetResult } from "./sfnt-subset.js";
4
4
  //#region src/embedded-font-write.d.ts
5
5
  declare function embeddedSubsetTag(postScriptName: string, glyphIds: readonly number[]): string;
@@ -3,6 +3,7 @@ const require_sfnt = require("./sfnt.cjs");
3
3
  const require_cmap_table = require("./cmap-table.cjs");
4
4
  const require_font_tables = require("./font-tables.cjs");
5
5
  const require_glyf = require("./glyf.cjs");
6
+ const require_gpos_table = require("./gpos-table.cjs");
6
7
  const require_hmtx_table = require("./hmtx-table.cjs");
7
8
  //#region src/embedded-font.ts
8
9
  const GLYPH_SPACE_UNITS_PER_EM = 1e3;
@@ -55,6 +56,7 @@ function readEmbeddedFace(font) {
55
56
  const { unitsPerEm } = head;
56
57
  const scale = (designUnits) => scaleToGlyphSpace(designUnits, unitsPerEm);
57
58
  const capHeight = resolveCapHeight(font, cmap, os2?.sCapHeight, hhea.ascent);
59
+ const kern = require_gpos_table.buildGposKernLookup(font);
58
60
  return {
59
61
  font,
60
62
  postScriptName: name.postScriptName,
@@ -83,7 +85,8 @@ function readEmbeddedFace(font) {
83
85
  serif: isSerifByPanose(os2?.panose)
84
86
  },
85
87
  glyphId: (codePoint) => cmap(codePoint),
86
- glyphSpaceWidth: (glyphId) => scale(hmtx.advanceWidth(glyphId))
88
+ glyphSpaceWidth: (glyphId) => scale(hmtx.advanceWidth(glyphId)),
89
+ kernGlyphSpace: kern === void 0 ? () => 0 : (leftGlyphId, rightGlyphId) => scale(kern(leftGlyphId, rightGlyphId) ?? 0)
87
90
  };
88
91
  }
89
92
  const HHEA_TABLE_SIZE = 36;
@@ -104,26 +107,40 @@ function parseHhea(font) {
104
107
  numberOfHMetrics
105
108
  };
106
109
  }
110
+ const CID_BYTE_LENGTH = 2;
107
111
  function encodeForShowEmbedded(text, face) {
108
112
  const glyphIds = [];
109
113
  const substitutions = [];
114
+ const kerns = [];
110
115
  let width1000 = 0;
111
116
  for (const character of text) {
112
117
  const glyphId = face.glyphId(character.codePointAt(0));
113
118
  if (glyphId === void 0) substitutions.push({ from: character });
114
119
  const shown = glyphId ?? 0;
120
+ const previous = glyphIds[glyphIds.length - 1];
121
+ if (previous !== void 0) {
122
+ const adjustment1000 = face.kernGlyphSpace(previous, shown);
123
+ if (adjustment1000 !== 0) {
124
+ kerns.push({
125
+ codeOffset: glyphIds.length * CID_BYTE_LENGTH,
126
+ adjustment1000
127
+ });
128
+ width1000 += adjustment1000;
129
+ }
130
+ }
115
131
  glyphIds.push(shown);
116
132
  width1000 += face.glyphSpaceWidth(shown);
117
133
  }
118
- const codes = new Uint8Array(glyphIds.length * 2);
134
+ const codes = new Uint8Array(glyphIds.length * CID_BYTE_LENGTH);
119
135
  glyphIds.forEach((glyphId, index) => {
120
- codes[index * 2] = glyphId >> 8 & 255;
121
- codes[index * 2 + 1] = glyphId & 255;
136
+ codes[index * CID_BYTE_LENGTH] = glyphId >> 8 & 255;
137
+ codes[index * CID_BYTE_LENGTH + 1] = glyphId & 255;
122
138
  });
123
139
  return {
124
140
  codes,
125
141
  width1000,
126
- substitutions
142
+ substitutions,
143
+ kerns
127
144
  };
128
145
  }
129
146
  function collectEmbeddedGlyphs(texts, face) {
@@ -1,2 +1,2 @@
1
- import { a as NOTDEF_GLYPH_ID, c as loadEmbeddedFace, i as EmbeddedShow, n as EmbeddedFaceMetrics, o as collectEmbeddedGlyphs, r as EmbeddedFaceSubstitution, s as encodeForShowEmbedded, t as EmbeddedFace } from "./embedded-font-CLedn-mT.cjs";
2
- export { EmbeddedFace, EmbeddedFaceMetrics, EmbeddedFaceSubstitution, EmbeddedShow, NOTDEF_GLYPH_ID, collectEmbeddedGlyphs, encodeForShowEmbedded, loadEmbeddedFace };
1
+ import { a as EmbeddedShow, c as encodeForShowEmbedded, i as EmbeddedKern, l as loadEmbeddedFace, n as EmbeddedFaceMetrics, o as NOTDEF_GLYPH_ID, r as EmbeddedFaceSubstitution, s as collectEmbeddedGlyphs, t as EmbeddedFace } from "./embedded-font-BU6Jgcof.cjs";
2
+ export { EmbeddedFace, EmbeddedFaceMetrics, EmbeddedFaceSubstitution, EmbeddedKern, EmbeddedShow, NOTDEF_GLYPH_ID, collectEmbeddedGlyphs, encodeForShowEmbedded, loadEmbeddedFace };
@@ -1,2 +1,2 @@
1
- import { a as NOTDEF_GLYPH_ID, c as loadEmbeddedFace, i as EmbeddedShow, n as EmbeddedFaceMetrics, o as collectEmbeddedGlyphs, r as EmbeddedFaceSubstitution, s as encodeForShowEmbedded, t as EmbeddedFace } from "./embedded-font-CbqkKgBl.js";
2
- export { EmbeddedFace, EmbeddedFaceMetrics, EmbeddedFaceSubstitution, EmbeddedShow, NOTDEF_GLYPH_ID, collectEmbeddedGlyphs, encodeForShowEmbedded, loadEmbeddedFace };
1
+ import { a as EmbeddedShow, c as encodeForShowEmbedded, i as EmbeddedKern, l as loadEmbeddedFace, n as EmbeddedFaceMetrics, o as NOTDEF_GLYPH_ID, r as EmbeddedFaceSubstitution, s as collectEmbeddedGlyphs, t as EmbeddedFace } from "./embedded-font-fREdbxnr.js";
2
+ export { EmbeddedFace, EmbeddedFaceMetrics, EmbeddedFaceSubstitution, EmbeddedKern, EmbeddedShow, NOTDEF_GLYPH_ID, collectEmbeddedGlyphs, encodeForShowEmbedded, loadEmbeddedFace };
@@ -2,6 +2,7 @@ import { hasBytes, i16, sfntTableBytes, u16 } from "./sfnt.js";
2
2
  import { buildCmapLookup } from "./cmap-table.js";
3
3
  import { parseHead, parseMaxp, parseName, parseOs2, parsePost } from "./font-tables.js";
4
4
  import { parseGlyf } from "./glyf.js";
5
+ import { buildGposKernLookup } from "./gpos-table.js";
5
6
  import { parseHmtx } from "./hmtx-table.js";
6
7
  //#region src/embedded-font.ts
7
8
  const GLYPH_SPACE_UNITS_PER_EM = 1e3;
@@ -54,6 +55,7 @@ function readEmbeddedFace(font) {
54
55
  const { unitsPerEm } = head;
55
56
  const scale = (designUnits) => scaleToGlyphSpace(designUnits, unitsPerEm);
56
57
  const capHeight = resolveCapHeight(font, cmap, os2?.sCapHeight, hhea.ascent);
58
+ const kern = buildGposKernLookup(font);
57
59
  return {
58
60
  font,
59
61
  postScriptName: name.postScriptName,
@@ -82,7 +84,8 @@ function readEmbeddedFace(font) {
82
84
  serif: isSerifByPanose(os2?.panose)
83
85
  },
84
86
  glyphId: (codePoint) => cmap(codePoint),
85
- glyphSpaceWidth: (glyphId) => scale(hmtx.advanceWidth(glyphId))
87
+ glyphSpaceWidth: (glyphId) => scale(hmtx.advanceWidth(glyphId)),
88
+ kernGlyphSpace: kern === void 0 ? () => 0 : (leftGlyphId, rightGlyphId) => scale(kern(leftGlyphId, rightGlyphId) ?? 0)
86
89
  };
87
90
  }
88
91
  const HHEA_TABLE_SIZE = 36;
@@ -103,26 +106,40 @@ function parseHhea(font) {
103
106
  numberOfHMetrics
104
107
  };
105
108
  }
109
+ const CID_BYTE_LENGTH = 2;
106
110
  function encodeForShowEmbedded(text, face) {
107
111
  const glyphIds = [];
108
112
  const substitutions = [];
113
+ const kerns = [];
109
114
  let width1000 = 0;
110
115
  for (const character of text) {
111
116
  const glyphId = face.glyphId(character.codePointAt(0));
112
117
  if (glyphId === void 0) substitutions.push({ from: character });
113
118
  const shown = glyphId ?? 0;
119
+ const previous = glyphIds[glyphIds.length - 1];
120
+ if (previous !== void 0) {
121
+ const adjustment1000 = face.kernGlyphSpace(previous, shown);
122
+ if (adjustment1000 !== 0) {
123
+ kerns.push({
124
+ codeOffset: glyphIds.length * CID_BYTE_LENGTH,
125
+ adjustment1000
126
+ });
127
+ width1000 += adjustment1000;
128
+ }
129
+ }
114
130
  glyphIds.push(shown);
115
131
  width1000 += face.glyphSpaceWidth(shown);
116
132
  }
117
- const codes = new Uint8Array(glyphIds.length * 2);
133
+ const codes = new Uint8Array(glyphIds.length * CID_BYTE_LENGTH);
118
134
  glyphIds.forEach((glyphId, index) => {
119
- codes[index * 2] = glyphId >> 8 & 255;
120
- codes[index * 2 + 1] = glyphId & 255;
135
+ codes[index * CID_BYTE_LENGTH] = glyphId >> 8 & 255;
136
+ codes[index * CID_BYTE_LENGTH + 1] = glyphId & 255;
121
137
  });
122
138
  return {
123
139
  codes,
124
140
  width1000,
125
- substitutions
141
+ substitutions,
142
+ kerns
126
143
  };
127
144
  }
128
145
  function collectEmbeddedGlyphs(texts, face) {
@@ -1,5 +1,5 @@
1
1
  import { r as StandardFontName } from "./afm-widths-DyDq56Ph.cjs";
2
- import { t as EmbeddedFace } from "./embedded-font-CLedn-mT.cjs";
2
+ import { t as EmbeddedFace } from "./embedded-font-BU6Jgcof.cjs";
3
3
  import { LayoutFont } from "document-schema.js";
4
4
  //#region src/font-registry.d.ts
5
5
  interface ProvidedFont {
@@ -1,5 +1,5 @@
1
1
  import { r as StandardFontName } from "./afm-widths-DyDq56Ph.js";
2
- import { t as EmbeddedFace } from "./embedded-font-CbqkKgBl.js";
2
+ import { t as EmbeddedFace } from "./embedded-font-fREdbxnr.js";
3
3
  import { LayoutFont } from "document-schema.js";
4
4
  //#region src/font-registry.d.ts
5
5
  interface ProvidedFont {
@@ -1,2 +1,2 @@
1
- import { a as ResolvedFace, i as ProvidedFont, n as FontRegistryOptions, o as createFontRegistry, r as FontSubstitution, s as resolveFaceWithRegistry, t as FontRegistry } from "./font-registry-yby02snZ.cjs";
1
+ import { a as ResolvedFace, i as ProvidedFont, n as FontRegistryOptions, o as createFontRegistry, r as FontSubstitution, s as resolveFaceWithRegistry, t as FontRegistry } from "./font-registry-B5qL0Vi0.cjs";
2
2
  export { FontRegistry, FontRegistryOptions, FontSubstitution, ProvidedFont, ResolvedFace, createFontRegistry, resolveFaceWithRegistry };
@@ -1,2 +1,2 @@
1
- import { a as ResolvedFace, i as ProvidedFont, n as FontRegistryOptions, o as createFontRegistry, r as FontSubstitution, s as resolveFaceWithRegistry, t as FontRegistry } from "./font-registry-Cn7P9lAh.js";
1
+ import { a as ResolvedFace, i as ProvidedFont, n as FontRegistryOptions, o as createFontRegistry, r as FontSubstitution, s as resolveFaceWithRegistry, t as FontRegistry } from "./font-registry-mgdbNP8_.js";
2
2
  export { FontRegistry, FontRegistryOptions, FontSubstitution, ProvidedFont, ResolvedFace, createFontRegistry, resolveFaceWithRegistry };