@stll/folio-core 0.32.2 → 0.33.1

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 (227) hide show
  1. package/dist/ai-edits/__fixtures__/paragraphs.d.ts +17 -0
  2. package/dist/ai-edits/__fixtures__/paragraphs.js +33 -0
  3. package/dist/ai-edits/apply.d.ts +49 -3
  4. package/dist/ai-edits/apply.js +652 -86
  5. package/dist/ai-edits/blockRange.d.ts +7 -3
  6. package/dist/ai-edits/blockRange.js +14 -6
  7. package/dist/ai-edits/headless.d.ts +39 -5
  8. package/dist/ai-edits/headless.js +76 -17
  9. package/dist/ai-edits/index.d.ts +6 -6
  10. package/dist/ai-edits/index.js +3 -3
  11. package/dist/ai-edits/read.d.ts +4 -0
  12. package/dist/ai-edits/read.js +30 -6
  13. package/dist/ai-edits/scoped-reading.js +4 -3
  14. package/dist/ai-edits/snapshot.d.ts +42 -2
  15. package/dist/ai-edits/snapshot.js +127 -62
  16. package/dist/ai-edits/table-row-column-mutations.d.ts +24 -1
  17. package/dist/ai-edits/table-row-column-mutations.js +98 -6
  18. package/dist/ai-edits/table-targets.d.ts +12 -3
  19. package/dist/ai-edits/table-targets.js +15 -5
  20. package/dist/ai-edits/types.d.ts +219 -15
  21. package/dist/ai-edits/word-diff.d.ts +54 -11
  22. package/dist/ai-edits/word-diff.js +221 -61
  23. package/dist/compare/__fixtures__/body-sequence.d.ts +56 -0
  24. package/dist/compare/__fixtures__/body-sequence.js +136 -0
  25. package/dist/compare/__fixtures__/nested-table.d.ts +16 -0
  26. package/dist/compare/__fixtures__/nested-table.js +50 -0
  27. package/dist/compare/__fixtures__/numbered-list.d.ts +34 -0
  28. package/dist/compare/__fixtures__/numbered-list.js +85 -0
  29. package/dist/compare/column-alignment.d.ts +27 -0
  30. package/dist/compare/column-alignment.js +162 -0
  31. package/dist/compare/compare.d.ts +107 -0
  32. package/dist/compare/compare.js +412 -0
  33. package/dist/compare/formatting.d.ts +29 -0
  34. package/dist/compare/formatting.js +84 -0
  35. package/dist/compare/plan.d.ts +22 -0
  36. package/dist/compare/plan.js +1141 -0
  37. package/dist/compare/reproducible-package.d.ts +21 -0
  38. package/dist/compare/reproducible-package.js +47 -0
  39. package/dist/compare/scenario.d.ts +77 -0
  40. package/dist/compare/scenario.js +263 -0
  41. package/dist/compare/types.d.ts +297 -0
  42. package/dist/compare/types.js +42 -0
  43. package/dist/compare/verification.d.ts +83 -0
  44. package/dist/compare/verification.js +189 -0
  45. package/dist/compat/eigenpal.d.ts +9 -6
  46. package/dist/compat/eigenpal.js +6 -3
  47. package/dist/controller/layoutPipeline.d.ts +13 -0
  48. package/dist/controller/layoutPipeline.js +15 -1
  49. package/dist/display-list/build/buildContext.d.ts +39 -0
  50. package/dist/display-list/build/buildContext.js +31 -0
  51. package/dist/display-list/build/buildDisplayList.d.ts +79 -0
  52. package/dist/display-list/build/buildDisplayList.js +283 -0
  53. package/dist/display-list/build/colors.d.ts +20 -0
  54. package/dist/display-list/build/colors.js +136 -0
  55. package/dist/display-list/build/floatingImages.d.ts +24 -0
  56. package/dist/display-list/build/floatingImages.js +41 -0
  57. package/dist/display-list/build/fontTable.d.ts +31 -0
  58. package/dist/display-list/build/fontTable.js +97 -0
  59. package/dist/display-list/build/furniture.d.ts +86 -0
  60. package/dist/display-list/build/furniture.js +98 -0
  61. package/dist/display-list/build/glyphs.d.ts +55 -0
  62. package/dist/display-list/build/glyphs.js +103 -0
  63. package/dist/display-list/build/headerFooterPrimitives.d.ts +17 -0
  64. package/dist/display-list/build/headerFooterPrimitives.js +40 -0
  65. package/dist/display-list/build/imagePrimitives.d.ts +35 -0
  66. package/dist/display-list/build/imagePrimitives.js +264 -0
  67. package/dist/display-list/build/pageBorderPrimitives.d.ts +23 -0
  68. package/dist/display-list/build/pageBorderPrimitives.js +111 -0
  69. package/dist/display-list/build/pageFurniture.d.ts +35 -0
  70. package/dist/display-list/build/pageFurniture.js +141 -0
  71. package/dist/display-list/build/paragraphPrimitives.d.ts +19 -0
  72. package/dist/display-list/build/paragraphPrimitives.js +819 -0
  73. package/dist/display-list/build/regions.d.ts +46 -0
  74. package/dist/display-list/build/regions.js +56 -0
  75. package/dist/display-list/build/storyPrimitives.d.ts +29 -0
  76. package/dist/display-list/build/storyPrimitives.js +279 -0
  77. package/dist/display-list/build/strokes.d.ts +19 -0
  78. package/dist/display-list/build/strokes.js +78 -0
  79. package/dist/display-list/build/tablePrimitives.d.ts +38 -0
  80. package/dist/display-list/build/tablePrimitives.js +409 -0
  81. package/dist/display-list/build/textBoxPrimitives.d.ts +14 -0
  82. package/dist/display-list/build/textBoxPrimitives.js +129 -0
  83. package/dist/display-list/build/textDecorations.d.ts +29 -0
  84. package/dist/display-list/build/textDecorations.js +29 -0
  85. package/dist/display-list/build/unsupported.d.ts +57 -0
  86. package/dist/display-list/build/unsupported.js +0 -0
  87. package/dist/display-list/build/watermarkPrimitives.d.ts +15 -0
  88. package/dist/display-list/build/watermarkPrimitives.js +128 -0
  89. package/dist/display-list/dom/renderDisplayListToDom.d.ts +26 -0
  90. package/dist/display-list/dom/renderDisplayListToDom.js +620 -0
  91. package/dist/display-list/editor/displayListPagePainter.d.ts +22 -0
  92. package/dist/display-list/editor/displayListPagePainter.js +63 -0
  93. package/dist/display-list/editor/pageRenderer.d.ts +24 -0
  94. package/dist/display-list/editor/pageRenderer.js +36 -0
  95. package/dist/display-list/primitives.d.ts +95 -0
  96. package/dist/display-list/primitives.js +127 -0
  97. package/dist/display-list/types.d.ts +472 -0
  98. package/dist/display-list/types.js +0 -0
  99. package/dist/document-operations.d.ts +39 -8
  100. package/dist/document-operations.js +169 -16
  101. package/dist/document-stories.d.ts +16 -0
  102. package/dist/document-stories.js +48 -4
  103. package/dist/docx/hyperlinkParser.d.ts +9 -1
  104. package/dist/docx/hyperlinkParser.js +19 -13
  105. package/dist/docx/packageParts.d.ts +35 -0
  106. package/dist/docx/packageParts.js +120 -0
  107. package/dist/docx/paraIdRangeNormalization.d.ts +40 -0
  108. package/dist/docx/paraIdRangeNormalization.js +64 -0
  109. package/dist/docx/paragraphParser.js +101 -26
  110. package/dist/docx/paragraphTraversal.js +1 -1
  111. package/dist/docx/renderedPageBreakNormalization.js +3 -1
  112. package/dist/docx/revisionIdNormalization.d.ts +27 -0
  113. package/dist/docx/revisionIdNormalization.js +121 -0
  114. package/dist/docx/rezip.d.ts +6 -0
  115. package/dist/docx/rezip.js +51 -19
  116. package/dist/docx/selectiveSave.js +2 -2
  117. package/dist/docx/serializer/commentSerializer.js +6 -1
  118. package/dist/docx/serializer/paragraphSerializer.js +73 -20
  119. package/dist/docx/serializer/runSerializer.js +11 -10
  120. package/dist/docx/serializer/tableSerializer.js +26 -10
  121. package/dist/docx/server/applyDocxXmlPatchProposal.js +1 -1
  122. package/dist/docx/server/createBilingualDocx.js +3 -2
  123. package/dist/docx/streamingXmlParser.d.ts +13 -1
  124. package/dist/docx/streamingXmlParser.js +49 -11
  125. package/dist/docx/unzip.d.ts +1 -2
  126. package/dist/docx/unzip.js +25 -6
  127. package/dist/docx/xmlParser.d.ts +3 -1
  128. package/dist/docx/xmlParser.js +33 -27
  129. package/dist/export-pdf.d.ts +43 -0
  130. package/dist/export-pdf.js +98 -0
  131. package/dist/fonts/headlessMeasure.d.ts +76 -0
  132. package/dist/fonts/headlessMeasure.js +0 -0
  133. package/dist/fonts/sfnt/parse.d.ts +51 -0
  134. package/dist/fonts/sfnt/parse.js +525 -0
  135. package/dist/fonts/sfnt/subset.d.ts +20 -0
  136. package/dist/fonts/sfnt/subset.js +350 -0
  137. package/dist/fonts/sfnt/tables.d.ts +88 -0
  138. package/dist/fonts/sfnt/tables.js +131 -0
  139. package/dist/fonts/sfnt/woff.d.ts +14 -0
  140. package/dist/fonts/sfnt/woff.js +163 -0
  141. package/dist/generated/text_shaper.js +324 -0
  142. package/dist/generated/text_shaper_bg.wasm +0 -0
  143. package/dist/headless-layout.d.ts +75 -0
  144. package/dist/headless-layout.js +350 -0
  145. package/dist/index.d.ts +9 -6
  146. package/dist/index.js +6 -3
  147. package/dist/layout-bridge/convert/footnoteLayout.d.ts +1 -0
  148. package/dist/layout-bridge/convert/footnoteLayout.js +1 -0
  149. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -0
  150. package/dist/layout-bridge/convert/headerFooterLayout.js +2 -0
  151. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +3 -1
  152. package/dist/layout-bridge/convert/toFlowBlocks.js +2 -0
  153. package/dist/layout-engine/measure/advanceComposition.d.ts +62 -0
  154. package/dist/layout-engine/measure/advanceComposition.js +148 -0
  155. package/dist/layout-engine/measure/cache.d.ts +2 -0
  156. package/dist/layout-engine/measure/measureContainer.js +13 -0
  157. package/dist/layout-engine/measure/measureParagraph.js +1 -2
  158. package/dist/layout-engine/measure/measureTypes.d.ts +12 -0
  159. package/dist/layout-engine/measure/tableInlinePlacement.d.ts +3 -2
  160. package/dist/layout-engine/measure/tableInlinePlacement.js +14 -1
  161. package/dist/layout-engine/tableIndentCompatibility.d.ts +5 -0
  162. package/dist/layout-engine/tableIndentCompatibility.js +15 -0
  163. package/dist/layout-engine/types.d.ts +8 -0
  164. package/dist/layout-painter/renderPage.d.ts +29 -1
  165. package/dist/layout-painter/renderPage.js +47 -19
  166. package/dist/layout-painter/renderParagraph.d.ts +21 -2
  167. package/dist/layout-painter/renderParagraph.js +1 -1
  168. package/dist/layout-painter/renderTable.js +37 -4
  169. package/dist/managers/editorShortcuts.d.ts +33 -1
  170. package/dist/managers/editorShortcuts.js +20 -1
  171. package/dist/markdown/renderRuns.js +7 -2
  172. package/dist/markdown/renderTable.js +5 -1
  173. package/dist/model.d.ts +3 -3
  174. package/dist/model.js +2 -2
  175. package/dist/pdf/contentStream.d.ts +73 -0
  176. package/dist/pdf/contentStream.js +143 -0
  177. package/dist/pdf/fonts.d.ts +96 -0
  178. package/dist/pdf/fonts.js +593 -0
  179. package/dist/pdf/images.d.ts +28 -0
  180. package/dist/pdf/images.js +439 -0
  181. package/dist/pdf/objects.d.ts +93 -0
  182. package/dist/pdf/objects.js +237 -0
  183. package/dist/pdf/pageSpace.d.ts +43 -0
  184. package/dist/pdf/pageSpace.js +57 -0
  185. package/dist/pdf/paint.d.ts +32 -0
  186. package/dist/pdf/paint.js +442 -0
  187. package/dist/pdf/writePdf.d.ts +56 -0
  188. package/dist/pdf/writePdf.js +345 -0
  189. package/dist/prosemirror/commands/comments.js +96 -6
  190. package/dist/prosemirror/conversion/fromProseDoc.js +41 -7
  191. package/dist/prosemirror/conversion/toProseDoc.js +41 -4
  192. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.d.ts +15 -2
  193. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +35 -6
  194. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +2 -2
  195. package/dist/prosemirror/extensions/features/pasteCleanup.js +4 -3
  196. package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.d.ts +3 -1
  197. package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.js +5 -3
  198. package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.d.ts +7 -1
  199. package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.js +9 -3
  200. package/dist/prosemirror/extensions/nodes/TableExtension.js +56 -39
  201. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.d.ts +3 -1
  202. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.js +5 -3
  203. package/dist/prosemirror/plugins/suggestionMode.js +1 -0
  204. package/dist/prosemirror/revisionCarriers.js +19 -2
  205. package/dist/prosemirror/utils/extractTrackedChanges.d.ts +2 -2
  206. package/dist/prosemirror/utils/extractTrackedChanges.js +59 -23
  207. package/dist/prosemirror/validation.js +55 -27
  208. package/dist/prosemirror/zeroWidthAnchors.d.ts +10 -0
  209. package/dist/prosemirror/zeroWidthAnchors.js +23 -0
  210. package/dist/redline.js +12 -79
  211. package/dist/server.d.ts +3 -3
  212. package/dist/server.js +2 -2
  213. package/dist/shaping/placeRun.d.ts +51 -0
  214. package/dist/shaping/placeRun.js +84 -0
  215. package/dist/shaping/shaper.d.ts +92 -0
  216. package/dist/shaping/shaper.js +102 -0
  217. package/dist/types/block-id.d.ts +18 -2
  218. package/dist/types/block-id.js +49 -5
  219. package/dist/types/content.d.ts +2 -2
  220. package/dist/utils/clipboard.js +4 -3
  221. package/dist/utils/fontResolver.js +183 -0
  222. package/dist/utils/htmlComments.d.ts +17 -0
  223. package/dist/utils/htmlComments.js +22 -0
  224. package/dist/utils/scriptSegments.d.ts +12 -1
  225. package/dist/utils/scriptSegments.js +17 -1
  226. package/dist/version-comparison.d.ts +1 -1
  227. package/package.json +7 -3
