@cj-tech-master/excelts 9.6.0 → 9.6.1
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/dist/browser/modules/archive/io/random-access.d.ts +1 -1
- package/dist/browser/modules/excel/workbook.browser.d.ts +1 -1
- package/dist/browser/modules/excel/xlsx/xform/comment/comment-xform.d.ts +3 -0
- package/dist/browser/modules/excel/xlsx/xform/comment/comment-xform.js +30 -7
- package/dist/browser/modules/pdf/excel-bridge.d.ts +32 -0
- package/dist/browser/modules/pdf/excel-bridge.js +67 -1
- package/dist/browser/modules/pdf/word-bridge.d.ts +20 -15
- package/dist/browser/modules/pdf/word-bridge.js +49 -34
- package/dist/browser/modules/stream/common/consumers.d.ts +2 -1
- package/dist/browser/modules/word/advanced/diff.js +125 -13
- package/dist/browser/modules/word/advanced/drawing-shapes.js +3 -0
- package/dist/browser/modules/word/bridge/excel-bridge.js +21 -1
- package/dist/browser/modules/word/builder/document-handle.d.ts +2 -0
- package/dist/browser/modules/word/builder/document-handle.js +14 -2
- package/dist/browser/modules/word/builder/paragraph-builders.js +10 -1
- package/dist/browser/modules/word/builder/run-builders.d.ts +19 -2
- package/dist/browser/modules/word/builder/run-builders.js +2 -6
- package/dist/browser/modules/word/convert/odt/odt.js +6 -1
- package/dist/browser/modules/word/layout/layout-full.d.ts +12 -0
- package/dist/browser/modules/word/layout/layout-full.js +74 -9
- package/dist/browser/modules/word/layout/layout-model.d.ts +12 -0
- package/dist/browser/modules/word/query/merge.js +26 -10
- package/dist/browser/modules/word/query/split.js +68 -2
- package/dist/browser/modules/word/reader/docx-reader.js +23 -0
- package/dist/browser/modules/word/security/cfb-reader.d.ts +14 -3
- package/dist/browser/modules/word/security/cfb-reader.js +271 -153
- package/dist/browser/modules/word/security/document-protection.js +10 -4
- package/dist/browser/modules/word/security/encryption.js +194 -32
- package/dist/browser/modules/word/types.d.ts +17 -0
- package/dist/browser/modules/word/units.d.ts +10 -4
- package/dist/browser/modules/word/units.js +10 -4
- package/dist/browser/modules/word/writer/document-writer.js +28 -4
- package/dist/browser/modules/word/writer/docx-packager.js +45 -5
- package/dist/browser/modules/word/writer/image-writer.d.ts +1 -1
- package/dist/browser/modules/word/writer/image-writer.js +2 -2
- package/dist/browser/modules/word/writer/render-context.d.ts +15 -0
- package/dist/browser/modules/word/writer/run-writer.js +8 -4
- package/dist/browser/modules/word/writer/section-writer.js +46 -35
- package/dist/browser/modules/word/writer/streaming-writer.js +4 -0
- package/dist/browser/modules/word/writer/styles-writer.js +11 -0
- package/dist/browser/modules/word/writer/table-writer.js +6 -0
- package/dist/cjs/modules/excel/xlsx/xform/comment/comment-xform.js +30 -7
- package/dist/cjs/modules/pdf/excel-bridge.js +67 -0
- package/dist/cjs/modules/pdf/word-bridge.js +49 -34
- package/dist/cjs/modules/word/advanced/diff.js +125 -13
- package/dist/cjs/modules/word/advanced/drawing-shapes.js +3 -0
- package/dist/cjs/modules/word/bridge/excel-bridge.js +21 -1
- package/dist/cjs/modules/word/builder/document-handle.js +14 -2
- package/dist/cjs/modules/word/builder/paragraph-builders.js +10 -1
- package/dist/cjs/modules/word/builder/run-builders.js +2 -6
- package/dist/cjs/modules/word/convert/odt/odt.js +6 -1
- package/dist/cjs/modules/word/layout/layout-full.js +74 -9
- package/dist/cjs/modules/word/query/merge.js +26 -10
- package/dist/cjs/modules/word/query/split.js +68 -2
- package/dist/cjs/modules/word/reader/docx-reader.js +23 -0
- package/dist/cjs/modules/word/security/cfb-reader.js +271 -153
- package/dist/cjs/modules/word/security/document-protection.js +10 -4
- package/dist/cjs/modules/word/security/encryption.js +193 -31
- package/dist/cjs/modules/word/units.js +10 -4
- package/dist/cjs/modules/word/writer/document-writer.js +28 -4
- package/dist/cjs/modules/word/writer/docx-packager.js +45 -5
- package/dist/cjs/modules/word/writer/image-writer.js +2 -2
- package/dist/cjs/modules/word/writer/run-writer.js +8 -4
- package/dist/cjs/modules/word/writer/section-writer.js +46 -35
- package/dist/cjs/modules/word/writer/streaming-writer.js +4 -0
- package/dist/cjs/modules/word/writer/styles-writer.js +11 -0
- package/dist/cjs/modules/word/writer/table-writer.js +6 -0
- package/dist/esm/modules/excel/xlsx/xform/comment/comment-xform.js +30 -7
- package/dist/esm/modules/pdf/excel-bridge.js +67 -1
- package/dist/esm/modules/pdf/word-bridge.js +49 -34
- package/dist/esm/modules/word/advanced/diff.js +125 -13
- package/dist/esm/modules/word/advanced/drawing-shapes.js +3 -0
- package/dist/esm/modules/word/bridge/excel-bridge.js +21 -1
- package/dist/esm/modules/word/builder/document-handle.js +14 -2
- package/dist/esm/modules/word/builder/paragraph-builders.js +10 -1
- package/dist/esm/modules/word/builder/run-builders.js +2 -6
- package/dist/esm/modules/word/convert/odt/odt.js +6 -1
- package/dist/esm/modules/word/layout/layout-full.js +74 -9
- package/dist/esm/modules/word/query/merge.js +26 -10
- package/dist/esm/modules/word/query/split.js +68 -2
- package/dist/esm/modules/word/reader/docx-reader.js +23 -0
- package/dist/esm/modules/word/security/cfb-reader.js +271 -153
- package/dist/esm/modules/word/security/document-protection.js +10 -4
- package/dist/esm/modules/word/security/encryption.js +194 -32
- package/dist/esm/modules/word/units.js +10 -4
- package/dist/esm/modules/word/writer/document-writer.js +28 -4
- package/dist/esm/modules/word/writer/docx-packager.js +45 -5
- package/dist/esm/modules/word/writer/image-writer.js +2 -2
- package/dist/esm/modules/word/writer/run-writer.js +8 -4
- package/dist/esm/modules/word/writer/section-writer.js +46 -35
- package/dist/esm/modules/word/writer/streaming-writer.js +4 -0
- package/dist/esm/modules/word/writer/styles-writer.js +11 -0
- package/dist/esm/modules/word/writer/table-writer.js +6 -0
- package/dist/iife/excelts.iife.js +20 -8
- package/dist/iife/excelts.iife.js.map +1 -1
- package/dist/iife/excelts.iife.min.js +2 -2
- package/dist/types/modules/archive/io/random-access.d.ts +1 -1
- package/dist/types/modules/excel/workbook.browser.d.ts +1 -1
- package/dist/types/modules/excel/xlsx/xform/comment/comment-xform.d.ts +3 -0
- package/dist/types/modules/pdf/excel-bridge.d.ts +32 -0
- package/dist/types/modules/pdf/word-bridge.d.ts +20 -15
- package/dist/types/modules/stream/common/consumers.d.ts +2 -1
- package/dist/types/modules/word/builder/document-handle.d.ts +2 -0
- package/dist/types/modules/word/builder/run-builders.d.ts +19 -2
- package/dist/types/modules/word/layout/layout-full.d.ts +12 -0
- package/dist/types/modules/word/layout/layout-model.d.ts +12 -0
- package/dist/types/modules/word/security/cfb-reader.d.ts +14 -3
- package/dist/types/modules/word/types.d.ts +17 -0
- package/dist/types/modules/word/units.d.ts +10 -4
- package/dist/types/modules/word/writer/image-writer.d.ts +1 -1
- package/dist/types/modules/word/writer/render-context.d.ts +15 -0
- package/package.json +2 -2
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* - MS-OFFCRYPTO: Office Document Cryptography Structure
|
|
21
21
|
* - ECMA-376 Part 3: Markup Compatibility and Extensibility
|
|
22
22
|
*/
|
|
23
|
-
import {
|
|
23
|
+
import { aesCbcDecryptRaw as aesCbcDecryptRawSync, aesCbcEncryptRaw as aesCbcEncryptRawSync, hash as hashSyncMaybe, hashAsync } from "../../../utils/crypto.browser.js";
|
|
24
24
|
import { base64ToBytes, bytesToBase64, randomBytes, utf8Decoder, utf8Encoder } from "../core/internal-utils.js";
|
|
25
25
|
import { DocxDecryptionError } from "../errors.js";
|
|
26
26
|
import { readCfb, writeCfb } from "./cfb-reader.js";
|
|
@@ -139,15 +139,19 @@ export async function verifyPassword(password, info) {
|
|
|
139
139
|
try {
|
|
140
140
|
// Derive verifier hash input key
|
|
141
141
|
const verifierInputKey = await deriveEncryptionKey(password, info, AGILE_BLOCK_KEYS.verifierHashInput);
|
|
142
|
-
// Decrypt the verifier hash input
|
|
143
|
-
|
|
144
|
-
//
|
|
142
|
+
// Decrypt the verifier hash input. MS-OFFCRYPTO §2.3.4.13 specifies
|
|
143
|
+
// these blobs are AES-CBC with the plaintext zero-padded to the block
|
|
144
|
+
// boundary — NOT PKCS#7. Using a PKCS#7 decrypt would mis-strip bytes
|
|
145
|
+
// and break interop with Word / msoffcrypto.
|
|
146
|
+
const verifierInput = aesCbcRawDecrypt(info.encryptedVerifierHashInput, verifierInputKey, info.keySalt);
|
|
147
|
+
// Hash the verifier input. The plaintext is exactly 16 bytes (saltSize),
|
|
148
|
+
// so trim any zero padding before hashing.
|
|
145
149
|
const hashAlg = mapHashName(info.hashAlgorithm);
|
|
146
|
-
const computedHash = await hashAsync(hashAlg, verifierInput);
|
|
150
|
+
const computedHash = await hashAsync(hashAlg, verifierInput.slice(0, info.blockSize));
|
|
147
151
|
// Derive verifier hash value key
|
|
148
152
|
const verifierValueKey = await deriveEncryptionKey(password, info, AGILE_BLOCK_KEYS.verifierHashValue);
|
|
149
|
-
// Decrypt the verifier hash value (
|
|
150
|
-
const expectedHash =
|
|
153
|
+
// Decrypt the verifier hash value (zero-padded AES-CBC, see above).
|
|
154
|
+
const expectedHash = aesCbcRawDecrypt(info.encryptedVerifierHashValue, verifierValueKey, info.keySalt);
|
|
151
155
|
// Compare (truncate to hashSize in case of padding)
|
|
152
156
|
return bytesEqual(computedHash.slice(0, info.hashSize), expectedHash.slice(0, info.hashSize));
|
|
153
157
|
}
|
|
@@ -170,8 +174,9 @@ export async function verifyPassword(password, info) {
|
|
|
170
174
|
export async function decryptPackage(encryptedPackage, info, password, maxDecryptedSize = 512 * 1024 * 1024) {
|
|
171
175
|
// Derive key encryption key
|
|
172
176
|
const keyEncryptionKey = await deriveEncryptionKey(password, info, AGILE_BLOCK_KEYS.encryptedKey);
|
|
173
|
-
// Decrypt the actual package key
|
|
174
|
-
|
|
177
|
+
// Decrypt the actual package key. Zero-padded AES-CBC per MS-OFFCRYPTO
|
|
178
|
+
// §2.3.4.13 (NOT PKCS#7); the key is exactly keyBits/8 bytes.
|
|
179
|
+
const packageKey = aesCbcRawDecrypt(info.encryptedKeyValue, keyEncryptionKey, info.keySalt).slice(0, info.keyBits / 8);
|
|
175
180
|
if (encryptedPackage.length < 8) {
|
|
176
181
|
throw new DocxDecryptionError("EncryptedPackage too small (missing 8-byte size header)");
|
|
177
182
|
}
|
|
@@ -255,25 +260,18 @@ function getHashSizeFor(hashName) {
|
|
|
255
260
|
}
|
|
256
261
|
}
|
|
257
262
|
/**
|
|
258
|
-
* Decrypt an AES-CBC blob
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
* bytes per the OOXML spec.
|
|
263
|
+
* Decrypt an AES-CBC blob written with zero padding (no PKCS#7). Used for
|
|
264
|
+
* the encryptedKeyValue / encryptedVerifierHashInput / encryptedVerifierHashValue
|
|
265
|
+
* blobs, per MS-OFFCRYPTO §2.3.4.13. The IV is truncated/extended to 16 bytes.
|
|
262
266
|
*/
|
|
263
|
-
function
|
|
264
|
-
return
|
|
265
|
-
}
|
|
266
|
-
/**
|
|
267
|
-
* Encrypt with AES-CBC + PKCS#7 padding — the Agile spec's standard
|
|
268
|
-
* choice for verifier blobs and the encrypted package key.
|
|
269
|
-
*/
|
|
270
|
-
function aesCbcPkcs7Encrypt(data, key, iv) {
|
|
271
|
-
return aesCbcEncryptPkcs7Sync(data, key, ivToBlockSize(iv));
|
|
267
|
+
function aesCbcRawDecrypt(data, key, iv) {
|
|
268
|
+
return aesCbcDecryptRawSync(data, key, ivToBlockSize(iv));
|
|
272
269
|
}
|
|
273
270
|
/**
|
|
274
271
|
* Encrypt with AES-CBC and zero-padding (no PKCS#7). Used by package
|
|
275
|
-
* segment encryption: data is already padded
|
|
276
|
-
* the caller, and the on-disk format does not
|
|
272
|
+
* segment encryption and the verifier / key blobs: data is already padded
|
|
273
|
+
* to a 16-byte boundary by the caller, and the on-disk format does not
|
|
274
|
+
* include a PKCS#7 trailer.
|
|
277
275
|
*/
|
|
278
276
|
function aesCbcZeroPadEncrypt(data, key, iv) {
|
|
279
277
|
return aesCbcEncryptRawSync(data, key, ivToBlockSize(iv));
|
|
@@ -287,12 +285,51 @@ function ivToBlockSize(iv) {
|
|
|
287
285
|
out.set(iv.slice(0, 16));
|
|
288
286
|
return out;
|
|
289
287
|
}
|
|
288
|
+
/** Right-pad data with zeros so its length is a multiple of `blockSize`. */
|
|
289
|
+
function padToBlock(data, blockSize) {
|
|
290
|
+
if (data.length % blockSize === 0) {
|
|
291
|
+
return data;
|
|
292
|
+
}
|
|
293
|
+
const out = new Uint8Array(Math.ceil(data.length / blockSize) * blockSize);
|
|
294
|
+
out.set(data);
|
|
295
|
+
return out;
|
|
296
|
+
}
|
|
290
297
|
function concat(a, b) {
|
|
291
298
|
const result = new Uint8Array(a.length + b.length);
|
|
292
299
|
result.set(a, 0);
|
|
293
300
|
result.set(b, a.length);
|
|
294
301
|
return result;
|
|
295
302
|
}
|
|
303
|
+
/** HMAC block size (in bytes) for the supported hash algorithms. */
|
|
304
|
+
function hmacBlockSize(hashName) {
|
|
305
|
+
// SHA-1 / SHA-256 use a 512-bit (64-byte) block; SHA-384 / SHA-512 use a
|
|
306
|
+
// 1024-bit (128-byte) block.
|
|
307
|
+
return hashName === "SHA-384" || hashName === "SHA-512" ? 128 : 64;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Compute HMAC(hashName, key, message) using the generic hash primitive.
|
|
311
|
+
* Implemented here (rather than in @utils/crypto, which only ships
|
|
312
|
+
* hmacSha256) so agile encryption can use SHA-512 etc. for the data
|
|
313
|
+
* integrity HMAC that Word verifies on open.
|
|
314
|
+
*/
|
|
315
|
+
async function hmac(hashName, key, message) {
|
|
316
|
+
const blockSize = hmacBlockSize(hashName);
|
|
317
|
+
// Keys longer than the block size are hashed down first.
|
|
318
|
+
let k = key.length > blockSize ? await hashAsync(hashName, key) : key;
|
|
319
|
+
if (k.length < blockSize) {
|
|
320
|
+
const padded = new Uint8Array(blockSize);
|
|
321
|
+
padded.set(k);
|
|
322
|
+
k = padded;
|
|
323
|
+
}
|
|
324
|
+
const ipad = new Uint8Array(blockSize);
|
|
325
|
+
const opad = new Uint8Array(blockSize);
|
|
326
|
+
for (let i = 0; i < blockSize; i++) {
|
|
327
|
+
ipad[i] = k[i] ^ 0x36;
|
|
328
|
+
opad[i] = k[i] ^ 0x5c;
|
|
329
|
+
}
|
|
330
|
+
const inner = await hashAsync(hashName, concat(ipad, message));
|
|
331
|
+
return hashAsync(hashName, concat(opad, inner));
|
|
332
|
+
}
|
|
296
333
|
function stringToUtf16LE(s) {
|
|
297
334
|
const buf = new Uint8Array(s.length * 2);
|
|
298
335
|
for (let i = 0; i < s.length; i++) {
|
|
@@ -592,17 +629,33 @@ export async function encryptDocx(zipBytes, password, options) {
|
|
|
592
629
|
const packageKey = randomBytes(keyBytes);
|
|
593
630
|
// 2. Generate key encryption key (for encrypting the package key)
|
|
594
631
|
const keyEncryptionKey = await deriveEncryptionKey(password, { keySalt, spinCount, hashAlgorithm, keyBits }, AGILE_BLOCK_KEYS.encryptedKey);
|
|
595
|
-
// 3. Encrypt the package key
|
|
596
|
-
|
|
632
|
+
// 3. Encrypt the package key. MS-OFFCRYPTO §2.3.4.13: these blobs use
|
|
633
|
+
// zero-padded AES-CBC (no PKCS#7). packageKey is already block-aligned.
|
|
634
|
+
const encryptedKeyValue = aesCbcZeroPadEncrypt(padToBlock(packageKey, blockSize), keyEncryptionKey, keySalt);
|
|
597
635
|
// 4. Generate verifier: random 16 bytes, hash them, encrypt both
|
|
598
636
|
const verifierInput = randomBytes(16);
|
|
599
637
|
const verifierHash = await hashAsync(hashName, verifierInput);
|
|
600
638
|
const verifierInputKey = await deriveEncryptionKey(password, { keySalt, spinCount, hashAlgorithm, keyBits }, AGILE_BLOCK_KEYS.verifierHashInput);
|
|
601
|
-
const encryptedVerifierHashInput =
|
|
639
|
+
const encryptedVerifierHashInput = aesCbcZeroPadEncrypt(padToBlock(verifierInput, blockSize), verifierInputKey, keySalt);
|
|
602
640
|
const verifierValueKey = await deriveEncryptionKey(password, { keySalt, spinCount, hashAlgorithm, keyBits }, AGILE_BLOCK_KEYS.verifierHashValue);
|
|
603
|
-
const encryptedVerifierHashValue =
|
|
641
|
+
const encryptedVerifierHashValue = aesCbcZeroPadEncrypt(padToBlock(verifierHash, blockSize), verifierValueKey, keySalt);
|
|
604
642
|
// 5. Encrypt the ZIP data in 4096-byte segments
|
|
605
643
|
const encryptedPackage = await encryptPackageData(zipBytes, packageKey, keySalt, hashName, blockSize);
|
|
644
|
+
// 5b. Data integrity (MS-OFFCRYPTO §2.3.4.14). Word verifies this HMAC on
|
|
645
|
+
// open; an empty or missing value makes it report the file as corrupt.
|
|
646
|
+
// - hmacKey: random, hashSize bytes (padded to block size).
|
|
647
|
+
// - encryptedHmacKey = AES-CBC(packageKey, IV0, hmacKey)
|
|
648
|
+
// - hmacValue = HMAC(hashAlg, hmacKey, encryptedPackage) (entire
|
|
649
|
+
// stream including the 8-byte size prefix)
|
|
650
|
+
// - encryptedHmacValue = AES-CBC(packageKey, IV1, hmacValue)
|
|
651
|
+
// IV0 = H(keySalt + blockKeyDataIntegrityKey)[:blockSize]
|
|
652
|
+
// IV1 = H(keySalt + blockKeyDataIntegrityValue)[:blockSize]
|
|
653
|
+
const hmacKey = randomBytes(hashSize);
|
|
654
|
+
const ivHmacKey = (await hashAsync(hashName, concat(keySalt, AGILE_BLOCK_KEYS.dataIntegrityKey))).slice(0, blockSize);
|
|
655
|
+
const ivHmacValue = (await hashAsync(hashName, concat(keySalt, AGILE_BLOCK_KEYS.dataIntegrityValue))).slice(0, blockSize);
|
|
656
|
+
const encryptedHmacKey = aesCbcZeroPadEncrypt(padToBlock(hmacKey, blockSize), packageKey, ivHmacKey);
|
|
657
|
+
const hmacValue = await hmac(hashName, hmacKey, encryptedPackage);
|
|
658
|
+
const encryptedHmacValue = aesCbcZeroPadEncrypt(padToBlock(hmacValue, blockSize), packageKey, ivHmacValue);
|
|
606
659
|
// 6. Generate EncryptionInfo XML
|
|
607
660
|
const encInfoXml = buildEncryptionInfoXml({
|
|
608
661
|
keyBits,
|
|
@@ -613,7 +666,9 @@ export async function encryptDocx(zipBytes, password, options) {
|
|
|
613
666
|
keySalt,
|
|
614
667
|
encryptedVerifierHashInput,
|
|
615
668
|
encryptedVerifierHashValue,
|
|
616
|
-
encryptedKeyValue
|
|
669
|
+
encryptedKeyValue,
|
|
670
|
+
encryptedHmacKey,
|
|
671
|
+
encryptedHmacValue
|
|
617
672
|
});
|
|
618
673
|
// Prepend 8-byte version header: version 4.4 + flags 0x40
|
|
619
674
|
const xmlBytes = utf8Encoder.encode(encInfoXml);
|
|
@@ -623,8 +678,12 @@ export async function encryptDocx(zipBytes, password, options) {
|
|
|
623
678
|
encInfoView.setUint16(2, 4, true); // version minor
|
|
624
679
|
encInfoView.setUint32(4, 0x40, true); // flags (agile)
|
|
625
680
|
encInfoStream.set(xmlBytes, 8);
|
|
626
|
-
// 7. Package into CFB
|
|
681
|
+
// 7. Package into CFB.
|
|
682
|
+
// Office requires the \x06DataSpaces structure (MS-OFFCRYPTO §2.3.2)
|
|
683
|
+
// in addition to EncryptionInfo + EncryptedPackage; without it Word
|
|
684
|
+
// rejects the file as corrupt even when the password is correct.
|
|
627
685
|
const cfbBytes = writeCfb([
|
|
686
|
+
...buildDataSpacesStreams(),
|
|
628
687
|
{ name: "EncryptionInfo", data: encInfoStream },
|
|
629
688
|
{ name: "EncryptedPackage", data: encryptedPackage }
|
|
630
689
|
]);
|
|
@@ -633,6 +692,107 @@ export async function encryptDocx(zipBytes, password, options) {
|
|
|
633
692
|
// =============================================================================
|
|
634
693
|
// Encrypt Helpers
|
|
635
694
|
// =============================================================================
|
|
695
|
+
// -----------------------------------------------------------------------------
|
|
696
|
+
// \x06DataSpaces structure (MS-OFFCRYPTO §2.3.2)
|
|
697
|
+
//
|
|
698
|
+
// Office encrypted documents wrap the EncryptedPackage in a DataSpaces map so
|
|
699
|
+
// the consumer knows which transform (StrongEncryption) was applied. The four
|
|
700
|
+
// streams below are byte-for-byte what Office writes for password-based agile
|
|
701
|
+
// encryption. Word validates this structure on open; omitting it makes the
|
|
702
|
+
// file "corrupt" even with the correct password.
|
|
703
|
+
// -----------------------------------------------------------------------------
|
|
704
|
+
/** Encode a UTF-8 length-prefixed unicode string (UNICODE-LP-P4):
|
|
705
|
+
* [4-byte LE byte length of UTF-16LE payload][UTF-16LE chars][pad to 4-byte boundary]. */
|
|
706
|
+
function lengthPrefixedUtf16(str) {
|
|
707
|
+
const chars = stringToUtf16LE(str);
|
|
708
|
+
const padded = Math.ceil(chars.length / 4) * 4;
|
|
709
|
+
const out = new Uint8Array(4 + padded);
|
|
710
|
+
new DataView(out.buffer).setUint32(0, chars.length, true);
|
|
711
|
+
out.set(chars, 4);
|
|
712
|
+
return out;
|
|
713
|
+
}
|
|
714
|
+
/** Concatenate several byte arrays. */
|
|
715
|
+
function concatAll(...parts) {
|
|
716
|
+
const total = parts.reduce((s, p) => s + p.length, 0);
|
|
717
|
+
const out = new Uint8Array(total);
|
|
718
|
+
let off = 0;
|
|
719
|
+
for (const p of parts) {
|
|
720
|
+
out.set(p, off);
|
|
721
|
+
off += p.length;
|
|
722
|
+
}
|
|
723
|
+
return out;
|
|
724
|
+
}
|
|
725
|
+
/** Build the four \x06DataSpaces streams Office requires for agile encryption. */
|
|
726
|
+
function buildDataSpacesStreams() {
|
|
727
|
+
const DATASPACES = "\u0006DataSpaces";
|
|
728
|
+
// --- Version stream (DataSpaceVersionInfo) ---
|
|
729
|
+
// FeatureIdentifier "Microsoft.Container.DataSpaces" + reader/updater/writer
|
|
730
|
+
// version (each major=1, minor=0).
|
|
731
|
+
const versionStream = concatAll(lengthPrefixedUtf16("Microsoft.Container.DataSpaces"), u16le(1), u16le(0), // reader version
|
|
732
|
+
u16le(1), u16le(0), // updater version
|
|
733
|
+
u16le(1), u16le(0) // writer version
|
|
734
|
+
);
|
|
735
|
+
// --- DataSpaceMap stream ---
|
|
736
|
+
// Header: HeaderLength(8) + EntryCount(1) followed by one MapEntry.
|
|
737
|
+
// MapEntry: EntryLength + ReferenceComponentCount(1) +
|
|
738
|
+
// [ReferenceComponent: type(0=stream) + LP name "EncryptedPackage"] +
|
|
739
|
+
// LP DataSpaceName "StrongEncryptionDataSpace".
|
|
740
|
+
const refName = lengthPrefixedUtf16("EncryptedPackage");
|
|
741
|
+
const dsName = lengthPrefixedUtf16("StrongEncryptionDataSpace");
|
|
742
|
+
const refComponent = concatAll(u32le(0), refName); // 0 = stream component
|
|
743
|
+
const mapEntryBody = concatAll(u32le(1), refComponent, dsName); // 1 reference component
|
|
744
|
+
const entryLength = 4 + mapEntryBody.length; // include the EntryLength field itself
|
|
745
|
+
const mapEntry = concatAll(u32le(entryLength), mapEntryBody);
|
|
746
|
+
const dataSpaceMap = concatAll(u32le(8), u32le(1), mapEntry); // headerLen=8, entryCount=1
|
|
747
|
+
// --- DataSpaceInfo/StrongEncryptionDataSpace stream (DataSpaceDefinition) ---
|
|
748
|
+
// HeaderLength(8) + TransformReferenceCount(1) + LP transform name.
|
|
749
|
+
const transformName = lengthPrefixedUtf16("StrongEncryptionTransform");
|
|
750
|
+
const dataSpaceDefinition = concatAll(u32le(8), u32le(1), transformName);
|
|
751
|
+
// --- TransformInfo/StrongEncryptionTransform/\x06Primary stream ---
|
|
752
|
+
// TransformInfoHeader:
|
|
753
|
+
// TransformLength + TransformType(1) + LP TransformId +
|
|
754
|
+
// LP TransformName + reader/updater/writer versions.
|
|
755
|
+
// Followed by EncryptionTransformInfo:
|
|
756
|
+
// LP EncryptionName + EncryptionBlockSize(4) + CipherMode(4).
|
|
757
|
+
const transformId = lengthPrefixedUtf16("{FF9A3F03-56EF-4613-BDD5-5A41C1D07246}");
|
|
758
|
+
const transformNamePrimary = lengthPrefixedUtf16("Microsoft.Container.EncryptionTransform");
|
|
759
|
+
const headerBody = concatAll(u32le(1), // TransformType = 1
|
|
760
|
+
transformId, transformNamePrimary, u16le(1), u16le(0), // reader version
|
|
761
|
+
u16le(1), u16le(0), // updater version
|
|
762
|
+
u16le(1), u16le(0) // writer version
|
|
763
|
+
);
|
|
764
|
+
const transformLength = 4 + headerBody.length; // include the length field itself
|
|
765
|
+
const transformHeader = concatAll(u32le(transformLength), headerBody);
|
|
766
|
+
const encryptionTransformInfo = concatAll(lengthPrefixedUtf16(""), // EncryptionName (empty for agile)
|
|
767
|
+
u32le(0), // EncryptionBlockSize
|
|
768
|
+
u32le(0) // CipherMode
|
|
769
|
+
);
|
|
770
|
+
const primary = concatAll(transformHeader, encryptionTransformInfo);
|
|
771
|
+
return [
|
|
772
|
+
{ name: "Version", path: [DATASPACES], data: versionStream },
|
|
773
|
+
{ name: "DataSpaceMap", path: [DATASPACES], data: dataSpaceMap },
|
|
774
|
+
{
|
|
775
|
+
name: "StrongEncryptionDataSpace",
|
|
776
|
+
path: [DATASPACES, "DataSpaceInfo"],
|
|
777
|
+
data: dataSpaceDefinition
|
|
778
|
+
},
|
|
779
|
+
{
|
|
780
|
+
name: "\u0006Primary",
|
|
781
|
+
path: [DATASPACES, "TransformInfo", "StrongEncryptionTransform"],
|
|
782
|
+
data: primary
|
|
783
|
+
}
|
|
784
|
+
];
|
|
785
|
+
}
|
|
786
|
+
function u16le(n) {
|
|
787
|
+
const b = new Uint8Array(2);
|
|
788
|
+
new DataView(b.buffer).setUint16(0, n, true);
|
|
789
|
+
return b;
|
|
790
|
+
}
|
|
791
|
+
function u32le(n) {
|
|
792
|
+
const b = new Uint8Array(4);
|
|
793
|
+
new DataView(b.buffer).setUint32(0, n, true);
|
|
794
|
+
return b;
|
|
795
|
+
}
|
|
636
796
|
/**
|
|
637
797
|
* Encrypt the package data in 4096-byte segments.
|
|
638
798
|
*
|
|
@@ -674,11 +834,13 @@ async function encryptPackageData(data, packageKey, keySalt, hashName, blockSize
|
|
|
674
834
|
}
|
|
675
835
|
/** Build the Agile EncryptionInfo XML document. */
|
|
676
836
|
function buildEncryptionInfoXml(params) {
|
|
677
|
-
const { keyBits, hashAlgorithm, hashSize, spinCount, blockSize, keySalt, encryptedVerifierHashInput, encryptedVerifierHashValue, encryptedKeyValue } = params;
|
|
837
|
+
const { keyBits, hashAlgorithm, hashSize, spinCount, blockSize, keySalt, encryptedVerifierHashInput, encryptedVerifierHashValue, encryptedKeyValue, encryptedHmacKey, encryptedHmacValue } = params;
|
|
678
838
|
const saltB64 = bytesToBase64(keySalt);
|
|
679
839
|
const vhiB64 = bytesToBase64(encryptedVerifierHashInput);
|
|
680
840
|
const vhvB64 = bytesToBase64(encryptedVerifierHashValue);
|
|
681
841
|
const ekvB64 = bytesToBase64(encryptedKeyValue);
|
|
842
|
+
const hmacKeyB64 = bytesToBase64(encryptedHmacKey);
|
|
843
|
+
const hmacValB64 = bytesToBase64(encryptedHmacValue);
|
|
682
844
|
return ('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\r\n' +
|
|
683
845
|
'<encryption xmlns="http://schemas.microsoft.com/office/2006/encryption" ' +
|
|
684
846
|
'xmlns:p="http://schemas.microsoft.com/office/2006/keyEncryptor/password" ' +
|
|
@@ -686,7 +848,7 @@ function buildEncryptionInfoXml(params) {
|
|
|
686
848
|
`<keyData saltSize="16" blockSize="${blockSize}" keyBits="${keyBits}" ` +
|
|
687
849
|
`hashSize="${hashSize}" cipherAlgorithm="AES" cipherChaining="ChainingModeCBC" ` +
|
|
688
850
|
`hashAlgorithm="${hashAlgorithm}" saltValue="${saltB64}"/>\r\n` +
|
|
689
|
-
|
|
851
|
+
`<dataIntegrity encryptedHmacKey="${hmacKeyB64}" encryptedHmacValue="${hmacValB64}"/>\r\n` +
|
|
690
852
|
"<keyEncryptors>\r\n" +
|
|
691
853
|
'<keyEncryptor uri="http://schemas.microsoft.com/office/2006/keyEncryptor/password">\r\n' +
|
|
692
854
|
`<p:encryptedKey spinCount="${spinCount}" saltSize="16" blockSize="${blockSize}" ` +
|
|
@@ -991,6 +991,17 @@ export interface TableProperties {
|
|
|
991
991
|
readonly indent?: Twips;
|
|
992
992
|
/** Table look (conditional formatting). */
|
|
993
993
|
readonly look?: TableLook;
|
|
994
|
+
/**
|
|
995
|
+
* Number of rows in a single horizontal banding stripe (`w:tblStyleRowBandSize`).
|
|
996
|
+
* Defaults to 1 in Word. Set on table styles so the `band1Horz`/`band2Horz`
|
|
997
|
+
* conditional formats render as visible row banding.
|
|
998
|
+
*/
|
|
999
|
+
readonly rowBandSize?: number;
|
|
1000
|
+
/**
|
|
1001
|
+
* Number of columns in a single vertical banding stripe (`w:tblStyleColBandSize`).
|
|
1002
|
+
* Defaults to 1 in Word.
|
|
1003
|
+
*/
|
|
1004
|
+
readonly colBandSize?: number;
|
|
994
1005
|
/** Floating table properties. */
|
|
995
1006
|
readonly float?: TableFloat;
|
|
996
1007
|
/** Cell spacing in twips. */
|
|
@@ -1450,6 +1461,8 @@ export interface DrawingShape {
|
|
|
1450
1461
|
readonly noOutline?: boolean;
|
|
1451
1462
|
/** Text content inside the shape. */
|
|
1452
1463
|
readonly textContent?: readonly Paragraph[];
|
|
1464
|
+
/** Vertical anchoring of the text body inside the shape (a:bodyPr/@anchor). */
|
|
1465
|
+
readonly textBodyAnchor?: "t" | "ctr" | "b" | "just" | "dist";
|
|
1453
1466
|
/** Alternative text. */
|
|
1454
1467
|
readonly altText?: string;
|
|
1455
1468
|
/** Shape name. */
|
|
@@ -1475,6 +1488,10 @@ export interface DrawingShape {
|
|
|
1475
1488
|
readonly behindDoc?: boolean;
|
|
1476
1489
|
/** Rotation in 60,000ths of a degree. */
|
|
1477
1490
|
readonly rotation?: number;
|
|
1491
|
+
/** Flip the shape horizontally (a:xfrm/@flipH). */
|
|
1492
|
+
readonly flipHorizontal?: boolean;
|
|
1493
|
+
/** Flip the shape vertically (a:xfrm/@flipV). */
|
|
1494
|
+
readonly flipVertical?: boolean;
|
|
1478
1495
|
/** Raw XML string (for preserving unrecognized shape details on round-trip). */
|
|
1479
1496
|
readonly rawXml?: string;
|
|
1480
1497
|
/**
|
|
@@ -7,10 +7,16 @@
|
|
|
7
7
|
export declare const TWIPS_PER_INCH = 1440;
|
|
8
8
|
/** Twips per point. */
|
|
9
9
|
export declare const TWIPS_PER_POINT = 20;
|
|
10
|
-
/**
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Twips per centimeter, derived exactly from 1 inch = 2.54 cm = 1440 twips
|
|
12
|
+
* (= 566.9291…). Using the exact factor — rather than the rounded 567 — keeps
|
|
13
|
+
* metric page sizes aligned with their canonical twip values, e.g.
|
|
14
|
+
* `cmToTwips(21)` → 11906 and `cmToTwips(29.7)` → 16838 (A4), matching
|
|
15
|
+
* `A4_PAGE_WIDTH` / `A4_PAGE_HEIGHT` in constants.ts.
|
|
16
|
+
*/
|
|
17
|
+
export declare const TWIPS_PER_CM: number;
|
|
18
|
+
/** Twips per millimeter, derived exactly from {@link TWIPS_PER_CM}. */
|
|
19
|
+
export declare const TWIPS_PER_MM: number;
|
|
14
20
|
/** EMU (English Metric Units) per inch — DrawingML coordinate space. */
|
|
15
21
|
export declare const EMU_PER_INCH = 914400;
|
|
16
22
|
/** EMU per centimeter. */
|
|
@@ -10,10 +10,16 @@
|
|
|
10
10
|
export const TWIPS_PER_INCH = 1440;
|
|
11
11
|
/** Twips per point. */
|
|
12
12
|
export const TWIPS_PER_POINT = 20;
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Twips per centimeter, derived exactly from 1 inch = 2.54 cm = 1440 twips
|
|
15
|
+
* (= 566.9291…). Using the exact factor — rather than the rounded 567 — keeps
|
|
16
|
+
* metric page sizes aligned with their canonical twip values, e.g.
|
|
17
|
+
* `cmToTwips(21)` → 11906 and `cmToTwips(29.7)` → 16838 (A4), matching
|
|
18
|
+
* `A4_PAGE_WIDTH` / `A4_PAGE_HEIGHT` in constants.ts.
|
|
19
|
+
*/
|
|
20
|
+
export const TWIPS_PER_CM = TWIPS_PER_INCH / 2.54;
|
|
21
|
+
/** Twips per millimeter, derived exactly from {@link TWIPS_PER_CM}. */
|
|
22
|
+
export const TWIPS_PER_MM = TWIPS_PER_CM / 10;
|
|
17
23
|
/** EMU (English Metric Units) per inch — DrawingML coordinate space. */
|
|
18
24
|
export const EMU_PER_INCH = 914400;
|
|
19
25
|
/** EMU per centimeter. */
|
|
@@ -42,6 +42,7 @@ export function renderSdt(xml, sdt, ctx, helpers) {
|
|
|
42
42
|
? {
|
|
43
43
|
imageRemap: ctx.imageRIdRemap,
|
|
44
44
|
hyperlinkRIds: ctx.hyperlinkRIds,
|
|
45
|
+
nextDocPrId: ctx.ids.nextDocPrId,
|
|
45
46
|
rawXmlPolicy: ctx.rawXmlPolicy
|
|
46
47
|
}
|
|
47
48
|
: undefined;
|
|
@@ -78,6 +79,7 @@ export function renderBodyContent(xml, content, ctx) {
|
|
|
78
79
|
const helpers = {
|
|
79
80
|
imageRemap: renderCtx.imageRIdRemap,
|
|
80
81
|
hyperlinkRIds: renderCtx.hyperlinkRIds,
|
|
82
|
+
nextDocPrId: renderCtx.ids.nextDocPrId,
|
|
81
83
|
rawXmlPolicy: renderCtx.rawXmlPolicy
|
|
82
84
|
};
|
|
83
85
|
switch (content.type) {
|
|
@@ -88,7 +90,7 @@ export function renderBodyContent(xml, content, ctx) {
|
|
|
88
90
|
renderTable(xml, content, helpers);
|
|
89
91
|
break;
|
|
90
92
|
case "floatingImage":
|
|
91
|
-
renderFloatingImage(xml, content, renderCtx.imageRIdRemap);
|
|
93
|
+
renderFloatingImage(xml, content, renderCtx.imageRIdRemap, renderCtx.ids.nextDocPrId);
|
|
92
94
|
break;
|
|
93
95
|
case "tableOfContents":
|
|
94
96
|
renderTableOfContents(xml, content);
|
|
@@ -103,7 +105,13 @@ export function renderBodyContent(xml, content, ctx) {
|
|
|
103
105
|
renderSdt(xml, content, renderCtx);
|
|
104
106
|
break;
|
|
105
107
|
case "checkBox":
|
|
108
|
+
// A checkbox renders as an inline (run-level) SDT whose sdtContent holds
|
|
109
|
+
// a run. At block level that run would be an illegal child of
|
|
110
|
+
// CT_SdtContentBlock, so wrap it in a paragraph — making it a valid
|
|
111
|
+
// run-level SDT inside a block-level paragraph.
|
|
112
|
+
xml.openNode("w:p");
|
|
106
113
|
renderCheckBox(xml, content);
|
|
114
|
+
xml.closeNode();
|
|
107
115
|
break;
|
|
108
116
|
case "drawingShape":
|
|
109
117
|
renderDrawingShape(xml, content, renderCtx);
|
|
@@ -290,7 +298,17 @@ function renderDrawingShape(xml, shape, ctx) {
|
|
|
290
298
|
// Shape properties
|
|
291
299
|
xml.openNode("wps:spPr");
|
|
292
300
|
// Transform
|
|
293
|
-
|
|
301
|
+
const xfrmAttrs = {};
|
|
302
|
+
if (shape.rotation) {
|
|
303
|
+
xfrmAttrs["rot"] = String(shape.rotation);
|
|
304
|
+
}
|
|
305
|
+
if (shape.flipHorizontal) {
|
|
306
|
+
xfrmAttrs["flipH"] = "1";
|
|
307
|
+
}
|
|
308
|
+
if (shape.flipVertical) {
|
|
309
|
+
xfrmAttrs["flipV"] = "1";
|
|
310
|
+
}
|
|
311
|
+
xml.openNode("a:xfrm", Object.keys(xfrmAttrs).length > 0 ? xfrmAttrs : {});
|
|
294
312
|
xml.leafNode("a:off", { x: "0", y: "0" });
|
|
295
313
|
xml.leafNode("a:ext", { cx: String(shape.width), cy: String(shape.height) });
|
|
296
314
|
xml.closeNode(); // a:xfrm
|
|
@@ -366,6 +384,7 @@ function renderDrawingShape(xml, shape, ctx) {
|
|
|
366
384
|
? {
|
|
367
385
|
imageRemap: ctx.imageRIdRemap,
|
|
368
386
|
hyperlinkRIds: ctx.hyperlinkRIds,
|
|
387
|
+
nextDocPrId: ctx.ids.nextDocPrId,
|
|
369
388
|
rawXmlPolicy: ctx.rawXmlPolicy
|
|
370
389
|
}
|
|
371
390
|
: undefined;
|
|
@@ -375,8 +394,13 @@ function renderDrawingShape(xml, shape, ctx) {
|
|
|
375
394
|
xml.closeNode(); // w:txbxContent
|
|
376
395
|
xml.closeNode(); // wps:txbx
|
|
377
396
|
}
|
|
378
|
-
// Body properties (required)
|
|
379
|
-
|
|
397
|
+
// Body properties (required). The vertical text anchor lives on a:bodyPr/@anchor.
|
|
398
|
+
if (shape.textBodyAnchor) {
|
|
399
|
+
xml.leafNode("wps:bodyPr", { anchor: shape.textBodyAnchor });
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
xml.leafNode("wps:bodyPr");
|
|
403
|
+
}
|
|
380
404
|
xml.closeNode(); // wps:wsp
|
|
381
405
|
xml.closeNode(); // a:graphicData
|
|
382
406
|
xml.closeNode(); // a:graphic
|
|
@@ -91,6 +91,24 @@ function collectHyperlinks(body) {
|
|
|
91
91
|
});
|
|
92
92
|
return links;
|
|
93
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* A minimal valid 1×1 transparent PNG.
|
|
96
|
+
*
|
|
97
|
+
* Used as an automatic raster fallback for SVG images that ship without an
|
|
98
|
+
* explicit `fallbackData`. In OOXML, `a:blip/@r:embed` must reference a raster
|
|
99
|
+
* image — Microsoft Word does not rasterize an SVG referenced directly by
|
|
100
|
+
* `a:blip`, so an SVG without a raster fallback renders as a broken/empty
|
|
101
|
+
* picture. By always pairing the SVG (`asvg:svgBlip`) with a raster blip we
|
|
102
|
+
* guarantee the drawing is renderable everywhere: SVG-aware Word shows the
|
|
103
|
+
* vector, older readers show the raster placeholder.
|
|
104
|
+
*/
|
|
105
|
+
const SVG_RASTER_FALLBACK_PNG = Uint8Array.from([
|
|
106
|
+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
|
|
107
|
+
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4,
|
|
108
|
+
0x89, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, 0x60, 0x00, 0x02, 0x00,
|
|
109
|
+
0x00, 0x05, 0x00, 0x01, 0xe2, 0x26, 0x05, 0x9b, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44,
|
|
110
|
+
0xae, 0x42, 0x60, 0x82
|
|
111
|
+
]);
|
|
94
112
|
/** Infer a content type for an opaque part based on its file extension. */
|
|
95
113
|
function inferContentType(ext) {
|
|
96
114
|
const map = {
|
|
@@ -343,7 +361,12 @@ async function _packageDocxInner(doc, options) {
|
|
|
343
361
|
};
|
|
344
362
|
for (const img of doc.images) {
|
|
345
363
|
const oldRid = img.rId;
|
|
346
|
-
if (img.mediaType === "svg"
|
|
364
|
+
if (img.mediaType === "svg") {
|
|
365
|
+
// SVG must always be paired with a raster fallback that `a:blip`
|
|
366
|
+
// references; Word cannot rasterize an SVG referenced directly by
|
|
367
|
+
// a:blip. If the caller did not provide one, synthesize a minimal
|
|
368
|
+
// transparent PNG so the drawing is still valid and renderable.
|
|
369
|
+
const fallbackData = img.fallbackData ?? SVG_RASTER_FALLBACK_PNG;
|
|
347
370
|
// Main rId points at the PNG fallback (the raster image consumed by
|
|
348
371
|
// a:blip).
|
|
349
372
|
const baseName = img.fileName.replace(/\.[^.]+$/, "");
|
|
@@ -359,7 +382,7 @@ async function _packageDocxInner(doc, options) {
|
|
|
359
382
|
if (ext) {
|
|
360
383
|
imageExtensions.add(ext);
|
|
361
384
|
}
|
|
362
|
-
svgFallbacks.push({ fallbackFileName, data:
|
|
385
|
+
svgFallbacks.push({ fallbackFileName, data: fallbackData });
|
|
363
386
|
}
|
|
364
387
|
else {
|
|
365
388
|
registerImageRel(oldRid, `media/${img.fileName}`);
|
|
@@ -1164,14 +1187,24 @@ async function _packageDocxInner(doc, options) {
|
|
|
1164
1187
|
archive.add(PartPath.Footnotes,
|
|
1165
1188
|
// Footnotes are an independent OPC part — their r:id values must
|
|
1166
1189
|
// resolve against word/_rels/footnotes.xml.rels, not document.xml.rels.
|
|
1167
|
-
renderXml(xml => renderFootnotes(xml, doc.footnotes, {
|
|
1190
|
+
renderXml(xml => renderFootnotes(xml, doc.footnotes, {
|
|
1191
|
+
imageRemap: new Map(),
|
|
1192
|
+
hyperlinkRIds,
|
|
1193
|
+
nextDocPrId: renderCtx.ids.nextDocPrId,
|
|
1194
|
+
rawXmlPolicy
|
|
1195
|
+
})));
|
|
1168
1196
|
if (getRelationshipCount(footnoteRels) > 0) {
|
|
1169
1197
|
archive.add("word/_rels/footnotes.xml.rels", renderXml(xml => renderRelationships(footnoteRels, xml)));
|
|
1170
1198
|
}
|
|
1171
1199
|
}
|
|
1172
1200
|
// word/endnotes.xml + endnotes.xml.rels
|
|
1173
1201
|
if (hasEndnotes) {
|
|
1174
|
-
archive.add(PartPath.Endnotes, renderXml(xml => renderEndnotes(xml, doc.endnotes, {
|
|
1202
|
+
archive.add(PartPath.Endnotes, renderXml(xml => renderEndnotes(xml, doc.endnotes, {
|
|
1203
|
+
imageRemap: new Map(),
|
|
1204
|
+
hyperlinkRIds,
|
|
1205
|
+
nextDocPrId: renderCtx.ids.nextDocPrId,
|
|
1206
|
+
rawXmlPolicy
|
|
1207
|
+
})));
|
|
1175
1208
|
if (getRelationshipCount(endnoteRels) > 0) {
|
|
1176
1209
|
archive.add("word/_rels/endnotes.xml.rels", renderXml(xml => renderRelationships(endnoteRels, xml)));
|
|
1177
1210
|
}
|
|
@@ -1182,7 +1215,12 @@ async function _packageDocxInner(doc, options) {
|
|
|
1182
1215
|
// Comments live in their own OPC part; pass helpers so embedded
|
|
1183
1216
|
// hyperlinks/images render with the right r:id (the rels manager
|
|
1184
1217
|
// below registered them under their model-original id).
|
|
1185
|
-
renderXml(xml => renderComments(xml, doc.comments, {
|
|
1218
|
+
renderXml(xml => renderComments(xml, doc.comments, {
|
|
1219
|
+
imageRemap: new Map(),
|
|
1220
|
+
hyperlinkRIds,
|
|
1221
|
+
nextDocPrId: renderCtx.ids.nextDocPrId,
|
|
1222
|
+
rawXmlPolicy
|
|
1223
|
+
})));
|
|
1186
1224
|
if (getRelationshipCount(commentRels) > 0) {
|
|
1187
1225
|
archive.add("word/_rels/comments.xml.rels", renderXml(xml => renderRelationships(commentRels, xml)));
|
|
1188
1226
|
}
|
|
@@ -1207,6 +1245,7 @@ async function _packageDocxInner(doc, options) {
|
|
|
1207
1245
|
renderXml(xml => renderHeader(xml, headerDef.content, {
|
|
1208
1246
|
imageRemap: new Map(),
|
|
1209
1247
|
hyperlinkRIds,
|
|
1248
|
+
nextDocPrId: renderCtx.ids.nextDocPrId,
|
|
1210
1249
|
rawXmlPolicy
|
|
1211
1250
|
})));
|
|
1212
1251
|
// Header .rels file
|
|
@@ -1230,6 +1269,7 @@ async function _packageDocxInner(doc, options) {
|
|
|
1230
1269
|
renderXml(xml => renderFooter(xml, footerDef.content, {
|
|
1231
1270
|
imageRemap: new Map(),
|
|
1232
1271
|
hyperlinkRIds,
|
|
1272
|
+
nextDocPrId: renderCtx.ids.nextDocPrId,
|
|
1233
1273
|
rawXmlPolicy
|
|
1234
1274
|
})));
|
|
1235
1275
|
// Footer .rels file
|
|
@@ -7,4 +7,4 @@
|
|
|
7
7
|
import type { XmlSink } from "../../xml/types.js";
|
|
8
8
|
import type { FloatingImage } from "../types.js";
|
|
9
9
|
/** Render a floating image as a standalone paragraph with wp:anchor. */
|
|
10
|
-
export declare function renderFloatingImage(xml: XmlSink, img: FloatingImage, imageRemap?: ReadonlyMap<string, string
|
|
10
|
+
export declare function renderFloatingImage(xml: XmlSink, img: FloatingImage, imageRemap?: ReadonlyMap<string, string>, nextDocPrId?: () => number): void;
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
import { NS_A, NS_PIC, URI_PIC, NS_ASVG, GUID_SVG } from "../constants.js";
|
|
8
8
|
import { DEFAULT_RELATIVE_HEIGHT, DEFAULT_WRAP_MARGIN_EMU } from "../units.js";
|
|
9
9
|
/** Render a floating image as a standalone paragraph with wp:anchor. */
|
|
10
|
-
export function renderFloatingImage(xml, img, imageRemap) {
|
|
11
|
-
const drawingId = img.drawingId ?? 1;
|
|
10
|
+
export function renderFloatingImage(xml, img, imageRemap, nextDocPrId) {
|
|
11
|
+
const drawingId = nextDocPrId?.() ?? img.drawingId ?? 1;
|
|
12
12
|
const name = img.name ?? "Picture";
|
|
13
13
|
// Resolve relationship id used in r:embed via packager-provided remap.
|
|
14
14
|
const embedRId = imageRemap?.get(img.rId) ?? img.rId;
|
|
@@ -29,6 +29,21 @@ export interface RenderHelpers {
|
|
|
29
29
|
readonly imageRemap?: ReadonlyMap<string, string>;
|
|
30
30
|
/** See WordRenderContext.hyperlinkRIds. */
|
|
31
31
|
readonly hyperlinkRIds?: ReadonlyWeakMap<object, string>;
|
|
32
|
+
/**
|
|
33
|
+
* Allocates the next document-wide unique drawing object id, used for
|
|
34
|
+
* `wp:docPr/@id` (and the matching `pic:cNvPr/@id` / `wps:cNvPr/@id`).
|
|
35
|
+
*
|
|
36
|
+
* Word requires every drawing object id to be a unique positive integer
|
|
37
|
+
* across the entire document — including body, headers, footers, footnotes,
|
|
38
|
+
* endnotes, comments and text boxes. The packager seeds a single counter on
|
|
39
|
+
* the render context and exposes it here so every drawing renderer draws
|
|
40
|
+
* from the same id space, regardless of what (possibly duplicate or unset)
|
|
41
|
+
* `drawingId` the model carries.
|
|
42
|
+
*
|
|
43
|
+
* When undefined (e.g. a standalone renderer call without a context),
|
|
44
|
+
* renderers fall back to the model's `drawingId` or `1`.
|
|
45
|
+
*/
|
|
46
|
+
readonly nextDocPrId?: () => number;
|
|
32
47
|
/**
|
|
33
48
|
* Raw XML output policy. Controls how preserved/opaque rawXml fragments
|
|
34
49
|
* (opaqueRun, opaqueParagraphChild, opaqueDrawing, _advancedFillXml, …)
|