@jasy/pdf 1.0.0-alpha.1 → 1.0.0-alpha.11

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 (279) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +35 -21
  3. package/dist/api/args.d.ts +1 -1
  4. package/dist/api/args.js +2 -5
  5. package/dist/api/color.d.ts +4 -4
  6. package/dist/api/color.js +11 -17
  7. package/dist/api/content.d.ts +20 -13
  8. package/dist/api/content.js +44 -29
  9. package/dist/api/descriptor.d.ts +2 -2
  10. package/dist/api/descriptor.js +121 -31
  11. package/dist/api/dimension.d.ts +76 -0
  12. package/dist/api/dimension.js +43 -0
  13. package/dist/api/font-url.d.ts +28 -0
  14. package/dist/api/font-url.js +134 -0
  15. package/dist/api/forms.d.ts +347 -0
  16. package/dist/api/forms.js +189 -0
  17. package/dist/api/gradient.d.ts +53 -0
  18. package/dist/api/gradient.js +97 -0
  19. package/dist/api/index.d.ts +13 -8
  20. package/dist/api/index.js +13 -24
  21. package/dist/api/insets.d.ts +19 -12
  22. package/dist/api/insets.js +19 -12
  23. package/dist/api/layout.d.ts +149 -25
  24. package/dist/api/layout.js +182 -51
  25. package/dist/api/page-builder.d.ts +40 -0
  26. package/dist/api/page-builder.js +44 -0
  27. package/dist/api/structure.d.ts +107 -13
  28. package/dist/api/structure.js +187 -88
  29. package/dist/api/table.d.ts +5 -5
  30. package/dist/api/table.js +38 -26
  31. package/dist/api/text.d.ts +90 -4
  32. package/dist/api/text.js +116 -31
  33. package/dist/assets/font-data.d.ts +2 -0
  34. package/dist/assets/font-data.js +21 -0
  35. package/dist/assets/font-data.ts +37 -0
  36. package/dist/common/color.js +1 -5
  37. package/dist/constants/page-sizes.js +3 -6
  38. package/dist/constants/pdf-parts.js +1 -4
  39. package/dist/crypto/legacy-handler.d.ts +43 -0
  40. package/dist/crypto/legacy-handler.js +145 -0
  41. package/dist/crypto/security-handler.d.ts +62 -0
  42. package/dist/crypto/security-handler.js +155 -0
  43. package/dist/crypto/webcrypto.d.ts +11 -0
  44. package/dist/crypto/webcrypto.js +62 -0
  45. package/dist/edit/acroform-reader.d.ts +70 -0
  46. package/dist/edit/acroform-reader.js +126 -0
  47. package/dist/edit/appearance.d.ts +47 -0
  48. package/dist/edit/appearance.js +171 -0
  49. package/dist/edit/document.d.ts +145 -0
  50. package/dist/edit/document.js +642 -0
  51. package/dist/edit/fill.d.ts +65 -0
  52. package/dist/edit/fill.js +424 -0
  53. package/dist/edit/flatten.d.ts +54 -0
  54. package/dist/edit/flatten.js +462 -0
  55. package/dist/edit/index.d.ts +20 -0
  56. package/dist/edit/index.js +18 -0
  57. package/dist/edit/lexer.d.ts +30 -0
  58. package/dist/edit/lexer.js +285 -0
  59. package/dist/edit/objects.d.ts +68 -0
  60. package/dist/edit/objects.js +83 -0
  61. package/dist/edit/writer.d.ts +60 -0
  62. package/dist/edit/writer.js +283 -0
  63. package/dist/elements/container-element.d.ts +50 -5
  64. package/dist/elements/container-element.js +100 -39
  65. package/dist/elements/forms/checkbox-element.d.ts +51 -0
  66. package/dist/elements/forms/checkbox-element.js +48 -0
  67. package/dist/elements/forms/choice-element.d.ts +53 -0
  68. package/dist/elements/forms/choice-element.js +59 -0
  69. package/dist/elements/forms/push-button-element.d.ts +56 -0
  70. package/dist/elements/forms/push-button-element.js +49 -0
  71. package/dist/elements/forms/radio-element.d.ts +52 -0
  72. package/dist/elements/forms/radio-element.js +49 -0
  73. package/dist/elements/forms/signature-element.d.ts +54 -0
  74. package/dist/elements/forms/signature-element.js +47 -0
  75. package/dist/elements/forms/text-field-element.d.ts +61 -0
  76. package/dist/elements/forms/text-field-element.js +54 -0
  77. package/dist/elements/image-element.d.ts +58 -6
  78. package/dist/elements/image-element.js +145 -111
  79. package/dist/elements/index.d.ts +26 -10
  80. package/dist/elements/index.js +26 -28
  81. package/dist/elements/layout/anchor-element.d.ts +23 -0
  82. package/dist/elements/layout/anchor-element.js +27 -0
  83. package/dist/elements/layout/bookmark-element.d.ts +27 -0
  84. package/dist/elements/layout/bookmark-element.js +30 -0
  85. package/dist/elements/layout/default-text-style-element.d.ts +30 -0
  86. package/dist/elements/layout/default-text-style-element.js +47 -0
  87. package/dist/elements/layout/deferred-element.d.ts +3 -3
  88. package/dist/elements/layout/deferred-element.js +4 -8
  89. package/dist/elements/layout/expanded-element.d.ts +6 -4
  90. package/dist/elements/layout/expanded-element.js +22 -17
  91. package/dist/elements/layout/keep-together-element.d.ts +42 -0
  92. package/dist/elements/layout/keep-together-element.js +85 -0
  93. package/dist/elements/layout/link-element.d.ts +33 -0
  94. package/dist/elements/layout/link-element.js +39 -0
  95. package/dist/elements/layout/padding-element.d.ts +10 -5
  96. package/dist/elements/layout/padding-element.js +25 -16
  97. package/dist/elements/layout/page-break-element.d.ts +17 -0
  98. package/dist/elements/layout/page-break-element.js +24 -0
  99. package/dist/elements/layout/page-builder-element.d.ts +32 -0
  100. package/dist/elements/layout/page-builder-element.js +45 -0
  101. package/dist/elements/layout/positioned-element.d.ts +47 -0
  102. package/dist/elements/layout/positioned-element.js +73 -0
  103. package/dist/elements/layout/repeating-header-element.d.ts +3 -3
  104. package/dist/elements/layout/repeating-header-element.js +8 -12
  105. package/dist/elements/layout/rotated-box-element.d.ts +35 -0
  106. package/dist/elements/layout/rotated-box-element.js +54 -0
  107. package/dist/elements/layout/rotated-element.d.ts +32 -0
  108. package/dist/elements/layout/rotated-element.js +41 -0
  109. package/dist/elements/layout/sized-container-element.d.ts +2 -2
  110. package/dist/elements/layout/sized-container-element.js +6 -11
  111. package/dist/elements/layout/struct-group.d.ts +28 -0
  112. package/dist/elements/layout/struct-group.js +42 -0
  113. package/dist/elements/line-element.d.ts +3 -3
  114. package/dist/elements/line-element.js +5 -10
  115. package/dist/elements/page-element.d.ts +26 -6
  116. package/dist/elements/page-element.js +72 -27
  117. package/dist/elements/pdf-document-element.d.ts +10 -4
  118. package/dist/elements/pdf-document-element.js +11 -10
  119. package/dist/elements/pdf-element.d.ts +150 -4
  120. package/dist/elements/pdf-element.js +138 -20
  121. package/dist/elements/rectangle-element.d.ts +52 -11
  122. package/dist/elements/rectangle-element.js +129 -36
  123. package/dist/elements/row-element.d.ts +45 -4
  124. package/dist/elements/row-element.js +66 -18
  125. package/dist/elements/text-element.d.ts +139 -11
  126. package/dist/elements/text-element.js +234 -57
  127. package/dist/forms/acroform.d.ts +63 -0
  128. package/dist/forms/acroform.js +427 -0
  129. package/dist/forms/appearance.d.ts +58 -0
  130. package/dist/forms/appearance.js +204 -0
  131. package/dist/forms/field.d.ts +164 -0
  132. package/dist/forms/field.js +1 -0
  133. package/dist/forms/pdf.d.ts +19 -0
  134. package/dist/forms/pdf.js +28 -0
  135. package/dist/index.d.ts +3 -3
  136. package/dist/index.js +3 -19
  137. package/dist/ir/display-list.d.ts +197 -8
  138. package/dist/ir/display-list.js +8 -2
  139. package/dist/layout/alignment.d.ts +10 -0
  140. package/dist/layout/alignment.js +6 -0
  141. package/dist/layout/box-constraints.d.ts +61 -0
  142. package/dist/layout/box-constraints.js +88 -6
  143. package/dist/layout/collect-images.d.ts +9 -0
  144. package/dist/layout/collect-images.js +34 -0
  145. package/dist/layout/fragmentation.d.ts +20 -1
  146. package/dist/layout/fragmentation.js +74 -18
  147. package/dist/layout/insets.d.ts +28 -0
  148. package/dist/layout/insets.js +20 -0
  149. package/dist/platform/browser-fs.d.ts +2 -0
  150. package/dist/platform/browser-fs.js +9 -0
  151. package/dist/platform/browser-image.d.ts +5 -0
  152. package/dist/platform/browser-image.js +13 -0
  153. package/dist/platform/node-fs.d.ts +2 -0
  154. package/dist/platform/node-fs.js +10 -0
  155. package/dist/platform/node-image.d.ts +5 -0
  156. package/dist/platform/node-image.js +9 -0
  157. package/dist/renderer/anchor-renderer.d.ts +6 -0
  158. package/dist/renderer/anchor-renderer.js +11 -0
  159. package/dist/renderer/bookmark-renderer.d.ts +6 -0
  160. package/dist/renderer/bookmark-renderer.js +11 -0
  161. package/dist/renderer/container-renderer.d.ts +3 -3
  162. package/dist/renderer/container-renderer.js +11 -28
  163. package/dist/renderer/default-text-style-renderer.d.ts +6 -0
  164. package/dist/renderer/default-text-style-renderer.js +10 -0
  165. package/dist/renderer/deferred-renderer.d.ts +3 -3
  166. package/dist/renderer/deferred-renderer.js +8 -23
  167. package/dist/renderer/emoji-image.d.ts +9 -0
  168. package/dist/renderer/emoji-image.js +68 -0
  169. package/dist/renderer/expanded-renderer.d.ts +3 -3
  170. package/dist/renderer/expanded-renderer.js +6 -21
  171. package/dist/renderer/form-field-renderer.d.ts +20 -0
  172. package/dist/renderer/form-field-renderer.js +12 -0
  173. package/dist/renderer/image-renderer.d.ts +4 -4
  174. package/dist/renderer/image-renderer.js +86 -75
  175. package/dist/renderer/index.d.ts +10 -10
  176. package/dist/renderer/index.js +10 -26
  177. package/dist/renderer/keep-together-renderer.d.ts +10 -0
  178. package/dist/renderer/keep-together-renderer.js +12 -0
  179. package/dist/renderer/line-renderer.d.ts +3 -3
  180. package/dist/renderer/line-renderer.js +13 -28
  181. package/dist/renderer/link-renderer.d.ts +6 -0
  182. package/dist/renderer/link-renderer.js +11 -0
  183. package/dist/renderer/padding-renderer.d.ts +3 -3
  184. package/dist/renderer/padding-renderer.js +6 -21
  185. package/dist/renderer/page-break-renderer.d.ts +14 -0
  186. package/dist/renderer/page-break-renderer.js +15 -0
  187. package/dist/renderer/page-builder-renderer.d.ts +6 -0
  188. package/dist/renderer/page-builder-renderer.js +11 -0
  189. package/dist/renderer/page-renderer.d.ts +2 -2
  190. package/dist/renderer/page-renderer.js +115 -77
  191. package/dist/renderer/pdf-backend.d.ts +31 -4
  192. package/dist/renderer/pdf-backend.js +287 -49
  193. package/dist/renderer/pdf-config.js +4 -7
  194. package/dist/renderer/pdf-document-class.d.ts +5 -5
  195. package/dist/renderer/pdf-document-class.js +27 -41
  196. package/dist/renderer/pdf-document-renderer.d.ts +19 -10
  197. package/dist/renderer/pdf-document-renderer.js +135 -91
  198. package/dist/renderer/pdf-renderer.d.ts +2 -2
  199. package/dist/renderer/pdf-renderer.js +165 -90
  200. package/dist/renderer/positioned-renderer.d.ts +6 -0
  201. package/dist/renderer/positioned-renderer.js +10 -0
  202. package/dist/renderer/rectangle-renderer.d.ts +3 -3
  203. package/dist/renderer/rectangle-renderer.js +49 -44
  204. package/dist/renderer/repeating-header-renderer.d.ts +3 -3
  205. package/dist/renderer/repeating-header-renderer.js +10 -26
  206. package/dist/renderer/rotated-renderer.d.ts +7 -0
  207. package/dist/renderer/rotated-renderer.js +29 -0
  208. package/dist/renderer/row-renderer.d.ts +3 -3
  209. package/dist/renderer/row-renderer.js +11 -28
  210. package/dist/renderer/struct-group-renderer.d.ts +10 -0
  211. package/dist/renderer/struct-group-renderer.js +24 -0
  212. package/dist/renderer/text-renderer.d.ts +9 -5
  213. package/dist/renderer/text-renderer.js +437 -89
  214. package/dist/text/advance.d.ts +34 -0
  215. package/dist/text/advance.js +45 -0
  216. package/dist/text/arabic.d.ts +20 -0
  217. package/dist/text/arabic.js +101 -0
  218. package/dist/text/bidi.d.ts +33 -0
  219. package/dist/text/bidi.js +94 -0
  220. package/dist/text/emoji-codepoints.d.ts +1 -0
  221. package/dist/text/emoji-codepoints.js +29 -0
  222. package/dist/text/font-fallback.d.ts +25 -0
  223. package/dist/text/font-fallback.js +38 -0
  224. package/dist/text/glyph-coverage.d.ts +15 -0
  225. package/dist/text/glyph-coverage.js +77 -0
  226. package/dist/text/line-breaker.d.ts +50 -12
  227. package/dist/text/line-breaker.js +200 -54
  228. package/dist/text/line-metrics.d.ts +49 -0
  229. package/dist/text/line-metrics.js +57 -0
  230. package/dist/text/orphans-widows.d.ts +27 -0
  231. package/dist/text/orphans-widows.js +53 -0
  232. package/dist/text/shape.d.ts +29 -0
  233. package/dist/text/shape.js +67 -0
  234. package/dist/text/text-decoration.d.ts +50 -0
  235. package/dist/text/text-decoration.js +62 -0
  236. package/dist/text/text-style.d.ts +60 -0
  237. package/dist/text/text-style.js +79 -0
  238. package/dist/text/whitespace.d.ts +4 -0
  239. package/dist/text/whitespace.js +43 -0
  240. package/dist/utils/afm-parser.d.ts +34 -0
  241. package/dist/utils/afm-parser.js +76 -17
  242. package/dist/utils/bytes.d.ts +24 -0
  243. package/dist/utils/bytes.js +76 -0
  244. package/dist/utils/dest-registry.d.ts +22 -0
  245. package/dist/utils/dest-registry.js +32 -0
  246. package/dist/utils/flex-layout.d.ts +36 -9
  247. package/dist/utils/flex-layout.js +207 -30
  248. package/dist/utils/font-metrics.d.ts +26 -1
  249. package/dist/utils/font-metrics.js +1 -2
  250. package/dist/utils/font-path.js +3 -6
  251. package/dist/utils/gsub.d.ts +36 -0
  252. package/dist/utils/gsub.js +178 -0
  253. package/dist/utils/image-helper.d.ts +6 -5
  254. package/dist/utils/image-helper.js +101 -111
  255. package/dist/utils/inflate.d.ts +16 -0
  256. package/dist/utils/inflate.js +52 -0
  257. package/dist/utils/md5.d.ts +4 -0
  258. package/dist/utils/md5.js +79 -0
  259. package/dist/utils/outline.d.ts +21 -0
  260. package/dist/utils/outline.js +79 -0
  261. package/dist/utils/pdf-object-manager.d.ts +127 -8
  262. package/dist/utils/pdf-object-manager.js +615 -141
  263. package/dist/utils/renderer-registry.d.ts +13 -1
  264. package/dist/utils/renderer-registry.js +23 -6
  265. package/dist/utils/struct-tree.d.ts +44 -0
  266. package/dist/utils/struct-tree.js +129 -0
  267. package/dist/utils/ttf-parser.d.ts +134 -2
  268. package/dist/utils/ttf-parser.js +873 -50
  269. package/dist/utils/ttf-subsetter.d.ts +1 -1
  270. package/dist/utils/ttf-subsetter.js +40 -42
  271. package/dist/utils/ua-xmp.d.ts +9 -0
  272. package/dist/utils/ua-xmp.js +32 -0
  273. package/dist/utils/utf8-to-windows1252-encoder.d.ts +6 -0
  274. package/dist/utils/utf8-to-windows1252-encoder.js +11 -4
  275. package/dist/utils/woff.d.ts +10 -0
  276. package/dist/utils/woff.js +116 -0
  277. package/dist/validators/element-validator.d.ts +2 -2
  278. package/dist/validators/element-validator.js +31 -26
  279. package/package.json +22 -2
