pdf-lite 1.6.0 → 1.6.2

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 CHANGED
@@ -92,6 +92,89 @@ await document.encrypt()
92
92
  console.log(document.toString())
93
93
  ```
94
94
 
95
+ ### Filling AcroForms
96
+
97
+ ```typescript
98
+ import { PdfDocument } from 'pdf-lite/pdf/pdf-document'
99
+ import { readFile, writeFile } from 'fs/promises'
100
+
101
+ const pdfBytes = await readFile('form.pdf')
102
+ const doc = await PdfDocument.fromBytes([pdfBytes])
103
+
104
+ const form = doc.acroform
105
+ if (!form) throw new Error('No AcroForm found')
106
+
107
+ // Set multiple field values at once
108
+ form.setValues({
109
+ name: 'John Doe',
110
+ email: 'john@example.com',
111
+ subscribe: 'Yes', // checkbox: 'Yes' or 'Off'
112
+ })
113
+
114
+ // Or work with individual fields
115
+ const field = form.fields.find((f) => f.name === 'name')
116
+ field.value = 'Jane Doe'
117
+
118
+ // Export all current values
119
+ const values = form.exportData()
120
+ // => { name: 'Jane Doe', email: 'john@example.com', subscribe: 'Yes' }
121
+
122
+ await writeFile('filled.pdf', doc.toBytes())
123
+ ```
124
+
125
+ ### Generating Appearances
126
+
127
+ Appearance streams control how form fields render visually. The library can automatically generate them when field values are set, or you can generate them manually.
128
+
129
+ ```typescript
130
+ // Auto-generate appearances when setting values (default behavior)
131
+ field.value = 'Hello' // appearance is generated automatically
132
+
133
+ // Or generate manually with options
134
+ field.generateAppearance({ makeReadOnly: true })
135
+
136
+ // Configure font and size for a field
137
+ field.fontSize = 14
138
+ field.fontName = 'Helv'
139
+
140
+ // For checkbox fields
141
+ const checkbox = form.fields.find((f) => f.name === 'agree')
142
+ checkbox.checked = true
143
+ checkbox.generateAppearance()
144
+
145
+ // For choice fields (dropdowns/listboxes)
146
+ const dropdown = form.fields.find((f) => f.name === 'country')
147
+ dropdown.value = 'US'
148
+ dropdown.generateAppearance()
149
+ ```
150
+
151
+ ### Working with Fonts
152
+
153
+ ```typescript
154
+ import { PdfFont } from 'pdf-lite'
155
+
156
+ // Standard PDF fonts (built into all PDF readers)
157
+ const helvetica = PdfFont.fromStandardFont('Helvetica')
158
+ const timesBold = PdfFont.fromStandardFont('Times-Bold')
159
+ const courier = PdfFont.fromStandardFont('Courier')
160
+
161
+ // Embed custom fonts from file bytes (auto-detects TTF, OTF, WOFF)
162
+ import { readFileSync } from 'fs'
163
+ const fontData = readFileSync('MyFont.ttf')
164
+ const customFont = PdfFont.fromBytes(fontData)
165
+
166
+ // Use pre-defined font constants
167
+ const font = PdfFont.HELVETICA_BOLD
168
+
169
+ // Assign resource names for use in content streams
170
+ customFont.resourceName = 'F1'
171
+
172
+ // Add to document
173
+ document.add(customFont)
174
+ ```
175
+
176
+ **Standard font names:** Helvetica, Helvetica-Bold, Helvetica-Oblique, Helvetica-BoldOblique, Times-Roman, Times-Bold, Times-Italic, Times-BoldItalic, Courier, Courier-Bold, Courier-Oblique, Courier-BoldOblique, Symbol, ZapfDingbats
177
+
95
178
  ### Signing PDFs
96
179
 
97
180
  ```typescript
@@ -187,14 +270,47 @@ Long-Term Validation (LTV) support ensures that digital signatures remain valid
187
270
  - [x] Timestamping
188
271
  - [x] Verification of existing signatures
189
272
 
190
- ### AcroForm filling
273
+ ### AcroForms
274
+
275
+ Supports reading, filling, and creating AcroForm fields within PDF documents.
191
276
 
