pdf-lite 1.5.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/EXAMPLES.md +51 -70
  2. package/README.md +1 -1
  3. package/dist/acroform/appearance/pdf-button-appearance-stream.d.ts +1 -1
  4. package/dist/acroform/appearance/pdf-button-appearance-stream.js +6 -2
  5. package/dist/acroform/fields/pdf-button-form-field.d.ts +0 -9
  6. package/dist/acroform/fields/pdf-button-form-field.js +7 -39
  7. package/dist/acroform/fields/pdf-choice-form-field.d.ts +2 -1
  8. package/dist/acroform/fields/pdf-choice-form-field.js +19 -38
  9. package/dist/acroform/fields/pdf-default-appearance.js +1 -1
  10. package/dist/acroform/fields/pdf-form-field.d.ts +27 -32
  11. package/dist/acroform/fields/pdf-form-field.js +180 -94
  12. package/dist/acroform/fields/pdf-text-form-field.js +6 -33
  13. package/dist/acroform/fields/types.d.ts +1 -1
  14. package/dist/acroform/index.d.ts +0 -2
  15. package/dist/acroform/index.js +0 -2
  16. package/dist/acroform/pdf-acro-form.d.ts +12 -36
  17. package/dist/acroform/pdf-acro-form.js +111 -201
  18. package/dist/acroform/xfa/pdf-xfa-data.d.ts +4 -3
  19. package/dist/acroform/xfa/pdf-xfa-data.js +16 -12
  20. package/dist/acroform/xfa/pdf-xfa-form.d.ts +9 -4
  21. package/dist/acroform/xfa/pdf-xfa-form.js +17 -39
  22. package/dist/annotations/index.d.ts +0 -1
  23. package/dist/annotations/index.js +0 -1
  24. package/dist/annotations/pdf-annotation.d.ts +7 -2
  25. package/dist/annotations/pdf-annotation.js +30 -19
  26. package/dist/annotations/pdf-default-resources.d.ts +11 -0
  27. package/dist/annotations/pdf-default-resources.js +3 -0
  28. package/dist/core/decoder.js +1 -1
  29. package/dist/core/objects/pdf-array.d.ts +8 -1
  30. package/dist/core/objects/pdf-array.js +31 -0
  31. package/dist/core/objects/pdf-dictionary.d.ts +2 -0
  32. package/dist/core/objects/pdf-dictionary.js +14 -7
  33. package/dist/core/objects/pdf-hexadecimal.d.ts +1 -0
  34. package/dist/core/objects/pdf-hexadecimal.js +3 -3
  35. package/dist/core/objects/pdf-indirect-object.d.ts +18 -9
  36. package/dist/core/objects/pdf-indirect-object.js +75 -16
  37. package/dist/core/objects/pdf-number.d.ts +1 -0
  38. package/dist/core/objects/pdf-number.js +5 -4
  39. package/dist/core/objects/pdf-object-reference.d.ts +8 -1
  40. package/dist/core/objects/pdf-object-reference.js +14 -0
  41. package/dist/core/objects/pdf-object.d.ts +14 -0
  42. package/dist/core/objects/pdf-object.js +36 -0
  43. package/dist/core/objects/pdf-start-xref.d.ts +1 -0
  44. package/dist/core/objects/pdf-start-xref.js +4 -0
  45. package/dist/core/objects/pdf-stream.d.ts +47 -7
  46. package/dist/core/objects/pdf-stream.js +301 -32
  47. package/dist/core/objects/pdf-string.d.ts +1 -0
  48. package/dist/core/objects/pdf-string.js +3 -6
  49. package/dist/core/objects/pdf-trailer.d.ts +1 -0
  50. package/dist/core/objects/pdf-trailer.js +6 -3
  51. package/dist/core/objects/pdf-xref-table.js +1 -1
  52. package/dist/core/ref.d.ts +3 -1
  53. package/dist/core/ref.js +8 -5
  54. package/dist/core/tokens/token.d.ts +2 -1
  55. package/dist/core/tokens/token.js +3 -0
  56. package/dist/fonts/index.d.ts +0 -1
  57. package/dist/fonts/index.js +0 -1
  58. package/dist/fonts/pdf-font.d.ts +32 -27
  59. package/dist/fonts/pdf-font.js +115 -77
  60. package/dist/pdf/index.d.ts +2 -0
  61. package/dist/pdf/index.js +2 -0
  62. package/dist/pdf/pdf-document.d.ts +63 -37
  63. package/dist/pdf/pdf-document.js +351 -135
  64. package/dist/pdf/pdf-page.d.ts +50 -0
  65. package/dist/pdf/pdf-page.js +144 -0
  66. package/dist/pdf/pdf-pages.d.ts +28 -0
  67. package/dist/pdf/pdf-pages.js +94 -0
  68. package/dist/pdf/pdf-reader.d.ts +5 -1
  69. package/dist/pdf/pdf-reader.js +36 -2
  70. package/dist/pdf/pdf-revision.d.ts +3 -3
  71. package/dist/pdf/pdf-revision.js +7 -7
  72. package/dist/pdf/pdf-xref-lookup.js +34 -14
  73. package/dist/signing/document-security-store.d.ts +14 -17
  74. package/dist/signing/document-security-store.js +19 -34
  75. package/dist/signing/signer.d.ts +23 -8
  76. package/dist/signing/signer.js +51 -17
  77. package/dist/utils/index.d.ts +0 -1
  78. package/dist/utils/index.js +0 -1
  79. package/dist/utils/needsCentralWhitespace.d.ts +10 -0
  80. package/dist/utils/needsCentralWhitespace.js +34 -0
  81. package/package.json +3 -3
  82. package/dist/acroform/acroform.d.ts +0 -9
  83. package/dist/acroform/acroform.js +0 -7
  84. package/dist/acroform/manager.d.ts +0 -37
  85. package/dist/acroform/manager.js +0 -57
  86. package/dist/acroform/pdf-font-encoding-cache.d.ts +0 -27
  87. package/dist/acroform/pdf-font-encoding-cache.js +0 -188
  88. package/dist/annotations/pdf-annotation-writer.d.ts +0 -20
  89. package/dist/annotations/pdf-annotation-writer.js +0 -76
  90. package/dist/fonts/manager.d.ts +0 -127
  91. package/dist/fonts/manager.js +0 -378
  92. package/dist/utils/predictors.d.ts +0 -113
  93. package/dist/utils/predictors.js +0 -279
