reamkit 1.18.0 → 1.20.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.
@@ -14,8 +14,10 @@ export interface ReamParseOptions {
14
14
  /** Reader registry override; defaults to the built-in docx + xlsx readers. */
15
15
  readonly readers?: ReadonlyArray<DocumentReader<SourceDoc>>;
16
16
  /**
17
- * The user password for an encrypted PDF source (ISO 32000 §7.6). Defaults to
18
- * the empty string, which opens the common permissions-only encryption (EP14).
17
+ * The password an encrypted source is opened with: a PDF's user password
18
+ * (ISO 32000 §7.6) or an OOXML package's (ECMA-376 §2.3, MS-OFFCRYPTO).
19
+ * Defaults to the empty string, which opens a PDF's common permissions-only
20
+ * encryption (EP14); an encrypted OOXML package always names a password.
19
21
  */
20
22
  readonly password?: string;
21
23
  }
@@ -13,8 +13,8 @@ import { writeXlsx } from "../../excel/xlsx-writer.js";
13
13
  import { writeHtml } from "../../html/html-writer.js";
14
14
  import { writeSvg } from "../../svg/svg-writer.js";
15
15
  import { resolveDocxAutoFonts } from "../../word/docx-to-pdf.js";
16
- import { isCfb, openCfb } from "../ole/cfb.js";
17
16
  import { DEFAULT_READERS, resolveFontsViaChain, toFlowDoc } from "./facade.js";
17
+ import { decryptPackage, isEncryptedPackage } from "../crypto/offcrypto.js";
18
18
  //#region src/core/converter/ream.ts
19
19
  /**
20
20
  * The point size Excel's column-width unit is quoted at — its default theme
@@ -77,12 +77,14 @@ var Ream = class Ream {
77
77
  */
78
78
  static parse(bytes, options = {}) {
79
79
  const readers = options.readers ?? DEFAULT_READERS;
80
- const reader = readers.find((r) => r.sniff(bytes));
81
- if (!reader) {
82
- if (isEncryptedOoxml(bytes)) throw new Error("Password-protected OOXML document (ECMA-376 §2.3 EncryptedPackage) — re-save it without a password");
83
- throw new Error(`Unrecognized document format (readers: ${readers.map((r) => r.id).join(", ")})`);
80
+ let source = bytes;
81
+ if (isEncryptedPackage(bytes)) {
82
+ if (options.password === void 0) throw new Error("Password-protected OOXML document (ECMA-376 §2.3 EncryptedPackage) — pass the password as `password`");
83
+ source = decryptPackage(bytes, options.password);
84
84
  }
85
- const { doc, losses } = reader.read(bytes, { password: options.password });
85
+ const reader = readers.find((r) => r.sniff(source));
86
+ if (!reader) throw new Error(`Unrecognized document format (readers: ${readers.map((r) => r.id).join(", ")})`);
87
+ const { doc, losses } = reader.read(source, { password: options.password });
86
88
  const sheet = doc.kind === "sheet" ? doc : void 0;
87
89
  return new Ream(toFlowDoc(doc), sheet, losses, bytes, reader.id);
88
90
  }
@@ -232,13 +234,5 @@ var Ream = class Ream {
232
234
  if (options.strict && losses.length > 0) throw new ConversionLossError(losses[0]);
233
235
  }
234
236
  };
235
- function isEncryptedOoxml(bytes) {
236
- if (!isCfb(bytes)) return false;
237
- try {
238
- return openCfb(bytes).hasStream("EncryptedPackage");
239
- } catch {
240
- return false;
241
- }
242
- }
243
237
  //#endregion
244
238
  export { Ream };
