pdf-lite 1.7.3 → 1.7.4-alpha.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.
@@ -1,3 +1,4 @@
1
+ import { PdfInvalidPasswordError } from '../../errors.js';
1
2
  import { aes128CbcNoPaddingEncrypt, aes256CbcNoPaddingDecrypt, sha256, sha384, sha512, } from '../../utils/algos.js';
2
3
  import { assert } from '../../utils/assert.js';
3
4
  /**
@@ -162,7 +163,7 @@ export async function getFileKey(userPassword, ownerPassword, u, ue, o, oe) {
162
163
  return key;
163
164
  }
164
165
  catch (e) {
165
- throw new Error('Invalid password');
166
+ throw new PdfInvalidPasswordError();
166
167
  }
167
168
  }
168
169
  }
package/dist/errors.d.ts CHANGED
@@ -20,3 +20,17 @@ export declare class UnexpectedTokenError extends Error {
20
20
  */
21
21
  export declare class FoundCompressedObjectError extends Error {
22
22
  }
23
+ /**
24
+ * Error thrown when a PDF is encrypted and the blank password
25
+ * does not grant access. Callers should catch this and prompt
26
+ * the user for a password, then retry with the `password` option.
27
+ */
28
+ export declare class PdfPasswordProtectedError extends Error {
29
+ constructor(message?: string);
30
+ }
31
+ /**
32
+ * Error thrown when a provided password is invalid for decrypting a PDF.
33
+ */
34
+ export declare class PdfInvalidPasswordError extends Error {
35
+ constructor(message?: string);
36
+ }
package/dist/errors.js CHANGED
@@ -22,3 +22,21 @@ export class UnexpectedTokenError extends Error {
22
22
  */
23
23
  export class FoundCompressedObjectError extends Error {
24
24
  }
25
+ /**
26
+ * Error thrown when a PDF is encrypted and the blank password
27
+ * does not grant access. Callers should catch this and prompt
28
+ * the user for a password, then retry with the `password` option.
29
+ */
30
+ export class PdfPasswordProtectedError extends Error {
31
+ constructor(message) {
32
+ super(message ?? 'PDF is password protected');
33
+ }
34
+ }
35
+ /**
36
+ * Error thrown when a provided password is invalid for decrypting a PDF.
37
+ */
38
+ export class PdfInvalidPasswordError extends Error {
39
+ constructor(message) {
40
+ super(message ?? 'Invalid password');
41
+ }
42
+ }
@@ -47,6 +47,8 @@ export declare class PdfDocument extends PdfObject implements IPdfObjectResolver
47
47
  private _updating;
48
48
  private _finalized;
49
49
  private _signed;
50
+ /** @internal */
51
+ _batching: boolean;
50
52
  /**
51
53
  * Creates a new PDF document instance.
52
54
  *
@@ -71,14 +73,48 @@ export declare class PdfDocument extends PdfObject implements IPdfObjectResolver
71
73
  get pages(): PdfPages;
72
74
  get header(): PdfComment | undefined;
73
75
  set header(comment: PdfComment | undefined);
76
+ /**
77
+ * Loads PDF objects into the document, organizing them into revisions.
78
+ * Parses objects into revisions based on EOF comments.
79
+ *
80
+ * @param objects - Array of PDF objects to load into the document
81
+ * @param options - Optional security and mode configuration
82
+ * @param options.password - User password for encrypted documents
83
+ * @param options.ownerPassword - Owner password for encrypted documents
84
+ * @param options.incremental - Whether to use incremental mode
85
+ */
86
+ loadObjects(objects: PdfObject[], options?: {
87
+ password?: string;
88
+ ownerPassword?: string;
89
+ incremental?: boolean;
90
+ }): Promise<this>;
91
+ /**
92
+ * Loads a PDF document from a byte stream, parsing it into objects and revisions.
93
+ * @param input - Async or sync iterable of byte arrays representing the PDF file
94
+ * @param options - Optional security and mode configuration
95
+ * @returns A promise that resolves to the loaded PdfDocument instance
96
+ */
97
+ load(input: AsyncIterable<ByteArray> | Iterable<ByteArray>, options?: {
98
+ password?: string;
99
+ ownerPassword?: string;
100
+ incremental?: boolean;
101
+ }): Promise<this>;
74
102
  /**
75
103
  * Creates a PdfDocument from an array of PDF objects.
76
104
  * Parses objects into revisions based on EOF comments.
77
105
  *
78
106
  * @param objects - Array of PDF objects to construct the document from
79
- * @returns A new PdfDocument instance
107
+ * @param options - Optional security and mode configuration
108
+ * @param options.password - User password for encrypted documents
109
+ * @param options.ownerPassword - Owner password for encrypted documents
110
+ * @param options.incremental - Whether to use incremental mode
111
+ * @returns A promise that resolves to the new PdfDocument instance
80
112
  */
81
- static fromObjects(objects: PdfObject[]): PdfDocument;
113
+ static fromObjects(objects: PdfObject[], options?: {
114
+ password?: string;
115
+ ownerPassword?: string;
116
+ incremental?: boolean;
117
+ }): Promise<PdfDocument>;
82
118
  /**
83
119
  * Starts a new revision for incremental updates.
84
120
  * Creates a new revision linked to the previous one.
@@ -94,6 +130,31 @@ export declare class PdfDocument extends PdfObject implements IPdfObjectResolver
94
130
  * @param objects - PDF objects to add to the document
95
131
  */
96
132
  add(...objects: PdfObject[]): void;
133
+ /**
134
+ * Recursively collects and adds all missing referenced objects in batches.
135
+ * More efficient than recursive add() calls since it collects all missing
136
+ * objects before processing them.
137
+ * @private
138
+ */
139
+ private addAllMissingReferences;
140
+ /**
141
+ * Creates a batch for adding multiple objects with a single update pass.
142
+ * This is significantly faster than calling `add()` multiple times when
143
+ * adding objects with many sub-references (e.g. fonts with descriptors,
144
+ * CIDToGIDMap streams, ToUnicode CMaps).
145
+ *
146
+ * @example
147
+ * ```typescript
148
+ * const batch = document.batch()
149
+ * batch.add(font1)
150
+ * batch.add(font2)
151
+ * batch.add(imageStream)
152
+ * batch.commit()
153
+ * ```
154
+ *
155
+ * @returns A PdfDocumentBatch instance
156
+ */
157
+ batch(): PdfDocumentBatch;
97
158
  /**
98
159
  * Packs non-stream indirect objects into compressed ObjStm containers.
99
160
  * Objects that already have an object number are left unchanged.
@@ -289,8 +350,9 @@ export declare class PdfDocument extends PdfObject implements IPdfObjectResolver
289
350
  private updateRevisions;
290
351
  /**
291
352
  * Performs a full update cycle to ensure all revisions are consistent and offsets are correct.
353
+ * @internal
292
354
  */
293
- private update;
355
+ update(): void;
294
356
  /**
295
357
  * Walks all objects in the document and registers any newly created
296
358
  * PdfIndirectObjects that are referenced but not yet part of the document
@@ -318,6 +380,20 @@ export declare class PdfDocument extends PdfObject implements IPdfObjectResolver
318
380
  objects: object[];
319
381
  }[];
320
382
  };
383
+ /**
384
+ * Creates a new PdfDocument instance.
385
+ *
386
+ * @param options - Configuration options for the document
387
+ * @returns A new PdfDocument instance
388
+ */
389
+ static newDocument(options?: {
390
+ revisions?: PdfRevision[];
391
+ version?: string | PdfComment;
392
+ password?: string;
393
+ ownerPassword?: string;
394
+ securityHandler?: PdfSecurityHandler;
395
+ signer?: PdfSigner;
396
+ }): PdfDocument;
321
397
  /**
322
398
  * Creates a PdfDocument from a byte stream.
323
399
  *
@@ -337,3 +413,29 @@ export declare class PdfDocument extends PdfObject implements IPdfObjectResolver
337
413
  */
338
414
  verifySignatures(): Promise<PdfDocumentVerificationResult>;
339
415
  }
