@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.
- package/CHANGELOG.md +13 -0
- package/README.md +54 -20
- package/agent-map.d.ts +32 -0
- package/agent-map.js +5 -3
- package/ascii-export.d.ts +10 -0
- package/ascii-export.js +21 -6
- package/capture-frames.js +5 -5
- package/color-tint.js +4 -0
- package/context-export.d.ts +23 -0
- package/context-export.js +4 -1
- package/filter.js +1 -1
- package/gif-export.d.ts +6 -0
- package/gif-export.js +3 -3
- package/html-export.d.ts +8 -0
- package/html-export.js +6 -4
- package/index.d.ts +2 -1
- package/index.js +2 -1
- package/package.json +17 -6
- package/pdf/LICENSE +21 -0
- package/pdf/README.md +22 -0
- package/pdf/REFERENCE.md +520 -0
- package/pdf/src/frontmatter.js +200 -0
- package/pdf/src/furniture.js +398 -0
- package/pdf/src/index.js +2415 -0
- package/pdf/src/mega/README.md +47 -0
- package/pdf/src/mega/diag.js +148 -0
- package/pdf/src/mega/flow.js +56 -0
- package/pdf/src/mega/fonts.js +287 -0
- package/pdf/src/mega/forms.js +479 -0
- package/pdf/src/mega/labels.js +43 -0
- package/pdf/src/mega/meta.js +145 -0
- package/pdf/src/nav.js +357 -0
- package/pdf/src/paginate.js +583 -0
- package/pdf/src/tagged.js +355 -0
- package/pdf/src/text-layer.js +1771 -0
- package/pdf/src/writer/encrypt.js +218 -0
- package/pdf/src/writer/font.js +507 -0
- package/pdf/src/writer/index.js +1135 -0
- package/pdf/src/writer/subset.js +387 -0
- package/pdf.d.ts +294 -0
- package/pdf.js +1 -0
- package/redact-inputs.js +6 -6
- package/vector/CONTRACT.md +906 -0
- package/vector/LICENSE +21 -0
- package/vector/README.md +35 -0
- package/vector/src/adapters/snapdom.js +795 -0
- package/vector/src/collect/box.js +1676 -0
- package/vector/src/collect/font.js +487 -0
- package/vector/src/collect/image.js +1458 -0
- package/vector/src/collect/mathml.js +272 -0
- package/vector/src/collect/text.js +3027 -0
- package/vector/src/css/color.js +823 -0
- package/vector/src/css/gradient.js +1126 -0
- package/vector/src/css/matrix.js +398 -0
- package/vector/src/css/radius.js +124 -0
- package/vector/src/css/shadow.js +201 -0
- package/vector/src/emit/figma-json.js +1995 -0
- package/vector/src/emit/h2d-vector.js +215 -0
- package/vector/src/emit/h2d.js +1147 -0
- package/vector/src/emit/pdf.js +2571 -0
- package/vector/src/emit/svg-flat.js +4842 -0
- package/vector/src/exclusion.js +13 -0
- package/vector/src/index.js +2910 -0
- package/vector/src/paint.js +2908 -0
- package/vector/src/public.js +57 -0
- package/vector/src/writer/encrypt.js +227 -0
- package/vector/src/writer/font.js +507 -0
- package/vector/src/writer/index.js +1148 -0
- package/vector/src/writer/subset.js +387 -0
- package/vector/svd/index.js +72 -0
- package/vector/svd/schema.js +985 -0
- package/vector.d.ts +28 -0
- package/vector.js +1 -0
- package/video-export.d.ts +6 -0
- package/video-export.js +7 -7
- package/pdf-image.d.ts +0 -18
- 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';
|