@@ -0,0 +1,16 @@
1
+ /** Thrown when the bytes are encrypted and the password does not open them. */
2
+ export declare class WrongPasswordError extends Error {
3
+ constructor(message?: string);
4
+ }
5
+ /** Whether the bytes are an OOXML package encrypted per ECMA-376 §2.3. */
6
+ export declare function isEncryptedPackage(bytes: Uint8Array): boolean;
7
+ /**
8
+ * Decrypt an encrypted OOXML package into the plain OPC zip inside it.
9
+ *
10
+ * @param bytes The compound file (`EncryptionInfo` + `EncryptedPackage`).
11
+ * @param password The password to open it with.
12
+ * @returns The decrypted package bytes — an ordinary `.docx`/`.xlsx` zip.
13
+ * @throws WrongPasswordError when the password fails the document's own
14
+ * verifier, and Error when the encryption is one this reader does not know.
15
+ */
16
+ export declare function decryptPackage(bytes: Uint8Array, password: string): Uint8Array;
@@ -0,0 +1,222 @@
1
+ import { openCfb } from "../ole/cfb.js";
2
+ import { aesCbcDecrypt, aesEcbDecrypt, sha1, sha256, sha384, sha512 } from "./primitives.js";
3
+ //#region src/core/crypto/offcrypto.ts
4
+ /** Thrown when the bytes are encrypted and the password does not open them. */
5
+ var WrongPasswordError = class extends Error {
6
+ constructor(message = "Wrong password for this document") {
7
+ super(message);
8
+ this.name = "WrongPasswordError";
9
+ }
10
+ };
11
+ /** Whether the bytes are an OOXML package encrypted per ECMA-376 §2.3. */
12
+ function isEncryptedPackage(bytes) {
13
+ try {
14
+ const cfb = openCfb(bytes);
15
+ return cfb.hasStream("EncryptedPackage") && cfb.hasStream("EncryptionInfo");
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+ /**
21
+ * Decrypt an encrypted OOXML package into the plain OPC zip inside it.
22
+ *
23
+ * @param bytes The compound file (`EncryptionInfo` + `EncryptedPackage`).
24
+ * @param password The password to open it with.
25
+ * @returns The decrypted package bytes — an ordinary `.docx`/`.xlsx` zip.
26
+ * @throws WrongPasswordError when the password fails the document's own
27
+ * verifier, and Error when the encryption is one this reader does not know.
28
+ */
29
+ function decryptPackage(bytes, password) {
30
+ const cfb = openCfb(bytes);
31
+ const info = cfb.readStream("EncryptionInfo");
32
+ const payload = cfb.readStream("EncryptedPackage");
33
+ if (!info || !payload) throw new Error("Not an encrypted OOXML package");
34
+ const v = new DataView(info.buffer, info.byteOffset, info.byteLength);
35
+ const major = v.getUint16(0, true);
36
+ const minor = v.getUint16(2, true);
37
+ if (major === 4 && minor === 4) return agile(info, payload, password);
38
+ if ((major === 3 || major === 4) && minor === 2) return standard(info, payload, password);
39
+ throw new Error(`Unsupported OOXML encryption (version ${String(major)}.${String(minor)})`);
40
+ }
41
+ function packageSize(payload) {
42
+ const v = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
43
+ const lo = v.getUint32(0, true);
44
+ return v.getUint32(4, true) * 4294967296 + lo;
45
+ }
46
+ function standard(info, payload, password) {
47
+ const v = new DataView(info.buffer, info.byteOffset, info.byteLength);
48
+ const headerSize = v.getUint32(8, true);
49
+ const algId = v.getUint32(20, true);
50
+ const keyBits = v.getUint32(28, true);
51
+ let at = 12 + headerSize;
52
+ const saltSize = v.getUint32(at, true);
53
+ at += 4;
54
+ const salt = info.subarray(at, at + saltSize);
55
+ at += saltSize;
56
+ const encryptedVerifier = info.subarray(at, at + 16);
57
+ at += 16;
58
+ const verifierHashSize = v.getUint32(at, true);
59
+ at += 4;
60
+ const encryptedVerifierHash = info.subarray(at, at + (verifierHashSize <= 20 ? 32 : 32));
61
+ if (algId !== 0 && (algId < 26126 || algId > 26128)) throw new Error(`Unsupported OOXML cipher (algId 0x${algId.toString(16)})`);
62
+ const key = standardKey(password, salt, Math.max(16, Math.floor((keyBits || 128) / 8)));
63
+ const verifier = aesEcbDecrypt(key, encryptedVerifier);
64
+ const expected = aesEcbDecrypt(key, encryptedVerifierHash).subarray(0, 20);
65
+ if (!sameBytes(sha1(verifier), expected)) throw new WrongPasswordError();
66
+ const plain = aesEcbDecrypt(key, payload.subarray(8));
67
+ return plain.subarray(0, Math.min(packageSize(payload), plain.length));
68
+ }
69
+ function standardKey(password, salt, keyLength) {
70
+ let h = sha1(concat(salt, utf16le(password)));
71
+ const counter = new Uint8Array(4);
72
+ for (let i = 0; i < 5e4; i++) {
73
+ new DataView(counter.buffer).setUint32(0, i, true);
74
+ h = sha1(concat(counter, h));
75
+ }
76
+ h = sha1(concat(h, new Uint8Array(4)));
77
+ return concat(sha1(padded(h, 54)), sha1(padded(h, 92))).subarray(0, keyLength);
78
+ }
79
+ function padded(h, byte) {
80
+ const buf = new Uint8Array(64).fill(byte);
81
+ for (let i = 0; i < h.length && i < 64; i++) buf[i] = buf[i] ^ h[i];
82
+ return buf;
83
+ }
84
+ var BLOCK_VERIFIER_INPUT = Uint8Array.of(254, 167, 210, 118, 59, 75, 158, 121);
85
+ var BLOCK_VERIFIER_VALUE = Uint8Array.of(215, 170, 15, 109, 48, 97, 52, 78);
86
+ var BLOCK_KEY_VALUE = Uint8Array.of(20, 110, 11, 231, 171, 172, 208, 214);
87
+ var SEGMENT = 4096;
88
+ function agile(info, payload, password) {
89
+ const xml = new TextDecoder().decode(info.subarray(8));
90
+ const keyData = agileKeyData(attrsOf(xml, "keyData"));
91
+ const encAttrs = passwordKeyEncryptor(xml);
92
+ const enc = agileKeyData(encAttrs);
93
+ const spinCount = Number(encAttrs["spinCount"] ?? "0");
94
+ const encryptedKeyValue = b64(encAttrs["encryptedKeyValue"] ?? "");
95
+ const encryptedVerifierHashInput = b64(encAttrs["encryptedVerifierHashInput"] ?? "");
96
+ const encryptedVerifierHashValue = b64(encAttrs["encryptedVerifierHashValue"] ?? "");
97
+ const spun = agileSpin(password, enc, spinCount);
98
+ const keyFor = (block) => agileBlockKey(spun, enc, block);
99
+ const verifier = decryptAgile(keyFor(BLOCK_VERIFIER_INPUT), enc.saltValue, encryptedVerifierHashInput, enc.cbc);
100
+ const expected = decryptAgile(keyFor(BLOCK_VERIFIER_VALUE), enc.saltValue, encryptedVerifierHashValue, enc.cbc);
101
+ const actual = enc.hash(verifier.subarray(0, enc.saltValue.length));
102
+ if (!sameBytes(actual, expected.subarray(0, actual.length))) throw new WrongPasswordError();
103
+ const packageKey = decryptAgile(keyFor(BLOCK_KEY_VALUE), enc.saltValue, encryptedKeyValue, enc.cbc).subarray(0, keyData.keyBits / 8);
104
+ const cipher = payload.subarray(8);
105
+ const out = new Uint8Array(cipher.length);
106
+ const counter = new Uint8Array(4);
107
+ for (let i = 0; i * SEGMENT < cipher.length; i++) {
108
+ new DataView(counter.buffer).setUint32(0, i, true);
109
+ const iv = keyData.hash(concat(keyData.saltValue, counter)).subarray(0, keyData.blockSize || 16);
110
+ const block = cipher.subarray(i * SEGMENT, Math.min((i + 1) * SEGMENT, cipher.length));
111
+ out.set(decryptAgile(packageKey, iv, block, keyData.cbc), i * SEGMENT);
112
+ }
113
+ return out.subarray(0, Math.min(packageSize(payload), out.length));
114
+ }
115
+ function agileSpin(password, enc, spinCount) {
116
+ let h = enc.hash(concat(enc.saltValue, utf16le(password)));
117
+ const counter = new Uint8Array(4);
118
+ for (let i = 0; i < spinCount; i++) {
119
+ new DataView(counter.buffer).setUint32(0, i, true);
120
+ h = enc.hash(concat(counter, h));
121
+ }
122
+ return h;
123
+ }
124
+ function agileBlockKey(spun, enc, block) {
125
+ const keyLength = enc.keyBits / 8;
126
+ const derived = enc.hash(concat(spun, block));
127
+ if (derived.length >= keyLength) return derived.subarray(0, keyLength);
128
+ const out = new Uint8Array(keyLength).fill(54);
129
+ out.set(derived);
130
+ return out;
131
+ }
132
+ function decryptAgile(key, iv, data, cbc) {
133
+ if (!cbc) return aesEcbDecrypt(key, data);
134
+ return aesCbcDecrypt(key, iv.length >= 16 ? iv.subarray(0, 16) : concat(iv, new Uint8Array(16 - iv.length)), data, false);
135
+ }
136
+ var HASHES = new Map([
137
+ ["sha1", sha1],
138
+ ["sha-1", sha1],
139
+ ["sha256", sha256],
140
+ ["sha-256", sha256],
141
+ ["sha384", sha384],
142
+ ["sha-384", sha384],
143
+ ["sha512", sha512],
144
+ ["sha-512", sha512]
145
+ ]);
146
+ function agileKeyData(attrs) {
147
+ const name = (attrs["hashAlgorithm"] ?? "sha512").toLowerCase();
148
+ const hash = HASHES.get(name);
149
+ if (!hash) throw new Error(`Unsupported OOXML hash (${name})`);
150
+ const cipher = (attrs["cipherAlgorithm"] ?? "AES").toUpperCase();
151
+ if (!cipher.startsWith("AES")) throw new Error(`Unsupported OOXML cipher (${cipher})`);
152
+ return {
153
+ saltValue: b64(attrs["saltValue"] ?? ""),
154
+ blockSize: Number(attrs["blockSize"] ?? "16"),
155
+ keyBits: Number(attrs["keyBits"] ?? "256"),
156
+ hash,
157
+ cbc: (attrs["cipherChaining"] ?? "ChainingModeCBC") !== "ChainingModeECB"
158
+ };
159
+ }
160
+ function passwordKeyEncryptor(xml) {
161
+ const re = /<[A-Za-z0-9]*:?encryptedKey\b([^>]*)\/?>/gu;
162
+ let m = re.exec(xml);
163
+ while (m) {
164
+ const attrs = parseAttrs(m[1] ?? "");
165
+ if (attrs["spinCount"] !== void 0) return attrs;
166
+ m = re.exec(xml);
167
+ }
168
+ throw new Error("No password key encryptor in the encryption descriptor");
169
+ }
170
+ function attrsOf(xml, tag) {
171
+ const m = new RegExp(`<[A-Za-z0-9]*:?${tag}\\b([^>]*)/?>`, "u").exec(xml);
172
+ return m ? parseAttrs(m[1] ?? "") : {};
173
+ }
174
+ function parseAttrs(raw) {
175
+ const out = {};
176
+ const re = /([A-Za-z0-9_:]+)\s*=\s*"([^"]*)"/gu;
177
+ let m = re.exec(raw);
178
+ while (m) {
179
+ out[m[1].replace(/^.*:/u, "")] = m[2];
180
+ m = re.exec(raw);
181
+ }
182
+ return out;
183
+ }
184
+ function utf16le(s) {
185
+ const out = new Uint8Array(s.length * 2);
186
+ const v = new DataView(out.buffer);
187
+ for (let i = 0; i < s.length; i++) v.setUint16(i * 2, s.charCodeAt(i), true);
188
+ return out;
189
+ }
190
+ function concat(a, b) {
191
+ const out = new Uint8Array(a.length + b.length);
192
+ out.set(a);
193
+ out.set(b, a.length);
194
+ return out;
195
+ }
196
+ function sameBytes(a, b) {
197
+ if (a.length !== b.length) return false;
198
+ let diff = 0;
199
+ for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
200
+ return diff === 0;
201
+ }
202
+ var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
203
+ function b64(s) {
204
+ const clean = s.replace(/[^A-Za-z0-9+/]/gu, "");
205
+ const out = new Uint8Array(Math.floor(clean.length * 3 / 4));
206
+ let acc = 0;
207
+ let bits = 0;
208
+ let at = 0;
209
+ for (const ch of clean) {
210
+ const v = B64.indexOf(ch);
211
+ if (v < 0) continue;
212
+ acc = acc << 6 | v;
213
+ bits += 6;
214
+ if (bits >= 8) {
215
+ bits -= 8;
216
+ out[at++] = acc >> bits & 255;
217
+ }
218
+ }
219
+ return out.subarray(0, at);
220
+ }
221
+ //#endregion
222
+ export { decryptPackage, isEncryptedPackage };
@@ -5,8 +5,18 @@
5
5
  export declare function rc4(key: Uint8Array, data: Uint8Array): Uint8Array;