416
+ /**
417
+ * Batches multiple `add()` calls into a single update pass.
418
+ * Created via {@link PdfDocument.batch}.
419
+ */
420
+ export declare class PdfDocumentBatch {
421
+ private _document;
422
+ private _completed;
423
+ private _addedObjects;
424
+ /** @internal */
425
+ constructor(document: PdfDocument);
426
+ /**
427
+ * Adds one or more objects to the document without triggering an update.
428
+ *
429
+ * @param objects - PDF objects to add
430
+ */
431
+ add(...objects: PdfObject[]): this;
432
+ /**
433
+ * Commits the batch, running a single update pass for all added objects.
434
+ */
435
+ commit(): void;
436
+ /**
437
+ * Rolls back the batch, removing all objects that were added and
438
+ * restoring the document to its state before the batch was created.
439
+ */
440
+ rollback(): void;
441
+ }
@@ -15,12 +15,13 @@ import { PdfByteOffsetToken } from '../core/tokens/byte-offset-token.js';
15
15
  import { PdfNumberToken } from '../core/tokens/number-token.js';
16
16
  import { PdfXRefTableEntryToken } from '../core/tokens/xref-table-entry-token.js';
17
17
  import { PdfStartXRef } from '../core/objects/pdf-start-xref.js';
18
- import { FoundCompressedObjectError } from '../errors.js';
18
+ import { FoundCompressedObjectError, PdfPasswordProtectedError, } from '../errors.js';
19
19
  import { PdfReader } from './pdf-reader.js';
