pdf-lite 1.3.0 → 1.3.1

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