@zumer/snapdom-plugins 3.0.0-beta.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +54 -20
  3. package/agent-map.d.ts +32 -0
  4. package/agent-map.js +5 -3
  5. package/ascii-export.d.ts +10 -0
  6. package/ascii-export.js +21 -6
  7. package/capture-frames.js +5 -5
  8. package/color-tint.js +4 -0
  9. package/context-export.d.ts +23 -0
  10. package/context-export.js +4 -1
  11. package/filter.js +1 -1
  12. package/gif-export.d.ts +6 -0
  13. package/gif-export.js +3 -3
  14. package/html-export.d.ts +8 -0
  15. package/html-export.js +6 -4
  16. package/index.d.ts +2 -1
  17. package/index.js +2 -1
  18. package/package.json +17 -6
  19. package/pdf/LICENSE +21 -0
  20. package/pdf/README.md +22 -0
  21. package/pdf/REFERENCE.md +520 -0
  22. package/pdf/src/frontmatter.js +200 -0
  23. package/pdf/src/furniture.js +398 -0
  24. package/pdf/src/index.js +2415 -0
  25. package/pdf/src/mega/README.md +47 -0
  26. package/pdf/src/mega/diag.js +148 -0
  27. package/pdf/src/mega/flow.js +56 -0
  28. package/pdf/src/mega/fonts.js +287 -0
  29. package/pdf/src/mega/forms.js +479 -0
  30. package/pdf/src/mega/labels.js +43 -0
  31. package/pdf/src/mega/meta.js +145 -0
  32. package/pdf/src/nav.js +357 -0
  33. package/pdf/src/paginate.js +583 -0
  34. package/pdf/src/tagged.js +355 -0
  35. package/pdf/src/text-layer.js +1771 -0
  36. package/pdf/src/writer/encrypt.js +218 -0
  37. package/pdf/src/writer/font.js +507 -0
  38. package/pdf/src/writer/index.js +1135 -0
  39. package/pdf/src/writer/subset.js +387 -0
  40. package/pdf.d.ts +294 -0
  41. package/pdf.js +1 -0
  42. package/redact-inputs.js +6 -6
  43. package/vector/CONTRACT.md +906 -0
  44. package/vector/LICENSE +21 -0
  45. package/vector/README.md +35 -0
  46. package/vector/src/adapters/snapdom.js +795 -0
  47. package/vector/src/collect/box.js +1676 -0
  48. package/vector/src/collect/font.js +487 -0
  49. package/vector/src/collect/image.js +1458 -0
  50. package/vector/src/collect/mathml.js +272 -0
  51. package/vector/src/collect/text.js +3027 -0
  52. package/vector/src/css/color.js +823 -0
  53. package/vector/src/css/gradient.js +1126 -0
  54. package/vector/src/css/matrix.js +398 -0
  55. package/vector/src/css/radius.js +124 -0
  56. package/vector/src/css/shadow.js +201 -0
  57. package/vector/src/emit/figma-json.js +1995 -0
  58. package/vector/src/emit/h2d-vector.js +215 -0
  59. package/vector/src/emit/h2d.js +1147 -0
  60. package/vector/src/emit/pdf.js +2571 -0
  61. package/vector/src/emit/svg-flat.js +4842 -0
  62. package/vector/src/exclusion.js +13 -0
  63. package/vector/src/index.js +2910 -0
  64. package/vector/src/paint.js +2908 -0
  65. package/vector/src/public.js +57 -0
  66. package/vector/src/writer/encrypt.js +227 -0
  67. package/vector/src/writer/font.js +507 -0
  68. package/vector/src/writer/index.js +1148 -0
  69. package/vector/src/writer/subset.js +387 -0
  70. package/vector/svd/index.js +72 -0
  71. package/vector/svd/schema.js +985 -0
  72. package/vector.d.ts +28 -0
  73. package/vector.js +1 -0
  74. package/video-export.d.ts +6 -0
  75. package/video-export.js +7 -7
  76. package/pdf-image.d.ts +0 -18
  77. package/pdf-image.js +0 -114
