pdf-lite 1.2.1 → 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/EXAMPLES.md +1 -1
- package/dist/acroform/acroform.d.ts +272 -16
- package/dist/acroform/acroform.js +1084 -144
- package/dist/acroform/manager.d.ts +2 -2
- package/dist/acroform/manager.js +3 -3
- package/dist/core/decoder.d.ts +1 -1
- package/dist/core/decoder.js +3 -3
- 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 +9 -2
- package/dist/core/objects/pdf-hexadecimal.js +25 -5
- 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.d.ts +11 -1
- package/dist/core/objects/pdf-string.js +24 -6
- 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 +37 -75
- package/dist/core/tokens/hexadecimal-token.d.ts +8 -1
- package/dist/core/tokens/hexadecimal-token.js +20 -2
- package/dist/core/tokens/name-token.js +0 -3
- package/dist/core/tokens/string-token.d.ts +8 -1
- package/dist/core/tokens/string-token.js +20 -2
- package/dist/fonts/font-manager.js +6 -8
- package/dist/pdf/pdf-document.d.ts +12 -11
- package/dist/pdf/pdf-document.js +50 -42
- 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/utils/decodeWithFontEncoding.d.ts +20 -0
- package/dist/utils/decodeWithFontEncoding.js +67 -0
- package/dist/utils/escapeString.d.ts +1 -1
- package/dist/utils/escapeString.js +12 -3
- package/dist/utils/glyphNameToUnicode.d.ts +10 -0
- package/dist/utils/glyphNameToUnicode.js +4292 -0
- 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,8 @@ 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';
|
|
10
|
+
import { buildEncodingMap, decodeWithFontEncoding, } from '../utils/decodeWithFontEncoding.js';
|
|
9
11
|
/**
|
|
10
12
|
* Field types for AcroForm fields
|
|
11
13
|
*/
|
|
@@ -15,85 +17,118 @@ export const PdfFieldType = {
|
|
|
15
17
|
Choice: 'Ch',
|
|
16
18
|
Signature: 'Sig',
|
|
17
19
|
};
|
|
18
|
-
export class PdfAcroFormField extends
|
|
20
|
+
export class PdfAcroFormField extends PdfIndirectObject {
|
|
19
21
|
parent;
|
|
20
|
-
|
|
22
|
+
defaultGenerateAppearance = true;
|
|
23
|
+
_appearanceStream;
|
|
24
|
+
_appearanceStreamYes; // For button fields: checked state
|
|
25
|
+
form;
|
|
21
26
|
constructor(options) {
|
|
22
|
-
super(
|
|
23
|
-
|
|
27
|
+
super(options?.other ??
|
|
28
|
+
new PdfIndirectObject({ content: new PdfDictionary() }));
|
|
29
|
+
this.form = options?.form;
|
|
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();
|
|
24
42
|
}
|
|
25
43
|
/**
|
|
26
44
|
* Gets the field type
|
|
27
45
|
*/
|
|
28
46
|
get fieldType() {
|
|
29
|
-
|
|
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
|
+
}
|
|
30
60
|
}
|
|
31
61
|
set fieldType(type) {
|
|
32
62
|
if (type === null) {
|
|
33
|
-
this.delete('FT');
|
|
63
|
+
this.content.delete('FT');
|
|
34
64
|
}
|
|
35
65
|
else {
|
|
36
|
-
this.set('FT', new PdfName(type));
|
|
66
|
+
this.content.set('FT', new PdfName(PdfFieldType[type]));
|
|
37
67
|
}
|
|
38
68
|
}
|
|
39
69
|
get rect() {
|
|
40
|
-
const rectArray = this.get('Rect')?.as((PdfArray));
|
|
70
|
+
const rectArray = this.content.get('Rect')?.as((PdfArray));
|
|
41
71
|
if (!rectArray)
|
|
42
72
|
return null;
|
|
43
73
|
return rectArray.items.map((num) => num.value);
|
|
44
74
|
}
|
|
45
75
|
set rect(rect) {
|
|
46
76
|
if (rect === null) {
|
|
47
|
-
this.delete('Rect');
|
|
77
|
+
this.content.delete('Rect');
|
|
48
78
|
return;
|
|
49
79
|
}
|
|
50
80
|
const rectArray = new PdfArray(rect.map((num) => new PdfNumber(num)));
|
|
51
|
-
this.set('Rect', rectArray);
|
|
81
|
+
this.content.set('Rect', rectArray);
|
|
52
82
|
}
|
|
53
83
|
get parentRef() {
|
|
54
|
-
const ref = this.get('P')?.as(PdfObjectReference);
|
|
84
|
+
const ref = this.content.get('P')?.as(PdfObjectReference);
|
|
55
85
|
return ref ?? null;
|
|
56
86
|
}
|
|
57
87
|
set parentRef(ref) {
|
|
58
88
|
if (ref === null) {
|
|
59
|
-
this.delete('P');
|
|
89
|
+
this.content.delete('P');
|
|
60
90
|
}
|
|
61
91
|
else {
|
|
62
|
-
this.set('P', ref);
|
|
92
|
+
this.content.set('P', ref);
|
|
63
93
|
}
|
|
64
94
|
}
|
|
65
95
|
get isWidget() {
|
|
66
|
-
const type = this.get('Type')?.as(PdfName)?.value;
|
|
67
|
-
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;
|
|
68
98
|
return type === 'Annot' && subtype === 'Widget';
|
|
69
99
|
}
|
|
70
100
|
set isWidget(isWidget) {
|
|
71
101
|
if (isWidget) {
|
|
72
|
-
this.set('Type', new PdfName('Annot'));
|
|
73
|
-
this.set('Subtype', new PdfName('Widget'));
|
|
102
|
+
this.content.set('Type', new PdfName('Annot'));
|
|
103
|
+
this.content.set('Subtype', new PdfName('Widget'));
|
|
74
104
|
}
|
|
75
105
|
else {
|
|
76
|
-
this.delete('Type');
|
|
77
|
-
this.delete('Subtype');
|
|
106
|
+
this.content.delete('Type');
|
|
107
|
+
this.content.delete('Subtype');
|
|
78
108
|
}
|
|
79
109
|
}
|
|
80
110
|
/**
|
|
81
111
|
* Gets the field name
|
|
82
112
|
*/
|
|
83
113
|
get name() {
|
|
84
|
-
|
|
114
|
+
const parentName = this.parent?.name ?? '';
|
|
115
|
+
const ownName = this.content.get('T')?.as(PdfString)?.value ?? '';
|
|
116
|
+
if (parentName && ownName) {
|
|
117
|
+
return `${parentName}.${ownName}`;
|
|
118
|
+
}
|
|
119
|
+
return parentName || ownName;
|
|
85
120
|
}
|
|
86
121
|
/**
|
|
87
122
|
* Sets the field name
|
|
88
123
|
*/
|
|
89
124
|
set name(name) {
|
|
90
|
-
this.set('T', new PdfString(name));
|
|
125
|
+
this.content.set('T', new PdfString(name));
|
|
91
126
|
}
|
|
92
127
|
/**
|
|
93
128
|
* Gets the default value
|
|
94
129
|
*/
|
|
95
130
|
get defaultValue() {
|
|
96
|
-
const dv = this.get('DV');
|
|
131
|
+
const dv = this.content.get('DV');
|
|
97
132
|
if (dv instanceof PdfString) {
|
|
98
133
|
return dv.value;
|
|
99
134
|
}
|
|
@@ -107,16 +142,23 @@ export class PdfAcroFormField extends PdfDictionary {
|
|
|
107
142
|
*/
|
|
108
143
|
set defaultValue(val) {
|
|
109
144
|
const fieldType = this.fieldType;
|
|
110
|
-
if (fieldType ===
|
|
111
|
-
this.set('DV', new PdfName(val));
|
|
145
|
+
if (fieldType === 'Button') {
|
|
146
|
+
this.content.set('DV', new PdfName(val));
|
|
112
147
|
}
|
|
113
148
|
else {
|
|
114
|
-
this.set('DV', new PdfString(val));
|
|
149
|
+
this.content.set('DV', new PdfString(val));
|
|
115
150
|
}
|
|
116
151
|
}
|
|
117
152
|
get value() {
|
|
118
|
-
const v = this.get('V');
|
|
153
|
+
const v = this.content.get('V');
|
|
119
154
|
if (v instanceof PdfString) {
|
|
155
|
+
// UTF-16BE strings should always use UTF-16BE decoding regardless of font encoding
|
|
156
|
+
if (v.isUTF16BE) {
|
|
157
|
+
return v.value; // Use PdfString's built-in UTF-16BE decoder
|
|
158
|
+
}
|
|
159
|
+
if (this.encodingMap) {
|
|
160
|
+
return decodeWithFontEncoding(v.raw, this.encodingMap);
|
|
161
|
+
}
|
|
120
162
|
return v.value;
|
|
121
163
|
}
|
|
122
164
|
else if (v instanceof PdfName) {
|
|
@@ -125,43 +167,48 @@ export class PdfAcroFormField extends PdfDictionary {
|
|
|
125
167
|
return '';
|
|
126
168
|
}
|
|
127
169
|
set value(val) {
|
|
128
|
-
|
|
129
|
-
|
|
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;
|
|
130
176
|
if (val.trim() === '') {
|
|
131
|
-
this.delete('V');
|
|
132
|
-
this.delete('AS');
|
|
177
|
+
this.content.delete('V');
|
|
178
|
+
this.content.delete('AS');
|
|
133
179
|
return;
|
|
134
180
|
}
|
|
135
|
-
this.set('V', new PdfName(val));
|
|
136
|
-
this.set('AS', new PdfName(val));
|
|
181
|
+
this.content.set('V', new PdfName(val));
|
|
182
|
+
this.content.set('AS', new PdfName(val));
|
|
137
183
|
}
|
|
138
184
|
else {
|
|
139
|
-
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();
|
|
140
189
|
}
|
|
141
190
|
}
|
|
142
191
|
get checked() {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const v = this.get('V');
|
|
192
|
+
if (this.fieldType === 'Button') {
|
|
193
|
+
const v = this.content.get('V');
|
|
146
194
|
return v instanceof PdfName && v.value === 'Yes';
|
|
147
195
|
}
|
|
148
196
|
return false;
|
|
149
197
|
}
|
|
150
198
|
set checked(isChecked) {
|
|
151
|
-
|
|
152
|
-
if (fieldType === PdfFieldType.Button) {
|
|
199
|
+
if (this.fieldType === 'Button') {
|
|
153
200
|
if (isChecked) {
|
|
154
|
-
this.set('V', new PdfName('Yes'));
|
|
155
|
-
this.set('AS', new PdfName('Yes'));
|
|
201
|
+
this.content.set('V', new PdfName('Yes'));
|
|
202
|
+
this.content.set('AS', new PdfName('Yes'));
|
|
156
203
|
}
|
|
157
204
|
else {
|
|
158
|
-
this.set('V', new PdfName('Off'));
|
|
159
|
-
this.set('AS', new PdfName('Off'));
|
|
205
|
+
this.content.set('V', new PdfName('Off'));
|
|
206
|
+
this.content.set('AS', new PdfName('Off'));
|
|
160
207
|
}
|
|
161
208
|
}
|
|
162
209
|
}
|
|
163
210
|
get fontSize() {
|
|
164
|
-
const da = this.get('DA')?.as(PdfString)?.value || '';
|
|
211
|
+
const da = this.content.get('DA')?.as(PdfString)?.value || '';
|
|
165
212
|
const match = da.match(/\/[A-Za-z0-9_-]+\s+([\d.]+)\s+Tf/);
|
|
166
213
|
if (match) {
|
|
167
214
|
return parseFloat(match[1]);
|
|
@@ -169,16 +216,16 @@ export class PdfAcroFormField extends PdfDictionary {
|
|
|
169
216
|
return null;
|
|
170
217
|
}
|
|
171
218
|
set fontSize(size) {
|
|
172
|
-
const da = this.get('DA')?.as(PdfString)?.value || '';
|
|
219
|
+
const da = this.content.get('DA')?.as(PdfString)?.value || '';
|
|
173
220
|
if (!da) {
|
|
174
|
-
this.set('DA', new PdfString(`/F1 ${size} Tf 0 g`));
|
|
221
|
+
this.content.set('DA', new PdfString(`/F1 ${size} Tf 0 g`));
|
|
175
222
|
return;
|
|
176
223
|
}
|
|
177
224
|
const updatedDa = da.replace(/(\/[A-Za-z0-9_-]+)\s+[\d.]+\s+Tf/g, `$1 ${size} Tf`);
|
|
178
|
-
this.set('DA', new PdfString(updatedDa));
|
|
225
|
+
this.content.set('DA', new PdfString(updatedDa));
|
|
179
226
|
}
|
|
180
227
|
get fontName() {
|
|
181
|
-
const da = this.get('DA')?.as(PdfString)?.value || '';
|
|
228
|
+
const da = this.content.get('DA')?.as(PdfString)?.value || '';
|
|
182
229
|
const match = da.match(/\/([A-Za-z0-9_-]+)\s+[\d.]+\s+Tf/);
|
|
183
230
|
if (match) {
|
|
184
231
|
return match[1];
|
|
@@ -186,13 +233,13 @@ export class PdfAcroFormField extends PdfDictionary {
|
|
|
186
233
|
return null;
|
|
187
234
|
}
|
|
188
235
|
set fontName(fontName) {
|
|
189
|
-
const da = this.get('DA')?.as(PdfString)?.value || '';
|
|
236
|
+
const da = this.content.get('DA')?.as(PdfString)?.value || '';
|
|
190
237
|
if (!da) {
|
|
191
|
-
this.set('DA', new PdfString(`/${fontName} 12 Tf 0 g`));
|
|
238
|
+
this.content.set('DA', new PdfString(`/${fontName} 12 Tf 0 g`));
|
|
192
239
|
return;
|
|
193
240
|
}
|
|
194
241
|
const updatedDa = da.replace(/\/[A-Za-z0-9_-]+(\s+[\d.]+\s+Tf)/g, `/${fontName}$1`);
|
|
195
|
-
this.set('DA', new PdfString(updatedDa));
|
|
242
|
+
this.content.set('DA', new PdfString(updatedDa));
|
|
196
243
|
}
|
|
197
244
|
/**
|
|
198
245
|
* Sets the font using a PdfFont object.
|
|
@@ -201,39 +248,51 @@ export class PdfAcroFormField extends PdfDictionary {
|
|
|
201
248
|
set font(font) {
|
|
202
249
|
if (font === null) {
|
|
203
250
|
// Clear font - set to empty or default
|
|
204
|
-
this.set('DA', new PdfString(''));
|
|
251
|
+
this.content.set('DA', new PdfString(''));
|
|
205
252
|
return;
|
|
206
253
|
}
|
|
207
254
|
const resourceName = font.resourceName;
|
|
208
255
|
const currentSize = this.fontSize ?? 12;
|
|
209
|
-
const da = this.get('DA')?.as(PdfString)?.value || '';
|
|
256
|
+
const da = this.content.get('DA')?.as(PdfString)?.value || '';
|
|
210
257
|
if (!da) {
|
|
211
|
-
this.set('DA', new PdfString(`/${resourceName} ${currentSize} Tf 0 g`));
|
|
258
|
+
this.content.set('DA', new PdfString(`/${resourceName} ${currentSize} Tf 0 g`));
|
|
212
259
|
return;
|
|
213
260
|
}
|
|
214
261
|
const updatedDa = da.replace(/\/[A-Za-z0-9_-]+(\s+[\d.]+\s+Tf)/g, `/${resourceName}$1`);
|
|
215
|
-
this.set('DA', new PdfString(updatedDa));
|
|
262
|
+
this.content.set('DA', new PdfString(updatedDa));
|
|
216
263
|
}
|
|
217
264
|
/**
|
|
218
265
|
* Gets field flags (bitwise combination of field attributes)
|
|
219
266
|
*/
|
|
220
267
|
get flags() {
|
|
221
|
-
return this.get('Ff')?.as(PdfNumber)?.value ?? 0;
|
|
268
|
+
return this.content.get('Ff')?.as(PdfNumber)?.value ?? 0;
|
|
222
269
|
}
|
|
223
270
|
/**
|
|
224
271
|
* Sets field flags
|
|
225
272
|
*/
|
|
226
273
|
set flags(flags) {
|
|
227
|
-
this.set('Ff', new PdfNumber(flags));
|
|
274
|
+
this.content.set('Ff', new PdfNumber(flags));
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
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));
|
|
228
287
|
}
|
|
229
288
|
/**
|
|
230
|
-
* Checks if the field is read-only
|
|
289
|
+
* Checks if the field is read-only (Ff bit 1)
|
|
231
290
|
*/
|
|
232
291
|
get readOnly() {
|
|
233
292
|
return (this.flags & 1) !== 0;
|
|
234
293
|
}
|
|
235
294
|
/**
|
|
236
|
-
* Sets the field as read-only or editable
|
|
295
|
+
* Sets the field as read-only or editable (Ff bit 1)
|
|
237
296
|
*/
|
|
238
297
|
set readOnly(isReadOnly) {
|
|
239
298
|
if (isReadOnly) {
|
|
@@ -294,64 +353,865 @@ export class PdfAcroFormField extends PdfDictionary {
|
|
|
294
353
|
this.flags = this.flags & ~8192;
|
|
295
354
|
}
|
|
296
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
|
+
}
|
|
297
1113
|
}
|
|
298
|
-
export class PdfAcroForm extends
|
|
1114
|
+
export class PdfAcroForm extends PdfIndirectObject {
|
|
299
1115
|
fields;
|
|
300
|
-
|
|
1116
|
+
fontEncodingMaps = new Map();
|
|
1117
|
+
document;
|
|
301
1118
|
constructor(options) {
|
|
302
|
-
super(
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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();
|
|
306
1149
|
}
|
|
307
1150
|
/**
|
|
308
1151
|
* Gets the NeedAppearances flag
|
|
309
1152
|
*/
|
|
310
1153
|
get needAppearances() {
|
|
311
|
-
return this.get('NeedAppearances')?.as(PdfBoolean)?.value ?? false;
|
|
1154
|
+
return (this.content.get('NeedAppearances')?.as(PdfBoolean)?.value ?? false);
|
|
312
1155
|
}
|
|
313
1156
|
/**
|
|
314
1157
|
* Sets the NeedAppearances flag to indicate that appearance streams need to be regenerated
|
|
315
1158
|
*/
|
|
316
1159
|
set needAppearances(value) {
|
|
317
|
-
this.set('NeedAppearances', new PdfBoolean(value));
|
|
1160
|
+
this.content.set('NeedAppearances', new PdfBoolean(value));
|
|
318
1161
|
}
|
|
319
1162
|
/**
|
|
320
1163
|
* Gets the signature flags
|
|
321
1164
|
*/
|
|
322
1165
|
get signatureFlags() {
|
|
323
|
-
return this.get('SigFlags')?.as(PdfNumber)?.value ?? 0;
|
|
1166
|
+
return this.content.get('SigFlags')?.as(PdfNumber)?.value ?? 0;
|
|
324
1167
|
}
|
|
325
1168
|
/**
|
|
326
1169
|
* Sets the signature flags
|
|
327
1170
|
*/
|
|
328
1171
|
set signatureFlags(flags) {
|
|
329
|
-
this.set('SigFlags', new PdfNumber(flags));
|
|
1172
|
+
this.content.set('SigFlags', new PdfNumber(flags));
|
|
330
1173
|
}
|
|
331
1174
|
/**
|
|
332
1175
|
* Gets the default appearance string for the form
|
|
333
1176
|
*/
|
|
334
1177
|
get defaultAppearance() {
|
|
335
|
-
return this.get('DA')?.as(PdfString)?.value ?? null;
|
|
1178
|
+
return this.content.get('DA')?.as(PdfString)?.value ?? null;
|
|
336
1179
|
}
|
|
337
1180
|
/**
|
|
338
1181
|
* Sets the default appearance string for the form
|
|
339
1182
|
*/
|
|
340
1183
|
set defaultAppearance(da) {
|
|
341
|
-
this.set('DA', new PdfString(da));
|
|
1184
|
+
this.content.set('DA', new PdfString(da));
|
|
342
1185
|
}
|
|
343
1186
|
/**
|
|
344
1187
|
* Gets the default quadding (alignment) for the form
|
|
345
1188
|
* 0 = left, 1 = center, 2 = right
|
|
346
1189
|
*/
|
|
347
1190
|
get defaultQuadding() {
|
|
348
|
-
return this.get('Q')?.as(PdfNumber)?.value ?? 0;
|
|
1191
|
+
return this.content.get('Q')?.as(PdfNumber)?.value ?? 0;
|
|
349
1192
|
}
|
|
350
1193
|
/**
|
|
351
1194
|
* Sets the default quadding (alignment) for the form
|
|
352
1195
|
*/
|
|
353
1196
|
set defaultQuadding(q) {
|
|
354
|
-
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
|
+
}
|
|
355
1215
|
}
|
|
356
1216
|
/**
|
|
357
1217
|
* Sets multiple field values by field name.
|
|
@@ -383,11 +1243,71 @@ export class PdfAcroForm extends PdfDictionary {
|
|
|
383
1243
|
}
|
|
384
1244
|
return result;
|
|
385
1245
|
}
|
|
1246
|
+
/**
|
|
1247
|
+
* Gets the encoding map for a specific font in the form's resources.
|
|
1248
|
+
* Returns null if no custom encoding is found.
|
|
1249
|
+
* Results are cached for performance.
|
|
1250
|
+
*/
|
|
1251
|
+
async getFontEncodingMap(fontName) {
|
|
1252
|
+
// Check cache first
|
|
1253
|
+
if (this.fontEncodingMaps.has(fontName)) {
|
|
1254
|
+
return this.fontEncodingMaps.get(fontName);
|
|
1255
|
+
}
|
|
1256
|
+
// Get the font from default resources
|
|
1257
|
+
const dr = this.defaultResources;
|
|
1258
|
+
if (!dr) {
|
|
1259
|
+
return null;
|
|
1260
|
+
}
|
|
1261
|
+
const fonts = dr.get('Font')?.as(PdfDictionary);
|
|
1262
|
+
if (!fonts) {
|
|
1263
|
+
return null;
|
|
1264
|
+
}
|
|
1265
|
+
const fontRef = fonts.get(fontName)?.as(PdfObjectReference);
|
|
1266
|
+
if (!fontRef || !this.document) {
|
|
1267
|
+
return null;
|
|
1268
|
+
}
|
|
1269
|
+
// Read the font object
|
|
1270
|
+
const fontObj = await this.document.readObject({
|
|
1271
|
+
objectNumber: fontRef.objectNumber,
|
|
1272
|
+
generationNumber: fontRef.generationNumber,
|
|
1273
|
+
});
|
|
1274
|
+
if (!fontObj) {
|
|
1275
|
+
return null;
|
|
1276
|
+
}
|
|
1277
|
+
const fontDict = fontObj.content.as(PdfDictionary);
|
|
1278
|
+
const encoding = fontDict.get('Encoding');
|
|
1279
|
+
// Handle encoding reference
|
|
1280
|
+
let encodingDict = null;
|
|
1281
|
+
if (encoding instanceof PdfObjectReference) {
|
|
1282
|
+
const encodingObj = await this.document.readObject({
|
|
1283
|
+
objectNumber: encoding.objectNumber,
|
|
1284
|
+
generationNumber: encoding.generationNumber,
|
|
1285
|
+
});
|
|
1286
|
+
encodingDict = encodingObj?.content.as(PdfDictionary) ?? null;
|
|
1287
|
+
}
|
|
1288
|
+
else if (encoding instanceof PdfDictionary) {
|
|
1289
|
+
encodingDict = encoding;
|
|
1290
|
+
}
|
|
1291
|
+
if (!encodingDict) {
|
|
1292
|
+
return null;
|
|
1293
|
+
}
|
|
1294
|
+
// Parse the Differences array
|
|
1295
|
+
const differences = encodingDict.get('Differences')?.as(PdfArray);
|
|
1296
|
+
if (!differences) {
|
|
1297
|
+
return null;
|
|
1298
|
+
}
|
|
1299
|
+
const encodingMap = buildEncodingMap(differences);
|
|
1300
|
+
if (!encodingMap) {
|
|
1301
|
+
return null;
|
|
1302
|
+
}
|
|
1303
|
+
this.fontEncodingMaps.set(fontName, encodingMap);
|
|
1304
|
+
return encodingMap;
|
|
1305
|
+
}
|
|
386
1306
|
static async fromDocument(document) {
|
|
387
|
-
const catalog = document.
|
|
1307
|
+
const catalog = document.root;
|
|
388
1308
|
if (!catalog)
|
|
389
1309
|
return null;
|
|
390
|
-
const acroFormRef = catalog.get('AcroForm');
|
|
1310
|
+
const acroFormRef = catalog.content.get('AcroForm');
|
|
391
1311
|
if (!acroFormRef)
|
|
392
1312
|
return null;
|
|
393
1313
|
let acroFormDict;
|
|
@@ -406,21 +1326,22 @@ export class PdfAcroForm extends PdfDictionary {
|
|
|
406
1326
|
}
|
|
407
1327
|
else if (acroFormRef instanceof PdfDictionary) {
|
|
408
1328
|
acroFormDict = acroFormRef;
|
|
1329
|
+
acroFormContainer = new PdfIndirectObject({ content: acroFormDict });
|
|
409
1330
|
}
|
|
410
1331
|
else {
|
|
411
1332
|
return null;
|
|
412
1333
|
}
|
|
413
|
-
const acroForm = new PdfAcroForm({
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const getFields = async (
|
|
418
|
-
for (const fieldRef of
|
|
419
|
-
const refKey = fieldRef.toString();
|
|
420
|
-
if (
|
|
1334
|
+
const acroForm = new PdfAcroForm({ other: acroFormContainer, document });
|
|
1335
|
+
// Pre-cache font encoding maps for all fonts used in fields
|
|
1336
|
+
await acroForm.cacheAllFontEncodings();
|
|
1337
|
+
const fields = new Map();
|
|
1338
|
+
const getFields = async (fieldRefs, parent) => {
|
|
1339
|
+
for (const fieldRef of fieldRefs) {
|
|
1340
|
+
const refKey = fieldRef.toString().trim();
|
|
1341
|
+
if (fields.has(refKey)) {
|
|
1342
|
+
fields.get(refKey).parent = parent;
|
|
421
1343
|
continue;
|
|
422
1344
|
}
|
|
423
|
-
seen.add(refKey);
|
|
424
1345
|
const fieldObject = await document.readObject({
|
|
425
1346
|
objectNumber: fieldRef.objectNumber,
|
|
426
1347
|
generationNumber: fieldRef.generationNumber,
|
|
@@ -430,37 +1351,62 @@ export class PdfAcroForm extends PdfDictionary {
|
|
|
430
1351
|
if (!(fieldObject.content instanceof PdfDictionary))
|
|
431
1352
|
continue;
|
|
432
1353
|
const field = new PdfAcroFormField({
|
|
433
|
-
|
|
1354
|
+
other: fieldObject,
|
|
1355
|
+
form: acroForm,
|
|
434
1356
|
});
|
|
435
1357
|
field.parent = parent;
|
|
436
|
-
field.copyFrom(fieldObject.content);
|
|
437
1358
|
// Process child fields (Kids) before adding the parent
|
|
438
|
-
const kids = field.
|
|
439
|
-
if (kids) {
|
|
440
|
-
await getFields(kids,
|
|
1359
|
+
const kids = field.kids;
|
|
1360
|
+
if (kids.length > 0) {
|
|
1361
|
+
await getFields(kids, field);
|
|
441
1362
|
}
|
|
442
1363
|
acroForm.fields.push(field);
|
|
1364
|
+
fields.set(refKey, field);
|
|
443
1365
|
}
|
|
444
1366
|
};
|
|
445
1367
|
const fieldsArray = new PdfArray();
|
|
446
|
-
if (acroForm.get('Fields') instanceof PdfArray) {
|
|
447
|
-
fieldsArray.items.push(...acroForm.
|
|
448
|
-
.
|
|
1368
|
+
if (acroForm.content.get('Fields') instanceof PdfArray) {
|
|
1369
|
+
fieldsArray.items.push(...acroForm.content
|
|
1370
|
+
.get('Fields')
|
|
1371
|
+
.as((PdfArray)).items);
|
|
449
1372
|
}
|
|
450
|
-
else if (acroForm.get('Fields') instanceof PdfObjectReference) {
|
|
1373
|
+
else if (acroForm.content.get('Fields') instanceof PdfObjectReference) {
|
|
451
1374
|
const fieldsObj = await document.readObject({
|
|
452
|
-
objectNumber: acroForm.
|
|
453
|
-
.
|
|
454
|
-
|
|
455
|
-
|
|
1375
|
+
objectNumber: acroForm.content
|
|
1376
|
+
.get('Fields')
|
|
1377
|
+
.as(PdfObjectReference).objectNumber,
|
|
1378
|
+
generationNumber: acroForm.content
|
|
1379
|
+
.get('Fields')
|
|
1380
|
+
.as(PdfObjectReference).generationNumber,
|
|
456
1381
|
});
|
|
457
1382
|
if (fieldsObj && fieldsObj.content instanceof PdfArray) {
|
|
458
1383
|
fieldsArray.items.push(...fieldsObj.content.as((PdfArray)).items);
|
|
459
1384
|
}
|
|
460
1385
|
}
|
|
461
|
-
await getFields(fieldsArray);
|
|
1386
|
+
await getFields(fieldsArray.items);
|
|
462
1387
|
return acroForm;
|
|
463
1388
|
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Pre-caches encoding maps for all fonts used in the form fields.
|
|
1391
|
+
* This makes subsequent field value access faster and synchronous.
|
|
1392
|
+
*/
|
|
1393
|
+
async cacheAllFontEncodings() {
|
|
1394
|
+
const fontNames = new Set();
|
|
1395
|
+
// Collect all font names from field DA strings
|
|
1396
|
+
for (const field of this.fields) {
|
|
1397
|
+
const da = field.content.get('DA')?.as(PdfString)?.value;
|
|
1398
|
+
if (da) {
|
|
1399
|
+
const fontMatch = da.match(/\/(\w+)\s+[\d.]+\s+Tf/);
|
|
1400
|
+
if (fontMatch) {
|
|
1401
|
+
fontNames.add(fontMatch[1]);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
// Pre-cache encoding for each font
|
|
1406
|
+
for (const fontName of fontNames) {
|
|
1407
|
+
await this.getFontEncodingMap(fontName);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
464
1410
|
/**
|
|
465
1411
|
* Gets or creates the Annots array for a page.
|
|
466
1412
|
* Returns the array and metadata about whether it's an indirect object.
|
|
@@ -518,7 +1464,7 @@ export class PdfAcroForm extends PdfDictionary {
|
|
|
518
1464
|
});
|
|
519
1465
|
if (!pageObj)
|
|
520
1466
|
continue;
|
|
521
|
-
const pageDict = pageObj.content.as(PdfDictionary);
|
|
1467
|
+
const pageDict = pageObj.content.as(PdfDictionary).clone();
|
|
522
1468
|
const annotsInfo = await this.getPageAnnotsArray(document, pageDict);
|
|
523
1469
|
this.addFieldsToAnnots(annotsInfo.annotsArray, fieldRefs);
|
|
524
1470
|
// Write the Annots array if it's an indirect object
|
|
@@ -541,33 +1487,45 @@ export class PdfAcroForm extends PdfDictionary {
|
|
|
541
1487
|
}
|
|
542
1488
|
}
|
|
543
1489
|
async write(document) {
|
|
544
|
-
const catalog = document.
|
|
545
|
-
if (!catalog) {
|
|
546
|
-
throw new Error('Document has no root catalog');
|
|
547
|
-
}
|
|
1490
|
+
const catalog = document.root;
|
|
548
1491
|
const isIncremental = document.isIncremental();
|
|
549
1492
|
document.setIncremental(true);
|
|
550
1493
|
const fieldsArray = new PdfArray();
|
|
551
|
-
this.set('Fields', fieldsArray);
|
|
1494
|
+
this.content.set('Fields', fieldsArray);
|
|
552
1495
|
// Track fields that need to be added to page annotations
|
|
553
1496
|
const fieldsByPage = new Map();
|
|
554
1497
|
for (const field of this.fields) {
|
|
555
|
-
if (!field.isModified())
|
|
556
|
-
continue;
|
|
557
|
-
const acroFormFieldIndirect = new PdfIndirectObject({
|
|
558
|
-
...field.container,
|
|
559
|
-
content: field,
|
|
560
|
-
});
|
|
561
1498
|
let fieldReference;
|
|
562
1499
|
if (field.isModified()) {
|
|
563
|
-
//
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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);
|
|
569
1527
|
// Create a proper PdfObjectReference (not the proxy from .reference)
|
|
570
|
-
fieldReference = new PdfObjectReference(
|
|
1528
|
+
fieldReference = new PdfObjectReference(field.objectNumber, field.generationNumber);
|
|
571
1529
|
// Track if this field needs to be added to a page's Annots
|
|
572
1530
|
const parentRef = field.parentRef;
|
|
573
1531
|
const isWidget = field.isWidget;
|
|
@@ -583,39 +1541,21 @@ export class PdfAcroForm extends PdfDictionary {
|
|
|
583
1541
|
}
|
|
584
1542
|
}
|
|
585
1543
|
else {
|
|
586
|
-
|
|
587
|
-
const container = field.container;
|
|
588
|
-
if (container &&
|
|
589
|
-
typeof container.objectNumber === 'number' &&
|
|
590
|
-
typeof container.generationNumber === 'number') {
|
|
591
|
-
fieldReference = new PdfObjectReference(container.objectNumber, container.generationNumber);
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
if (fieldReference) {
|
|
595
|
-
fieldsArray.push(fieldReference);
|
|
1544
|
+
fieldReference = field.reference;
|
|
596
1545
|
}
|
|
1546
|
+
fieldsArray.push(fieldReference);
|
|
597
1547
|
}
|
|
598
1548
|
// Add field references to page annotations
|
|
599
1549
|
await this.updatePageAnnotations(document, fieldsByPage);
|
|
600
1550
|
if (this.isModified()) {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
const rootRef = document.trailerDict
|
|
610
|
-
.get('Root')
|
|
611
|
-
?.as(PdfObjectReference);
|
|
612
|
-
if (rootRef) {
|
|
613
|
-
const rootIndirect = new PdfIndirectObject({
|
|
614
|
-
objectNumber: rootRef.objectNumber,
|
|
615
|
-
generationNumber: rootRef.generationNumber,
|
|
616
|
-
content: catalog,
|
|
617
|
-
});
|
|
618
|
-
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);
|
|
619
1559
|
}
|
|
620
1560
|
}
|
|
621
1561
|
await document.commit();
|