20
20
  import { PdfSigner } from '../signing/signer.js';
21
21
  import { concatUint8Arrays } from '../utils/concatUint8Arrays.js';
22
22
  import { PdfAcroForm } from '../acroform/pdf-acro-form.js';
23
23
  import { PdfPages } from './pdf-pages.js';
24
+ import { PdfObjectStream } from '../index.js';
24
25
  /**
25
26
  * Represents a PDF document with support for reading, writing, and modifying PDF files.
26
27
  * Handles document structure, revisions, encryption, and digital signatures.
@@ -55,6 +56,8 @@ export class PdfDocument extends PdfObject {
55
56
  _updating = false;
56
57
  _finalized = false;
57
58
  _signed = false;
59
+ /** @internal */
60
+ _batching = false;
58
61
  /**
59
62
  * Creates a new PDF document instance.
60
63
  *
@@ -75,18 +78,18 @@ export class PdfDocument extends PdfObject {
75
78
  else {
76
79
  this.setVersion(options?.version ?? '2.0');
77
80
  }
78
- if (options?.password) {
79
- this.setPassword(options.password);
80
- }
81
- if (options?.ownerPassword) {
82
- this.setOwnerPassword(options.ownerPassword);
83
- }
84
81
  this.signer = options?.signer ?? new PdfSigner({ document: this });
85
82
  this.linkRevisions();
86
83
  this.wireResolvers(...this.objects.filter((x) => x instanceof PdfIndirectObject), ...this.revisions.map((rev) => rev.xref.trailerDict));
87
84
  this.calculateOffsets();
88
85
  this.originalSecurityHandler = options?.securityHandler;
89
86
  this.resetSecurityHandler();
87
+ if (options?.password) {
88
+ this.setPassword(options.password);
89
+ }
90
+ if (options?.ownerPassword) {
91
+ this.setOwnerPassword(options.ownerPassword);
92
+ }
90
93
  }
91
94
  resolve(objectNumber, generationNumber) {
92
95
  const cacheKey = `${objectNumber} ${generationNumber}`;
@@ -132,13 +135,16 @@ export class PdfDocument extends PdfObject {
132
135
  this.revisions[0].header = comment;
133
136
  }
134
137
  /**
135
- * Creates a PdfDocument from an array of PDF objects.
138
+ * Loads PDF objects into the document, organizing them into revisions.
136
139
  * Parses objects into revisions based on EOF comments.
137
140
  *
138
- * @param objects - Array of PDF objects to construct the document from
139
- * @returns A new PdfDocument instance
141
+ * @param objects - Array of PDF objects to load into the document
142
+ * @param options - Optional security and mode configuration
143
+ * @param options.password - User password for encrypted documents
144
+ * @param options.ownerPassword - Owner password for encrypted documents
145
+ * @param options.incremental - Whether to use incremental mode
140
146
  */