6
6
  /** MD5 digest (RFC 1321) → 16 bytes; the key-derivation hash for V1/V2/V4. */
7
7
  export declare function md5(input: Uint8Array): Uint8Array;
8
+ /**
9
+ * SHA-1. Used by MS-OFFCRYPTO's standard encryption for its key derivation and
10
+ * password verifier, and by agile encryption when the descriptor names it.
11
+ */
12
+ export declare function sha1(input: Uint8Array): Uint8Array;
8
13
  export declare function sha256(input: Uint8Array): Uint8Array;
9
14
  export declare function sha512(input: Uint8Array): Uint8Array;
10
15
  export declare function sha384(input: Uint8Array): Uint8Array;
11
16
  export declare function aesCbcDecrypt(key: Uint8Array, iv: Uint8Array, data: Uint8Array, stripPad: boolean): Uint8Array;
17
+ /**
18
+ * AES-ECB decrypt — every block on its own key, no chaining. MS-OFFCRYPTO's
19
+ * standard encryption uses it for both the verifier and the package.
20
+ */
21
+ export declare function aesEcbDecrypt(key: Uint8Array, data: Uint8Array): Uint8Array;
12
22
  export declare function aesCbcEncrypt(key: Uint8Array, iv: Uint8Array, data: Uint8Array): Uint8Array;