@@ -5,8 +5,9 @@ import { PdfIndirectObject } from '../core/objects/pdf-indirect-object.js';
5
5
  import { PdfComment } from '../core/objects/pdf-comment.js';
6
6
  import { PdfWhitespaceToken } from '../core/tokens/whitespace-token.js';
7
7
  import { PdfObjStream, PdfStream, PdfXRefStreamCompressedEntry, } from '../core/objects/pdf-stream.js';
8
+ import { PdfArray } from '../core/objects/pdf-array.js';
8
9
  import { PdfDictionary } from '../core/objects/pdf-dictionary.js';
9
- import { PdfObjectReference } from '../core/objects/pdf-object-reference.js';
10
+ import { PdfObjectReference, } from '../core/objects/pdf-object-reference.js';
10
11
  import { PdfTokenSerializer } from '../core/serializer.js';
11
12
  import { PdfRevision } from './pdf-revision.js';
12
13
  import { PdfV5SecurityHandler } from '../security/handlers/v5.js';
@@ -17,9 +18,9 @@ import { PdfStartXRef } from '../core/objects/pdf-start-xref.js';
17
18
  import { FoundCompressedObjectError } from '../errors.js';
18
19
  import { PdfReader } from './pdf-reader.js';
19
20
  import { PdfSigner } from '../signing/signer.js';
20
- import { PdfAcroFormManager } from '../acroform/manager.js';
21
- import { PdfFontManager } from '../fonts/manager.js';
22
21
  import { concatUint8Arrays } from '../utils/concatUint8Arrays.js';
22
+ import { PdfAcroForm } from '../acroform/pdf-acro-form.js';
23
+ import { PdfPages } from './pdf-pages.js';
23
24
  /**
24
25
  * Represents a PDF document with support for reading, writing, and modifying PDF files.
25
26
  * Handles document structure, revisions, encryption, and digital signatures.
@@ -44,11 +45,16 @@ export class PdfDocument extends PdfObject {
44
45
  signer;
45
46
  /** Security handler for encryption/decryption operations */
