pdf-lite 1.6.3 → 1.7.0

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 (59) hide show
  1. package/dist/acroform/appearance/pdf-button-appearance-stream.js +1 -1
  2. package/dist/acroform/appearance/pdf-text-appearance-stream.js +42 -25
  3. package/dist/acroform/fields/pdf-button-form-field.d.ts +11 -2
  4. package/dist/acroform/fields/pdf-button-form-field.js +76 -37
  5. package/dist/acroform/fields/pdf-choice-form-field.js +2 -2
  6. package/dist/acroform/fields/pdf-form-field.d.ts +15 -9
  7. package/dist/acroform/fields/pdf-form-field.js +124 -47
  8. package/dist/acroform/fields/pdf-text-form-field.js +2 -2
  9. package/dist/acroform/pdf-acro-form.d.ts +1 -0
  10. package/dist/acroform/pdf-acro-form.js +15 -4
  11. package/dist/acroform/xfa/pdf-xfa-data.d.ts +2 -1
  12. package/dist/acroform/xfa/pdf-xfa-data.js +36 -28
  13. package/dist/annotations/pdf-annotation.d.ts +1 -1
  14. package/dist/annotations/pdf-annotation.js +16 -2
  15. package/dist/core/generators.js +12 -6
  16. package/dist/core/objects/pdf-array.d.ts +6 -1
  17. package/dist/core/objects/pdf-array.js +3 -0
  18. package/dist/core/objects/pdf-boolean.d.ts +6 -2
  19. package/dist/core/objects/pdf-boolean.js +3 -0
  20. package/dist/core/objects/pdf-comment.d.ts +6 -2
  21. package/dist/core/objects/pdf-comment.js +3 -0
  22. package/dist/core/objects/pdf-date.d.ts +4 -0
  23. package/dist/core/objects/pdf-date.js +3 -0
  24. package/dist/core/objects/pdf-dictionary.d.ts +6 -0
  25. package/dist/core/objects/pdf-dictionary.js +19 -0
  26. package/dist/core/objects/pdf-hexadecimal.d.ts +6 -2
  27. package/dist/core/objects/pdf-hexadecimal.js +3 -0
  28. package/dist/core/objects/pdf-indirect-object.d.ts +8 -1
  29. package/dist/core/objects/pdf-indirect-object.js +14 -0
  30. package/dist/core/objects/pdf-name.d.ts +6 -2
  31. package/dist/core/objects/pdf-name.js +3 -0
  32. package/dist/core/objects/pdf-null.d.ts +5 -2
  33. package/dist/core/objects/pdf-null.js +3 -0
  34. package/dist/core/objects/pdf-number.d.ts +6 -1
  35. package/dist/core/objects/pdf-number.js +3 -0
  36. package/dist/core/objects/pdf-object-reference.d.ts +5 -0
  37. package/dist/core/objects/pdf-object-reference.js +7 -0
  38. package/dist/core/objects/pdf-object.d.ts +1 -0
  39. package/dist/core/objects/pdf-start-xref.d.ts +6 -1
  40. package/dist/core/objects/pdf-start-xref.js +3 -0
  41. package/dist/core/objects/pdf-stream.d.ts +8 -0
  42. package/dist/core/objects/pdf-stream.js +7 -0
  43. package/dist/core/objects/pdf-string.d.ts +8 -2
  44. package/dist/core/objects/pdf-string.js +9 -0
  45. package/dist/core/objects/pdf-trailer.d.ts +7 -0
  46. package/dist/core/objects/pdf-trailer.js +3 -0
  47. package/dist/core/objects/pdf-xref-table.d.ts +31 -5
  48. package/dist/core/objects/pdf-xref-table.js +23 -0
  49. package/dist/core/parser/incremental-parser.d.ts +7 -2
  50. package/dist/core/parser/incremental-parser.js +9 -3
  51. package/dist/pdf/pdf-document.d.ts +7 -0
  52. package/dist/pdf/pdf-document.js +6 -0
  53. package/dist/pdf/pdf-revision.d.ts +4 -0
  54. package/dist/pdf/pdf-revision.js +6 -0
  55. package/dist/utils/iterable-readable-stream.d.ts +2 -0
  56. package/dist/utils/iterable-readable-stream.js +8 -1
  57. package/dist/utils/xml.d.ts +9 -0
  58. package/dist/utils/xml.js +59 -0
  59. package/package.json +2 -2
