@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
@@ -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 { aesCbcDecrypt as aesCbcDecryptPkcs7Sync, aesCbcDecryptRaw as aesCbcDecryptRawSync, aesCbcEncrypt as aesCbcEncryptPkcs7Sync, aesCbcEncryptRaw as aesCbcEncryptRawSync, hash as hashSyncMaybe, hashAsync } from "../../../utils/crypto.js";
23
+ import { aesCbcDecryptRaw as aesCbcDecryptRawSync, aesCbcEncryptRaw as aesCbcEncryptRawSync, hash as hashSyncMaybe, hashAsync } from "../../../utils/crypto.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 (PKCS#7 padded by encryption write path)
143
- const verifierInput = aesCbcPkcs7Decrypt(info.encryptedVerifierHashInput, verifierInputKey, info.keySalt);
144
- // Hash the verifier input
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 (PKCS#7 padded by encryption write path)
150
- const expectedHash = aesCbcPkcs7Decrypt(info.encryptedVerifierHashValue, verifierValueKey, info.keySalt);
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 (PKCS#7 padded by encryption write path)
174
- const packageKey = aesCbcPkcs7Decrypt(info.encryptedKeyValue, keyEncryptionKey, info.keySalt);
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 whose plaintext was written with PKCS#7 padding.
259
- * Used for the encryptedKeyValue / encryptedVerifierHashInput /
260
- * encryptedVerifierHashValue blobs. The IV is truncated/extended to 16
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 aesCbcPkcs7Decrypt(data, key, iv) {
264
- return aesCbcDecryptPkcs7Sync(data, key, ivToBlockSize(iv));
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 to a 16-byte boundary by
276
- * the caller, and the on-disk format does not include a PKCS#7 trailer.
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
- const encryptedKeyValue = aesCbcPkcs7Encrypt(packageKey, keyEncryptionKey, keySalt);
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 = aesCbcPkcs7Encrypt(verifierInput, verifierInputKey, keySalt);
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 = aesCbcPkcs7Encrypt(verifierHash, verifierValueKey, keySalt);
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
- '<dataIntegrity encryptedHmacKey="" encryptedHmacValue=""/>\r\n' +
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}" ` +
@@ -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
- /** Twips per centimeter (approximate). */
14
- export const TWIPS_PER_CM = 567;
15
- /** Twips per millimeter (approximate). */
16
- export const TWIPS_PER_MM = 56.7;
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
- xml.openNode("a:xfrm", shape.rotation ? { rot: String(shape.rotation) } : {});
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
- xml.leafNode("wps:bodyPr");
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" && img.fallbackData) {
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: img.fallbackData });
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, { imageRemap: new Map(), hyperlinkRIds, rawXmlPolicy })));
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, { imageRemap: new Map(), hyperlinkRIds, rawXmlPolicy })));
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, { imageRemap: new Map(), hyperlinkRIds, rawXmlPolicy })));
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,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;
@@ -316,8 +316,8 @@ export function renderShading(xml, shd) {
316
316
  });
317
317
  }
318
318
  /** Render an inline image (w:drawing > wp:inline). */
319
- function renderInlineImage(xml, img, imageRemap) {
320
- const drawingId = img.drawingId ?? 1;
319
+ function renderInlineImage(xml, img, imageRemap, nextDocPrId) {
320
+ const drawingId = nextDocPrId?.() ?? img.drawingId ?? 1;
321
321
  const name = img.name ?? "Picture";
322
322
  // Resolve the relationship id used in r:embed: prefer a packager-provided
323
323
  // remap (used when the model rId clashed with an existing relationship in
@@ -481,7 +481,11 @@ function renderFfData(xml, ff) {
481
481
  }
482
482
  if (ff.entries) {
483
483
  for (const entry of ff.entries) {
484
- xml.leafNode("w:listEntry", { "w:val": entry });
484
+ // Word rejects FORMDROPDOWN list entries with an empty value
485
+ // ("Word experienced an error trying to open the file"). Substitute a
486
+ // single space so an intended blank/placeholder item still renders and
487
+ // the entry indices (and `w:default`) stay aligned.
488
+ xml.leafNode("w:listEntry", { "w:val": entry === "" ? " " : entry });
485
489
  }
486
490
  }
487
491
  xml.closeNode();
@@ -668,7 +672,7 @@ function renderRunContent(xml, content, helpers) {
668
672
  }
669
673
  return true;
670
674
  }
671
- renderInlineImage(xml, content, helpers?.imageRemap);
675
+ renderInlineImage(xml, content, helpers?.imageRemap, helpers?.nextDocPrId);
672
676
  return true;
673
677
  case "field":
674
678
  // Fields create their own runs — must be rendered outside the current run