46
47
  securityHandler;
47
- /** */
48
- acroForm;
49
- fonts;
48
+ /** Whether the document is currently in incremental mode (appending changes as a new revision) */
49
+ incremental = false;
50
+ originalSecurityHandler;
50
51
  hasEncryptionDictionary = false;
51
- toBeCommitted = [];
52
+ _resolvedCache = new Map();
53
+ _objStreamCache = new Map();
54
+ _committing = false;
55
+ _updating = false;
56
+ _finalized = false;
57
+ _signed = false;
52
58
  /**
53
59
  * Creates a new PDF document instance.
54
60
  *
@@ -75,13 +81,48 @@ export class PdfDocument extends PdfObject {
75
81
  if (options?.ownerPassword) {
76
82
  this.setOwnerPassword(options.ownerPassword);
77
83
  }
78
- this.signer = options?.signer ?? new PdfSigner();
84
+ this.signer = options?.signer ?? new PdfSigner({ document: this });
79
85
  this.linkRevisions();
86
+ this.wireResolvers(...this.objects.filter((x) => x instanceof PdfIndirectObject), ...this.revisions.map((rev) => rev.xref.trailerDict));
80
87
  this.calculateOffsets();
81
- this.securityHandler =
82
- options?.securityHandler ?? this.getSecurityHandler();
83
- this.acroForm = new PdfAcroFormManager(this);
84
- this.fonts = new PdfFontManager(this);
88
+ this.originalSecurityHandler = options?.securityHandler;
89
+ this.resetSecurityHandler();
90
+ }
91
+ resolve(objectNumber, generationNumber) {
92
+ const cacheKey = `${objectNumber} ${generationNumber}`;
93
+ const cached = this._resolvedCache.get(cacheKey);
94
+ if (cached)
95
+ return cached;
96
+ const found = this.readObject({ objectNumber, generationNumber });
97
+ if (!found) {
98
+ throw new Error(`Object ${objectNumber} ${generationNumber} not found`);
99
+ }
100
+ this.wireResolvers(found);
101
+ this._resolvedCache.set(cacheKey, found);
102
+ return found;
103
+ }
104
+ get acroform() {
105
+ const root = this.root;
106
+ const acroFormEntry = root.content.get('AcroForm');
107
+ if (!acroFormEntry)
108
+ return null;
109
+ const acroFormRef = acroFormEntry.as(PdfObjectReference)?.resolve();
110
+ if (!acroFormRef)
111
+ return null;
112
+ return acroFormRef.becomes(PdfAcroForm);
113
+ }
114
+ get pages() {
115
+ const root = this.root;
116
+ const pagesEntry = root.content.get('Pages');
117
+ if (pagesEntry) {
118
+ const pagesRef = pagesEntry.as(PdfObjectReference)?.resolve();
119
+ if (pagesRef)
120
+ return pagesRef.becomes(PdfPages);
121
+ }
122
+ const pages = new PdfPages();
123
+ this.add(pages);
124
+ root.content.set('Pages', pages.reference);
125
+ return pages;
85
126
  }
86
127
  get header() {
87
128
  return this.revisions[0].header;
@@ -132,6 +173,9 @@ export class PdfDocument extends PdfObject {
132
173
  }
133
174
  return this;
134
175
  }
176
+ hasObjectInLatestRevision(obj) {
177
+ return this.latestRevision.objects.includes(obj);
178
+ }
135
179
  /**
136
180
  * Adds objects to the document's latest revision.
137
181
  * Automatically starts a new revision if the current one is locked.
@@ -143,12 +187,21 @@ export class PdfDocument extends PdfObject {
143
187
  this.startNewRevision();
144
188
  }
145
189
  for (const obj of objects) {
146
- if (obj.isImmutable()) {
147
- throw new Error('Risky adding a immutable obj');
190
+ this.wireResolvers(obj);
191
+ if (this.hasObjectInLatestRevision(obj)) {
192
+ continue;
148
193
  }
149
- this.toBeCommitted.push(obj);
150
194
  this.latestRevision.addObject(obj);
151
195
  }
196
+ // Auto-add any referenced-but-missing objects
197
+ const missing = this.collectMissingReferences(...objects);
198
+ if (missing.length > 0) {
199
+ this.add(...missing);
200
+ }
201
+ this.update();
202
+ for (const obj of objects) {
203
+ obj.setModified(false);
204
+ }
152
205
  }
153
206
  /**
154
207
  * Packs non-stream indirect objects into compressed ObjStm containers.
@@ -290,6 +343,10 @@ export class PdfDocument extends PdfObject {
290
343
  }
291
344
  return metadataRef;
292
345
  }
346
+ resetSecurityHandler() {
347
+ this.securityHandler =
348
+ this.originalSecurityHandler ?? this.getSecurityHandler();
349
+ }
293
350
  getSecurityHandler() {
294
351
  const encryptionDictionaryRef = this.trailerDict
295
352
  .get('Encrypt')
@@ -383,11 +440,47 @@ export class PdfDocument extends PdfObject {
383
440
  }
384
441
  return true;
385
442
  }
443
+ commitIncrementalUpdates() {
444
+ if (!this.incremental || this._committing) {
445
+ return;
446
+ }
447
+ this._committing = true;
448
+ try {
449
+ for (const revision of this.revisions) {
450
+ if (!revision.locked)
451
+ continue;
452
+ for (const obj of revision.objects) {
453
+ if (!(obj instanceof PdfIndirectObject)) {
454
+ continue;
455
+ }
456
+ if (obj.isModified()) {
457
+ // Pre-register any new objects referenced by obj before
458
+ // cloning so the clone gets correct object numbers.
459
+ // Without this, proxy references (objectNumber = -1) get
460
+ // frozen into the clone as "-1 0 R" — a dangling reference.
461
+ const missing = this.collectMissingReferences(obj);
462
+ if (missing.length > 0) {
463
+ this.add(...missing);
464
+ }
465
+ const adding = obj.clone();
466
+ this.add(adding);
467
+ adding.setModified(false);
468
+ obj.setModified(false);
469
+ }
470
+ }
471
+ }
472
+ }
473
+ finally {
474
+ this._committing = false;
475
+ }
476
+ }
386
477
  /**
387
- * Decrypts all encrypted objects in the document.
388
- * Removes the security handler and encryption dictionary after decryption.
478
+ * Decrypts all encrypted object data in-place without removing
479
+ * the encryption infrastructure. Useful in incremental mode where
480
+ * the original (encrypted) bytes are preserved via cached tokens
481
+ * but the live object data needs to be readable.
389
482
  */