@@ -39,7 +39,7 @@ export class PdfButtonAppearanceStream extends PdfAppearanceStream {
39
39
  g.restore();
40
40
  }
41
41
  else {
42
- const checkSize = size * 0.8;
42
+ const checkSize = size * 0.65;
43
43
  const offset = (size - checkSize) / 2;
44
44
  g.save();
45
45
  g.beginText();
@@ -28,8 +28,8 @@ export class PdfTextAppearanceStream extends PdfAppearanceStream {
28
28
  g.setDefaultAppearance(new PdfDefaultAppearance(ctx.da.fontName, DEFAULT_FONT_SIZE, ctx.da.colorOp));
29
29
  // ── Determine font size ──────────────────────────────────────
30
30
  let finalFontSize;
31
- if (autoSize) {
32
- // Acrobat auto-size: default to 12pt with wrapping, then
31
+ if (autoSize && ctx.multiline) {
32
+ // Multiline auto-size: default to 12pt with wrapping, then
33
33
  // shrink only if the wrapped text still doesn't fit.
34
34
  finalFontSize = DEFAULT_FONT_SIZE;
35
35
  const testLines = g.wrapTextToLines(value, availableWidth);
@@ -38,6 +38,17 @@ export class PdfTextAppearanceStream extends PdfAppearanceStream {
38
38
  }
39
39
  finalFontSize = Math.max(finalFontSize, 0.5);
40
40
  }
41
+ else if (autoSize) {
42
+ // Single-line auto-size: fit to available height, then
43
+ // shrink further if text overflows the width.
44
+ finalFontSize = Math.min(DEFAULT_FONT_SIZE, availableHeight);
45
+ g.setDefaultAppearance(new PdfDefaultAppearance(ctx.da.fontName, finalFontSize, ctx.da.colorOp));
46
+ const textWidth = g.measureTextWidth(value);
47
+ if (textWidth > availableWidth) {
48
+ finalFontSize = g.calculateFittingFontSize(value, availableWidth);
49
+ }
50
+ finalFontSize = Math.max(finalFontSize, 0.5);
51
+ }
41
52
  else {
42
53
  finalFontSize = ctx.da.fontSize;
43
54
  }
@@ -45,30 +56,14 @@ export class PdfTextAppearanceStream extends PdfAppearanceStream {
45
56
  const finalDA = new PdfDefaultAppearance(ctx.da.fontName, finalFontSize, ctx.da.colorOp);
46
57
  g.setDefaultAppearance(finalDA);
47
58
  let lines = [];
48
- if (ctx.multiline || autoSize) {
49
- if (!autoSize) {
50
- const testLines = g.wrapTextToLines(value, availableWidth);
51
- const lineHeight = finalFontSize * 1.2;
52
- if (testLines.length * lineHeight > availableHeight) {
53
- finalFontSize = g.calculateFittingFontSize(value, availableWidth, availableHeight, 1.2);
54
- g.setDefaultAppearance(new PdfDefaultAppearance(ctx.da.fontName, finalFontSize, ctx.da.colorOp));
55
- }
56
- }
57
- lines = g.wrapTextToLines(value, availableWidth);
58
- const renderLineHeight = finalFontSize * 1.2;
59
- const startY = height - padding - finalFontSize;
60
- g.beginText();
61
- g.moveTo(padding, startY);
62
- for (let i = 0; i < lines.length; i++) {
63
- if (i > 0)
64
- g.moveTo(0, -renderLineHeight);
65
- g.showText(lines[i].replace(/\r/g, ''), isUnicode, reverseEncodingMap);
66
- }
67
- g.endText();
68
- }
69
- else if (ctx.comb && ctx.maxLen) {
59
+ if (ctx.comb && ctx.maxLen) {
70
60
  const cellWidth = width / ctx.maxLen;
71
61
  const chars = [...value];
62
+ // For auto-size comb, fit font to cell height
63
+ if (autoSize) {
64
+ finalFontSize = Math.min(height - 2 * padding, cellWidth);
65
+ g.setDefaultAppearance(new PdfDefaultAppearance(ctx.da.fontName, finalFontSize, ctx.da.colorOp));
66
+ }
72
67
  let maxCharWidth = 0;
73
68
  let widestChar = chars[0] ?? '';
74
69
  for (const char of chars) {
@@ -85,13 +80,35 @@ export class PdfTextAppearanceStream extends PdfAppearanceStream {
85
80
  const textY = (height - finalFontSize) / 2 + finalFontSize * 0.2;
86
81
  g.beginText();
87
82
  for (let i = 0; i < chars.length && i < ctx.maxLen; i++) {
88
- const cellX = cellWidth * i + cellWidth / 2 - finalFontSize * 0.3;
83
+ const charWidth = g.measureTextWidth(chars[i]);
84
+ const cellX = cellWidth * i + (cellWidth - charWidth) / 2;
89
85
  g.moveTo(cellX, textY);
90
86
  g.showText(chars[i], isUnicode, reverseEncodingMap);
91
87
  g.moveTo(-cellX, -textY);
92
88
  }
93
89
  g.endText();
94
90
  }
91
+ else if (ctx.multiline) {
92
+ if (!autoSize) {
93
+ const testLines = g.wrapTextToLines(value, availableWidth);
94
+ const lineHeight = finalFontSize * 1.2;
95
+ if (testLines.length * lineHeight > availableHeight) {
96
+ finalFontSize = g.calculateFittingFontSize(value, availableWidth, availableHeight, 1.2);
97
+ g.setDefaultAppearance(new PdfDefaultAppearance(ctx.da.fontName, finalFontSize, ctx.da.colorOp));
98
+ }
99
+ }
100
+ lines = g.wrapTextToLines(value, availableWidth);
101
+ const renderLineHeight = finalFontSize * 1.2;
102
+ const startY = height - padding - finalFontSize;
103
+ g.beginText();
104
+ g.moveTo(padding, startY);
105
+ for (let i = 0; i < lines.length; i++) {
106
+ if (i > 0)
107
+ g.moveTo(0, -renderLineHeight);
108
+ g.showText(lines[i].replace(/\r/g, ''), isUnicode, reverseEncodingMap);
109
+ }
110
+ g.endText();
111
+ }
95
112
  else {
96
113
  // Single line — for non-auto-size, shrink if text overflows
97
114
  if (!autoSize) {
@@ -1,14 +1,23 @@
1
1
  import { PdfFormField } from './pdf-form-field.js';
2
2
  import { PdfString } from '../../core/objects/pdf-string.js';
3
+ import type { PdfAcroForm } from '../pdf-acro-form.js';
4
+ import type { PdfIndirectObject } from '../../core/objects/pdf-indirect-object.js';
3
5
  /**
4
6
  * Button form field subtype (checkboxes, radio buttons, push buttons).
5
7
  */
6
8
  export declare class PdfButtonFormField extends PdfFormField {
7
- set defaultValue(val: string);
8
- protected _storeValue(val: string | PdfString, fieldParent: PdfFormField | undefined): boolean;
9
+ constructor(other?: PdfIndirectObject | {
10
+ form?: PdfAcroForm;
11
+ });
12
+ private initWidget;
13
+ set isWidget(val: boolean);
14
+ get isGroup(): boolean;
15
+ get value(): string;
16
+ set value(val: string | PdfString);
9
17
  get checked(): boolean;
10
18
  set checked(isChecked: boolean);
11
19
  generateAppearance(options?: {
12
20
  makeReadOnly?: boolean;
21
+ onStateName?: string;
13
22
  }): boolean;
14
23
  }
@@ -1,6 +1,5 @@
1
1
  import { PdfFormField } from './pdf-form-field.js';
2
2
  import { PdfButtonAppearanceStream } from '../appearance/pdf-button-appearance-stream.js';
3
- import { PdfName } from '../../core/objects/pdf-name.js';
4
3
  import { PdfString } from '../../core/objects/pdf-string.js';
5
4
  /**
6
5
  * Button form field subtype (checkboxes, radio buttons, push buttons).
@@ -9,50 +8,80 @@ export class PdfButtonFormField extends PdfFormField {
9
8
  static {
10
9
  PdfFormField.registerFieldType('Btn', PdfButtonFormField);
11
10
  }
12
- set defaultValue(val) {
13
- this.content.set('DV', new PdfName(val));
14
- }
15
- _storeValue(val, fieldParent) {
16
- const strVal = val instanceof PdfString ? val.value : val;
17
- if (strVal.trim() === '') {
18
- this.content.delete('V');
19
- fieldParent?.content.delete('V');
20
- this.content.delete('AS');
21
- return false;
11
+ constructor(other) {
12
+ super(other);
13
+ if (this.isWidget && this.appearanceStates.length === 0) {
14
+ this.initWidget();
22
15
  }
23
- // Check if the value matches an existing appearance state;
24
- // otherwise map truthy values to the widget's "on" state (the
25
- // first state that isn't "Off"), falling back to "Yes".
26
- const states = this.appearanceStates;
27
- let resolved;
28
- if (states.includes(strVal)) {
29
- resolved = strVal;
16
+ }
17
+ initWidget() {
18
+ this.rect ||= [0, 0, 50, 50];
19
+ this.generateAppearance({
20
+ onStateName: this.onState ?? 'Yes',
21
+ });
22
+ this.appearanceState ??= 'Off';
23
+ }
24
+ set isWidget(val) {
25
+ super.isWidget = val;
26
+ // Only initialize the widget if it has no existing appearances.
27
+ // `appearanceStream` is write-only on PdfFormField, so instead check
28
+ // the underlying appearance data structures.
29
+ if (val &&
30
+ !this.appearanceStreamDict &&
31
+ this.appearanceStates.length === 0) {
32
+ this.initWidget();
30
33
  }
31
- else if (strVal === 'Off' || strVal === 'No') {
32
- resolved = 'Off';
34
+ }
35
+ get isGroup() {
36
+ return this.children.length > 0;
37
+ }
38
+ get value() {
39
+ return super.value;
40
+ }
41
+ set value(val) {
42
+ const strVal = val instanceof PdfString ? val.value : val;
43
+ this.setRawValue(new PdfString(strVal));
44
+ if (this.isGroup) {
45
+ const children = this.children;
46
+ // 'Off' means explicitly uncheck all children
47
+ if (strVal === 'Off') {
48
+ for (const child of children) {
49
+ child.appearanceState = 'Off';
50
+ if (this._form)
51
+ child.form = this._form;
52
+ }
53
+ }
54
+ else {
55
+ let wasSet = false;
56
+ for (const child of children) {
57
+ const foundState = child.onStates.includes(strVal);
58
+ if (!wasSet && foundState) {
59
+ wasSet = true;
60
+ }
61
+ child.appearanceState = foundState ? strVal : 'Off';
62
+ if (this._form)
63
+ child.form = this._form;
64
+ }
65
+ if (!wasSet && children.length > 0) {
66
+ // If value doesn't match any on-state, check first child by default
67
+ children[0].appearanceState =
68
+ children[0].onStates[0] ?? 'Off';
69
+ }
70
+ }
33
71
  }
34
72
  else {
35
- resolved = states.find((s) => s !== 'Off') ?? 'Yes';
73
+ this.appearanceState = strVal;
36
74
  }
37
- this.content.set('V', new PdfName(resolved));
38
- fieldParent?.content.set('V', new PdfName(resolved));
39
- this.content.set('AS', new PdfName(resolved));
40
- return true;
41
75
  }
42
76
  get checked() {
43
- const v = this.content.get('V') ?? this.parent?.content.get('V');
44
- return v instanceof PdfName && v.value !== 'Off';
77
+ return !(this.appearanceState === 'Off' || this.value === 'Off');
45
78
  }
46
79
  set checked(isChecked) {
47
- const target = this.parent ?? this;
48
80
  if (isChecked) {
49
- const onState = this.appearanceStates.find((s) => s !== 'Off') ?? 'Yes';
50
- target.content.set('V', new PdfName(onState));
51
- this.content.set('AS', new PdfName(onState));
81
+ this.value = this.onState ?? 'Yes';
52
82
  }
53
83
  else {
54
- target.content.set('V', new PdfName('Off'));
55
- this.content.set('AS', new PdfName('Off'));
84
+ this.value = 'Off';
56
85
  }
57
86
  }
58
87
  generateAppearance(options) {
@@ -71,11 +100,21 @@ export class PdfButtonFormField extends PdfFormField {
71
100
  height,
72
101
  contentStream: '',
73
102
  });
74
- const onState = this.appearanceStates.find((s) => s !== 'Off') ?? 'Yes';
75
- this.setAppearanceStream({
76
- [onState]: yesAppearance,
103
+ const existingOnState = this.onState;
104
+ const as = this.appearanceState;
105
+ const onKey = options?.onStateName ??
106
+ (as && as !== 'Off' ? as : (existingOnState ?? 'Yes'));
107
+ if (onKey === 'Off') {
108
+ throw new Error("Invalid on-state name 'Off' for button field appearance stream");
109
+ }
110
+ this.appearanceStream = {
111
+ [onKey]: yesAppearance,
77
112
  Off: noAppearance,
78
- });
113
+ };
114
+ this.downAppearanceStream = {
115
+ [onKey]: yesAppearance,
116
+ Off: noAppearance,
117
+ };
79
118
  if (options?.makeReadOnly) {
80
119
  this.readOnly = true;
81
120
  this.print = true;
@@ -91,7 +91,7 @@ export class PdfChoiceFormField extends PdfFormField {
91
91
  const fontResources = this.buildFontResources(parsed.fontName);
92
92
  const isUnicode = font?.isUnicode ?? false;
93
93
  const reverseEncodingMap = font?.reverseEncodingMap;
94
- this.setAppearanceStream(new PdfChoiceAppearanceStream({
94
+ this.appearanceStream = new PdfChoiceAppearanceStream({
95
95
  rect: rect,
96
96
  value,
97
97
  da: parsed,
@@ -101,7 +101,7 @@ export class PdfChoiceFormField extends PdfFormField {
101
101
  reverseEncodingMap,
102
102
  displayOptions: this.options.map((opt) => opt.label),
103
103
  selectedIndex: this.selectedIndex,
104
- }));
104
+ });
105
105
  if (options?.makeReadOnly) {
106
106
  this.readOnly = true;
107
107
  this.print = true;
@@ -25,6 +25,7 @@ export declare abstract class PdfFormField extends PdfWidgetAnnotation {
25
25
  form?: PdfAcroForm;
26
26
  });
27
27
  set form(f: PdfAcroForm);
28
+ static getFieldType(other: PdfIndirectObject): 'Btn' | 'Sig' | 'Tx' | 'Ch' | null;
28
29
  static create(other?: PdfIndirectObject): PdfFormField;
29
30
  get parent(): PdfFormField | undefined;
30
31
  set parent(field: PdfFormField | PdfIndirectObject | undefined);
@@ -46,14 +47,12 @@ export declare abstract class PdfFormField extends PdfWidgetAnnotation {
46
47
  set name(name: string);
47
48
  get defaultValue(): string;
48
49
  set defaultValue(val: string);
50
+ get onStates(): string[];
51
+ get onState(): string | null;
52
+ set onState(state: string);
49
53
  get value(): string;
54
+ protected setRawValue(val: string | PdfString): void;
50
55
  set value(val: string | PdfString);
51
- /**
52
- * Writes the value to the dictionary. Returns true if appearance generation
53
- * should proceed, false to skip it (e.g. when value was cleared).
54
- * Override in subclasses to change the stored representation.
55
- */
56
- protected _storeValue(val: string | PdfString, fieldParent: PdfFormField | undefined): boolean;
57
56
  get fontSize(): number | null;
58
57
  set fontSize(size: number);
59
58
  get fontName(): string | null;
@@ -108,16 +107,23 @@ export declare abstract class PdfFormField extends PdfWidgetAnnotation {
108
107
  abstract generateAppearance(options?: {
109
108
  makeReadOnly?: boolean;
110
109
  textYOffset?: number;
110
+ onStateName?: string;
111
111
  }): boolean;
112
- setAppearanceStream(stream: PdfIndirectObject | {
112
+ set appearanceStream(stream: PdfIndirectObject | {
113
113
  [key: string]: PdfIndirectObject;
114
- }): void;
114
+ });
115
+ set downAppearanceStream(stream: PdfIndirectObject | {
116
+ [key: string]: PdfIndirectObject;
117
+ });
118
+ get appearanceState(): string | null;
119
+ set appearanceState(state: string | null);
115
120
  /**
116
121
  * Returns the list of appearance state names from the normal appearance
117
122
  * dictionary (e.g. ["Yes", "Off"] for a checkbox).
118
123
  */
119
- get appearanceStates(): string[];
124
+ get appearanceStates(): ReadonlyArray<string>;
120
125
  getAppearanceStream(setting?: string): PdfIndirectObject<PdfStream> | null;
126
+ hasAppearanceStream(setting: string): boolean;
121
127
  private static _fallbackCtor?;
122
128
  private static _registry;
123
129
  static registerFieldType(ft: 'Sig' | 'Btn' | 'Tx' | 'Ch', ctor: new (other?: PdfIndirectObject) => PdfFormField, options?: {
@@ -32,10 +32,25 @@ export class PdfFormField extends PdfWidgetAnnotation {
32
32
  set form(f) {
33
33
  this._form = f;
34
34
  }
35
+ static getFieldType(other) {
36
+ if (!(other.content instanceof PdfDictionary))
37
+ return null;
38
+ const ft = other.content.get('FT')?.as(PdfName)?.value;
39
+ if (ft)
40
+ return ft;
41
+ const parentRef = other.content.get('Parent');
42
+ if (parentRef instanceof PdfObjectReference) {
43
+ const parentResolved = parentRef.resolve();
44
+ if (parentResolved?.content instanceof PdfDictionary) {
45
+ return (parentResolved.content.get('FT')?.as(PdfName)?.value ?? null);
46
+ }
47
+ }
48
+ return null;
49
+ }
35
50
  static create(other) {
36
51
  if (!(other?.content instanceof PdfDictionary))
37
52
  throw new Error('Invalid form field object');
38
- const ft = other?.content.get('FT')?.as(PdfName)?.value;
53
+ const ft = PdfFormField.getFieldType(other);
39
54
  const cls = ft ? PdfFormField._registry.get(ft) : undefined;
40
55
  if (!cls) {
41
56
  if (PdfFormField._fallbackCtor) {
@@ -60,14 +75,23 @@ export class PdfFormField extends PdfWidgetAnnotation {
60
75
  if (field instanceof PdfFormField) {
61
76
  field.children = [...field.children, this];
62
77
  }
78
+ // Auto-add widget to page's Annots array
79
+ const page = this.page;
80
+ if (page) {
81
+ const annots = page.annotations;
82
+ const ref = this.reference;
83
+ const key = ref.key;
84
+ const alreadyPresent = annots.items.some((r) => r instanceof PdfObjectReference && r.key === key);
85
+ if (!alreadyPresent) {
86
+ annots.items.push(ref);
87
+ }
88
+ }
63
89
  }
64
90
  get children() {
65
91
  const kids = this.content.get('Kids')?.items ?? [];
66
92
  const result = [];
67
93
  for (const ref of kids) {
68
94
  const resolved = ref.resolve();
69
- if (!resolved || !(resolved.content instanceof PdfDictionary))
70
- continue;
71
95
  result.push(PdfFormField.create(resolved));
72
96
  }
73
97
  return result;
@@ -157,8 +181,7 @@ export class PdfFormField extends PdfWidgetAnnotation {
157
181
  return undefined;
158
182
  }
159
183
  get fieldType() {
160
- const ft = this.content.get('FT')?.as(PdfName)?.value ??
161
- this.parent?.content.get('FT')?.as(PdfName)?.value;
184
+ const ft = PdfFormField.getFieldType(this);
162
185
  switch (ft) {
163
186
  case 'Tx':
164
187
  return 'Text';
@@ -202,6 +225,27 @@ export class PdfFormField extends PdfWidgetAnnotation {
202
225
  set defaultValue(val) {
203
226
  this.content.set('DV', new PdfString(val));
204
227
  }
228
+ get onStates() {
229
+ return this.appearanceStates.filter((s) => s !== 'Off');
230
+ }
231
+ get onState() {
232
+ return this.appearanceStates.find((s) => s !== 'Off') || null;
233
+ }
234
+ set onState(state) {
235
+ if (!this.appearanceStates.includes(state)) {
236
+ const currentOnState = this.onState;
237
+ if (currentOnState) {
238
+ this.appearanceStreamDict
239
+ ?.get('N')
240
+ ?.as(PdfDictionary)
241
+ ?.move(currentOnState, state);
242
+ }
243
+ else {
244
+ // No existing on-state; generate a new appearance stream for the new state
245
+ this.generateAppearance({ onStateName: state });
246
+ }
247
+ }
248
+ }
205
249
  get value() {
206
250
  const v = this.content.get('V') ?? this.parent?.content.get('V');
207
251
  if (v instanceof PdfString) {
@@ -221,48 +265,51 @@ export class PdfFormField extends PdfWidgetAnnotation {
221
265
  }
222
266
  return '';
223
267
  }
224
- set value(val) {
225
- if (this.value === val)
226
- return;
227
- const fieldParent = this.parent?.content.get('FT')
228
- ? this.parent
229
- : undefined;
230
- const generateAppearance = this._storeValue(val, fieldParent);
231
- if (generateAppearance && this.defaultGenerateAppearance) {
232
- this.generateAppearance();
233
- for (const sibling of this.siblings) {
234
- if (sibling !== this &&
235
- sibling.rect &&
236
- sibling.defaultGenerateAppearance) {
237
- sibling.generateAppearance();
238
- }
268
+ setRawValue(val) {
269
+ const targets = [this];
270
+ const parent = this.parent;
271
+ if (parent?.fieldType) {
272
+ targets.push(parent);
273
+ }
274
+ const pdfVal = val instanceof PdfString ? val : new PdfString(val);
275
+ const isEmpty = pdfVal.length === 0;
276
+ for (const target of targets) {
277
+ if (isEmpty) {
278
+ target.content.delete('V');
279
+ target.appearanceState = null;
239
280
  }
240
- // Separated field/widget structure: field has no Rect but its Kids
241
- // are widget annotations that do. Generate appearances for them.
242
- if (!this.rect) {
243
- for (const child of this.children) {
244
- if (child.rect && child.defaultGenerateAppearance) {
245
- if (this._form)
246
- child.form = this._form;
247
- child.generateAppearance();
248
- }
249
- }
281
+ else {
282
+ target.content.set('V', pdfVal);
250
283
  }
251
284
  }
252
- if (this._form) {
253
- this._form.xfa?.datasets?.updateField(this.name, this.value);
285
+ if (isEmpty) {
286
+ this._form?.xfa?.datasets?.updateField(this.name, '');
287
+ }
288
+ if (this.defaultGenerateAppearance) {
289
+ this.generateAppearance();
290
+ }
291
+ for (const sibling of this.siblings) {
292
+ if (sibling !== this && sibling.defaultGenerateAppearance) {
293
+ sibling.generateAppearance();
294
+ }
254
295
  }
296
+ // Separated field/widget structure: field has no Rect but its Kids
297
+ // are widget annotations that do. Clear stale V entries on children
298
+ // so they inherit the parent's value, then generate appearances.
299
+ for (const child of this.children) {
300
+ if (child.content.has('V')) {
301
+ child.content.delete('V');
302
+ }
303
+ if (child.defaultGenerateAppearance) {
304
+ if (this._form)
305
+ child.form = this._form;
306
+ child.generateAppearance();
307
+ }
308
+ }
309
+ this._form?.xfa?.datasets?.updateField(this.name, this.value);
255
310
  }
256
- /**
257
- * Writes the value to the dictionary. Returns true if appearance generation
258
- * should proceed, false to skip it (e.g. when value was cleared).
259
- * Override in subclasses to change the stored representation.
260
- */
261
- _storeValue(val, fieldParent) {
262
- const pdfVal = val instanceof PdfString ? val : new PdfString(val);
263
- this.content.set('V', pdfVal);
264
- fieldParent?.content.set('V', pdfVal);
265
- return true;
311
+ set value(val) {
312
+ this.setRawValue(val);
266
313
  }
267
314
  get fontSize() {
268
315
  const da = this.defaultAppearance || '';
@@ -499,7 +546,7 @@ export class PdfFormField extends PdfWidgetAnnotation {
499
546
  engine: this._form?.jsEngine,
500
547
  });
501
548
  }
502
- setAppearanceStream(stream) {
549
+ set appearanceStream(stream) {
503
550
  this.appearanceStreamDict ||= new PdfDictionary();
504
551
  if (stream instanceof PdfIndirectObject) {
505
552
  this.appearanceStreamDict.set('N', stream.reference);
@@ -512,6 +559,35 @@ export class PdfFormField extends PdfWidgetAnnotation {
512
559
  this.appearanceStreamDict.set('N', dict);
513
560
  }
514
561
  }
562
+ set downAppearanceStream(stream) {
563
+ this.appearanceStreamDict ||= new PdfDictionary();
564
+ if (stream instanceof PdfIndirectObject) {
565
+ this.appearanceStreamDict.set('D', stream.reference);
566
+ }
567
+ else {
568
+ const dict = new PdfDictionary();
569
+ for (const key in stream) {
570
+ dict.set(key, stream[key].reference);
571
+ }
572
+ this.appearanceStreamDict.set('D', dict);
573
+ }
574
+ }
575
+ get appearanceState() {
576
+ return this.content.get('AS')?.as(PdfName)?.value ?? null;
577
+ }
578
+ set appearanceState(state) {
579
+ if (state === null) {
580
+ this.content.delete('AS');
581
+ return;
582
+ }
583
+ else {
584
+ this.content.set('AS', new PdfName(state));
585
+ }
586
+ if (this.defaultGenerateAppearance &&
587
+ !this.hasAppearanceStream(state)) {
588
+ this.generateAppearance();
589
+ }
590
+ }
515
591
  /**
516
592
  * Returns the list of appearance state names from the normal appearance
517
593
  * dictionary (e.g. ["Yes", "Off"] for a checkbox).
@@ -519,7 +595,7 @@ export class PdfFormField extends PdfWidgetAnnotation {
519
595
  get appearanceStates() {
520
596
  const n = this.appearanceStreamDict?.get('N');
521
597
  if (n instanceof PdfDictionary) {
522
- return Array.from(n.entries(), ([key]) => key);
598
+ return n.keys().map((k) => k.value);
523
599
  }
524
600
  return [];
525
601
  }
@@ -534,9 +610,7 @@ export class PdfFormField extends PdfWidgetAnnotation {
534
610
  }
535
611
  }
536
612
  else if (n instanceof PdfDictionary) {
537
- const key = setting ??
538
- this.content.get('AS')?.as(PdfName)?.value ??
539
- undefined;
613
+ const key = setting ?? this.appearanceState ?? undefined;
540
614
  if (key) {
541
615
  const entry = n.get(key);
542
616
  if (entry instanceof PdfObjectReference) {
@@ -549,6 +623,9 @@ export class PdfFormField extends PdfWidgetAnnotation {
549
623
  }
550
624
  return null;
551
625
  }
626
+ hasAppearanceStream(setting) {
627
+ return this.appearanceStates.includes(setting);
628
+ }
552
629
  static _fallbackCtor;
553
630
  static _registry = new Map();
554
631
  static registerFieldType(ft, ctor, options) {
@@ -24,7 +24,7 @@ export class PdfTextFormField extends PdfFormField {
24
24
  const fontResources = this.buildFontResources(parsed.fontName);
25
25
  const isUnicode = font?.isUnicode ?? false;
26
26
  const reverseEncodingMap = font?.reverseEncodingMap;
27
- this.setAppearanceStream(new PdfTextAppearanceStream({
27
+ this.appearanceStream = new PdfTextAppearanceStream({
28
28
  rect: rect,
29
29
  value: this.value,
30
30
  da: parsed,
@@ -34,7 +34,7 @@ export class PdfTextFormField extends PdfFormField {
34
34
  fontResources,
35
35
  isUnicode,
36
36
  reverseEncodingMap,
37
- }));
37
+ });
38
38
  if (options?.makeReadOnly) {
39
39
  this.readOnly = true;
40
40
  if (!this.print)
@@ -36,6 +36,7 @@ export declare class PdfAcroForm<T extends Record<string, string> = Record<strin
36
36
  set defaultResources(resources: PdfDefaultResourcesDictionary | null);
37
37
  get fields(): ReadonlyArray<PdfFormField>;
38
38
  addField(...fields: PdfFormField[]): void;
39
+ private _addWidgetToPage;
39
40
  set fields(newFields: PdfFormField[]);
40
41
  setValues(values: Partial<T>): void;
41
42
  importData(fields: T): void;
@@ -108,13 +108,24 @@ export class PdfAcroForm extends PdfIndirectObject {
108
108
  }
109
109
  for (const field of fields) {
110
110
  fieldsArray.items.push(field.reference);
111
- // Auto-add to the page's Annots array
112
- const page = field.page;
113
- if (page) {
114
- page.annotations.items.push(field.reference);
111
+ this._addWidgetToPage(field);
112
+ // Also register any child widgets (e.g. radio button group kids)
113
+ for (const child of field.children) {
114
+ this._addWidgetToPage(child);
115
115
  }
116
116
  }
117
117
  }
118
+ _addWidgetToPage(field) {
119
+ const page = field.page;
120
+ if (!page)
121
+ return;
122
+ const ref = field.reference;
123
+ const key = ref.key;
124
+ const alreadyPresent = page.annotations.items.some((r) => r instanceof PdfObjectReference && r.key === key);
125
+ if (!alreadyPresent) {
126
+ page.annotations.items.push(ref);
127
+ }
128
+ }
118
129
  set fields(newFields) {
119
130
  this.content.set('Fields', PdfArray.refs(newFields));
120
131
  }