@cantoo/fontkit 2.0.4 → 2.0.6

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.
Files changed (142) hide show
  1. package/dist/browser-module.mjs +4448 -1142
  2. package/dist/browser-module.mjs.map +1 -1
  3. package/dist/browser.cjs +4449 -1142
  4. package/dist/browser.cjs.map +1 -1
  5. package/dist/main.cjs +4449 -1142
  6. package/dist/main.cjs.map +1 -1
  7. package/dist/module.mjs +4448 -1142
  8. package/dist/module.mjs.map +1 -1
  9. package/package.json +4 -1
  10. package/src/CmapProcessor.js +60 -21
  11. package/src/DFont.js +114 -14
  12. package/src/TTFFont.js +260 -62
  13. package/src/TrueTypeCollection.js +59 -15
  14. package/src/WOFF2Font.js +128 -22
  15. package/src/WOFFFont.js +45 -8
  16. package/src/aat/AATFeatureMap.js +83 -40
  17. package/src/aat/AATLayoutEngine.js +34 -2
  18. package/src/aat/AATLookupTable.js +24 -6
  19. package/src/aat/AATMorxProcessor.js +202 -65
  20. package/src/aat/AATStateMachine.js +35 -3
  21. package/src/base.js +20 -1
  22. package/src/cff/CFFCharsets.js +3 -0
  23. package/src/cff/CFFDict.js +81 -11
  24. package/src/cff/CFFEncodings.js +2 -0
  25. package/src/cff/CFFFont.js +120 -24
  26. package/src/cff/CFFIndex.js +66 -9
  27. package/src/cff/CFFOperand.js +53 -28
  28. package/src/cff/CFFPointer.js +41 -2
  29. package/src/cff/CFFPrivateDict.js +34 -24
  30. package/src/cff/CFFStandardStrings.js +68 -67
  31. package/src/cff/CFFTop.js +161 -68
  32. package/src/decorators.js +34 -15
  33. package/src/encodings.js +133 -116
  34. package/src/fs.js +10 -0
  35. package/src/glyph/BBox.js +14 -0
  36. package/src/glyph/CFFGlyph.js +293 -152
  37. package/src/glyph/COLRGlyph.js +40 -5
  38. package/src/glyph/Glyph.js +72 -13
  39. package/src/glyph/GlyphVariationProcessor.js +158 -62
  40. package/src/glyph/Path.js +119 -32
  41. package/src/glyph/SBIXGlyph.js +57 -8
  42. package/src/glyph/StandardNames.js +1 -0
  43. package/src/glyph/TTFGlyph.js +203 -63
  44. package/src/glyph/TTFGlyphEncoder.js +58 -26
  45. package/src/glyph/WOFF2Glyph.js +25 -6
  46. package/src/layout/GlyphPosition.js +6 -0
  47. package/src/layout/GlyphRun.js +28 -10
  48. package/src/layout/KernProcessor.js +63 -4
  49. package/src/layout/LayoutEngine.js +99 -26
  50. package/src/layout/Script.js +52 -26
  51. package/src/layout/UnicodeLayoutEngine.js +34 -10
  52. package/src/opentype/GPOSProcessor.js +156 -45
  53. package/src/opentype/GSUBProcessor.js +92 -33
  54. package/src/opentype/GlyphInfo.js +53 -5
  55. package/src/opentype/GlyphIterator.js +51 -2
  56. package/src/opentype/OTLayoutEngine.js +66 -8
  57. package/src/opentype/OTProcessor.js +284 -71
  58. package/src/opentype/ShapingPlan.js +52 -5
  59. package/src/opentype/shapers/ArabicShaper.js +27 -8
  60. package/src/opentype/shapers/DefaultShaper.js +54 -10
  61. package/src/opentype/shapers/HangulShaper.js +120 -42
  62. package/src/opentype/shapers/HebrewShaper.js +50 -16
  63. package/src/opentype/shapers/IndicShaper.js +159 -44
  64. package/src/opentype/shapers/ThaiShaper.js +50 -19
  65. package/src/opentype/shapers/UniversalShaper.js +83 -17
  66. package/src/opentype/shapers/index.js +48 -35
  67. package/src/opentype/shapers/indic-data.js +2 -2
  68. package/src/packages/unicode-properties/data-trie.js +2 -1
  69. package/src/packages/unicode-properties/data.js +13 -1
  70. package/src/packages/unicode-properties/dist/index.cjs +23 -10
  71. package/src/packages/unicode-properties/dist/index.cjs.map +2 -2
  72. package/src/packages/unicode-properties/dist/index.mjs +22 -10
  73. package/src/packages/unicode-properties/dist/index.mjs.map +2 -2
  74. package/src/packages/unicode-properties/generate.js +3 -4
  75. package/src/packages/unicode-properties/index.js +95 -30
  76. package/src/packages/unicode-properties/package.json +1 -1
  77. package/src/packages/unicode-trie/builder.js +146 -29
  78. package/src/packages/unicode-trie/dist/builder.cjs +135 -25
  79. package/src/packages/unicode-trie/dist/builder.cjs.map +2 -2
  80. package/src/packages/unicode-trie/dist/builder.mjs +134 -25
  81. package/src/packages/unicode-trie/dist/builder.mjs.map +2 -2
  82. package/src/packages/unicode-trie/dist/index.cjs +22 -9
  83. package/src/packages/unicode-trie/dist/index.cjs.map +2 -2
  84. package/src/packages/unicode-trie/dist/index.mjs +21 -9
  85. package/src/packages/unicode-trie/dist/index.mjs.map +2 -2
  86. package/src/packages/unicode-trie/index.js +63 -12
  87. package/src/packages/unicode-trie/package.json +1 -1
  88. package/src/packages/unicode-trie/swap.js +15 -2
  89. package/src/subset/CFFSubset.js +112 -22
  90. package/src/subset/Subset.js +23 -3
  91. package/src/subset/TTFSubset.js +84 -41
  92. package/src/tables/BASE.js +28 -27
  93. package/src/tables/COLR.js +10 -9
  94. package/src/tables/CPAL.js +2 -1
  95. package/src/tables/DSIG.js +8 -7
  96. package/src/tables/EBDT.js +10 -4
  97. package/src/tables/EBLC.js +70 -10
  98. package/src/tables/GDEF.js +18 -17
  99. package/src/tables/GPOS.js +153 -90
  100. package/src/tables/GSUB.js +50 -39
  101. package/src/tables/HVAR.js +51 -5
  102. package/src/tables/JSTF.js +17 -16
  103. package/src/tables/LTSH.js +4 -3
  104. package/src/tables/OS2.js +50 -49
  105. package/src/tables/PCLT.js +16 -15
  106. package/src/tables/STAT.js +46 -6
  107. package/src/tables/VDMX.js +18 -17
  108. package/src/tables/VORG.js +8 -7
  109. package/src/tables/WOFF2Directory.js +57 -9
  110. package/src/tables/WOFFDirectory.js +32 -20
  111. package/src/tables/aat.js +160 -35
  112. package/src/tables/avar.js +1 -0
  113. package/src/tables/bsln.js +3 -2
  114. package/src/tables/cmap.js +83 -63
  115. package/src/tables/cvt.js +1 -0
  116. package/src/tables/directory.js +56 -28
  117. package/src/tables/feat.js +24 -2
  118. package/src/tables/fpgm.js +1 -0
  119. package/src/tables/fvar.js +32 -4
  120. package/src/tables/gasp.js +6 -5
  121. package/src/tables/glyf.js +2 -1
  122. package/src/tables/gvar.js +29 -2
  123. package/src/tables/hdmx.js +18 -7
  124. package/src/tables/head.js +17 -16
  125. package/src/tables/hhea.js +15 -14
  126. package/src/tables/hmtx.js +18 -2
  127. package/src/tables/index.js +1 -5
  128. package/src/tables/just.js +15 -4
  129. package/src/tables/kern.js +84 -47
  130. package/src/tables/loca.js +23 -10
  131. package/src/tables/maxp.js +16 -15
  132. package/src/tables/morx.js +27 -20
  133. package/src/tables/name.js +120 -39
  134. package/src/tables/opbd.js +2 -1
  135. package/src/tables/opentype.js +126 -91
  136. package/src/tables/post.js +18 -11
  137. package/src/tables/prep.js +1 -0
  138. package/src/tables/sbix.js +12 -1
  139. package/src/tables/variations.js +29 -9
  140. package/src/tables/vhea.js +15 -14
  141. package/src/tables/vmtx.js +20 -4
  142. package/src/utils.js +15 -0