192
- Supports filling out AcroForm forms within PDF documents, allowing for dynamic content generation and user interaction.
277
+ **Field types:**
193
278
 
194
- - [x] Text fields
279
+ - [x] Text fields (single-line, multi-line, comb, password)
195
280
  - [x] Checkboxes
196
281
  - [x] Radio buttons
197
- - [x] Dropdowns
282
+ - [x] Dropdowns (combo boxes)
283
+ - [x] List boxes
284
+ - [x] Signature fields
285
+
286
+ **Form operations:**
287
+
288
+ - [x] Import/export field values (`importData`, `exportData`, `setValues`)
289
+ - [x] Read individual field properties (name, value, type, flags)
290
+ - [x] Hierarchical field support (parent/child/sibling fields)
291
+
292
+ ### Appearance Streams
293
+
294
+ Automatic generation of visual appearance streams for form fields, so filled forms render correctly in all PDF viewers without relying on `NeedAppearances`.
295
+
296
+ - [x] Text field rendering (word wrap, auto-sizing, comb layout)
297
+ - [x] Checkbox/radio button rendering
298
+ - [x] Dropdown/list box rendering
299
+ - [x] Font resolution from field, form, or document resources
300
+ - [x] Auto-size font (fontSize=0) for single-line text fields
301
+ - [x] Custom font color via Default Appearance strings
302
+
303
+ ### Fonts
304
+
305
+ Supports standard PDF fonts and embedding custom fonts.
306
+
307
+ - [x] All 14 standard PDF fonts (Helvetica, Times, Courier, Symbol, ZapfDingbats + variants)
308
+ - [x] TrueType font embedding (TTF)
309
+ - [x] OpenType font embedding (OTF, non-CFF)
310
+ - [x] WOFF font embedding
311
+ - [x] Auto-detect font format via `PdfFont.fromBytes()`
312
+ - [x] Font encoding maps (WinAnsi, Unicode/Identity-H)
313
+ - [x] Character width metrics
198
314
 
199
315
  ### XFA Forms
200
316
 