141
- static fromObjects(objects) {
147
+ async loadObjects(objects, options) {
142
148
  let header;
143
149
  const revisions = [];
144
150
  let currentObjects = [];
@@ -156,7 +162,111 @@ export class PdfDocument extends PdfObject {
156
162
  if (currentObjects.length > 0) {
157
163
  revisions.push(new PdfRevision({ objects: currentObjects }));
158
164
  }
159
- return new PdfDocument({ revisions, version: header });
165
+ this.revisions = revisions;
166
+ if (header) {
167
+ this.header = header;
168
+ }
169
+ this.linkRevisions();
170
+ this.wireResolvers(...this.objects.filter((x) => x instanceof PdfIndirectObject), ...this.revisions.map((rev) => rev.xref.trailerDict));
171
+ this.calculateOffsets();
172
+ // Reset security handler to detect and initialize encryption from the PDF
173
+ // Preserve any passwords that were set before load() was called
174
+ let presetPassword = options?.password;
175
+ let presetOwnerPassword = options?.ownerPassword;
176
+ if (this.securityHandler instanceof PdfStandardSecurityHandler) {
177
+ const pw = this.securityHandler.getPassword();
178
+ const opw = this.securityHandler.getOwnerPassword();
179
+ if (!presetPassword && pw.length > 0) {
180
+ presetPassword = new TextDecoder().decode(pw);
181
+ }
182
+ if (!presetOwnerPassword && opw) {
183
+ presetOwnerPassword = new TextDecoder().decode(opw);
184
+ }
185
+ }
186
+ this.resetSecurityHandler();
187
+ // Apply passwords
188
+ if (presetPassword) {
189
+ this.setPassword(presetPassword);
190
+ }
191
+ if (presetOwnerPassword) {
192
+ this.setOwnerPassword(presetOwnerPassword);
193
+ }
194
+ // Handle encryption/decryption
195
+ let shouldDecrypt = Boolean(this.encryptionDictionary);
196
+ const hasExplicitPassword = !!presetPassword || !!presetOwnerPassword;
197
+ // If encrypted, verify the password is valid before attempting decryption
198
+ if (shouldDecrypt &&
199
+ this.securityHandler instanceof PdfStandardSecurityHandler) {
200
+ const valid = await this.securityHandler.testPassword();
201
+ if (!valid) {
202
+ if (!hasExplicitPassword) {
203
+ throw new PdfPasswordProtectedError();
204
+ }
205
+ this.resetSecurityHandler();
206
+ shouldDecrypt = false;
207
+ }
208
+ }
209
+ if (options?.incremental) {
210
+ // Lock revisions first to preserve the original bytes
211
+ // (including encrypted data) via cached tokens.
212
+ this.setIncremental(true);
213
+ // Then decrypt the live object data so built-in operations
214
+ // (AcroForm, fonts, etc.) can read it. The cached tokens
215
+ // still produce the original encrypted bytes on serialization.
216
+ if (shouldDecrypt) {
217
+ try {
218
+ await this.decryptObjects();
219
+ }
220
+ catch (e) {
221
+ if (!hasExplicitPassword) {
222
+ throw new PdfPasswordProtectedError();
223
+ }
224
+ this.resetSecurityHandler();
225
+ }
226
+ }
227
+ }
228
+ else if (shouldDecrypt) {
229
+ try {
230
+ await this.decryptObjects();
231
+ }
232
+ catch (e) {
233
+ if (!hasExplicitPassword) {
234
+ throw new PdfPasswordProtectedError();
235
+ }
236
+ this.resetSecurityHandler();
237
+ }
238
+ }
239
+ return this;
240
+ }
241
+ /**
242
+ * Loads a PDF document from a byte stream, parsing it into objects and revisions.
243
+ * @param input - Async or sync iterable of byte arrays representing the PDF file
244
+ * @param options - Optional security and mode configuration
245
+ * @returns A promise that resolves to the loaded PdfDocument instance
246
+ */
247
+ async load(input, options) {
248
+ const objectStream = new PdfObjectStream(input);
249
+ const objects = [];
250
+ for await (const obj of objectStream) {
251
+ objects.push(obj);
252
+ }
253
+ return await this.loadObjects(objects, options);
254
+ }
255
+ /**
256
+ * Creates a PdfDocument from an array of PDF objects.
257
+ * Parses objects into revisions based on EOF comments.
258
+ *
259
+ * @param objects - Array of PDF objects to construct the document from
260
+ * @param options - Optional security and mode configuration
261
+ * @param options.password - User password for encrypted documents
262
+ * @param options.ownerPassword - Owner password for encrypted documents
263
+ * @param options.incremental - Whether to use incremental mode
264
+ * @returns A promise that resolves to the new PdfDocument instance
265
+ */
266
+ static async fromObjects(objects, options) {
267
+ const document = new PdfDocument();
268
+ await document.loadObjects(objects, options);
269
+ return document;
160
270
  }
161
271
  /**
162
272
  * Starts a new revision for incremental updates.
@@ -193,16 +303,54 @@ export class PdfDocument extends PdfObject {
193
303
  }
194
304
  this.latestRevision.addObject(obj);
195
305
  }
196
- // Auto-add any referenced-but-missing objects
197
- const missing = this.collectMissingReferences(...objects);
198
- if (missing.length > 0) {
199
- this.add(...missing);
306
+ // Auto-add any referenced-but-missing objects recursively in one pass
307
+ this.addAllMissingReferences(objects);
308
+ if (!this._batching) {
309
+ this.update();
200
310
  }
201
- this.update();
202
311
  for (const obj of objects) {
203
312
  obj.setModified(false);
204
313
  }
205
314
  }
315
+ /**
316
+ * Recursively collects and adds all missing referenced objects in batches.
317
+ * More efficient than recursive add() calls since it collects all missing
318
+ * objects before processing them.
319
+ * @private
320
+ */
321
+ addAllMissingReferences(objects) {
322
+ let batch = this.collectMissingReferences(...objects);
323
+ while (batch.length > 0) {
324
+ for (const obj of batch) {
325
+ this.wireResolvers(obj);
326
+ if (!this.hasObjectInLatestRevision(obj)) {
327
+ this.latestRevision.addObject(obj);
328
+ }
329
+ }
330
+ // Check if these newly added objects reference more missing objects
331
+ batch = this.collectMissingReferences(...batch);
332
+ }
333
+ }
334
+ /**
335
+ * Creates a batch for adding multiple objects with a single update pass.
336
+ * This is significantly faster than calling `add()` multiple times when
337
+ * adding objects with many sub-references (e.g. fonts with descriptors,
338
+ * CIDToGIDMap streams, ToUnicode CMaps).
339
+ *
340
+ * @example
341
+ * ```typescript
342
+ * const batch = document.batch()
343
+ * batch.add(font1)
344
+ * batch.add(font2)
345
+ * batch.add(imageStream)
346
+ * batch.commit()
347
+ * ```
348
+ *
349
+ * @returns A PdfDocumentBatch instance
350
+ */
351
+ batch() {
352
+ return new PdfDocumentBatch(this);
353
+ }
206
354
  /**
207
355
  * Packs non-stream indirect objects into compressed ObjStm containers.
208
356
  * Objects that already have an object number are left unchanged.
@@ -826,9 +974,9 @@ export class PdfDocument extends PdfObject {
826
974
  }
827
975
  }
828
976
  }
829
- linkOffsets() {
977
+ linkOffsets(tokens) {
830
978
  const refMap = new Map();
831
- const tokens = this.toTokens();
979
+ tokens = tokens ?? this.toTokens();
832
980
  for (let i = 0; i < tokens.length; i++) {
833
981
  const token = tokens[i];
834
982
  let main;
@@ -862,11 +1010,13 @@ export class PdfDocument extends PdfObject {
862
1010
  }
863
1011
  }
864
1012
  }
865
- calculateOffsets() {
1013
+ calculateOffsets(cachedTokens) {
866
1014
  const serializer = new PdfTokenSerializer();
867
- serializer.feedMany(this.toTokens());
1015
+ const tokens = cachedTokens ?? this.toTokens();
1016
+ this.linkOffsets(tokens);
1017
+ serializer.feedMany(tokens);
868
1018
  serializer.calculateOffsets();
869
- this.linkOffsets();
1019
+ return tokens;
870
1020
  }
871
1021
  updateRevisions() {
872
1022
  let modified = false;
@@ -890,6 +1040,7 @@ export class PdfDocument extends PdfObject {
890
1040
  }
891
1041
  /**
892
1042
  * Performs a full update cycle to ensure all revisions are consistent and offsets are correct.
1043
+ * @internal
893
1044
  */
894
1045
  update() {
895
1046
  if (this._updating)
@@ -899,17 +1050,17 @@ export class PdfDocument extends PdfObject {
899
1050
  this.commitIncrementalUpdates();
900
1051
  this.flushResolvedCache();
901
1052
  this.registerNewReferences();
1053
+ // First pass: generate tokens and calculate offsets
902
1054
  this.calculateOffsets();
903
1055
  this.updateRevisions();
904
1056
  // Second pass: xref binary may have changed size (e.g. FlateDecode removed
905
- // from xref stream), shifting objects that follow it. Recalculate so entry
906
- // byteOffset refs hold the new positions, then rebuild the xref binary once
907
- // more so the baked bytes match those positions.
908
- this.calculateOffsets();
1057
+ // from xref stream), shifting objects that follow it. Regenerate tokens
1058
+ // to capture xref changes, then recalculate offsets.
1059
+ const tokens = this.calculateOffsets();
909
1060
  this.updateRevisions();
910
- // Third pass: confirm positions are stable (xref binary size should not
911
- // change again because W widths and entry count are the same).
912
- this.calculateOffsets();
1061
+ // Third pass: reuse tokens from second pass since xref should now be stable
1062
+ // (xref binary size should not change because W widths and entry count are same).
1063
+ this.calculateOffsets(tokens);
913
1064
  }
914
1065
  finally {
915
1066
  this._updating = false;
@@ -921,7 +1072,7 @@ export class PdfDocument extends PdfObject {
921
1072
  * (e.g. appearance streams created by generateAppearance).
922
1073
  */
923
1074
  registerNewReferences() {
924
- const missing = this.collectMissingReferences(...this.latestRevision.objects);
1075
+ const missing = this.collectMissingReferences(...this.objects);
925
1076
  if (missing.length > 0) {
926
1077
  this.add(...missing);
927
1078
  }
@@ -961,11 +1112,13 @@ export class PdfDocument extends PdfObject {
961
1112
  */
962
1113
  cloneImpl() {
963
1114
  const clonedRevisions = this.revisions.map((rev) => rev.clone());
964
- return new PdfDocument({
1115
+ const cloned = new PdfDocument({
965
1116
  revisions: clonedRevisions,
966
1117
  version: this.header?.clone(),
967
- securityHandler: this.securityHandler,
1118
+ securityHandler: this.securityHandler?.clone(),
968
1119
  });
1120
+ cloned.hasEncryptionDictionary = this.hasEncryptionDictionary;
1121
+ return cloned;
969
1122
  }
970
1123
  toJSON() {
971
1124
  return {
@@ -973,6 +1126,15 @@ export class PdfDocument extends PdfObject {
973
1126
  revisions: this.revisions.map((rev) => rev.toJSON()),
974
1127
  };
975
1128
  }
1129
+ /**
1130
+ * Creates a new PdfDocument instance.
1131
+ *
1132
+ * @param options - Configuration options for the document
1133
+ * @returns A new PdfDocument instance
1134
+ */
1135
+ static newDocument(options) {
1136
+ return new PdfDocument(options);
1137
+ }
976
1138
  /**
977
1139
  * Creates a PdfDocument from a byte stream.
978
1140
  *
@@ -994,3 +1156,56 @@ export class PdfDocument extends PdfObject {
994
1156
  return await this.signer.verify();
995
1157
  }
996
1158
  }
1159
+ /**
1160
+ * Batches multiple `add()` calls into a single update pass.
1161
+ * Created via {@link PdfDocument.batch}.
1162
+ */
1163
+ export class PdfDocumentBatch {
1164
+ _document;
1165
+ _completed = false;
1166
+ _addedObjects = [];
1167
+ /** @internal */
1168
+ constructor(document) {
1169
+ this._document = document;
1170
+ this._document._batching = true;
1171
+ }
1172
+ /**
1173
+ * Adds one or more objects to the document without triggering an update.
1174
+ *
1175
+ * @param objects - PDF objects to add
1176
+ */
1177
+ add(...objects) {
1178
+ if (this._completed) {
1179
+ throw new Error('Batch already completed (committed or rolled back)');
1180
+ }
1181
+ this._addedObjects.push(...objects);
1182
+ this._document.add(...objects);
1183
+ return this;
1184
+ }
1185
+ /**
1186
+ * Commits the batch, running a single update pass for all added objects.
1187
+ */
1188
+ commit() {
1189
+ if (this._completed) {
1190
+ throw new Error('Batch already completed (committed or rolled back)');
1191
+ }
1192
+ this._completed = true;
1193
+ this._document._batching = false;
1194
+ this._document.update();
1195
+ }
1196
+ /**
1197
+ * Rolls back the batch, removing all objects that were added and
1198
+ * restoring the document to its state before the batch was created.
1199
+ */
1200
+ rollback() {
1201
+ if (this._completed) {
1202
+ throw new Error('Batch already completed (committed or rolled back)');
1203
+ }
1204
+ this._completed = true;
1205
+ this._document._batching = false;
1206
+ // Remove all objects that were added during this batch
1207
+ for (const obj of this._addedObjects) {
1208
+ this._document.deleteObject(obj);
1209
+ }
1210
+ }
1211
+ }
@@ -1,4 +1,3 @@
1
- import { PdfObjectStream } from '../core/streams/object-stream.js';
2
1
  import { PdfDocument } from './pdf-document.js';
3
2
  /**
4
3
  * A reader for parsing PDF data into PdfDocument instances.
@@ -31,7 +30,7 @@ export class PdfReader {
31
30
  for await (const obj of this.objectStream) {
32
31
  objects.push(obj);
33
32
  }
34
- return PdfDocument.fromObjects(objects);
33
+ return await PdfDocument.fromObjects(objects);
35
34
  }
36
35
  /**
37
36
  * Creates a PdfDocument directly from a byte stream.
@@ -41,41 +40,8 @@ export class PdfReader {
41
40
  * @returns A promise that resolves to the parsed PdfDocument
42
41
  */
43
42
  static async fromBytes(input, options) {
44
- const reader = new PdfReader(new PdfObjectStream(input));
45
- const document = await reader.read();
46
- let shouldDecrypt = Boolean(document.encryptionDictionary);
47
- if (typeof options?.password === 'string') {
48
- document.setPassword(options.password);
49
- shouldDecrypt = true;
50
- }
51
- if (typeof options?.ownerPassword === 'string') {
52
- document.setOwnerPassword(options.ownerPassword);
53
- shouldDecrypt = true;
54
- }
55
- if (options?.incremental) {
56
- // Lock revisions first to preserve the original bytes
57
- // (including encrypted data) via cached tokens.
58
- document.setIncremental(true);
59
- // Then decrypt the live object data so built-in operations
60
- // (AcroForm, fonts, etc.) can read it. The cached tokens
61
- // still produce the original encrypted bytes on serialization.
62
- if (shouldDecrypt) {
63
- try {
64
- await document.decryptObjects();
65
- }
66
- catch (e) {
67
- document.resetSecurityHandler();
68
- }
69
- }
70
- }
71
- else if (shouldDecrypt) {
72
- try {
73
- await document.decryptObjects();
74
- }
75
- catch (e) {
76
- document.resetSecurityHandler();
77
- }
78
- }
43
+ const document = new PdfDocument();
44
+ await document.load(input, options);
79
45
  return document;
80
46
  }
81
47
  }
@@ -116,6 +116,11 @@ export declare abstract class PdfSecurityHandler {
116
116
  * Writes the encryption dictionary with computed keys.
117
117
  */
118
118
  abstract write(): Promise<void>;
119
+ /**
120
+ * Tests whether the current password can decrypt this document.
121
+ * Returns true if the password is valid, false otherwise.
122
+ */
123
+ abstract testPassword(): Promise<boolean>;
119
124
  /**
120
125
  * Builds the numeric permission flags from a PdfPermissions object.
121
126
  *
@@ -123,6 +128,12 @@ export declare abstract class PdfSecurityHandler {
123
128
  * @returns The numeric permission flags.
124
129
  */
125
130
  protected buildPermissions(perm: PdfPermissions): number;
131
+ /**
132
+ * Creates a shallow clone of this security handler with an independent
133
+ * encryption dictionary, so that mutating the clone (e.g. during
134
+ * finalize/encrypt) does not affect the original.
135
+ */
136
+ clone(): this;
126
137
  /**
127
138
  * Recursively decrypts all strings and streams within an indirect object.
128
139
  *
@@ -213,6 +224,11 @@ export declare abstract class PdfStandardSecurityHandler extends PdfSecurityHand
213
224
  * @returns The document ID, or undefined if not set.
214
225
  */
215
226
  getDocumentId(): PdfId | undefined;
227
+ /**
228
+ * Tests whether the current password can decrypt this document.
229
+ * Attempts to compute the master key and returns true if successful.
230
+ */
231
+ testPassword(): Promise<boolean>;
216
232
  /**
217
233
  * Sets the user password.
218
234
  *
@@ -225,6 +241,18 @@ export declare abstract class PdfStandardSecurityHandler extends PdfSecurityHand
225
241
  * @param ownerPassword - The owner password string or bytes.
226
242
  */
227
243
  setOwnerPassword(ownerPassword: string | ByteArray): void;
244
+ /**
245
+ * Gets the user password.
246
+ *
247
+ * @returns The user password as bytes.
248
+ */
249
+ getPassword(): ByteArray;
250
+ /**
251
+ * Gets the owner password.
252
+ *
253
+ * @returns The owner password as bytes, or undefined if not set.
254
+ */
255
+ getOwnerPassword(): ByteArray | undefined;
228
256
  /**
229
257
  * Checks if metadata encryption is enabled.
230
258
  *
@@ -243,6 +271,13 @@ export declare abstract class PdfStandardSecurityHandler extends PdfSecurityHand
243
271
  * @returns The computed user key.
244
272
  */
245
273
  protected abstract computeUserKey(): Promise<ByteArray>;
274
+ /**
275
+ * Computes the master encryption key from the password.
276
+ *
277
+ * @returns The computed master key.
278
+ * @throws Error if the password is incorrect or required parameters are missing.
279
+ */
280
+ protected abstract computeMasterKey(): Promise<ByteArray>;
246
281
  /**
247
282
  * Computes the owner key (O value) for the encryption dictionary.
248
283
  *
@@ -69,6 +69,17 @@ export class PdfSecurityHandler {
69
69
  }
70
70
  return flags | 0xfffff000; // ensure unused high bits are set
71
71
  }
72
+ /**
73
+ * Creates a shallow clone of this security handler with an independent
74
+ * encryption dictionary, so that mutating the clone (e.g. during
75
+ * finalize/encrypt) does not affect the original.
76
+ */
77
+ clone() {
78
+ const cloned = Object.create(Object.getPrototypeOf(this));
79
+ Object.assign(cloned, this);
80
+ cloned.dict = this.dict.clone();
81
+ return cloned;
82
+ }
72
83
  /**
73
84
  * Recursively decrypts all strings and streams within an indirect object.
74
85
  *
@@ -241,6 +252,19 @@ export class PdfStandardSecurityHandler extends PdfSecurityHandler {
241
252
  getDocumentId() {
242
253
  return this.documentId;
243
254
  }
255
+ /**
256
+ * Tests whether the current password can decrypt this document.
257
+ * Attempts to compute the master key and returns true if successful.
258
+ */
259
+ async testPassword() {
260
+ try {
261
+ await this.computeMasterKey();
262
+ return true;
263
+ }
264
+ catch {
265
+ return false;
266
+ }
267
+ }
244
268
  /**
245
269
  * Sets the user password.
246
270
  *
@@ -261,6 +285,22 @@ export class PdfStandardSecurityHandler extends PdfSecurityHandler {
261
285
  ? stringToBytes(ownerPassword)
262
286
  : ownerPassword;
263
287
  }
288
+ /**
289
+ * Gets the user password.
290
+ *
291
+ * @returns The user password as bytes.
292
+ */
293
+ getPassword() {
294
+ return this.password;
295
+ }
296
+ /**
297
+ * Gets the owner password.
298
+ *
299
+ * @returns The owner password as bytes, or undefined if not set.
300
+ */
301
+ getOwnerPassword() {
302
+ return this.ownerPassword;
303
+ }
264
304
  /**
265
305
  * Checks if metadata encryption is enabled.
266
306
  *
@@ -36,6 +36,7 @@ export declare class PdfPublicKeySecurityHandler extends PdfSecurityHandler {
36
36
  permissions?: PdfPermissions | number;
37
37
  encryptMetadata?: boolean;
38
38
  });
39
+ clone(): this;
39
40
  /**
40
41
  * Gets the security handler filter name.
41
42
  *
@@ -66,6 +67,7 @@ export declare class PdfPublicKeySecurityHandler extends PdfSecurityHandler {
66
67
  * @returns True if the underlying handler is ready.
67
68
  */
68
69
  isReady(): boolean;
70
+ testPassword(): Promise<boolean>;
69
71
  /**
70
72
  * Gets the encryption version number.
71
73
  *
@@ -50,6 +50,11 @@ export class PdfPublicKeySecurityHandler extends PdfSecurityHandler {
50
50
  pkcs7Input[23] = this.permissions & 0xff;
51
51
  this.recipientsCms = this.getRecipientsPkcs7(pkcs7Input);
52
52
  }
53
+ clone() {
54
+ const cloned = super.clone();
55
+ cloned.standardSecurityHandler = this.standardSecurityHandler.clone();
56
+ return cloned;
57
+ }
53
58
  /**
54
59
  * Gets the security handler filter name.
55
60
  *
@@ -90,6 +95,9 @@ export class PdfPublicKeySecurityHandler extends PdfSecurityHandler {
90
95
  isReady() {
91
96
  return this.standardSecurityHandler.isReady();
92
97
  }
98
+ async testPassword() {
99
+ return this.standardSecurityHandler.testPassword();
100
+ }
93
101
  /**
94
102
  * Gets the encryption version number.
95
103
  *
@@ -58,6 +58,7 @@ export declare class PdfV4SecurityHandler extends PdfV2SecurityHandler {
58
58
  * @param filter - The crypt filter instance.
59
59
  */
60
60
  setCryptFilter(name: string, filter: PdfCryptFilter): void;
61
+ clone(): this;
61
62
  /**
62
63
  * Gets the encryption revision number.
63
64
  *
@@ -84,6 +84,16 @@ export class PdfV4SecurityHandler extends PdfV2SecurityHandler {
84
84
  filter.setSecurityHandler(this);
85
85
  this.cryptFilters.set(name, filter);
86
86
  }
87
+ clone() {
88
+ const cloned = super.clone();
89
+ cloned.cryptFilters = new Map();
90
+ cloned.cryptFiltersByType = { ...this.cryptFiltersByType };
91
+ for (const [name, filter] of this.cryptFilters) {
92
+ cloned.cryptFilters.set(name, filter);
93
+ filter.setSecurityHandler(cloned);
94
+ }
95
+ return cloned;
96
+ }
87
97
  /**
88
98
  * Gets the encryption revision number.
89
99
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pdf-lite",
3
- "version": "1.7.3",
3
+ "version": "1.7.4-alpha.1",
4
4
  "main": "dist/index.js",
5
5
  "type": "module",
6
6
  "exports": {
@@ -35,7 +35,7 @@
35
35
  "@vitest/browser-playwright": "^4.0.14",
36
36
  "@vitest/coverage-v8": "^4.0.14",
37
37
  "playwright": "^1.56.1",
38
- "typescript": "6.0.2",
38
+ "typescript": "6.0.3",
39
39
  "vitest": "^4.0.14"
40
40
  },
41
41
  "repository": {