@@ -1,13 +1,24 @@
1
- import {getCombiningClass} from '../../packages/unicode-properties/index.js';
1
+ import { getCombiningClass } from '../../packages/unicode-properties/index.js';
2
2
  import DefaultShaper from './DefaultShaper';
3
3
  import GlyphInfo from '../GlyphInfo';
4
4
 
5
+ /** @typedef {import('../../../types/fontkit').ShapingPlanLike} ShapingPlanLike */
6
+ /** @typedef {import('../../../types/fontkit').GlyphInfoLike} GlyphInfoLike */
7
+ /** @typedef {import('../../../types/fontkit').LayoutFont} LayoutFont */
8
+ /** @typedef {import('../../../types/fontkit').OTScriptRecord} OTScriptRecord */
9
+ /** @typedef {import('../../../types/fontkit').OTLangSys} OTLangSys */
10
+ /** @typedef {import('../../../types/fontkit').OTLangSysRecord} OTLangSysRecord */
11
+
5
12
  /**
6
13
  * Hebrew shaper (HarfBuzz hb-ot-shaper-hebrew.cc):
7
14
  * - Compose presentation forms (FBxx) when the font lacks GPOS mark for `hebr`
8
15
  * - Reorder patah/qamats + sheva/hiriq + meteg/below so meteg sits next to the base
9
16
  */
