@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,218 @@
1
+ /**
2
+ * Standard security handler, revision 6: AES-256, and nothing older.
3
+ *
4
+ * A PDF that leaves the browser as a download IS the delivery channel, so the
5
+ * only encryption worth writing here is the kind that protects the file at rest
6
+ * once it has been shared. That rules two things out, in opposite directions:
7
+ *
8
+ * - **RC4 and the /V 1–4 revisions are not offered.** A 40-bit key is brute
9
+ * forced in seconds and 128-bit RC4 is not far behind; both also need MD5,
10
+ * which the platform deliberately does not provide. Writing them by hand to
11
+ * ship a weaker product is not a trade this file makes.
12
+ * - **Permission flags are not protection and are not sold as such.** `/P` is
13
+ * a request a viewer may honour; any tool can ignore it. What actually keeps
14
+ * a document shut is the user password, because without it the bytes cannot
15
+ * be read at all. The flags are written because the format has them, and the
16
+ * caller is told what they are worth.
17
+ *
18
+ * Everything here comes from `crypto.subtle`: SHA-256/384/512 for Algorithm 2.B
19
+ * and AES-CBC for the rest. No dependency, no hand-rolled cipher, no MD5.
20
+ *
21
+ * **One thing WebCrypto will not do**, and the workaround, because it looks like
22
+ * a bug otherwise: 2.B and the key wrapping need AES-CBC with NO padding, and
23
+ * WebCrypto always appends a PKCS#7 block. The bytes it produces before that
24
+ * block ARE the unpadded CBC stream — CBC is sequential, so block n cannot
25
+ * depend on anything after it — so every no-padding call here encrypts and then
26
+ * truncates. `encryptcheck.mjs` asserts that prefix property rather than
27
+ * trusting it.
28
+ *
29
+ * **Every secret is random, so encryption is not deterministic.** The file key,
30
+ * the four salts, the /Perms filler and every IV come from
31
+ * `crypto.getRandomValues`, as ISO 32000-2 intends. The rest of this writer
32
+ * produces the same bytes for the same input; this file is the exception on
33
+ * purpose. An earlier version derived all of them from the passwords and a
34
+ * digest of the plaintext, so two encrypted exports of the same document under
35
+ * the same password were byte-identical, and anyone holding both files learned
36
+ * that their contents matched without knowing either password.
37
+ * `encryptcheck.mjs` asserts that two such exports differ and open to the same
38
+ * text.
39
+ */
40
+
41
+ const utf8 = (s) => new TextEncoder().encode(s)
42
+
43
+ /** Concatenate byte runs. */
44
+ function cat(...runs) {
45
+ let n = 0
46
+ for (const r of runs) n += r.length
47
+ const out = new Uint8Array(n)
48
+ let at = 0
49
+ for (const r of runs) { out.set(r, at); at += r.length }
50
+ return out
51
+ }
52
+
53
+ const hex = (b) => [...b].map(x => x.toString(16).padStart(2, '0')).join('').toUpperCase()
54
+
55
+ const random = (bytes) => crypto.getRandomValues(new Uint8Array(bytes))
56
+
57
+ async function sha(bits, data) {
58
+ return new Uint8Array(await crypto.subtle.digest(`SHA-${bits}`, data))
59
+ }
60
+
61
+ /**
62
+ * AES-CBC with no padding, by encrypting and dropping the block WebCrypto adds.
63
+ * `data.length` must be a multiple of 16, which every caller here guarantees.
64
+ */
65
+ async function aesNoPad(key, iv, data) {
66
+ const k = await crypto.subtle.importKey('raw', key, 'AES-CBC', false, ['encrypt'])
67
+ const full = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-CBC', iv }, k, data))
68
+ return full.subarray(0, data.length)
69
+ }
70
+
71
+ /** AES-CBC with the PKCS#7 padding the PDF stream/string format wants, IV first. */
72
+ async function aesCbc(key, iv, data) {
73
+ const k = await crypto.subtle.importKey('raw', key, 'AES-CBC', false, ['encrypt'])
74
+ return cat(iv, new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-CBC', iv }, k, data)))
75
+ }
76
+
77
+ /**
78
+ * ISO 32000-2, Algorithm 2.B — the hardened password hash revision 6 uses.
79
+ *
80
+ * The loop is the point of it: at least 64 rounds, each one AES over 64 copies
81
+ * of (password ‖ K ‖ udata) and then one of three SHA-2 variants chosen by the
82
+ * data itself, and it only stops once the last byte of a round's output falls
83
+ * below round − 32. That makes a guess cost work no table can amortise.
84
+ *
85
+ * @param {Uint8Array} password UTF-8, already truncated to 127 bytes
86
+ * @param {Uint8Array} salt 8 bytes
87
+ * @param {Uint8Array} udata the 48-byte /U for an owner hash; empty for a user one
88
+ */
89
+ async function hash2B(password, salt, udata) {
90
+ let k = await sha(256, cat(password, salt, udata))
91
+ for (let round = 0; ; round++) {
92
+ const block = cat(password, k, udata)
93
+ const k1 = new Uint8Array(block.length * 64)
94
+ for (let i = 0; i < 64; i++) k1.set(block, i * block.length)
95
+ const e = await aesNoPad(k.subarray(0, 16), k.subarray(16, 32), k1)
96
+ let mod = 0
97
+ for (let i = 0; i < 16; i++) mod += e[i]
98
+ k = await sha([256, 384, 512][mod % 3], e)
99
+ if (round >= 63 && e[e.length - 1] <= round - 31) break
100
+ }
101
+ return k.subarray(0, 32)
102
+ }
103
+
104
+ /** A PDF password: UTF-8, capped at 127 bytes, cut on a code point boundary. */
105
+ function passwordBytes(text) {
106
+ const raw = utf8(String(text ?? ''))
107
+ if (raw.length <= 127) return raw
108
+ let end = 127
109
+ while (end > 0 && (raw[end] & 0xc0) === 0x80) end--
110
+ return raw.subarray(0, end)
111
+ }
112
+
113
+ /**
114
+ * The permission bits of `/P`. Bits 1–2 and 7–8 are reserved and always 1; every
115
+ * bit this does not turn off stays on, so the default grants everything.
116
+ *
117
+ * Bit 10 — extraction for accessibility — is NOT exposed. It is deprecated in
118
+ * PDF 2.0 (a conforming reader must always allow it) and PDF/UA requires an
119
+ * encrypted file to keep permitting it, so an exporter whose entire selling
120
+ * point is an extractable text layer has no business offering to switch it off.
121
+ */
122
+ const PERMISSION_BITS = {
123
+ print: 3, modify: 4, copy: 5, annotate: 6,
124
+ fillForms: 9, assemble: 11, printHighRes: 12,
125
+ }
126
+
127
+ function permissionValue(permissions, warn) {
128
+ let p = -1 // every bit set
129
+ // Bits 1, 2, 7, 8 are reserved and must be 1; -1 already has them.
130
+ for (const [name, value] of Object.entries(permissions || {})) {
131
+ const bit = PERMISSION_BITS[name]
132
+ if (bit === undefined) {
133
+ warn(`encrypt.permissions.${name} is not a permission — ignored. Known: ` +
134
+ `${Object.keys(PERMISSION_BITS).join(', ')}.`)
135
+ continue
136
+ }
137
+ if (value === false) p &= ~(1 << (bit - 1))
138
+ }
139
+ return p | 0
140
+ }
141
+
142
+ /**
143
+ * Everything a document needs to be encrypted: the file key, and the /Encrypt
144
+ * dictionary that lets a reader recover it from a password.
145
+ *
146
+ * @param {object} args
147
+ * @param {string} args.userPassword opens the document; '' means "opens without one"
148
+ * @param {string} [args.ownerPassword] lifts the permission flags; defaults to the user one
149
+ * @param {object} [args.permissions] `{print:false, copy:false, …}`
150
+ * @param {(msg:string)=>void} args.warn
151
+ */
152
+ export async function buildEncryption({ userPassword, ownerPassword, permissions, warn }) {
153
+ const user = passwordBytes(userPassword)
154
+ // An owner password equal to the user one is the common case and is fine; what
155
+ // is NOT fine is an owner password alone, because then anyone opens the file
156
+ // and only the advisory flags stand between them and everything.
157
+ const owner = passwordBytes(ownerPassword ?? userPassword)
158
+ const p = permissionValue(permissions, warn)
159
+
160
+ // Random, never derived: see the file comment.
161
+ const fileKey = random(32)
162
+ const uValidation = random(8)
163
+ const uKeySalt = random(8)
164
+ const oValidation = random(8)
165
+ const oKeySalt = random(8)
166
+
167
+ // Algorithm 8: /U proves a user password, /UE hands over the file key.
168
+ const uHash = await hash2B(user, uValidation, new Uint8Array(0))
169
+ const U = cat(uHash, uValidation, uKeySalt)
170
+ const UE = await aesNoPad(await hash2B(user, uKeySalt, new Uint8Array(0)), new Uint8Array(16), fileKey)
171
+
172
+ // Algorithm 9: the owner hashes are bound to /U, so /O cannot be lifted onto
173
+ // another document.
174
+ const oHash = await hash2B(owner, oValidation, U)
175
+ const O = cat(oHash, oValidation, oKeySalt)
176
+ const OE = await aesNoPad(await hash2B(owner, oKeySalt, U), new Uint8Array(16), fileKey)
177
+
178
+ // Algorithm 10: /Perms is the permission set encrypted under the file key, so
179
+ // a reader that HAS the key can tell whether the flags were tampered with.
180
+ // Bytes 12–15 are the random filler the algorithm asks for.
181
+ const perms = new Uint8Array(16)
182
+ new DataView(perms.buffer).setInt32(0, p, true)
183
+ perms.set([0xff, 0xff, 0xff, 0xff], 4)
184
+ perms[8] = 0x54 // 'T': the metadata stream is encrypted too
185
+ perms.set([0x61, 0x64, 0x62], 9) // 'adb'
186
+ perms.set(random(4), 12)
187
+ const Perms = await aesNoPad(fileKey, new Uint8Array(16), perms)
188
+
189
+ const dict =
190
+ '/Filter /Standard /V 5 /R 6 /Length 256 ' +
191
+ '/CF << /StdCF << /CFM /AESV3 /AuthEvent /DocOpen /Length 32 >> >> ' +
192
+ '/StmF /StdCF /StrF /StdCF ' +
193
+ `/O <${hex(O)}> /U <${hex(U)}> /OE <${hex(OE)}> /UE <${hex(UE)}> ` +
194
+ `/Perms <${hex(Perms)}> /P ${p} /EncryptMetadata true`
195
+
196
+ return {
197
+ dict,
198
+ p,
199
+ /**
200
+ * Encrypt one stream payload or one string. Revision 6 uses the file key
201
+ * directly (the per-object key derivation of revisions 2–4 is gone), so an
202
+ * object number is not needed and cannot be got wrong. Every call draws its
203
+ * own random IV, which travels as the first 16 bytes.
204
+ */
205
+ async encrypt(bytes) {
206
+ return aesCbc(fileKey, random(16), bytes)
207
+ },
208
+ }
209
+ }
210
+
211
+ /**
212
+ * The catalog entry that tells an ISO 32000-1 reader this file uses a feature
213
+ * from the next edition. `%PDF-1.7` stays in the header — the file structure IS
214
+ * 1.7 — and AES-256 arrived as Adobe extension level 3 before ISO 32000-2
215
+ * standardised it, which is the shape every reader in the field learned.
216
+ */
217
+ export const AES256_EXTENSION =
218
+ '/Extensions << /ADBE << /BaseVersion /1.7 /ExtensionLevel 3 >> >>'