@@ -192,13 +192,14 @@ export class PdfGraphics {
192
192
  const startSize = this.currentFont.size;
193
193
  const minSize = 0.5;
194
194
  const fits = (size) => {
195
- if (this.measureTextWidth(text, size) > maxWidth)
196
- return false;
197
195
  if (maxHeight !== undefined) {
196
+ // Wrapping mode: text is allowed to span multiple lines,
197
+ // so only check that the wrapped result fits the box.
198
198
  const lines = this.wrapTextToLines(text, maxWidth, size);
199
199
  return lines.length * size * lineHeight <= maxHeight;
200
200
  }
201
- return true;
201
+ // Single-line mode: text must fit within maxWidth.
202
+ return this.measureTextWidth(text, size) <= maxWidth;
202
203
  };
203
204
  if (fits(startSize))
204
205
  return startSize;
@@ -1,6 +1,7 @@
1
1
  import { PdfDefaultAppearance } from '../fields/pdf-default-appearance.js';
2
2
  import { PdfAppearanceStream } from './pdf-appearance-stream.js';
3
3
  import { PdfGraphics } from './pdf-graphics.js';
4
+ const DEFAULT_FONT_SIZE = 12;
4
5
  /**
5
6
  * Appearance stream for text fields (single-line, multiline, comb).
6
7
  * Enhanced with word wrapping and automatic font scaling.
@@ -16,29 +17,44 @@ export class PdfTextAppearanceStream extends PdfAppearanceStream {
16
17
  const padding = 2;
17
18
  const availableWidth = width - 2 * padding;
18
19
  const availableHeight = height - 2 * padding;
20
+ const autoSize = ctx.da.fontSize <= 0;
19
21
  // Create graphics with font context for text measurement
20
22
  const g = new PdfGraphics({
21
23
  resolvedFonts: ctx.resolvedFonts,
22
24
  });
23
25
  g.beginMarkedContent();
24
26
  g.save();
25
- // Set initial font to enable measurement
26
- g.setDefaultAppearance(ctx.da);
27
- let finalFontSize = ctx.da.fontSize;
28
- let lines = [];
29
- if (ctx.multiline) {
27
+ // Bootstrap with a reference size so measureTextWidth works
28
+ g.setDefaultAppearance(new PdfDefaultAppearance(ctx.da.fontName, DEFAULT_FONT_SIZE, ctx.da.colorOp));
29
+ // ── Determine font size ──────────────────────────────────────
30
+ let finalFontSize;
31
+ if (autoSize) {
32
+ // Acrobat auto-size: default to 12pt with wrapping, then
33
+ // shrink only if the wrapped text still doesn't fit.
34
+ finalFontSize = DEFAULT_FONT_SIZE;
30
35
  const testLines = g.wrapTextToLines(value, availableWidth);
31
- const lineHeight = finalFontSize * 1.2;
32
- if (testLines.length * lineHeight > availableHeight) {
33
- // Scale font down to fit
36
+ if (testLines.length * DEFAULT_FONT_SIZE * 1.2 > availableHeight) {
34
37
  finalFontSize = g.calculateFittingFontSize(value, availableWidth, availableHeight, 1.2);
35
- const adjustedDA = new PdfDefaultAppearance(ctx.da.fontName, finalFontSize, ctx.da.colorOp);
36
- g.setDefaultAppearance(adjustedDA);
37
- lines = g.wrapTextToLines(value, availableWidth);
38
38
  }
39
- else {
40
- lines = testLines;
39
+ finalFontSize = Math.max(finalFontSize, 0.5);
40
+ }
41
+ else {
42
+ finalFontSize = ctx.da.fontSize;
43
+ }
44
+ // ── Render ───────────────────────────────────────────────────
45
+ const finalDA = new PdfDefaultAppearance(ctx.da.fontName, finalFontSize, ctx.da.colorOp);
46
+ g.setDefaultAppearance(finalDA);
47
+ let lines = [];
48
+ if (ctx.multiline || autoSize) {
49
+ if (!autoSize) {
50
+ const testLines = g.wrapTextToLines(value, availableWidth);
51
+ const lineHeight = finalFontSize * 1.2;
52
+ if (testLines.length * lineHeight > availableHeight) {
53
+ finalFontSize = g.calculateFittingFontSize(value, availableWidth, availableHeight, 1.2);
54
+ g.setDefaultAppearance(new PdfDefaultAppearance(ctx.da.fontName, finalFontSize, ctx.da.colorOp));
55
+ }
41
56
  }
57
+ lines = g.wrapTextToLines(value, availableWidth);
42
58
  const renderLineHeight = finalFontSize * 1.2;
43
59
  const startY = height - padding - finalFontSize;
44
60
  g.beginText();
@@ -53,7 +69,6 @@ export class PdfTextAppearanceStream extends PdfAppearanceStream {
53
69
  else if (ctx.comb && ctx.maxLen) {
54
70
  const cellWidth = width / ctx.maxLen;
55
71
  const chars = [...value];
56
- // Calculate font size to fit the widest character in its cell
57
72
  let maxCharWidth = 0;
58
73
  let widestChar = chars[0] ?? '';
59
74
  for (const char of chars) {
@@ -65,8 +80,7 @@ export class PdfTextAppearanceStream extends PdfAppearanceStream {
65
80
  }
66
81
  if (maxCharWidth > cellWidth) {
67
82
  finalFontSize = g.calculateFittingFontSize(widestChar, cellWidth);
68
- const adjustedDA = new PdfDefaultAppearance(ctx.da.fontName, finalFontSize, ctx.da.colorOp);
69
- g.setDefaultAppearance(adjustedDA);
83
+ g.setDefaultAppearance(new PdfDefaultAppearance(ctx.da.fontName, finalFontSize, ctx.da.colorOp));
70
84
  }
71
85
  const textY = (height - finalFontSize) / 2 + finalFontSize * 0.2;
72
86
  g.beginText();
@@ -79,12 +93,13 @@ export class PdfTextAppearanceStream extends PdfAppearanceStream {
79
93
  g.endText();
80
94
  }
81
95
  else {
82
- // Single line text
83
- const textWidth = g.measureTextWidth(value);
84
- if (textWidth > availableWidth) {
85
- finalFontSize = g.calculateFittingFontSize(value, availableWidth);
86
- const adjustedDA = new PdfDefaultAppearance(ctx.da.fontName, finalFontSize, ctx.da.colorOp);
87
- g.setDefaultAppearance(adjustedDA);
96
+ // Single line — for non-auto-size, shrink if text overflows
97
+ if (!autoSize) {
98
+ const textWidth = g.measureTextWidth(value);
99
+ if (textWidth > availableWidth) {
100
+ finalFontSize = g.calculateFittingFontSize(value, availableWidth);
101
+ g.setDefaultAppearance(new PdfDefaultAppearance(ctx.da.fontName, finalFontSize, ctx.da.colorOp));
102
+ }
88
103
  }
89
104
  const textY = (height - finalFontSize) / 2 + finalFontSize * 0.2;
90
105
  g.beginText();
@@ -20,20 +20,35 @@ export class PdfButtonFormField extends PdfFormField {
20
20
  this.content.delete('AS');
21
21
  return false;
22
22
  }
23
- this.content.set('V', new PdfName(strVal));
24
- fieldParent?.content.set('V', new PdfName(strVal));
25
- this.content.set('AS', new PdfName(strVal));
23
+ // Check if the value matches an existing appearance state;
24
+ // otherwise map truthy values to the widget's "on" state (the
25
+ // first state that isn't "Off"), falling back to "Yes".
26
+ const states = this.appearanceStates;
27
+ let resolved;
28
+ if (states.includes(strVal)) {
29
+ resolved = strVal;
30
+ }
31
+ else if (strVal === 'Off' || strVal === 'No') {
32
+ resolved = 'Off';
33
+ }
34
+ else {
35
+ resolved = states.find((s) => s !== 'Off') ?? 'Yes';
36
+ }
37
+ this.content.set('V', new PdfName(resolved));
38
+ fieldParent?.content.set('V', new PdfName(resolved));
39
+ this.content.set('AS', new PdfName(resolved));
26
40
  return true;
27
41
  }
28
42
  get checked() {
29
43
  const v = this.content.get('V') ?? this.parent?.content.get('V');
30
- return v instanceof PdfName && v.value === 'Yes';
44
+ return v instanceof PdfName && v.value !== 'Off';
31
45
  }
32
46
  set checked(isChecked) {
33
47
  const target = this.parent ?? this;
34
48
  if (isChecked) {
35
- target.content.set('V', new PdfName('Yes'));
36
- this.content.set('AS', new PdfName('Yes'));
49
+ const onState = this.appearanceStates.find((s) => s !== 'Off') ?? 'Yes';
50
+ target.content.set('V', new PdfName(onState));
51
+ this.content.set('AS', new PdfName(onState));
37
52
  }
38
53
  else {
39
54
  target.content.set('V', new PdfName('Off'));
@@ -56,8 +71,9 @@ export class PdfButtonFormField extends PdfFormField {
56
71
  height,
57
72
  contentStream: '',
58
73
  });
74
+ const onState = this.appearanceStates.find((s) => s !== 'Off') ?? 'Yes';
59
75
  this.setAppearanceStream({
60
- Yes: yesAppearance,
76
+ [onState]: yesAppearance,
61
77
  Off: noAppearance,
62
78
  });
63
79
  if (options?.makeReadOnly) {
@@ -46,14 +46,11 @@ export class PdfDefaultAppearance extends PdfString {
46
46
  return `/${this._fontName} ${this._fontSize} Tf ${this._colorOp}`;
47
47
  }
48
48
  static parse(da) {
49
- const fontMatch = da.match(/\/(\w+)\s+([\d.]+)\s+Tf/);
49
+ const fontMatch = da.match(/\/([\w-]+)\s+([\d.]+)\s+Tf/);
50
50
  if (!fontMatch)
51
51
  return null;
52
52
  const fontName = fontMatch[1];
53
- let fontSize = parseFloat(fontMatch[2]);
54
- if (!fontSize || fontSize <= 0) {
55
- fontSize = 12;
56
- }
53
+ const fontSize = parseFloat(fontMatch[2]) || 0;
57
54
  let colorOp = '0 g';
58
55
  const rgMatch = da.match(/([\d.]+\s+[\d.]+\s+[\d.]+)\s+rg/);
59
56
  const gMatch = da.match(/([\d.]+)\s+g/);
@@ -107,6 +107,11 @@ export declare abstract class PdfFormField extends PdfWidgetAnnotation {
107
107
  setAppearanceStream(stream: PdfIndirectObject | {
108
108
  [key: string]: PdfIndirectObject;
109
109
  }): void;
110
+ /**
111
+ * Returns the list of appearance state names from the normal appearance
112
+ * dictionary (e.g. ["Yes", "Off"] for a checkbox).
113
+ */
114
+ get appearanceStates(): string[];
110
115
  getAppearanceStream(setting?: string): PdfIndirectObject<PdfStream> | null;
111
116
  private static _fallbackCtor?;
112
117
  private static _registry;
@@ -480,6 +480,17 @@ export class PdfFormField extends PdfWidgetAnnotation {
480
480
  this.appearanceStreamDict.set('N', dict);
481
481
  }
482
482
  }