10
17
  export default class HebrewShaper extends DefaultShaper {
18
+ /**
19
+ * @param {ShapingPlanLike} plan
20
+ * @param {GlyphInfoLike[]} glyphs
21
+ */
11
22
  static assignFeatures(plan, glyphs) {
12
23
  super.assignFeatures(plan, glyphs);
13
24
 
@@ -26,7 +37,12 @@ const DAGESH_FORMS = [
26
37
  0xFB48, 0xFB49, 0xFB4A
27
38
  ];
28
39
 
29
- // Precomposed Hebrew presentation form for `a + b`, or null (HB compose_hebrew).
40
+ /**
41
+ * Precomposed Hebrew presentation form for `a + b`, or null (HB compose_hebrew).
42
+ * @param {number} a
43
+ * @param {number} b
44
+ * @returns {number | null}
45
+ */
30
46
  function composeHebrewPair(a, b) {
31
47
  switch (b) {
32
48
  case 0x05B4: // HIRIQ
@@ -53,10 +69,14 @@ function composeHebrewPair(a, b) {
53
69
  }
54
70
  }
55
71
 
56
- // Greedily compose adjacent pairs when the font has the glyph
57
- // (e.g. SHIN + SHIN_DOT FB2A, then + DAGESH FB2C).
72
+ /**
73
+ * Greedily compose adjacent pairs when the font has the glyph
74
+ * (e.g. SHIN + SHIN_DOT → FB2A, then + DAGESH → FB2C).
75
+ * @param {GlyphInfoLike[]} glyphs
76
+ * @param {LayoutFont} font
77
+ */
58
78
  function composeHebrew(glyphs, font) {
59
- for (let i = 0; i + 1 < glyphs.length; ) {
79
+ for (let i = 0; i + 1 < glyphs.length;) {
60
80
  let composed = glyphs[i].codePoints[0];
61
81
  let consumed = 1;
62
82
  while (i + consumed < glyphs.length) {
@@ -66,26 +86,35 @@ function composeHebrew(glyphs, font) {
66
86
  consumed++;
67
87
  }
68
88
  if (consumed > 1) {
89
+ let composedGlyph = font.glyphForCodePoint(composed);
90
+ if (!composedGlyph) {
91
+ i++;
92
+ continue;
93
+ }
94
+ /** @type {number[]} */
69
95
  let cps = [];
70
96
  for (let j = 0; j < consumed; j++) cps.push(...glyphs[i + j].codePoints);
71
- glyphs[i] = new GlyphInfo(font, font.glyphForCodePoint(composed).id, cps, glyphs[i].features);
97
+ glyphs[i] = new GlyphInfo(font, composedGlyph.id, cps, glyphs[i].features);
72
98
  glyphs.splice(i + 1, consumed - 1);
73
99
  }
74
100
  i++;
75
101
  }
76
102
  }
77
103
 
78
- // HB reorder_marks_hebrew: [patah/qamats, sheva/hiriq, meteg/below] → swap last two.
79
- // unicode-properties already exposes Hebrew modified CCCs (CCC10/14/17/18/22).
104
+ /**
105
+ * HB reorder_marks_hebrew: [patah/qamats, sheva/hiriq, meteg/below] → swap last two.
106
+ * unicode-properties already exposes Hebrew modified CCCs (CCC10/14/17/18/22).
107
+ * @param {GlyphInfoLike[]} glyphs
108
+ */
80
109
  function reorderMarksHebrew(glyphs) {
81
110
  for (let i = 2; i < glyphs.length; i++) {
82
111
  let c0 = getCombiningClass(glyphs[i - 2].codePoints[0]);
83
112
  let c1 = getCombiningClass(glyphs[i - 1].codePoints[0]);
84
113
  let c2 = getCombiningClass(glyphs[i].codePoints[0]);
85
114
  if (
86
- (c0 === 'CCC17' || c0 === 'CCC18') &&
87
- (c1 === 'CCC10' || c1 === 'CCC14') &&
88
- (c2 === 'CCC22' || c2 === 'Below')
115
+ (c0 === 'CCC17' || c0 === 'CCC18')
116
+ && (c1 === 'CCC10' || c1 === 'CCC14')
117
+ && (c2 === 'CCC22' || c2 === 'Below')
89
118
  ) {
90
119
  [glyphs[i - 1], glyphs[i]] = [glyphs[i], glyphs[i - 1]];
91
120
  break;
@@ -93,13 +122,18 @@ function reorderMarksHebrew(glyphs) {
93
122
  }
94
123
  }
95
124
 
96
- // Gate fallback composition on Hebrew-script GPOS `mark` (not the global featureList).
125
+ /**
126
+ * Gate fallback composition on Hebrew-script GPOS `mark` (not the global featureList).
127
+ * @param {LayoutFont} font
128
+ * @returns {boolean}
129
+ */
97
130
  function hasGposMark(font) {
98
- let hebr = font.GPOS?.scriptList?.find(e => e.tag === 'hebr')?.script;
131
+ let hebr = font.GPOS?.scriptList?.find(/** @param {OTScriptRecord} e */ e => e.tag === 'hebr')?.script;
99
132
  if (!hebr) return false;
100
133
 
101
- let langs = [hebr.defaultLangSys, ...(hebr.langSysRecords || []).map(l => l.langSys)].filter(Boolean);
102
- return langs.some(ls =>
103
- (ls.featureIndexes || []).some(i => font.GPOS.featureList[i]?.tag === 'mark')
134
+ let langs = [hebr.defaultLangSys, ...(hebr.langSysRecords || []).map(/** @param {OTLangSysRecord} l */ l => l.langSys)]
135
+ .filter(/** @returns {value is OTLangSys} */ (/** @type {OTLangSys | null} */ value) => value != null);
136
+ return langs.some(/** @param {OTLangSys} ls */ ls =>
137
+ (ls.featureIndexes || []).some(/** @param {number} i */ i => font.GPOS?.featureList[i]?.tag === 'mark')
104
138
  );
105
139
  }
@@ -1,7 +1,7 @@
1
1
  import DefaultShaper from './DefaultShaper';
2
2
  import StateMachine from 'dfa';
3
3
  import UnicodeTrie from '../../packages/unicode-trie/index.js';
4
- import {getCategory} from '../../packages/unicode-properties/index.js';
4
+ import { getCategory } from '../../packages/unicode-properties/index.js';
5
5
  import * as Script from '../../layout/Script';
6
6
  import GlyphInfo from '../GlyphInfo';
7
7
  import indicMachine from './indic.json';
@@ -17,9 +17,24 @@ import {
17
17
  import { decodeBase64 } from '../../utils';
18
18
  import indicTrie from './indic.trie';
19
19
 
20
- const {decompositions} = useData;
20
+ /** @typedef {import('../../../types/fontkit').ShapingPlanLike} ShapingPlanLike */
21
+ /** @typedef {import('../../../types/fontkit').GlyphInfoLike} GlyphInfoLike */
22
+ /** @typedef {import('../../../types/fontkit').IndicGlyphInfo} IndicGlyphInfo */
23
+ /** @typedef {import('../../../types/fontkit').LayoutFont} LayoutFont */
24
+ /** @typedef {import('../../../types/fontkit').IndicConfig} IndicConfig */
25
+ /** @typedef {import('../../../types/fontkit').IndicShaperInfo} IndicShaperInfo */
26
+ /** @typedef {import('../../../types/fontkit').ShapingStageFn} ShapingStageFn */
27
+ /** @typedef {import('dfa').StateMachineDefinition} StateMachineDefinition */
28
+
29
+ /**
30
+ * @typedef {{ decompositions: Record<string, number[]> }} UseDataPartial
31
+ */
32
+
33
+ /** @type {UseDataPartial} */
34
+ const useDataTyped = /** @type {UseDataPartial} */ (useData);
35
+ const { decompositions } = useDataTyped;
21
36
  const trie = new UnicodeTrie(decodeBase64(indicTrie));
22
- const stateMachine = new StateMachine(indicMachine);
37
+ const stateMachine = new StateMachine(/** @type {StateMachineDefinition} */ (indicMachine));
23
38
 
24
39
  /**
25
40
  * The IndicShaper supports indic scripts e.g. Devanagari, Kannada, etc.
@@ -27,6 +42,10 @@ const stateMachine = new StateMachine(indicMachine);
27
42
  */
28
43
  export default class IndicShaper extends DefaultShaper {
29
44
  static zeroMarkWidths = 'NONE';
45
+
46
+ /**
47
+ * @param {ShapingPlanLike} plan
48
+ */
30
49
  static planFeatures(plan) {
31
50
  plan.addStage(setupSyllables);
32
51
 
@@ -55,24 +74,39 @@ export default class IndicShaper extends DefaultShaper {
55
74
  });
56
75
 
57
76
  // Setup the indic config for the selected script
58
- plan.unicodeScript = Script.fromOpenType(plan.script);
59
- plan.indicConfig = INDIC_CONFIGS[plan.unicodeScript] || INDIC_CONFIGS.Default;
60
- plan.isOldSpec = plan.indicConfig.hasOldSpec && plan.script[plan.script.length - 1] !== '2';
77
+ let scriptTag = Array.isArray(plan.script) ? plan.script[0] : plan.script;
78
+ plan.unicodeScript = scriptTag != null ? Script.fromOpenType(scriptTag) : undefined;
79
+ /** @type {Record<string, IndicConfig>} */
80
+ let configs = INDIC_CONFIGS;
81
+ plan.indicConfig = (plan.unicodeScript && configs[plan.unicodeScript]) || configs.Default;
82
+ let scriptKey = Array.isArray(plan.script)
83
+ ? plan.script[plan.script.length - 1]
84
+ : plan.script;
85
+ plan.isOldSpec = !!(plan.indicConfig.hasOldSpec && scriptKey != null && scriptKey[scriptKey.length - 1] !== '2');
61
86
 
62
87
  // TODO: turn off kern (Khmer) and liga features.
63
88
  }
64
89
 
90
+ /**
91
+ * @param {ShapingPlanLike} plan
92
+ * @param {GlyphInfoLike[]} glyphs
93
+ */
65
94
  static assignFeatures(plan, glyphs) {
66
95
  // Decompose split matras
67
96
  // TODO: do this in a more general unicode normalizer
68
97
  for (let i = glyphs.length - 1; i >= 0; i--) {
69
98
  let codepoint = glyphs[i].codePoints[0];
70
- let d = INDIC_DECOMPOSITIONS[codepoint] || decompositions[codepoint];
99
+ let d = /** @type {Record<number, number[]>} */ (INDIC_DECOMPOSITIONS)[codepoint]
100
+ || decompositions[String(codepoint)];
71
101
  if (d) {
72
- let decomposed = d.map(c => {
102
+ /** @type {GlyphInfo[]} */
103
+ let decomposed = [];
104
+ for (let c of d) {
73
105
  let g = plan.font.glyphForCodePoint(c);
74
- return new GlyphInfo(plan.font, g.id, [c], glyphs[i].features);
75
- });
106
+ if (g) {
107
+ decomposed.push(new GlyphInfo(plan.font, g.id, [c], glyphs[i].features));
108
+ }
109
+ }
76
110
 
77
111
  glyphs.splice(i, 1, ...decomposed);
78
112
  }
@@ -80,23 +114,63 @@ export default class IndicShaper extends DefaultShaper {
80
114
  }
81
115
  }
82
116
 
117
+ /**
118
+ * @param {GlyphInfoLike} glyph
119
+ * @returns {number}
120
+ */
83
121
  function indicCategory(glyph) {
84
122
  return trie.get(glyph.codePoints[0]) >> 8;
85
123
  }
86
124
 
125
+ /**
126
+ * @param {GlyphInfoLike} glyph
127
+ * @returns {number}
128
+ */
87
129
  function indicPosition(glyph) {
88
130
  return 1 << (trie.get(glyph.codePoints[0]) & 0xff);
89
131
  }
90
132
 
91
133
  class IndicInfo {
134
+ /**
135
+ * @param {number} category
136
+ * @param {number} position
137
+ * @param {string} syllableType
138
+ * @param {number} syllable
139
+ */
92
140
  constructor(category, position, syllableType, syllable) {
141
+ /** @type {number} */
93
142
  this.category = category;
143
+ /** @type {number} */
94
144
  this.position = position;
145
+ /** @type {string} */
95
146
  this.syllableType = syllableType;
147
+ /** @type {number} */
96
148
  this.syllable = syllable;
97
149
  }
98
150
  }
99
151
 
152
+ /**
153
+ * @param {import('../../../types/fontkit').IndicShaperInfo | import('../../../types/fontkit').USEShaperInfo | null} info
154
+ * @returns {info is IndicShaperInfo}
155
+ */
156
+ function isIndicShaperInfo(info) {
157
+ // Discriminate on category kind: Indic uses numeric bitflags, USE uses strings.
158
+ return info != null && typeof info.category === 'number';
159
+ }
160
+
161
+ /**
162
+ * @param {GlyphInfoLike} glyph
163
+ * @returns {IndicShaperInfo}
164
+ */
165
+ function indicInfo(glyph) {
166
+ let info = glyph.shaperInfo;
167
+ if (!isIndicShaperInfo(info)) {
168
+ throw new Error('Expected Indic shaperInfo');
169
+ }
170
+ return info;
171
+ }
172
+
173
+ /** @type {ShapingStageFn} */
100
174
  function setupSyllables(font, glyphs) {
101
175
  let syllable = 0;
102
176
  let last = 0;
@@ -131,29 +205,55 @@ function setupSyllables(font, glyphs) {
131
205
  }
132
206
  }
133
207
 
208
+ /**
209
+ * @param {GlyphInfoLike} glyph
210
+ * @returns {number}
211
+ */
134
212
  function isConsonant(glyph) {
135
- return glyph.shaperInfo.category & CONSONANT_FLAGS;
213
+ return indicInfo(glyph).category & CONSONANT_FLAGS;
136
214
  }
137
215
 
216
+ /**
217
+ * @param {GlyphInfoLike} glyph
218
+ * @returns {number}
219
+ */
138
220
  function isJoiner(glyph) {
139
- return glyph.shaperInfo.category & JOINER_FLAGS;
221
+ return indicInfo(glyph).category & JOINER_FLAGS;
140
222
  }
141
223
 
224
+ /**
225
+ * @param {GlyphInfoLike} glyph
226
+ * @returns {number}
227
+ */
142
228
  function isHalantOrCoeng(glyph) {
143
- return glyph.shaperInfo.category & HALANT_OR_COENG_FLAGS;
229
+ return indicInfo(glyph).category & HALANT_OR_COENG_FLAGS;
144
230
  }
145
231
 
232
+ /**
233
+ * @param {GlyphInfoLike[]} glyphs
234
+ * @param {string} feature
235
+ * @returns {boolean}
236
+ */
146
237
  function wouldSubstitute(glyphs, feature) {
147
238
  for (let glyph of glyphs) {
148
- glyph.features = {[feature]: true};
239
+ glyph.features = { [feature]: true };
149
240
  }
150
241
 
151
- let GSUB = glyphs[0]._font._layoutEngine.engine.GSUBProcessor;
152
- GSUB.applyFeatures([feature], glyphs);
242
+ let engine = glyphs[0]._font._layoutEngine.engine;
243
+ if (!engine?.GSUBProcessor) {
244
+ return false;
245
+ }
246
+ engine.GSUBProcessor.applyFeatures([feature], glyphs);
153
247
 
154
248
  return glyphs.length === 1;
155
249
  }
156
250
 
251
+ /**
252
+ * @param {LayoutFont} font
253
+ * @param {GlyphInfoLike} consonant
254
+ * @param {GlyphInfoLike} virama
255
+ * @returns {number}
256
+ */
157
257
  function consonantPosition(font, consonant, virama) {
158
258
  let glyphs = [virama, consonant, virama];
159
259
  if (wouldSubstitute(glyphs.slice(0, 2), 'blwf') || wouldSubstitute(glyphs.slice(1, 3), 'blwf')) {
@@ -167,12 +267,18 @@ function consonantPosition(font, consonant, virama) {
167
267
  return POSITIONS.Base_C;
168
268
  }
169
269
 
170
- function initialReordering(font, glyphs, plan) {
171
- let indicConfig = plan.indicConfig;
172
- let features = font._layoutEngine.engine.GSUBProcessor.features;
173
-
174
- let dottedCircle = font.glyphForCodePoint(0x25cc).id;
175
- let virama = font.glyphForCodePoint(indicConfig.virama).id;
270
+ /** @type {ShapingStageFn} */
271
+ function initialReordering(font, glyphsArg, plan) {
272
+ // setupSyllables always attaches IndicShaperInfo before this stage.
273
+ const glyphs = /** @type {IndicGlyphInfo[]} */ (glyphsArg);
274
+ let indicConfig = /** @type {IndicConfig} */ (plan.indicConfig);
275
+ let engine = font._layoutEngine.engine;
276
+ let features = engine?.GSUBProcessor ? engine.GSUBProcessor.features : {};
277
+
278
+ let dottedCircleGlyph = font.glyphForCodePoint(0x25cc);
279
+ let dottedCircle = dottedCircleGlyph ? dottedCircleGlyph.id : 0;
280
+ let viramaGlyph = font.glyphForCodePoint(indicConfig.virama);
281
+ let virama = viramaGlyph ? viramaGlyph.id : 0;
176
282
  if (virama) {
177
283
  let info = new GlyphInfo(font, virama, [indicConfig.virama]);
178
284
  for (let i = 0; i < glyphs.length; i++) {
@@ -183,14 +289,14 @@ function initialReordering(font, glyphs, plan) {
183
289
  }
184
290
 
185
291
  for (let start = 0, end = nextSyllable(glyphs, 0); start < glyphs.length; start = end, end = nextSyllable(glyphs, start)) {
186
- let {syllableType} = glyphs[start].shaperInfo;
292
+ let { syllableType } = glyphs[start].shaperInfo;
187
293
 
188
294
  if (syllableType === 'symbol_cluster' || syllableType === 'non_indic_cluster') {
189
295
  continue;
190
296
  }
191
297
 
192
298
  if (syllableType === 'broken_cluster' && dottedCircle) {
193
- let g = new GlyphInfo(font, dottedCircle, [0x25cc]);
299
+ let g = /** @type {IndicGlyphInfo} */ (new GlyphInfo(font, dottedCircle, [0x25cc]));
194
300
  g.shaperInfo = new IndicInfo(
195
301
  1 << indicCategory(g),
196
302
  indicPosition(g),
@@ -224,12 +330,12 @@ function initialReordering(font, glyphs, plan) {
224
330
  // If the syllable starts with Ra + Halant (in a script that has Reph)
225
331
  // and has more than one consonant, Ra is excluded from candidates for
226
332
  // base consonants.
227
- if (indicConfig.rephPos !== POSITIONS.Ra_To_Become_Reph &&
228
- features.rphf &&
229
- start + 3 <= end && (
230
- (indicConfig.rephMode === 'Implicit' && !isJoiner(glyphs[start + 2])) ||
231
- (indicConfig.rephMode === 'Explicit' && glyphs[start + 2].shaperInfo.category === CATEGORIES.ZWJ)
232
- )
333
+ if (indicConfig.rephPos !== POSITIONS.Ra_To_Become_Reph
334
+ && features.rphf
335
+ && start + 3 <= end && (
336
+ (indicConfig.rephMode === 'Implicit' && !isJoiner(glyphs[start + 2]))
337
+ || (indicConfig.rephMode === 'Explicit' && glyphs[start + 2].shaperInfo.category === CATEGORIES.ZWJ)
338
+ )
233
339
  ) {
234
340
  // See if it matches the 'rphf' feature.
235
341
  let g = [glyphs[start].copy(), glyphs[start + 1].copy(), glyphs[start + 2].copy()];
@@ -510,9 +616,9 @@ function initialReordering(font, glyphs, plan) {
510
616
  //
511
617
  // Test case: U+0924,U+094D,U+0930,U+094d,U+200D,U+0915
512
618
  for (let i = start; i + 1 < base; i++) {
513
- if (glyphs[i].shaperInfo.category === CATEGORIES.Ra &&
514
- glyphs[i + 1].shaperInfo.category === CATEGORIES.H &&
515
- (i + 1 === base || glyphs[i + 2].shaperInfo.category === CATEGORIES.ZWJ)
619
+ if (glyphs[i].shaperInfo.category === CATEGORIES.Ra
620
+ && glyphs[i + 1].shaperInfo.category === CATEGORIES.H
621
+ && (i + 1 === base || glyphs[i + 2].shaperInfo.category === CATEGORIES.ZWJ)
516
622
  ) {
517
623
  glyphs[i].features.blwf = true;
518
624
  glyphs[i + 1].features.blwf = true;
@@ -569,9 +675,13 @@ function initialReordering(font, glyphs, plan) {
569
675
  }
570
676
  }
571
677
 
572
- function finalReordering(font, glyphs, plan) {
573
- let indicConfig = plan.indicConfig;
574
- let features = font._layoutEngine.engine.GSUBProcessor.features;
678
+ /** @type {ShapingStageFn} */
679
+ function finalReordering(font, glyphsArg, plan) {
680
+ // setupSyllables / initialReordering always attach IndicShaperInfo before this stage.
681
+ const glyphs = /** @type {IndicGlyphInfo[]} */ (glyphsArg);
682
+ let indicConfig = /** @type {IndicConfig} */ (plan.indicConfig);
683
+ let engine = font._layoutEngine.engine;
684
+ let features = engine?.GSUBProcessor ? engine.GSUBProcessor.features : {};
575
685
 
576
686
  for (let start = 0, end = nextSyllable(glyphs, 0); start < glyphs.length; start = end, end = nextSyllable(glyphs, start)) {
577
687
  // 4. Final reordering:
@@ -597,7 +707,7 @@ function finalReordering(font, glyphs, plan) {
597
707
  while (base < end && isHalantOrCoeng(glyphs[base])) {
598
708
  base++;
599
709
  }
600
- glyphs[base].shaperInfo.position = POSITIONS.BASE_C;
710
+ glyphs[base].shaperInfo.position = POSITIONS.Base_C;
601
711
  tryPref = false;
602
712
  }
603
713
  break;
@@ -715,11 +825,11 @@ function finalReordering(font, glyphs, plan) {
715
825
  // - If repha is encoded separately and in the logical position, we should only
716
826
  // move it if it did NOT ligate. If it ligated, it's probably the font trying
717
827
  // to make it work without the reordering.
718
- if (start + 1 < end &&
719
- glyphs[start].shaperInfo.position === POSITIONS.Ra_To_Become_Reph &&
720
- (glyphs[start].shaperInfo.category === CATEGORIES.Repha) !== (glyphs[start].isLigated && !glyphs[start].isMultiplied)
828
+ if (start + 1 < end
829
+ && glyphs[start].shaperInfo.position === POSITIONS.Ra_To_Become_Reph
830
+ && (glyphs[start].shaperInfo.category === CATEGORIES.Repha) !== (glyphs[start].isLigated && !glyphs[start].isMultiplied)
721
831
  ) {
722
- let newRephPos;
832
+ let newRephPos = end - 1;
723
833
  let rephPos = indicConfig.rephPos;
724
834
  let found = false;
725
835
 
@@ -838,9 +948,9 @@ function finalReordering(font, glyphs, plan) {
838
948
  if (tryPref && base + 1 < end) {
839
949
  for (let i = base + 1; i < end; i++) {
840
950
  if (glyphs[i].features.pref) {
841
- // 1. Only reorder a glyph produced by substitution during application
842
- // of the <pref> feature. (Note that a font may shape a Ra consonant with
843
- // the feature generally but block it in certain contexts.)
951
+ // 1. Only reorder a glyph produced by substitution during application
952
+ // of the <pref> feature. (Note that a font may shape a Ra consonant with
953
+ // the feature generally but block it in certain contexts.)
844
954
 
845
955
  // Note: We just check that something got substituted. We don't check that
846
956
  // the <pref> feature actually did it...
@@ -906,6 +1016,11 @@ function finalReordering(font, glyphs, plan) {
906
1016
  }
907
1017
  }
908
1018
 
1019
+ /**
1020
+ * @param {IndicGlyphInfo[]} glyphs
1021
+ * @param {number} start
1022
+ * @returns {number}
1023
+ */
909
1024
  function nextSyllable(glyphs, start) {
910
1025
  if (start >= glyphs.length) return start;
911
1026
  let syllable = glyphs[start].shaperInfo.syllable;
@@ -1,12 +1,24 @@
1
1
  import DefaultShaper from './DefaultShaper';
2
2
  import GlyphInfo from '../GlyphInfo';
3
3
 
4
+ /** @typedef {import('../../../types/fontkit').ShapingPlanLike} ShapingPlanLike */
5
+ /** @typedef {import('../../../types/fontkit').GlyphInfoLike} GlyphInfoLike */
6
+ /** @typedef {import('../../../types/fontkit').LayoutFont} LayoutFont */
7
+ /** @typedef {import('../../../types/fontkit').FeatureMap} FeatureMap */
8
+ /** @typedef {import('../../../types/fontkit').FeatureInput} FeatureInput */
9
+ /** @typedef {import('../../../types/fontkit').ScriptTag} ScriptTag */
10
+ /** @typedef {import('../../../types/fontkit').OTScriptRecord} OTScriptRecord */
11
+
4
12
  /**
5
13
  * Thai / Lao shaper (HarfBuzz hb-ot-shaper-thai.cc):
6
14
  * 1. Decompose SARA AM → NIKHAHIT + SARA AA and reorder NIKHAHIT past above marks
7
15
  * 2. PUA tone/vowel shift fallback for legacy fonts without Thai GSUB
8
16
  */
9
17
  export default class ThaiShaper extends DefaultShaper {
18
+ /**
19
+ * @param {ShapingPlanLike} plan
20
+ * @param {GlyphInfoLike[]} glyphs
21
+ */
10
22
  static assignFeatures(plan, glyphs) {
11
23
  super.assignFeatures(plan, glyphs);
12
24
  preprocessThai(glyphs, plan.font);
@@ -18,19 +30,23 @@ export default class ThaiShaper extends DefaultShaper {
18
30
  }
19
31
 
20
32
  // Thai/Lao SARA AM differ only by the 0x80 bit (U+0E33 / U+0EB3).
33
+ /** @param {number} u @returns {boolean} */
21
34
  function isSaraAm(u) {
22
35
  return (u & ~0x0080) === 0x0E33;
23
36
  }
24
37
 
38
+ /** @param {number} u @returns {number} */
25
39
  function nikhahitFromSaraAm(u) {
26
40
  return u - 0x0E33 + 0x0E4D;
27
41
  }
28
42
 
43
+ /** @param {number} u @returns {number} */
29
44
  function saraAaFromSaraAm(u) {
30
45
  return u - 1;
31
46
  }
32
47
 
33
48
  // Above-base marks (Thai; Lao is the same set with +0x80).
49
+ /** @param {number} u @returns {boolean} */
34
50
  function isAboveBaseMark(u) {
35
51
  const c = u & ~0x0080;
36
52
  return c === 0x0E31
@@ -39,6 +55,7 @@ function isAboveBaseMark(u) {
39
55
  || c === 0x0E3B;
40
56
  }
41
57
 
58
+ /** @param {GlyphInfoLike[]} glyphs @param {LayoutFont} font */
42
59
  function preprocessThai(glyphs, font) {
43
60
  let i = 0;
44
61
  while (i < glyphs.length) {
@@ -68,9 +85,11 @@ function preprocessThai(glyphs, font) {
68
85
  }
69
86
  }
70
87
 
88
+ /** @param {LayoutFont} font @param {number} codePoint @param {FeatureMap | FeatureInput | null | undefined} features @returns {GlyphInfo} */
71
89
  function makeGlyph(font, codePoint, features) {
72
- const id = font.glyphForCodePoint(codePoint).id;
73
- return new GlyphInfo(font, id, [codePoint], features);
90
+ const g = font.glyphForCodePoint(codePoint);
91
+ // .notdef when the mapped code point is somehow missing from the font.
92
+ return new GlyphInfo(font, g ? g.id : 0, [codePoint], features);
74
93
  }
75
94
 
76
95
  // PUA fallback: above/below state machines remap marks (and some bases) to
@@ -89,9 +108,10 @@ const NOT_CONSONANT = 4;
89
108
 
90
109
  const AV = 0; // above-base vowel/mark
91
110
  const BV = 1; // below-base vowel/mark
92
- const T = 2; // tone mark
111
+ const T = 2; // tone mark
93
112
  const NOT_MARK = 3;
94
113
 
114
+ /** @param {number} u @returns {number} */
95
115
  function getConsonantType(u) {
96
116
  if (u === 0x0E1B || u === 0x0E1D || u === 0x0E1F) return AC;
97
117
  if (u === 0x0E0D || u === 0x0E10) return RC;
@@ -100,12 +120,13 @@ function getConsonantType(u) {
100
120
  return NOT_CONSONANT;
101
121
  }
102
122
 
123
+ /** @param {number} u @returns {number} */
103
124
  function getMarkType(u) {
104
125
  if (
105
- u === 0x0E31 ||
106
- (u >= 0x0E34 && u <= 0x0E37) ||
107
- u === 0x0E47 ||
108
- (u >= 0x0E4D && u <= 0x0E4E)
126
+ u === 0x0E31
127
+ || (u >= 0x0E34 && u <= 0x0E37)
128
+ || u === 0x0E47
129
+ || (u >= 0x0E4D && u <= 0x0E4E)
109
130
  ) {
110
131
  return AV;
111
132
  }
@@ -116,24 +137,27 @@ function getMarkType(u) {
116
137
 
117
138
  const T0 = 0, T1 = 1, T2 = 2, T3 = 3;
118
139
  const ABOVE_START_STATE = [T0, T1, T0, T0, T3]; // NC AC RC DC NOT_CONSONANT
140
+ /** @type {[number, number][][]} */
119
141
  const ABOVE_STATE_MACHINE = [
120
142
  // AV BV T
121
- [[NOP, T3], [NOP, T0], [SD, T3]], // T0
122
- [[SL, T2], [NOP, T1], [SDL, T2]], // T1
123
- [[NOP, T3], [NOP, T2], [SL, T3]], // T2
124
- [[NOP, T3], [NOP, T3], [NOP, T3]] // T3
143
+ [[NOP, T3], [NOP, T0], [SD, T3]], // T0
144
+ [[SL, T2], [NOP, T1], [SDL, T2]], // T1
145
+ [[NOP, T3], [NOP, T2], [SL, T3]], // T2
146
+ [[NOP, T3], [NOP, T3], [NOP, T3]] // T3
125
147
  ];
126
148
 
127
149
  const B0 = 0, B1 = 1, B2 = 2;
128
150
  const BELOW_START_STATE = [B0, B0, B1, B2, B2];
151
+ /** @type {[number, number][][]} */
129
152
  const BELOW_STATE_MACHINE = [
130
153
  // AV BV T
131
- [[NOP, B0], [NOP, B2], [NOP, B0]], // B0
132
- [[NOP, B1], [RD, B2], [NOP, B1]], // B1
133
- [[NOP, B2], [SD, B2], [NOP, B2]] // B2
154
+ [[NOP, B0], [NOP, B2], [NOP, B0]], // B0
155
+ [[NOP, B1], [RD, B2], [NOP, B1]], // B1
156
+ [[NOP, B2], [SD, B2], [NOP, B2]] // B2
134
157
  ];
135
158
 
136
159
  // [original, Windows PUA, Mac PUA] per action
160
+ /** @type {Record<number, number[][]>} */
137
161
  const PUA_MAPPINGS = {
138
162
  [SD]: [
139
163
  [0x0E48, 0xF70A, 0xF88B], // MAI EK
@@ -143,7 +167,7 @@ const PUA_MAPPINGS = {
143
167
  [0x0E4C, 0xF70E, 0xF897], // THANTHAKHAT
144
168
  [0x0E38, 0xF718, 0xF89B], // SARA U
145
169
  [0x0E39, 0xF719, 0xF89C], // SARA UU
146
- [0x0E3A, 0xF71A, 0xF89D] // PHINTHU
170
+ [0x0E3A, 0xF71A, 0xF89D] // PHINTHU
147
171
  ],
148
172
  [SDL]: [
149
173
  [0x0E48, 0xF705, 0xF88C],
@@ -164,14 +188,15 @@ const PUA_MAPPINGS = {
164
188
  [0x0E36, 0xF703, 0xF887], // SARA UE
165
189
  [0x0E37, 0xF704, 0xF888], // SARA UEE
166
190
  [0x0E47, 0xF712, 0xF889], // MAITAIKHU
167
- [0x0E4D, 0xF711, 0xF899] // NIKHAHIT
191
+ [0x0E4D, 0xF711, 0xF899] // NIKHAHIT
168
192
  ],
169
193
  [RD]: [
170
194
  [0x0E0D, 0xF70F, 0xF89A], // YO YING
171
- [0x0E10, 0xF700, 0xF89E] // THO THAN
195
+ [0x0E10, 0xF700, 0xF89E] // THO THAN
172
196
  ]
173
197
  };
174
198
 
199
+ /** @param {number} u @param {number} action @param {LayoutFont} font @returns {number} */
175
200
  function thaiPuaShape(u, action, font) {
176
201
  if (action === NOP) return u;
177
202
  const mappings = PUA_MAPPINGS[action];
@@ -185,12 +210,16 @@ function thaiPuaShape(u, action, font) {
185
210
  return u;
186
211
  }
187
212
 
213
+ /** @param {GlyphInfoLike[]} glyphs @param {number} index @param {number} newCp @param {LayoutFont} font */
188
214
  function replaceGlyphCodePoint(glyphs, index, newCp, font) {
189
215
  const prev = glyphs[index];
190
216
  if (prev.codePoints[0] === newCp) return;
191
- glyphs[index] = new GlyphInfo(font, font.glyphForCodePoint(newCp).id, [newCp], prev.features);
217
+ const g = font.glyphForCodePoint(newCp);
218
+ if (!g) return;
219
+ glyphs[index] = new GlyphInfo(font, g.id, [newCp], prev.features);
192
220
  }
193
221
 
222
+ /** @param {GlyphInfoLike[]} glyphs @param {LayoutFont} font */
194
223
  function applyThaiPuaShaping(glyphs, font) {
195
224
  let aboveState = ABOVE_START_STATE[NOT_CONSONANT];
196
225
  let belowState = BELOW_START_STATE[NOT_CONSONANT];
@@ -226,12 +255,14 @@ function applyThaiPuaShaping(glyphs, font) {
226
255
  }
227
256
 
228
257
  // Gate PUA shaping on absence of Thai GSUB (HB plan->map.found_script[0]).
258
+ /** @param {LayoutFont} font @returns {boolean} */
229
259
  function hasThaiGsub(font) {
230
260
  const gsub = font.GSUB;
231
261
  if (!gsub || !gsub.scriptList) return false;
232
- return gsub.scriptList.some(entry => entry.tag === 'thai' || entry.tag === 'tha2');
262
+ return gsub.scriptList.some(/** @param {OTScriptRecord} entry */ entry => entry.tag === 'thai' || entry.tag === 'tha2');
233
263
  }
234
264
 
265
+ /** @param {ScriptTag | string[] | null | undefined} script @returns {boolean} */
235
266
  function isThaiBufferScript(script) {
236
267
  if (Array.isArray(script)) return script.includes('thai');
237
268
  return script === 'thai';