@@ -1,4 +1,4 @@
1
- //#region src/pdf-reader/crypto.ts
1
+ //#region src/core/crypto/primitives.ts
2
2
  /**
3
3
  * RC4 stream cipher (§7.6.2). Symmetric, so the same call both encrypts and
4
4
  * decrypts; used by the legacy V1/V2/V4 Standard security handlers.
@@ -217,6 +217,58 @@ var SHA256_K = [
217
217
  3204031479,
218
218
  3329325298
219
219
  ];
220
+ /**
221
+ * SHA-1. Used by MS-OFFCRYPTO's standard encryption for its key derivation and
222
+ * password verifier, and by agile encryption when the descriptor names it.
223
+ */
224
+ function sha1(input) {
225
+ const ml = input.length * 8;
226
+ const withPad = new Uint8Array((input.length + 8 >> 6 << 6) + 64);
227
+ withPad.set(input);
228
+ withPad[input.length] = 128;
229
+ const view = new DataView(withPad.buffer);
230
+ view.setUint32(withPad.length - 4, ml >>> 0, false);
231
+ view.setUint32(withPad.length - 8, Math.floor(ml / 4294967296), false);
232
+ let h0 = 1732584193;
233
+ let h1 = 4023233417;
234
+ let h2 = 2562383102;
235
+ let h3 = 271733878;
236
+ let h4 = 3285377520;
237
+ const w = new Uint32Array(80);
238
+ for (let off = 0; off < withPad.length; off += 64) {
239
+ for (let i = 0; i < 16; i++) w[i] = view.getUint32(off + i * 4, false);
240
+ for (let i = 16; i < 80; i++) {
241
+ const x = w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16];
242
+ w[i] = x << 1 | x >>> 31;
243
+ }
244
+ let a = h0;
245
+ let b = h1;
246
+ let c = h2;
247
+ let d = h3;
248
+ let e = h4;
249
+ for (let i = 0; i < 80; i++) {
250
+ const [f, k] = i < 20 ? [b & c | ~b & d, 1518500249] : i < 40 ? [b ^ c ^ d, 1859775393] : i < 60 ? [b & c | b & d | c & d, 2400959708] : [b ^ c ^ d, 3395469782];
251
+ const t = (a << 5 | a >>> 27) + f + e + k + w[i] >>> 0;
252
+ e = d;
253
+ d = c;
254
+ c = (b << 30 | b >>> 2) >>> 0;
255
+ b = a;
256
+ a = t;
257
+ }
258
+ h0 = h0 + a >>> 0;
259
+ h1 = h1 + b >>> 0;
260
+ h2 = h2 + c >>> 0;
261
+ h3 = h3 + d >>> 0;
262
+ h4 = h4 + e >>> 0;
263
+ }
264
+ const out = new Uint8Array(20);
265
+ new DataView(out.buffer).setUint32(0, h0, false);
266
+ new DataView(out.buffer).setUint32(4, h1, false);
267
+ new DataView(out.buffer).setUint32(8, h2, false);
268
+ new DataView(out.buffer).setUint32(12, h3, false);
269
+ new DataView(out.buffer).setUint32(16, h4, false);
270
+ return out;
271
+ }
220
272
  function sha256(input) {
221
273
  const h = [
222
274
  1779033703,
@@ -275,7 +327,6 @@ function sha256(input) {
275
327
  for (let i = 0; i < 8; i++) new DataView(out.buffer).setUint32(i * 4, h[i] >>> 0);
276
328
  return out;
277
329
  }
278
- var MASK64 = (1n << 64n) - 1n;
279
330
  var SHA512_K = [
280
331
  4794697086780616226n,
281
332
  8158064640168781261n,
@@ -358,62 +409,130 @@ var SHA512_K = [
358
409
  6902733635092675308n,
359
410
  7801388544844847127n
360
411
  ];
412
+ var SHA512_KH = new Int32Array(80);
413
+ var SHA512_KL = new Int32Array(80);
414
+ for (let i = 0; i < 80; i++) {
415
+ SHA512_KH[i] = Number(SHA512_K[i] >> 32n & 4294967295n) | 0;
416
+ SHA512_KL[i] = Number(SHA512_K[i] & 4294967295n) | 0;
417
+ }
361
418
  function sha512Core(iv, input, outLen) {
362
- const rotr = (x, n) => (x >> n | x << 64n - n) & MASK64;
363
- const h = [...iv];
419
+ const hh = new Int32Array(8);
420
+ const hl = new Int32Array(8);
421
+ for (let i = 0; i < 8; i++) {
422
+ hh[i] = Number(iv[i] >> 32n & 4294967295n) | 0;
423
+ hl[i] = Number(iv[i] & 4294967295n) | 0;
424
+ }
364
425
  const len = input.length;
365
426
  const blocks = (len + 16 >> 7) + 1;
366
427
  const msg = new Uint8Array(blocks * 128);
367
428
  msg.set(input);
368
429
  msg[len] = 128;
369
- let bitLen = BigInt(len) * 8n;
370
- for (let i = 0; i < 16; i++) {
371
- msg[msg.length - 1 - i] = Number(bitLen & 255n);
372
- bitLen >>= 8n;
373
- }
374
- const w = new Array(80);
430
+ const view = new DataView(msg.buffer);
431
+ const bits = len * 8;
432
+ view.setUint32(msg.length - 4, bits >>> 0, false);
433
+ view.setUint32(msg.length - 8, Math.floor(bits / 4294967296), false);
434
+ const wh = new Int32Array(80);
435
+ const wl = new Int32Array(80);
375
436
  for (let off = 0; off < msg.length; off += 128) {
376
437
  for (let i = 0; i < 16; i++) {
377
- let v = 0n;
378
- for (let b = 0; b < 8; b++) v = v << 8n | BigInt(msg[off + i * 8 + b]);
379
- w[i] = v;
438
+ wh[i] = view.getInt32(off + i * 8, false);
439
+ wl[i] = view.getInt32(off + i * 8 + 4, false);
380
440
  }
381
441
  for (let i = 16; i < 80; i++) {
382
- const s0 = rotr(w[i - 15], 1n) ^ rotr(w[i - 15], 8n) ^ w[i - 15] >> 7n;
383
- const s1 = rotr(w[i - 2], 19n) ^ rotr(w[i - 2], 61n) ^ w[i - 2] >> 6n;
384
- w[i] = w[i - 16] + s0 + w[i - 7] + s1 & MASK64;
442
+ const x15h = wh[i - 15];
443
+ const x15l = wl[i - 15];
444
+ const s0h = (x15h >>> 1 | x15l << 31) ^ (x15h >>> 8 | x15l << 24) ^ x15h >>> 7;
445
+ const s0l = (x15l >>> 1 | x15h << 31) ^ (x15l >>> 8 | x15h << 24) ^ (x15l >>> 7 | x15h << 25);
446
+ const x2h = wh[i - 2];
447
+ const x2l = wl[i - 2];
448
+ const s1h = (x2h >>> 19 | x2l << 13) ^ (x2l >>> 29 | x2h << 3) ^ x2h >>> 6;
449
+ const s1l = (x2l >>> 19 | x2h << 13) ^ (x2h >>> 29 | x2l << 3) ^ (x2l >>> 6 | x2h << 26);
450
+ let lo = (wl[i - 16] >>> 0) + (s0l >>> 0);
451
+ let hi = (wh[i - 16] >>> 0) + (s0h >>> 0) + (lo > 4294967295 ? 1 : 0);
452
+ lo = (lo >>> 0) + (wl[i - 7] >>> 0);
453
+ hi = hi + (wh[i - 7] >>> 0) + (lo > 4294967295 ? 1 : 0);
454
+ lo = (lo >>> 0) + (s1l >>> 0);
455
+ hi = hi + (s1h >>> 0) + (lo > 4294967295 ? 1 : 0);
456
+ wh[i] = hi | 0;
457
+ wl[i] = lo | 0;
385
458
  }
386
- let [a, b, c, d, e, f, g, hh] = h;
459
+ let ah = hh[0];
460
+ let al = hl[0];
461
+ let bh = hh[1];
462
+ let bl = hl[1];
463
+ let ch = hh[2];
464
+ let cl = hl[2];
465
+ let dh = hh[3];
466
+ let dl = hl[3];
467
+ let eh = hh[4];
468
+ let el = hl[4];
469
+ let fh = hh[5];
470
+ let fl = hl[5];
471
+ let gh = hh[6];
472
+ let gl = hl[6];
473
+ let xh = hh[7];
474
+ let xl = hl[7];
387
475
  for (let i = 0; i < 80; i++) {
388
- const S1 = rotr(e, 14n) ^ rotr(e, 18n) ^ rotr(e, 41n);
389
- const ch = e & f ^ ~e & MASK64 & g;
390
- const t1 = hh + S1 + ch + SHA512_K[i] + w[i] & MASK64;
391
- const t2 = (rotr(a, 28n) ^ rotr(a, 34n) ^ rotr(a, 39n)) + (a & b ^ a & c ^ b & c) & MASK64;
392
- hh = g;
393
- g = f;
394
- f = e;
395
- e = d + t1 & MASK64;
396
- d = c;
397
- c = b;
398
- b = a;
399
- a = t1 + t2 & MASK64;
476
+ const S1h = (eh >>> 14 | el << 18) ^ (eh >>> 18 | el << 14) ^ (el >>> 9 | eh << 23);
477
+ const S1l = (el >>> 14 | eh << 18) ^ (el >>> 18 | eh << 14) ^ (eh >>> 9 | el << 23);
478
+ const chh = eh & fh ^ ~eh & gh;
479
+ const chl = el & fl ^ ~el & gl;
480
+ const S0h = (ah >>> 28 | al << 4) ^ (al >>> 2 | ah << 30) ^ (al >>> 7 | ah << 25);
481
+ const S0l = (al >>> 28 | ah << 4) ^ (ah >>> 2 | al << 30) ^ (ah >>> 7 | al << 25);
482
+ const majh = ah & bh ^ ah & ch ^ bh & ch;
483
+ const majl = al & bl ^ al & cl ^ bl & cl;
484
+ let lo = (xl >>> 0) + (S1l >>> 0);
485
+ let hi = (xh >>> 0) + (S1h >>> 0) + (lo > 4294967295 ? 1 : 0);
486
+ lo = (lo >>> 0) + (chl >>> 0);
487
+ hi = hi + (chh >>> 0) + (lo > 4294967295 ? 1 : 0);
488
+ lo = (lo >>> 0) + (SHA512_KL[i] >>> 0);
489
+ hi = hi + (SHA512_KH[i] >>> 0) + (lo > 4294967295 ? 1 : 0);
490
+ lo = (lo >>> 0) + (wl[i] >>> 0);
491
+ hi = hi + (wh[i] >>> 0) + (lo > 4294967295 ? 1 : 0);
492
+ const t1h = hi | 0;
493
+ const t1l = lo | 0;
494
+ const lo2 = (S0l >>> 0) + (majl >>> 0);
495
+ const t2h = (S0h >>> 0) + (majh >>> 0) + (lo2 > 4294967295 ? 1 : 0) | 0;
496
+ const t2l = lo2 | 0;
497
+ xh = gh;
498
+ xl = gl;
499
+ gh = fh;
500
+ gl = fl;
501
+ fh = eh;
502
+ fl = el;
503
+ lo = (dl >>> 0) + (t1l >>> 0);
504
+ eh = (dh >>> 0) + (t1h >>> 0) + (lo > 4294967295 ? 1 : 0) | 0;
505
+ el = lo | 0;
506
+ dh = ch;
507
+ dl = cl;
508
+ ch = bh;
509
+ cl = bl;
510
+ bh = ah;
511
+ bl = al;
512
+ lo = (t1l >>> 0) + (t2l >>> 0);
513
+ ah = (t1h >>> 0) + (t2h >>> 0) + (lo > 4294967295 ? 1 : 0) | 0;
514
+ al = lo | 0;
400
515
  }
401
- h[0] = h[0] + a & MASK64;
402
- h[1] = h[1] + b & MASK64;
403
- h[2] = h[2] + c & MASK64;
404
- h[3] = h[3] + d & MASK64;
405
- h[4] = h[4] + e & MASK64;
406
- h[5] = h[5] + f & MASK64;
407
- h[6] = h[6] + g & MASK64;
408
- h[7] = h[7] + hh & MASK64;
516
+ const add = (i, addH, addL) => {
517
+ const lo = (hl[i] >>> 0) + (addL >>> 0);
518
+ hh[i] = (hh[i] >>> 0) + (addH >>> 0) + (lo > 4294967295 ? 1 : 0) | 0;
519
+ hl[i] = lo | 0;
520
+ };
521
+ add(0, ah, al);
522
+ add(1, bh, bl);
523
+ add(2, ch, cl);
524
+ add(3, dh, dl);
525
+ add(4, eh, el);
526
+ add(5, fh, fl);
527
+ add(6, gh, gl);
528
+ add(7, xh, xl);
529
+ }
530
+ const out = new Uint8Array(64);
531
+ const ov = new DataView(out.buffer);
532
+ for (let i = 0; i < 8; i++) {
533
+ ov.setInt32(i * 8, hh[i], false);
534
+ ov.setInt32(i * 8 + 4, hl[i], false);
409
535
  }
410
- const out = new Uint8Array(h.length * 8);
411
- h.forEach((v, i) => {
412
- for (let b = 7; b >= 0; b--) {
413
- out[i * 8 + b] = Number(v & 255n);
414
- v >>= 8n;
415
- }
416
- });
417
536
  return out.slice(0, outLen);
418
537
  }
419
538
  function sha512(input) {
@@ -559,6 +678,18 @@ function aesCbcDecrypt(key, iv, data, stripPad) {
559
678
  const pad = out[out.length - 1];
560
679
  return pad >= 1 && pad <= 16 && pad <= out.length ? out.subarray(0, out.length - pad) : out;
561
680
  }
681
+ /**
682
+ * AES-ECB decrypt — every block on its own key, no chaining. MS-OFFCRYPTO's
683
+ * standard encryption uses it for both the verifier and the package.
684
+ */
685
+ function aesEcbDecrypt(key, data) {
686
+ const w = expandKey(key);
687
+ const nr = key.length / 4 + 6;
688
+ const blocks = Math.floor(data.length / 16);
689
+ const out = new Uint8Array(blocks * 16);
690
+ for (let i = 0; i < blocks; i++) decryptBlock(data.subarray(i * 16, i * 16 + 16), w, nr, out, i * 16);
691
+ return out;
692
+ }
562
693
  function aesCbcEncrypt(key, iv, data) {
563
694
  const w = expandKey(key);
564
695
  const nr = key.length / 4 + 6;
@@ -611,4 +742,4 @@ function mixColumns(s) {
611
742
  }
612
743
  }
613
744
  //#endregion
614
- export { aesCbcDecrypt, aesCbcEncrypt, md5, rc4, sha256, sha384, sha512 };
745
+ export { aesCbcDecrypt, aesCbcEncrypt, aesEcbDecrypt, md5, rc4, sha1, sha256, sha384, sha512 };
@@ -992,6 +992,12 @@ export interface ShapeFill {
992
992
  readonly kind: ShapeFillKind;
993
993
  readonly colorHex?: string;
994
994
  readonly gradient?: ShapeGradient;
995
+ /**
996
+ * §14.1.2.5 `@type="tile"` — the picture REPEATS at its own size rather than
997
+ * stretching over the box. A parchment behind a page, a texture behind a
998
+ * text box: stretched, such a fill is a blur where it should be a pattern.
999
+ */
1000
+ readonly tiled?: boolean;
995
1001
  /**
996
1002
  * §20.1.8.14 `a:blipFill` — the picture painted across the shape's box. A
997
1003
  * DrawingML picture IS a shape with one of these, which is how a `pic:pic`
@@ -69,6 +69,13 @@ function hslToRgb(h, s, l) {
69
69
  b + m
70
70
  ];
71
71
  }
72
+ function toLinear(c) {
73
+ return c <= .04045 ? c / 12.92 : Math.pow((c + .055) / 1.055, 2.4);
74
+ }
75
+ function toSrgb(c) {
76
+ const v = clamp01(c);
77
+ return v <= .0031308 ? v * 12.92 : 1.055 * Math.pow(v, 1 / 2.4) - .055;
78
+ }
72
79
  /**
73
80
  * Apply DrawingML colour transforms ({@link ColorMod}s) to a 6-hex value,
74
81
  * returning a 6-hex value. `shade`/`tint` scale in RGB; `lumMod`/`lumOff` adjust
@@ -85,13 +92,13 @@ function applyColorMods(hex, mods) {
85
92
  let g = (n >> 8 & 255) / 255;
86
93
  let b = (n & 255) / 255;
87
94
  for (const m of mods) if (m.kind === "shade") {
88
- r *= m.val;
89
- g *= m.val;
90
- b *= m.val;
95
+ r = toSrgb(toLinear(r) * m.val);
96
+ g = toSrgb(toLinear(g) * m.val);
97
+ b = toSrgb(toLinear(b) * m.val);
91
98
  } else if (m.kind === "tint") {
92
- r = r * m.val + (1 - m.val);
93
- g = g * m.val + (1 - m.val);
94
- b = b * m.val + (1 - m.val);
99
+ r = toSrgb(toLinear(r) * m.val + (1 - m.val));
100
+ g = toSrgb(toLinear(g) * m.val + (1 - m.val));
101
+ b = toSrgb(toLinear(b) * m.val + (1 - m.val));
95
102
  } else if (m.kind === "lumMod" || m.kind === "lumOff") {
96
103
  const [h, s, l] = rgbToHsl(r, g, b);
97
104
  const l2 = m.kind === "lumMod" ? l * m.val : clamp01(l + m.val);
@@ -34,6 +34,15 @@ export declare function poAttrLocal(node: PoNode | undefined, name: string): str
34
34
  export declare function poIsLocal(node: PoNode | undefined, name: string): boolean;
35
35
  /** {@link poAttr} parsed as a finite number, or undefined when absent/non-numeric. */
36
36
  export declare function poIntAttr(node: PoNode | undefined, name: string): number | undefined;
37
+ /**
38
+ * §22.9.2.15 ST_UniversalMeasure — a number with a UNIT (`"28.35pt"`, `"1cm"`)
39
+ * as twips. Producers write whole page geometries, tab stops and indents this
40
+ * way; read as a plain number they are nothing at all.
41
+ *
42
+ * @param raw The attribute value.
43
+ * @returns The measure in twips, or undefined when it names no unit.
44
+ */
45
+ export declare function universalMeasureTwips(raw: string): number | undefined;
37
46
  /** Shorthand for the `val` attribute (`@w:val`/…). */
38
47
  export declare function poVal(node: PoNode | undefined): string | undefined;
39
48
  /**
@@ -76,6 +76,14 @@ var MEASURE_TWIPS = new Map([
76
76
  ["cm", 1440 / 2.54],
77
77
  ["mm", 144 / 2.54]
78
78
  ]);
79
+ /**
80
+ * §22.9.2.15 ST_UniversalMeasure — a number with a UNIT (`"28.35pt"`, `"1cm"`)
81
+ * as twips. Producers write whole page geometries, tab stops and indents this
82
+ * way; read as a plain number they are nothing at all.
83
+ *
84
+ * @param raw The attribute value.
85
+ * @returns The measure in twips, or undefined when it names no unit.
86
+ */
79
87
  function universalMeasureTwips(raw) {
80
88
  const m = /^\s*(-?[0-9]+(?:\.[0-9]+)?)(pt|pc|pi|in|cm|mm)\s*$/iu.exec(raw);
81
89
  if (!m) return void 0;
@@ -134,4 +142,4 @@ function poFindDescendant(node, tag) {
134
142
  }
135
143
  }
136
144
  //#endregion
137
- export { poAttr, poAttrLocal, poChildren, poChildrenWith, poFindByPath, poFindDescendant, poFirstChild, poIntAttr, poIs, poIsLocal, poTag, poText, poToggle, poVal };
145
+ export { poAttr, poAttrLocal, poChildren, poChildrenWith, poFindByPath, poFindDescendant, poFirstChild, poIntAttr, poIs, poIsLocal, poTag, poText, poToggle, poVal, universalMeasureTwips };
@@ -432,7 +432,7 @@ function layoutStyledDocument(body, options) {
432
432
  const commentNotes = options.commentAnnotations && options.comments ? buildCommentNotes(noteAssigned.comments, options.comments) : void 0;
433
433
  const reflowParagraph = (paragraph, width, widths) => layoutParagraphBlock(paragraph, options, fontResources, imageResources, width, widths);
434
434
  return {
435
- pages: paginateSections(blocks, sectionCtxs, structBuilder, options.language ?? "en-US", notePlan, bookmarks, reflowParagraph, options.pageBackgroundFill?.imageResource !== void 0 ? imageResources.get(options.pageBackgroundFill.imageResource)?.resourceName : void 0),
435
+ pages: paginateSections(blocks, sectionCtxs, structBuilder, options.language ?? "en-US", notePlan, bookmarks, reflowParagraph, backgroundImageOf(options, imageResources)),
436
436
  resources: options.resources ?? new ResourceStore(),
437
437
  fontResources,
438
438
  imageResources,
@@ -609,7 +609,7 @@ function balancedColumnHeight(bandHeightPt, colCount, roomPt) {
609
609
  /** Total laid-out height of a run of blocks, paragraph spacing included. */
610
610
  function blocksHeight(blocks) {
611
611
  return blocks.reduce((sum, b) => {
612
- if (b.kind !== "paragraph" && b.kind !== "table" && isOutOfFlowFloat(b.float)) return sum;
612
+ if (b.kind !== "paragraph" && isOutOfFlowFloat(b.float)) return sum;
613
613
  return sum + (b.kind === "paragraph" ? b.spacingBeforePt + b.heightPt + b.spacingAfterPt : b.heightPt);
614
614
  }, 0);
615
615
  }
@@ -1024,7 +1024,12 @@ function layoutShapeBlock(shape, options, fontResources, imageResources, content
1024
1024
  heightPt = maxHeight;
1025
1025
  }
1026
1026
  const fillGradient = shape.fill.kind === "gradient" ? shape.fill.gradient : void 0;
1027
- const fillImageResourceName = shape.fill.kind === "picture" && shape.fill.imageResource !== void 0 ? imageResources?.get(shape.fill.imageResource)?.resourceName : void 0;
1027
+ const fillImage = shape.fill.kind === "picture" && shape.fill.imageResource !== void 0 ? imageResources?.get(shape.fill.imageResource) : void 0;
1028
+ const fillImageResourceName = fillImage?.resourceName;
1029
+ const fillImageTile = shape.fill.kind === "picture" && shape.fill.tiled === true && fillImage?.prepared ? {
1030
+ widthPt: fillImage.prepared.widthPx * 72 / 96,
1031
+ heightPt: fillImage.prepared.heightPx * 72 / 96
1032
+ } : void 0;
1028
1033
  const fillColorHex = shape.fill.kind === "solid" ? shape.fill.colorHex : fillGradient ? gradientToSolid(fillGradient) : void 0;
1029
1034
  const stroke = buildStroke(shape.line);
1030
1035
  const t = shape.transform;
@@ -1178,6 +1183,7 @@ function layoutShapeBlock(shape, options, fontResources, imageResources, content
1178
1183
  paths,
1179
1184
  ...markerPaths.length > 0 ? { markerPaths } : {},
1180
1185
  ...fillImageResourceName ? { fillImageResourceName } : {},
1186
+ ...fillImageTile ? { fillImageTile } : {},
1181
1187
  ...fillImageResourceName && shape.fill.imageCrop ? { fillImageCrop: shape.fill.imageCrop } : {},
1182
1188
  ...fillColorHex ? { fillColorHex } : {},
1183
1189
  ...fillGradient ? { fillGradient } : {},
@@ -1612,6 +1618,21 @@ function makeChartLabelLine(text, font, sizePt, colorHex) {
1612
1618
  isLastInParagraph: true
1613
1619
  };
1614
1620
  }
1621
+ /**
1622
+ * §17.2.1 — the page background's picture, with the size a TILE of it is drawn
1623
+ * at: the picture's own pixels read at 96 dpi, which is the resolution Office
1624
+ * measures a picture in.
1625
+ */
1626
+ function backgroundImageOf(options, imageResources) {
1627
+ const id = options.pageBackgroundFill?.imageResource;
1628
+ const res = id === void 0 ? void 0 : imageResources.get(id);
1629
+ if (!res?.resourceName || !res.prepared) return void 0;
1630
+ return {
1631
+ name: res.resourceName,
1632
+ widthPt: res.prepared.widthPx * 72 / 96,
1633
+ heightPt: res.prepared.heightPx * 72 / 96
1634
+ };
1635
+ }
1615
1636
  function collectImageResources(body, headersFooters, options) {
1616
1637
  const out = /* @__PURE__ */ new Map();
1617
1638
  if (!options.resources || options.resources.size === 0) return out;
@@ -1794,20 +1815,38 @@ function emitCellFloat(cf, ctx, frame) {
1794
1815
  if (float.wrap !== "none") ctx.exclude(float, x, topYUp, cf.block.widthPt, cf.block.heightPt);
1795
1816
  }
1796
1817
  function emitShapeItems(sh, x, bottomYUp, sink, pageHeight, figId) {
1797
- if (sh.fillImageResourceName) sink.push({
1798
- type: "image",
1799
- x: pt(x),
1800
- y: pt(pageHeight - (bottomYUp + sh.heightPt)),
1801
- width: pt(sh.widthPt),
1802
- height: pt(sh.heightPt),
1803
- imageResourceName: sh.fillImageResourceName,
1804
- ...sh.fillImageCrop ? { crop: sh.fillImageCrop } : {},
1805
- clip: {
1818
+ if (sh.fillImageResourceName) {
1819
+ const clip = {
1806
1820
  paths: sh.paths,
1807
1821
  transform: flipTransform(buildShapeTransform(x, bottomYUp, sh.widthPt, sh.heightPt, sh.rotation60k, sh.flipH, sh.flipV), pageHeight)
1808
- },
1809
- ...figId !== void 0 ? { structId: figId } : {}
1810
- });
1822
+ };
1823
+ const fig = figId !== void 0 ? { structId: figId } : {};
1824
+ const tile = sh.fillImageTile;
1825
+ if (tile) for (const item of PageAssembler.tileItems(sh.fillImageResourceName, tile, {
1826
+ x,
1827
+ y: pageHeight - (bottomYUp + sh.heightPt),
1828
+ width: sh.widthPt,
1829
+ height: sh.heightPt
1830
+ })) {
1831
+ if (item.type !== "image") continue;
1832
+ sink.push({
1833
+ ...item,
1834
+ clip,
1835
+ ...fig
1836
+ });
1837
+ }
1838
+ else sink.push({
1839
+ type: "image",
1840
+ x: pt(x),
1841
+ y: pt(pageHeight - (bottomYUp + sh.heightPt)),
1842
+ width: pt(sh.widthPt),
1843
+ height: pt(sh.heightPt),
1844
+ imageResourceName: sh.fillImageResourceName,
1845
+ ...sh.fillImageCrop ? { crop: sh.fillImageCrop } : {},
1846
+ clip,
1847
+ ...fig
1848
+ });
1849
+ }
1811
1850
  if (sh.markerPaths && sh.stroke) sink.push({
1812
1851
  type: "shape",
1813
1852
  shape: {
@@ -1906,6 +1945,15 @@ function drawBlocksSequentially(blocks, startX, startY, pageHeight, contentWidth
1906
1945
  const ordered = blocks.some(isBehind) ? [...blocks.filter(isBehind), ...blocks.filter((b) => !isBehind(b))] : blocks;
1907
1946
  for (const block of ordered) {
1908
1947
  if (block.kind === "table") {
1948
+ const anchored = isOutOfFlowFloat(block.float) ? bandAnchor(block.float, block.totalWidthPt, contentWidth, startX) : void 0;
1949
+ if (anchored !== void 0 && block.float) {
1950
+ let rowY = bandAnchorTop(block.float, cursorY, pageHeight, block.heightPt, bandMargins);
1951
+ for (const row of block.rows) {
1952
+ emitRowChunk(out, row, startX + anchored, rowY, pageHeight, block.colCount);
1953
+ rowY -= row.heightPt;
1954
+ }
1955
+ continue;
1956
+ }
1909
1957
  const tableX = startX + block.xOffsetPt;
1910
1958
  for (const row of block.rows) {
1911
1959
  emitRowChunk(out, row, tableX, cursorY, pageHeight, block.colCount);
@@ -3211,9 +3259,67 @@ function alignmentOffset(alignment, lineWidth, available) {
3211
3259
  if (alignment === "center") return Math.max(0, (available - lineWidth) / 2);
3212
3260
  return 0;
3213
3261
  }
3262
+ /**
3263
+ * §17.4.85 `w:vMerge` — a vertical merge is ONE cell, so its continuation rows
3264
+ * are painted with the START cell's shading. Word writes the fill on the cell
3265
+ * that opens the merge and nothing on the rest; painting only what is written,
3266
+ * tdf123104.docx's three-row banner came out a staircase of yellow with white
3267
+ * notches where every merged cell continued.
3268
+ */
3269
+ function withMergedShading(table, roles) {
3270
+ const columnOf = table.rows.map((row) => {
3271
+ const at = [];
3272
+ let col = row.properties.gridBefore ?? 0;
3273
+ for (const cell of row.cells) {
3274
+ at.push(col);
3275
+ col += Math.max(1, cell.properties.colSpan ?? 1);
3276
+ }
3277
+ return at;
3278
+ });
3279
+ let changed = false;
3280
+ const rows = table.rows.map((row, r) => {
3281
+ const cells = row.cells.map((cell, c) => {
3282
+ const role = roles[r]?.[c] ?? "standalone";
3283
+ if (role !== "middle" && role !== "end") return cell;
3284
+ if (cell.properties.shading?.colorHex) return cell;
3285
+ const col = columnOf[r]?.[c];
3286
+ if (col === void 0) return cell;
3287
+ for (let up = r - 1; up >= 0; up--) {
3288
+ const idx = columnOf[up]?.indexOf(col) ?? -1;
3289
+ if (idx < 0) break;
3290
+ const above = table.rows[up]?.cells[idx];
3291
+ const aboveRole = roles[up]?.[idx] ?? "standalone";
3292
+ if (!above) break;
3293
+ if (aboveRole === "start") {
3294
+ const shading = above.properties.shading;
3295
+ if (!shading?.colorHex) break;
3296
+ changed = true;
3297
+ return {
3298
+ ...cell,
3299
+ properties: {
3300
+ ...cell.properties,
3301
+ shading
3302
+ }
3303
+ };
3304
+ }
3305
+ if (aboveRole !== "middle") break;
3306
+ }
3307
+ return cell;
3308
+ });
3309
+ return {
3310
+ ...row,
3311
+ cells
3312
+ };
3313
+ });
3314
+ return changed ? {
3315
+ ...table,
3316
+ rows
3317
+ } : table;
3318
+ }
3214
3319
  function layoutTableBlock(table, options, fontResources, imageResources, contentWidth) {
3215
3320
  const columnWidthsPt = computeColumnWidths(table, options, fontResources, contentWidth);
3216
3321
  const mergeRoles = table.rows.map((r) => r.cells.map((c) => c.properties.merge ?? "standalone"));
3322
+ table = withMergedShading(table, mergeRoles);
3217
3323
  const rows = [];
3218
3324
  const colCount = columnWidthsPt.length;
3219
3325
  let aboveBordersByCol = [];
@@ -3792,12 +3898,12 @@ var PageAssembler = class PageAssembler {
3792
3898
  * `undefined` when there are none.
3793
3899
  * @param bookmarkPositions Out-param: bookmark name → its GoTo destination.
3794
3900
  */
3795
- constructor(sectionCtxs, builder, notes, bookmarkPositions, backgroundImageName) {
3901
+ constructor(sectionCtxs, builder, notes, bookmarkPositions, backgroundImage) {
3796
3902
  this.sectionCtxs = sectionCtxs;
3797
3903
  this.builder = builder;
3798
3904
  this.notes = notes;
3799
3905
  this.bookmarkPositions = bookmarkPositions;
3800
- this.backgroundImageName = backgroundImageName;
3906
+ this.backgroundImage = backgroundImage;
3801
3907
  this.ctx = sectionCtxs[0];
3802
3908
  this.pageStartCtx = this.ctx;
3803
3909
  this.pageNumber = this.ctx.properties.pageNumberStart ?? 1;
@@ -4305,19 +4411,49 @@ var PageAssembler = class PageAssembler {
4305
4411
  *
4306
4412
  * @returns The background items for this page (at most one).
4307
4413
  */
4414
+ /**
4415
+ * §14.1.2.5 `@type="tile"` — the picture repeated over a box at its own size,
4416
+ * as image items. A tile is a texture, so its natural size is what makes it
4417
+ * one: stretched over the page it is a blur, and its 128 pixels are 96 points
4418
+ * at the resolution Office measures a picture in.
4419
+ */
4420
+ static tileItems = (resourceName, natural, box) => {
4421
+ const tw = Math.max(1, natural.widthPt);
4422
+ const th = Math.max(1, natural.heightPt);
4423
+ const cols = Math.min(200, Math.ceil(box.width / tw));
4424
+ const rows = Math.min(200, Math.ceil(box.height / th));
4425
+ const out = [];
4426
+ for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) out.push({
4427
+ type: "image",
4428
+ x: pt(box.x + c * tw),
4429
+ y: pt(box.y + r * th),
4430
+ width: pt(tw),
4431
+ height: pt(th),
4432
+ imageResourceName: resourceName
4433
+ });
4434
+ return out;
4435
+ };
4308
4436
  pageBackgroundItems = () => {
4309
4437
  const options = this.ctx.options;
4310
4438
  const w = this.ctx.pageWidth;
4311
4439
  const h = this.ctx.pageHeight;
4312
4440
  const fill = options?.pageBackgroundFill;
4313
- if (fill?.kind === "picture" && this.backgroundImageName !== void 0) return [{
4314
- type: "image",
4315
- x: pt(0),
4316
- y: pt(0),
4317
- width: pt(w),
4318
- height: pt(h),
4319
- imageResourceName: this.backgroundImageName
4320
- }];
4441
+ if (fill?.kind === "picture" && this.backgroundImage !== void 0) {
4442
+ const bg = this.backgroundImage;
4443
+ return fill.tiled ? PageAssembler.tileItems(bg.name, bg, {
4444
+ x: 0,
4445
+ y: 0,
4446
+ width: w,
4447
+ height: h
4448
+ }) : [{
4449
+ type: "image",
4450
+ x: pt(0),
4451
+ y: pt(0),
4452
+ width: pt(w),
4453
+ height: pt(h),
4454
+ imageResourceName: bg.name
4455
+ }];
4456
+ }
4321
4457
  if (fill?.kind === "gradient" && fill.gradient) return [{
4322
4458
  type: "shape",
4323
4459
  shape: {
@@ -4401,9 +4537,9 @@ var PageAssembler = class PageAssembler {
4401
4537
  this.balanceHeightPt = 0;
4402
4538
  };
4403
4539
  };
4404
- function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", notes, bookmarkPositions, reflowParagraph, backgroundImageName) {
4540
+ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", notes, bookmarkPositions, reflowParagraph, backgroundImage) {
4405
4541
  if (sectionCtxs.length === 0) return [];
4406
- const asm = new PageAssembler(sectionCtxs, builder, notes, bookmarkPositions, backgroundImageName);
4542
+ const asm = new PageAssembler(sectionCtxs, builder, notes, bookmarkPositions, backgroundImage);
4407
4543
  const balanceIfEndsContinuous = (fromBlock) => {
4408
4544
  const here = asm.ctx;
4409
4545
  const next = sectionCtxs[asm.secIdx + 1];
@@ -1,5 +1,5 @@
1
1
  import { PdfHexString, PdfName, PdfStream } from "../pdf/objects.js";
2
- import { aesCbcDecrypt, aesCbcEncrypt, md5, rc4, sha256, sha384, sha512 } from "./crypto.js";
2
+ import { aesCbcDecrypt, aesCbcEncrypt, md5, rc4, sha256, sha384, sha512 } from "../core/crypto/primitives.js";
3
3
  //#region src/pdf-reader/decrypt.ts
4
4
  var PAD = Uint8Array.from([
5
5
  40,
@@ -838,11 +838,13 @@ function vmlFill(shape, shapeType, resolveImage) {
838
838
  const fillEl = poChildren(shape).find((c) => poIs(c, "v:fill"));
839
839
  if (fillEl && (poAttr(fillEl, "type") === "none" || poAttr(fillEl, "on") === "f")) return { kind: "none" };
840
840
  const base = vmlColor(poAttr(shape, "fillcolor")) ?? (fillEl ? vmlColor(poAttr(fillEl, "color")) : void 0) ?? vmlColor(shapeType ? poAttr(shapeType, "fillcolor") : void 0) ?? "FFFFFF";
841
- const fillRelId = fillEl && poAttr(fillEl, "type") === "frame" ? poAttr(fillEl, "r:id") : void 0;
841
+ const fillType = fillEl ? poAttr(fillEl, "type") : void 0;
842
+ const fillRelId = fillType === "frame" || fillType === "tile" ? poAttr(fillEl, "r:id") : void 0;
842
843
  const picture = fillRelId !== void 0 ? resolveImage?.(fillRelId) : void 0;
843
844
  if (picture !== void 0) return {
844
845
  kind: "picture",
845
- imageResource: picture
846
+ imageResource: picture,
847
+ ...fillType === "tile" ? { tiled: true } : {}
846
848
  };
847
849
  const gradient = fillEl ? vmlGradient(fillEl, base) : void 0;
848
850
  if (gradient) return {
@@ -1802,6 +1804,7 @@ function fillFromNode(child, resolveColor, resolveImage) {
1802
1804
  return {
1803
1805
  kind: "picture",
1804
1806
  imageResource: resource,
1807
+ ...poChildren(child).some((c) => poIs(c, "a:tile")) ? { tiled: true } : {},
1805
1808
  ...crop ? { imageCrop: crop } : {}
1806
1809
  };
1807
1810
  }
@@ -131,8 +131,8 @@ function parseParagraphProperties(pPr, autoSpacingPt = 0) {
131
131
  }
132
132
  if ("w:ind" in el) {
133
133
  const node = el["w:ind"];
134
- const left = parseIntAttr(node, "left");
135
- const right = parseIntAttr(node, "right");
134
+ const left = parseIntAttr(node, "left") ?? parseIntAttr(node, "start");
135
+ const right = parseIntAttr(node, "right") ?? parseIntAttr(node, "end");
136
136
  const firstLine = parseIntAttr(node, "firstLine");
137
137
  const hanging = parseIntAttr(node, "hanging");
138
138
  if (left !== void 0) out.indentLeft = twipsToPt(left);
@@ -1,5 +1,5 @@
1
1
  import { halfPtToPt, twipsToPt } from "../core/ir/units.js";
2
- import { asElement, getAttr, getVal, parseIntAttr, parseToggle } from "./xml-helpers.js";
2
+ import { asElement, getAttr, getVal, parseHalfPointAttr, parseToggle } from "./xml-helpers.js";
3
3
  import { shadingFillHex } from "./shading.js";
4
4
  //#region src/word/run-properties.ts
5
5
  var UNDERLINE_STYLES = new Set([
@@ -81,7 +81,7 @@ function parseRunProperties(rPr) {
81
81
  if (c && /^[0-9A-Fa-f]{6}$/u.test(c)) out.underlineColorHex = c.toUpperCase();
82
82
  }
83
83
  if ("w:sz" in el) {
84
- const v = parseIntAttr(el["w:sz"], "val");
84
+ const v = parseHalfPointAttr(el["w:sz"], "val");
85
85
  if (v !== void 0) out.fontSizePt = halfPtToPt(v);
86
86
  }
87
87
  if ("w:color" in el) {
@@ -36,6 +36,18 @@ export declare function getVal(node: unknown): string | undefined;
36
36
  * @param name The local attribute name.
37
37
  * @returns The parsed number, or `undefined` when absent or not finite.
38
38
  */
39
+ /**
40
+ * §17.3.2.38 ST_HpsMeasure — a size in HALF-POINTS, or the same universal
41
+ * measure with a unit on it ("20pt"). The two units differ by a factor of ten
42
+ * from the twips a length is written in, so a size is read here rather than
43
+ * through {@link parseIntAttr}: tdf108408.docx writes `w:sz w:val="20pt"` and
44
+ * read as twips it set its sample text 200 points tall.
45
+ *
46
+ * @param node The element carrying the attribute.
47
+ * @param name The attribute name.
48
+ * @returns The size in half-points, or undefined.
49
+ */
50
+ export declare function parseHalfPointAttr(node: unknown, name: string): number | undefined;
39
51
  export declare function parseIntAttr(node: unknown, name: string): number | undefined;
40
52
  /**
41
53
  * Parse a toggle (boolean) property per ECMA-376 Part 1 §17.17.4:
@@ -1,3 +1,4 @@
1
+ import { universalMeasureTwips } from "../core/po-helpers.js";
1
2
  //#region src/word/xml-helpers.ts
2
3
  /**
3
4
  * Narrow an arbitrary parsed value to an {@link XmlElement}.
@@ -47,12 +48,31 @@ function getVal(node) {
47
48
  * @param name The local attribute name.
48
49
  * @returns The parsed number, or `undefined` when absent or not finite.
49
50
  */
51
+ /**
52
+ * §17.3.2.38 ST_HpsMeasure — a size in HALF-POINTS, or the same universal
53
+ * measure with a unit on it ("20pt"). The two units differ by a factor of ten
54
+ * from the twips a length is written in, so a size is read here rather than
55
+ * through {@link parseIntAttr}: tdf108408.docx writes `w:sz w:val="20pt"` and
56
+ * read as twips it set its sample text 200 points tall.
57
+ *
58
+ * @param node The element carrying the attribute.
59
+ * @param name The attribute name.
60
+ * @returns The size in half-points, or undefined.
61
+ */
62
+ function parseHalfPointAttr(node, name) {
63
+ const v = getAttr(node, name);
64
+ if (v === void 0) return void 0;
65
+ const n = Number(v);
66
+ if (Number.isFinite(n)) return n;
67
+ const twips = universalMeasureTwips(v);
68
+ return twips === void 0 ? void 0 : twips / 10;
69
+ }
50
70
  function parseIntAttr(node, name) {
51
71
  const v = getAttr(node, name);
52
72
  if (v === void 0) return void 0;
53
73
  const n = Number(v);
54
- if (!Number.isFinite(n)) return void 0;
55
- return n;
74
+ if (Number.isFinite(n)) return n;
75
+ return universalMeasureTwips(v);
56
76
  }
57
77
  /**
58
78
  * Parse a toggle (boolean) property per ECMA-376 Part 1 §17.17.4:
@@ -71,4 +91,4 @@ function parseToggle(node) {
71
91
  return !(v === "false" || v === "0" || v === "off");
72
92
  }
73
93
  //#endregion
74
- export { asArray, asElement, getAttr, getVal, parseIntAttr, parseToggle };
94
+ export { asArray, asElement, getAttr, getVal, parseHalfPointAttr, parseIntAttr, parseToggle };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reamkit",
3
- "version": "1.18.0",
3
+ "version": "1.20.0",
4
4
  "description": "Ream — convert DOCX, XLSX, PPTX and PDF to PDF, SVG, HTML, DOCX and XLSX, built from scratch on the ECMA-376 and ISO 32000 specifications. Parse once, convert anywhere.",
5
5
  "license": "MIT",
6
6
  "author": "Alex Krassavin <info@reamkit.dev>",