Repository navigation
Merge upstream's master, 2026-10-28 - #8
Merged
Merged
Conversation
* Fix glyph code point cache invalidation * fix: isolate glyph cache by code points * fix(glyphs): refresh empty metadata without changing cache identity
getCharacterSet returned every code in each format 4 segment, including the required 0xFFFF closing segment and codes mapped to glyph 0, so it disagreed with hasGlyphForCodePoint. Keep only codes whose lookup is not .notdef, for every cmap format. Co-authored-by: youdie006 <[email protected]>
The format 10 struct referenced a numChars field that does not exist, so any font with a format 10 subtable threw "Not a fixed size" on lookup. Co-authored-by: youdie006 <[email protected]>
codePointsForGlyph handled formats 0, 4, 12 and 13 only, so stringsForGlyph threw "Unknown cmap format 10" for a font whose best cmap is format 6 or 10, although lookup and characterSet support both. Co-authored-by: youdie006 <[email protected]>
foliojs#393) * Keep a glyph's variation metrics stable when several composites use it _getPhantomPoints built the phantom points from this._metrics, which _getContours replaces with the varied metrics. A glyph is decoded again for every composite that uses it, so the gvar delta was applied once per composite: in Mada-VF at wght 100, dotless i advanced 229, 202, 175, 148 after laying out its accented composites. Build the phantom points from the default metrics. * Use !this._defaultMetrics and test a common Latin case --------- Co-authored-by: youdie006 <[email protected]>
…oliojs#394) * Map space and missing code points correctly with a legacy Mac cmap The hand-written Mac encoding tables had a plain space at 0xCA where the no-break space belongs, so U+0020 looked up the NBSP glyph and U+00A0 fell back to byte 0xA0. lookup also passed through any code point the encoding lacks, so U+00B9 and U+0080 hit unrelated glyphs. Fix the tables and return glyph 0 for non-ASCII code points the encoding cannot represent. * Check the re-encoded code point with !codepoint --------- Co-authored-by: youdie006 <[email protected]>
…g beyond their data (foliojs#353, foliojs#378, foliojs#303, foliojs#312). Based on foliojs#379
The USE state machine only matches syllables, so a ZWNJ, ZWJ, CGJ or another character that is not part of one kept a null shaperInfo, and reorder() threw "Cannot read properties of null (reading 'syllable')". Give these glyphs their own syllable, as the Indic shaper does with non_indic_cluster and HarfBuzz does with non_cluster.
TTFGlyphEncoder still passed a byte count to EncodeStream, which since restructure 3 takes a Uint8Array, so createSubset().encode() threw a TypeError for any font with set variations (foliojs#308). The existing test for this only runs when /Library/Fonts/Skia.ttf exists, so add one with a bundled variable font.
* Continue after the glyphs a lookup matched or inserted After a lookup applied, fontkit always moved on to the glyph after the one it started at. A multiple substitution was then applied to its own output, and a contextual lookup to glyphs inside its input sequence or inserted by its nested lookups, so fonts like Libre Barcode EAN13 Text ran out of memory (foliojs#383). After a deletion, the following glyph was skipped. Like HarfBuzz, continue after the inserted glyphs, at the glyph that followed a deleted one, and after the input sequence of a contextual lookup, adjusted for the glyphs its nested lookups inserted or deleted. * Use decoded filed values --------- Co-authored-by: Luiz Americo <[email protected]>
…#400) * Fix repha, pre-base vowel and dotted circle placement in USE reorder() in the Universal Shaping Engine differed from HarfBuzz in three places: - The dotted circle of a broken cluster was inserted after the first non-repha glyph instead of before it, and shared that glyph's shaperInfo, so it was never treated as a base. - Moving a repha read glyphs[i] after splicing the array, which duplicated a glyph or inserted undefined, so a repha followed by a base (e.g. RA + virama + KA in Modi or Tirhuta) crashed or gave extra glyphs. Move it before the first post-base glyph, as HarfBuzz does. - Every part of a pre-base vowel split by a multiple substitution was moved before the base. Only move the first, like HarfBuzz and the Indic shaper. * Small optimization --------- Co-authored-by: Luiz Americo <[email protected]>
- Decompose SARA AM (U+0E33) into NIKHAHIT + SARA AA and reorder NIKHAHIT past above-base marks, matching `preprocess_text_thai` - Apply PUA tone/vowel shift fallback for legacy fonts without Thai GSUB, mirroring `do_thai_pua_shaping` (above/below state machines + Windows/Mac PUA mapping tables) - Fall back to the buffer's Unicode script in OTLayoutEngine when neither GSUB nor GPOS picks an OT script — lets script-specific shapers run on fonts without matching GSUB/GPOS so the Thai PUA fallback is actually reachable - Fix typo in `UnicodeLayoutEngine` Thai mark classification: `0x0E3D` (unassigned) → `0x0E4D` (NIKHAHIT) - Register `thai` and `'lao '` (the 4-char OT tag with trailing space), add Noto Sans Thai + Noto Sans Lao (OFL) as test fixtures with 8 shaping tests
- Compose Hebrew presentation forms (FBxx range) for fonts without GPOS marks, matching `compose_hebrew`; chain compositions step by step (e.g. SHIN + SHIN_DOT + DAGESH → FB2C) instead of bailing after the first match - Reorder patah/qamats + sheva/hiriq + meteg/below mark triplet by codepoint identity, matching `reorder_marks_hebrew`'s end state for both canonical NFC input and HarfBuzz's modified-CCC pre-reorder order - Scope the `hasGposMark` gate to the Hebrew script's GPOS features instead of scanning the global featureList, matching HarfBuzz's per-script `has_gpos_mark` - Add Taamey Frank CLM fixture (GPL v2 + font exception, also used by HB) with 6 shaping tests; reorder tests now exercise real code (each fails with the function disabled)
…er (foliojs#403) * Compose canonical decompositions (NFC) in the default shaper - Compose each base + combining-mark cluster into the font's precomposed glyph via font-aware NFC before GSUB, matching HarfBuzz: decomposed input (i + U+0300, or Arabic alef + fathatan + hamza-above) shapes to the precomposed glyph instead of separate marks - Only apply the result when it changes the glyph count (composition or its decompose fallback); leave pure canonical reordering alone so it can't disturb downstream GSUB (e.g. Arabic shadda + vowel calt) - Decompose back any composed codepoint the font has no glyph for, so its marks stay available for GPOS mark positioning - Scope to the default shaper and the Arabic/Hebrew/Thai shapers that inherit it; Indic/Hangul/Universal keep their own composition - Add 6 canonical-composition shaping tests (FiraSans Latin, Amiri Arabic incl. the reorder regression) * Fix canonical composition fallback and mark positioning --------- Co-authored-by: Jürg Lehni <[email protected]> Co-authored-by: Luiz Americo <[email protected]>
…iojs#410) - Place below marks under the base instead of above it, and never shift them up - Add the gap for unattached marks instead of attached ones, and limit how far above marks shift down - Center marks on the base advance with integer rounding, and stack marks per combining class - Add tests with an OFL subset of Noticia Text Italic, checked against hb-shape
- Attached marks were offset by the advances of the marks between them and their base, which shapers zero only after GPOS, misplacing stacked marks in fonts whose marks have an advance (Roboto Serif, and all Hebrew vowels in Akkurat Hebrew) - Resolve cursive and mark attachments once, after the advances are final, as HarfBuzz does, instead of after every GPOS stage - Add a test with a Roboto Serif subset (OFL), matching hb-shape Co-authored-by: Luiz Américo <[email protected]>
- A feature requested by the user was ignored if the shaper had already planned it locally, e.g. frac, which is only applied around U+2044 by default - Such features now apply to the whole run while keeping their stage, matching HarfBuzz's feature merging - Replaces the previous approach of always applying frac globally, which turned all digits into numerators - Add a test for user-enabled frac, matching HarfBuzz output
* Support variations of fonts in WOFF and WOFF2 containers getVariation() rebuilt the font as a TTFFont, from the stream at the directory's offset. For a WOFF that read a WOFF directory as an sfnt's, and for a WOFF2 the stream is the compressed file, or once anything was decoded the decompressed tables with no directory at all: the variation had no tables, and threw from wherever its first table was asked for. A variation is now made by the font's own class (_createVariation), so a container reads its directory and its tables the way it holds them. WOFF: the glyph variation data was read from the font's stream at offsets that are positions in the inflated gvar table. GlyphVariationProcessor reads it from the stream the table was decoded from. A table is inflated once and kept, where it was inflated again for every glyph that asked for it. WOFF2: a variation reads the directory from the file again and shares the font's decompressed tables and decoded glyphs, so nothing is decompressed or decoded twice. The transformed glyf table is decoded without a variation applied, and WOFF2Glyph moves a copy of a glyph's points, or of its components' offsets, where TTFGlyph moves what it decoded. The phantom points need the bounding box a glyf header has, which the transformed table stores for some glyphs and leaves to be computed from the points for the rest, so the bounding box stream is read. The tests compare every glyph of a variation (path, bounding box, advances) with the same variation of the sfnt the container was made from: TrueType outlines with and without HVAR, composite glyphs, CFF2, and a WOFF2 whose glyf table is not transformed. The containers were made with fontTools from the fonts already in test/data. * Address the review of foliojs#389 - Decode the points and deltas of glyph variations in functions at module scope - Move the components of composite glyphs only for composite glyphs, as TTFGlyph does --------- Co-authored-by: Andrey Sidorov <[email protected]>
…itution) (foliojs#370) - Fix the GSUB Type 8 struct to include the missing `backtrackGlyphCount` field; without it the binary reader couldn't decode the table at all - Implement Type 8 application in GSUBProcessor: check coverage at the current glyph, verify backtrack + lookahead context, replace the glyph with `substitutes[coverageIndex]` on match - Iterate Type 8 lookups right-to-left in OTProcessor's applyLookups (per spec) so lookahead always sees post-substitution glyphs and backtrack sees the pre-substitution ones - Add Noto Coptic as a test fixture (its ccmp feature uses Type 8; without this fix any Coptic layout throws "GSUB lookupType 8 is not supported")
- A soft hyphen replaced by its case form was still swapped for an invisible space, as the substituted glyph keeps the soft hyphen's code point - Hide only default ignorables that still show their nominal glyph, as HarfBuzz does - Add tests for hiding a soft hyphen and keeping its case form, with Fira Sans
- Type 3 hardcoded USER_INDEX = 0; `nalt: 2` silently rendered as alt #1 - Thread user features through ShapingPlan so GSUB reads the 1-based index - Don't mutate caller's features in OTLayoutEngine.setup; overlay instead
- A TrueType variable font with no explicit variation set skipped FeatureVariations entirely, unlike HarfBuzz, which always evaluates them at the current location - Evaluate them at the default location for such fonts, so default-location substitutions (e.g. bracket-layer alternates) apply - Add a test with a subset of Archivo (OFL), whose rvrn alternates for $ and ¢ cover the default weight
- Backtrack arrays are stored in reverse buffer order per OT spec - Reverse before matching so backtrack[0] aligns with position before input
- Sort every mark run by combining class instead of keeping the typed order of marks that don't compose (q + dot above + dot below now orders the dot below first) - Use HarfBuzz's modified combining classes, so shadda sorts before Arabic vowels even when the vowel is typed first, and Hebrew, Thai, Telugu and Tibetan marks follow the order fonts expect - Move Arabic modifier combining marks (hamza above or below, small high letters) to the start of their run, as in UTR foliojs#53 - Add tests for Latin reordering, Arabic modifier marks and Allah typed with fatha before shadda in Amiri, matching hb-shape
…s#373) - Skip marks not in the lookup's filtering set per OT spec - Overrides ignoreMarks and markAttachmentType for marks
Name records with platform 3 and encoding 0 (Symbol) are UTF-16BE like the other Windows encodings, but were mapped to an encoding the string decoder doesn't know, so their names came back as Buffers. Co-authored-by: Wleeaf <[email protected]>
…t ones (foliojs#413) * Use non-default variation sequences when the selector also has default ones getVariationSelector only searched the non-default UVS table when the code point was found in the default UVS table, so a selector with both tables never returned its variant glyphs. * Use a subset of Noto Sans CJK JP for the variation sequence test --------- Co-authored-by: Wleeaf <[email protected]>
…oliojs#419) * Respect useTypoMetrics, and fix vhea and empty glyph metrics - Fix the vhea table structure - Give empty glyphs zero coordinates instead of infinite ones - Read the maxp, post and vhea versions as 16.16 version numbers - Respect the useTypoMetrics flag for the font's ascent, descent and line gap, and add lineHeight - Fix an undefined control box in the glyph metrics * Follow HarfBuzz for the font metrics - Use the typo metrics only when the font sets useTypoMetrics and the hhea metrics otherwise, with a positive ascent and a negative descent, as HarfBuzz does, instead of FreeType's fallbacks - Drop lineHeight, which HarfBuzz doesn't have and clients can derive - Expect HarfBuzz's vertical advance for Open Sans's space, which has no vmtx and doesn't set useTypoMetrics - Add tests for the typo metrics, the vertical advance without vmtx, the vhea and maxp versions, and empty CFF glyph boxes, with HarfBuzz's values --------- Co-authored-by: Vsevolod Volkov <[email protected]>
Co-authored-by: Wleeaf <[email protected]>
- Round the varied advances from HVAR to whole units, as HarfBuzz does - Subsets of fonts with set variations no longer truncate advances like 851.9999 to 851 - Make the two HVAR tests check exact advances
…js#39) (foliojs#415) * Reverse right-to-left text in fonts without GSUB, GPOS or morx (foliojs#39) * Test order and marks with the existing mark fallback font --------- Co-authored-by: Zahraa Sadeq <[email protected]>
…js#431) U+1A60 TAI THAM SIGN SAKOT was classified as a halant, which the USE grammar only allows before vowel signs. In Tai Tham the sakot and its consonant can also follow vowel signs and vowel modifiers, so words like ᨩᩢ᩠ᨿ became broken clusters and got a dotted circle. Split the sakot off of H into its own Sk category and allow (Sk B)* after the vowel modifiers and a sakot-terminated cluster, as HarfBuzz does.
Upstream has not released since 2.0.4, and its master has taken three of the fork's patches in its own form, so those are upstream's now: variations of WOFF and WOFF2 fonts (the fork's #1, upstream's foliojs#411), a feature the caller asks for applying to every glyph (the fork's #7, upstream's foliojs#294 and foliojs#371), and the Node build decompressing with the runtime's zlib (half of the fork's #5). Every conflict is resolved to upstream's side, and on top of it the fork keeps what upstream does not have: - decoded structs with their hidden fields in private fields of a class (#4): every file that declares structs imports restructure from src/restructure.js again. The first shaping of SourceSansPro still takes 17.5 ms against upstream's 23.3. - setBrotliDecompressor and the size check (#5, #6), now over upstream's #compression, exported from base.js. test/entrypoints.js expects the extra export. - the browser build in one file, dist/browser-module.mjs, which ntk vendors, as a third output of rolldown.config.ts. The parcel targets for Chrome 91 go with parcel: upstream builds for Chrome 120 and Node 22. The release workflow runs on Node 22, checks the new dist files, and runs the whole suite: the setDefaultLanguage block the parcel build broke passes now.
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.
This merges foliojs/fontkit's master as of 2026-10-28 (
f4c2243). Upstream has released nothing since 2.0.4 in 2024. Its master is a large step past that release: a rolldown build, ESM only, Node 22 and newer, and many HarfBuzz-parity fixes, with breaking changes listed under "Unreleased" in its CHANGELOG. It has also taken three of this fork's patches, in its own form:layout()leaves the caller's features object alone, as Honour user alternateNumber in GSUB Type 3 substitutions foliojs/fontkit#371;Don't tag this yet. ntk, which vendors the fork, needs two things first. Both are below.
How the conflicts were resolved
Every conflict goes to upstream's side. On top of that, the fork keeps only what upstream does not have:
src/restructure.jsagain. That is a single import line in each of 61 files. The patch still pays: SourceSansPro's first shaping takes 17.5 ms against upstream's 23.3, the median of 15 fresh processes.setBrotliDecompressorand the size check (Decompress WOFF2 with the runtime's own Brotli in the builds for Node #5, Export setBrotliDecompressor, so a browser build can be handed a native Brotli #6). Both now sit on upstream's#compressionand are exported fromsrc/base.js. Upstream'stest/entrypoints.jschecks each entry's exact exports, so it gains one expected key.dist/browser-module.mjs. This is the browser build in one file, a third output inrolldown.config.ts. Upstream's browser build is now split into chunks, and ntk's vendor script carries a single file.test/brotli.jschecks that the file has no chunks and imports no builtins.dist/files. It runs the whole suite: thesetDefaultLanguageblock that the parcel build broke passes now.WINDOWKIT.md. The table lists only what the fork still carries. A list below it records what came back from upstream.README.mddocumentssetBrotliDecompressor.The Chrome 91 target went away with parcel. Upstream builds for Chrome 120 and Node 22, where private fields are native.
Checked
npm test: 722 passing, 7 pending, on Node 22, 24 and 26.npm run format:checkpasses too. Upstream'slint-test.ymlruns both on this PR.lib/text/marks.js, hadflags: {}. Upstream Honour useMarkFilteringSet lookup flag during glyph iteration foliojs/fontkit#373 readslookup.flags.flags.useMarkFilteringSet, so every shaping with mark lookups threw. The stand-in now has the shape of a decoded lookup's flags.test/brotli-dictionary.test.js. Upstream now needs brotli ^2.1.0, which is ESM and initialises its 756 KB static dictionary when it is imported, about 11 ms. ntk#427'srequire.cachestand-in cannot reach an ESM module. ntk's startup goes from 33.7 ms to 36.2: the rest of upstream's build saves about 8 ms. The fix belongs in brotli.js, a lazy dictionary, and is being prepared separately.useTypoMetricsamong them, move nothing those tests measure.