@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.
Files changed (112) hide show
  1. package/dist/browser/modules/archive/io/random-access.d.ts +1 -1
  2. package/dist/browser/modules/excel/workbook.browser.d.ts +1 -1
  3. package/dist/browser/modules/excel/xlsx/xform/comment/comment-xform.d.ts +3 -0
  4. package/dist/browser/modules/excel/xlsx/xform/comment/comment-xform.js +30 -7
  5. package/dist/browser/modules/pdf/excel-bridge.d.ts +32 -0
  6. package/dist/browser/modules/pdf/excel-bridge.js +67 -1
  7. package/dist/browser/modules/pdf/word-bridge.d.ts +20 -15
  8. package/dist/browser/modules/pdf/word-bridge.js +49 -34
  9. package/dist/browser/modules/stream/common/consumers.d.ts +2 -1
  10. package/dist/browser/modules/word/advanced/diff.js +125 -13
  11. package/dist/browser/modules/word/advanced/drawing-shapes.js +3 -0
  12. package/dist/browser/modules/word/bridge/excel-bridge.js +21 -1
  13. package/dist/browser/modules/word/builder/document-handle.d.ts +2 -0
  14. package/dist/browser/modules/word/builder/document-handle.js +14 -2
  15. package/dist/browser/modules/word/builder/paragraph-builders.js +10 -1
  16. package/dist/browser/modules/word/builder/run-builders.d.ts +19 -2
  17. package/dist/browser/modules/word/builder/run-builders.js +2 -6
  18. package/dist/browser/modules/word/convert/odt/odt.js +6 -1
  19. package/dist/browser/modules/word/layout/layout-full.d.ts +12 -0
  20. package/dist/browser/modules/word/layout/layout-full.js +74 -9
  21. package/dist/browser/modules/word/layout/layout-model.d.ts +12 -0
  22. package/dist/browser/modules/word/query/merge.js +26 -10
  23. package/dist/browser/modules/word/query/split.js +68 -2
  24. package/dist/browser/modules/word/reader/docx-reader.js +23 -0
  25. package/dist/browser/modules/word/security/cfb-reader.d.ts +14 -3
  26. package/dist/browser/modules/word/security/cfb-reader.js +271 -153
  27. package/dist/browser/modules/word/security/document-protection.js +10 -4
  28. package/dist/browser/modules/word/security/encryption.js +194 -32
  29. package/dist/browser/modules/word/types.d.ts +17 -0
  30. package/dist/browser/modules/word/units.d.ts +10 -4
  31. package/dist/browser/modules/word/units.js +10 -4
  32. package/dist/browser/modules/word/writer/document-writer.js +28 -4
  33. package/dist/browser/modules/word/writer/docx-packager.js +45 -5
  34. package/dist/browser/modules/word/writer/image-writer.d.ts +1 -1
  35. package/dist/browser/modules/word/writer/image-writer.js +2 -2
  36. package/dist/browser/modules/word/writer/render-context.d.ts +15 -0
  37. package/dist/browser/modules/word/writer/run-writer.js +8 -4
  38. package/dist/browser/modules/word/writer/section-writer.js +46 -35
  39. package/dist/browser/modules/word/writer/streaming-writer.js +4 -0
  40. package/dist/browser/modules/word/writer/styles-writer.js +11 -0
  41. package/dist/browser/modules/word/writer/table-writer.js +6 -0
  42. package/dist/cjs/modules/excel/xlsx/xform/comment/comment-xform.js +30 -7
  43. package/dist/cjs/modules/pdf/excel-bridge.js +67 -0
  44. package/dist/cjs/modules/pdf/word-bridge.js +49 -34
  45. package/dist/cjs/modules/word/advanced/diff.js +125 -13
  46. package/dist/cjs/modules/word/advanced/drawing-shapes.js +3 -0
  47. package/dist/cjs/modules/word/bridge/excel-bridge.js +21 -1
  48. package/dist/cjs/modules/word/builder/document-handle.js +14 -2
  49. package/dist/cjs/modules/word/builder/paragraph-builders.js +10 -1
  50. package/dist/cjs/modules/word/builder/run-builders.js +2 -6
  51. package/dist/cjs/modules/word/convert/odt/odt.js +6 -1
  52. package/dist/cjs/modules/word/layout/layout-full.js +74 -9
  53. package/dist/cjs/modules/word/query/merge.js +26 -10
  54. package/dist/cjs/modules/word/query/split.js +68 -2
  55. package/dist/cjs/modules/word/reader/docx-reader.js +23 -0
  56. package/dist/cjs/modules/word/security/cfb-reader.js +271 -153
  57. package/dist/cjs/modules/word/security/document-protection.js +10 -4
  58. package/dist/cjs/modules/word/security/encryption.js +193 -31
  59. package/dist/cjs/modules/word/units.js +10 -4
  60. package/dist/cjs/modules/word/writer/document-writer.js +28 -4
  61. package/dist/cjs/modules/word/writer/docx-packager.js +45 -5
  62. package/dist/cjs/modules/word/writer/image-writer.js +2 -2
  63. package/dist/cjs/modules/word/writer/run-writer.js +8 -4
  64. package/dist/cjs/modules/word/writer/section-writer.js +46 -35
  65. package/dist/cjs/modules/word/writer/streaming-writer.js +4 -0
  66. package/dist/cjs/modules/word/writer/styles-writer.js +11 -0
  67. package/dist/cjs/modules/word/writer/table-writer.js +6 -0
  68. package/dist/esm/modules/excel/xlsx/xform/comment/comment-xform.js +30 -7
  69. package/dist/esm/modules/pdf/excel-bridge.js +67 -1
  70. package/dist/esm/modules/pdf/word-bridge.js +49 -34
  71. package/dist/esm/modules/word/advanced/diff.js +125 -13
  72. package/dist/esm/modules/word/advanced/drawing-shapes.js +3 -0
  73. package/dist/esm/modules/word/bridge/excel-bridge.js +21 -1
  74. package/dist/esm/modules/word/builder/document-handle.js +14 -2
  75. package/dist/esm/modules/word/builder/paragraph-builders.js +10 -1
  76. package/dist/esm/modules/word/builder/run-builders.js +2 -6
  77. package/dist/esm/modules/word/convert/odt/odt.js +6 -1
  78. package/dist/esm/modules/word/layout/layout-full.js +74 -9
  79. package/dist/esm/modules/word/query/merge.js +26 -10
  80. package/dist/esm/modules/word/query/split.js +68 -2
  81. package/dist/esm/modules/word/reader/docx-reader.js +23 -0
  82. package/dist/esm/modules/word/security/cfb-reader.js +271 -153
  83. package/dist/esm/modules/word/security/document-protection.js +10 -4
  84. package/dist/esm/modules/word/security/encryption.js +194 -32
  85. package/dist/esm/modules/word/units.js +10 -4
  86. package/dist/esm/modules/word/writer/document-writer.js +28 -4
  87. package/dist/esm/modules/word/writer/docx-packager.js +45 -5
  88. package/dist/esm/modules/word/writer/image-writer.js +2 -2
  89. package/dist/esm/modules/word/writer/run-writer.js +8 -4
  90. package/dist/esm/modules/word/writer/section-writer.js +46 -35
  91. package/dist/esm/modules/word/writer/streaming-writer.js +4 -0
  92. package/dist/esm/modules/word/writer/styles-writer.js +11 -0
  93. package/dist/esm/modules/word/writer/table-writer.js +6 -0
  94. package/dist/iife/excelts.iife.js +20 -8
  95. package/dist/iife/excelts.iife.js.map +1 -1
  96. package/dist/iife/excelts.iife.min.js +2 -2
  97. package/dist/types/modules/archive/io/random-access.d.ts +1 -1
  98. package/dist/types/modules/excel/workbook.browser.d.ts +1 -1
  99. package/dist/types/modules/excel/xlsx/xform/comment/comment-xform.d.ts +3 -0
  100. package/dist/types/modules/pdf/excel-bridge.d.ts +32 -0
  101. package/dist/types/modules/pdf/word-bridge.d.ts +20 -15
  102. package/dist/types/modules/stream/common/consumers.d.ts +2 -1
  103. package/dist/types/modules/word/builder/document-handle.d.ts +2 -0
  104. package/dist/types/modules/word/builder/run-builders.d.ts +19 -2
  105. package/dist/types/modules/word/layout/layout-full.d.ts +12 -0
  106. package/dist/types/modules/word/layout/layout-model.d.ts +12 -0
  107. package/dist/types/modules/word/security/cfb-reader.d.ts +14 -3
  108. package/dist/types/modules/word/types.d.ts +17 -0
  109. package/dist/types/modules/word/units.d.ts +10 -4
  110. package/dist/types/modules/word/writer/image-writer.d.ts +1 -1
  111. package/dist/types/modules/word/writer/render-context.d.ts +15 -0
  112. package/package.json +2 -2