@@ -0,0 +1,350 @@
1
+ import { SFNT_TABLE, SFNT_VERSION, fitsInView, readLocaOffsets, readSfntDirectory } from "./tables.js";
2
+ import { Result, TaggedError, panic } from "better-result";
3
+ //#region src/fonts/sfnt/subset.ts
4
+ /**
5
+ * TrueType (`glyf`) subsetting.
6
+ *
7
+ * The output is what a PDF CIDFontType2 needs and nothing else: glyph
8
+ * outlines, metrics, and the hinting tables that go with them. `cmap`, `name`
9
+ * and `post` are dropped, because a CIDFontType2 addresses glyphs by index
10
+ * through its own CIDToGIDMap and never consults the font's own character
11
+ * mapping.
12
+ *
13
+ * Determinism contract: `subsetTrueType(font, glyphIds)` is a pure function of
14
+ * its arguments and produces byte-identical output across runs. Glyph ids are
15
+ * iterated in ascending numeric order and tables are written in tag order, so
16
+ * nothing depends on a Set's or Map's insertion order.
17
+ */
18
+ var SubsetError = class extends TaggedError("SubsetError") {};
19
+ const fail = (message) => Result.err(new SubsetError({ message }));
20
+ /** .notdef is glyph 0 in every font and is always kept. */
21
+ const NOTDEF_GLYPH_ID = 0;
22
+ const HEAD = {
23
+ checkSumAdjustment: 8,
24
+ indexToLocFormat: 50,
25
+ size: 54
26
+ };
27
+ const HHEA = {
28
+ numberOfHMetrics: 34,
29
+ size: 36
30
+ };
31
+ const MAXP = {
32
+ numGlyphs: 4,
33
+ size: 6
34
+ };
35
+ /** One hmtx `longHorMetric`: advanceWidth (uint16) then lsb (int16). */
36
+ const HMTX_METRIC_SIZE = 4;
37
+ /** Glyph header: numberOfContours, then the four bounding-box int16s. */
38
+ const GLYPH_HEADER_SIZE = 10;
39
+ const COMPOSITE_CONTOUR_COUNT = -1;
40
+ /** Flags of a composite glyph component record. */
41
+ const COMPONENT_FLAG = {
42
+ arg1And2AreWords: 1,
43
+ weHaveAScale: 8,
44
+ moreComponents: 32,
45
+ weHaveAnXAndYScale: 64,
46
+ weHaveATwoByTwo: 128
47
+ };
48
+ /**
49
+ * Short `loca` stores offsets halved in a uint16, so it can only address
50
+ * 0x1FFFE bytes of `glyf`. Beyond that the long format is mandatory.
51
+ */
52
+ const MAX_SHORT_LOCA_GLYF_LENGTH = 131070;
53
+ /** `head.checkSumAdjustment` is this constant minus the whole-file checksum. */
54
+ const CHECKSUM_ADJUSTMENT_MAGIC = 2981146554;
55
+ const UINT32_MODULUS = 4294967296;
56
+ /** Tables copied byte for byte when the source font has them. */
57
+ const VERBATIM_TABLES = [
58
+ SFNT_TABLE.cvt,
59
+ SFNT_TABLE.fpgm,
60
+ SFNT_TABLE.prep
61
+ ];
62
+ const alignUp = (value) => Math.ceil(value / 4) * 4;
63
+ const readCompositeComponents = (view, start, end) => {
64
+ const glyphIndexOffsets = [];
65
+ const glyphIds = [];
66
+ let at = start + GLYPH_HEADER_SIZE;
67
+ for (;;) {
68
+ if (at + 4 > end || !fitsInView(view, at, 4)) return;
69
+ const flags = view.getUint16(at);
70
+ glyphIndexOffsets.push(at + 2);
71
+ glyphIds.push(view.getUint16(at + 2));
72
+ at += 4;
73
+ at += (flags & COMPONENT_FLAG.arg1And2AreWords) === 0 ? 2 : 4;
74
+ if ((flags & COMPONENT_FLAG.weHaveAScale) !== 0) at += 2;
75
+ else if ((flags & COMPONENT_FLAG.weHaveAnXAndYScale) !== 0) at += 4;
76
+ else if ((flags & COMPONENT_FLAG.weHaveATwoByTwo) !== 0) at += 8;
77
+ if (at > end) return;
78
+ if ((flags & COMPONENT_FLAG.moreComponents) === 0) return {
79
+ glyphIndexOffsets,
80
+ glyphIds
81
+ };
82
+ }
83
+ };
84
+ /** Byte range of one glyph inside `glyf`, or `undefined` for an empty glyph. */
85
+ const glyphRange = ({ glyf, offsets }, glyphId) => {
86
+ const start = offsets[glyphId];
87
+ const end = offsets[glyphId + 1];
88
+ if (start === void 0 || end === void 0 || end <= start || end > glyf.length) return;
89
+ return {
90
+ start: glyf.offset + start,
91
+ end: glyf.offset + end
92
+ };
93
+ };
94
+ /**
95
+ * The requested glyphs plus every glyph reachable through composite component
96
+ * references, plus .notdef, sorted ascending.
97
+ */
98
+ const closeOverComponents = (source, requested) => {
99
+ const selected = /* @__PURE__ */ new Set([NOTDEF_GLYPH_ID]);
100
+ const queue = [NOTDEF_GLYPH_ID];
101
+ for (const glyphId of [...requested].sort((left, right) => left - right)) {
102
+ if (!Number.isInteger(glyphId) || glyphId < 0 || glyphId >= source.numGlyphs) return fail(`glyph id ${glyphId} is outside 0..${source.numGlyphs - 1}`);
103
+ if (!selected.has(glyphId)) {
104
+ selected.add(glyphId);
105
+ queue.push(glyphId);
106
+ }
107
+ }
108
+ for (let glyphId = queue.pop(); glyphId !== void 0; glyphId = queue.pop()) {
109
+ const range = glyphRange(source, glyphId);
110
+ if (!range) continue;
111
+ if (range.end - range.start < GLYPH_HEADER_SIZE || !fitsInView(source.view, range.start, GLYPH_HEADER_SIZE)) return fail(`glyph ${glyphId} is shorter than its ${GLYPH_HEADER_SIZE}-byte header`);
112
+ if (source.view.getInt16(range.start) > COMPOSITE_CONTOUR_COUNT) continue;
113
+ const components = readCompositeComponents(source.view, range.start, range.end);
114
+ if (!components) return fail(`composite glyph ${glyphId} has a truncated component record`);
115
+ for (const component of components.glyphIds) {
116
+ if (component >= source.numGlyphs) return fail(`composite glyph ${glyphId} references glyph ${component}, which does not exist`);
117
+ if (!selected.has(component)) {
118
+ selected.add(component);
119
+ queue.push(component);
120
+ }
121
+ }
122
+ }
123
+ return Result.ok([...selected].sort((left, right) => left - right));
124
+ };
125
+ /**
126
+ * Copies the selected glyphs into a new `glyf`, renumbering the component
127
+ * glyph index of every composite record on the way. That uint16 sits directly
128
+ * after each component's flags word, and leaving it pointing at an old id is
129
+ * the classic way a subset renders as garbage.
130
+ */
131
+ const buildGlyf = (source, glyphIds, glyphIdMap) => {
132
+ const parts = [];
133
+ const locaOffsets = [];
134
+ let total = 0;
135
+ for (const glyphId of glyphIds) {
136
+ locaOffsets.push(total);
137
+ const range = glyphRange(source, glyphId);
138
+ if (!range) continue;
139
+ const length = range.end - range.start;
140
+ const data = new Uint8Array(alignUp(length));
141
+ data.set(new Uint8Array(source.view.buffer, source.view.byteOffset + range.start, length));
142
+ if (source.view.getInt16(range.start) <= COMPOSITE_CONTOUR_COUNT) {
143
+ const components = readCompositeComponents(source.view, range.start, range.end);
144
+ if (!components) return fail(`composite glyph ${glyphId} has a truncated component record`);
145
+ const copied = new DataView(data.buffer);
146
+ for (const offset of components.glyphIndexOffsets) {
147
+ const oldComponent = source.view.getUint16(offset);
148
+ const newComponent = glyphIdMap.get(oldComponent);
149
+ if (newComponent === void 0) panic(`component glyph ${oldComponent} escaped the subset closure`);
150
+ copied.setUint16(offset - range.start, newComponent);
151
+ }
152
+ }
153
+ parts.push(data);
154
+ total += data.byteLength;
155
+ }
156
+ locaOffsets.push(total);
157
+ const glyf = new Uint8Array(total);
158
+ let at = 0;
159
+ for (const part of parts) {
160
+ glyf.set(part, at);
161
+ at += part.byteLength;
162
+ }
163
+ return Result.ok({
164
+ glyf,
165
+ locaOffsets
166
+ });
167
+ };
168
+ const buildLoca = (offsets, longFormat) => {
169
+ const entrySize = longFormat ? 4 : 2;
170
+ const loca = new Uint8Array(offsets.length * entrySize);
171
+ const view = new DataView(loca.buffer);
172
+ for (const [index, offset] of offsets.entries()) if (longFormat) view.setUint32(index * entrySize, offset);
173
+ else view.setUint16(index * entrySize, offset / 2);
174
+ return loca;
175
+ };
176
+ /**
177
+ * Left side bearing of one glyph in the source font: from its longHorMetric
178
+ * when it has one, else from the trailing int16 array. Returns 0 when the
179
+ * table is too short, which is what a monospaced tail with no lsb array means.
180
+ */
181
+ const sourceLeftSideBearing = ({ view, hmtx, numberOfHMetrics }, glyphId) => {
182
+ const at = glyphId < numberOfHMetrics ? hmtx.offset + glyphId * HMTX_METRIC_SIZE + 2 : hmtx.offset + numberOfHMetrics * HMTX_METRIC_SIZE + (glyphId - numberOfHMetrics) * 2;
183
+ if (at + 2 > hmtx.offset + hmtx.length || !fitsInView(view, at, 2)) return 0;
184
+ return view.getInt16(at);
185
+ };
186
+ /** Sums a table as big-endian uint32s, zero-padded, modulo 2^32. */
187
+ const checksumOf = (bytes) => {
188
+ let sum = 0;
189
+ for (let at = 0; at < bytes.byteLength; at += 4) {
190
+ const word = (bytes[at] ?? 0) << 24 | (bytes[at + 1] ?? 0) << 16 | (bytes[at + 2] ?? 0) << 8 | (bytes[at + 3] ?? 0);
191
+ sum = (sum + (word >>> 0)) % UINT32_MODULUS;
192
+ }
193
+ return sum >>> 0;
194
+ };
195
+ const searchParametersFor = (numTables) => {
196
+ let entrySelector = 0;
197
+ while (1 << entrySelector + 1 <= numTables) entrySelector += 1;
198
+ const searchRange = (1 << entrySelector) * 16;
199
+ return {
200
+ searchRange,
201
+ entrySelector,
202
+ rangeShift: numTables * 16 - searchRange
203
+ };
204
+ };
205
+ /**
206
+ * Writes the sfnt and stamps `head.checkSumAdjustment`: the field is zero
207
+ * while the file checksum is taken, then set to the magic constant minus that
208
+ * sum, so a validator summing the finished file lands on the magic.
209
+ */
210
+ const assembleSfnt = (tables) => {
211
+ const sorted = [...tables].sort((left, right) => left.tag < right.tag ? -1 : 1);
212
+ const directorySize = sorted.length * 16;
213
+ const bodySize = sorted.reduce((total, table) => total + alignUp(table.data.byteLength), 0);
214
+ const output = new Uint8Array(12 + directorySize + bodySize);
215
+ const view = new DataView(output.buffer);
216
+ const { searchRange, entrySelector, rangeShift } = searchParametersFor(sorted.length);
217
+ view.setUint32(0, SFNT_VERSION.trueType);
218
+ view.setUint16(4, sorted.length);
219
+ view.setUint16(6, searchRange);
220
+ view.setUint16(8, entrySelector);
221
+ view.setUint16(10, rangeShift);
222
+ let entry = 12;
223
+ let dataOffset = 12 + directorySize;
224
+ let headOffset = -1;
225
+ for (const table of sorted) {
226
+ for (let index = 0; index < table.tag.length; index++) view.setUint8(entry + index, table.tag.charCodeAt(index));
227
+ view.setUint32(entry + 4, checksumOf(table.data));
228
+ view.setUint32(entry + 8, dataOffset);
229
+ view.setUint32(entry + 12, table.data.byteLength);
230
+ output.set(table.data, dataOffset);
231
+ if (table.tag === SFNT_TABLE.head) headOffset = dataOffset;
232
+ dataOffset += alignUp(table.data.byteLength);
233
+ entry += 16;
234
+ }
235
+ if (headOffset >= 0) view.setUint32(headOffset + HEAD.checkSumAdjustment, CHECKSUM_ADJUSTMENT_MAGIC - checksumOf(output) >>> 0);
236
+ return output;
237
+ };
238
+ /** A copy of a source table, so edits never touch the caller's buffer. */
239
+ const copyTable = (directory, record) => new Uint8Array(directory.view.buffer.slice(directory.view.byteOffset + record.offset, directory.view.byteOffset + record.offset + record.length));
240
+ const requireTable = (directory, tag, size) => {
241
+ const record = directory.tables.get(tag);
242
+ if (!record) return fail(`font has no '${tag}' table, which a TrueType subset needs`);
243
+ if (record.length < size || !fitsInView(directory.view, record.offset, size)) return fail(`'${tag}' table is ${record.length} bytes, needs at least ${size}`);
244
+ return Result.ok(record);
245
+ };
246
+ /**
247
+ * Build a `glyf`-flavoured subset containing `glyphIds` plus the components
248
+ * they reference transitively, plus .notdef.
249
+ */
250
+ const subsetTrueType = (font, glyphIds) => {
251
+ if (font.isCff) return fail("cannot subset a CFF font: its outlines are charstrings, not glyf data; embed the font whole instead");
252
+ const directoryResult = readSfntDirectory(font.bytes).mapError((error) => new SubsetError({ message: error.message }));
253
+ if (directoryResult.isErr()) return Result.err(directoryResult.error);
254
+ const directory = directoryResult.value;
255
+ const { view } = directory;
256
+ const headResult = requireTable(directory, SFNT_TABLE.head, HEAD.size);
257
+ if (headResult.isErr()) return Result.err(headResult.error);
258
+ const hheaResult = requireTable(directory, SFNT_TABLE.hhea, HHEA.size);
259
+ if (hheaResult.isErr()) return Result.err(hheaResult.error);
260
+ const maxpResult = requireTable(directory, SFNT_TABLE.maxp, MAXP.size);
261
+ if (maxpResult.isErr()) return Result.err(maxpResult.error);
262
+ const hmtxResult = requireTable(directory, SFNT_TABLE.hmtx, HMTX_METRIC_SIZE);
263
+ if (hmtxResult.isErr()) return Result.err(hmtxResult.error);
264
+ const glyfResult = requireTable(directory, SFNT_TABLE.glyf, 0);
265
+ if (glyfResult.isErr()) return Result.err(glyfResult.error);
266
+ const locaResult = requireTable(directory, SFNT_TABLE.loca, 0);
267
+ if (locaResult.isErr()) return Result.err(locaResult.error);
268
+ const head = headResult.value;
269
+ const offsets = readLocaOffsets({
270
+ view,
271
+ loca: locaResult.value,
272
+ numGlyphs: font.numGlyphs,
273
+ longFormat: view.getInt16(head.offset + HEAD.indexToLocFormat) === 1
274
+ });
275
+ if (!offsets) return fail("'loca' is truncated or its offsets are not monotonic");
276
+ const source = {
277
+ view,
278
+ glyf: glyfResult.value,
279
+ offsets,
280
+ numGlyphs: font.numGlyphs
281
+ };
282
+ const selectedResult = closeOverComponents(source, glyphIds);
283
+ if (selectedResult.isErr()) return Result.err(selectedResult.error);
284
+ const selected = selectedResult.value;
285
+ const glyphIdMap = /* @__PURE__ */ new Map();
286
+ for (const [newId, oldId] of selected.entries()) glyphIdMap.set(oldId, newId);
287
+ const glyfResultBuilt = buildGlyf(source, selected, glyphIdMap);
288
+ if (glyfResultBuilt.isErr()) return Result.err(glyfResultBuilt.error);
289
+ const { glyf, locaOffsets } = glyfResultBuilt.value;
290
+ const longLoca = glyf.byteLength > MAX_SHORT_LOCA_GLYF_LENGTH;
291
+ const loca = buildLoca(locaOffsets, longLoca);
292
+ const newHead = copyTable(directory, head);
293
+ const headView = new DataView(newHead.buffer);
294
+ headView.setInt16(HEAD.indexToLocFormat, longLoca ? 1 : 0);
295
+ headView.setUint32(HEAD.checkSumAdjustment, 0);
296
+ const newHhea = copyTable(directory, hheaResult.value);
297
+ new DataView(newHhea.buffer).setUint16(HHEA.numberOfHMetrics, selected.length);
298
+ const newMaxp = copyTable(directory, maxpResult.value);
299
+ new DataView(newMaxp.buffer).setUint16(MAXP.numGlyphs, selected.length);
300
+ const sourceMetrics = {
301
+ view,
302
+ hmtx: hmtxResult.value,
303
+ numberOfHMetrics: view.getUint16(hheaResult.value.offset + HHEA.numberOfHMetrics)
304
+ };
305
+ const newHmtx = new Uint8Array(selected.length * HMTX_METRIC_SIZE);
306
+ const hmtxView = new DataView(newHmtx.buffer);
307
+ for (const [newId, oldId] of selected.entries()) {
308
+ hmtxView.setUint16(newId * HMTX_METRIC_SIZE, font.advanceWidthFor(oldId));
309
+ hmtxView.setInt16(newId * HMTX_METRIC_SIZE + 2, sourceLeftSideBearing(sourceMetrics, oldId));
310
+ }
311
+ const tables = [
312
+ {
313
+ tag: SFNT_TABLE.head,
314
+ data: newHead
315
+ },
316
+ {
317
+ tag: SFNT_TABLE.hhea,
318
+ data: newHhea
319
+ },
320
+ {
321
+ tag: SFNT_TABLE.maxp,
322
+ data: newMaxp
323
+ },
324
+ {
325
+ tag: SFNT_TABLE.hmtx,
326
+ data: newHmtx
327
+ },
328
+ {
329
+ tag: SFNT_TABLE.loca,
330
+ data: loca
331
+ },
332
+ {
333
+ tag: SFNT_TABLE.glyf,
334
+ data: glyf
335
+ }
336
+ ];
337
+ for (const tag of VERBATIM_TABLES) {
338
+ const record = directory.tables.get(tag);
339
+ if (record) tables.push({
340
+ tag,
341
+ data: copyTable(directory, record)
342
+ });
343
+ }
344
+ return Result.ok({
345
+ bytes: assembleSfnt(tables),
346
+ glyphIdMap
347
+ });
348
+ };
349
+ //#endregion
350
+ export { SubsetError, subsetTrueType };
@@ -0,0 +1,88 @@
1
+ import { Result } from "better-result";
2
+ //#region src/fonts/sfnt/tables.d.ts
3
+ /** Four-character table tags this package addresses by name. */
4
+ declare const SFNT_TABLE: {
5
+ readonly cff: "CFF ";
6
+ readonly cmap: "cmap";
7
+ readonly cvt: "cvt ";
8
+ readonly fpgm: "fpgm";
9
+ readonly glyf: "glyf";
10
+ readonly head: "head";
11
+ readonly hhea: "hhea";
12
+ readonly hmtx: "hmtx";
13
+ readonly loca: "loca";
14
+ readonly maxp: "maxp";
15
+ readonly name: "name";
16
+ readonly os2: "OS/2";
17
+ readonly post: "post";
18
+ readonly prep: "prep";
19
+ };
20
+ /**
21
+ * sfnt `version` words (the same field WOFF calls `flavor`), plus the
22
+ * container magics that are *not* a bare sfnt and must be reported as such.
23
+ */
24
+ declare const SFNT_VERSION: {
25
+ /** `0x00010000`: TrueType outlines. */
26
+ readonly trueType: 65536;
27
+ /** `true`: the legacy Apple spelling of the same thing. */
28
+ readonly trueMac: 1953658213;
29
+ /** `OTTO`: OpenType with CFF outlines. */
30
+ readonly openTypeCff: 1330926671;
31
+ /** `ttcf`: a font collection, not a single font. */
32
+ readonly collection: 1953784678;
33
+ /** `wOFF`: WOFF 1.0 container. */
34
+ readonly woff: 2001684038;
35
+ /** `wOF2`: WOFF 2.0 container. */
36
+ readonly woff2: 2001684018;
37
+ };
38
+ /** sfnt header: version, numTables, searchRange, entrySelector, rangeShift. */
39
+ declare const SFNT_HEADER_SIZE = 12;
40
+ /** Directory entry: tag, checksum, offset, length. */
41
+ declare const SFNT_DIRECTORY_ENTRY_SIZE = 16;
42
+ /** Every table starts on a four-byte boundary and is zero-padded to one. */
43
+ declare const SFNT_TABLE_ALIGNMENT = 4;
44
+ declare const SfntDirectoryError_base: import("better-result").TaggedErrorClass<"SfntDirectoryError">;
45
+ /** A structural problem in the container: bad header, or a table out of bounds. */
46
+ declare class SfntDirectoryError extends SfntDirectoryError_base<{
47
+ message: string;
48
+ }> {}
49
+ /** One entry of the table directory, already bounds-checked against the buffer. */
50
+ type SfntTableRecord = {
51
+ readonly tag: string;
52
+ readonly offset: number;
53
+ readonly length: number;
54
+ };
55
+ type SfntDirectory = {
56
+ readonly version: number;
57
+ readonly view: DataView;
58
+ readonly tables: ReadonlyMap<string, SfntTableRecord>;
59
+ };
60
+ /** Whether `[offset, offset + length)` lies wholly inside `view`. */
61
+ declare const fitsInView: (view: DataView, offset: number, length: number) => boolean;
62
+ /** The four-byte tag at `offset`, or `""` when it does not fit. */
63
+ declare const readTag: (view: DataView, offset: number) => string;
64
+ /** The leading version word, or `undefined` when the buffer is shorter than it. */
65
+ declare const readSfntVersion: (bytes: Uint8Array) => number | undefined;
66
+ /**
67
+ * Reads the table directory, validating every entry against the buffer.
68
+ *
69
+ * The caller is expected to have rejected non-sfnt magics first: a collection
70
+ * or WOFF header parsed as a directory yields nonsense offsets and a much
71
+ * worse error message than the container check gives.
72
+ */
73
+ declare const readSfntDirectory: (bytes: Uint8Array) => Result<SfntDirectory, SfntDirectoryError>;
74
+ type LocaOffsetsOptions = {
75
+ readonly view: DataView;
76
+ readonly loca: SfntTableRecord;
77
+ readonly numGlyphs: number;
78
+ /** `head.indexToLocFormat === 1`: offsets are uint32, not uint16 halves. */
79
+ readonly longFormat: boolean;
80
+ };
81
+ /**
82
+ * The `numGlyphs + 1` glyph offsets from `loca`, or `undefined` when the table
83
+ * is too short or its offsets run backwards (either makes glyph extraction
84
+ * meaningless).
85
+ */
86
+ declare const readLocaOffsets: ({ view, loca, numGlyphs, longFormat }: LocaOffsetsOptions) => readonly number[] | undefined;
87
+ //#endregion
88
+ export { SFNT_DIRECTORY_ENTRY_SIZE, SFNT_HEADER_SIZE, SFNT_TABLE, SFNT_TABLE_ALIGNMENT, SFNT_VERSION, SfntDirectory, SfntDirectoryError, SfntTableRecord, fitsInView, readLocaOffsets, readSfntDirectory, readSfntVersion, readTag };
@@ -0,0 +1,131 @@
1
+ import { Result, TaggedError } from "better-result";
2
+ //#region src/fonts/sfnt/tables.ts
3
+ /**
4
+ * Shared sfnt container primitives: table tags, the container magics, the
5
+ * table directory, and the `loca` offset array.
6
+ *
7
+ * `parse.ts`, `woff.ts` and `subset.ts` all address tables through the same
8
+ * directory, and both the parser (deriving cap height from glyph bounds) and
9
+ * the subsetter (copying glyph data) read `loca`. Keeping one reader here
10
+ * means the two cannot disagree about what a truncated or non-monotonic
11
+ * table means.
12
+ */
13
+ /** Four-character table tags this package addresses by name. */
14
+ const SFNT_TABLE = {
15
+ cff: "CFF ",
16
+ cmap: "cmap",
17
+ cvt: "cvt ",
18
+ fpgm: "fpgm",
19
+ glyf: "glyf",
20
+ head: "head",
21
+ hhea: "hhea",
22
+ hmtx: "hmtx",
23
+ loca: "loca",
24
+ maxp: "maxp",
25
+ name: "name",
26
+ os2: "OS/2",
27
+ post: "post",
28
+ prep: "prep"
29
+ };
30
+ /**
31
+ * sfnt `version` words (the same field WOFF calls `flavor`), plus the
32
+ * container magics that are *not* a bare sfnt and must be reported as such.
33
+ */
34
+ const SFNT_VERSION = {
35
+ /** `0x00010000`: TrueType outlines. */
36
+ trueType: 65536,
37
+ /** `true`: the legacy Apple spelling of the same thing. */
38
+ trueMac: 1953658213,
39
+ /** `OTTO`: OpenType with CFF outlines. */
40
+ openTypeCff: 1330926671,
41
+ /** `ttcf`: a font collection, not a single font. */
42
+ collection: 1953784678,
43
+ /** `wOFF`: WOFF 1.0 container. */
44
+ woff: 2001684038,
45
+ /** `wOF2`: WOFF 2.0 container. */
46
+ woff2: 2001684018
47
+ };
48
+ /** sfnt header: version, numTables, searchRange, entrySelector, rangeShift. */
49
+ const SFNT_HEADER_SIZE = 12;
50
+ /** Directory entry: tag, checksum, offset, length. */
51
+ const SFNT_DIRECTORY_ENTRY_SIZE = 16;
52
+ /** Every table starts on a four-byte boundary and is zero-padded to one. */
53
+ const SFNT_TABLE_ALIGNMENT = 4;
54
+ const DIRECTORY_ENTRY = {
55
+ tag: 0,
56
+ checkSum: 4,
57
+ offset: 8,
58
+ length: 12
59
+ };
60
+ const TAG_LENGTH = 4;
61
+ /** A structural problem in the container: bad header, or a table out of bounds. */
62
+ var SfntDirectoryError = class extends TaggedError("SfntDirectoryError") {};
63
+ /** Whether `[offset, offset + length)` lies wholly inside `view`. */
64
+ const fitsInView = (view, offset, length) => Number.isInteger(offset) && Number.isInteger(length) && offset >= 0 && length >= 0 && offset + length <= view.byteLength;
65
+ /** The four-byte tag at `offset`, or `""` when it does not fit. */
66
+ const readTag = (view, offset) => {
67
+ if (!fitsInView(view, offset, TAG_LENGTH)) return "";
68
+ let tag = "";
69
+ for (let index = 0; index < TAG_LENGTH; index++) tag += String.fromCharCode(view.getUint8(offset + index));
70
+ return tag;
71
+ };
72
+ /** The leading version word, or `undefined` when the buffer is shorter than it. */
73
+ const readSfntVersion = (bytes) => {
74
+ if (bytes.byteLength < TAG_LENGTH) return;
75
+ return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(0);
76
+ };
77
+ /**
78
+ * Reads the table directory, validating every entry against the buffer.
79
+ *
80
+ * The caller is expected to have rejected non-sfnt magics first: a collection
81
+ * or WOFF header parsed as a directory yields nonsense offsets and a much
82
+ * worse error message than the container check gives.
83
+ */
84
+ const readSfntDirectory = (bytes) => {
85
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
86
+ if (!fitsInView(view, 0, 12)) return Result.err(new SfntDirectoryError({ message: `sfnt header needs 12 bytes, buffer has ${view.byteLength}` }));
87
+ const version = view.getUint32(0);
88
+ const numTables = view.getUint16(4);
89
+ const directorySize = numTables * 16;
90
+ if (!fitsInView(view, 12, directorySize)) return Result.err(new SfntDirectoryError({ message: `table directory declares ${numTables} tables, which does not fit in ${view.byteLength} bytes` }));
91
+ const tables = /* @__PURE__ */ new Map();
92
+ for (let index = 0; index < numTables; index++) {
93
+ const entry = 12 + index * 16;
94
+ const tag = readTag(view, entry + DIRECTORY_ENTRY.tag);
95
+ const offset = view.getUint32(entry + DIRECTORY_ENTRY.offset);
96
+ const length = view.getUint32(entry + DIRECTORY_ENTRY.length);
97
+ if (!fitsInView(view, offset, length)) return Result.err(new SfntDirectoryError({ message: `table '${tag}' spans [${offset}, ${offset + length}) outside the ${view.byteLength}-byte font` }));
98
+ if (!tables.has(tag)) tables.set(tag, {
99
+ tag,
100
+ offset,
101
+ length
102
+ });
103
+ }
104
+ return Result.ok({
105
+ version,
106
+ view,
107
+ tables
108
+ });
109
+ };
110
+ /**
111
+ * The `numGlyphs + 1` glyph offsets from `loca`, or `undefined` when the table
112
+ * is too short or its offsets run backwards (either makes glyph extraction
113
+ * meaningless).
114
+ */
115
+ const readLocaOffsets = ({ view, loca, numGlyphs, longFormat }) => {
116
+ const entrySize = longFormat ? 4 : 2;
117
+ const count = numGlyphs + 1;
118
+ if (loca.length < count * entrySize || !fitsInView(view, loca.offset, count * entrySize)) return;
119
+ const offsets = [];
120
+ let previous = -1;
121
+ for (let index = 0; index < count; index++) {
122
+ const at = loca.offset + index * entrySize;
123
+ const offset = longFormat ? view.getUint32(at) : view.getUint16(at) * 2;
124
+ if (offset < previous) return;
125
+ previous = offset;
126
+ offsets.push(offset);
127
+ }
128
+ return offsets;
129
+ };
130
+ //#endregion
131
+ export { SFNT_DIRECTORY_ENTRY_SIZE, SFNT_HEADER_SIZE, SFNT_TABLE, SFNT_TABLE_ALIGNMENT, SFNT_VERSION, SfntDirectoryError, fitsInView, readLocaOffsets, readSfntDirectory, readSfntVersion, readTag };
@@ -0,0 +1,14 @@
1
+ import { Result } from "better-result";
2
+ //#region src/fonts/sfnt/woff.d.ts
3
+ declare const WoffDecodeError_base: import("better-result").TaggedErrorClass<"WoffDecodeError">;
4
+ declare class WoffDecodeError extends WoffDecodeError_base<{
5
+ message: string;
6
+ }> {}
7
+ declare const decodeWoff: (bytes: Uint8Array) => Result<Uint8Array, WoffDecodeError>;
8
+ /**
9
+ * The entry point a caller with bytes of unknown provenance wants: plain sfnt
10
+ * passes through untouched, WOFF is decoded, WOFF2 is refused by name.
11
+ */
12
+ declare const toSfntBytes: (bytes: Uint8Array) => Result<Uint8Array, WoffDecodeError>;
13
+ //#endregion
14
+ export { WoffDecodeError, decodeWoff, toSfntBytes };