@cj-tech-master/excelts 9.1.0 → 9.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/README.md +16 -1
  2. package/dist/browser/modules/archive/compression/crc32.js +1 -1
  3. package/dist/browser/modules/archive/crypto/aes.d.ts +0 -8
  4. package/dist/browser/modules/archive/crypto/aes.js +1 -20
  5. package/dist/browser/modules/archive/crypto/index.d.ts +2 -1
  6. package/dist/browser/modules/archive/crypto/index.js +3 -1
  7. package/dist/browser/modules/csv/parse/row-processor.d.ts +1 -1
  8. package/dist/browser/modules/csv/worker/worker-script.generated.js +1 -1
  9. package/dist/browser/modules/excel/utils/cell-matrix.js +1 -0
  10. package/dist/browser/modules/excel/utils/encryptor.browser.d.ts +4 -5
  11. package/dist/browser/modules/excel/utils/encryptor.browser.js +7 -12
  12. package/dist/browser/modules/excel/utils/encryptor.d.ts +1 -1
  13. package/dist/browser/modules/excel/utils/encryptor.js +4 -7
  14. package/dist/browser/modules/pdf/builder/document-builder.d.ts +517 -0
  15. package/dist/browser/modules/pdf/builder/document-builder.js +1493 -0
  16. package/dist/browser/modules/pdf/builder/form-appearance.d.ts +56 -0
  17. package/dist/browser/modules/pdf/builder/form-appearance.js +140 -0
  18. package/dist/browser/modules/pdf/builder/image-utils.d.ts +39 -0
  19. package/dist/browser/modules/pdf/builder/image-utils.js +129 -0
  20. package/dist/browser/modules/pdf/builder/pdf-editor.d.ts +230 -0
  21. package/dist/browser/modules/pdf/builder/pdf-editor.js +1574 -0
  22. package/dist/browser/modules/pdf/builder/resource-merger.d.ts +41 -0
  23. package/dist/browser/modules/pdf/builder/resource-merger.js +258 -0
  24. package/dist/browser/modules/pdf/core/digital-signature.d.ts +109 -0
  25. package/dist/browser/modules/pdf/core/digital-signature.js +659 -0
  26. package/dist/browser/modules/pdf/core/encryption.js +8 -7
  27. package/dist/browser/modules/pdf/core/pdf-object.d.ts +11 -0
  28. package/dist/browser/modules/pdf/core/pdf-object.js +38 -0
  29. package/dist/browser/modules/pdf/core/pdf-stream.d.ts +32 -0
  30. package/dist/browser/modules/pdf/core/pdf-stream.js +66 -0
  31. package/dist/browser/modules/pdf/core/pdf-writer.d.ts +55 -1
  32. package/dist/browser/modules/pdf/core/pdf-writer.js +271 -6
  33. package/dist/browser/modules/pdf/core/pdfa.d.ts +62 -0
  34. package/dist/browser/modules/pdf/core/pdfa.js +261 -0
  35. package/dist/browser/modules/pdf/index.d.ts +11 -0
  36. package/dist/browser/modules/pdf/index.js +9 -0
  37. package/dist/browser/modules/pdf/reader/bookmark-extractor.d.ts +35 -0
  38. package/dist/browser/modules/pdf/reader/bookmark-extractor.js +324 -0
  39. package/dist/browser/modules/pdf/reader/pdf-decrypt.js +6 -5
  40. package/dist/browser/modules/pdf/reader/pdf-reader.d.ts +17 -0
  41. package/dist/browser/modules/pdf/reader/pdf-reader.js +26 -2
  42. package/dist/browser/modules/pdf/reader/table-extractor.d.ts +69 -0
  43. package/dist/browser/modules/pdf/reader/table-extractor.js +365 -0
  44. package/dist/browser/modules/pdf/render/layout-engine.d.ts +21 -1
  45. package/dist/browser/modules/pdf/render/layout-engine.js +112 -5
  46. package/dist/browser/modules/pdf/render/page-renderer.d.ts +2 -9
  47. package/dist/browser/modules/pdf/render/page-renderer.js +62 -103
  48. package/dist/browser/modules/pdf/render/pdf-exporter.js +2 -61
  49. package/dist/browser/modules/pdf/render/style-converter.d.ts +4 -0
  50. package/dist/browser/modules/pdf/render/style-converter.js +1 -1
  51. package/dist/browser/modules/pdf/types.d.ts +14 -1
  52. package/dist/browser/modules/stream/browser/readable.js +8 -2
  53. package/dist/browser/utils/crypto.browser.d.ts +64 -0
  54. package/dist/browser/{modules/pdf/core/crypto.js → utils/crypto.browser.js} +91 -101
  55. package/dist/browser/utils/crypto.d.ts +97 -0
  56. package/dist/browser/utils/crypto.js +209 -0
  57. package/dist/cjs/modules/archive/compression/crc32.js +1 -1
  58. package/dist/cjs/modules/archive/crypto/aes.js +2 -23
  59. package/dist/cjs/modules/archive/crypto/index.js +3 -1
  60. package/dist/cjs/modules/csv/worker/worker-script.generated.js +1 -1
  61. package/dist/cjs/modules/excel/utils/cell-matrix.js +1 -0
  62. package/dist/cjs/modules/excel/utils/encryptor.browser.js +7 -12
  63. package/dist/cjs/modules/excel/utils/encryptor.js +4 -10
  64. package/dist/cjs/modules/pdf/builder/document-builder.js +1532 -0
  65. package/dist/cjs/modules/pdf/builder/form-appearance.js +145 -0
  66. package/dist/cjs/modules/pdf/builder/image-utils.js +135 -0
  67. package/dist/cjs/modules/pdf/builder/pdf-editor.js +1612 -0
  68. package/dist/cjs/modules/pdf/builder/resource-merger.js +263 -0
  69. package/dist/cjs/modules/pdf/core/digital-signature.js +667 -0
  70. package/dist/cjs/modules/pdf/core/encryption.js +8 -7
  71. package/dist/cjs/modules/pdf/core/pdf-object.js +38 -0
  72. package/dist/cjs/modules/pdf/core/pdf-stream.js +66 -0
  73. package/dist/cjs/modules/pdf/core/pdf-writer.js +272 -6
  74. package/dist/cjs/modules/pdf/core/pdfa.js +266 -0
  75. package/dist/cjs/modules/pdf/index.js +19 -1
  76. package/dist/cjs/modules/pdf/reader/bookmark-extractor.js +327 -0
  77. package/dist/cjs/modules/pdf/reader/pdf-decrypt.js +6 -5
  78. package/dist/cjs/modules/pdf/reader/pdf-reader.js +26 -2
  79. package/dist/cjs/modules/pdf/reader/table-extractor.js +368 -0
  80. package/dist/cjs/modules/pdf/render/layout-engine.js +113 -4
  81. package/dist/cjs/modules/pdf/render/page-renderer.js +63 -105
  82. package/dist/cjs/modules/pdf/render/pdf-exporter.js +3 -62
  83. package/dist/cjs/modules/pdf/render/style-converter.js +1 -0
  84. package/dist/cjs/modules/stream/browser/readable.js +8 -2
  85. package/dist/cjs/{modules/pdf/core/crypto.js → utils/crypto.browser.js} +95 -102
  86. package/dist/cjs/utils/crypto.js +228 -0
  87. package/dist/esm/modules/archive/compression/crc32.js +1 -1
  88. package/dist/esm/modules/archive/crypto/aes.js +1 -20
  89. package/dist/esm/modules/archive/crypto/index.js +3 -1
  90. package/dist/esm/modules/csv/worker/worker-script.generated.js +1 -1
  91. package/dist/esm/modules/excel/utils/cell-matrix.js +1 -0
  92. package/dist/esm/modules/excel/utils/encryptor.browser.js +7 -12
  93. package/dist/esm/modules/excel/utils/encryptor.js +4 -7
  94. package/dist/esm/modules/pdf/builder/document-builder.js +1493 -0
  95. package/dist/esm/modules/pdf/builder/form-appearance.js +140 -0
  96. package/dist/esm/modules/pdf/builder/image-utils.js +129 -0
  97. package/dist/esm/modules/pdf/builder/pdf-editor.js +1574 -0
  98. package/dist/esm/modules/pdf/builder/resource-merger.js +258 -0
  99. package/dist/esm/modules/pdf/core/digital-signature.js +659 -0
  100. package/dist/esm/modules/pdf/core/encryption.js +8 -7
  101. package/dist/esm/modules/pdf/core/pdf-object.js +38 -0
  102. package/dist/esm/modules/pdf/core/pdf-stream.js +66 -0
  103. package/dist/esm/modules/pdf/core/pdf-writer.js +271 -6
  104. package/dist/esm/modules/pdf/core/pdfa.js +261 -0
  105. package/dist/esm/modules/pdf/index.js +9 -0
  106. package/dist/esm/modules/pdf/reader/bookmark-extractor.js +324 -0
  107. package/dist/esm/modules/pdf/reader/pdf-decrypt.js +6 -5
  108. package/dist/esm/modules/pdf/reader/pdf-reader.js +26 -2
  109. package/dist/esm/modules/pdf/reader/table-extractor.js +365 -0
  110. package/dist/esm/modules/pdf/render/layout-engine.js +112 -5
  111. package/dist/esm/modules/pdf/render/page-renderer.js +62 -103
  112. package/dist/esm/modules/pdf/render/pdf-exporter.js +2 -61
  113. package/dist/esm/modules/pdf/render/style-converter.js +1 -1
  114. package/dist/esm/modules/stream/browser/readable.js +8 -2
  115. package/dist/esm/{modules/pdf/core/crypto.js → utils/crypto.browser.js} +91 -101
  116. package/dist/esm/utils/crypto.js +209 -0
  117. package/dist/iife/excelts.iife.js +1248 -1074
  118. package/dist/iife/excelts.iife.js.map +1 -1
  119. package/dist/iife/excelts.iife.min.js +53 -54
  120. package/dist/types/modules/archive/crypto/aes.d.ts +0 -8
  121. package/dist/types/modules/archive/crypto/index.d.ts +2 -1
  122. package/dist/types/modules/csv/parse/row-processor.d.ts +1 -1
  123. package/dist/types/modules/excel/utils/encryptor.browser.d.ts +4 -5
  124. package/dist/types/modules/excel/utils/encryptor.d.ts +1 -1
  125. package/dist/types/modules/pdf/builder/document-builder.d.ts +517 -0
  126. package/dist/types/modules/pdf/builder/form-appearance.d.ts +56 -0
  127. package/dist/types/modules/pdf/builder/image-utils.d.ts +39 -0
  128. package/dist/types/modules/pdf/builder/pdf-editor.d.ts +230 -0
  129. package/dist/types/modules/pdf/builder/resource-merger.d.ts +41 -0
  130. package/dist/types/modules/pdf/core/digital-signature.d.ts +109 -0
  131. package/dist/types/modules/pdf/core/pdf-object.d.ts +11 -0
  132. package/dist/types/modules/pdf/core/pdf-stream.d.ts +32 -0
  133. package/dist/types/modules/pdf/core/pdf-writer.d.ts +55 -1
  134. package/dist/types/modules/pdf/core/pdfa.d.ts +62 -0
  135. package/dist/types/modules/pdf/index.d.ts +11 -0
  136. package/dist/types/modules/pdf/reader/bookmark-extractor.d.ts +35 -0
  137. package/dist/types/modules/pdf/reader/pdf-reader.d.ts +17 -0
  138. package/dist/types/modules/pdf/reader/table-extractor.d.ts +69 -0
  139. package/dist/types/modules/pdf/render/layout-engine.d.ts +21 -1
  140. package/dist/types/modules/pdf/render/page-renderer.d.ts +2 -9
  141. package/dist/types/modules/pdf/render/style-converter.d.ts +4 -0
  142. package/dist/types/modules/pdf/types.d.ts +14 -1
  143. package/dist/types/utils/crypto.browser.d.ts +64 -0
  144. package/dist/types/utils/crypto.d.ts +97 -0
  145. package/package.json +110 -111
  146. package/dist/browser/modules/pdf/core/crypto.d.ts +0 -65
  147. package/dist/types/modules/pdf/core/crypto.d.ts +0 -65
