@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.
- package/README.md +16 -1
- package/dist/browser/modules/archive/compression/crc32.js +1 -1
- package/dist/browser/modules/archive/crypto/aes.d.ts +0 -8
- package/dist/browser/modules/archive/crypto/aes.js +1 -20
- package/dist/browser/modules/archive/crypto/index.d.ts +2 -1
- package/dist/browser/modules/archive/crypto/index.js +3 -1
- package/dist/browser/modules/csv/parse/row-processor.d.ts +1 -1
- package/dist/browser/modules/csv/worker/worker-script.generated.js +1 -1
- package/dist/browser/modules/excel/utils/cell-matrix.js +1 -0
- package/dist/browser/modules/excel/utils/encryptor.browser.d.ts +4 -5
- package/dist/browser/modules/excel/utils/encryptor.browser.js +7 -12
- package/dist/browser/modules/excel/utils/encryptor.d.ts +1 -1
- package/dist/browser/modules/excel/utils/encryptor.js +4 -7
- package/dist/browser/modules/pdf/builder/document-builder.d.ts +517 -0
- package/dist/browser/modules/pdf/builder/document-builder.js +1493 -0
- package/dist/browser/modules/pdf/builder/form-appearance.d.ts +56 -0
- package/dist/browser/modules/pdf/builder/form-appearance.js +140 -0
- package/dist/browser/modules/pdf/builder/image-utils.d.ts +39 -0
- package/dist/browser/modules/pdf/builder/image-utils.js +129 -0
- package/dist/browser/modules/pdf/builder/pdf-editor.d.ts +230 -0
- package/dist/browser/modules/pdf/builder/pdf-editor.js +1574 -0
- package/dist/browser/modules/pdf/builder/resource-merger.d.ts +41 -0
- package/dist/browser/modules/pdf/builder/resource-merger.js +258 -0
- package/dist/browser/modules/pdf/core/digital-signature.d.ts +109 -0
- package/dist/browser/modules/pdf/core/digital-signature.js +659 -0
- package/dist/browser/modules/pdf/core/encryption.js +8 -7
- package/dist/browser/modules/pdf/core/pdf-object.d.ts +11 -0
- package/dist/browser/modules/pdf/core/pdf-object.js +38 -0
- package/dist/browser/modules/pdf/core/pdf-stream.d.ts +32 -0
- package/dist/browser/modules/pdf/core/pdf-stream.js +66 -0
- package/dist/browser/modules/pdf/core/pdf-writer.d.ts +55 -1
- package/dist/browser/modules/pdf/core/pdf-writer.js +271 -6
- package/dist/browser/modules/pdf/core/pdfa.d.ts +62 -0
- package/dist/browser/modules/pdf/core/pdfa.js +261 -0
- package/dist/browser/modules/pdf/index.d.ts +11 -0
- package/dist/browser/modules/pdf/index.js +9 -0
- package/dist/browser/modules/pdf/reader/bookmark-extractor.d.ts +35 -0
- package/dist/browser/modules/pdf/reader/bookmark-extractor.js +324 -0
- package/dist/browser/modules/pdf/reader/pdf-decrypt.js +6 -5
- package/dist/browser/modules/pdf/reader/pdf-reader.d.ts +17 -0
- package/dist/browser/modules/pdf/reader/pdf-reader.js +26 -2
- package/dist/browser/modules/pdf/reader/table-extractor.d.ts +69 -0
- package/dist/browser/modules/pdf/reader/table-extractor.js +365 -0
- package/dist/browser/modules/pdf/render/layout-engine.d.ts +21 -1
- package/dist/browser/modules/pdf/render/layout-engine.js +112 -5
- package/dist/browser/modules/pdf/render/page-renderer.d.ts +2 -9
- package/dist/browser/modules/pdf/render/page-renderer.js +62 -103
- package/dist/browser/modules/pdf/render/pdf-exporter.js +2 -61
- package/dist/browser/modules/pdf/render/style-converter.d.ts +4 -0
- package/dist/browser/modules/pdf/render/style-converter.js +1 -1
- package/dist/browser/modules/pdf/types.d.ts +14 -1
- package/dist/browser/modules/stream/browser/readable.js +8 -2
- package/dist/browser/utils/crypto.browser.d.ts +64 -0
- package/dist/browser/{modules/pdf/core/crypto.js → utils/crypto.browser.js} +91 -101
- package/dist/browser/utils/crypto.d.ts +97 -0
- package/dist/browser/utils/crypto.js +209 -0
- package/dist/cjs/modules/archive/compression/crc32.js +1 -1
- package/dist/cjs/modules/archive/crypto/aes.js +2 -23
- package/dist/cjs/modules/archive/crypto/index.js +3 -1
- package/dist/cjs/modules/csv/worker/worker-script.generated.js +1 -1
- package/dist/cjs/modules/excel/utils/cell-matrix.js +1 -0
- package/dist/cjs/modules/excel/utils/encryptor.browser.js +7 -12
- package/dist/cjs/modules/excel/utils/encryptor.js +4 -10
- package/dist/cjs/modules/pdf/builder/document-builder.js +1532 -0
- package/dist/cjs/modules/pdf/builder/form-appearance.js +145 -0
- package/dist/cjs/modules/pdf/builder/image-utils.js +135 -0
- package/dist/cjs/modules/pdf/builder/pdf-editor.js +1612 -0
- package/dist/cjs/modules/pdf/builder/resource-merger.js +263 -0
- package/dist/cjs/modules/pdf/core/digital-signature.js +667 -0
- package/dist/cjs/modules/pdf/core/encryption.js +8 -7
- package/dist/cjs/modules/pdf/core/pdf-object.js +38 -0
- package/dist/cjs/modules/pdf/core/pdf-stream.js +66 -0
- package/dist/cjs/modules/pdf/core/pdf-writer.js +272 -6
- package/dist/cjs/modules/pdf/core/pdfa.js +266 -0
- package/dist/cjs/modules/pdf/index.js +19 -1
- package/dist/cjs/modules/pdf/reader/bookmark-extractor.js +327 -0
- package/dist/cjs/modules/pdf/reader/pdf-decrypt.js +6 -5
- package/dist/cjs/modules/pdf/reader/pdf-reader.js +26 -2
- package/dist/cjs/modules/pdf/reader/table-extractor.js +368 -0
- package/dist/cjs/modules/pdf/render/layout-engine.js +113 -4
- package/dist/cjs/modules/pdf/render/page-renderer.js +63 -105
- package/dist/cjs/modules/pdf/render/pdf-exporter.js +3 -62
- package/dist/cjs/modules/pdf/render/style-converter.js +1 -0
- package/dist/cjs/modules/stream/browser/readable.js +8 -2
- package/dist/cjs/{modules/pdf/core/crypto.js → utils/crypto.browser.js} +95 -102
- package/dist/cjs/utils/crypto.js +228 -0
- package/dist/esm/modules/archive/compression/crc32.js +1 -1
- package/dist/esm/modules/archive/crypto/aes.js +1 -20
- package/dist/esm/modules/archive/crypto/index.js +3 -1
- package/dist/esm/modules/csv/worker/worker-script.generated.js +1 -1
- package/dist/esm/modules/excel/utils/cell-matrix.js +1 -0
- package/dist/esm/modules/excel/utils/encryptor.browser.js +7 -12
- package/dist/esm/modules/excel/utils/encryptor.js +4 -7
- package/dist/esm/modules/pdf/builder/document-builder.js +1493 -0
- package/dist/esm/modules/pdf/builder/form-appearance.js +140 -0
- package/dist/esm/modules/pdf/builder/image-utils.js +129 -0
- package/dist/esm/modules/pdf/builder/pdf-editor.js +1574 -0
- package/dist/esm/modules/pdf/builder/resource-merger.js +258 -0
- package/dist/esm/modules/pdf/core/digital-signature.js +659 -0
- package/dist/esm/modules/pdf/core/encryption.js +8 -7
- package/dist/esm/modules/pdf/core/pdf-object.js +38 -0
- package/dist/esm/modules/pdf/core/pdf-stream.js +66 -0
- package/dist/esm/modules/pdf/core/pdf-writer.js +271 -6
- package/dist/esm/modules/pdf/core/pdfa.js +261 -0
- package/dist/esm/modules/pdf/index.js +9 -0
- package/dist/esm/modules/pdf/reader/bookmark-extractor.js +324 -0
- package/dist/esm/modules/pdf/reader/pdf-decrypt.js +6 -5
- package/dist/esm/modules/pdf/reader/pdf-reader.js +26 -2
- package/dist/esm/modules/pdf/reader/table-extractor.js +365 -0
- package/dist/esm/modules/pdf/render/layout-engine.js +112 -5
- package/dist/esm/modules/pdf/render/page-renderer.js +62 -103
- package/dist/esm/modules/pdf/render/pdf-exporter.js +2 -61
- package/dist/esm/modules/pdf/render/style-converter.js +1 -1
- package/dist/esm/modules/stream/browser/readable.js +8 -2
- package/dist/esm/{modules/pdf/core/crypto.js → utils/crypto.browser.js} +91 -101
- package/dist/esm/utils/crypto.js +209 -0
- package/dist/iife/excelts.iife.js +1248 -1074
- package/dist/iife/excelts.iife.js.map +1 -1
- package/dist/iife/excelts.iife.min.js +53 -54
- package/dist/types/modules/archive/crypto/aes.d.ts +0 -8
- package/dist/types/modules/archive/crypto/index.d.ts +2 -1
- package/dist/types/modules/csv/parse/row-processor.d.ts +1 -1
- package/dist/types/modules/excel/utils/encryptor.browser.d.ts +4 -5
- package/dist/types/modules/excel/utils/encryptor.d.ts +1 -1
- package/dist/types/modules/pdf/builder/document-builder.d.ts +517 -0
- package/dist/types/modules/pdf/builder/form-appearance.d.ts +56 -0
- package/dist/types/modules/pdf/builder/image-utils.d.ts +39 -0
- package/dist/types/modules/pdf/builder/pdf-editor.d.ts +230 -0
- package/dist/types/modules/pdf/builder/resource-merger.d.ts +41 -0
- package/dist/types/modules/pdf/core/digital-signature.d.ts +109 -0
- package/dist/types/modules/pdf/core/pdf-object.d.ts +11 -0
- package/dist/types/modules/pdf/core/pdf-stream.d.ts +32 -0
- package/dist/types/modules/pdf/core/pdf-writer.d.ts +55 -1
- package/dist/types/modules/pdf/core/pdfa.d.ts +62 -0
- package/dist/types/modules/pdf/index.d.ts +11 -0
- package/dist/types/modules/pdf/reader/bookmark-extractor.d.ts +35 -0
- package/dist/types/modules/pdf/reader/pdf-reader.d.ts +17 -0
- package/dist/types/modules/pdf/reader/table-extractor.d.ts +69 -0
- package/dist/types/modules/pdf/render/layout-engine.d.ts +21 -1
- package/dist/types/modules/pdf/render/page-renderer.d.ts +2 -9
- package/dist/types/modules/pdf/render/style-converter.d.ts +4 -0
- package/dist/types/modules/pdf/types.d.ts +14 -1
- package/dist/types/utils/crypto.browser.d.ts +64 -0
- package/dist/types/utils/crypto.d.ts +97 -0
- package/package.json +110 -111
- package/dist/browser/modules/pdf/core/crypto.d.ts +0 -65
- 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("
|
|
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,
|
|
41
|
-
const uValue = (0,
|
|
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,
|
|
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,
|
|
52
|
-
const oValue = (0,
|
|
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,
|
|
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,
|
|
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 =
|
|
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
|
+
}
|