390
- async decrypt() {
483
+ async decryptObjects() {
391
484
  if (!this.securityHandler) {
392
485
  return;
393
486
  }
@@ -400,19 +493,55 @@ export class PdfDocument extends PdfObject {
400
493
  }
401
494
  await this.securityHandler.decryptObject(object);
402
495
  }
403
- this.securityHandler = undefined;
496
+ }
497
+ /**
498
+ * Re-encrypts all objects and updates the document structure.
499
+ * No-op if the document has no security handler (unencrypted document).
500
+ */
501
+ async finalize() {
502
+ if (this._finalized) {
503
+ throw new Error('Document has already been finalized');
504
+ }
505
+ this._finalized = true;
506
+ this.update();
507
+ if (this.securityHandler) {
508
+ await this.encrypt();
509
+ }
510
+ this.update();
511
+ await this.sign();
512
+ }
513
+ /**
514
+ * Decrypts all encrypted objects in the document.
515
+ * Removes the security handler and encryption dictionary after decryption.
516
+ */
517
+ async decrypt() {
518
+ if (!this.securityHandler) {
519
+ return;
520
+ }
521
+ await this.decryptObjects();
404
522
  this.hasEncryptionDictionary = false;
523
+ this.securityHandler = undefined;
405
524
  const encryptionDict = this.encryptionDictionary;
525
+ this.trailerDict.delete('Encrypt');
406
526
  if (encryptionDict) {
407
- await this.deleteObject(encryptionDict);
527
+ this.deleteObject(encryptionDict);
408
528
  }
409
- await this.update();
529
+ this.update();
410
530
  }
411
531
  /**
412
- * Encrypts all objects in the document using the security handler.
413
- * Creates and adds an encryption dictionary to all revisions.
532
+ * Encrypts all encryptable objects using the security handler.
533
+ * Re-uses the existing encryption dictionary or creates one if needed,
534
+ * propagating it to all revisions.
414
535
  */