@@ -17,7 +17,8 @@
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
18
  exports.initEncryption = initEncryption;
19
19
  exports.encryptData = encryptData;
20
- const crypto_1 = require("./crypto");
20
+ const crypto_1 = require("../../../utils/crypto.js");
21
+ const binary_1 = require("../../../utils/binary.js");
21
22
  // =============================================================================
22
23
  // Public API
23
24
  // =============================================================================
@@ -37,22 +38,22 @@ function initEncryption(options) {
37
38
  const oKeySalt = (0, crypto_1.randomBytes)(8);
38
39
  // Step 3: Compute U value
39
40
  // U hash = SHA-256(userPassword + uValidationSalt)
40
- const uHash = (0, crypto_1.sha256)((0, crypto_1.concatArrays)(userPwd, uValidationSalt));
41
- const uValue = (0, crypto_1.concatArrays)(uHash, uValidationSalt, uKeySalt);
41
+ const uHash = (0, crypto_1.sha256)((0, binary_1.concatUint8Arrays)([userPwd, uValidationSalt]));
42
+ const uValue = (0, binary_1.concatUint8Arrays)([uHash, uValidationSalt, uKeySalt]);
42
43
  // Step 4: Compute UE value
43
44
  // UE = AES-256-CBC-encrypt(encryptionKey, SHA-256(userPassword + uKeySalt), zeroIV)
44
45
  // Actually: the key for encrypting UE is SHA-256(password + key_salt),
45
46
  // and we encrypt the file encryption key with it.
46
- const ueKey = (0, crypto_1.sha256)((0, crypto_1.concatArrays)(userPwd, uKeySalt));
47
+ const ueKey = (0, crypto_1.sha256)((0, binary_1.concatUint8Arrays)([userPwd, uKeySalt]));
47
48
  const zeroIv = new Uint8Array(16);
48
49
  const ueValue = (0, crypto_1.aesCbcEncryptRaw)(encryptionKey, ueKey, zeroIv);
49
50
  // Step 5: Compute O value
50
51
  // O hash = SHA-256(ownerPassword + oValidationSalt + U(0..47))
51
- const oHash = (0, crypto_1.sha256)((0, crypto_1.concatArrays)(ownerPwd, oValidationSalt, uValue));
52
- const oValue = (0, crypto_1.concatArrays)(oHash, oValidationSalt, oKeySalt);
52
+ const oHash = (0, crypto_1.sha256)((0, binary_1.concatUint8Arrays)([ownerPwd, oValidationSalt, uValue]));
53
+ const oValue = (0, binary_1.concatUint8Arrays)([oHash, oValidationSalt, oKeySalt]);
53
54
  // Step 6: Compute OE value
54
55
  // OE = AES-256-CBC-encrypt(encryptionKey, SHA-256(ownerPassword + oKeySalt + U(0..47)), zeroIV)
55
- const oeKey = (0, crypto_1.sha256)((0, crypto_1.concatArrays)(ownerPwd, oKeySalt, uValue));
56
+ const oeKey = (0, crypto_1.sha256)((0, binary_1.concatUint8Arrays)([ownerPwd, oKeySalt, uValue]));
56
57
  const oeValue = (0, crypto_1.aesCbcEncryptRaw)(encryptionKey, oeKey, zeroIv);
57
58
  // Step 7: Compute Perms value
58
59
  // 16-byte block: P(4 LE bytes) + 0xFF(4 bytes) + 'T' or 'F' (encryptMetadata) + 'a' 'd' 'b' + 0(3 bytes)
@@ -153,12 +153,37 @@ function encodePdfUtf16String(value) {
153
153
  class PdfDict {
154
154
  constructor() {
155
155
  this.entries = [];
156
+ /** When set, toString() returns this raw string. set() appends/replaces entries within it. */
157
+ this._raw = null;
158
+ }
159
+ /**
160
+ * Create a PdfDict that wraps a pre-serialized dictionary string.
161
+ * toString() returns the raw string (with any set() overrides applied).
162
+ */
163
+ static fromRawString(raw) {
164
+ const d = new PdfDict();
165
+ d._raw = raw;
166
+ return d;
156
167
  }
157
168
  /**
158
169
  * Set a dictionary entry. The key should NOT include the leading /.
159
170
  * The value should be a pre-serialized PDF value string.
160
171
  */
161
172
  set(key, value) {
173
+ if (this._raw !== null) {
174
+ // Update or append the entry in the raw string.
175
+ // Use a regex that matches /Key followed by a simple token value (number, name, ref).
176
+ // This is safe for keys like /Length, /Filter which have simple values.
177
+ const keyPattern = new RegExp(`(/${key})\\s+\\S+(?:\\s+\\d+\\s+R)?`);
178
+ if (keyPattern.test(this._raw)) {
179
+ this._raw = this._raw.replace(keyPattern, `/${key} ${value}`);
180
+ }
181
+ else {
182
+ // Append before the closing >>
183
+ this._raw = this._raw.replace(/>>$/, `\n${pdfName(key)} ${value}\n>>`);
184
+ }
185
+ return this;
186
+ }
162
187
  const idx = this.entries.findIndex(([k]) => k === key);
163
188
  if (idx >= 0) {
164
189
  this.entries[idx] = [key, value];
@@ -177,10 +202,23 @@ class PdfDict {
177
202
  }
178
203
  return this;
179
204
  }
205
+ /**
206
+ * Remove a dictionary entry by key.
207
+ */
208
+ delete(key) {
209
+ const idx = this.entries.findIndex(([k]) => k === key);
210
+ if (idx >= 0) {
211
+ this.entries.splice(idx, 1);
212
+ }
213
+ return this;
214
+ }
180
215
  /**
181
216
  * Serialize to a PDF dictionary string.
182
217
  */
183
218
  toString() {
219
+ if (this._raw !== null) {
220
+ return this._raw;
221
+ }
184
222
  const parts = ["<<"];
185
223
  for (const [key, value] of this.entries) {
186
224
  parts.push(`${pdfName(key)} ${value}`);
@@ -132,6 +132,30 @@ class PdfContentStream {
132
132
  this.parts.push(`${(0, pdf_object_1.pdfNumber)(x)} ${(0, pdf_object_1.pdfNumber)(y)} ${(0, pdf_object_1.pdfNumber)(width)} ${(0, pdf_object_1.pdfNumber)(height)} re`);
133
133
  return this;
134
134
  }
135
+ /**
136
+ * Append a cubic Bezier curve to the current path.
137
+ * From current point to (x3, y3), with control points (x1, y1) and (x2, y2).
138
+ */
139
+ curveTo(x1, y1, x2, y2, x3, y3) {
140
+ this.parts.push(`${(0, pdf_object_1.pdfNumber)(x1)} ${(0, pdf_object_1.pdfNumber)(y1)} ${(0, pdf_object_1.pdfNumber)(x2)} ${(0, pdf_object_1.pdfNumber)(y2)} ${(0, pdf_object_1.pdfNumber)(x3)} ${(0, pdf_object_1.pdfNumber)(y3)} c`);
141
+ return this;
142
+ }
143
+ /**
144
+ * Append a cubic Bezier curve where the first control point is the current point.
145
+ * From current point to (x3, y3), with control points (current, y1) and (x2, y2).
146
+ */
147
+ curveToV(x2, y2, x3, y3) {
148
+ this.parts.push(`${(0, pdf_object_1.pdfNumber)(x2)} ${(0, pdf_object_1.pdfNumber)(y2)} ${(0, pdf_object_1.pdfNumber)(x3)} ${(0, pdf_object_1.pdfNumber)(y3)} v`);
149
+ return this;
150
+ }
151
+ /**
152
+ * Append a cubic Bezier curve where the second control point equals (x3, y3).
153
+ * From current point to (x3, y3), with control point (x1, y1).
154
+ */
155
+ curveToY(x1, y1, x3, y3) {
156
+ this.parts.push(`${(0, pdf_object_1.pdfNumber)(x1)} ${(0, pdf_object_1.pdfNumber)(y1)} ${(0, pdf_object_1.pdfNumber)(x3)} ${(0, pdf_object_1.pdfNumber)(y3)} y`);
157
+ return this;
158
+ }
135
159
  // ===========================================================================
136
160
  // Path Painting
137
161
  // ===========================================================================
@@ -357,6 +381,48 @@ class PdfContentStream {
357
381
  }
358
382
  return this.moveTo(x1, y1).lineTo(x2, y2).stroke().restore();
359
383
  }
384
+ /**
385
+ * Append an ellipse to the current path using 4 cubic Bezier curves.
386
+ * (cx, cy) is the center; rx, ry are the radii.
387
+ *
388
+ * Uses the standard kappa = 4 * (sqrt(2) - 1) / 3 ≈ 0.5522847 approximation.
389
+ */
390
+ ellipse(cx, cy, rx, ry) {
391
+ const k = 0.5522847;
392
+ const kx = k * rx;
393
+ const ky = k * ry;
394
+ this.moveTo(cx + rx, cy);
395
+ this.curveTo(cx + rx, cy + ky, cx + kx, cy + ry, cx, cy + ry);
396
+ this.curveTo(cx - kx, cy + ry, cx - rx, cy + ky, cx - rx, cy);
397
+ this.curveTo(cx - rx, cy - ky, cx - kx, cy - ry, cx, cy - ry);
398
+ this.curveTo(cx + kx, cy - ry, cx + rx, cy - ky, cx + rx, cy);
399
+ return this;
400
+ }
401
+ /**
402
+ * Append a circle to the current path.
403
+ * (cx, cy) is the center; r is the radius.
404
+ */
405
+ circle(cx, cy, r) {
406
+ return this.ellipse(cx, cy, r, r);
407
+ }
408
+ /**
409
+ * Append a rounded rectangle to the current path.
410
+ * (x, y) is the lower-left corner; r is the corner radius.
411
+ */
412
+ roundedRect(x, y, width, height, r) {
413
+ const k = 0.5522847;
414
+ const kr = k * r;
415
+ this.moveTo(x + r, y);
416
+ this.lineTo(x + width - r, y);
417
+ this.curveTo(x + width - r + kr, y, x + width, y + r - kr, x + width, y + r);
418
+ this.lineTo(x + width, y + height - r);
419
+ this.curveTo(x + width, y + height - r + kr, x + width - r + kr, y + height, x + width - r, y + height);
420
+ this.lineTo(x + r, y + height);
421
+ this.curveTo(x + r - kr, y + height, x, y + height - r + kr, x, y + height - r);
422
+ this.lineTo(x, y + r);
423
+ this.curveTo(x, y + r - kr, x + r - kr, y, x + r, y);
424
+ return this;
425
+ }
360
426
  // ===========================================================================
361
427
  // Serialization
362
428
  // ===========================================================================
@@ -9,12 +9,16 @@
9
9
  * 3. Cross-reference table
10
10
  * 4. Trailer (with document catalog reference)
11
11
  *
12
+ * Also provides {@link buildIncremental} for appending incremental updates
13
+ * to an existing PDF without rewriting the original bytes.
14
+ *
12
15
  * Encryption uses AES-256 (V=5, R=5) per ISO 32000-2:2020.
13
16
  *
14
17
  * @see ISO 32000-2:2020, Chapter 7.5 — File Structure
15
18
  */
16
19
  Object.defineProperty(exports, "__esModule", { value: true });
17
20
  exports.PdfWriter = void 0;
21
+ exports.buildIncremental = buildIncremental;
18
22
  const pdf_object_1 = require("./pdf-object");
19
23
  const errors_1 = require("../errors");
20
24
  const binary_1 = require("../../../utils/binary.js");
@@ -38,6 +42,14 @@ class PdfWriter {
38
42
  this.catalogRef = 0;
39
43
  this.infoRef = 0;
40
44
  this.encryption = null;
45
+ this.pdfVersion = "2.0";
46
+ }
47
+ /**
48
+ * Set the PDF version string (e.g. "1.4", "1.7", "2.0").
49
+ * Default is "2.0".
50
+ */
51
+ setVersion(version) {
52
+ this.pdfVersion = version;
41
53
  }
42
54
  /**
43
55
  * Enable encryption for this document.
@@ -67,10 +79,11 @@ class PdfWriter {
67
79
  content: typeof dict === "string" ? dict : dict.toString()
68
80
  });
69
81
  }
70
- addStreamObject(objectNumber, dict, data) {
82
+ addStreamObject(objectNumber, dict, data, options) {
71
83
  let streamData = data instanceof Uint8Array ? data : data.toUint8Array();
84
+ const compress = options?.compress ?? true;
72
85
  // Compress with zlib (RFC 1950) for PDF /FlateDecode
73
- if (streamData.length > 256 && !dict.toString().includes("/Filter")) {
86
+ if (compress && streamData.length > 256 && !dict.toString().includes("/Filter")) {
74
87
  const compressed = (0, compress_1.zlibSync)(streamData, { level: 6 });
75
88
  if (compressed.length < streamData.length) {
76
89
  dict.set("Filter", "/FlateDecode");
@@ -89,6 +102,22 @@ class PdfWriter {
89
102
  streamData
90
103
  });
91
104
  }
105
+ /**
106
+ * Return all stored objects for inspection (e.g., incremental update remapping).
107
+ * Stream objects include their binary data.
108
+ */
109
+ getObjects() {
110
+ return this.objects.map(o => {
111
+ const result = {
112
+ objectNumber: o.objectNumber,
113
+ content: o.content
114
+ };
115
+ if ("streamData" in o) {
116
+ result.streamData = o.streamData;
117
+ }
118
+ return result;
119
+ });
120
+ }
92
121
  /**
93
122
  * Set the document catalog object number.
94
123
  * This is required and references the root of the document structure.
@@ -147,14 +176,25 @@ class PdfWriter {
147
176
  }
148
177
  /**
149
178
  * Create and add the Catalog dictionary.
179
+ *
180
+ * @param pagesRef - Object number of the Pages tree root
181
+ * @param optionsOrOutlinesRef - Either an outlinesRef number (legacy) or an options object
150
182
  */
151
- addCatalog(pagesRef, outlinesRef) {
183
+ addCatalog(pagesRef, optionsOrOutlinesRef) {
184
+ const resolvedOptions = typeof optionsOrOutlinesRef === "number"
185
+ ? { outlinesRef: optionsOrOutlinesRef }
186
+ : (optionsOrOutlinesRef ?? {});
152
187
  const objNum = this.allocObject();
153
188
  const dict = new pdf_object_1.PdfDict().set("Type", "/Catalog").set("Pages", (0, pdf_object_1.pdfRef)(pagesRef));
154
- if (outlinesRef) {
155
- dict.set("Outlines", (0, pdf_object_1.pdfRef)(outlinesRef));
189
+ if (resolvedOptions.outlinesRef) {
190
+ dict.set("Outlines", (0, pdf_object_1.pdfRef)(resolvedOptions.outlinesRef));
156
191
  dict.set("PageMode", "/UseOutlines");
157
192
  }
193
+ if (resolvedOptions.extraEntries) {
194
+ for (const [key, value] of resolvedOptions.extraEntries) {
195
+ dict.set(key, value);
196
+ }
197
+ }
158
198
  this.addObject(objNum, dict);
159
199
  this.setCatalog(objNum);
160
200
  return objNum;
@@ -174,7 +214,7 @@ class PdfWriter {
174
214
  let byteOffset = 0;
175
215
  // --- Header ---
176
216
  // Include a comment with high bytes to signal binary content per PDF spec §3.4.1
177
- const headerStr = "%PDF-2.0\n";
217
+ const headerStr = `%PDF-${this.pdfVersion}\n`;
178
218
  const headerStrBytes = encoder.encode(headerStr);
179
219
  chunks.push(headerStrBytes);
180
220
  byteOffset += headerStrBytes.length;
@@ -464,3 +504,229 @@ function unescapePdfString(value) {
464
504
  }
465
505
  return result;
466
506
  }
507
+ // =============================================================================
508
+ // Incremental Update
509
+ // =============================================================================
510
+ /**
511
+ * Build an incremental update that appends new/modified objects to an
512
+ * existing PDF without rewriting the original bytes.
513
+ *
514
+ * The result is `originalData + "\n" + new objects + xref + trailer + %%EOF`.
515
+ * The new trailer's `/Prev` points to the original xref offset so that PDF
516
+ * readers can follow the chain of incremental updates.
517
+ *
518
+ * @param originalData - The original, unmodified PDF bytes (preserved byte-for-byte)
519
+ * @param modifiedObjects - Map of object number → serialized content.
520
+ * Values are either a plain string (for non-stream objects) or
521
+ * `{ dict, data }` for stream objects.
522
+ * @param newTrailerEntries - Additional/override entries for the new trailer.
523
+ * Keys like `/Root`, `/Info`, `/Encrypt`, `/ID` are preserved from the
524
+ * original trailer by default but can be overridden here.
525
+ *
526
+ * @see ISO 32000-2:2020, §7.5.6 — Incremental Updates
527
+ */
528
+ function buildIncremental(originalData, modifiedObjects, newTrailerEntries) {
529
+ if (modifiedObjects.size === 0) {
530
+ return originalData;
531
+ }
532
+ const encoder = new TextEncoder();
533
+ // --- Locate the original startxref offset ---
534
+ const oldXrefOffset = findOriginalXrefOffset(originalData);
535
+ // --- Extract original trailer entries we want to preserve ---
536
+ const originalTrailerEntries = extractOriginalTrailerEntries(originalData);
537
+ // --- Determine /Size for the new trailer ---
538
+ // /Size must be one more than the highest object number across original + new
539
+ const originalSize = originalTrailerEntries.get("Size") ?? "0";
540
+ let maxObjNum = parseInt(originalSize, 10) - 1;
541
+ for (const objNum of modifiedObjects.keys()) {
542
+ if (objNum > maxObjNum) {
543
+ maxObjNum = objNum;
544
+ }
545
+ }
546
+ const newSize = maxObjNum + 1;
547
+ // --- Build the appended body ---
548
+ const chunks = [];
549
+ let byteOffset = originalData.length;
550
+ // Start with a newline separator after the original %%EOF
551
+ const separator = encoder.encode("\n");
552
+ chunks.push(separator);
553
+ byteOffset += separator.length;
554
+ // Sort modified objects by object number for deterministic output
555
+ const sortedObjects = [...modifiedObjects.entries()].sort((a, b) => a[0] - b[0]);
556
+ // Track offsets for the xref entries
557
+ const objectOffsets = new Map();
558
+ for (const [objNum, content] of sortedObjects) {
559
+ objectOffsets.set(objNum, byteOffset);
560
+ const objHeader = encoder.encode(`${objNum} 0 obj\n`);
561
+ chunks.push(objHeader);
562
+ byteOffset += objHeader.length;
563
+ if (typeof content === "string") {
564
+ // Non-stream object
565
+ const contentBytes = encoder.encode(content + "\n");
566
+ chunks.push(contentBytes);
567
+ byteOffset += contentBytes.length;
568
+ }
569
+ else {
570
+ // Stream object: dict + stream data
571
+ let streamData = content.data;
572
+ const dict = content.dict;
573
+ // Compress if beneficial and not already filtered
574
+ if (streamData.length > 256 && !dict.toString().includes("/Filter")) {
575
+ const compressed = (0, compress_1.zlibSync)(streamData, { level: 6 });
576
+ if (compressed.length < streamData.length) {
577
+ dict.set("Filter", "/FlateDecode");
578
+ streamData = compressed;
579
+ }
580
+ }
581
+ dict.set("Length", (0, pdf_object_1.pdfNumber)(streamData.length));
582
+ const dictBytes = encoder.encode(dict.toString() + "\n");
583
+ chunks.push(dictBytes);
584
+ byteOffset += dictBytes.length;
585
+ const streamStart = encoder.encode("stream\n");
586
+ chunks.push(streamStart);
587
+ byteOffset += streamStart.length;
588
+ chunks.push(streamData);
589
+ byteOffset += streamData.length;
590
+ const streamEnd = encoder.encode("\nendstream\n");
591
+ chunks.push(streamEnd);
592
+ byteOffset += streamEnd.length;
593
+ }
594
+ const objFooter = encoder.encode("endobj\n");
595
+ chunks.push(objFooter);
596
+ byteOffset += objFooter.length;
597
+ }
598
+ // --- Build the new xref section ---
599
+ const xrefOffset = byteOffset;
600
+ // Group consecutive object numbers into subsections
601
+ const objNums = [...objectOffsets.keys()].sort((a, b) => a - b);
602
+ const subsections = [];
603
+ for (const objNum of objNums) {
604
+ const last = subsections[subsections.length - 1];
605
+ if (last && objNum === last.start + last.entries.length) {
606
+ // Consecutive — extend current subsection
607
+ last.entries.push({ objNum, offset: objectOffsets.get(objNum) });
608
+ }
609
+ else {
610
+ // New subsection
611
+ subsections.push({
612
+ start: objNum,
613
+ entries: [{ objNum, offset: objectOffsets.get(objNum) }]
614
+ });
615
+ }
616
+ }
617
+ let xrefStr = "xref\n";
618
+ for (const sub of subsections) {
619
+ xrefStr += `${sub.start} ${sub.entries.length}\n`;
620
+ for (const entry of sub.entries) {
621
+ const offsetStr = entry.offset.toString().padStart(10, "0");
622
+ xrefStr += `${offsetStr} 00000 n \n`;
623
+ }
624
+ }
625
+ const xrefBytes = encoder.encode(xrefStr);
626
+ chunks.push(xrefBytes);
627
+ // --- Build the new trailer ---
628
+ let trailerStr = "trailer\n<<\n";
629
+ trailerStr += `/Size ${newSize}\n`;
630
+ // Preserve original trailer keys: Root, Info, Encrypt, ID
631
+ for (const key of ["Root", "Info", "Encrypt", "ID"]) {
632
+ if (newTrailerEntries.has(key)) {
633
+ trailerStr += `/${key} ${newTrailerEntries.get(key)}\n`;
634
+ }
635
+ else if (originalTrailerEntries.has(key)) {
636
+ trailerStr += `/${key} ${originalTrailerEntries.get(key)}\n`;
637
+ }
638
+ }
639
+ // Add any extra new trailer entries not already handled
640
+ for (const [key, value] of newTrailerEntries) {
641
+ if (key === "Root" || key === "Info" || key === "Encrypt" || key === "ID" || key === "Size") {
642
+ continue; // Already handled above
643
+ }
644
+ trailerStr += `/${key} ${value}\n`;
645
+ }
646
+ // /Prev points to the original xref offset
647
+ trailerStr += `/Prev ${oldXrefOffset}\n`;
648
+ trailerStr += ">>\n";
649
+ trailerStr += "startxref\n";
650
+ trailerStr += `${xrefOffset}\n`;
651
+ trailerStr += "%%EOF\n";
652
+ const trailerBytes = encoder.encode(trailerStr);
653
+ chunks.push(trailerBytes);
654
+ // --- Concatenate: originalData + appended chunks ---
655
+ return (0, binary_1.concatUint8Arrays)([originalData, ...chunks]);
656
+ }
657
+ /**
658
+ * Find the xref offset stored after the last `startxref` keyword in the PDF.
659
+ */
660
+ function findOriginalXrefOffset(data) {
661
+ // Scan backward from the end to find "startxref"
662
+ const keyword = "startxref";
663
+ const decoder = new TextDecoder("latin1");
664
+ // Search in the last 1024 bytes (%%EOF + startxref are typically near the end)
665
+ const searchStart = Math.max(0, data.length - 1024);
666
+ const tail = decoder.decode(data.subarray(searchStart));
667
+ const idx = tail.lastIndexOf(keyword);
668
+ if (idx < 0) {
669
+ throw new errors_1.PdfStructureError("Could not find startxref in original PDF");
670
+ }
671
+ // Extract the number after "startxref"
672
+ const afterKeyword = tail.substring(idx + keyword.length).trim();
673
+ const match = afterKeyword.match(/^(\d+)/);
674
+ if (!match) {
675
+ throw new errors_1.PdfStructureError("Invalid startxref offset in original PDF");
676
+ }
677
+ return parseInt(match[1], 10);
678
+ }
679
+ /**
680
+ * Extract key trailer entries from the original PDF as serialized strings.
681
+ * This is a lightweight scan — it doesn't fully parse the trailer, just
682
+ * extracts the values we need for preservation.
683
+ */
684
+ function extractOriginalTrailerEntries(data) {
685
+ const entries = new Map();
686
+ const decoder = new TextDecoder("latin1");
687
+ // Find the last "trailer" keyword — scan backward
688
+ const text = decoder.decode(data);
689
+ // Find the last trailer dict. For PDFs with incremental updates,
690
+ // we want the most recent (last) trailer.
691
+ const trailerIdx = text.lastIndexOf("trailer");
692
+ if (trailerIdx < 0) {
693
+ // Could be an xref stream PDF — no traditional trailer
694
+ return entries;
695
+ }
696
+ // Find the << >> dict after "trailer"
697
+ const afterTrailer = text.substring(trailerIdx + 7);
698
+ const dictStart = afterTrailer.indexOf("<<");
699
+ if (dictStart < 0) {
700
+ return entries;
701
+ }
702
+ // Find the matching >>
703
+ let depth = 0;
704
+ let dictEnd = -1;
705
+ for (let i = dictStart; i < afterTrailer.length - 1; i++) {
706
+ if (afterTrailer[i] === "<" && afterTrailer[i + 1] === "<") {
707
+ depth++;
708
+ i++;
709
+ }
710
+ else if (afterTrailer[i] === ">" && afterTrailer[i + 1] === ">") {
711
+ depth--;
712
+ i++;
713
+ if (depth === 0) {
714
+ dictEnd = i + 1;
715
+ break;
716
+ }
717
+ }
718
+ }
719
+ if (dictEnd < 0) {
720
+ return entries;
721
+ }
722
+ const dictStr = afterTrailer.substring(dictStart, dictEnd);
723
+ // Extract known keys with a simple regex-based approach
724
+ for (const key of ["Root", "Info", "Encrypt", "ID", "Size"]) {
725
+ const keyPattern = new RegExp(`/${key}\\s+(.+?)(?=\\s*/[A-Z]|\\s*>>)`, "s");
726
+ const match = dictStr.match(keyPattern);
727
+ if (match) {
728
+ entries.set(key, match[1].trim());
729
+ }
730
+ }
731
+ return entries;
732
+ }