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.
Files changed (52) hide show
  1. package/EXAMPLES.md +1 -1
  2. package/dist/acroform/acroform.d.ts +272 -16
  3. package/dist/acroform/acroform.js +1084 -144
  4. package/dist/acroform/manager.d.ts +2 -2
  5. package/dist/acroform/manager.js +3 -3
  6. package/dist/core/decoder.d.ts +1 -1
  7. package/dist/core/decoder.js +3 -3
  8. package/dist/core/index.d.ts +2 -2
  9. package/dist/core/index.js +2 -2
  10. package/dist/core/objects/pdf-array.d.ts +1 -0
  11. package/dist/core/objects/pdf-array.js +4 -0
  12. package/dist/core/objects/pdf-dictionary.d.ts +1 -0
  13. package/dist/core/objects/pdf-dictionary.js +12 -0
  14. package/dist/core/objects/pdf-hexadecimal.d.ts +9 -2
  15. package/dist/core/objects/pdf-hexadecimal.js +25 -5
  16. package/dist/core/objects/pdf-indirect-object.d.ts +5 -3
  17. package/dist/core/objects/pdf-indirect-object.js +23 -5
  18. package/dist/core/objects/pdf-number.js +3 -0
  19. package/dist/core/objects/pdf-object.d.ts +6 -0
  20. package/dist/core/objects/pdf-object.js +10 -0
  21. package/dist/core/objects/pdf-stream.js +3 -0
  22. package/dist/core/objects/pdf-string.d.ts +11 -1
  23. package/dist/core/objects/pdf-string.js +24 -6
  24. package/dist/core/ref.d.ts +5 -0
  25. package/dist/core/ref.js +14 -0
  26. package/dist/core/serializer.d.ts +1 -1
  27. package/dist/core/serializer.js +1 -1
  28. package/dist/core/tokeniser.d.ts +2 -2
  29. package/dist/core/tokeniser.js +37 -75
  30. package/dist/core/tokens/hexadecimal-token.d.ts +8 -1
  31. package/dist/core/tokens/hexadecimal-token.js +20 -2
  32. package/dist/core/tokens/name-token.js +0 -3
  33. package/dist/core/tokens/string-token.d.ts +8 -1
  34. package/dist/core/tokens/string-token.js +20 -2
  35. package/dist/fonts/font-manager.js +6 -8
  36. package/dist/pdf/pdf-document.d.ts +12 -11
  37. package/dist/pdf/pdf-document.js +50 -42
  38. package/dist/pdf/pdf-revision.d.ts +33 -4
  39. package/dist/pdf/pdf-revision.js +100 -26
  40. package/dist/pdf/pdf-xref-lookup.js +3 -2
  41. package/dist/utils/decodeWithFontEncoding.d.ts +20 -0
  42. package/dist/utils/decodeWithFontEncoding.js +67 -0
  43. package/dist/utils/escapeString.d.ts +1 -1
  44. package/dist/utils/escapeString.js +12 -3
  45. package/dist/utils/glyphNameToUnicode.d.ts +10 -0
  46. package/dist/utils/glyphNameToUnicode.js +4292 -0
  47. package/dist/xfa/manager.js +2 -4
  48. package/package.json +1 -1
  49. /package/dist/core/{incremental-parser.d.ts → parser/incremental-parser.d.ts} +0 -0
  50. /package/dist/core/{incremental-parser.js → parser/incremental-parser.js} +0 -0
  51. /package/dist/core/{parser.d.ts → parser/parser.d.ts} +0 -0
  52. /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 PdfDictionary {
20
+ export class PdfAcroFormField extends PdfIndirectObject {
19
21
  parent;
20
- container;
22
+ defaultGenerateAppearance = true;
23
+ _appearanceStream;
24
+ _appearanceStreamYes; // For button fields: checked state
25
+ form;
21
26
  constructor(options) {
22
- super();
23
- this.container = options?.container;
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
- return this.get('FT')?.as(PdfName)?.value ?? null;
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
- return this.get('T')?.as(PdfString)?.value ?? '';
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 === PdfFieldType.Button) {
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
- const fieldType = this.get('FT')?.as(PdfName)?.value;
129
- if (fieldType === PdfFieldType.Button) {
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
- const fieldType = this.get('FT')?.as(PdfName)?.value;
144
- if (fieldType === PdfFieldType.Button) {
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
- const fieldType = this.get('FT')?.as(PdfName)?.value;
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 PdfDictionary {
1114
+ export class PdfAcroForm extends PdfIndirectObject {
299
1115
  fields;
300
- container;
1116
+ fontEncodingMaps = new Map();
1117
+ document;
301
1118
  constructor(options) {
302
- super();
303
- this.copyFrom(options.dict);
304
- this.fields = options.fields ?? [];
305
- this.container = options.container;
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.rootDictionary;
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
- dict: acroFormDict,
415
- container: acroFormContainer,
416
- });
417
- const getFields = async (fields, seen = new Set(), parent) => {
418
- for (const fieldRef of fields.items) {
419
- const refKey = fieldRef.toString();
420
- if (seen.has(refKey)) {
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
- container: fieldObject,
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.get('Kids')?.as((PdfArray));
439
- if (kids) {
440
- await getFields(kids, seen, field);
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.get('Fields').as((PdfArray))
448
- .items);
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.get('Fields').as(PdfObjectReference)
453
- .objectNumber,
454
- generationNumber: acroForm.get('Fields').as(PdfObjectReference)
455
- .generationNumber,
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.rootDictionary?.clone();
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
- // Write modified field as an indirect object
564
- const acroFormFieldIndirect = new PdfIndirectObject({
565
- ...field.container,
566
- content: field,
567
- });
568
- document.add(acroFormFieldIndirect);
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(acroFormFieldIndirect.objectNumber, acroFormFieldIndirect.generationNumber);
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
- // Unmodified field: reuse existing indirect reference information
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
- // Create or update the AcroForm entry in the catalog
602
- const acroFormIndirect = new PdfIndirectObject({
603
- ...this.container,
604
- content: this,
605
- });
606
- document.add(acroFormIndirect);
607
- catalog.set('AcroForm', acroFormIndirect.reference);
608
- // In incremental mode, ensure the updated catalog is written
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();