@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.
- package/CHANGELOG.md +13 -0
- package/README.md +50 -16
- 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 +16 -5
- 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
|
|
@@ -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
|
+
}
|