@zumer/snapdom-plugins 3.0.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 +50 -16
  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 +16 -5
  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
package/pdf.d.ts ADDED
@@ -0,0 +1,294 @@
1
+ import type { SnapdomPlugin } from '@zumer/snapdom'
2
+
3
+ /** PDF page sizes supported by the exporter. Custom pairs are PDF points. */
4
+ export type PdfPageSize =
5
+ | 'fit'
6
+ | 'a3'
7
+ | 'a4'
8
+ | 'a5'
9
+ | 'a6'
10
+ | 'b4'
11
+ | 'b5'
12
+ | 'letter'
13
+ | 'legal'
14
+ | 'tabloid'
15
+ | 'executive'
16
+ | readonly [width: number, height: number]
17
+
18
+ export type PdfOrientation = 'portrait' | 'landscape'
19
+ export type PdfImageCodec = 'auto' | 'jpeg' | 'flate'
20
+ export type PdfBase14Family = 'sans' | 'serif' | 'mono'
21
+ /**
22
+ * Furniture type. `'document'`, the default, is the body's own embedded face,
23
+ * falling back to base-14 sans per string; the other three mean base-14.
24
+ */
25
+ export type PdfFontFamily = 'document' | PdfBase14Family
26
+ export type PdfTextValue = string | number | null | undefined
27
+ export type PdfPageText = PdfTextValue | ((page: number, pages: number) => PdfTextValue)
28
+
29
+ export interface PdfTextBand {
30
+ left?: PdfPageText
31
+ center?: PdfPageText
32
+ right?: PdfPageText
33
+ /** Font size in PDF points. Default 9. */
34
+ size?: number
35
+ font?: PdfFontFamily
36
+ /** Three- or six-digit hexadecimal colour. */
37
+ color?: string
38
+ /** Alias accepted by the runtime. */
39
+ colour?: string
40
+ /** Draw the separator rule. Default true. */
41
+ rule?: boolean
42
+ /** Space between the text and rule, in PDF points. Default 10. */
43
+ gap?: number
44
+ }
45
+
46
+ export type PdfBand = string | ((page: number, pages: number) => PdfTextValue) | PdfTextBand | Element | null
47
+
48
+ export interface PdfPageNumbers {
49
+ format?: (page: number, pages: number) => PdfTextValue
50
+ align?: 'left' | 'center' | 'right'
51
+ size?: number
52
+ font?: PdfFontFamily
53
+ color?: string
54
+ colour?: string
55
+ }
56
+
57
+ export interface PdfTableOfContents {
58
+ title?: string
59
+ /** Outline depths to include. Default 3. */
60
+ levels?: number
61
+ size?: number
62
+ font?: PdfFontFamily
63
+ indent?: number
64
+ dots?: boolean
65
+ /** Formats the body page number printed by the index. */
66
+ label?: (page: number, bodyPages: number) => PdfTextValue
67
+ color?: string
68
+ colour?: string
69
+ leaderColor?: string
70
+ leaderColour?: string
71
+ }
72
+
73
+ export interface PdfTextWatermark {
74
+ text: string
75
+ opacity?: number
76
+ angle?: number
77
+ /** Zero or omitted auto-fits the text to the page. */
78
+ size?: number
79
+ font?: PdfFontFamily
80
+ color?: string
81
+ colour?: string
82
+ }
83
+
84
+ export interface PdfElementWatermark {
85
+ element: Element
86
+ opacity?: number
87
+ angle?: number
88
+ /** Fraction of the page's limiting dimension. Default 0.6. */
89
+ fit?: number
90
+ }
91
+
92
+ export type PdfWatermark = string | Element | PdfTextWatermark | PdfElementWatermark | null
93
+
94
+ /** Document properties written to /Info and mirrored into the XMP packet. */
95
+ /**
96
+ * Permission flags. Every one defaults to true, and every one is ADVISORY: a
97
+ * viewer may honour it and any other tool may ignore it. Only `userPassword`
98
+ * protects anything. Extraction for accessibility is deliberately not offered:
99
+ * PDF 2.0 deprecates that bit and PDF/UA requires an encrypted file to keep
100
+ * permitting it.
101
+ */
102
+ export interface PdfPermissions {
103
+ print?: boolean
104
+ modify?: boolean
105
+ copy?: boolean
106
+ annotate?: boolean
107
+ fillForms?: boolean
108
+ assemble?: boolean
109
+ printHighRes?: boolean
110
+ }
111
+
112
+ export interface PdfEncryption {
113
+ /** Required, non-empty: without it the reader cannot open the file at all. */
114
+ userPassword: string
115
+ /** Lifts the permission flags. Defaults to the user password. */
116
+ ownerPassword?: string
117
+ permissions?: PdfPermissions
118
+ }
119
+
120
+ export interface PdfDocumentMeta {
121
+ title?: string
122
+ author?: string
123
+ subject?: string
124
+ keywords?: string | readonly string[]
125
+ /** The producing APPLICATION (xmp:CreatorTool); Producer is always snapdom-pdf. */
126
+ creator?: string
127
+ /**
128
+ * Written only when given: an invented "now" would break byte determinism.
129
+ * Pass new Date() yourself when you want it.
130
+ */
131
+ created?: Date | string
132
+ modified?: Date | string
133
+ }
134
+
135
+ export type PdfProgressPhase = 'measure' | 'page' | 'matter' | 'assemble'
136
+
137
+ export interface PdfProgress {
138
+ phase: PdfProgressPhase
139
+ /** Pages done in this phase, or null when the phase has no count. */
140
+ done: number | null
141
+ /** Body page total, or null before pagination knows it. */
142
+ total: number | null
143
+ }
144
+
145
+ /** One warning, carrying its stable machine-readable code. */
146
+ export interface PdfWarning {
147
+ code: string
148
+ message: string
149
+ }
150
+
151
+ /** One face whose font program travels in the document. */
152
+ export interface PdfEmbeddedFont {
153
+ /** The /BaseFont name; a subset carries the `AAAAAA+` tag PDF 9.6.4 asks for. */
154
+ name: string
155
+ /** The CSS-facing request this face answered, e.g. `"Inter" 700`. */
156
+ label: string
157
+ /** Size of the embedded program before stream compression. */
158
+ kB: number
159
+ /** true when only the used glyphs travel; false when the whole file does. */
160
+ subset: boolean
161
+ }
162
+
163
+ /** Everything gateable about a finished export. Identical exports produce identical reports. */
164
+ export interface PdfReport {
165
+ warnings: readonly PdfWarning[]
166
+ /** Warning tallies keyed by code. */
167
+ counts: Readonly<Record<string, number>>
168
+ pages: number
169
+ bytes: number
170
+ /** Fillable fields written. */
171
+ fields: number
172
+ /** Link and widget annotations written. */
173
+ annotations: number
174
+ /** Faces embedded from the capture's own @font-face rules, subset or whole. */
175
+ fontsEmbedded: readonly PdfEmbeddedFont[]
176
+ /**
177
+ * true when this export EARNED and wrote the PDF/UA-1 identifier: every
178
+ * text operation on an embedded face, tagged structure, a language, a
179
+ * displayed title, and no painted image without alternative text. Never
180
+ * configurable; the `ua-withheld` warning names the blockers when false.
181
+ */
182
+ pdfua: boolean
183
+ /** Text runs `vectorText` painted as real glyphs. 0 when it is off. */
184
+ vectorText: number
185
+ }
186
+
187
+ /** Options evaluated when a finished SnapDOM capture is exported. */
188
+ export interface PdfExportOptions {
189
+ page?: PdfPageSize
190
+ orientation?: PdfOrientation
191
+ /** Margin on all sides, in PDF points. Ignored by page: 'fit'. */
192
+ margin?: number
193
+ text?: boolean
194
+ links?: boolean
195
+ /**
196
+ * Draw raster/XObject content. false keeps enabled text, links and structure,
197
+ * but omits Element headers, footers and watermarks.
198
+ */
199
+ image?: boolean
200
+ /** Compress PDF content streams. Default true. */
201
+ deflate?: boolean
202
+ /** Quality of the JPEG candidate. Ignored when lossless Flate wins. */
203
+ quality?: number
204
+ codec?: PdfImageCodec
205
+ /** Flatten the capture onto this colour for this PDF export. */
206
+ backgroundColor?: string | null
207
+ header?: PdfBand
208
+ footer?: PdfBand
209
+ pageNumbers?: boolean | PdfPageNumbers
210
+ repeatHeaders?: boolean
211
+ /** Emit a PDF structure tree. Structure alone is not a PDF/UA-1 claim; see `PdfReport.pdfua`. */
212
+ tagged?: boolean
213
+ /** PDF document language, for example "en" or "es-MX". */
214
+ lang?: string | null
215
+ /**
216
+ * Embed the capture's @font-face binaries and point the text at them. `true`
217
+ * (default) subsets each face to the glyphs the document drew; `'full'`
218
+ * embeds whole files; `false` embeds nothing. Needs the capture to carry its
219
+ * fonts: snapdom's own `embedFonts: true` capture option.
220
+ */
221
+ embedFonts?: boolean | 'full'
222
+ /**
223
+ * Paint eligible text as real vector glyphs from the embedded fonts instead
224
+ * of leaving it invisible over the raster, crisp at any zoom. Only faithful
225
+ * where no shaping intervenes (left-to-right runs in an embedded face), so
226
+ * RTL and uncovered runs stay on the raster. Pair with `image: false` for a
227
+ * tiny, fully-vector text PDF; with `image: true` the glyphs overpaint the
228
+ * raster. Default false.
229
+ */
230
+ vectorText?: boolean
231
+ cover?: Element | null
232
+ back?: Element | null
233
+ toc?: boolean | string | PdfTableOfContents | null
234
+ watermark?: PdfWatermark
235
+ debugText?: boolean
236
+ /** A filename, or true for "snapdom.pdf". The Blob is always returned. */
237
+ download?: boolean | string
238
+ /**
239
+ * Emit measured form controls as fillable AcroForm fields. Default true.
240
+ * false keeps their values in the invisible text layer instead.
241
+ */
242
+ fields?: boolean
243
+ /** Document properties: /Info, XMP, and DisplayDocTitle when title is set. */
244
+ meta?: PdfDocumentMeta | null
245
+ /**
246
+ * Encrypt the file with AES-256 (standard security handler, revision 6), so
247
+ * the download itself is protected at rest. `userPassword` is required: a PDF
248
+ * with an empty one is encrypted and still opens without asking.
249
+ */
250
+ encrypt?: PdfEncryption | null
251
+ /** Abort the export between pages. Rejects with the signal's AbortError. */
252
+ signal?: AbortSignal
253
+ /** Progress narration; a throwing callback is dropped, never fatal. */
254
+ onProgress?: (progress: PdfProgress) => void
255
+ /** Receives the structured diagnostics report after the Blob is finished. */
256
+ onReport?: (report: PdfReport) => void
257
+
258
+ /** @deprecated Capture resolution belongs to snapdom(...), not toPdf(...). */
259
+ scale?: never
260
+ /** @deprecated Use deflate for PDF streams; SnapDOM owns compress. */
261
+ compress?: never
262
+ /** @deprecated Use download; SnapDOM owns filename. */
263
+ filename?: never
264
+ /** @deprecated Use backgroundColor. */
265
+ background?: never
266
+ }
267
+
268
+ /**
269
+ * Defaults accepted by pdf(). The first five options are capture-time and cannot
270
+ * be changed by a later toPdf() call. Every PdfExportOptions field may also be a
271
+ * factory default and can then be overridden by toPdf().
272
+ */
273
+ export interface PdfPluginOptions extends PdfExportOptions {
274
+ formValues?: boolean
275
+ shadow?: boolean
276
+ breakAvoid?: boolean
277
+ /** true for h1-h6/role=heading, a selector string, or false. */
278
+ outline?: boolean | string
279
+ /** Measure form controls for fillable fields. Default true. Capture-time. */
280
+ forms?: boolean
281
+ }
282
+
283
+ /** Create a PDF plugin for @zumer/snapdom >=3.0.0 <4. */
284
+ declare function pdf(defaults?: PdfPluginOptions): SnapdomPlugin
285
+
286
+ export { pdf }
287
+ export default pdf
288
+
289
+ declare module '@zumer/snapdom' {
290
+ interface CaptureResult {
291
+ /** Export this already-created capture as a PDF Blob. */
292
+ toPdf(options?: PdfExportOptions): Promise<Blob>
293
+ }
294
+ }
package/pdf.js ADDED
@@ -0,0 +1 @@
1
+ export { default, pdf } from './pdf/src/index.js';