@@ -1,39 +1,129 @@
1
- "use strict";
2
1
  // Parses the metric tables of a TrueType (.ttf) font - the binary counterpart to AFMParser.
3
2
  // Slice 1: glyph advance widths (hmtx) + the Unicode→glyph map (cmap), enough to compute
4
3
  // text width the same way AFMParser does for the standard-14. Embedding/subsetting come later.
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.TTFParser = void 0;
7
- class TTFParser {
4
+ import { i16, latin1FromBytes, u8, u16, u32 } from "./bytes.js";
5
+ import { GsubTable } from "./gsub.js";
6
+ const IDENTITY = [1, 0, 0, 1, 0, 0];
7
+ /** Sorts x-intervals and unions the overlapping ones. */
8
+ export function mergeSpans(spans) {
9
+ if (spans.length === 0)
10
+ return spans;
11
+ const sorted = [...spans].sort((a, b) => a[0] - b[0]);
12
+ const out = [sorted[0]];
13
+ for (const [start, end] of sorted.slice(1)) {
14
+ const last = out[out.length - 1];
15
+ if (start <= last[1])
16
+ last[1] = Math.max(last[1], end);
17
+ else
18
+ out.push([start, end]);
19
+ }
20
+ return out;
21
+ }
22
+ function isIdentity(m) {
23
+ return m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1 && m[4] === 0 && m[5] === 0;
24
+ }
25
+ // compose(m, n): the transform that applies n first, then m (the matrix product m . n).
26
+ function compose(m, n) {
27
+ return [
28
+ m[0] * n[0] + m[2] * n[1],
29
+ m[1] * n[0] + m[3] * n[1],
30
+ m[0] * n[2] + m[2] * n[3],
31
+ m[1] * n[2] + m[3] * n[3],
32
+ m[0] * n[4] + m[2] * n[5] + m[4],
33
+ m[1] * n[4] + m[3] * n[5] + m[5],
34
+ ];
35
+ }
36
+ // A transform applied about a center point: translate(c) . t . translate(-c).
37
+ function aroundCenter(t, cx, cy) {
38
+ return compose([1, 0, 0, 1, cx, cy], compose(t, [1, 0, 0, 1, -cx, -cy]));
39
+ }
40
+ // Applies an affine to a point (font units).
41
+ function applyAffine(m, x, y) {
42
+ return [m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5]];
43
+ }
44
+ // The affine's mean scale factor sqrt(|det|), used to scale a gradient radius (a circle can only
45
+ // stay a circle under uniform scaling; this is the best single-radius approximation otherwise).
46
+ function detScale(m) {
47
+ return Math.sqrt(Math.abs(m[0] * m[3] - m[1] * m[2]));
48
+ }
49
+ export class TTFParser {
8
50
  constructor(data) {
9
51
  this.data = data;
10
52
  this.unitsPerEm = 1000;
11
53
  // FontDescriptor metrics, in PDF glyph space (1000 units / em).
12
54
  this.ascent = 0;
13
55
  this.descent = 0;
56
+ // Extra leading the font asks for between two lines (hhea), same glyph space. Often 0.
57
+ this.lineGap = 0;
58
+ // Decoration metrics in RAW font units (divide by unitsPerEm). `post` always carries the underline
59
+ // pair; `OS/2` only carries sxHeight/sCapHeight from version 2 on, so those may stay 0 and are
60
+ // then measured off the glyph outlines instead (see `decoration()`).
61
+ this.underlinePositionRaw = 0; // negative = below the baseline
62
+ this.underlineThicknessRaw = 0;
63
+ this.xHeightRaw = 0;
64
+ this.capHeightRaw = 0;
14
65
  this.bbox = [0, 0, 0, 0];
15
66
  this.numGlyphs = 0;
16
67
  this.numHMetrics = 0;
68
+ this.indexToLocFormat = 0; // head[50]: 0 = short loca (offsets/2), 1 = long loca
17
69
  this.advanceWidths = []; // per glyph id, in font units
18
70
  this.cmap = new Map(); // codepoint → glyph id (format 4)
19
71
  this.cmapGroups = []; // format 12, searched on lookup
20
72
  this.tables = {};
73
+ // COLR v0: base glyph id → its slice of the layer-record array. Empty when the font has no
74
+ // v0 color layers, so getColorGlyph falls back to v1 or to normal (monochrome) drawing.
75
+ this.colrBase = new Map();
76
+ this.colrLayersOffset = 0; // byte offset of the v0 layer-record array in the COLR table
77
+ this.palette = []; // CPAL palette 0, indexed by palette entry
78
+ // COLR v1: base glyph id → the absolute offset of its Paint table (a recursive paint graph).
79
+ this.colrBaseV1 = new Map();
80
+ this.colrLayerListOffset = 0; // absolute offset of the v1 LayerList (paint-offset array)
81
+ // Kerning: (leftGid << 16 | rightGid) -> value in FONT UNITS (from the `kern` table and/or GPOS).
82
+ this.kernPairs = new Map();
83
+ this.hasKerning = false;
84
+ // GPOS Type 2 (pair positioning) subtables, in absolute byte offsets. Queried per pair (format 2
85
+ // is class-based, so it cannot be enumerated into `kernPairs` up front). Preferred over `kern`.
86
+ this.gposPairPos = [];
87
+ // Memoised getKerning results, keyed (leftGid << 16 | rightGid) -> em/1000. A GPOS lookup walks
88
+ // subtables (coverage + classDef) per pair, and a long document repeats the same pairs thousands
89
+ // of times, so the first lookup of a pair pays; the rest are O(1).
90
+ this.kernCache = new Map();
21
91
  this.readTableDirectory();
22
92
  const head = this.table("head").offset;
23
- this.unitsPerEm = this.data.readUInt16BE(head + 18);
24
- this.numGlyphs = this.data.readUInt16BE(this.table("maxp").offset + 4);
25
- this.numHMetrics = this.data.readUInt16BE(this.table("hhea").offset + 34);
93
+ this.unitsPerEm = u16(this.data, head + 18);
94
+ this.numGlyphs = u16(this.data, this.table("maxp").offset + 4);
95
+ this.numHMetrics = u16(this.data, this.table("hhea").offset + 34);
26
96
  this.bbox = [
27
- this.toGlyphSpace(this.data.readInt16BE(head + 36)), // xMin
28
- this.toGlyphSpace(this.data.readInt16BE(head + 38)), // yMin
29
- this.toGlyphSpace(this.data.readInt16BE(head + 40)), // xMax
30
- this.toGlyphSpace(this.data.readInt16BE(head + 42)), // yMax
97
+ this.toGlyphSpace(i16(this.data, head + 36)), // xMin
98
+ this.toGlyphSpace(i16(this.data, head + 38)), // yMin
99
+ this.toGlyphSpace(i16(this.data, head + 40)), // xMax
100
+ this.toGlyphSpace(i16(this.data, head + 42)), // yMax
31
101
  ];
32
102
  const hhea = this.table("hhea").offset;
33
- this.ascent = this.toGlyphSpace(this.data.readInt16BE(hhea + 4));
34
- this.descent = this.toGlyphSpace(this.data.readInt16BE(hhea + 6));
103
+ this.ascent = this.toGlyphSpace(i16(this.data, hhea + 4));
104
+ this.descent = this.toGlyphSpace(i16(this.data, hhea + 6));
105
+ this.lineGap = this.toGlyphSpace(i16(this.data, hhea + 8));
106
+ this.readDecorationMetrics();
107
+ this.indexToLocFormat = i16(this.data, head + 50);
35
108
  this.readHmtx();
36
109
  this.readCmap();
110
+ if (this.tables["COLR"] && this.tables["CPAL"]) {
111
+ this.readColr();
112
+ this.readCpal();
113
+ }
114
+ // Kerning tables are optional and only ever make text prettier; a malformed one must degrade to
115
+ // "no kerning", never break font loading. Guard both.
116
+ try {
117
+ if (this.tables["kern"])
118
+ this.readKern();
119
+ if (this.tables["GPOS"])
120
+ this.readGpos();
121
+ }
122
+ catch {
123
+ this.kernPairs.clear();
124
+ this.gposPairPos = [];
125
+ this.hasKerning = false;
126
+ }
37
127
  }
38
128
  // The raw font bytes, for the /FontFile2 stream.
39
129
  getData() {
@@ -62,6 +152,20 @@ class TTFParser {
62
152
  toGlyphSpace(v) {
63
153
  return Math.round((v * 1000) / this.unitsPerEm);
64
154
  }
155
+ /** The font's `GSUB`, or undefined when it has none. Parsed once, on demand. */
156
+ gsub() {
157
+ if (this.gsubTable === undefined) {
158
+ const record = this.tables["GSUB"];
159
+ // Degrade to "no shaping" rather than break font loading - same rule as kern.
160
+ try {
161
+ this.gsubTable = record ? new GsubTable(this.data, record.offset) : null;
162
+ }
163
+ catch {
164
+ this.gsubTable = null;
165
+ }
166
+ }
167
+ return this.gsubTable ?? undefined;
168
+ }
65
169
  // Codepoint → glyph id (0 = .notdef when the font has no glyph for it).
66
170
  getGlyphIndex(codePoint) {
67
171
  const g = this.cmap.get(codePoint);
@@ -76,11 +180,14 @@ class TTFParser {
76
180
  }
77
181
  // Advance width of a glyph in font units. Glyphs past numHMetrics reuse the last advance.
78
182
  getAdvanceWidth(glyphIndex) {
79
- var _a;
80
183
  const i = Math.min(glyphIndex, this.numHMetrics - 1);
81
- return (_a = this.advanceWidths[i]) !== null && _a !== void 0 ? _a : 0;
184
+ return this.advanceWidths[i] ?? 0;
82
185
  }
83
186
  // Char width at fontSize, scaled from font units (em = unitsPerEm) to points.
187
+ /** Font units -> points at `fontSize`; the one scaling rule for shaped and plain advances alike. */
188
+ unitsToPoints(units, fontSize) {
189
+ return (units / this.unitsPerEm) * fontSize;
190
+ }
84
191
  getCharWidth(char, fontSize) {
85
192
  const units = this.getAdvanceWidth(this.getGlyphIndex(char.codePointAt(0)));
86
193
  return (units / this.unitsPerEm) * fontSize;
@@ -92,6 +199,325 @@ class TTFParser {
92
199
  width += this.getCharWidth(ch, fontSize);
93
200
  return width;
94
201
  }
202
+ // `post` carries the underline pair (always, from its header); `OS/2` carries the x- and cap-height
203
+ // but only from version 2 on. Both tables are optional in principle, so nothing here may throw.
204
+ readDecorationMetrics() {
205
+ const post = this.tables["post"];
206
+ if (post) {
207
+ this.underlinePositionRaw = i16(this.data, post.offset + 8);
208
+ this.underlineThicknessRaw = i16(this.data, post.offset + 10);
209
+ }
210
+ const os2 = this.tables["OS/2"];
211
+ if (os2 && u16(this.data, os2.offset) >= 2) {
212
+ this.xHeightRaw = i16(this.data, os2.offset + 86);
213
+ this.capHeightRaw = i16(this.data, os2.offset + 88);
214
+ }
215
+ }
216
+ /**
217
+ * Underline / strikethrough geometry, as fractions of the em.
218
+ *
219
+ * A font without `post` (rare) gets the classic 10% below the baseline at 5% thickness - the same
220
+ * numbers all 14 standard AFMs happen to declare, so it is a measured convention rather than an
221
+ * invention. A font whose `OS/2` predates version 2 has no x- or cap-height, so we MEASURE them off
222
+ * the outlines of `x` and `H` instead of guessing.
223
+ */
224
+ decoration() {
225
+ if (this.cachedDecoration)
226
+ return this.cachedDecoration;
227
+ const em = this.unitsPerEm;
228
+ const xHeight = this.xHeightRaw || this.outlineTop(0x78); // "x"
229
+ const capHeight = this.capHeightRaw || this.outlineTop(0x48); // "H"
230
+ this.cachedDecoration = {
231
+ underlinePosition: this.underlinePositionRaw ? -this.underlinePositionRaw / em : 0.1,
232
+ underlineThickness: this.underlineThicknessRaw ? this.underlineThicknessRaw / em : 0.05,
233
+ xHeight: xHeight / em,
234
+ capHeight: capHeight / em,
235
+ };
236
+ return this.cachedDecoration;
237
+ }
238
+ /** The highest ON-CURVE point of a code point's outline, in font units; 0 for a missing glyph.
239
+ * A quadratic's control point may sit above the curve, so it is not a bound - and `x`/`H` have
240
+ * flat tops in any face where this fallback matters. */
241
+ outlineTop(codePoint) {
242
+ let top = 0;
243
+ for (const cmd of this.getGlyphPath(this.getGlyphIndex(codePoint))) {
244
+ if (cmd.type !== "Z" && cmd.y > top)
245
+ top = cmd.y;
246
+ }
247
+ return top;
248
+ }
249
+ /**
250
+ * Where a glyph's INK crosses a horizontal band, as x-intervals in font units (pen origin at 0).
251
+ * This is what lets an underline step around a descender ("skip-ink"), the way a browser does.
252
+ *
253
+ * Scanline fill: sample a few horizontal lines across the band, find every place the outline
254
+ * crosses them, and pair the crossings up into filled spans. A handful of scanlines is plenty -
255
+ * the band is a few percent of the em tall, and a descender stem does not wander inside it.
256
+ *
257
+ * `yTop` / `yBottom` are in font units, y UP from the baseline (so both are negative under it).
258
+ * Returns [] for a glyph with no outline (a space, an unmapped code point, a composite we cannot
259
+ * decompose) - the caller then leaves the line unbroken there, which is the safe direction.
260
+ */
261
+ inkSpansInBand(codePoint, yTop, yBottom) {
262
+ const path = this.getGlyphPath(this.getGlyphIndex(codePoint));
263
+ if (path.length === 0)
264
+ return [];
265
+ // Flatten to straight edges; a quadratic becomes a short polyline (the band is thin, so a
266
+ // coarse subdivision is already below the printing resolution).
267
+ const edges = [];
268
+ let startX = 0;
269
+ let startY = 0;
270
+ let curX = 0;
271
+ let curY = 0;
272
+ const edge = (x, y) => {
273
+ edges.push([curX, curY, x, y]);
274
+ curX = x;
275
+ curY = y;
276
+ };
277
+ for (const cmd of path) {
278
+ if (cmd.type === "M") {
279
+ [curX, curY] = [cmd.x, cmd.y];
280
+ [startX, startY] = [cmd.x, cmd.y];
281
+ }
282
+ else if (cmd.type === "L") {
283
+ edge(cmd.x, cmd.y);
284
+ }
285
+ else if (cmd.type === "Q") {
286
+ const [x0, y0] = [curX, curY];
287
+ const STEPS = 8;
288
+ for (let i = 1; i <= STEPS; i++) {
289
+ const t = i / STEPS;
290
+ const u = 1 - t;
291
+ edge(u * u * x0 + 2 * u * t * cmd.cx + t * t * cmd.x, u * u * y0 + 2 * u * t * cmd.cy + t * t * cmd.y);
292
+ }
293
+ }
294
+ else {
295
+ edge(startX, startY); // Z: close the contour
296
+ }
297
+ }
298
+ const SCANLINES = 5;
299
+ const spans = [];
300
+ for (let i = 0; i < SCANLINES; i++) {
301
+ const y = yTop + ((yBottom - yTop) * i) / (SCANLINES - 1);
302
+ const xs = [];
303
+ for (const [x0, y0, x1, y1] of edges) {
304
+ if (y0 === y1)
305
+ continue;
306
+ // Half-open test so a vertex on the scanline is counted exactly once.
307
+ if (y >= Math.min(y0, y1) && y < Math.max(y0, y1)) {
308
+ xs.push(x0 + ((y - y0) / (y1 - y0)) * (x1 - x0));
309
+ }
310
+ }
311
+ xs.sort((a, b) => a - b);
312
+ for (let k = 0; k + 1 < xs.length; k += 2)
313
+ spans.push([xs[k], xs[k + 1]]);
314
+ }
315
+ return mergeSpans(spans);
316
+ }
317
+ /** True if the font declares any horizontal kerning (kern table or GPOS). */
318
+ kerns() {
319
+ return this.hasKerning;
320
+ }
321
+ /** Kerning between two glyphs, in em/1000 (the standard-14 unit); 0 if the pair is not kerned.
322
+ * Negative tightens, matching the AFM sign. GPOS wins over the legacy `kern` table when both
323
+ * exist (it is what a modern shaper uses, and can carry more pairs). */
324
+ getKerning(leftGid, rightGid) {
325
+ const key = (leftGid << 16) | rightGid;
326
+ const cached = this.kernCache.get(key);
327
+ if (cached !== undefined)
328
+ return cached;
329
+ let units;
330
+ if (this.gposPairPos.length > 0)
331
+ units = this.gposKern(leftGid, rightGid);
332
+ if (units === undefined)
333
+ units = this.kernPairs.get(key);
334
+ const em = units === undefined ? 0 : Math.round((units * 1000) / this.unitsPerEm);
335
+ this.kernCache.set(key, em);
336
+ return em;
337
+ }
338
+ // The legacy `kern` table, format 0 (an explicit list of glyph-pair adjustments). Only horizontal
339
+ // format-0 subtables are read; GPOS (parsed separately) supersedes this in modern fonts, but old
340
+ // TrueType (DejaVu, FreeSans) carries its kerning here.
341
+ readKern() {
342
+ const base = this.tables["kern"].offset;
343
+ // The OpenType (Microsoft) header is version:u16 = 0, nTables:u16. (Apple's is a u32 version; the
344
+ // fonts we target use the OpenType form.)
345
+ if (u16(this.data, base) !== 0)
346
+ return;
347
+ const nTables = u16(this.data, base + 2);
348
+ let p = base + 4;
349
+ for (let t = 0; t < nTables; t++) {
350
+ const length = u16(this.data, p + 2);
351
+ const coverage = u16(this.data, p + 4);
352
+ const format = coverage >> 8;
353
+ const horizontal = (coverage & 0x1) === 1;
354
+ if (format === 0 && horizontal) {
355
+ const nPairs = u16(this.data, p + 6);
356
+ let q = p + 14; // past nPairs, searchRange, entrySelector, rangeShift
357
+ for (let i = 0; i < nPairs; i++) {
358
+ const left = u16(this.data, q);
359
+ const right = u16(this.data, q + 2);
360
+ const value = i16(this.data, q + 4);
361
+ this.kernPairs.set((left << 16) | right, value);
362
+ q += 6;
363
+ }
364
+ }
365
+ p += length;
366
+ }
367
+ if (this.kernPairs.size > 0)
368
+ this.hasKerning = true;
369
+ }
370
+ // GPOS pair-adjustment kerning. We collect the PairPos (lookup type 2) subtables of the `kern`
371
+ // FEATURE only - not every pair-positioning lookup - so this is kerning, not arbitrary shaping.
372
+ // Script/language selection is skipped: the `kern` feature is the same across the Latin scripts we
373
+ // target, so a flat scan of the feature list for tag "kern" is correct here. Only pair positioning
374
+ // (type 2) is read; the rest of GPOS (mark attachment, cursive, contextual) is not kerning.
375
+ readGpos() {
376
+ const base = this.tables["GPOS"].offset;
377
+ const featureListOffset = base + u16(this.data, base + 6);
378
+ const lookupListOffset = base + u16(this.data, base + 8);
379
+ // Every lookup index referenced by a "kern" feature.
380
+ const kernLookups = new Set();
381
+ const featureCount = u16(this.data, featureListOffset);
382
+ for (let i = 0; i < featureCount; i++) {
383
+ const rec = featureListOffset + 2 + i * 6;
384
+ const tag = String.fromCharCode(this.data[rec], this.data[rec + 1], this.data[rec + 2], this.data[rec + 3]);
385
+ if (tag !== "kern")
386
+ continue;
387
+ const feature = featureListOffset + u16(this.data, rec + 4);
388
+ const lookupIndexCount = u16(this.data, feature + 2);
389
+ for (let j = 0; j < lookupIndexCount; j++) {
390
+ kernLookups.add(u16(this.data, feature + 4 + j * 2));
391
+ }
392
+ }
393
+ if (kernLookups.size === 0)
394
+ return;
395
+ // The PairPos subtables of those lookups (a lookup of another type in the set is ignored).
396
+ const lookupCount = u16(this.data, lookupListOffset);
397
+ for (const idx of kernLookups) {
398
+ if (idx >= lookupCount)
399
+ continue;
400
+ const lookup = lookupListOffset + u16(this.data, lookupListOffset + 2 + idx * 2);
401
+ if (u16(this.data, lookup) !== 2)
402
+ continue; // lookupType 2 = pair adjustment
403
+ const subTableCount = u16(this.data, lookup + 4);
404
+ for (let s = 0; s < subTableCount; s++) {
405
+ this.gposPairPos.push(lookup + u16(this.data, lookup + 6 + s * 2));
406
+ }
407
+ }
408
+ if (this.gposPairPos.length > 0)
409
+ this.hasKerning = true;
410
+ }
411
+ // The XAdvance adjustment on the LEFT glyph for a pair, in font units, from the first PairPos
412
+ // subtable that covers it; undefined if none does. (Kerning only ever uses value1's XAdvance.)
413
+ gposKern(leftGid, rightGid) {
414
+ for (const sub of this.gposPairPos) {
415
+ const posFormat = u16(this.data, sub);
416
+ const coverage = sub + u16(this.data, sub + 2);
417
+ const covIndex = this.coverageIndex(coverage, leftGid);
418
+ if (covIndex < 0)
419
+ continue;
420
+ const valueFormat1 = u16(this.data, sub + 4);
421
+ const valueFormat2 = u16(this.data, sub + 6);
422
+ const v1size = TTFParser.valueRecordSize(valueFormat1);
423
+ const v2size = TTFParser.valueRecordSize(valueFormat2);
424
+ if (posFormat === 1) {
425
+ const pairSet = sub + u16(this.data, sub + 10 + covIndex * 2);
426
+ const pairValueCount = u16(this.data, pairSet);
427
+ const recSize = 2 + v1size + v2size;
428
+ for (let i = 0; i < pairValueCount; i++) {
429
+ const rec = pairSet + 2 + i * recSize;
430
+ if (u16(this.data, rec) === rightGid) {
431
+ return this.xAdvance(rec + 2, valueFormat1);
432
+ }
433
+ }
434
+ }
435
+ else if (posFormat === 2) {
436
+ const classDef1 = sub + u16(this.data, sub + 8);
437
+ const classDef2 = sub + u16(this.data, sub + 10);
438
+ const class2Count = u16(this.data, sub + 14);
439
+ const c1 = this.classOf(classDef1, leftGid);
440
+ const c2 = this.classOf(classDef2, rightGid);
441
+ const recSize = v1size + v2size;
442
+ const record = sub + 16 + (c1 * class2Count + c2) * recSize;
443
+ return this.xAdvance(record, valueFormat1);
444
+ }
445
+ }
446
+ return undefined;
447
+ }
448
+ static valueRecordSize(valueFormat) {
449
+ let bits = 0;
450
+ for (let m = valueFormat; m; m >>= 1)
451
+ bits += m & 1;
452
+ return bits * 2;
453
+ }
454
+ // The XAdvance field of a GPOS value record, or 0 if the format has none. XAdvance (bit 0x0004) is
455
+ // preceded by XPlacement (0x0001) and YPlacement (0x0002), each an i16 when present.
456
+ xAdvance(recordOffset, valueFormat) {
457
+ if ((valueFormat & 0x0004) === 0)
458
+ return 0;
459
+ let off = recordOffset;
460
+ if (valueFormat & 0x0001)
461
+ off += 2;
462
+ if (valueFormat & 0x0002)
463
+ off += 2;
464
+ return i16(this.data, off);
465
+ }
466
+ // Coverage index of a glyph, or -1. Format 1: sorted glyph list. Format 2: sorted ranges.
467
+ coverageIndex(offset, gid) {
468
+ const format = u16(this.data, offset);
469
+ if (format === 1) {
470
+ const count = u16(this.data, offset + 2);
471
+ let lo = 0;
472
+ let hi = count - 1;
473
+ while (lo <= hi) {
474
+ const mid = (lo + hi) >> 1;
475
+ const g = u16(this.data, offset + 4 + mid * 2);
476
+ if (gid < g)
477
+ hi = mid - 1;
478
+ else if (gid > g)
479
+ lo = mid + 1;
480
+ else
481
+ return mid;
482
+ }
483
+ return -1;
484
+ }
485
+ if (format === 2) {
486
+ const rangeCount = u16(this.data, offset + 2);
487
+ for (let i = 0; i < rangeCount; i++) {
488
+ const rec = offset + 4 + i * 6;
489
+ const start = u16(this.data, rec);
490
+ const end = u16(this.data, rec + 2);
491
+ if (gid >= start && gid <= end)
492
+ return u16(this.data, rec + 4) + (gid - start);
493
+ }
494
+ }
495
+ return -1;
496
+ }
497
+ // Class of a glyph in a ClassDef, or 0 (the default class). Format 1: run from a start glyph.
498
+ // Format 2: ranges. Glyphs not listed are class 0.
499
+ classOf(offset, gid) {
500
+ const format = u16(this.data, offset);
501
+ if (format === 1) {
502
+ const startGlyph = u16(this.data, offset + 2);
503
+ const glyphCount = u16(this.data, offset + 4);
504
+ if (gid >= startGlyph && gid < startGlyph + glyphCount) {
505
+ return u16(this.data, offset + 6 + (gid - startGlyph) * 2);
506
+ }
507
+ return 0;
508
+ }
509
+ if (format === 2) {
510
+ const rangeCount = u16(this.data, offset + 2);
511
+ for (let i = 0; i < rangeCount; i++) {
512
+ const rec = offset + 4 + i * 6;
513
+ const start = u16(this.data, rec);
514
+ const end = u16(this.data, rec + 2);
515
+ if (gid >= start && gid <= end)
516
+ return u16(this.data, rec + 4);
517
+ }
518
+ }
519
+ return 0;
520
+ }
95
521
  table(tag) {
96
522
  const t = this.tables[tag];
97
523
  if (!t)
@@ -99,13 +525,13 @@ class TTFParser {
99
525
  return t;
100
526
  }
101
527
  readTableDirectory() {
102
- const numTables = this.data.readUInt16BE(4);
528
+ const numTables = u16(this.data, 4);
103
529
  let p = 12; // after the offset table (sfntVersion + numTables + 3 fields)
104
530
  for (let i = 0; i < numTables; i++) {
105
- const tag = this.data.toString("latin1", p, p + 4);
531
+ const tag = latin1FromBytes(this.data.subarray(p, p + 4));
106
532
  this.tables[tag] = {
107
- offset: this.data.readUInt32BE(p + 8),
108
- length: this.data.readUInt32BE(p + 12),
533
+ offset: u32(this.data, p + 8),
534
+ length: u32(this.data, p + 12),
109
535
  };
110
536
  p += 16;
111
537
  }
@@ -116,56 +542,54 @@ class TTFParser {
116
542
  let last = 0;
117
543
  for (let i = 0; i < this.numGlyphs; i++) {
118
544
  if (i < this.numHMetrics)
119
- last = this.data.readUInt16BE(o + i * 4);
545
+ last = u16(this.data, o + i * 4);
120
546
  this.advanceWidths.push(last);
121
547
  }
122
548
  }
123
549
  readCmap() {
124
550
  const base = this.table("cmap").offset;
125
- const numTables = this.data.readUInt16BE(base + 2);
126
- // Prefer the Windows Unicode BMP subtable (3,1); accept full-Unicode (3,10) or any (0,x).
127
- let subtable = -1;
551
+ const numTables = u16(this.data, base + 2);
552
+ // Read EVERY Unicode subtable, not just one: fonts commonly ship a (3,1) format-4 for the BMP
553
+ // AND a (3,10) format-12 for astral code points (emoji, CJK-ext). Reading only the first would
554
+ // drop astral coverage. Format-4 fills the direct map; format-12 groups are searched on lookup,
555
+ // and getGlyphIndex checks the map first, so an overlapping BMP entry stays correct.
128
556
  let p = base + 4;
129
557
  for (let i = 0; i < numTables; i++) {
130
- const platform = this.data.readUInt16BE(p);
131
- const encoding = this.data.readUInt16BE(p + 2);
132
- const offset = this.data.readUInt32BE(p + 4);
133
- const unicode = (platform === 3 && (encoding === 1 || encoding === 10)) || platform === 0;
134
- if (unicode) {
135
- subtable = base + offset;
136
- if (platform === 3 && encoding === 1)
137
- break; // best fit, stop looking
138
- }
558
+ const platform = u16(this.data, p);
559
+ const encoding = u16(this.data, p + 2);
560
+ const offset = u32(this.data, p + 4);
139
561
  p += 8;
562
+ const unicode = (platform === 3 && (encoding === 1 || encoding === 10)) || platform === 0;
563
+ if (!unicode)
564
+ continue;
565
+ const subtable = base + offset;
566
+ const format = u16(this.data, subtable);
567
+ if (format === 4)
568
+ this.readCmapFormat4(subtable);
569
+ else if (format === 12)
570
+ this.readCmapFormat12(subtable);
140
571
  }
141
- if (subtable < 0)
142
- return;
143
- const format = this.data.readUInt16BE(subtable);
144
- if (format === 4)
145
- this.readCmapFormat4(subtable);
146
- else if (format === 12)
147
- this.readCmapFormat12(subtable);
148
572
  }
149
573
  // Format 4: segment mapping. Four parallel arrays keyed by segment; the glyph comes either
150
574
  // from idDelta or, when idRangeOffset != 0, from the glyphIdArray it points into.
151
575
  readCmapFormat4(o) {
152
- const segCountX2 = this.data.readUInt16BE(o + 6);
576
+ const segCountX2 = u16(this.data, o + 6);
153
577
  const endCodes = o + 14;
154
578
  const startCodes = endCodes + segCountX2 + 2; // +2 reservedPad
155
579
  const idDeltas = startCodes + segCountX2;
156
580
  const idRangeOffsets = idDeltas + segCountX2;
157
581
  for (let i = 0; i < segCountX2 / 2; i++) {
158
- const end = this.data.readUInt16BE(endCodes + i * 2);
159
- const start = this.data.readUInt16BE(startCodes + i * 2);
160
- const delta = this.data.readUInt16BE(idDeltas + i * 2);
161
- const rangeOffset = this.data.readUInt16BE(idRangeOffsets + i * 2);
582
+ const end = u16(this.data, endCodes + i * 2);
583
+ const start = u16(this.data, startCodes + i * 2);
584
+ const delta = u16(this.data, idDeltas + i * 2);
585
+ const rangeOffset = u16(this.data, idRangeOffsets + i * 2);
162
586
  for (let c = start; c <= end && c !== 0xffff; c++) {
163
587
  let glyph;
164
588
  if (rangeOffset === 0) {
165
589
  glyph = (c + delta) & 0xffff;
166
590
  }
167
591
  else {
168
- glyph = this.data.readUInt16BE(idRangeOffsets + i * 2 + rangeOffset + (c - start) * 2);
592
+ glyph = u16(this.data, idRangeOffsets + i * 2 + rangeOffset + (c - start) * 2);
169
593
  if (glyph !== 0)
170
594
  glyph = (glyph + delta) & 0xffff;
171
595
  }
@@ -176,16 +600,415 @@ class TTFParser {
176
600
  }
177
601
  // Format 12: sequential groups for full Unicode (incl. astral). Kept un-expanded.
178
602
  readCmapFormat12(o) {
179
- const nGroups = this.data.readUInt32BE(o + 12);
603
+ const nGroups = u32(this.data, o + 12);
180
604
  let p = o + 16;
181
605
  for (let i = 0; i < nGroups; i++) {
182
606
  this.cmapGroups.push({
183
- start: this.data.readUInt32BE(p),
184
- end: this.data.readUInt32BE(p + 4),
185
- startGlyph: this.data.readUInt32BE(p + 8),
607
+ start: u32(this.data, p),
608
+ end: u32(this.data, p + 4),
609
+ startGlyph: u32(this.data, p + 8),
186
610
  });
187
611
  p += 12;
188
612
  }
189
613
  }
614
+ // True when the font carries glyph outlines we can draw (a `glyf`+`loca` pair). CFF/OTF fonts
615
+ // and bitmap-only fonts return false - color layers need vector outlines.
616
+ hasGlyfOutlines() {
617
+ return this.tables["glyf"] !== undefined && this.tables["loca"] !== undefined;
618
+ }
619
+ // Byte range of glyph `glyphId` inside the glyf table, via the loca offset array. An empty range
620
+ // (start === end) means the glyph has no outline (e.g. the space) - returns null.
621
+ glyfRange(glyphId) {
622
+ const loca = this.table("loca").offset;
623
+ let start;
624
+ let end;
625
+ if (this.indexToLocFormat === 0) {
626
+ // short loca stores offsets divided by two
627
+ start = u16(this.data, loca + glyphId * 2) * 2;
628
+ end = u16(this.data, loca + (glyphId + 1) * 2) * 2;
629
+ }
630
+ else {
631
+ start = u32(this.data, loca + glyphId * 4);
632
+ end = u32(this.data, loca + (glyphId + 1) * 4);
633
+ }
634
+ return end > start ? { start, end } : null;
635
+ }
636
+ // Glyph outline as drawing commands in font units. Empty for glyphs without an outline. Composite
637
+ // glyphs (numberOfContours < 0) are not assembled yet - they return empty (a follow-up sub-step).
638
+ getGlyphPath(glyphId) {
639
+ if (!this.hasGlyfOutlines())
640
+ return [];
641
+ const range = this.glyfRange(glyphId);
642
+ if (!range)
643
+ return [];
644
+ const g = this.table("glyf").offset + range.start;
645
+ const numberOfContours = i16(this.data, g);
646
+ if (numberOfContours < 0)
647
+ return []; // composite: deferred
648
+ return this.parseSimpleGlyph(g, numberOfContours);
649
+ }
650
+ // Simple-glyph body: end-point indices per contour, then flags, then delta-encoded x/y runs.
651
+ parseSimpleGlyph(g, numberOfContours) {
652
+ let p = g + 10; // past numberOfContours + the four bbox int16s
653
+ const endPts = [];
654
+ for (let i = 0; i < numberOfContours; i++) {
655
+ endPts.push(u16(this.data, p));
656
+ p += 2;
657
+ }
658
+ const numPoints = numberOfContours === 0 ? 0 : endPts[endPts.length - 1] + 1;
659
+ const instructionLength = u16(this.data, p);
660
+ p += 2 + instructionLength; // skip hinting instructions
661
+ // Flags, with the REPEAT bit (0x08) run-length-encoding a flag byte.
662
+ const flags = [];
663
+ while (flags.length < numPoints) {
664
+ const flag = u8(this.data, p++);
665
+ flags.push(flag);
666
+ if (flag & 0x08) {
667
+ let repeat = u8(this.data, p++);
668
+ while (repeat-- > 0)
669
+ flags.push(flag);
670
+ }
671
+ }
672
+ // X then Y coordinates, each a delta from the previous. Short (1 byte, sign in a flag bit) or
673
+ // long (int16), or "same as previous" (delta 0) when the short bit is clear but the same bit set.
674
+ const xs = this.readCoordinates(flags, p, 0x02, 0x10);
675
+ p = xs.next;
676
+ const ys = this.readCoordinates(flags, p, 0x04, 0x20);
677
+ const points = flags.map((flag, i) => ({
678
+ x: xs.values[i],
679
+ y: ys.values[i],
680
+ on: (flag & 0x01) !== 0,
681
+ }));
682
+ const commands = [];
683
+ let contourStart = 0;
684
+ for (const endPt of endPts) {
685
+ this.contourToCommands(points.slice(contourStart, endPt + 1), commands);
686
+ contourStart = endPt + 1;
687
+ }
688
+ return commands;
689
+ }
690
+ // Decodes one delta-encoded coordinate axis. `shortBit` = value is a single unsigned byte;
691
+ // `sameBit` = (short) its sign, or (long) repeat the previous value (delta 0).
692
+ readCoordinates(flags, start, shortBit, sameBit) {
693
+ let p = start;
694
+ let value = 0;
695
+ const values = [];
696
+ for (const flag of flags) {
697
+ if (flag & shortBit) {
698
+ const delta = u8(this.data, p++);
699
+ value += flag & sameBit ? delta : -delta;
700
+ }
701
+ else if (!(flag & sameBit)) {
702
+ value += i16(this.data, p);
703
+ p += 2;
704
+ }
705
+ // else: sameBit set without shortBit => delta 0, value unchanged
706
+ values.push(value);
707
+ }
708
+ return { values, next: p };
709
+ }
710
+ // Converts one TrueType contour (mixed on/off-curve points) into path commands. Between two
711
+ // consecutive off-curve control points TrueType implies an on-curve midpoint; we insert those,
712
+ // rotate the contour to begin on an on-curve anchor, then emit quads (off->on) and lines (on->on).
713
+ contourToCommands(raw, out) {
714
+ const n = raw.length;
715
+ if (n === 0)
716
+ return;
717
+ const pts = [];
718
+ for (let i = 0; i < n; i++) {
719
+ const cur = raw[i];
720
+ pts.push(cur);
721
+ const next = raw[(i + 1) % n];
722
+ if (!cur.on && !next.on) {
723
+ pts.push({ x: (cur.x + next.x) / 2, y: (cur.y + next.y) / 2, on: true });
724
+ }
725
+ }
726
+ const firstOn = pts.findIndex((pt) => pt.on);
727
+ if (firstOn < 0)
728
+ return; // degenerate: no anchor even after inserting midpoints
729
+ const loop = [...pts.slice(firstOn), ...pts.slice(0, firstOn)]; // starts on an on-curve anchor
730
+ const start = loop[0];
731
+ out.push({ type: "M", x: start.x, y: start.y });
732
+ let i = 1;
733
+ while (i < loop.length) {
734
+ const cur = loop[i];
735
+ if (cur.on) {
736
+ out.push({ type: "L", x: cur.x, y: cur.y });
737
+ i += 1;
738
+ }
739
+ else {
740
+ // An off-curve control is always followed by an on-curve point (real or implied); at the
741
+ // end of the loop that closing anchor is the start point itself.
742
+ const end = i + 1 < loop.length ? loop[i + 1] : start;
743
+ out.push({ type: "Q", cx: cur.x, cy: cur.y, x: end.x, y: end.y });
744
+ i += 2;
745
+ }
746
+ }
747
+ out.push({ type: "Z" });
748
+ }
749
+ // True when the font has color glyphs we can render: COLR base glyphs (v0 or v1) plus a CPAL
750
+ // palette to color them.
751
+ hasColorGlyphs() {
752
+ return (this.colrBase.size > 0 || this.colrBaseV1.size > 0) && this.palette.length > 0;
753
+ }
754
+ // The color layers of a base glyph, back-to-front (first drawn underneath), or null when the glyph
755
+ // has no COLR entry (draw it as a normal monochrome glyph then). Prefers the richer v1 paint graph
756
+ // when present, else reads the v0 records; both resolve to the same `ColorGlyphLayer` shape.
757
+ getColorGlyph(glyphId) {
758
+ const v1 = this.colrBaseV1.get(glyphId);
759
+ if (v1 !== undefined)
760
+ return this.walkColrLayers(v1, IDENTITY, new Set());
761
+ const rec = this.colrBase.get(glyphId);
762
+ if (!rec)
763
+ return null;
764
+ const layers = [];
765
+ for (let i = 0; i < rec.count; i++) {
766
+ const o = this.colrLayersOffset + (rec.first + i) * 4;
767
+ const paletteIndex = u16(this.data, o + 2);
768
+ layers.push({
769
+ glyphId: u16(this.data, o),
770
+ paint: { type: "solid", color: this.paletteColor(paletteIndex, 1) },
771
+ });
772
+ }
773
+ return layers;
774
+ }
775
+ // Walks a v1 paint (sub)tree into the flat list of glyph-clipped layers it draws, carrying the
776
+ // accumulated affine `m` down the graph. Handles the structural paints: PaintColrLayers (a list),
777
+ // PaintGlyph (a glyph + its fill), PaintColrGlyph (a reference), the transform paints (compose
778
+ // into `m`), and PaintComposite (backdrop under source). Unhandled paints (rotate/skew, the
779
+ // variable variants) are skipped. `seen` guards against cyclic references.
780
+ walkColrLayers(paintOffset, m, seen) {
781
+ const key = paintOffset;
782
+ if (seen.has(key))
783
+ return [];
784
+ seen.add(key);
785
+ const format = u8(this.data, paintOffset);
786
+ const child = () => paintOffset + this.u24(paintOffset + 1);
787
+ const descend = (t) => this.walkColrLayers(child(), compose(m, t), seen);
788
+ switch (format) {
789
+ case 1: {
790
+ // PaintColrLayers: numLayers (u8) starting at firstLayerIndex (u32) into the LayerList.
791
+ const numLayers = u8(this.data, paintOffset + 1);
792
+ const first = u32(this.data, paintOffset + 2);
793
+ const out = [];
794
+ for (let i = 0; i < numLayers; i++) {
795
+ const layerPaint = this.colrLayerListOffset +
796
+ u32(this.data, this.colrLayerListOffset + 4 + (first + i) * 4);
797
+ out.push(...this.walkColrLayers(layerPaint, m, new Set(seen)));
798
+ }
799
+ return out;
800
+ }
801
+ case 10: {
802
+ // PaintGlyph: paint (Offset24 to the fill) + glyphID (u16). The fill paints inside the glyph.
803
+ const fill = this.resolveFill(paintOffset + this.u24(paintOffset + 1), IDENTITY);
804
+ if (!fill)
805
+ return [];
806
+ const layer = { glyphId: u16(this.data, paintOffset + 4), paint: fill };
807
+ if (!isIdentity(m))
808
+ layer.transform = m;
809
+ return [layer];
810
+ }
811
+ case 11: {
812
+ // PaintColrGlyph: draw another base glyph's paint graph under the current transform.
813
+ const ref = this.colrBaseV1.get(u16(this.data, paintOffset + 1));
814
+ return ref !== undefined ? this.walkColrLayers(ref, m, new Set(seen)) : [];
815
+ }
816
+ case 12: // PaintTransform: child + an Affine2x3 (six Fixed 16.16 values).
817
+ return descend(this.readAffine(paintOffset + this.u24(paintOffset + 4)));
818
+ case 14: // PaintTranslate: dx, dy (FWORD).
819
+ return descend([
820
+ 1,
821
+ 0,
822
+ 0,
823
+ 1,
824
+ i16(this.data, paintOffset + 4),
825
+ i16(this.data, paintOffset + 6),
826
+ ]);
827
+ case 16: // PaintScale: scaleX, scaleY (F2Dot14).
828
+ return descend([this.f2dot14(paintOffset + 4), 0, 0, this.f2dot14(paintOffset + 6), 0, 0]);
829
+ case 18: // PaintScaleAroundCenter: scaleX, scaleY, centerX, centerY.
830
+ return descend(aroundCenter([this.f2dot14(paintOffset + 4), 0, 0, this.f2dot14(paintOffset + 6), 0, 0], i16(this.data, paintOffset + 8), i16(this.data, paintOffset + 10)));
831
+ case 20: {
832
+ // PaintScaleUniform: a single scale for both axes.
833
+ const s = this.f2dot14(paintOffset + 4);
834
+ return descend([s, 0, 0, s, 0, 0]);
835
+ }
836
+ case 22: {
837
+ // PaintScaleUniformAroundCenter: uniform scale about a center.
838
+ const s = this.f2dot14(paintOffset + 4);
839
+ return descend(aroundCenter([s, 0, 0, s, 0, 0], i16(this.data, paintOffset + 6), i16(this.data, paintOffset + 8)));
840
+ }
841
+ case 32: {
842
+ // PaintComposite: sourcePaint, mode (u8), backdropPaint. We treat every mode as source-over:
843
+ // draw the backdrop underneath, then the source on top (correct for the common case).
844
+ const source = paintOffset + this.u24(paintOffset + 1);
845
+ const backdrop = paintOffset + this.u24(paintOffset + 5);
846
+ return [
847
+ ...this.walkColrLayers(backdrop, m, new Set(seen)),
848
+ ...this.walkColrLayers(source, m, new Set(seen)),
849
+ ];
850
+ }
851
+ default:
852
+ return []; // a bare fill with no glyph, or a paint format not supported yet
853
+ }
854
+ }
855
+ // Resolves the fill under a PaintGlyph to a solid or gradient `Paint`, descending through any
856
+ // transform paints and baking the accumulated affine `t` into the gradient's coordinates (so the
857
+ // renderer only has to apply the OUTER glyph transform). Returns null for a still-unsupported fill.
858
+ resolveFill(paintOffset, t) {
859
+ const format = u8(this.data, paintOffset);
860
+ const child = () => paintOffset + this.u24(paintOffset + 1);
861
+ const p = (x, y) => applyAffine(t, x, y);
862
+ switch (format) {
863
+ case 2: {
864
+ // PaintSolid: paletteIndex (u16) + alpha (F2Dot14). A solid fill ignores the transform.
865
+ const paletteIndex = u16(this.data, paintOffset + 1);
866
+ return {
867
+ type: "solid",
868
+ color: this.paletteColor(paletteIndex, this.f2dot14(paintOffset + 3)),
869
+ };
870
+ }
871
+ case 4: {
872
+ // PaintLinearGradient: a color line + p0/p1 (a rotation point p2 we treat axially).
873
+ const line = this.readColorLine(paintOffset + this.u24(paintOffset + 1));
874
+ return {
875
+ type: "linearGradient",
876
+ p0: p(i16(this.data, paintOffset + 4), i16(this.data, paintOffset + 6)),
877
+ p1: p(i16(this.data, paintOffset + 8), i16(this.data, paintOffset + 10)),
878
+ stops: line.stops,
879
+ extend: line.extend,
880
+ };
881
+ }
882
+ case 6: {
883
+ // PaintRadialGradient: a color line + two circles. Radii scale by the affine's mean scale.
884
+ const line = this.readColorLine(paintOffset + this.u24(paintOffset + 1));
885
+ const rScale = detScale(t);
886
+ const [c0x, c0y] = p(i16(this.data, paintOffset + 4), i16(this.data, paintOffset + 6));
887
+ const [c1x, c1y] = p(i16(this.data, paintOffset + 10), i16(this.data, paintOffset + 12));
888
+ return {
889
+ type: "radialGradient",
890
+ c0: [c0x, c0y, u16(this.data, paintOffset + 8) * rScale],
891
+ c1: [c1x, c1y, u16(this.data, paintOffset + 14) * rScale],
892
+ stops: line.stops,
893
+ extend: line.extend,
894
+ };
895
+ }
896
+ // Transform paints wrapping the fill: compose into `t` and descend to the actual gradient.
897
+ case 12:
898
+ return this.resolveFill(child(), compose(t, this.readAffine(paintOffset + this.u24(paintOffset + 4))));
899
+ case 14:
900
+ return this.resolveFill(child(), compose(t, [
901
+ 1,
902
+ 0,
903
+ 0,
904
+ 1,
905
+ i16(this.data, paintOffset + 4),
906
+ i16(this.data, paintOffset + 6),
907
+ ]));
908
+ case 16:
909
+ return this.resolveFill(child(), compose(t, [this.f2dot14(paintOffset + 4), 0, 0, this.f2dot14(paintOffset + 6), 0, 0]));
910
+ case 18:
911
+ return this.resolveFill(child(), compose(t, aroundCenter([this.f2dot14(paintOffset + 4), 0, 0, this.f2dot14(paintOffset + 6), 0, 0], i16(this.data, paintOffset + 8), i16(this.data, paintOffset + 10))));
912
+ case 20: {
913
+ const s = this.f2dot14(paintOffset + 4);
914
+ return this.resolveFill(child(), compose(t, [s, 0, 0, s, 0, 0]));
915
+ }
916
+ case 22: {
917
+ const s = this.f2dot14(paintOffset + 4);
918
+ return this.resolveFill(child(), compose(t, aroundCenter([s, 0, 0, s, 0, 0], i16(this.data, paintOffset + 6), i16(this.data, paintOffset + 8))));
919
+ }
920
+ default:
921
+ return null;
922
+ }
923
+ }
924
+ // A ColorLine: an extend mode then color stops (position + palette color).
925
+ readColorLine(o) {
926
+ const extend = ["pad", "repeat", "reflect"][u8(this.data, o)] ?? "pad";
927
+ const numStops = u16(this.data, o + 1);
928
+ const stops = [];
929
+ for (let i = 0; i < numStops; i++) {
930
+ const p = o + 3 + i * 6;
931
+ stops.push({
932
+ offset: this.f2dot14(p),
933
+ color: this.paletteColor(u16(this.data, p + 2), this.f2dot14(p + 4)),
934
+ });
935
+ }
936
+ return { stops, extend };
937
+ }
938
+ // A CPAL palette color scaled by a gradient/solid alpha (0..1). paletteIndex 0xFFFF is the spec's
939
+ // "use the text foreground color" sentinel -> null.
940
+ paletteColor(paletteIndex, alpha) {
941
+ if (paletteIndex === 0xffff)
942
+ return null;
943
+ const c = this.palette[paletteIndex];
944
+ if (!c)
945
+ return null;
946
+ return { r: c.r, g: c.g, b: c.b, a: Math.round(c.a * alpha) };
947
+ }
948
+ // A big-endian 24-bit offset (used for a paint's sub-offsets).
949
+ u24(o) {
950
+ return (u8(this.data, o) << 16) | (u8(this.data, o + 1) << 8) | u8(this.data, o + 2);
951
+ }
952
+ // An F2Dot14 fixed-point number (signed, 2 integer + 14 fraction bits) as a float.
953
+ f2dot14(o) {
954
+ return i16(this.data, o) / 16384;
955
+ }
956
+ // A signed 32-bit big-endian integer.
957
+ i32(o) {
958
+ const v = u32(this.data, o);
959
+ return v >= 0x80000000 ? v - 0x100000000 : v;
960
+ }
961
+ // An Affine2x3 struct: six Fixed (16.16) values xx, yx, xy, yy, dx, dy -> [a, b, c, d, e, f].
962
+ readAffine(o) {
963
+ const fixed = (p) => this.i32(p) / 65536;
964
+ return [fixed(o), fixed(o + 4), fixed(o + 8), fixed(o + 12), fixed(o + 16), fixed(o + 20)];
965
+ }
966
+ // COLR header (v0 and v1). v0: base-glyph + layer records (solid). v1 (version >= 1): additionally
967
+ // a BaseGlyphList of paint graphs + a LayerList they index into.
968
+ readColr() {
969
+ const o = this.table("COLR").offset;
970
+ const version = u16(this.data, o);
971
+ const numBaseGlyphs = u16(this.data, o + 2);
972
+ const baseOffset = o + u32(this.data, o + 4);
973
+ this.colrLayersOffset = o + u32(this.data, o + 8);
974
+ for (let i = 0; i < numBaseGlyphs; i++) {
975
+ const p = baseOffset + i * 6;
976
+ this.colrBase.set(u16(this.data, p), {
977
+ first: u16(this.data, p + 2),
978
+ count: u16(this.data, p + 4),
979
+ });
980
+ }
981
+ if (version < 1)
982
+ return;
983
+ // v1 fields follow the v0 ones: BaseGlyphList (glyphID -> paint offset) + LayerList.
984
+ const baseGlyphListOffset = u32(this.data, o + 14);
985
+ const layerListOffset = u32(this.data, o + 18);
986
+ if (layerListOffset)
987
+ this.colrLayerListOffset = o + layerListOffset;
988
+ if (baseGlyphListOffset) {
989
+ const listStart = o + baseGlyphListOffset;
990
+ const numRecords = u32(this.data, listStart);
991
+ for (let i = 0; i < numRecords; i++) {
992
+ const p = listStart + 4 + i * 6;
993
+ this.colrBaseV1.set(u16(this.data, p), listStart + u32(this.data, p + 2));
994
+ }
995
+ }
996
+ }
997
+ // CPAL: reads palette 0 into `palette`. Color records are BGRA bytes; colorRecordIndices[0] is
998
+ // where palette 0 starts in the shared record array.
999
+ readCpal() {
1000
+ const o = this.table("CPAL").offset;
1001
+ const numPaletteEntries = u16(this.data, o + 2);
1002
+ const recordsOffset = o + u32(this.data, o + 8);
1003
+ const firstRecord = u16(this.data, o + 12); // colorRecordIndices[0], the start of palette 0
1004
+ for (let i = 0; i < numPaletteEntries; i++) {
1005
+ const p = recordsOffset + (firstRecord + i) * 4;
1006
+ this.palette.push({
1007
+ b: u8(this.data, p),
1008
+ g: u8(this.data, p + 1),
1009
+ r: u8(this.data, p + 2),
1010
+ a: u8(this.data, p + 3),
1011
+ });
1012
+ }
1013
+ }
190
1014
  }
191
- exports.TTFParser = TTFParser;