@@ -149,15 +149,19 @@ async function verifyPassword(password, info) {
149
149
  try {
150
150
  // Derive verifier hash input key
151
151
  const verifierInputKey = await deriveEncryptionKey(password, info, exports.AGILE_BLOCK_KEYS.verifierHashInput);
152
- // Decrypt the verifier hash input (PKCS#7 padded by encryption write path)
153
- const verifierInput = aesCbcPkcs7Decrypt(info.encryptedVerifierHashInput, verifierInputKey, info.keySalt);
154
- // Hash the verifier input
152
+ // Decrypt the verifier hash input. MS-OFFCRYPTO §2.3.4.13 specifies
153
+ // these blobs are AES-CBC with the plaintext zero-padded to the block
154
+ // boundary NOT PKCS#7. Using a PKCS#7 decrypt would mis-strip bytes
155
+ // and break interop with Word / msoffcrypto.
156
+ const verifierInput = aesCbcRawDecrypt(info.encryptedVerifierHashInput, verifierInputKey, info.keySalt);
157
+ // Hash the verifier input. The plaintext is exactly 16 bytes (saltSize),
158
+ // so trim any zero padding before hashing.
155
159
  const hashAlg = mapHashName(info.hashAlgorithm);
156
- const computedHash = await (0, crypto_1.hashAsync)(hashAlg, verifierInput);
160
+ const computedHash = await (0, crypto_1.hashAsync)(hashAlg, verifierInput.slice(0, info.blockSize));
157
161
  // Derive verifier hash value key
158
162
  const verifierValueKey = await deriveEncryptionKey(password, info, exports.AGILE_BLOCK_KEYS.verifierHashValue);
159
- // Decrypt the verifier hash value (PKCS#7 padded by encryption write path)
160
- const expectedHash = aesCbcPkcs7Decrypt(info.encryptedVerifierHashValue, verifierValueKey, info.keySalt);
163
+ // Decrypt the verifier hash value (zero-padded AES-CBC, see above).
164
+ const expectedHash = aesCbcRawDecrypt(info.encryptedVerifierHashValue, verifierValueKey, info.keySalt);
161
165
  // Compare (truncate to hashSize in case of padding)
162
166
  return bytesEqual(computedHash.slice(0, info.hashSize), expectedHash.slice(0, info.hashSize));
163
167
  }
@@ -180,8 +184,9 @@ async function verifyPassword(password, info) {
180
184
  async function decryptPackage(encryptedPackage, info, password, maxDecryptedSize = 512 * 1024 * 1024) {
181
185
  // Derive key encryption key
182
186
  const keyEncryptionKey = await deriveEncryptionKey(password, info, exports.AGILE_BLOCK_KEYS.encryptedKey);
183
- // Decrypt the actual package key (PKCS#7 padded by encryption write path)
184
- const packageKey = aesCbcPkcs7Decrypt(info.encryptedKeyValue, keyEncryptionKey, info.keySalt);
187
+ // Decrypt the actual package key. Zero-padded AES-CBC per MS-OFFCRYPTO
188
+ // §2.3.4.13 (NOT PKCS#7); the key is exactly keyBits/8 bytes.
189
+ const packageKey = aesCbcRawDecrypt(info.encryptedKeyValue, keyEncryptionKey, info.keySalt).slice(0, info.keyBits / 8);
185
190
  if (encryptedPackage.length < 8) {
186
191
  throw new errors_1.DocxDecryptionError("EncryptedPackage too small (missing 8-byte size header)");
187
192
  }
@@ -265,25 +270,18 @@ function getHashSizeFor(hashName) {
265
270
  }
266
271
  }
267
272
  /**
268
- * Decrypt an AES-CBC blob whose plaintext was written with PKCS#7 padding.
269
- * Used for the encryptedKeyValue / encryptedVerifierHashInput /
270
- * encryptedVerifierHashValue blobs. The IV is truncated/extended to 16
271
- * bytes per the OOXML spec.
273
+ * Decrypt an AES-CBC blob written with zero padding (no PKCS#7). Used for
274
+ * the encryptedKeyValue / encryptedVerifierHashInput / encryptedVerifierHashValue
275
+ * blobs, per MS-OFFCRYPTO §2.3.4.13. The IV is truncated/extended to 16 bytes.
272
276
  */
273
- function aesCbcPkcs7Decrypt(data, key, iv) {
274
- return (0, crypto_1.aesCbcDecrypt)(data, key, ivToBlockSize(iv));
275
- }
276
- /**
277
- * Encrypt with AES-CBC + PKCS#7 padding — the Agile spec's standard
278
- * choice for verifier blobs and the encrypted package key.
279
- */
280
- function aesCbcPkcs7Encrypt(data, key, iv) {
281
- return (0, crypto_1.aesCbcEncrypt)(data, key, ivToBlockSize(iv));
277
+ function aesCbcRawDecrypt(data, key, iv) {
278
+ return (0, crypto_1.aesCbcDecryptRaw)(data, key, ivToBlockSize(iv));
282
279
  }
283
280
  /**
284
281
  * Encrypt with AES-CBC and zero-padding (no PKCS#7). Used by package
285
- * segment encryption: data is already padded to a 16-byte boundary by
286
- * the caller, and the on-disk format does not include a PKCS#7 trailer.
282
+ * segment encryption and the verifier / key blobs: data is already padded
283
+ * to a 16-byte boundary by the caller, and the on-disk format does not
284
+ * include a PKCS#7 trailer.
287
285
  */
288
286
  function aesCbcZeroPadEncrypt(data, key, iv) {
289
287
  return (0, crypto_1.aesCbcEncryptRaw)(data, key, ivToBlockSize(iv));
@@ -297,12 +295,51 @@ function ivToBlockSize(iv) {
297
295
  out.set(iv.slice(0, 16));
298
296
  return out;
299
297
  }
298
+ /** Right-pad data with zeros so its length is a multiple of `blockSize`. */
299
+ function padToBlock(data, blockSize) {
300
+ if (data.length % blockSize === 0) {
301
+ return data;
302
+ }
303
+ const out = new Uint8Array(Math.ceil(data.length / blockSize) * blockSize);
304
+ out.set(data);
305
+ return out;
306
+ }
300
307
  function concat(a, b) {
301
308
  const result = new Uint8Array(a.length + b.length);
302
309
  result.set(a, 0);
303
310
  result.set(b, a.length);
304
311
  return result;
305
312
  }
313
+ /** HMAC block size (in bytes) for the supported hash algorithms. */
314
+ function hmacBlockSize(hashName) {
315
+ // SHA-1 / SHA-256 use a 512-bit (64-byte) block; SHA-384 / SHA-512 use a
316
+ // 1024-bit (128-byte) block.
317
+ return hashName === "SHA-384" || hashName === "SHA-512" ? 128 : 64;
318
+ }
319
+ /**
320
+ * Compute HMAC(hashName, key, message) using the generic hash primitive.
321
+ * Implemented here (rather than in @utils/crypto, which only ships
322
+ * hmacSha256) so agile encryption can use SHA-512 etc. for the data
323
+ * integrity HMAC that Word verifies on open.
324
+ */
325
+ async function hmac(hashName, key, message) {
326
+ const blockSize = hmacBlockSize(hashName);
327
+ // Keys longer than the block size are hashed down first.
328
+ let k = key.length > blockSize ? await (0, crypto_1.hashAsync)(hashName, key) : key;
329
+ if (k.length < blockSize) {
330
+ const padded = new Uint8Array(blockSize);
331
+ padded.set(k);
332
+ k = padded;
333
+ }
334
+ const ipad = new Uint8Array(blockSize);
335
+ const opad = new Uint8Array(blockSize);
336
+ for (let i = 0; i < blockSize; i++) {
337
+ ipad[i] = k[i] ^ 0x36;
338
+ opad[i] = k[i] ^ 0x5c;
339
+ }
340
+ const inner = await (0, crypto_1.hashAsync)(hashName, concat(ipad, message));
341
+ return (0, crypto_1.hashAsync)(hashName, concat(opad, inner));
342
+ }
306
343
  function stringToUtf16LE(s) {
307
344
  const buf = new Uint8Array(s.length * 2);
308
345
  for (let i = 0; i < s.length; i++) {
@@ -602,17 +639,33 @@ async function encryptDocx(zipBytes, password, options) {
602
639
  const packageKey = (0, internal_utils_1.randomBytes)(keyBytes);
603
640
  // 2. Generate key encryption key (for encrypting the package key)
604
641
  const keyEncryptionKey = await deriveEncryptionKey(password, { keySalt, spinCount, hashAlgorithm, keyBits }, exports.AGILE_BLOCK_KEYS.encryptedKey);
605
- // 3. Encrypt the package key
606
- const encryptedKeyValue = aesCbcPkcs7Encrypt(packageKey, keyEncryptionKey, keySalt);
642
+ // 3. Encrypt the package key. MS-OFFCRYPTO §2.3.4.13: these blobs use
643
+ // zero-padded AES-CBC (no PKCS#7). packageKey is already block-aligned.
644
+ const encryptedKeyValue = aesCbcZeroPadEncrypt(padToBlock(packageKey, blockSize), keyEncryptionKey, keySalt);
607
645
  // 4. Generate verifier: random 16 bytes, hash them, encrypt both
608
646
  const verifierInput = (0, internal_utils_1.randomBytes)(16);
609
647
  const verifierHash = await (0, crypto_1.hashAsync)(hashName, verifierInput);
610
648
  const verifierInputKey = await deriveEncryptionKey(password, { keySalt, spinCount, hashAlgorithm, keyBits }, exports.AGILE_BLOCK_KEYS.verifierHashInput);
611
- const encryptedVerifierHashInput = aesCbcPkcs7Encrypt(verifierInput, verifierInputKey, keySalt);
649
+ const encryptedVerifierHashInput = aesCbcZeroPadEncrypt(padToBlock(verifierInput, blockSize), verifierInputKey, keySalt);
612
650
  const verifierValueKey = await deriveEncryptionKey(password, { keySalt, spinCount, hashAlgorithm, keyBits }, exports.AGILE_BLOCK_KEYS.verifierHashValue);
613
- const encryptedVerifierHashValue = aesCbcPkcs7Encrypt(verifierHash, verifierValueKey, keySalt);
651
+ const encryptedVerifierHashValue = aesCbcZeroPadEncrypt(padToBlock(verifierHash, blockSize), verifierValueKey, keySalt);
614
652
  // 5. Encrypt the ZIP data in 4096-byte segments
615
653
  const encryptedPackage = await encryptPackageData(zipBytes, packageKey, keySalt, hashName, blockSize);
654
+ // 5b. Data integrity (MS-OFFCRYPTO §2.3.4.14). Word verifies this HMAC on
655
+ // open; an empty or missing value makes it report the file as corrupt.
656
+ // - hmacKey: random, hashSize bytes (padded to block size).
657
+ // - encryptedHmacKey = AES-CBC(packageKey, IV0, hmacKey)
658
+ // - hmacValue = HMAC(hashAlg, hmacKey, encryptedPackage) (entire
659
+ // stream including the 8-byte size prefix)
660
+ // - encryptedHmacValue = AES-CBC(packageKey, IV1, hmacValue)
661
+ // IV0 = H(keySalt + blockKeyDataIntegrityKey)[:blockSize]
662
+ // IV1 = H(keySalt + blockKeyDataIntegrityValue)[:blockSize]
663
+ const hmacKey = (0, internal_utils_1.randomBytes)(hashSize);
664
+ const ivHmacKey = (await (0, crypto_1.hashAsync)(hashName, concat(keySalt, exports.AGILE_BLOCK_KEYS.dataIntegrityKey))).slice(0, blockSize);
665
+ const ivHmacValue = (await (0, crypto_1.hashAsync)(hashName, concat(keySalt, exports.AGILE_BLOCK_KEYS.dataIntegrityValue))).slice(0, blockSize);
666
+ const encryptedHmacKey = aesCbcZeroPadEncrypt(padToBlock(hmacKey, blockSize), packageKey, ivHmacKey);
667
+ const hmacValue = await hmac(hashName, hmacKey, encryptedPackage);
668
+ const encryptedHmacValue = aesCbcZeroPadEncrypt(padToBlock(hmacValue, blockSize), packageKey, ivHmacValue);
616
669
  // 6. Generate EncryptionInfo XML
617
670
  const encInfoXml = buildEncryptionInfoXml({
618
671
  keyBits,
@@ -623,7 +676,9 @@ async function encryptDocx(zipBytes, password, options) {
623
676
  keySalt,
624
677
  encryptedVerifierHashInput,
625
678
  encryptedVerifierHashValue,
626
- encryptedKeyValue
679
+ encryptedKeyValue,
680
+ encryptedHmacKey,
681
+ encryptedHmacValue
627
682
  });
628
683
  // Prepend 8-byte version header: version 4.4 + flags 0x40
629
684
  const xmlBytes = internal_utils_1.utf8Encoder.encode(encInfoXml);
@@ -633,8 +688,12 @@ async function encryptDocx(zipBytes, password, options) {
633
688
  encInfoView.setUint16(2, 4, true); // version minor
634
689
  encInfoView.setUint32(4, 0x40, true); // flags (agile)
635
690
  encInfoStream.set(xmlBytes, 8);
636
- // 7. Package into CFB
691
+ // 7. Package into CFB.
692
+ // Office requires the \x06DataSpaces structure (MS-OFFCRYPTO §2.3.2)
693
+ // in addition to EncryptionInfo + EncryptedPackage; without it Word
694
+ // rejects the file as corrupt even when the password is correct.
637
695
  const cfbBytes = (0, cfb_reader_1.writeCfb)([
696
+ ...buildDataSpacesStreams(),
638
697
  { name: "EncryptionInfo", data: encInfoStream },
639
698
  { name: "EncryptedPackage", data: encryptedPackage }
640
699
  ]);
@@ -643,6 +702,107 @@ async function encryptDocx(zipBytes, password, options) {
643
702
  // =============================================================================
644
703
  // Encrypt Helpers
645
704
  // =============================================================================
705
+ // -----------------------------------------------------------------------------
706
+ // \x06DataSpaces structure (MS-OFFCRYPTO §2.3.2)
707
+ //
708
+ // Office encrypted documents wrap the EncryptedPackage in a DataSpaces map so
709
+ // the consumer knows which transform (StrongEncryption) was applied. The four
710
+ // streams below are byte-for-byte what Office writes for password-based agile
711
+ // encryption. Word validates this structure on open; omitting it makes the
712
+ // file "corrupt" even with the correct password.
713
+ // -----------------------------------------------------------------------------
714
+ /** Encode a UTF-8 length-prefixed unicode string (UNICODE-LP-P4):
715
+ * [4-byte LE byte length of UTF-16LE payload][UTF-16LE chars][pad to 4-byte boundary]. */
716
+ function lengthPrefixedUtf16(str) {
717
+ const chars = stringToUtf16LE(str);
718
+ const padded = Math.ceil(chars.length / 4) * 4;
719
+ const out = new Uint8Array(4 + padded);
720
+ new DataView(out.buffer).setUint32(0, chars.length, true);
721
+ out.set(chars, 4);
722
+ return out;
723
+ }
724
+ /** Concatenate several byte arrays. */
725
+ function concatAll(...parts) {
726
+ const total = parts.reduce((s, p) => s + p.length, 0);
727
+ const out = new Uint8Array(total);
728
+ let off = 0;
729
+ for (const p of parts) {
730
+ out.set(p, off);
731
+ off += p.length;
732
+ }
733
+ return out;
734
+ }
735
+ /** Build the four \x06DataSpaces streams Office requires for agile encryption. */
736
+ function buildDataSpacesStreams() {
737
+ const DATASPACES = "\u0006DataSpaces";
738
+ // --- Version stream (DataSpaceVersionInfo) ---
739
+ // FeatureIdentifier "Microsoft.Container.DataSpaces" + reader/updater/writer
740
+ // version (each major=1, minor=0).
741
+ const versionStream = concatAll(lengthPrefixedUtf16("Microsoft.Container.DataSpaces"), u16le(1), u16le(0), // reader version
742
+ u16le(1), u16le(0), // updater version
743
+ u16le(1), u16le(0) // writer version
744
+ );
745
+ // --- DataSpaceMap stream ---
746
+ // Header: HeaderLength(8) + EntryCount(1) followed by one MapEntry.
747
+ // MapEntry: EntryLength + ReferenceComponentCount(1) +
748
+ // [ReferenceComponent: type(0=stream) + LP name "EncryptedPackage"] +
749
+ // LP DataSpaceName "StrongEncryptionDataSpace".
750
+ const refName = lengthPrefixedUtf16("EncryptedPackage");
751
+ const dsName = lengthPrefixedUtf16("StrongEncryptionDataSpace");
752
+ const refComponent = concatAll(u32le(0), refName); // 0 = stream component
753
+ const mapEntryBody = concatAll(u32le(1), refComponent, dsName); // 1 reference component
754
+ const entryLength = 4 + mapEntryBody.length; // include the EntryLength field itself
755
+ const mapEntry = concatAll(u32le(entryLength), mapEntryBody);
756
+ const dataSpaceMap = concatAll(u32le(8), u32le(1), mapEntry); // headerLen=8, entryCount=1
757
+ // --- DataSpaceInfo/StrongEncryptionDataSpace stream (DataSpaceDefinition) ---
758
+ // HeaderLength(8) + TransformReferenceCount(1) + LP transform name.
759
+ const transformName = lengthPrefixedUtf16("StrongEncryptionTransform");
760
+ const dataSpaceDefinition = concatAll(u32le(8), u32le(1), transformName);
761
+ // --- TransformInfo/StrongEncryptionTransform/\x06Primary stream ---
762
+ // TransformInfoHeader:
763
+ // TransformLength + TransformType(1) + LP TransformId +
764
+ // LP TransformName + reader/updater/writer versions.
765
+ // Followed by EncryptionTransformInfo:
766
+ // LP EncryptionName + EncryptionBlockSize(4) + CipherMode(4).
767
+ const transformId = lengthPrefixedUtf16("{FF9A3F03-56EF-4613-BDD5-5A41C1D07246}");
768
+ const transformNamePrimary = lengthPrefixedUtf16("Microsoft.Container.EncryptionTransform");
769
+ const headerBody = concatAll(u32le(1), // TransformType = 1
770
+ transformId, transformNamePrimary, u16le(1), u16le(0), // reader version
771
+ u16le(1), u16le(0), // updater version
772
+ u16le(1), u16le(0) // writer version
773
+ );
774
+ const transformLength = 4 + headerBody.length; // include the length field itself
775
+ const transformHeader = concatAll(u32le(transformLength), headerBody);
776
+ const encryptionTransformInfo = concatAll(lengthPrefixedUtf16(""), // EncryptionName (empty for agile)
777
+ u32le(0), // EncryptionBlockSize
778
+ u32le(0) // CipherMode
779
+ );
780
+ const primary = concatAll(transformHeader, encryptionTransformInfo);
781
+ return [
782
+ { name: "Version", path: [DATASPACES], data: versionStream },
783
+ { name: "DataSpaceMap", path: [DATASPACES], data: dataSpaceMap },
784
+ {
785
+ name: "StrongEncryptionDataSpace",
786
+ path: [DATASPACES, "DataSpaceInfo"],
787
+ data: dataSpaceDefinition
788
+ },
789
+ {
790
+ name: "\u0006Primary",
791
+ path: [DATASPACES, "TransformInfo", "StrongEncryptionTransform"],
792
+ data: primary
793
+ }
794
+ ];
795
+ }
796
+ function u16le(n) {
797
+ const b = new Uint8Array(2);
798
+ new DataView(b.buffer).setUint16(0, n, true);
799
+ return b;
800
+ }
801
+ function u32le(n) {
802
+ const b = new Uint8Array(4);
803
+ new DataView(b.buffer).setUint32(0, n, true);
804
+ return b;
805
+ }
646
806
  /**
647
807
  * Encrypt the package data in 4096-byte segments.
648
808
  *
@@ -684,11 +844,13 @@ async function encryptPackageData(data, packageKey, keySalt, hashName, blockSize
684
844
  }
685
845
  /** Build the Agile EncryptionInfo XML document. */
686
846
  function buildEncryptionInfoXml(params) {
687
- const { keyBits, hashAlgorithm, hashSize, spinCount, blockSize, keySalt, encryptedVerifierHashInput, encryptedVerifierHashValue, encryptedKeyValue } = params;
847
+ const { keyBits, hashAlgorithm, hashSize, spinCount, blockSize, keySalt, encryptedVerifierHashInput, encryptedVerifierHashValue, encryptedKeyValue, encryptedHmacKey, encryptedHmacValue } = params;
688
848
  const saltB64 = (0, internal_utils_1.bytesToBase64)(keySalt);
689
849
  const vhiB64 = (0, internal_utils_1.bytesToBase64)(encryptedVerifierHashInput);
690
850
  const vhvB64 = (0, internal_utils_1.bytesToBase64)(encryptedVerifierHashValue);
691
851
  const ekvB64 = (0, internal_utils_1.bytesToBase64)(encryptedKeyValue);
852
+ const hmacKeyB64 = (0, internal_utils_1.bytesToBase64)(encryptedHmacKey);
853
+ const hmacValB64 = (0, internal_utils_1.bytesToBase64)(encryptedHmacValue);
692
854
  return ('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\r\n' +
693
855
  '<encryption xmlns="http://schemas.microsoft.com/office/2006/encryption" ' +
694
856
  'xmlns:p="http://schemas.microsoft.com/office/2006/keyEncryptor/password" ' +
@@ -696,7 +858,7 @@ function buildEncryptionInfoXml(params) {
696
858
  `<keyData saltSize="16" blockSize="${blockSize}" keyBits="${keyBits}" ` +
697
859
  `hashSize="${hashSize}" cipherAlgorithm="AES" cipherChaining="ChainingModeCBC" ` +
698
860
  `hashAlgorithm="${hashAlgorithm}" saltValue="${saltB64}"/>\r\n` +
699
- '<dataIntegrity encryptedHmacKey="" encryptedHmacValue=""/>\r\n' +
861
+ `<dataIntegrity encryptedHmacKey="${hmacKeyB64}" encryptedHmacValue="${hmacValB64}"/>\r\n` +
700
862
  "<keyEncryptors>\r\n" +
701
863
  '<keyEncryptor uri="http://schemas.microsoft.com/office/2006/keyEncryptor/password">\r\n' +
702
864
  `<p:encryptedKey spinCount="${spinCount}" saltSize="16" blockSize="${blockSize}" ` +
@@ -35,10 +35,16 @@ exports.tablePctToPercent = tablePctToPercent;
35
35
  exports.TWIPS_PER_INCH = 1440;
36
36
  /** Twips per point. */
37
37
  exports.TWIPS_PER_POINT = 20;
38
- /** Twips per centimeter (approximate). */
39
- exports.TWIPS_PER_CM = 567;
40
- /** Twips per millimeter (approximate). */
41
- exports.TWIPS_PER_MM = 56.7;
38
+ /**
39
+ * Twips per centimeter, derived exactly from 1 inch = 2.54 cm = 1440 twips
40
+ * (= 566.9291…). Using the exact factor — rather than the rounded 567 — keeps
41
+ * metric page sizes aligned with their canonical twip values, e.g.
42
+ * `cmToTwips(21)` → 11906 and `cmToTwips(29.7)` → 16838 (A4), matching
43
+ * `A4_PAGE_WIDTH` / `A4_PAGE_HEIGHT` in constants.ts.
44
+ */
45
+ exports.TWIPS_PER_CM = exports.TWIPS_PER_INCH / 2.54;
46
+ /** Twips per millimeter, derived exactly from {@link TWIPS_PER_CM}. */
47
+ exports.TWIPS_PER_MM = exports.TWIPS_PER_CM / 10;
42
48
  /** EMU (English Metric Units) per inch — DrawingML coordinate space. */
43
49
  exports.EMU_PER_INCH = 914400;
44
50
  /** EMU per centimeter. */
@@ -47,6 +47,7 @@ function renderSdt(xml, sdt, ctx, helpers) {
47
47
  ? {
48
48
  imageRemap: ctx.imageRIdRemap,
49
49
  hyperlinkRIds: ctx.hyperlinkRIds,
50
+ nextDocPrId: ctx.ids.nextDocPrId,
50
51
  rawXmlPolicy: ctx.rawXmlPolicy
51
52
  }
52
53
  : undefined;
@@ -83,6 +84,7 @@ function renderBodyContent(xml, content, ctx) {
83
84
  const helpers = {
84
85
  imageRemap: renderCtx.imageRIdRemap,
85
86
  hyperlinkRIds: renderCtx.hyperlinkRIds,
87
+ nextDocPrId: renderCtx.ids.nextDocPrId,
86
88
  rawXmlPolicy: renderCtx.rawXmlPolicy
87
89
  };
88
90
  switch (content.type) {
@@ -93,7 +95,7 @@ function renderBodyContent(xml, content, ctx) {
93
95
  (0, table_writer_1.renderTable)(xml, content, helpers);
94
96
  break;
95
97
  case "floatingImage":
96
- (0, image_writer_1.renderFloatingImage)(xml, content, renderCtx.imageRIdRemap);
98
+ (0, image_writer_1.renderFloatingImage)(xml, content, renderCtx.imageRIdRemap, renderCtx.ids.nextDocPrId);
97
99
  break;
98
100
  case "tableOfContents":
99
101
  (0, toc_writer_1.renderTableOfContents)(xml, content);
@@ -108,7 +110,13 @@ function renderBodyContent(xml, content, ctx) {
108
110
  renderSdt(xml, content, renderCtx);
109
111
  break;
110
112
  case "checkBox":
113
+ // A checkbox renders as an inline (run-level) SDT whose sdtContent holds
114
+ // a run. At block level that run would be an illegal child of
115
+ // CT_SdtContentBlock, so wrap it in a paragraph — making it a valid
116
+ // run-level SDT inside a block-level paragraph.
117
+ xml.openNode("w:p");
111
118
  (0, checkbox_writer_1.renderCheckBox)(xml, content);
119
+ xml.closeNode();
112
120
  break;
113
121
  case "drawingShape":
114
122
  renderDrawingShape(xml, content, renderCtx);
@@ -295,7 +303,17 @@ function renderDrawingShape(xml, shape, ctx) {
295
303
  // Shape properties
296
304
  xml.openNode("wps:spPr");
297
305
  // Transform
298
- xml.openNode("a:xfrm", shape.rotation ? { rot: String(shape.rotation) } : {});
306
+ const xfrmAttrs = {};
307
+ if (shape.rotation) {
308
+ xfrmAttrs["rot"] = String(shape.rotation);
309
+ }
310
+ if (shape.flipHorizontal) {
311
+ xfrmAttrs["flipH"] = "1";
312
+ }
313
+ if (shape.flipVertical) {
314
+ xfrmAttrs["flipV"] = "1";
315
+ }
316
+ xml.openNode("a:xfrm", Object.keys(xfrmAttrs).length > 0 ? xfrmAttrs : {});
299
317
  xml.leafNode("a:off", { x: "0", y: "0" });
300
318
  xml.leafNode("a:ext", { cx: String(shape.width), cy: String(shape.height) });
301
319
  xml.closeNode(); // a:xfrm
@@ -371,6 +389,7 @@ function renderDrawingShape(xml, shape, ctx) {
371
389
  ? {
372
390
  imageRemap: ctx.imageRIdRemap,
373
391
  hyperlinkRIds: ctx.hyperlinkRIds,
392
+ nextDocPrId: ctx.ids.nextDocPrId,
374
393
  rawXmlPolicy: ctx.rawXmlPolicy
375
394
  }
376
395
  : undefined;
@@ -380,8 +399,13 @@ function renderDrawingShape(xml, shape, ctx) {
380
399
  xml.closeNode(); // w:txbxContent
381
400
  xml.closeNode(); // wps:txbx
382
401
  }
383
- // Body properties (required)
384
- xml.leafNode("wps:bodyPr");
402
+ // Body properties (required). The vertical text anchor lives on a:bodyPr/@anchor.
403
+ if (shape.textBodyAnchor) {
404
+ xml.leafNode("wps:bodyPr", { anchor: shape.textBodyAnchor });
405
+ }
406
+ else {
407
+ xml.leafNode("wps:bodyPr");
408
+ }
385
409
  xml.closeNode(); // wps:wsp
386
410
  xml.closeNode(); // a:graphicData
387
411
  xml.closeNode(); // a:graphic
@@ -127,6 +127,24 @@ function collectHyperlinks(body) {
127
127
  });
128
128
  return links;
129
129
  }
130
+ /**
131
+ * A minimal valid 1×1 transparent PNG.
132
+ *
133
+ * Used as an automatic raster fallback for SVG images that ship without an
134
+ * explicit `fallbackData`. In OOXML, `a:blip/@r:embed` must reference a raster
135
+ * image — Microsoft Word does not rasterize an SVG referenced directly by
136
+ * `a:blip`, so an SVG without a raster fallback renders as a broken/empty
137
+ * picture. By always pairing the SVG (`asvg:svgBlip`) with a raster blip we
138
+ * guarantee the drawing is renderable everywhere: SVG-aware Word shows the
139
+ * vector, older readers show the raster placeholder.
140
+ */
141
+ const SVG_RASTER_FALLBACK_PNG = Uint8Array.from([
142
+ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
143
+ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4,
144
+ 0x89, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x63, 0x60, 0x00, 0x02, 0x00,
145
+ 0x00, 0x05, 0x00, 0x01, 0xe2, 0x26, 0x05, 0x9b, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44,
146
+ 0xae, 0x42, 0x60, 0x82
147
+ ]);
130
148
  /** Infer a content type for an opaque part based on its file extension. */
131
149
  function inferContentType(ext) {
132
150
  const map = {
@@ -379,7 +397,12 @@ async function _packageDocxInner(doc, options) {
379
397
  };
380
398
  for (const img of doc.images) {
381
399
  const oldRid = img.rId;
382
- if (img.mediaType === "svg" && img.fallbackData) {
400
+ if (img.mediaType === "svg") {
401
+ // SVG must always be paired with a raster fallback that `a:blip`
402
+ // references; Word cannot rasterize an SVG referenced directly by
403
+ // a:blip. If the caller did not provide one, synthesize a minimal
404
+ // transparent PNG so the drawing is still valid and renderable.
405
+ const fallbackData = img.fallbackData ?? SVG_RASTER_FALLBACK_PNG;
383
406
  // Main rId points at the PNG fallback (the raster image consumed by
384
407
  // a:blip).
385
408
  const baseName = img.fileName.replace(/\.[^.]+$/, "");
@@ -395,7 +418,7 @@ async function _packageDocxInner(doc, options) {
395
418
  if (ext) {
396
419
  imageExtensions.add(ext);
397
420
  }
398
- svgFallbacks.push({ fallbackFileName, data: img.fallbackData });
421
+ svgFallbacks.push({ fallbackFileName, data: fallbackData });
399
422
  }
400
423
  else {
401
424
  registerImageRel(oldRid, `media/${img.fileName}`);
@@ -1200,14 +1223,24 @@ async function _packageDocxInner(doc, options) {
1200
1223
  archive.add(constants_1.PartPath.Footnotes,
1201
1224
  // Footnotes are an independent OPC part — their r:id values must
1202
1225
  // resolve against word/_rels/footnotes.xml.rels, not document.xml.rels.
1203
- renderXml(xml => (0, footnote_writer_1.renderFootnotes)(xml, doc.footnotes, { imageRemap: new Map(), hyperlinkRIds, rawXmlPolicy })));
1226
+ renderXml(xml => (0, footnote_writer_1.renderFootnotes)(xml, doc.footnotes, {
1227
+ imageRemap: new Map(),
1228
+ hyperlinkRIds,
1229
+ nextDocPrId: renderCtx.ids.nextDocPrId,
1230
+ rawXmlPolicy
1231
+ })));
1204
1232
  if ((0, relationships_1.getRelationshipCount)(footnoteRels) > 0) {
1205
1233
  archive.add("word/_rels/footnotes.xml.rels", renderXml(xml => (0, relationships_1.renderRelationships)(footnoteRels, xml)));
1206
1234
  }
1207
1235
  }
1208
1236
  // word/endnotes.xml + endnotes.xml.rels
1209
1237
  if (hasEndnotes) {
1210
- archive.add(constants_1.PartPath.Endnotes, renderXml(xml => (0, footnote_writer_1.renderEndnotes)(xml, doc.endnotes, { imageRemap: new Map(), hyperlinkRIds, rawXmlPolicy })));
1238
+ archive.add(constants_1.PartPath.Endnotes, renderXml(xml => (0, footnote_writer_1.renderEndnotes)(xml, doc.endnotes, {
1239
+ imageRemap: new Map(),
1240
+ hyperlinkRIds,
1241
+ nextDocPrId: renderCtx.ids.nextDocPrId,
1242
+ rawXmlPolicy
1243
+ })));
1211
1244
  if ((0, relationships_1.getRelationshipCount)(endnoteRels) > 0) {
1212
1245
  archive.add("word/_rels/endnotes.xml.rels", renderXml(xml => (0, relationships_1.renderRelationships)(endnoteRels, xml)));
1213
1246
  }
@@ -1218,7 +1251,12 @@ async function _packageDocxInner(doc, options) {
1218
1251
  // Comments live in their own OPC part; pass helpers so embedded
1219
1252
  // hyperlinks/images render with the right r:id (the rels manager
1220
1253
  // below registered them under their model-original id).
1221
- renderXml(xml => (0, comment_writer_1.renderComments)(xml, doc.comments, { imageRemap: new Map(), hyperlinkRIds, rawXmlPolicy })));
1254
+ renderXml(xml => (0, comment_writer_1.renderComments)(xml, doc.comments, {
1255
+ imageRemap: new Map(),
1256
+ hyperlinkRIds,
1257
+ nextDocPrId: renderCtx.ids.nextDocPrId,
1258
+ rawXmlPolicy
1259
+ })));
1222
1260
  if ((0, relationships_1.getRelationshipCount)(commentRels) > 0) {
1223
1261
  archive.add("word/_rels/comments.xml.rels", renderXml(xml => (0, relationships_1.renderRelationships)(commentRels, xml)));
1224
1262
  }
@@ -1243,6 +1281,7 @@ async function _packageDocxInner(doc, options) {
1243
1281
  renderXml(xml => (0, header_footer_writer_1.renderHeader)(xml, headerDef.content, {
1244
1282
  imageRemap: new Map(),
1245
1283
  hyperlinkRIds,
1284
+ nextDocPrId: renderCtx.ids.nextDocPrId,
1246
1285
  rawXmlPolicy
1247
1286
  })));
1248
1287
  // Header .rels file
@@ -1266,6 +1305,7 @@ async function _packageDocxInner(doc, options) {
1266
1305
  renderXml(xml => (0, header_footer_writer_1.renderFooter)(xml, footerDef.content, {
1267
1306
  imageRemap: new Map(),
1268
1307
  hyperlinkRIds,
1308
+ nextDocPrId: renderCtx.ids.nextDocPrId,
1269
1309
  rawXmlPolicy
1270
1310
  })));
1271
1311
  // Footer .rels file
@@ -10,8 +10,8 @@ exports.renderFloatingImage = renderFloatingImage;
10
10
  const constants_1 = require("../constants");
11
11
  const units_1 = require("../units");
12
12
  /** Render a floating image as a standalone paragraph with wp:anchor. */
13
- function renderFloatingImage(xml, img, imageRemap) {
14
- const drawingId = img.drawingId ?? 1;
13
+ function renderFloatingImage(xml, img, imageRemap, nextDocPrId) {
14
+ const drawingId = nextDocPrId?.() ?? img.drawingId ?? 1;
15
15
  const name = img.name ?? "Picture";
16
16
  // Resolve relationship id used in r:embed via packager-provided remap.
17
17
  const embedRId = imageRemap?.get(img.rId) ?? img.rId;
@@ -323,8 +323,8 @@ function renderShading(xml, shd) {
323
323
  });
324
324
  }
325
325
  /** Render an inline image (w:drawing > wp:inline). */
326
- function renderInlineImage(xml, img, imageRemap) {
327
- const drawingId = img.drawingId ?? 1;
326
+ function renderInlineImage(xml, img, imageRemap, nextDocPrId) {
327
+ const drawingId = nextDocPrId?.() ?? img.drawingId ?? 1;
328
328
  const name = img.name ?? "Picture";
329
329
  // Resolve the relationship id used in r:embed: prefer a packager-provided
330
330
  // remap (used when the model rId clashed with an existing relationship in
@@ -488,7 +488,11 @@ function renderFfData(xml, ff) {
488
488
  }
489
489
  if (ff.entries) {
490
490
  for (const entry of ff.entries) {
491
- xml.leafNode("w:listEntry", { "w:val": entry });
491
+ // Word rejects FORMDROPDOWN list entries with an empty value
492
+ // ("Word experienced an error trying to open the file"). Substitute a
493
+ // single space so an intended blank/placeholder item still renders and
494
+ // the entry indices (and `w:default`) stay aligned.
495
+ xml.leafNode("w:listEntry", { "w:val": entry === "" ? " " : entry });
492
496
  }
493
497
  }
494
498
  xml.closeNode();
@@ -675,7 +679,7 @@ function renderRunContent(xml, content, helpers) {
675
679
  }
676
680
  return true;
677
681
  }
678
- renderInlineImage(xml, content, helpers?.imageRemap);
682
+ renderInlineImage(xml, content, helpers?.imageRemap, helpers?.nextDocPrId);
679
683
  return true;
680
684
  case "field":
681
685
  // Fields create their own runs — must be rendered outside the current run