pdf-lite 1.3.0 → 1.3.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.
- package/dist/acroform/acroform.d.ts +258 -23
- package/dist/acroform/acroform.js +986 -174
- package/dist/core/decoder.d.ts +1 -1
- package/dist/core/decoder.js +1 -1
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +2 -2
- package/dist/core/objects/pdf-array.d.ts +1 -0
- package/dist/core/objects/pdf-array.js +4 -0
- package/dist/core/objects/pdf-dictionary.d.ts +1 -0
- package/dist/core/objects/pdf-dictionary.js +12 -0
- package/dist/core/objects/pdf-hexadecimal.d.ts +3 -1
- package/dist/core/objects/pdf-hexadecimal.js +14 -2
- package/dist/core/objects/pdf-indirect-object.d.ts +5 -3
- package/dist/core/objects/pdf-indirect-object.js +23 -5
- package/dist/core/objects/pdf-number.js +3 -0
- package/dist/core/objects/pdf-object.d.ts +6 -0
- package/dist/core/objects/pdf-object.js +10 -0
- package/dist/core/objects/pdf-stream.js +3 -0
- package/dist/core/objects/pdf-string.js +3 -0
- package/dist/core/ref.d.ts +5 -0
- package/dist/core/ref.js +14 -0
- package/dist/core/serializer.d.ts +1 -1
- package/dist/core/serializer.js +1 -1
- package/dist/core/tokeniser.d.ts +2 -2
- package/dist/core/tokeniser.js +2 -2
- package/dist/fonts/font-manager.js +6 -8
- package/dist/pdf/pdf-document.d.ts +6 -5
- package/dist/pdf/pdf-document.js +29 -21
- package/dist/pdf/pdf-revision.d.ts +33 -4
- package/dist/pdf/pdf-revision.js +100 -26
- package/dist/pdf/pdf-xref-lookup.js +3 -2
- package/dist/xfa/manager.js +2 -4
- package/package.json +1 -1
- /package/dist/core/{incremental-parser.d.ts → parser/incremental-parser.d.ts} +0 -0
- /package/dist/core/{incremental-parser.js → parser/incremental-parser.js} +0 -0
- /package/dist/core/{parser.d.ts → parser/parser.d.ts} +0 -0
- /package/dist/core/{parser.js → parser/parser.js} +0 -0
|
@@ -6,6 +6,7 @@ import { PdfIndirectObject } from '../core/objects/pdf-indirect-object.js';
|
|
|
6
6
|
import { PdfName } from '../core/objects/pdf-name.js';
|
|
7
7
|
import { PdfBoolean } from '../core/objects/pdf-boolean.js';
|
|
8
8
|
import { PdfNumber } from '../core/objects/pdf-number.js';
|
|
9
|
+
import { PdfStream } from '../core/objects/pdf-stream.js';
|
|
9
10
|
import { buildEncodingMap, decodeWithFontEncoding, } from '../utils/decodeWithFontEncoding.js';
|
|
10
11
|
/**
|
|
11
12
|
* Field types for AcroForm fields
|
|
@@ -16,68 +17,94 @@ export const PdfFieldType = {
|
|
|
16
17
|
Choice: 'Ch',
|
|
17
18
|
Signature: 'Sig',
|
|
18
19
|
};
|
|
19
|
-
export class PdfAcroFormField extends
|
|
20
|
+
export class PdfAcroFormField extends PdfIndirectObject {
|
|
20
21
|
parent;
|
|
21
|
-
|
|
22
|
+
defaultGenerateAppearance = true;
|
|
23
|
+
_appearanceStream;
|
|
24
|
+
_appearanceStreamYes; // For button fields: checked state
|
|
22
25
|
form;
|
|
23
26
|
constructor(options) {
|
|
24
|
-
super(
|
|
25
|
-
|
|
27
|
+
super(options?.other ??
|
|
28
|
+
new PdfIndirectObject({ content: new PdfDictionary() }));
|
|
26
29
|
this.form = options?.form;
|
|
27
30
|
}
|
|
31
|
+
get encodingMap() {
|
|
32
|
+
const fontName = this.fontName;
|
|
33
|
+
if (!fontName)
|
|
34
|
+
return undefined;
|
|
35
|
+
return this.form?.fontEncodingMaps?.get(fontName);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Convenience method to check if field dictionary is modified
|
|
39
|
+
*/
|
|
40
|
+
isModified() {
|
|
41
|
+
return this.content.isModified();
|
|
42
|
+
}
|
|
28
43
|
/**
|
|
29
44
|
* Gets the field type
|
|
30
45
|
*/
|
|
31
46
|
get fieldType() {
|
|
32
|
-
|
|
47
|
+
const ft = this.content.get('FT')?.value;
|
|
48
|
+
switch (ft) {
|
|
49
|
+
case 'Tx':
|
|
50
|
+
return 'Text';
|
|
51
|
+
case 'Btn':
|
|
52
|
+
return 'Button';
|
|
53
|
+
case 'Ch':
|
|
54
|
+
return 'Choice';
|
|
55
|
+
case 'Sig':
|
|
56
|
+
return 'Signature';
|
|
57
|
+
default:
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
33
60
|
}
|
|
34
61
|
set fieldType(type) {
|
|
35
62
|
if (type === null) {
|
|
36
|
-
this.delete('FT');
|
|
63
|
+
this.content.delete('FT');
|
|
37
64
|
}
|
|
38
65
|
else {
|
|
39
|
-
this.set('FT', new PdfName(type));
|
|
66
|
+
this.content.set('FT', new PdfName(PdfFieldType[type]));
|
|
40
67
|
}
|
|
41
68
|
}
|
|
42
69
|
get rect() {
|
|
43
|
-
const rectArray = this.get('Rect')?.as((PdfArray));
|
|
70
|
+
const rectArray = this.content.get('Rect')?.as((PdfArray));
|
|
44
71
|
if (!rectArray)
|
|
45
72
|
return null;
|
|
46
73
|
return rectArray.items.map((num) => num.value);
|
|
47
74
|
}
|
|
48
75
|
set rect(rect) {
|
|
49
76
|
if (rect === null) {
|
|
50
|
-
this.delete('Rect');
|
|
77
|
+
this.content.delete('Rect');
|
|
51
78
|
return;
|
|
52
79
|
}
|
|
53
80
|
const rectArray = new PdfArray(rect.map((num) => new PdfNumber(num)));
|
|
54
|
-
this.set('Rect', rectArray);
|
|
81
|
+
this.content.set('Rect', rectArray);
|
|
55
82
|
}
|
|
56
83
|
get parentRef() {
|
|
57
|
-
const ref = this.get('P')?.as(PdfObjectReference);
|
|
84
|
+
const ref = this.content.get('P')?.as(PdfObjectReference);
|
|
58
85
|
return ref ?? null;
|
|
59
86
|
}
|
|
60
87
|
set parentRef(ref) {
|
|
61
88
|
if (ref === null) {
|
|
62
|
-
this.delete('P');
|
|
89
|
+
this.content.delete('P');
|
|
63
90
|
}
|
|
64
91
|
else {
|
|
65
|
-
this.set('P', ref);
|
|
92
|
+
this.content.set('P', ref);
|
|
66
93
|
}
|
|
67
94
|
}
|
|
68
95
|
get isWidget() {
|
|
69
|
-
const type = this.get('Type')?.as(PdfName)?.value;
|
|
70
|
-
const subtype = this.get('Subtype')?.as(PdfName)?.value;
|
|
96
|
+
const type = this.content.get('Type')?.as(PdfName)?.value;
|
|
97
|
+
const subtype = this.content.get('Subtype')?.as(PdfName)?.value;
|
|
71
98
|
return type === 'Annot' && subtype === 'Widget';
|
|
72
99
|
}
|
|
73
100
|
set isWidget(isWidget) {
|
|
74
101
|
if (isWidget) {
|
|
75
|
-
this.set('Type', new PdfName('Annot'));
|
|
76
|
-
this.set('Subtype', new PdfName('Widget'));
|
|
102
|
+
this.content.set('Type', new PdfName('Annot'));
|
|
103
|
+
this.content.set('Subtype', new PdfName('Widget'));
|
|
77
104
|
}
|
|
78
105
|
else {
|
|
79
|
-
this.delete('Type');
|
|
80
|
-
this.delete('Subtype');
|
|
106
|
+
this.content.delete('Type');
|
|
107
|
+
this.content.delete('Subtype');
|
|
81
108
|
}
|
|
82
109
|
}
|
|
83
110
|
/**
|
|
@@ -85,7 +112,7 @@ export class PdfAcroFormField extends PdfDictionary {
|
|
|
85
112
|
*/
|
|
86
113
|
get name() {
|
|
87
114
|
const parentName = this.parent?.name ?? '';
|
|
88
|
-
const ownName = this.get('T')?.as(PdfString)?.value ?? '';
|
|
115
|
+
const ownName = this.content.get('T')?.as(PdfString)?.value ?? '';
|
|
89
116
|
if (parentName && ownName) {
|
|
90
117
|
return `${parentName}.${ownName}`;
|
|
91
118
|
}
|
|
@@ -95,13 +122,13 @@ export class PdfAcroFormField extends PdfDictionary {
|
|
|
95
122
|
* Sets the field name
|
|
96
123
|
*/
|
|
97
124
|
set name(name) {
|
|
98
|
-
this.set('T', new PdfString(name));
|
|
125
|
+
this.content.set('T', new PdfString(name));
|
|
99
126
|
}
|
|
100
127
|
/**
|
|
101
128
|
* Gets the default value
|
|
102
129
|
*/
|
|
103
130
|
get defaultValue() {
|
|
104
|
-
const dv = this.get('DV');
|
|
131
|
+
const dv = this.content.get('DV');
|
|
105
132
|
if (dv instanceof PdfString) {
|
|
106
133
|
return dv.value;
|
|
107
134
|
}
|
|
@@ -115,24 +142,22 @@ export class PdfAcroFormField extends PdfDictionary {
|
|
|
115
142
|
*/
|
|
116
143
|
set defaultValue(val) {
|
|
117
144
|
const fieldType = this.fieldType;
|
|
118
|
-
if (fieldType ===
|
|
119
|
-
this.set('DV', new PdfName(val));
|
|
145
|
+
if (fieldType === 'Button') {
|
|
146
|
+
this.content.set('DV', new PdfName(val));
|
|
120
147
|
}
|
|
121
148
|
else {
|
|
122
|
-
this.set('DV', new PdfString(val));
|
|
149
|
+
this.content.set('DV', new PdfString(val));
|
|
123
150
|
}
|
|
124
151
|
}
|
|
125
152
|
get value() {
|
|
126
|
-
const v = this.get('V');
|
|
153
|
+
const v = this.content.get('V');
|
|
127
154
|
if (v instanceof PdfString) {
|
|
128
155
|
// UTF-16BE strings should always use UTF-16BE decoding regardless of font encoding
|
|
129
156
|
if (v.isUTF16BE) {
|
|
130
157
|
return v.value; // Use PdfString's built-in UTF-16BE decoder
|
|
131
158
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (encodingMap !== undefined) {
|
|
135
|
-
return decodeWithFontEncoding(v.raw, encodingMap);
|
|
159
|
+
if (this.encodingMap) {
|
|
160
|
+
return decodeWithFontEncoding(v.raw, this.encodingMap);
|
|
136
161
|
}
|
|
137
162
|
return v.value;
|
|
138
163
|
}
|
|
@@ -141,62 +166,49 @@ export class PdfAcroFormField extends PdfDictionary {
|
|
|
141
166
|
}
|
|
142
167
|
return '';
|
|
143
168
|
}
|
|
144
|
-
/**
|
|
145
|
-
* Gets the cached encoding map for this field's font, if available.
|
|
146
|
-
* Returns undefined if no encoding has been cached yet.
|
|
147
|
-
*/
|
|
148
|
-
getCachedEncodingMap() {
|
|
149
|
-
if (!this.form)
|
|
150
|
-
return undefined;
|
|
151
|
-
// Parse font name from DA (default appearance) string
|
|
152
|
-
const da = this.get('DA')?.as(PdfString)?.value;
|
|
153
|
-
if (!da)
|
|
154
|
-
return undefined;
|
|
155
|
-
// Extract font name from DA string (format: /FontName size Tf ...)
|
|
156
|
-
const fontMatch = da.match(/\/(\w+)\s+[\d.]+\s+Tf/);
|
|
157
|
-
if (!fontMatch)
|
|
158
|
-
return undefined;
|
|
159
|
-
const fontName = fontMatch[1];
|
|
160
|
-
return this.form.fontEncodingMaps.get(fontName);
|
|
161
|
-
}
|
|
162
169
|
set value(val) {
|
|
163
|
-
|
|
164
|
-
|
|
170
|
+
if (this.value === val) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const fieldType = this.fieldType;
|
|
174
|
+
if (fieldType === 'Button') {
|
|
175
|
+
val = val instanceof PdfString ? val.value : val;
|
|
165
176
|
if (val.trim() === '') {
|
|
166
|
-
this.delete('V');
|
|
167
|
-
this.delete('AS');
|
|
177
|
+
this.content.delete('V');
|
|
178
|
+
this.content.delete('AS');
|
|
168
179
|
return;
|
|
169
180
|
}
|
|
170
|
-
this.set('V', new PdfName(val));
|
|
171
|
-
this.set('AS', new PdfName(val));
|
|
181
|
+
this.content.set('V', new PdfName(val));
|
|
182
|
+
this.content.set('AS', new PdfName(val));
|
|
172
183
|
}
|
|
173
184
|
else {
|
|
174
|
-
this.set('V', new PdfString(val));
|
|
185
|
+
this.content.set('V', val instanceof PdfString ? val : new PdfString(val));
|
|
186
|
+
}
|
|
187
|
+
if (this.defaultGenerateAppearance) {
|
|
188
|
+
this.generateAppearance();
|
|
175
189
|
}
|
|
176
190
|
}
|
|
177
191
|
get checked() {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const v = this.get('V');
|
|
192
|
+
if (this.fieldType === 'Button') {
|
|
193
|
+
const v = this.content.get('V');
|
|
181
194
|
return v instanceof PdfName && v.value === 'Yes';
|
|
182
195
|
}
|
|
183
196
|
return false;
|
|
184
197
|
}
|
|
185
198
|
set checked(isChecked) {
|
|
186
|
-
|
|
187
|
-
if (fieldType === PdfFieldType.Button) {
|
|
199
|
+
if (this.fieldType === 'Button') {
|
|
188
200
|
if (isChecked) {
|
|
189
|
-
this.set('V', new PdfName('Yes'));
|
|
190
|
-
this.set('AS', new PdfName('Yes'));
|
|
201
|
+
this.content.set('V', new PdfName('Yes'));
|
|
202
|
+
this.content.set('AS', new PdfName('Yes'));
|
|
191
203
|
}
|
|
192
204
|
else {
|
|
193
|
-
this.set('V', new PdfName('Off'));
|
|
194
|
-
this.set('AS', new PdfName('Off'));
|
|
205
|
+
this.content.set('V', new PdfName('Off'));
|
|
206
|
+
this.content.set('AS', new PdfName('Off'));
|
|
195
207
|
}
|
|
196
208
|
}
|
|
197
209
|
}
|
|
198
210
|
get fontSize() {
|
|
199
|
-
const da = this.get('DA')?.as(PdfString)?.value || '';
|
|
211
|
+
const da = this.content.get('DA')?.as(PdfString)?.value || '';
|
|
200
212
|
const match = da.match(/\/[A-Za-z0-9_-]+\s+([\d.]+)\s+Tf/);
|
|
201
213
|
if (match) {
|
|
202
214
|
return parseFloat(match[1]);
|
|
@@ -204,16 +216,16 @@ export class PdfAcroFormField extends PdfDictionary {
|
|
|
204
216
|
return null;
|
|
205
217
|
}
|
|
206
218
|
set fontSize(size) {
|
|
207
|
-
const da = this.get('DA')?.as(PdfString)?.value || '';
|
|
219
|
+
const da = this.content.get('DA')?.as(PdfString)?.value || '';
|
|
208
220
|
if (!da) {
|
|
209
|
-
this.set('DA', new PdfString(`/F1 ${size} Tf 0 g`));
|
|
221
|
+
this.content.set('DA', new PdfString(`/F1 ${size} Tf 0 g`));
|
|
210
222
|
return;
|
|
211
223
|
}
|
|
212
224
|
const updatedDa = da.replace(/(\/[A-Za-z0-9_-]+)\s+[\d.]+\s+Tf/g, `$1 ${size} Tf`);
|
|
213
|
-
this.set('DA', new PdfString(updatedDa));
|
|
225
|
+
this.content.set('DA', new PdfString(updatedDa));
|
|
214
226
|
}
|
|
215
227
|
get fontName() {
|
|
216
|
-
const da = this.get('DA')?.as(PdfString)?.value || '';
|
|
228
|
+
const da = this.content.get('DA')?.as(PdfString)?.value || '';
|
|
217
229
|
const match = da.match(/\/([A-Za-z0-9_-]+)\s+[\d.]+\s+Tf/);
|
|
218
230
|
if (match) {
|
|
219
231
|
return match[1];
|
|
@@ -221,13 +233,13 @@ export class PdfAcroFormField extends PdfDictionary {
|
|
|
221
233
|
return null;
|
|
222
234
|
}
|
|
223
235
|
set fontName(fontName) {
|
|
224
|
-
const da = this.get('DA')?.as(PdfString)?.value || '';
|
|
236
|
+
const da = this.content.get('DA')?.as(PdfString)?.value || '';
|
|
225
237
|
if (!da) {
|
|
226
|
-
this.set('DA', new PdfString(`/${fontName} 12 Tf 0 g`));
|
|
238
|
+
this.content.set('DA', new PdfString(`/${fontName} 12 Tf 0 g`));
|
|
227
239
|
return;
|
|
228
240
|
}
|
|
229
241
|
const updatedDa = da.replace(/\/[A-Za-z0-9_-]+(\s+[\d.]+\s+Tf)/g, `/${fontName}$1`);
|
|
230
|
-
this.set('DA', new PdfString(updatedDa));
|
|
242
|
+
this.content.set('DA', new PdfString(updatedDa));
|
|
231
243
|
}
|
|
232
244
|
/**
|
|
233
245
|
* Sets the font using a PdfFont object.
|
|
@@ -236,39 +248,51 @@ export class PdfAcroFormField extends PdfDictionary {
|
|
|
236
248
|
set font(font) {
|
|
237
249
|
if (font === null) {
|
|
238
250
|
// Clear font - set to empty or default
|
|
239
|
-
this.set('DA', new PdfString(''));
|
|
251
|
+
this.content.set('DA', new PdfString(''));
|
|
240
252
|
return;
|
|
241
253
|
}
|
|
242
254
|
const resourceName = font.resourceName;
|
|
243
255
|
const currentSize = this.fontSize ?? 12;
|
|
244
|
-
const da = this.get('DA')?.as(PdfString)?.value || '';
|
|
256
|
+
const da = this.content.get('DA')?.as(PdfString)?.value || '';
|
|
245
257
|
if (!da) {
|
|
246
|
-
this.set('DA', new PdfString(`/${resourceName} ${currentSize} Tf 0 g`));
|
|
258
|
+
this.content.set('DA', new PdfString(`/${resourceName} ${currentSize} Tf 0 g`));
|
|
247
259
|
return;
|
|
248
260
|
}
|
|
249
261
|
const updatedDa = da.replace(/\/[A-Za-z0-9_-]+(\s+[\d.]+\s+Tf)/g, `/${resourceName}$1`);
|
|
250
|
-
this.set('DA', new PdfString(updatedDa));
|
|
262
|
+
this.content.set('DA', new PdfString(updatedDa));
|
|
251
263
|
}
|
|
252
264
|
/**
|
|
253
265
|
* Gets field flags (bitwise combination of field attributes)
|
|
254
266
|
*/
|
|
255
267
|
get flags() {
|
|
256
|
-
return this.get('Ff')?.as(PdfNumber)?.value ?? 0;
|
|
268
|
+
return this.content.get('Ff')?.as(PdfNumber)?.value ?? 0;
|
|
257
269
|
}
|
|
258
270
|
/**
|
|
259
271
|
* Sets field flags
|
|
260
272
|
*/
|
|
261
273
|
set flags(flags) {
|
|
262
|
-
this.set('Ff', new PdfNumber(flags));
|
|
274
|
+
this.content.set('Ff', new PdfNumber(flags));
|
|
263
275
|
}
|
|
264
276
|
/**
|
|
265
|
-
*
|
|
277
|
+
* Gets annotation flags (for visual appearance and behavior)
|
|
278
|
+
*/
|
|
279
|
+
get annotationFlags() {
|
|
280
|
+
return this.content.get('F')?.as(PdfNumber)?.value ?? 0;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Sets annotation flags
|
|
284
|
+
*/
|
|
285
|
+
set annotationFlags(flags) {
|
|
286
|
+
this.content.set('F', new PdfNumber(flags));
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Checks if the field is read-only (Ff bit 1)
|
|
266
290
|
*/
|
|
267
291
|
get readOnly() {
|
|
268
292
|
return (this.flags & 1) !== 0;
|
|
269
293
|
}
|
|
270
294
|
/**
|
|
271
|
-
* Sets the field as read-only or editable
|
|
295
|
+
* Sets the field as read-only or editable (Ff bit 1)
|
|
272
296
|
*/
|
|
273
297
|
set readOnly(isReadOnly) {
|
|
274
298
|
if (isReadOnly) {
|
|
@@ -329,67 +353,865 @@ export class PdfAcroFormField extends PdfDictionary {
|
|
|
329
353
|
this.flags = this.flags & ~8192;
|
|
330
354
|
}
|
|
331
355
|
}
|
|
356
|
+
/**
|
|
357
|
+
* Checks if the field is a comb field (characters distributed evenly across cells)
|
|
358
|
+
*/
|
|
359
|
+
get comb() {
|
|
360
|
+
return (this.flags & 16777216) !== 0;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Gets the quadding (text alignment) for this field.
|
|
364
|
+
* 0 = left-justified, 1 = centered, 2 = right-justified
|
|
365
|
+
*/
|
|
366
|
+
get quadding() {
|
|
367
|
+
return this.content.get('Q')?.as(PdfNumber)?.value ?? 0;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Sets the quadding (text alignment) for this field.
|
|
371
|
+
* 0 = left-justified, 1 = centered, 2 = right-justified
|
|
372
|
+
*/
|
|
373
|
+
set quadding(q) {
|
|
374
|
+
this.content.set('Q', new PdfNumber(q));
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Gets the options for choice fields (dropdowns, list boxes).
|
|
378
|
+
* Returns an array of option strings.
|
|
379
|
+
*/
|
|
380
|
+
get options() {
|
|
381
|
+
const opt = this.content.get('Opt')?.as((PdfArray));
|
|
382
|
+
if (!opt)
|
|
383
|
+
return [];
|
|
384
|
+
return opt.items.map((item) => item.value);
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Sets the options for choice fields (dropdowns, list boxes).
|
|
388
|
+
* Pass an array of strings.
|
|
389
|
+
*/
|
|
390
|
+
set options(options) {
|
|
391
|
+
if (options.length === 0) {
|
|
392
|
+
this.content.delete('Opt');
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const optArray = new PdfArray(options.map((opt) => new PdfString(opt)));
|
|
396
|
+
this.content.set('Opt', optArray);
|
|
397
|
+
}
|
|
398
|
+
get defaultAppearance() {
|
|
399
|
+
return this.content.get('DA')?.as(PdfString)?.value ?? null;
|
|
400
|
+
}
|
|
401
|
+
set defaultAppearance(da) {
|
|
402
|
+
this.content.set('DA', new PdfString(da));
|
|
403
|
+
}
|
|
404
|
+
set combo(isCombo) {
|
|
405
|
+
if (isCombo) {
|
|
406
|
+
this.flags = this.flags | 131072;
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
this.flags = this.flags & ~131072;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
get combo() {
|
|
413
|
+
return (this.flags & 131072) !== 0;
|
|
414
|
+
}
|
|
415
|
+
get radio() {
|
|
416
|
+
return (this.flags & 32768) !== 0;
|
|
417
|
+
}
|
|
418
|
+
set radio(isRadio) {
|
|
419
|
+
if (isRadio) {
|
|
420
|
+
this.flags = this.flags | 32768;
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
this.flags = this.flags & ~32768;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
get noToggleToOff() {
|
|
427
|
+
return (this.flags & 16384) !== 0;
|
|
428
|
+
}
|
|
429
|
+
set noToggleToOff(noToggle) {
|
|
430
|
+
if (noToggle) {
|
|
431
|
+
this.flags = this.flags | 16384;
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
this.flags = this.flags & ~16384;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
get combField() {
|
|
438
|
+
return (this.flags & 16777216) !== 0;
|
|
439
|
+
}
|
|
440
|
+
set combField(isComb) {
|
|
441
|
+
if (isComb) {
|
|
442
|
+
this.flags = this.flags | 16777216;
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
this.flags = this.flags & ~16777216;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
get maxLen() {
|
|
449
|
+
return this.content.get('MaxLen')?.as(PdfNumber)?.value ?? null;
|
|
450
|
+
}
|
|
451
|
+
set maxLen(maxLen) {
|
|
452
|
+
if (maxLen === null) {
|
|
453
|
+
this.content.delete('MaxLen');
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
this.content.set('MaxLen', new PdfNumber(maxLen));
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
// ============================================
|
|
460
|
+
// Annotation Flags (F field)
|
|
461
|
+
// ============================================
|
|
462
|
+
/**
|
|
463
|
+
* If true, the annotation is invisible (F bit 1)
|
|
464
|
+
*/
|
|
465
|
+
get invisible() {
|
|
466
|
+
return (this.annotationFlags & 1) !== 0;
|
|
467
|
+
}
|
|
468
|
+
set invisible(value) {
|
|
469
|
+
if (value) {
|
|
470
|
+
this.annotationFlags = this.annotationFlags | 1;
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
this.annotationFlags = this.annotationFlags & ~1;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* If true, the annotation is hidden (F bit 2)
|
|
478
|
+
*/
|
|
479
|
+
get hidden() {
|
|
480
|
+
return (this.annotationFlags & 2) !== 0;
|
|
481
|
+
}
|
|
482
|
+
set hidden(value) {
|
|
483
|
+
if (value) {
|
|
484
|
+
this.annotationFlags = this.annotationFlags | 2;
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
this.annotationFlags = this.annotationFlags & ~2;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* If true, print the annotation when printing (F bit 3)
|
|
492
|
+
*/
|
|
493
|
+
get print() {
|
|
494
|
+
return (this.annotationFlags & 4) !== 0;
|
|
495
|
+
}
|
|
496
|
+
set print(value) {
|
|
497
|
+
if (value) {
|
|
498
|
+
this.annotationFlags = this.annotationFlags | 4;
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
this.annotationFlags = this.annotationFlags & ~4;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* If true, do not zoom annotation when zooming (F bit 4)
|
|
506
|
+
*/
|
|
507
|
+
get noZoom() {
|
|
508
|
+
return (this.annotationFlags & 8) !== 0;
|
|
509
|
+
}
|
|
510
|
+
set noZoom(value) {
|
|
511
|
+
if (value) {
|
|
512
|
+
this.annotationFlags = this.annotationFlags | 8;
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
this.annotationFlags = this.annotationFlags & ~8;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* If true, do not rotate annotation when rotating (F bit 5)
|
|
520
|
+
*/
|
|
521
|
+
get noRotate() {
|
|
522
|
+
return (this.annotationFlags & 16) !== 0;
|
|
523
|
+
}
|
|
524
|
+
set noRotate(value) {
|
|
525
|
+
if (value) {
|
|
526
|
+
this.annotationFlags = this.annotationFlags | 16;
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
this.annotationFlags = this.annotationFlags & ~16;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* If true, do not display annotation on screen (F bit 6)
|
|
534
|
+
*/
|
|
535
|
+
get noView() {
|
|
536
|
+
return (this.annotationFlags & 32) !== 0;
|
|
537
|
+
}
|
|
538
|
+
set noView(value) {
|
|
539
|
+
if (value) {
|
|
540
|
+
this.annotationFlags = this.annotationFlags | 32;
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
this.annotationFlags = this.annotationFlags & ~32;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* If true, annotation is locked (F bit 8)
|
|
548
|
+
*/
|
|
549
|
+
get locked() {
|
|
550
|
+
return (this.annotationFlags & 128) !== 0;
|
|
551
|
+
}
|
|
552
|
+
set locked(value) {
|
|
553
|
+
if (value) {
|
|
554
|
+
this.annotationFlags = this.annotationFlags | 128;
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
this.annotationFlags = this.annotationFlags & ~128;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
// ============================================
|
|
561
|
+
// Field Flags (Ff field) - Additional
|
|
562
|
+
// ============================================
|
|
563
|
+
/**
|
|
564
|
+
* If true, field value should not be exported (Ff bit 3)
|
|
565
|
+
*/
|
|
566
|
+
get noExport() {
|
|
567
|
+
return (this.flags & 4) !== 0;
|
|
568
|
+
}
|
|
569
|
+
set noExport(value) {
|
|
570
|
+
if (value) {
|
|
571
|
+
this.flags = this.flags | 4;
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
this.flags = this.flags & ~4;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* If true, field is a pushbutton (Ff bit 17)
|
|
579
|
+
*/
|
|
580
|
+
get pushButton() {
|
|
581
|
+
return (this.flags & 65536) !== 0;
|
|
582
|
+
}
|
|
583
|
+
set pushButton(value) {
|
|
584
|
+
if (value) {
|
|
585
|
+
this.flags = this.flags | 65536;
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
this.flags = this.flags & ~65536;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* If true, text field allows editing (Ff bit 19)
|
|
593
|
+
*/
|
|
594
|
+
get edit() {
|
|
595
|
+
return (this.flags & 262144) !== 0;
|
|
596
|
+
}
|
|
597
|
+
set edit(value) {
|
|
598
|
+
if (value) {
|
|
599
|
+
this.flags = this.flags | 262144;
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
this.flags = this.flags & ~262144;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* If true, choice options should be sorted alphabetically (Ff bit 20)
|
|
607
|
+
*/
|
|
608
|
+
get sort() {
|
|
609
|
+
return (this.flags & 524288) !== 0;
|
|
610
|
+
}
|
|
611
|
+
set sort(value) {
|
|
612
|
+
if (value) {
|
|
613
|
+
this.flags = this.flags | 524288;
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
this.flags = this.flags & ~524288;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* If true, allows multiple selections in choice field (Ff bit 22)
|
|
621
|
+
*/
|
|
622
|
+
get multiSelect() {
|
|
623
|
+
return (this.flags & 2097152) !== 0;
|
|
624
|
+
}
|
|
625
|
+
set multiSelect(value) {
|
|
626
|
+
if (value) {
|
|
627
|
+
this.flags = this.flags | 2097152;
|
|
628
|
+
}
|
|
629
|
+
else {
|
|
630
|
+
this.flags = this.flags & ~2097152;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* If true, do not spell check this field (Ff bit 23)
|
|
635
|
+
*/
|
|
636
|
+
get doNotSpellCheck() {
|
|
637
|
+
return (this.flags & 4194304) !== 0;
|
|
638
|
+
}
|
|
639
|
+
set doNotSpellCheck(value) {
|
|
640
|
+
if (value) {
|
|
641
|
+
this.flags = this.flags | 4194304;
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
this.flags = this.flags & ~4194304;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* If true, do not scroll text field (Ff bit 24)
|
|
649
|
+
*/
|
|
650
|
+
get doNotScroll() {
|
|
651
|
+
return (this.flags & 8388608) !== 0;
|
|
652
|
+
}
|
|
653
|
+
set doNotScroll(value) {
|
|
654
|
+
if (value) {
|
|
655
|
+
this.flags = this.flags | 8388608;
|
|
656
|
+
}
|
|
657
|
+
else {
|
|
658
|
+
this.flags = this.flags & ~8388608;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* If true, commit field value immediately on selection change (Ff bit 27)
|
|
663
|
+
*/
|
|
664
|
+
get commitOnSelChange() {
|
|
665
|
+
return (this.flags & 67108864) !== 0;
|
|
666
|
+
}
|
|
667
|
+
set commitOnSelChange(value) {
|
|
668
|
+
if (value) {
|
|
669
|
+
this.flags = this.flags | 67108864;
|
|
670
|
+
}
|
|
671
|
+
else {
|
|
672
|
+
this.flags = this.flags & ~67108864;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
get kids() {
|
|
676
|
+
const kidsArray = this.content
|
|
677
|
+
.get('Kids')
|
|
678
|
+
?.as((PdfArray));
|
|
679
|
+
if (!kidsArray)
|
|
680
|
+
return [];
|
|
681
|
+
return kidsArray.items;
|
|
682
|
+
}
|
|
683
|
+
set kids(kids) {
|
|
684
|
+
if (kids.length === 0) {
|
|
685
|
+
this.content.delete('Kids');
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
const kidsArray = new PdfArray(kids);
|
|
689
|
+
this.content.set('Kids', kidsArray);
|
|
690
|
+
}
|
|
691
|
+
get appearanceStreamDict() {
|
|
692
|
+
const apDict = this.content.get('AP')?.as(PdfDictionary);
|
|
693
|
+
if (!apDict)
|
|
694
|
+
return null;
|
|
695
|
+
return apDict;
|
|
696
|
+
}
|
|
697
|
+
set appearanceStreamDict(dict) {
|
|
698
|
+
if (dict === null) {
|
|
699
|
+
this.content.delete('AP');
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
this.content.set('AP', dict);
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Generates an appearance stream for a text field using iText's approach.
|
|
706
|
+
*
|
|
707
|
+
* This generates an appearance with text using the same positioning formula as iText:
|
|
708
|
+
* - textY = (height - fontSize) / 2 + fontSize * 0.2
|
|
709
|
+
* - Wrapped in marked content blocks (/Tx BMC ... EMC)
|
|
710
|
+
* - Field remains editable unless makeReadOnly is set
|
|
711
|
+
*
|
|
712
|
+
* For editable fields (default, no options):
|
|
713
|
+
* - Text visible immediately
|
|
714
|
+
* - Field remains fully editable
|
|
715
|
+
* - No save dialog (needAppearances = false)
|
|
716
|
+
* - Text positioning matches iText
|
|
717
|
+
*
|
|
718
|
+
* For read-only fields (makeReadOnly: true):
|
|
719
|
+
* - Same appearance generation
|
|
720
|
+
* - Field is set as read-only
|
|
721
|
+
*
|
|
722
|
+
* @param options.makeReadOnly - If true, sets field as read-only
|
|
723
|
+
* @returns true if appearance was generated successfully
|
|
724
|
+
*/
|
|
725
|
+
generateAppearance(options) {
|
|
726
|
+
const fieldType = this.fieldType;
|
|
727
|
+
// Route to appropriate generation method based on field type
|
|
728
|
+
if (fieldType === 'Text') {
|
|
729
|
+
return this.generateTextAppearance(options);
|
|
730
|
+
}
|
|
731
|
+
else if (fieldType === 'Button') {
|
|
732
|
+
return this.generateButtonAppearance(options);
|
|
733
|
+
}
|
|
734
|
+
else if (fieldType === 'Choice') {
|
|
735
|
+
return this.generateChoiceAppearance(options);
|
|
736
|
+
}
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Generates appearance for text fields
|
|
741
|
+
* @internal
|
|
742
|
+
*/
|
|
743
|
+
generateTextAppearance(options) {
|
|
744
|
+
const rect = this.rect;
|
|
745
|
+
if (!rect || rect.length !== 4)
|
|
746
|
+
return false;
|
|
747
|
+
const [x1, y1, x2, y2] = rect;
|
|
748
|
+
const width = x2 - x1;
|
|
749
|
+
const height = y2 - y1;
|
|
750
|
+
// Get the default appearance string
|
|
751
|
+
const da = this.content.get('DA')?.as(PdfString)?.value;
|
|
752
|
+
if (!da)
|
|
753
|
+
return false;
|
|
754
|
+
// Get the field value
|
|
755
|
+
const value = this.value;
|
|
756
|
+
// Parse font name and size from DA
|
|
757
|
+
const fontMatch = da.match(/\/(\w+)\s+([\d.]+)\s+Tf/);
|
|
758
|
+
if (!fontMatch)
|
|
759
|
+
return false;
|
|
760
|
+
const fontName = fontMatch[1];
|
|
761
|
+
let fontSize = parseFloat(fontMatch[2]);
|
|
762
|
+
// If font size is 0 or invalid, use a default size
|
|
763
|
+
if (!fontSize || fontSize <= 0) {
|
|
764
|
+
fontSize = 12; // Default to 12pt
|
|
765
|
+
}
|
|
766
|
+
// Parse color from DA (format: "r g b rg" or "g g")
|
|
767
|
+
let colorOp = '0 g'; // default to black
|
|
768
|
+
const rgMatch = da.match(/([\d.]+\s+[\d.]+\s+[\d.]+)\s+rg/);
|
|
769
|
+
const gMatch = da.match(/([\d.]+)\s+g/);
|
|
770
|
+
if (rgMatch) {
|
|
771
|
+
colorOp = `${rgMatch[1]} rg`;
|
|
772
|
+
}
|
|
773
|
+
else if (gMatch) {
|
|
774
|
+
colorOp = `${gMatch[1]} g`;
|
|
775
|
+
}
|
|
776
|
+
// Reconstruct the DA string with the correct font size
|
|
777
|
+
const reconstructedDA = `/${fontName} ${fontSize} Tf ${colorOp}`;
|
|
778
|
+
// Calculate text position using Adobe Acrobat's positioning formula
|
|
779
|
+
// After testing, this formula matches Acrobat's rendering most closely
|
|
780
|
+
const padding = 2;
|
|
781
|
+
// Vertical positioning: Position baseline to match viewer behavior
|
|
782
|
+
// This accounts for the font's typical metrics (cap height, descenders, etc.)
|
|
783
|
+
const textY = (height - fontSize) / 2 + fontSize * 0.2;
|
|
784
|
+
// Escape special characters in the text value
|
|
785
|
+
const escapedValue = value
|
|
786
|
+
.replace(/\\/g, '\\\\')
|
|
787
|
+
.replace(/\(/g, '\\(')
|
|
788
|
+
.replace(/\)/g, '\\)')
|
|
789
|
+
.replace(/\r/g, '\\r')
|
|
790
|
+
.replace(/\n/g, '\\n');
|
|
791
|
+
// Generate text positioning based on field type
|
|
792
|
+
let textContent;
|
|
793
|
+
if (this.multiline) {
|
|
794
|
+
// Multiline text field: handle line breaks
|
|
795
|
+
const lines = value.split('\n');
|
|
796
|
+
const lineHeight = fontSize * 1.2;
|
|
797
|
+
const startY = height - padding - fontSize;
|
|
798
|
+
textContent = 'BT\n';
|
|
799
|
+
textContent += `${reconstructedDA}\n`;
|
|
800
|
+
textContent += `${padding} ${startY} Td\n`;
|
|
801
|
+
for (let i = 0; i < lines.length; i++) {
|
|
802
|
+
const line = lines[i]
|
|
803
|
+
.replace(/\\/g, '\\\\')
|
|
804
|
+
.replace(/\(/g, '\\(')
|
|
805
|
+
.replace(/\)/g, '\\)')
|
|
806
|
+
.replace(/\r/g, '');
|
|
807
|
+
if (i > 0) {
|
|
808
|
+
textContent += `0 ${-lineHeight} Td\n`;
|
|
809
|
+
}
|
|
810
|
+
textContent += `(${line}) Tj\n`;
|
|
811
|
+
}
|
|
812
|
+
textContent += 'ET\n';
|
|
813
|
+
}
|
|
814
|
+
else if (this.comb && this.maxLen) {
|
|
815
|
+
// Comb field: position each character in its own cell
|
|
816
|
+
const cellWidth = width / this.maxLen;
|
|
817
|
+
const chars = value.split('');
|
|
818
|
+
textContent = 'BT\n';
|
|
819
|
+
textContent += `${reconstructedDA}\n`;
|
|
820
|
+
for (let i = 0; i < chars.length && i < this.maxLen; i++) {
|
|
821
|
+
// Center each character in its cell
|
|
822
|
+
const cellX = cellWidth * i + cellWidth / 2 - fontSize * 0.3;
|
|
823
|
+
const escapedChar = chars[i]
|
|
824
|
+
.replace(/\\/g, '\\\\')
|
|
825
|
+
.replace(/\(/g, '\\(')
|
|
826
|
+
.replace(/\)/g, '\\)');
|
|
827
|
+
textContent += `${cellX} ${textY} Td\n`;
|
|
828
|
+
textContent += `(${escapedChar}) Tj\n`;
|
|
829
|
+
textContent += `${-cellX} ${-textY} Td\n`; // Reset position
|
|
830
|
+
}
|
|
831
|
+
textContent += 'ET\n';
|
|
832
|
+
}
|
|
833
|
+
else {
|
|
834
|
+
// Regular text field
|
|
835
|
+
const textX = padding;
|
|
836
|
+
textContent = `BT
|
|
837
|
+
${reconstructedDA}
|
|
838
|
+
${textX} ${textY} Td
|
|
839
|
+
(${escapedValue}) Tj
|
|
840
|
+
ET
|
|
841
|
+
`;
|
|
842
|
+
}
|
|
843
|
+
// Generate appearance with text (iText approach)
|
|
844
|
+
// Use marked content to properly tag the text field content
|
|
845
|
+
const contentStream = `/Tx BMC
|
|
846
|
+
q
|
|
847
|
+
${textContent}Q
|
|
848
|
+
EMC
|
|
849
|
+
`;
|
|
850
|
+
// Create the appearance stream
|
|
851
|
+
const appearanceDict = new PdfDictionary();
|
|
852
|
+
appearanceDict.set('Type', new PdfName('XObject'));
|
|
853
|
+
appearanceDict.set('Subtype', new PdfName('Form'));
|
|
854
|
+
appearanceDict.set('FormType', new PdfNumber(1));
|
|
855
|
+
appearanceDict.set('BBox', new PdfArray([
|
|
856
|
+
new PdfNumber(0),
|
|
857
|
+
new PdfNumber(0),
|
|
858
|
+
new PdfNumber(width),
|
|
859
|
+
new PdfNumber(height),
|
|
860
|
+
]));
|
|
861
|
+
const stream = new PdfStream({
|
|
862
|
+
header: appearanceDict,
|
|
863
|
+
original: contentStream,
|
|
864
|
+
});
|
|
865
|
+
// Store the appearance stream for later writing
|
|
866
|
+
this._appearanceStream = stream;
|
|
867
|
+
// Configure field flags based on options
|
|
868
|
+
if (options?.makeReadOnly) {
|
|
869
|
+
// Set the read-only flag (Ff bit 0)
|
|
870
|
+
this.readOnly = true;
|
|
871
|
+
// Ensure the annotation is visible and printable
|
|
872
|
+
this.print = true;
|
|
873
|
+
this.noZoom = true;
|
|
874
|
+
}
|
|
875
|
+
else {
|
|
876
|
+
// For editable fields, just ensure print flag is set
|
|
877
|
+
this.print = true;
|
|
878
|
+
}
|
|
879
|
+
return true;
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Generates appearance for button fields (checkboxes, radio buttons)
|
|
883
|
+
* @internal
|
|
884
|
+
*/
|
|
885
|
+
generateButtonAppearance(options) {
|
|
886
|
+
const rect = this.rect;
|
|
887
|
+
if (!rect || rect.length !== 4)
|
|
888
|
+
return false;
|
|
889
|
+
const [x1, y1, x2, y2] = rect;
|
|
890
|
+
const width = x2 - x1;
|
|
891
|
+
const height = y2 - y1;
|
|
892
|
+
const size = Math.min(width, height);
|
|
893
|
+
// Check if this is a radio button by looking at parent/siblings
|
|
894
|
+
// Radio buttons typically have Ff bit 15 (Radio) set
|
|
895
|
+
const isRadio = (this.flags & 32768) !== 0;
|
|
896
|
+
// Helper to create appearance stream dictionary
|
|
897
|
+
const createAppearanceStream = (content) => {
|
|
898
|
+
const appearanceDict = new PdfDictionary();
|
|
899
|
+
appearanceDict.set('Type', new PdfName('XObject'));
|
|
900
|
+
appearanceDict.set('Subtype', new PdfName('Form'));
|
|
901
|
+
appearanceDict.set('FormType', new PdfNumber(1));
|
|
902
|
+
appearanceDict.set('BBox', new PdfArray([
|
|
903
|
+
new PdfNumber(0),
|
|
904
|
+
new PdfNumber(0),
|
|
905
|
+
new PdfNumber(width),
|
|
906
|
+
new PdfNumber(height),
|
|
907
|
+
]));
|
|
908
|
+
// Add ZapfDingbats font for checkmarks
|
|
909
|
+
const resources = new PdfDictionary();
|
|
910
|
+
const fonts = new PdfDictionary();
|
|
911
|
+
const zapfFont = new PdfDictionary();
|
|
912
|
+
zapfFont.set('Type', new PdfName('Font'));
|
|
913
|
+
zapfFont.set('Subtype', new PdfName('Type1'));
|
|
914
|
+
zapfFont.set('BaseFont', new PdfName('ZapfDingbats'));
|
|
915
|
+
fonts.set('ZaDb', zapfFont);
|
|
916
|
+
resources.set('Font', fonts);
|
|
917
|
+
appearanceDict.set('Resources', resources);
|
|
918
|
+
return new PdfStream({
|
|
919
|
+
header: appearanceDict,
|
|
920
|
+
original: content,
|
|
921
|
+
});
|
|
922
|
+
};
|
|
923
|
+
// Generate "Off" state appearance (unchecked/empty)
|
|
924
|
+
const offStream = createAppearanceStream('');
|
|
925
|
+
// Generate "Yes" state appearance (checked)
|
|
926
|
+
let yesContent;
|
|
927
|
+
if (isRadio) {
|
|
928
|
+
// Radio button: filled circle using 4 Bezier curves to approximate a circle
|
|
929
|
+
const center = size / 2;
|
|
930
|
+
const radius = size * 0.35;
|
|
931
|
+
const k = 0.5522847498; // Magic number for circular Bezier curves (4/3 * tan(π/8))
|
|
932
|
+
const kRadius = k * radius;
|
|
933
|
+
// Draw a filled circle using 4 cubic Bezier curves
|
|
934
|
+
yesContent = `q
|
|
935
|
+
0 0 0 rg
|
|
936
|
+
${center} ${center + radius} m
|
|
937
|
+
${center + kRadius} ${center + radius} ${center + radius} ${center + kRadius} ${center + radius} ${center} c
|
|
938
|
+
${center + radius} ${center - kRadius} ${center + kRadius} ${center - radius} ${center} ${center - radius} c
|
|
939
|
+
${center - kRadius} ${center - radius} ${center - radius} ${center - kRadius} ${center - radius} ${center} c
|
|
940
|
+
${center - radius} ${center + kRadius} ${center - kRadius} ${center + radius} ${center} ${center + radius} c
|
|
941
|
+
f
|
|
942
|
+
Q
|
|
943
|
+
`;
|
|
944
|
+
}
|
|
945
|
+
else {
|
|
946
|
+
// Checkbox: checkmark (using ZapfDingbats character)
|
|
947
|
+
const checkSize = size * 0.8;
|
|
948
|
+
const offset = (size - checkSize) / 2;
|
|
949
|
+
yesContent = `q
|
|
950
|
+
BT
|
|
951
|
+
/ZaDb ${checkSize} Tf
|
|
952
|
+
${offset} ${offset} Td
|
|
953
|
+
(4) Tj
|
|
954
|
+
ET
|
|
955
|
+
Q
|
|
956
|
+
`;
|
|
957
|
+
}
|
|
958
|
+
const yesStream = createAppearanceStream(yesContent);
|
|
959
|
+
// Store both appearance streams in a state dictionary
|
|
960
|
+
// We'll use a special structure to hold both states
|
|
961
|
+
this._appearanceStream = offStream; // Store Off as default
|
|
962
|
+
this._appearanceStreamYes = yesStream; // Store Yes state separately
|
|
963
|
+
if (options?.makeReadOnly) {
|
|
964
|
+
this.readOnly = true;
|
|
965
|
+
this.print = true;
|
|
966
|
+
this.noZoom = true;
|
|
967
|
+
}
|
|
968
|
+
return true;
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Generates appearance for choice fields (dropdowns, list boxes)
|
|
972
|
+
* @internal
|
|
973
|
+
*/
|
|
974
|
+
generateChoiceAppearance(options) {
|
|
975
|
+
const rect = this.rect;
|
|
976
|
+
if (!rect || rect.length !== 4)
|
|
977
|
+
return false;
|
|
978
|
+
const [x1, y1, x2, y2] = rect;
|
|
979
|
+
const width = x2 - x1;
|
|
980
|
+
const height = y2 - y1;
|
|
981
|
+
// Get the default appearance string
|
|
982
|
+
const da = this.content.get('DA')?.as(PdfString)?.value;
|
|
983
|
+
if (!da)
|
|
984
|
+
return false;
|
|
985
|
+
const value = this.value;
|
|
986
|
+
if (!value)
|
|
987
|
+
return false;
|
|
988
|
+
// Parse font and size from DA
|
|
989
|
+
const fontMatch = da.match(/\/(\w+)\s+([\d.]+)\s+Tf/);
|
|
990
|
+
if (!fontMatch)
|
|
991
|
+
return false;
|
|
992
|
+
const fontName = fontMatch[1];
|
|
993
|
+
let fontSize = parseFloat(fontMatch[2]);
|
|
994
|
+
if (!fontSize || fontSize <= 0) {
|
|
995
|
+
fontSize = 12;
|
|
996
|
+
}
|
|
997
|
+
const colorOp = '0 g';
|
|
998
|
+
const reconstructedDA = `/${fontName} ${fontSize} Tf ${colorOp}`;
|
|
999
|
+
const padding = 2;
|
|
1000
|
+
const textY = (height - fontSize) / 2 + fontSize * 0.2;
|
|
1001
|
+
const textX = padding;
|
|
1002
|
+
const escapedValue = value
|
|
1003
|
+
.replace(/\\/g, '\\\\')
|
|
1004
|
+
.replace(/\(/g, '\\(')
|
|
1005
|
+
.replace(/\)/g, '\\)');
|
|
1006
|
+
// Check if this is a combo box (dropdown) - Ff bit 17 (131072)
|
|
1007
|
+
const isCombo = (this.flags & 131072) !== 0;
|
|
1008
|
+
// Draw dropdown arrow for combo boxes
|
|
1009
|
+
let arrowGraphics = '';
|
|
1010
|
+
if (isCombo) {
|
|
1011
|
+
// Reserve space for the arrow on the right
|
|
1012
|
+
const arrowWidth = height * 0.8; // Arrow area width
|
|
1013
|
+
const arrowX = width - arrowWidth - 2; // X position for arrow
|
|
1014
|
+
const arrowY = height / 2; // Y center
|
|
1015
|
+
const arrowSize = height * 0.3; // Triangle size
|
|
1016
|
+
// Draw a small downward-pointing triangle
|
|
1017
|
+
arrowGraphics = `
|
|
1018
|
+
q
|
|
1019
|
+
0.5 0.5 0.5 rg
|
|
1020
|
+
${arrowX + arrowWidth / 2} ${arrowY - arrowSize / 3} m
|
|
1021
|
+
${arrowX + arrowWidth / 2 - arrowSize / 2} ${arrowY + arrowSize / 3} l
|
|
1022
|
+
${arrowX + arrowWidth / 2 + arrowSize / 2} ${arrowY + arrowSize / 3} l
|
|
1023
|
+
f
|
|
1024
|
+
Q
|
|
1025
|
+
`;
|
|
1026
|
+
}
|
|
1027
|
+
// Generate appearance with text and optional dropdown arrow
|
|
1028
|
+
const contentStream = `/Tx BMC
|
|
1029
|
+
q
|
|
1030
|
+
BT
|
|
1031
|
+
${reconstructedDA}
|
|
1032
|
+
${textX} ${textY} Td
|
|
1033
|
+
(${escapedValue}) Tj
|
|
1034
|
+
ET
|
|
1035
|
+
${arrowGraphics}Q
|
|
1036
|
+
EMC
|
|
1037
|
+
`;
|
|
1038
|
+
const appearanceDict = new PdfDictionary();
|
|
1039
|
+
appearanceDict.set('Type', new PdfName('XObject'));
|
|
1040
|
+
appearanceDict.set('Subtype', new PdfName('Form'));
|
|
1041
|
+
appearanceDict.set('FormType', new PdfNumber(1));
|
|
1042
|
+
appearanceDict.set('BBox', new PdfArray([
|
|
1043
|
+
new PdfNumber(0),
|
|
1044
|
+
new PdfNumber(0),
|
|
1045
|
+
new PdfNumber(width),
|
|
1046
|
+
new PdfNumber(height),
|
|
1047
|
+
]));
|
|
1048
|
+
const stream = new PdfStream({
|
|
1049
|
+
header: appearanceDict,
|
|
1050
|
+
original: contentStream,
|
|
1051
|
+
});
|
|
1052
|
+
this._appearanceStream = stream;
|
|
1053
|
+
if (options?.makeReadOnly) {
|
|
1054
|
+
this.readOnly = true;
|
|
1055
|
+
this.print = true;
|
|
1056
|
+
this.noZoom = true;
|
|
1057
|
+
}
|
|
1058
|
+
return true;
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Gets the stored appearance stream if one has been generated.
|
|
1062
|
+
* For button fields, returns the appropriate stream based on the current state.
|
|
1063
|
+
* @internal
|
|
1064
|
+
*/
|
|
1065
|
+
getAppearanceStream() {
|
|
1066
|
+
// For button fields, return the appropriate stream based on state
|
|
1067
|
+
if (this.fieldType === 'Button') {
|
|
1068
|
+
if (this.checked && this._appearanceStreamYes) {
|
|
1069
|
+
return this._appearanceStreamYes;
|
|
1070
|
+
}
|
|
1071
|
+
return this._appearanceStream; // Return "Off" state
|
|
1072
|
+
}
|
|
1073
|
+
return this._appearanceStream;
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Gets all appearance streams for writing to PDF.
|
|
1077
|
+
* For button fields, returns both Off and Yes states.
|
|
1078
|
+
* For other fields, returns just the primary appearance.
|
|
1079
|
+
* @internal
|
|
1080
|
+
*/
|
|
1081
|
+
getAppearanceStreamsForWriting() {
|
|
1082
|
+
if (!this._appearanceStream)
|
|
1083
|
+
return undefined;
|
|
1084
|
+
return {
|
|
1085
|
+
primary: this._appearanceStream,
|
|
1086
|
+
secondary: this.fieldType === 'Button'
|
|
1087
|
+
? this._appearanceStreamYes
|
|
1088
|
+
: undefined,
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* Sets the appearance dictionary reference for this field.
|
|
1093
|
+
* @internal - This is called automatically by PdfAcroForm.write()
|
|
1094
|
+
*/
|
|
1095
|
+
setAppearanceReference(appearanceStreamRef, appearanceStreamYesRef) {
|
|
1096
|
+
let apDict = this.appearanceStreamDict;
|
|
1097
|
+
if (!apDict) {
|
|
1098
|
+
apDict = new PdfDictionary();
|
|
1099
|
+
this.appearanceStreamDict = apDict;
|
|
1100
|
+
}
|
|
1101
|
+
// For button fields with multiple states, create a state dictionary
|
|
1102
|
+
if (appearanceStreamYesRef && this.fieldType === 'Button') {
|
|
1103
|
+
const stateDict = new PdfDictionary();
|
|
1104
|
+
stateDict.set('Off', appearanceStreamRef);
|
|
1105
|
+
stateDict.set('Yes', appearanceStreamYesRef);
|
|
1106
|
+
apDict.set('N', stateDict);
|
|
1107
|
+
}
|
|
1108
|
+
else {
|
|
1109
|
+
// For other fields, set the appearance stream directly
|
|
1110
|
+
apDict.set('N', appearanceStreamRef);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
332
1113
|
}
|
|
333
|
-
export class PdfAcroForm extends
|
|
1114
|
+
export class PdfAcroForm extends PdfIndirectObject {
|
|
334
1115
|
fields;
|
|
335
|
-
container;
|
|
336
1116
|
fontEncodingMaps = new Map();
|
|
337
1117
|
document;
|
|
338
1118
|
constructor(options) {
|
|
339
|
-
super(
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
this.
|
|
1119
|
+
super(options?.other ??
|
|
1120
|
+
new PdfIndirectObject({
|
|
1121
|
+
content: new PdfDictionary(),
|
|
1122
|
+
}));
|
|
1123
|
+
this.fields = options?.fields ?? [];
|
|
1124
|
+
this.document = options?.document;
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Convenience method to get a value from the form dictionary
|
|
1128
|
+
*/
|
|
1129
|
+
get(key) {
|
|
1130
|
+
return this.content.get(key);
|
|
1131
|
+
}
|
|
1132
|
+
/**
|
|
1133
|
+
* Convenience method to set a value in the form dictionary
|
|
1134
|
+
*/
|
|
1135
|
+
set(key, value) {
|
|
1136
|
+
this.content.set(key, value);
|
|
1137
|
+
}
|
|
1138
|
+
/**
|
|
1139
|
+
* Convenience method to delete a key from the form dictionary
|
|
1140
|
+
*/
|
|
1141
|
+
delete(key) {
|
|
1142
|
+
this.content.delete(key);
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* Convenience method to check if form dictionary is modified
|
|
1146
|
+
*/
|
|
1147
|
+
isModified() {
|
|
1148
|
+
return this.content.isModified();
|
|
344
1149
|
}
|
|
345
1150
|
/**
|
|
346
1151
|
* Gets the NeedAppearances flag
|
|
347
1152
|
*/
|
|
348
1153
|
get needAppearances() {
|
|
349
|
-
return this.get('NeedAppearances')?.as(PdfBoolean)?.value ?? false;
|
|
1154
|
+
return (this.content.get('NeedAppearances')?.as(PdfBoolean)?.value ?? false);
|
|
350
1155
|
}
|
|
351
1156
|
/**
|
|
352
1157
|
* Sets the NeedAppearances flag to indicate that appearance streams need to be regenerated
|
|
353
1158
|
*/
|
|
354
1159
|
set needAppearances(value) {
|
|
355
|
-
this.set('NeedAppearances', new PdfBoolean(value));
|
|
1160
|
+
this.content.set('NeedAppearances', new PdfBoolean(value));
|
|
356
1161
|
}
|
|
357
1162
|
/**
|
|
358
1163
|
* Gets the signature flags
|
|
359
1164
|
*/
|
|
360
1165
|
get signatureFlags() {
|
|
361
|
-
return this.get('SigFlags')?.as(PdfNumber)?.value ?? 0;
|
|
1166
|
+
return this.content.get('SigFlags')?.as(PdfNumber)?.value ?? 0;
|
|
362
1167
|
}
|
|
363
1168
|
/**
|
|
364
1169
|
* Sets the signature flags
|
|
365
1170
|
*/
|
|
366
1171
|
set signatureFlags(flags) {
|
|
367
|
-
this.set('SigFlags', new PdfNumber(flags));
|
|
1172
|
+
this.content.set('SigFlags', new PdfNumber(flags));
|
|
368
1173
|
}
|
|
369
1174
|
/**
|
|
370
1175
|
* Gets the default appearance string for the form
|
|
371
1176
|
*/
|
|
372
1177
|
get defaultAppearance() {
|
|
373
|
-
return this.get('DA')?.as(PdfString)?.value ?? null;
|
|
1178
|
+
return this.content.get('DA')?.as(PdfString)?.value ?? null;
|
|
374
1179
|
}
|
|
375
1180
|
/**
|
|
376
1181
|
* Sets the default appearance string for the form
|
|
377
1182
|
*/
|
|
378
1183
|
set defaultAppearance(da) {
|
|
379
|
-
this.set('DA', new PdfString(da));
|
|
1184
|
+
this.content.set('DA', new PdfString(da));
|
|
380
1185
|
}
|
|
381
1186
|
/**
|
|
382
1187
|
* Gets the default quadding (alignment) for the form
|
|
383
1188
|
* 0 = left, 1 = center, 2 = right
|
|
384
1189
|
*/
|
|
385
1190
|
get defaultQuadding() {
|
|
386
|
-
return this.get('Q')?.as(PdfNumber)?.value ?? 0;
|
|
1191
|
+
return this.content.get('Q')?.as(PdfNumber)?.value ?? 0;
|
|
387
1192
|
}
|
|
388
1193
|
/**
|
|
389
1194
|
* Sets the default quadding (alignment) for the form
|
|
390
1195
|
*/
|
|
391
1196
|
set defaultQuadding(q) {
|
|
392
|
-
this.set('Q', new PdfNumber(q));
|
|
1197
|
+
this.content.set('Q', new PdfNumber(q));
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Gets the default resources dictionary for the form
|
|
1201
|
+
*/
|
|
1202
|
+
get defaultResources() {
|
|
1203
|
+
return this.content.get('DR')?.as(PdfDictionary) ?? null;
|
|
1204
|
+
}
|
|
1205
|
+
/**
|
|
1206
|
+
* Sets the default resources dictionary for the form
|
|
1207
|
+
*/
|
|
1208
|
+
set defaultResources(resources) {
|
|
1209
|
+
if (resources === null) {
|
|
1210
|
+
this.content.delete('DR');
|
|
1211
|
+
}
|
|
1212
|
+
else {
|
|
1213
|
+
this.content.set('DR', resources);
|
|
1214
|
+
}
|
|
393
1215
|
}
|
|
394
1216
|
/**
|
|
395
1217
|
* Sets multiple field values by field name.
|
|
@@ -431,20 +1253,17 @@ export class PdfAcroForm extends PdfDictionary {
|
|
|
431
1253
|
if (this.fontEncodingMaps.has(fontName)) {
|
|
432
1254
|
return this.fontEncodingMaps.get(fontName);
|
|
433
1255
|
}
|
|
434
|
-
// Get the font from
|
|
435
|
-
const dr = this.
|
|
1256
|
+
// Get the font from default resources
|
|
1257
|
+
const dr = this.defaultResources;
|
|
436
1258
|
if (!dr) {
|
|
437
|
-
this.fontEncodingMaps.set(fontName, null);
|
|
438
1259
|
return null;
|
|
439
1260
|
}
|
|
440
1261
|
const fonts = dr.get('Font')?.as(PdfDictionary);
|
|
441
1262
|
if (!fonts) {
|
|
442
|
-
this.fontEncodingMaps.set(fontName, null);
|
|
443
1263
|
return null;
|
|
444
1264
|
}
|
|
445
1265
|
const fontRef = fonts.get(fontName)?.as(PdfObjectReference);
|
|
446
1266
|
if (!fontRef || !this.document) {
|
|
447
|
-
this.fontEncodingMaps.set(fontName, null);
|
|
448
1267
|
return null;
|
|
449
1268
|
}
|
|
450
1269
|
// Read the font object
|
|
@@ -453,7 +1272,6 @@ export class PdfAcroForm extends PdfDictionary {
|
|
|
453
1272
|
generationNumber: fontRef.generationNumber,
|
|
454
1273
|
});
|
|
455
1274
|
if (!fontObj) {
|
|
456
|
-
this.fontEncodingMaps.set(fontName, null);
|
|
457
1275
|
return null;
|
|
458
1276
|
}
|
|
459
1277
|
const fontDict = fontObj.content.as(PdfDictionary);
|
|
@@ -471,24 +1289,25 @@ export class PdfAcroForm extends PdfDictionary {
|
|
|
471
1289
|
encodingDict = encoding;
|
|
472
1290
|
}
|
|
473
1291
|
if (!encodingDict) {
|
|
474
|
-
this.fontEncodingMaps.set(fontName, null);
|
|
475
1292
|
return null;
|
|
476
1293
|
}
|
|
477
1294
|
// Parse the Differences array
|
|
478
1295
|
const differences = encodingDict.get('Differences')?.as(PdfArray);
|
|
479
1296
|
if (!differences) {
|
|
480
|
-
this.fontEncodingMaps.set(fontName, null);
|
|
481
1297
|
return null;
|
|
482
1298
|
}
|
|
483
1299
|
const encodingMap = buildEncodingMap(differences);
|
|
1300
|
+
if (!encodingMap) {
|
|
1301
|
+
return null;
|
|
1302
|
+
}
|
|
484
1303
|
this.fontEncodingMaps.set(fontName, encodingMap);
|
|
485
1304
|
return encodingMap;
|
|
486
1305
|
}
|
|
487
1306
|
static async fromDocument(document) {
|
|
488
|
-
const catalog = document.
|
|
1307
|
+
const catalog = document.root;
|
|
489
1308
|
if (!catalog)
|
|
490
1309
|
return null;
|
|
491
|
-
const acroFormRef = catalog.get('AcroForm');
|
|
1310
|
+
const acroFormRef = catalog.content.get('AcroForm');
|
|
492
1311
|
if (!acroFormRef)
|
|
493
1312
|
return null;
|
|
494
1313
|
let acroFormDict;
|
|
@@ -507,18 +1326,17 @@ export class PdfAcroForm extends PdfDictionary {
|
|
|
507
1326
|
}
|
|
508
1327
|
else if (acroFormRef instanceof PdfDictionary) {
|
|
509
1328
|
acroFormDict = acroFormRef;
|
|
1329
|
+
acroFormContainer = new PdfIndirectObject({ content: acroFormDict });
|
|
510
1330
|
}
|
|
511
1331
|
else {
|
|
512
1332
|
return null;
|
|
513
1333
|
}
|
|
514
|
-
const acroForm = new PdfAcroForm({
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
document,
|
|
518
|
-
});
|
|
1334
|
+
const acroForm = new PdfAcroForm({ other: acroFormContainer, document });
|
|
1335
|
+
// Pre-cache font encoding maps for all fonts used in fields
|
|
1336
|
+
await acroForm.cacheAllFontEncodings();
|
|
519
1337
|
const fields = new Map();
|
|
520
1338
|
const getFields = async (fieldRefs, parent) => {
|
|
521
|
-
for (const fieldRef of fieldRefs
|
|
1339
|
+
for (const fieldRef of fieldRefs) {
|
|
522
1340
|
const refKey = fieldRef.toString().trim();
|
|
523
1341
|
if (fields.has(refKey)) {
|
|
524
1342
|
fields.get(refKey).parent = parent;
|
|
@@ -533,14 +1351,13 @@ export class PdfAcroForm extends PdfDictionary {
|
|
|
533
1351
|
if (!(fieldObject.content instanceof PdfDictionary))
|
|
534
1352
|
continue;
|
|
535
1353
|
const field = new PdfAcroFormField({
|
|
536
|
-
|
|
1354
|
+
other: fieldObject,
|
|
537
1355
|
form: acroForm,
|
|
538
1356
|
});
|
|
539
1357
|
field.parent = parent;
|
|
540
|
-
field.copyFrom(fieldObject.content);
|
|
541
1358
|
// Process child fields (Kids) before adding the parent
|
|
542
|
-
const kids = field.
|
|
543
|
-
if (kids) {
|
|
1359
|
+
const kids = field.kids;
|
|
1360
|
+
if (kids.length > 0) {
|
|
544
1361
|
await getFields(kids, field);
|
|
545
1362
|
}
|
|
546
1363
|
acroForm.fields.push(field);
|
|
@@ -548,24 +1365,25 @@ export class PdfAcroForm extends PdfDictionary {
|
|
|
548
1365
|
}
|
|
549
1366
|
};
|
|
550
1367
|
const fieldsArray = new PdfArray();
|
|
551
|
-
if (acroForm.get('Fields') instanceof PdfArray) {
|
|
552
|
-
fieldsArray.items.push(...acroForm.
|
|
553
|
-
.
|
|
1368
|
+
if (acroForm.content.get('Fields') instanceof PdfArray) {
|
|
1369
|
+
fieldsArray.items.push(...acroForm.content
|
|
1370
|
+
.get('Fields')
|
|
1371
|
+
.as((PdfArray)).items);
|
|
554
1372
|
}
|
|
555
|
-
else if (acroForm.get('Fields') instanceof PdfObjectReference) {
|
|
1373
|
+
else if (acroForm.content.get('Fields') instanceof PdfObjectReference) {
|
|
556
1374
|
const fieldsObj = await document.readObject({
|
|
557
|
-
objectNumber: acroForm.
|
|
558
|
-
.
|
|
559
|
-
|
|
560
|
-
|
|
1375
|
+
objectNumber: acroForm.content
|
|
1376
|
+
.get('Fields')
|
|
1377
|
+
.as(PdfObjectReference).objectNumber,
|
|
1378
|
+
generationNumber: acroForm.content
|
|
1379
|
+
.get('Fields')
|
|
1380
|
+
.as(PdfObjectReference).generationNumber,
|
|
561
1381
|
});
|
|
562
1382
|
if (fieldsObj && fieldsObj.content instanceof PdfArray) {
|
|
563
1383
|
fieldsArray.items.push(...fieldsObj.content.as((PdfArray)).items);
|
|
564
1384
|
}
|
|
565
1385
|
}
|
|
566
|
-
await getFields(fieldsArray);
|
|
567
|
-
// Pre-cache font encoding maps for all fonts used in fields
|
|
568
|
-
await acroForm.cacheAllFontEncodings();
|
|
1386
|
+
await getFields(fieldsArray.items);
|
|
569
1387
|
return acroForm;
|
|
570
1388
|
}
|
|
571
1389
|
/**
|
|
@@ -576,7 +1394,7 @@ export class PdfAcroForm extends PdfDictionary {
|
|
|
576
1394
|
const fontNames = new Set();
|
|
577
1395
|
// Collect all font names from field DA strings
|
|
578
1396
|
for (const field of this.fields) {
|
|
579
|
-
const da = field.get('DA')?.as(PdfString)?.value;
|
|
1397
|
+
const da = field.content.get('DA')?.as(PdfString)?.value;
|
|
580
1398
|
if (da) {
|
|
581
1399
|
const fontMatch = da.match(/\/(\w+)\s+[\d.]+\s+Tf/);
|
|
582
1400
|
if (fontMatch) {
|
|
@@ -646,7 +1464,7 @@ export class PdfAcroForm extends PdfDictionary {
|
|
|
646
1464
|
});
|
|
647
1465
|
if (!pageObj)
|
|
648
1466
|
continue;
|
|
649
|
-
const pageDict = pageObj.content.as(PdfDictionary);
|
|
1467
|
+
const pageDict = pageObj.content.as(PdfDictionary).clone();
|
|
650
1468
|
const annotsInfo = await this.getPageAnnotsArray(document, pageDict);
|
|
651
1469
|
this.addFieldsToAnnots(annotsInfo.annotsArray, fieldRefs);
|
|
652
1470
|
// Write the Annots array if it's an indirect object
|
|
@@ -669,33 +1487,45 @@ export class PdfAcroForm extends PdfDictionary {
|
|
|
669
1487
|
}
|
|
670
1488
|
}
|
|
671
1489
|
async write(document) {
|
|
672
|
-
const catalog = document.
|
|
673
|
-
if (!catalog) {
|
|
674
|
-
throw new Error('Document has no root catalog');
|
|
675
|
-
}
|
|
1490
|
+
const catalog = document.root;
|
|
676
1491
|
const isIncremental = document.isIncremental();
|
|
677
1492
|
document.setIncremental(true);
|
|
678
1493
|
const fieldsArray = new PdfArray();
|
|
679
|
-
this.set('Fields', fieldsArray);
|
|
1494
|
+
this.content.set('Fields', fieldsArray);
|
|
680
1495
|
// Track fields that need to be added to page annotations
|
|
681
1496
|
const fieldsByPage = new Map();
|
|
682
1497
|
for (const field of this.fields) {
|
|
683
|
-
if (!field.isModified())
|
|
684
|
-
continue;
|
|
685
|
-
const acroFormFieldIndirect = new PdfIndirectObject({
|
|
686
|
-
...field.container,
|
|
687
|
-
content: field,
|
|
688
|
-
});
|
|
689
1498
|
let fieldReference;
|
|
690
1499
|
if (field.isModified()) {
|
|
691
|
-
//
|
|
692
|
-
const
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
1500
|
+
// If the field has generated appearance streams, create them as indirect objects
|
|
1501
|
+
const appearances = field.getAppearanceStreamsForWriting();
|
|
1502
|
+
if (appearances) {
|
|
1503
|
+
// Create the primary appearance stream
|
|
1504
|
+
const primaryAppearanceObj = new PdfIndirectObject({
|
|
1505
|
+
content: appearances.primary,
|
|
1506
|
+
});
|
|
1507
|
+
document.add(primaryAppearanceObj);
|
|
1508
|
+
// Create the secondary appearance stream if present (for button fields)
|
|
1509
|
+
let secondaryAppearanceRef;
|
|
1510
|
+
if (appearances.secondary) {
|
|
1511
|
+
const secondaryAppearanceObj = new PdfIndirectObject({
|
|
1512
|
+
content: appearances.secondary,
|
|
1513
|
+
});
|
|
1514
|
+
document.add(secondaryAppearanceObj);
|
|
1515
|
+
secondaryAppearanceRef =
|
|
1516
|
+
secondaryAppearanceObj.reference;
|
|
1517
|
+
}
|
|
1518
|
+
// Set the appearance references on the field
|
|
1519
|
+
field.setAppearanceReference(primaryAppearanceObj.reference, secondaryAppearanceRef);
|
|
1520
|
+
// Ensure field has the Print flag set
|
|
1521
|
+
// This ensures the appearance is used for display and printing
|
|
1522
|
+
if (!field.print) {
|
|
1523
|
+
field.print = true;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
document.add(field);
|
|
697
1527
|
// Create a proper PdfObjectReference (not the proxy from .reference)
|
|
698
|
-
fieldReference = new PdfObjectReference(
|
|
1528
|
+
fieldReference = new PdfObjectReference(field.objectNumber, field.generationNumber);
|
|
699
1529
|
// Track if this field needs to be added to a page's Annots
|
|
700
1530
|
const parentRef = field.parentRef;
|
|
701
1531
|
const isWidget = field.isWidget;
|
|
@@ -711,39 +1541,21 @@ export class PdfAcroForm extends PdfDictionary {
|
|
|
711
1541
|
}
|
|
712
1542
|
}
|
|
713
1543
|
else {
|
|
714
|
-
|
|
715
|
-
const container = field.container;
|
|
716
|
-
if (container &&
|
|
717
|
-
typeof container.objectNumber === 'number' &&
|
|
718
|
-
typeof container.generationNumber === 'number') {
|
|
719
|
-
fieldReference = new PdfObjectReference(container.objectNumber, container.generationNumber);
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
if (fieldReference) {
|
|
723
|
-
fieldsArray.push(fieldReference);
|
|
1544
|
+
fieldReference = field.reference;
|
|
724
1545
|
}
|
|
1546
|
+
fieldsArray.push(fieldReference);
|
|
725
1547
|
}
|
|
726
1548
|
// Add field references to page annotations
|
|
727
1549
|
await this.updatePageAnnotations(document, fieldsByPage);
|
|
728
1550
|
if (this.isModified()) {
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
const rootRef = document.trailerDict
|
|
738
|
-
.get('Root')
|
|
739
|
-
?.as(PdfObjectReference);
|
|
740
|
-
if (rootRef) {
|
|
741
|
-
const rootIndirect = new PdfIndirectObject({
|
|
742
|
-
objectNumber: rootRef.objectNumber,
|
|
743
|
-
generationNumber: rootRef.generationNumber,
|
|
744
|
-
content: catalog,
|
|
745
|
-
});
|
|
746
|
-
document.add(rootIndirect);
|
|
1551
|
+
document.add(this);
|
|
1552
|
+
if (!catalog.content.has('AcroForm')) {
|
|
1553
|
+
let updatableCatalog = catalog;
|
|
1554
|
+
if (catalog.isImmutable()) {
|
|
1555
|
+
updatableCatalog = catalog.clone();
|
|
1556
|
+
document.add(updatableCatalog);
|
|
1557
|
+
}
|
|
1558
|
+
updatableCatalog.content.set('AcroForm', this.reference);
|
|
747
1559
|
}
|
|
748
1560
|
}
|
|
749
1561
|
await document.commit();
|