483
+ /**
484
+ * Returns the list of appearance state names from the normal appearance
485
+ * dictionary (e.g. ["Yes", "Off"] for a checkbox).
486
+ */
487
+ get appearanceStates() {
488
+ const n = this.appearanceStreamDict?.get('N');
489
+ if (n instanceof PdfDictionary) {
490
+ return Array.from(n.entries(), ([key]) => key);
491
+ }
492
+ return [];
493
+ }
483
494
  getAppearanceStream(setting) {
484
495
  const n = this.appearanceStreamDict?.get('N');
485
496
  if (!n)
@@ -94,6 +94,9 @@ export declare class PdfStream<T extends PdfDictionary = PdfDictionary> extends
94
94
  static fromString(data: string): PdfStream;
95
95
  }
96
96
  export declare class PdfObjStream extends PdfStream {
97
+ private _parsedObjects?;
98
+ set raw(data: ByteArray);
99
+ get raw(): ByteArray;
97
100
  constructor(options: {
98
101
  header: PdfDictionary;
99
102
  original: ByteArray | string;
@@ -101,6 +104,7 @@ export declare class PdfObjStream extends PdfStream {
101
104
  });
102
105
  static fromObjects(objects: Iterable<PdfIndirectObject>): PdfObjStream;
103
106
  getObjectStream(): Generator<PdfIndirectObject>;
107
+ private getParsedObjects;
104
108
  getObject(options: {
105
109
  objectNumber: number;
106
110
  }): PdfIndirectObject | undefined;
@@ -486,6 +486,14 @@ export class PdfStream extends PdfObject {
486
486
  }
487
487
  }
488
488
  export class PdfObjStream extends PdfStream {
489
+ _parsedObjects;
490
+ set raw(data) {
491
+ super.raw = data;
492
+ this._parsedObjects = undefined;
493
+ }
494
+ get raw() {
495
+ return super.raw;
496
+ }
489
497
  constructor(options) {
490
498
  super(options);
491
499
  if (!this.isType('ObjStm')) {
@@ -548,8 +556,11 @@ export class PdfObjStream extends PdfStream {
548
556
  const { value: obj, done } = reader.next();
549
557
  if (done)
550
558
  break;
551
- if (obj instanceof PdfNumber) {
552
- // Collect object numbers and byte offsets
559
+ if (obj instanceof PdfNumber &&
560
+ (totalObjects === 0 || numbers.length < totalObjects * 2)) {
561
+ // Collect object numbers and byte offsets from the header section.
562
+ // Stop once we have all 2*N pairs so that object values that happen
563
+ // to be plain integers are not mistakenly treated as header entries.
553
564
  numbers.push(obj);
554
565
  }
555
566
  else {
@@ -574,16 +585,20 @@ export class PdfObjStream extends PdfStream {
574
585
  }
575
586
  }
576
587
  }
577
- getObject(options) {
578
- for (const obj of this.getObjectStream()) {
579
- if (obj.objectNumber === options.objectNumber) {
580
- return obj;
588
+ getParsedObjects() {
589
+ if (!this._parsedObjects) {
590
+ this._parsedObjects = new Map();
591
+ for (const obj of this.getObjectStream()) {
592
+ this._parsedObjects.set(obj.objectNumber, obj);
581
593
  }
582
594
  }
583
- return undefined;
595
+ return this._parsedObjects;
596
+ }
597
+ getObject(options) {
598
+ return this.getParsedObjects().get(options.objectNumber);
584
599
  }
585
600
  getObjects() {
586
- return Array.from(this.getObjectStream());
601
+ return Array.from(this.getParsedObjects().values());
587
602
  }
588
603
  cloneImpl() {
589
604
  return new PdfObjStream({
@@ -42,7 +42,9 @@ export declare class PdfDocument extends PdfObject implements IPdfObjectResolver
42
42
  private originalSecurityHandler?;
43
43
  private hasEncryptionDictionary?;
44
44
  private _resolvedCache;
45
+ private _objStreamCache;
45
46
  private _committing;
47
+ private _updating;
46
48
  private _finalized;
47
49
  private _signed;
48
50
  /**
@@ -50,7 +50,9 @@ export class PdfDocument extends PdfObject {
50
50
  originalSecurityHandler;
51
51
  hasEncryptionDictionary = false;
52
52
  _resolvedCache = new Map();
53
+ _objStreamCache = new Map();
53
54
  _committing = false;
55
+ _updating = false;
54
56
  _finalized = false;
55
57
  _signed = false;
56
58
  /**
@@ -452,6 +454,14 @@ export class PdfDocument extends PdfObject {
452
454
  continue;
453
455
  }
454
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
+ }
455
465
  const adding = obj.clone();
456
466
  this.add(adding);
457
467
  adding.setModified(false);
@@ -573,19 +583,21 @@ export class PdfDocument extends PdfObject {
573
583
  if (!(xrefEntry instanceof PdfXRefStreamCompressedEntry)) {
574
584
  throw new Error('Cannot find object inside object stream via PdfDocument.findObject');
575
585
  }
576
- const objectStreamIndirect = this.findUncompressedObject({
577
- objectNumber: xrefEntry.objectStreamNumber.value,
578
- })?.clone();
579
- if (!objectStreamIndirect) {
580
- throw new Error(`Cannot find object stream ${xrefEntry.objectStreamNumber.value} for object ${options.objectNumber}`);
581
- }
582
- const objectStream = objectStreamIndirect.content
583
- .as(PdfStream)
584
- .parseAs(PdfObjStream);
585
- const decompressedObject = objectStream.getObject({
586
- objectNumber: options.objectNumber,
587
- });
588
- 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 });
589
601
  }
590
602
  /**
591
603
  * Finds an uncompressed indirect object by its object number.
@@ -880,20 +892,28 @@ export class PdfDocument extends PdfObject {
880
892
  * Performs a full update cycle to ensure all revisions are consistent and offsets are correct.
881
893
  */
882
894
  update() {
883
- this.commitIncrementalUpdates();
884
- this.flushResolvedCache();
885
- this.registerNewReferences();
886
- this.calculateOffsets();
887
- this.updateRevisions();
888
- // Second pass: xref binary may have changed size (e.g. FlateDecode removed
889
- // from xref stream), shifting objects that follow it. Recalculate so entry
890
- // byteOffset refs hold the new positions, then rebuild the xref binary once
891
- // more so the baked bytes match those positions.
892
- this.calculateOffsets();
893
- this.updateRevisions();
894
- // Third pass: confirm positions are stable (xref binary size should not
895
- // change again because W widths and entry count are the same).
896
- this.calculateOffsets();
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
+ }
897
917
  }
898
918
  /**
899
919
  * Walks all objects in the document and registers any newly created
@@ -907,9 +927,11 @@ export class PdfDocument extends PdfObject {
907
927
  }
908
928
  }
909
929
  flushResolvedCache() {
930
+ const existing = new Set(this.objects);
910
931
  for (const [, obj] of this._resolvedCache) {
911
- if (obj.isModified() && !this.objects.includes(obj)) {
932
+ if (obj.isModified() && !existing.has(obj)) {
912
933
  this.add(obj);
934
+ existing.add(obj);
913
935
  }
914
936
  }
915
937
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pdf-lite",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "main": "dist/index.js",
5
5
  "type": "module",
6
6
  "exports": {