@@ -0,0 +1,387 @@
1
+ /**
2
+ * TrueType subsetting: keep only the glyphs a document actually used, renumbered
3
+ * into a dense space, and rebuild a valid sfnt around them.
4
+ *
5
+ * This is the font compiler the embedding path deliberately went without at
6
+ * first. It earns its risk with one number: an unsubsetted face is the whole
7
+ * file (Inter is ~320 kB); a Latin document touches ~80 glyphs, and those plus
8
+ * their metrics and a fresh cmap are tens of kB. The saving is the glyf table,
9
+ * which is most of a font and most of which no page ever draws.
10
+ *
11
+ * The dense renumber is the whole trick, and it is shared with `createEmbeddedFont`:
12
+ * that writer already assigns each glyph a compact id in first-use order as it
13
+ * encodes the content stream (0 is `.notdef`), and writes THAT id as the CID. So
14
+ * the content stream, `/W` and `/ToUnicode` all speak the subset's numbering
15
+ * already; this file only has to place original glyph `g` at its new index and
16
+ * rewrite the composite-component references that point across the renumber.
17
+ *
18
+ * What is rebuilt, and what is dropped:
19
+ * - `glyf` + `loca`: the retained outlines, composite component ids remapped,
20
+ * long `loca` throughout so offsets never need the ×2 short encoding.
21
+ * - `hmtx` + `hhea`: advances/lsbs for the dense set; `numberOfHMetrics` = count.
22
+ * - `maxp`: `numGlyphs` = count; the other ceilings are copied (they bound).
23
+ * - `cmap`: a fresh format 4 (+ format 12 when an astral code point is kept),
24
+ * mapping the retained CODE POINTS to their new ids. The PDF encodes through
25
+ * Identity-H and ignores this, but OTS and any standalone use want it valid.
26
+ * - `head`, `name`, `OS/2`, `post`: copied; `post` is rewritten to v3 (no glyph
27
+ * names — they are dead weight the PDF never reads).
28
+ * - Hinting (`fpgm`/`prep`/`cvt`/`gasp`) and layout (`GSUB`/`GPOS`/`GDEF`) are
29
+ * DROPPED. No shaping happens on this path, so the glyphs render unhinted and
30
+ * unshaped — correct for an invisible layer and honest for short furniture.
31
+ *
32
+ * CFF (`OTTO`) fonts are not subset here — their charstrings are a different
33
+ * compiler — so the caller embeds those whole.
34
+ */
35
+
36
+ const U16 = (b, at) => (b[at] << 8) | b[at + 1]
37
+ const U32 = (b, at) => ((b[at] << 24) | (b[at + 1] << 16) | (b[at + 2] << 8) | b[at + 3]) >>> 0
38
+ const TAG = (b, at) => String.fromCharCode(b[at], b[at + 1], b[at + 2], b[at + 3])
39
+
40
+ // glyf composite flags this file reads.
41
+ const ARG_1_AND_2_ARE_WORDS = 0x0001
42
+ const WE_HAVE_A_SCALE = 0x0008
43
+ const MORE_COMPONENTS = 0x0020
44
+ const WE_HAVE_AN_X_AND_Y_SCALE = 0x0040
45
+ const WE_HAVE_A_TWO_BY_TWO = 0x0080
46
+
47
+ /** The sfnt table directory as tag → {offset, length}. */
48
+ function directory(b) {
49
+ const numTables = U16(b, 4)
50
+ const tables = new Map()
51
+ for (let i = 0; i < numTables; i++) {
52
+ const at = 12 + i * 16
53
+ tables.set(TAG(b, at), { offset: U32(b, at + 8), length: U32(b, at + 12) })
54
+ }
55
+ return tables
56
+ }
57
+
58
+ /** glyph `gid`'s bytes in the source, or an empty view when it has no outline. */
59
+ function glyphSlice(b, loca, gid) {
60
+ const start = loca[gid]
61
+ const end = loca[gid + 1]
62
+ return end > start ? b.subarray(start, end) : b.subarray(0, 0)
63
+ }
64
+
65
+ /** loca as absolute byte offsets into glyf, in either stored format. */
66
+ function readLoca(b, locaT, glyfOffset, numGlyphs, longLoca) {
67
+ const out = new Array(numGlyphs + 1)
68
+ for (let i = 0; i <= numGlyphs; i++) {
69
+ out[i] = glyfOffset + (longLoca ? U32(b, locaT.offset + i * 4) : U16(b, locaT.offset + i * 2) * 2)
70
+ }
71
+ return out
72
+ }
73
+
74
+ /**
75
+ * Every glyph id a composite glyph reaches, transitively. A component may itself
76
+ * be composite, so the walk is a queue, not one pass; a self-reference or a cycle
77
+ * is bounded by the `seen` set.
78
+ */
79
+ function componentIds(b, loca, gid, into) {
80
+ const g = glyphSlice(b, loca, gid)
81
+ if (g.length < 10 || (U16(g, 0) & 0x8000) === 0) return // not composite (numberOfContours >= 0)
82
+ let at = 10
83
+ for (;;) {
84
+ const flags = U16(g, at)
85
+ const componentGid = U16(g, at + 2)
86
+ into.add(componentGid)
87
+ at += 4
88
+ at += (flags & ARG_1_AND_2_ARE_WORDS) ? 4 : 2
89
+ if (flags & WE_HAVE_A_SCALE) at += 2
90
+ else if (flags & WE_HAVE_AN_X_AND_Y_SCALE) at += 4
91
+ else if (flags & WE_HAVE_A_TWO_BY_TWO) at += 8
92
+ if (!(flags & MORE_COMPONENTS)) break
93
+ }
94
+ }
95
+
96
+ /** A copy of glyph `gid`'s bytes with every component id remapped through `map`. */
97
+ function remapComposite(bytes, map) {
98
+ if (bytes.length < 10 || (U16(bytes, 0) & 0x8000) === 0) return bytes
99
+ const out = bytes.slice()
100
+ let at = 10
101
+ for (;;) {
102
+ const flags = U16(out, at)
103
+ const newGid = map.get(U16(out, at + 2))
104
+ out[at + 2] = (newGid >> 8) & 0xff
105
+ out[at + 3] = newGid & 0xff
106
+ at += 4
107
+ at += (flags & ARG_1_AND_2_ARE_WORDS) ? 4 : 2
108
+ if (flags & WE_HAVE_A_SCALE) at += 2
109
+ else if (flags & WE_HAVE_AN_X_AND_Y_SCALE) at += 4
110
+ else if (flags & WE_HAVE_A_TWO_BY_TWO) at += 8
111
+ if (!(flags & MORE_COMPONENTS)) break
112
+ }
113
+ return out
114
+ }
115
+
116
+ /** sfnt table checksum: sum of big-endian uint32 over the 4-byte-padded data. */
117
+ function checksum(bytes) {
118
+ let sum = 0
119
+ const n = (bytes.length + 3) & ~3
120
+ for (let i = 0; i < n; i += 4) {
121
+ const v = ((bytes[i] || 0) << 24) | ((bytes[i + 1] || 0) << 16) |
122
+ ((bytes[i + 2] || 0) << 8) | (bytes[i + 3] || 0)
123
+ sum = (sum + (v >>> 0)) >>> 0
124
+ }
125
+ return sum >>> 0
126
+ }
127
+
128
+ /** A format-4 (BMP) cmap subtable body for `pairs` of [codepoint, gid], sorted. */
129
+ function cmapFormat4(pairs) {
130
+ const segments = []
131
+ let i = 0
132
+ while (i < pairs.length) {
133
+ const [startCp, startGid] = pairs[i]
134
+ let j = i
135
+ while (j + 1 < pairs.length &&
136
+ pairs[j + 1][0] === pairs[j][0] + 1 && pairs[j + 1][1] === pairs[j][1] + 1) j++
137
+ segments.push({ start: startCp, end: pairs[j][0], delta: (startGid - startCp) & 0xffff })
138
+ i = j + 1
139
+ }
140
+ segments.push({ start: 0xffff, end: 0xffff, delta: 1 }) // required terminator → gid 0
141
+ const segCount = segments.length
142
+ const segX2 = segCount * 2
143
+ let entropy = 0
144
+ while ((1 << (entropy + 1)) <= segCount) entropy++
145
+ const searchRange = 2 * (1 << entropy)
146
+ const words = []
147
+ words.push(4, segX2 + 16 /* length filled below */, 0, segX2, searchRange, entropy, segX2 - searchRange)
148
+ for (const s of segments) words.push(s.end)
149
+ words.push(0) // reservedPad
150
+ for (const s of segments) words.push(s.start)
151
+ for (const s of segments) words.push(s.delta)
152
+ for (const s of segments) words.push(0) // idRangeOffset — all pure-delta
153
+ const body = new Uint8Array(words.length * 2)
154
+ for (let k = 0; k < words.length; k++) {
155
+ body[k * 2] = (words[k] >> 8) & 0xff
156
+ body[k * 2 + 1] = words[k] & 0xff
157
+ }
158
+ body[2] = (body.length >> 8) & 0xff // real length now that it is known
159
+ body[3] = body.length & 0xff
160
+ return body
161
+ }
162
+
163
+ /** A format-12 (full Unicode) cmap subtable body for `pairs`, sorted. */
164
+ function cmapFormat12(pairs) {
165
+ const groups = []
166
+ let i = 0
167
+ while (i < pairs.length) {
168
+ const [startCp, startGid] = pairs[i]
169
+ let j = i
170
+ while (j + 1 < pairs.length &&
171
+ pairs[j + 1][0] === pairs[j][0] + 1 && pairs[j + 1][1] === pairs[j][1] + 1) j++
172
+ groups.push([startCp, pairs[j][0], startGid])
173
+ i = j + 1
174
+ }
175
+ const body = new Uint8Array(16 + groups.length * 12)
176
+ const w32 = (at, v) => { body[at] = (v >>> 24) & 0xff; body[at + 1] = (v >>> 16) & 0xff; body[at + 2] = (v >>> 8) & 0xff; body[at + 3] = v & 0xff }
177
+ body[0] = 0; body[1] = 12 // format 12.0
178
+ w32(4, body.length)
179
+ w32(8, 0) // language
180
+ w32(12, groups.length)
181
+ groups.forEach(([s, e, g], k) => {
182
+ w32(16 + k * 12, s); w32(16 + k * 12 + 4, e); w32(16 + k * 12 + 8, g)
183
+ })
184
+ return body
185
+ }
186
+
187
+ /** The whole cmap table: a (3,1) format 4, plus a (3,10) format 12 when needed. */
188
+ function buildCmap(codeToGid) {
189
+ const bmp = [...codeToGid].filter(([cp]) => cp <= 0xffff).sort((a, b) => a[0] - b[0])
190
+ const all = [...codeToGid].sort((a, b) => a[0] - b[0])
191
+ const astral = all.length > bmp.length
192
+ const subs = [{ platform: 3, encoding: 1, body: cmapFormat4(bmp) }]
193
+ if (astral) subs.push({ platform: 3, encoding: 10, body: cmapFormat12(all) })
194
+
195
+ const header = 4 + subs.length * 8
196
+ let offset = header
197
+ const out = []
198
+ const head = new Uint8Array(header)
199
+ head[0] = 0; head[1] = 0 // version
200
+ head[2] = (subs.length >> 8) & 0xff; head[3] = subs.length & 0xff
201
+ subs.forEach((s, i) => {
202
+ const at = 4 + i * 8
203
+ head[at] = (s.platform >> 8) & 0xff; head[at + 1] = s.platform & 0xff
204
+ head[at + 2] = (s.encoding >> 8) & 0xff; head[at + 3] = s.encoding & 0xff
205
+ head[at + 4] = (offset >>> 24) & 0xff; head[at + 5] = (offset >>> 16) & 0xff
206
+ head[at + 6] = (offset >>> 8) & 0xff; head[at + 7] = offset & 0xff
207
+ offset += s.body.length
208
+ })
209
+ out.push(head, ...subs.map(s => s.body))
210
+ return concat(out)
211
+ }
212
+
213
+ function concat(chunks) {
214
+ let n = 0
215
+ for (const c of chunks) n += c.length
216
+ const out = new Uint8Array(n)
217
+ let at = 0
218
+ for (const c of chunks) { out.set(c, at); at += c.length }
219
+ return out
220
+ }
221
+
222
+ /** A minimal post v3 table — no glyph names, which the PDF never reads. */
223
+ function postV3(src) {
224
+ const out = new Uint8Array(32)
225
+ out.set(src.subarray(4, 32), 4) // keep italicAngle…maxMemType1 from the original
226
+ out[0] = 0; out[1] = 3; out[2] = 0; out[3] = 0 // version 3.0
227
+ return out
228
+ }
229
+
230
+ /**
231
+ * Subset a TrueType sfnt to the glyphs named by `directMap`.
232
+ *
233
+ * @param {Uint8Array} sfnt the source font, already unpacked to plain sfnt
234
+ * @param {Map<number, number>} directMap original glyph id → new (dense) id,
235
+ * for exactly the glyphs the content stream references. Must map original 0
236
+ * to new 0 (`.notdef`) and be dense over 0…directMap.size-1.
237
+ * @param {Map<number, number>} codeToOrig code point → original glyph id, for
238
+ * the retained characters — used to rebuild the cmap in the new numbering.
239
+ * @returns {{sfnt: Uint8Array}|{error: string}}
240
+ */
241
+ export function subsetTrueType(sfnt, directMap, codeToOrig) {
242
+ const b = sfnt
243
+ const tables = directory(b)
244
+ const headT = tables.get('head')
245
+ const maxpT = tables.get('maxp')
246
+ const hheaT = tables.get('hhea')
247
+ const hmtxT = tables.get('hmtx')
248
+ const glyfT = tables.get('glyf')
249
+ const locaT = tables.get('loca')
250
+ if (!headT || !maxpT || !hheaT || !hmtxT || !glyfT || !locaT) {
251
+ return { error: 'subset needs head/maxp/hhea/hmtx/glyf/loca (TrueType outlines)' }
252
+ }
253
+ const numGlyphs = U16(b, maxpT.offset + 4)
254
+ const longLoca = U16(b, headT.offset + 50) === 1
255
+ const loca = readLoca(b, locaT, glyfT.offset, numGlyphs, longLoca)
256
+
257
+ // The retained set, closed over composite components. `newOf` extends
258
+ // directMap: components not directly used get ids after the dense prefix.
259
+ const newOf = new Map(directMap)
260
+ const queue = [...directMap.keys()]
261
+ while (queue.length) {
262
+ const gid = queue.pop()
263
+ if (gid >= numGlyphs) continue
264
+ const comps = new Set()
265
+ componentIds(b, loca, gid, comps)
266
+ for (const c of comps) {
267
+ if (!newOf.has(c)) { newOf.set(c, newOf.size); queue.push(c) }
268
+ }
269
+ }
270
+ const count = newOf.size
271
+ const origOf = new Array(count)
272
+ for (const [orig, nw] of newOf) origOf[nw] = orig
273
+
274
+ // glyf + loca in the new order. Each glyph padded to an even length; long loca
275
+ // throughout, so a large offset never overflows the short format.
276
+ const numH = U16(b, hheaT.offset + 34)
277
+ const glyphBytes = []
278
+ const locaOut = new Uint8Array((count + 1) * 4)
279
+ const putLoca = (i, v) => { const at = i * 4; locaOut[at] = (v >>> 24) & 0xff; locaOut[at + 1] = (v >>> 16) & 0xff; locaOut[at + 2] = (v >>> 8) & 0xff; locaOut[at + 3] = v & 0xff }
280
+ let glyfLen = 0
281
+ const hmtxOut = new Uint8Array(count * 4)
282
+ for (let nw = 0; nw < count; nw++) {
283
+ putLoca(nw, glyfLen)
284
+ const orig = origOf[nw]
285
+ let g = glyphSlice(b, loca, orig)
286
+ if (g.length) {
287
+ g = remapComposite(g, newOf)
288
+ glyphBytes.push(g)
289
+ glyfLen += g.length
290
+ if (g.length & 1) { glyphBytes.push(new Uint8Array(1)); glyfLen += 1 }
291
+ }
292
+ // hmtx: advance from the last real metric when orig sat in the monospaced
293
+ // tail, its own lsb otherwise; a subset keeps a full (advance,lsb) per glyph.
294
+ const hm = orig < numH ? orig : numH - 1
295
+ hmtxOut[nw * 4] = b[hmtxT.offset + hm * 4]
296
+ hmtxOut[nw * 4 + 1] = b[hmtxT.offset + hm * 4 + 1]
297
+ if (orig < numH) {
298
+ hmtxOut[nw * 4 + 2] = b[hmtxT.offset + orig * 4 + 2]
299
+ hmtxOut[nw * 4 + 3] = b[hmtxT.offset + orig * 4 + 3]
300
+ } else {
301
+ // A tail glyph's lsb lives in the leftSideBearings[] array that follows the
302
+ // hMetrics — not in a 4-byte hMetric. Reading it as one (above) would take
303
+ // the advance's bytes; dropping it (no else) would zero the bearing and
304
+ // shift the painted glyph sideways within its cell on a monospaced face.
305
+ const at = hmtxT.offset + numH * 4 + (orig - numH) * 2
306
+ hmtxOut[nw * 4 + 2] = b[at]
307
+ hmtxOut[nw * 4 + 3] = b[at + 1]
308
+ }
309
+ }
310
+ putLoca(count, glyfLen)
311
+ const glyfOut = concat(glyphBytes)
312
+
313
+ // Fixed-size tables, copied then patched.
314
+ const head = b.slice(headT.offset, headT.offset + headT.length)
315
+ head[50] = 0; head[51] = 1 // indexToLocFormat = long
316
+ head[8] = head[9] = head[10] = head[11] = 0 // checkSumAdjustment zeroed; set at the end
317
+ const maxp = b.slice(maxpT.offset, maxpT.offset + maxpT.length)
318
+ maxp[4] = (count >> 8) & 0xff; maxp[5] = count & 0xff
319
+ const hhea = b.slice(hheaT.offset, hheaT.offset + hheaT.length)
320
+ hhea[34] = (count >> 8) & 0xff; hhea[35] = count & 0xff // numberOfHMetrics
321
+
322
+ const codeToNew = new Map()
323
+ for (const [cp, orig] of codeToOrig) {
324
+ if (newOf.has(orig)) codeToNew.set(cp, newOf.get(orig))
325
+ }
326
+
327
+ const out = new Map()
328
+ out.set('head', head)
329
+ out.set('hhea', hhea)
330
+ out.set('maxp', maxp)
331
+ out.set('hmtx', hmtxOut)
332
+ out.set('loca', locaOut)
333
+ out.set('glyf', glyfOut)
334
+ out.set('cmap', buildCmap(codeToNew))
335
+ for (const tag of ['name', 'OS/2']) {
336
+ const t = tables.get(tag)
337
+ if (t) out.set(tag, b.slice(t.offset, t.offset + t.length))
338
+ }
339
+ const postT = tables.get('post')
340
+ if (postT) out.set('post', postV3(b.slice(postT.offset, postT.offset + postT.length)))
341
+
342
+ return { sfnt: assemble(out) }
343
+ }
344
+
345
+ /** Lay the tables out as a checksummed sfnt with a sorted directory. */
346
+ function assemble(tableMap) {
347
+ const tags = [...tableMap.keys()].sort()
348
+ const numTables = tags.length
349
+ let entropy = 0
350
+ while ((1 << (entropy + 1)) <= numTables) entropy++
351
+ const searchRange = (1 << entropy) * 16
352
+
353
+ const headerLen = 12 + numTables * 16
354
+ let offset = headerLen
355
+ const records = []
356
+ for (const tag of tags) {
357
+ const data = tableMap.get(tag)
358
+ records.push({ tag, data, offset, length: data.length, checksum: checksum(data) })
359
+ offset += (data.length + 3) & ~3
360
+ }
361
+
362
+ const out = new Uint8Array(offset)
363
+ const w16 = (at, v) => { out[at] = (v >> 8) & 0xff; out[at + 1] = v & 0xff }
364
+ const w32 = (at, v) => { out[at] = (v >>> 24) & 0xff; out[at + 1] = (v >>> 16) & 0xff; out[at + 2] = (v >>> 8) & 0xff; out[at + 3] = v & 0xff }
365
+ w32(0, 0x00010000)
366
+ w16(4, numTables)
367
+ w16(6, searchRange)
368
+ w16(8, entropy)
369
+ w16(10, numTables * 16 - searchRange)
370
+ records.forEach((r, i) => {
371
+ const at = 12 + i * 16
372
+ for (let k = 0; k < 4; k++) out[at + k] = r.tag.charCodeAt(k) || 0x20
373
+ w32(at + 4, r.checksum)
374
+ w32(at + 8, r.offset)
375
+ w32(at + 12, r.length)
376
+ out.set(r.data, r.offset)
377
+ })
378
+
379
+ // head.checkSumAdjustment closes the loop: 0xB1B0AFBA minus the whole file's
380
+ // checksum with this field zero (it already is).
381
+ const headRec = records.find(r => r.tag === 'head')
382
+ const adjustment = (0xb1b0afba - checksum(out)) >>> 0
383
+ w32(headRec.offset + 8, adjustment)
384
+ return out
385
+ }
386
+
387
+ export default subsetTrueType
@@ -0,0 +1,72 @@
1
+ /**
2
+ * SVD — Scalable Vector Document.
3
+ *
4
+ * The contract between the capture engine and every editor backend. The engine
5
+ * emits SVD; each backend is a pure `SVD -> output` function with no DOM, no
6
+ * `getComputedStyle`, no measuring. If a backend needs to measure something,
7
+ * the schema is missing a field — extend the schema, never measure downstream.
8
+ *
9
+ * Invariants (normative — a document violating any of these is a bug):
10
+ *
11
+ * 1. Units are CSS pixels, unrounded floats. Rounding belongs to the emitter.
12
+ * Angles are degrees, clockwise.
13
+ * 2. `frame` is relative to the emitted parent. `abs` is relative to the
14
+ * border box of the capture root, Y down. Both are always present: `abs`
15
+ * survives reparenting and is what the test harness asserts against.
16
+ * 3. `children` is in DOM containment order. `paint.z` carries the back-to-front
17
+ * order. The backend sorts by `paint.z`; it never re-derives it.
18
+ * 4. Colors are `[r, g, b, a]` floats 0..1 in sRGB, plus `srcCss` for round-trip.
19
+ * 5. `blur` is the CSS blur radius. SVG sigma is `blur / 2`. Stated once, here.
20
+ * 6. Gradient geometry is normalized to the unit square with a 2x3 matrix.
21
+ * Never percentages — that is the documented origin of Figma's
22
+ * "failed to invert transform" on SVG import.
23
+ * 7. Nothing relative, nothing external: assets are embedded or referenced by
24
+ * local id.
25
+ * 8. A document that degraded something without saying so in `diagnostics` is
26
+ * a bug. That rule outranks the rest of the schema.
27
+ */
28
+
29
+ export const SVD_VERSION = 'svd/1.0'
30
+
31
+ /** @typedef {'frame'|'group'|'shape'|'text'|'image'|'vector'} NodeType */
32
+ export const NODE_TYPES = ['frame', 'group', 'shape', 'text', 'image', 'vector']
33
+
34
+ /**
35
+ * Fidelity grades. Every node carries one; anything but `E` must have a
36
+ * matching entry in `diagnostics`.
37
+ *
38
+ * E exact — the target primitive reproduces the CSS with no loss
39
+ * A approximate — parametric, visually equivalent within tolerance
40
+ * C composed — one CSS declaration became N primitives
41
+ * R raster — rasterized, with the reason recorded
42
+ * RH raster hybrid — background rasterized, vector content kept on top
43
+ * O omitted — could not be represented; placeholder emitted
44
+ */
45
+ export const FIDELITY = ['E', 'A', 'C', 'R', 'RH', 'O']
46
+
47
+ export { validate, validateNode } from './schema.js'
48
+
49
+ /** Empty document with every required top-level key present. */
50
+ export function emptyDocument (capture = {}) {
51
+ return {
52
+ schema: SVD_VERSION,
53
+ generator: { name: '@zumer/snapdom-vector', version: '0.0.1', engine: 'dom' },
54
+ capture: {
55
+ url: '', selector: '', capturedAt: '',
56
+ viewport: { w: 0, h: 0, dpr: 1 },
57
+ docOrigin: { x: 0, y: 0 },
58
+ root: { w: 0, h: 0 },
59
+ conventions: {
60
+ units: 'px', y: 'down', angle: 'deg-cw',
61
+ colorSpace: 'srgb', blur: 'css-radius',
62
+ },
63
+ ...capture,
64
+ },
65
+ assets: {},
66
+ styles: { text: {} },
67
+ root: null,
68
+ nodes: {},
69
+ diagnostics: [],
70
+ report: {},
71
+ }
72
+ }