415
536
  async encrypt() {
537
+ if (!this.securityHandler) {
538
+ return;
539
+ }
540
+ const addingEncryption = !this.hasEncryptionDictionary;
541
+ const wasIncremental = this.incremental;
542
+ if (addingEncryption) {
543
+ this.setIncremental(false);
544
+ }
416
545
  this.initSecurityHandler({});
417
546
  await this.securityHandler.write();
418
547
  for (const object of this.objects) {
@@ -423,20 +552,24 @@ export class PdfDocument extends PdfObject {
423
552
  continue;
424
553
  }
425
554
  await this.securityHandler.encryptObject(object);
555
+ object.setModified(false);
426
556
  }
427
- const encryptionDictObject = new PdfIndirectObject({
428
- content: this.securityHandler.dict,
429
- encryptable: false,
430
- });
431
- for (const revision of this.revisions) {
432
- revision.xref.trailerDict.set('Encrypt', encryptionDictObject.reference);
433
- if (!revision.xref.trailerDict.get('ID')) {
434
- revision.xref.trailerDict.set('ID', this.securityHandler.getDocumentId());
557
+ if (addingEncryption) {
558
+ const encryptionDictObject = new PdfIndirectObject({
559
+ content: this.securityHandler.dict,
560
+ encryptable: false,
561
+ });
562
+ this.add(encryptionDictObject);
563
+ this.hasEncryptionDictionary = true;
564
+ for (const revision of this.revisions) {
565
+ revision.xref.trailerDict.set('Encrypt', encryptionDictObject.reference);
566
+ if (!revision.xref.trailerDict.get('ID')) {
567
+ revision.xref.trailerDict.set('ID', this.securityHandler.getDocumentId());
568
+ }
435
569
  }
570
+ this.setIncremental(wasIncremental);
436
571
  }
437
- await this.commit(encryptionDictObject);
438
- this.hasEncryptionDictionary = true;
439
- await this.update();
572
+ this.update();
440
573
  }
441
574
  /**
442
575
  * Finds a compressed object by its object number within an object stream.
@@ -445,28 +578,26 @@ export class PdfDocument extends PdfObject {
445
578
  * @returns The found indirect object or undefined if not found
446
579
  * @throws Error if the object cannot be found in the expected object stream
447
580
  */
448
- async findCompressedObject(options) {
581
+ findCompressedObject(options) {
449
582
  const xrefEntry = this.xrefLookup.getObject(options.objectNumber);
450
583
  if (!(xrefEntry instanceof PdfXRefStreamCompressedEntry)) {
451
584
  throw new Error('Cannot find object inside object stream via PdfDocument.findObject');
452
585
  }
453
- const objectStreamIndirect = this.findUncompressedObject({
454
- objectNumber: xrefEntry.objectStreamNumber.value,
455
- })?.clone();
456
- if (!objectStreamIndirect) {
457
- throw new Error(`Cannot find object stream ${xrefEntry.objectStreamNumber.value} for object ${options.objectNumber}`);
458
- }
459
- if (this.securityHandler &&
460
- this.isObjectEncryptable(objectStreamIndirect)) {
461
- await this.securityHandler.decryptObject(objectStreamIndirect);
462
- }
463
- const objectStream = objectStreamIndirect.content
464
- .as(PdfStream)
465
- .parseAs(PdfObjStream);
466
- const decompressedObject = objectStream.getObject({
467
- objectNumber: options.objectNumber,
468
- });
469
- return decompressedObject;
586
+ const streamNumber = xrefEntry.objectStreamNumber.value;
587
+ let objectStream = this._objStreamCache.get(streamNumber);
588
+ if (!objectStream) {
589
+ const objectStreamIndirect = this.findUncompressedObject({
590
+ objectNumber: streamNumber,
591
+ });
592
+ if (!objectStreamIndirect) {
593
+ throw new Error(`Cannot find object stream ${streamNumber} for object ${options.objectNumber}`);
594
+ }
595
+ objectStream = objectStreamIndirect.content
596
+ .as(PdfStream)
597
+ .parseAs(PdfObjStream);
598
+ this._objStreamCache.set(streamNumber, objectStream);
599
+ }
600
+ return objectStream.getObject({ objectNumber: options.objectNumber });
470
601
  }
471
602
  /**
472
603
  * Finds an uncompressed indirect object by its object number.
@@ -502,14 +633,14 @@ export class PdfDocument extends PdfObject {
502
633
  * @param options.allowUnindexed - If true, searches unindexed objects as fallback
503
634
  * @returns A cloned and decrypted copy of the object, or undefined if not found
504
635
  */
505
- async readObject(options) {
636
+ readObject(options) {
506
637
  let foundObject;
507
638
  try {
508
639
  foundObject = this.findUncompressedObject(options);
509
640
  }
510
641
  catch (e) {
511
642
  if (e instanceof FoundCompressedObjectError) {
512
- foundObject = await this.findCompressedObject(options);
643
+ foundObject = this.findCompressedObject(options);
513
644
  }
514
645
  else {
515
646
  throw e;
@@ -524,24 +655,32 @@ export class PdfDocument extends PdfObject {
524
655
  if (!foundObject) {
525
656
  return undefined;
526
657
  }
527
- if (this.securityHandler && this.isObjectEncryptable(foundObject)) {
658
+ if (options.cloned) {
528
659
  foundObject = foundObject.clone();
529
- await this.securityHandler.decryptObject(foundObject);
530
660
  }
531
661
  return foundObject;
532
662
  }
663
+ /**
664
+ * Finds the revision that contains a given PDF object.
665
+ * Useful for determining the origin of an object across multiple revisions.
666
+ * @param obj - The PDF object to find the revision for
667
+ * @returns The PdfRevision that contains the object, or undefined if not found in any revision
668
+ */
669
+ findRevisionForObject(obj) {
670
+ return this.revisions.find((rev) => rev.objects.includes(obj));
671
+ }
533
672
  /**
534
673
  * Deletes an object from all revisions in the document.
535
674
  *
536
675
  * @param obj - The PDF object to delete
537
676
  */
538
- async deleteObject(obj) {
677
+ deleteObject(obj) {
539
678
  if (!obj)
540
679
  return;
541
680
  for (const revision of this.revisions) {
542
681
  revision.deleteObject(obj);
543
682
  }
544
- await this.update();
683
+ this.update();
545
684
  }
546
685
  /**
547
686
  * Sets the PDF version for the document.
@@ -565,10 +704,10 @@ export class PdfDocument extends PdfObject {
565
704
  if (value === this.isIncremental()) {
566
705
  return;
567
706
  }
707
+ this.incremental = value;
568
708
  for (const revision of this.revisions) {
569
709
  revision.locked = value;
570
710
  }
571
- this.startNewRevision();
572
711
  }
573
712
  /**
574
713
  * Checks if the document is in incremental mode.
@@ -576,57 +715,7 @@ export class PdfDocument extends PdfObject {
576
715
  * @returns True if all revisions are locked for incremental updates
577
716
  */
578
717
  isIncremental() {
579
- return this.latestRevision.locked;
580
- }
581
- /**
582
- * Commits pending objects to the document.
583
- * Adds objects, applies encryption if configured, and updates the document structure.
584
- *
585
- * @param newObjects - Additional objects to add before committing
586
- */
587
- async commit(...newObjects) {
588
- this.add(...newObjects);
589
- const queue = this.toBeCommitted.slice();
590
- this.toBeCommitted = [];
591
- for (const newObject of queue) {
592
- if (this.securityHandler &&
593
- newObject instanceof PdfIndirectObject &&
594
- this.isObjectEncryptable(newObject)) {
595
- await this.securityHandler.write();
596
- this.ensureEncryptionDictionary();
597
- await this.securityHandler.encryptObject(newObject);
598
- }
599
- }
600
- await this.update();
601
- }
602
- ensureEncryptionDictionary() {
603
- if (this.hasEncryptionDictionary) {
604
- return;
605
- }
606
- const encryptionDictObject = new PdfIndirectObject({
607
- content: this.securityHandler.dict,
608
- encryptable: false,
609
- });
610
- this.latestRevision.addObject(encryptionDictObject);
611
- this.trailerDict.set('Encrypt', encryptionDictObject.reference);
612
- this.hasEncryptionDictionary = true;
613
- }
614
- /**
615
- * Sets the Document Security Store (DSS) for the document.
616
- * Used for long-term validation of digital signatures.
617
- *
618
- * @param dss - The Document Security Store object to set
619
- * @throws Error if the document has no root dictionary
620
- */
621
- async setDocumentSecurityStore(dss) {
622
- let rootDictionary = this.root?.content;
623
- if (!rootDictionary) {
624
- throw new Error('Cannot set DSS - document has no root dictionary');
625
- }
626
- rootDictionary.set('DSS', dss.reference);
627
- if (!this.hasObject(dss)) {
628
- await this.commit(dss);
629
- }
718
+ return this.incremental;
630
719
  }
631
720
  /**
632
721
  * Returns tokens paired with their source objects.
@@ -648,12 +737,93 @@ export class PdfDocument extends PdfObject {
648
737
  tokenize() {
649
738
  return this.tokensWithObjects().map(({ token }) => token);
650
739
  }
740
+ wireResolvers(...objects) {
741
+ const seen = new Set();
742
+ const walk = (obj) => {
743
+ if (seen.has(obj))
744
+ return;
745
+ seen.add(obj);
746
+ if (obj instanceof PdfObjectReference &&
747
+ !(obj instanceof PdfIndirectObject)) {
748
+ obj.resolver = this;
749
+ }
750
+ else if (obj instanceof PdfStream) {
751
+ walk(obj.header);
752
+ }
753
+ else if (obj instanceof PdfDictionary) {
754
+ for (const [, value] of obj.entries()) {
755
+ if (value)
756
+ walk(value);
757
+ }
758
+ }
759
+ else if (obj instanceof PdfArray) {
760
+ for (const item of obj.items) {
761
+ walk(item);
762
+ }
763
+ }
764
+ else if (obj instanceof PdfIndirectObject) {
765
+ walk(obj.content);
766
+ }
767
+ };
768
+ for (const obj of objects) {
769
+ walk(obj);
770
+ }
771
+ }
772
+ collectMissingReferences(...objects) {
773
+ const seen = new Set();
774
+ const missing = [];
775
+ const queue = [...objects];
776
+ while (queue.length > 0) {
777
+ const obj = queue.pop();
778
+ if (seen.has(obj))
779
+ continue;
780
+ seen.add(obj);
781
+ if (obj instanceof PdfObjectReference &&
782
+ !(obj instanceof PdfIndirectObject)) {
783
+ if (obj.resolver) {
784
+ try {
785
+ const resolved = obj.resolve();
786
+ if (resolved instanceof PdfIndirectObject &&
787
+ !resolved.inPdf()) {
788
+ missing.push(resolved);
789
+ queue.push(resolved);
790
+ }
791
+ }
792
+ catch {
793
+ // Skip unresolvable references
794
+ }
795
+ }
796
+ }
797
+ else if (obj instanceof PdfStream) {
798
+ queue.push(obj.header);
799
+ }
800
+ else if (obj instanceof PdfDictionary) {
801
+ for (const [, value] of obj.entries()) {
802
+ if (value)
803
+ queue.push(value);
804
+ }
805
+ }
806
+ else if (obj instanceof PdfArray) {
807
+ for (const item of obj.items)
808
+ queue.push(item);
809
+ }
810
+ else if (obj instanceof PdfIndirectObject) {
811
+ queue.push(obj.content);
812
+ }
813
+ }
814
+ return missing;
815
+ }
651
816
  linkRevisions() {
652
817
  const xrefLookups = this.revisions.map((rev) => rev.xref);
653
818
  const indirectObjects = this.objects.filter((x) => x instanceof PdfIndirectObject);
654
819
  for (const revision of this.revisions) {
655
820
  revision.xref.linkPrev(xrefLookups);
656
- revision.xref.linkIndirectObjects(indirectObjects);
821
+ // Walk the entire prev chain to link all xref entries
822
+ let xref = revision.xref;
823
+ while (xref) {
824
+ xref.linkIndirectObjects(indirectObjects);
825
+ xref = xref.prev;
826
+ }
657
827
  }
658
828
  }
659
829
  linkOffsets() {
@@ -700,7 +870,7 @@ export class PdfDocument extends PdfObject {
700
870
  }
701
871
  updateRevisions() {
702
872
  let modified = false;
703
- this.revisions.forEach((rev, i) => {
873
+ this.revisions.forEach((rev) => {
704
874
  if (rev.isModified()) {
705
875
  modified = true;
706
876
  }
@@ -708,11 +878,72 @@ export class PdfDocument extends PdfObject {
708
878
  rev.update();
709
879
  }
710
880
  });
881
+ // Always rebuild xref binary data to reflect recalculated offsets
882
+ // This walks the entire prev chain for each revision
883
+ for (const rev of this.revisions) {
884
+ let xref = rev.xref;
885
+ while (xref) {
886
+ xref.update();
887
+ xref = xref.prev;
888
+ }
889
+ }
711
890
  }
712
- async update() {
713
- this.calculateOffsets();
714
- this.updateRevisions();
715
- await this.signer?.sign(this);
891
+ /**
892
+ * Performs a full update cycle to ensure all revisions are consistent and offsets are correct.
893
+ */
894
+ update() {
895
+ if (this._updating)
896
+ return;
897
+ this._updating = true;
898
+ try {
899
+ this.commitIncrementalUpdates();
900
+ this.flushResolvedCache();
901
+ this.registerNewReferences();
902
+ this.calculateOffsets();
903
+ this.updateRevisions();
904
+ // 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();
909
+ 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();
913
+ }
914
+ finally {
915
+ this._updating = false;
916
+ }
917
+ }
918
+ /**
919
+ * Walks all objects in the document and registers any newly created
920
+ * PdfIndirectObjects that are referenced but not yet part of the document
921
+ * (e.g. appearance streams created by generateAppearance).
922
+ */
923
+ registerNewReferences() {
924
+ const missing = this.collectMissingReferences(...this.latestRevision.objects);
925
+ if (missing.length > 0) {
926
+ this.add(...missing);
927
+ }
928
+ }
929
+ flushResolvedCache() {
930
+ const existing = new Set(this.objects);
931
+ for (const [, obj] of this._resolvedCache) {
932
+ if (obj.isModified() && !existing.has(obj)) {
933
+ this.add(obj);
934
+ existing.add(obj);
935
+ }
936
+ }
937
+ }
938
+ async sign() {
939
+ if (this._signed) {
940
+ throw new Error('Document has already been signed');
941
+ }
942
+ if (!this.signer) {
943
+ return;
944
+ }
945
+ await this.signer.sign();
946
+ this._signed = true;
716
947
  }
717
948
  /**
718
949
  * Serializes the document to a byte array.
@@ -720,24 +951,9 @@ export class PdfDocument extends PdfObject {
720
951
  * @returns The PDF document as a Uint8Array
721
952
  */
722
953
  toBytes() {
723
- this.calculateOffsets();
724
- this.updateRevisions();
954
+ this.update();
725
955
  return concatUint8Arrays(...this.revisions.map((x) => x.toBytes()));
726
956
  }
727
- /**
728
- * Serializes the document to a Base64-encoded string.
729
- *
730
- * @returns A promise that resolves to the PDF document as a Base64 string
731
- */
732
- toBase64() {
733
- const bytes = this.toBytes();
734
- let binary = '';
735
- const len = bytes.byteLength;
736
- for (let i = 0; i < len; i++) {
737
- binary += String.fromCharCode(bytes[i]);
738
- }
739
- return btoa(binary);
740
- }
741
957
  /**
742
958
  * Creates a deep copy of the document.
743
959
  *
@@ -757,8 +973,8 @@ export class PdfDocument extends PdfObject {
757
973
  * @param input - Async or sync iterable of byte arrays
758
974
  * @returns A promise that resolves to the parsed PdfDocument
759
975
  */
760
- static fromBytes(input) {
761
- return PdfReader.fromBytes(input);
976
+ static fromBytes(input, options) {
977
+ return PdfReader.fromBytes(input, options);
762
978
  }
763
979
  isModified() {
764
980
  return (super.isModified() || this.revisions.some((rev) => rev.isModified()));
@@ -769,6 +985,6 @@ export class PdfDocument extends PdfObject {
769
985
  * @returns A promise that resolves to the verification result
770
986
  */
771
987
  async verifySignatures() {
772
- return await this.signer.verify(this);
988
+ return await this.signer.verify();
773
989
  }
774
990
  }