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 +120 -4
- package/dist/acroform/appearance/pdf-graphics.js +4 -3
- package/dist/acroform/appearance/pdf-text-appearance-stream.js +37 -22
- package/dist/acroform/fields/pdf-button-form-field.js +23 -7
- package/dist/acroform/fields/pdf-default-appearance.js +2 -5
- package/dist/acroform/fields/pdf-form-field.d.ts +5 -0
- package/dist/acroform/fields/pdf-form-field.js +11 -0
- package/dist/core/objects/pdf-stream.d.ts +4 -0
- package/dist/core/objects/pdf-stream.js +23 -8
- package/dist/pdf/pdf-document.d.ts +2 -0
- package/dist/pdf/pdf-document.js +50 -28
- package/package.json +1 -1
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
|
-
###
|
|
273
|
+
### AcroForms
|
|
274
|
+
|
|
275
|
+
Supports reading, filling, and creating AcroForm fields within PDF documents.
|
|
191
276
|
|
|
192
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
26
|
-
g.setDefaultAppearance(ctx.da);
|
|
27
|
-
|
|
28
|
-
let
|
|
29
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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
|
|
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.
|
|
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
|
/**
|
package/dist/pdf/pdf-document.js
CHANGED
|
@@ -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
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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.
|
|
884
|
-
|
|
885
|
-
this.
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
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() && !
|
|
932
|
+
if (obj.isModified() && !existing.has(obj)) {
|
|
912
933
|
this.add(obj);
|
|
934
|
+
existing.add(obj);
|
|
913
935
|
}
|
|
914
936
|
}
|
|
915
937
|
}
|