pdf-lite 1.6.1 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -92,6 +92,89 @@ await document.encrypt()
92
92
  console.log(document.toString())
93
93
  ```
94
94
 
95
+ ### Filling AcroForms
96
+
97
+ ```typescript
98
+ import { PdfDocument } from 'pdf-lite/pdf/pdf-document'
99
+ import { readFile, writeFile } from 'fs/promises'
100
+
101
+ const pdfBytes = await readFile('form.pdf')
102
+ const doc = await PdfDocument.fromBytes([pdfBytes])
103
+
104
+ const form = doc.acroform
105
+ if (!form) throw new Error('No AcroForm found')
106
+
107
+ // Set multiple field values at once
108
+ form.setValues({
109
+ name: 'John Doe',
110
+ email: 'john@example.com',
111
+ subscribe: 'Yes', // checkbox: 'Yes' or 'Off'
112
+ })
113
+
114
+ // Or work with individual fields
115
+ const field = form.fields.find((f) => f.name === 'name')
116
+ field.value = 'Jane Doe'
117
+
118
+ // Export all current values
119
+ const values = form.exportData()
120
+ // => { name: 'Jane Doe', email: 'john@example.com', subscribe: 'Yes' }
121
+
122
+ await writeFile('filled.pdf', doc.toBytes())
123
+ ```
124
+
125
+ ### Generating Appearances
126
+
127
+ Appearance streams control how form fields render visually. The library can automatically generate them when field values are set, or you can generate them manually.
128
+
129
+ ```typescript
130
+ // Auto-generate appearances when setting values (default behavior)
131
+ field.value = 'Hello' // appearance is generated automatically
132
+
133
+ // Or generate manually with options
134
+ field.generateAppearance({ makeReadOnly: true })
135
+
136
+ // Configure font and size for a field
137
+ field.fontSize = 14
138
+ field.fontName = 'Helv'
139
+
140
+ // For checkbox fields
141
+ const checkbox = form.fields.find((f) => f.name === 'agree')
142
+ checkbox.checked = true
143
+ checkbox.generateAppearance()
144
+
145
+ // For choice fields (dropdowns/listboxes)
146
+ const dropdown = form.fields.find((f) => f.name === 'country')
147
+ dropdown.value = 'US'
148
+ dropdown.generateAppearance()
149
+ ```
150
+
151
+ ### Working with Fonts
152
+
153
+ ```typescript
154
+ import { PdfFont } from 'pdf-lite'
155
+
156
+ // Standard PDF fonts (built into all PDF readers)
157
+ const helvetica = PdfFont.fromStandardFont('Helvetica')
158
+ const timesBold = PdfFont.fromStandardFont('Times-Bold')
159
+ const courier = PdfFont.fromStandardFont('Courier')
160
+
161
+ // Embed custom fonts from file bytes (auto-detects TTF, OTF, WOFF)
162
+ import { readFileSync } from 'fs'
163
+ const fontData = readFileSync('MyFont.ttf')
164
+ const customFont = PdfFont.fromBytes(fontData)
165
+
166
+ // Use pre-defined font constants
167
+ const font = PdfFont.HELVETICA_BOLD
168
+
169
+ // Assign resource names for use in content streams
170
+ customFont.resourceName = 'F1'
171
+
172
+ // Add to document
173
+ document.add(customFont)
174
+ ```
175
+
176
+ **Standard font names:** Helvetica, Helvetica-Bold, Helvetica-Oblique, Helvetica-BoldOblique, Times-Roman, Times-Bold, Times-Italic, Times-BoldItalic, Courier, Courier-Bold, Courier-Oblique, Courier-BoldOblique, Symbol, ZapfDingbats
177
+
95
178
  ### Signing PDFs
96
179
 
97
180
  ```typescript
@@ -187,14 +270,47 @@ Long-Term Validation (LTV) support ensures that digital signatures remain valid
187
270
  - [x] Timestamping
188
271
  - [x] Verification of existing signatures
189
272
 
190
- ### AcroForm filling
273
+ ### AcroForms
274
+
275
+ Supports reading, filling, and creating AcroForm fields within PDF documents.
191
276
 
192
- Supports filling out AcroForm forms within PDF documents, allowing for dynamic content generation and user interaction.
277
+ **Field types:**
193
278
 
194
- - [x] Text fields
279
+ - [x] Text fields (single-line, multi-line, comb, password)
195
280
  - [x] Checkboxes
196
281
  - [x] Radio buttons
197
- - [x] Dropdowns
282
+ - [x] Dropdowns (combo boxes)
283
+ - [x] List boxes
284
+ - [x] Signature fields
285
+
286
+ **Form operations:**
287
+
288
+ - [x] Import/export field values (`importData`, `exportData`, `setValues`)
289
+ - [x] Read individual field properties (name, value, type, flags)
290
+ - [x] Hierarchical field support (parent/child/sibling fields)
291
+
292
+ ### Appearance Streams
293
+
294
+ Automatic generation of visual appearance streams for form fields, so filled forms render correctly in all PDF viewers without relying on `NeedAppearances`.
295
+
296
+ - [x] Text field rendering (word wrap, auto-sizing, comb layout)
297
+ - [x] Checkbox/radio button rendering
298
+ - [x] Dropdown/list box rendering
299
+ - [x] Font resolution from field, form, or document resources
300
+ - [x] Auto-size font (fontSize=0) for single-line text fields
301
+ - [x] Custom font color via Default Appearance strings
302
+
303
+ ### Fonts
304
+
305
+ Supports standard PDF fonts and embedding custom fonts.
306
+
307
+ - [x] All 14 standard PDF fonts (Helvetica, Times, Courier, Symbol, ZapfDingbats + variants)
308
+ - [x] TrueType font embedding (TTF)
309
+ - [x] OpenType font embedding (OTF, non-CFF)
310
+ - [x] WOFF font embedding
311
+ - [x] Auto-detect font format via `PdfFont.fromBytes()`
312
+ - [x] Font encoding maps (WinAnsi, Unicode/Identity-H)
313
+ - [x] Character width metrics
198
314
 
199
315
  ### XFA Forms
200
316
 
@@ -192,13 +192,14 @@ export class PdfGraphics {
192
192
  const startSize = this.currentFont.size;
193
193
  const minSize = 0.5;
194
194
  const fits = (size) => {
195
- if (this.measureTextWidth(text, size) > maxWidth)
196
- return false;
197
195
  if (maxHeight !== undefined) {
196
+ // Wrapping mode: text is allowed to span multiple lines,
197
+ // so only check that the wrapped result fits the box.
198
198
  const lines = this.wrapTextToLines(text, maxWidth, size);
199
199
  return lines.length * size * lineHeight <= maxHeight;
200
200
  }
201
- return true;
201
+ // Single-line mode: text must fit within maxWidth.
202
+ return this.measureTextWidth(text, size) <= maxWidth;
202
203
  };
203
204
  if (fits(startSize))
204
205
  return startSize;
@@ -1,6 +1,7 @@
1
1
  import { PdfDefaultAppearance } from '../fields/pdf-default-appearance.js';
2
2
  import { PdfAppearanceStream } from './pdf-appearance-stream.js';
3
3
  import { PdfGraphics } from './pdf-graphics.js';
4
+ const DEFAULT_FONT_SIZE = 12;
4
5
  /**
5
6
  * Appearance stream for text fields (single-line, multiline, comb).
6
7
  * Enhanced with word wrapping and automatic font scaling.
@@ -16,29 +17,44 @@ export class PdfTextAppearanceStream extends PdfAppearanceStream {
16
17
  const padding = 2;
17
18
  const availableWidth = width - 2 * padding;
18
19
  const availableHeight = height - 2 * padding;
20
+ const autoSize = ctx.da.fontSize <= 0;
19
21
  // Create graphics with font context for text measurement
20
22
  const g = new PdfGraphics({
21
23
  resolvedFonts: ctx.resolvedFonts,
22
24
  });
23
25
  g.beginMarkedContent();
24
26
  g.save();
25
- // Set initial font to enable measurement
26
- g.setDefaultAppearance(ctx.da);
27
- let finalFontSize = ctx.da.fontSize;
28
- let lines = [];
29
- if (ctx.multiline) {
27
+ // Bootstrap with a reference size so measureTextWidth works
28
+ g.setDefaultAppearance(new PdfDefaultAppearance(ctx.da.fontName, DEFAULT_FONT_SIZE, ctx.da.colorOp));
29
+ // ── Determine font size ──────────────────────────────────────
30
+ let finalFontSize;
31
+ if (autoSize) {
32
+ // Acrobat auto-size: default to 12pt with wrapping, then
33
+ // shrink only if the wrapped text still doesn't fit.
34
+ finalFontSize = DEFAULT_FONT_SIZE;
30
35
  const testLines = g.wrapTextToLines(value, availableWidth);
31
- const lineHeight = finalFontSize * 1.2;
32
- if (testLines.length * lineHeight > availableHeight) {
33
- // Scale font down to fit
36
+ if (testLines.length * DEFAULT_FONT_SIZE * 1.2 > availableHeight) {
34
37
  finalFontSize = g.calculateFittingFontSize(value, availableWidth, availableHeight, 1.2);
35
- const adjustedDA = new PdfDefaultAppearance(ctx.da.fontName, finalFontSize, ctx.da.colorOp);
36
- g.setDefaultAppearance(adjustedDA);
37
- lines = g.wrapTextToLines(value, availableWidth);
38
38
  }
39
- else {
40
- lines = testLines;
39
+ finalFontSize = Math.max(finalFontSize, 0.5);
40
+ }
41
+ else {
42
+ finalFontSize = ctx.da.fontSize;
43
+ }
44
+ // ── Render ───────────────────────────────────────────────────
45
+ const finalDA = new PdfDefaultAppearance(ctx.da.fontName, finalFontSize, ctx.da.colorOp);
46
+ g.setDefaultAppearance(finalDA);
47
+ 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
+ }
41
56
  }
57
+ lines = g.wrapTextToLines(value, availableWidth);
42
58
  const renderLineHeight = finalFontSize * 1.2;
43
59
  const startY = height - padding - finalFontSize;
44
60
  g.beginText();
@@ -53,7 +69,6 @@ export class PdfTextAppearanceStream extends PdfAppearanceStream {
53
69
  else if (ctx.comb && ctx.maxLen) {
54
70
  const cellWidth = width / ctx.maxLen;
55
71
  const chars = [...value];
56
- // Calculate font size to fit the widest character in its cell
57
72
  let maxCharWidth = 0;
58
73
  let widestChar = chars[0] ?? '';
59
74
  for (const char of chars) {
@@ -65,8 +80,7 @@ export class PdfTextAppearanceStream extends PdfAppearanceStream {
65
80
  }
66
81
  if (maxCharWidth > cellWidth) {
67
82
  finalFontSize = g.calculateFittingFontSize(widestChar, cellWidth);
68
- const adjustedDA = new PdfDefaultAppearance(ctx.da.fontName, finalFontSize, ctx.da.colorOp);
69
- g.setDefaultAppearance(adjustedDA);
83
+ g.setDefaultAppearance(new PdfDefaultAppearance(ctx.da.fontName, finalFontSize, ctx.da.colorOp));
70
84
  }
71
85
  const textY = (height - finalFontSize) / 2 + finalFontSize * 0.2;
72
86
  g.beginText();
@@ -79,12 +93,13 @@ export class PdfTextAppearanceStream extends PdfAppearanceStream {
79
93
  g.endText();
80
94
  }
81
95
  else {
82
- // Single line text
83
- const textWidth = g.measureTextWidth(value);
84
- if (textWidth > availableWidth) {
85
- finalFontSize = g.calculateFittingFontSize(value, availableWidth);
86
- const adjustedDA = new PdfDefaultAppearance(ctx.da.fontName, finalFontSize, ctx.da.colorOp);
87
- g.setDefaultAppearance(adjustedDA);
96
+ // Single line — for non-auto-size, shrink if text overflows
97
+ if (!autoSize) {
98
+ const textWidth = g.measureTextWidth(value);
99
+ if (textWidth > availableWidth) {
100
+ finalFontSize = g.calculateFittingFontSize(value, availableWidth);
101
+ g.setDefaultAppearance(new PdfDefaultAppearance(ctx.da.fontName, finalFontSize, ctx.da.colorOp));
102
+ }
88
103
  }
89
104
  const textY = (height - finalFontSize) / 2 + finalFontSize * 0.2;
90
105
  g.beginText();
@@ -20,20 +20,35 @@ export class PdfButtonFormField extends PdfFormField {
20
20
  this.content.delete('AS');
21
21
  return false;
22
22
  }
23
- this.content.set('V', new PdfName(strVal));
24
- fieldParent?.content.set('V', new PdfName(strVal));
25
- this.content.set('AS', new PdfName(strVal));
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;
30
+ }
31
+ else if (strVal === 'Off' || strVal === 'No') {
32
+ resolved = 'Off';
33
+ }
34
+ else {
35
+ resolved = states.find((s) => s !== 'Off') ?? 'Yes';
36
+ }
37
+ this.content.set('V', new PdfName(resolved));
38
+ fieldParent?.content.set('V', new PdfName(resolved));
39
+ this.content.set('AS', new PdfName(resolved));
26
40
  return true;
27
41
  }
28
42
  get checked() {
29
43
  const v = this.content.get('V') ?? this.parent?.content.get('V');
30
- return v instanceof PdfName && v.value === 'Yes';
44
+ return v instanceof PdfName && v.value !== 'Off';
31
45
  }
32
46
  set checked(isChecked) {
33
47
  const target = this.parent ?? this;
34
48
  if (isChecked) {
35
- target.content.set('V', new PdfName('Yes'));
36
- this.content.set('AS', new PdfName('Yes'));
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));
37
52
  }
38
53
  else {
39
54
  target.content.set('V', new PdfName('Off'));
@@ -56,8 +71,9 @@ export class PdfButtonFormField extends PdfFormField {
56
71
  height,
57
72
  contentStream: '',
58
73
  });
74
+ const onState = this.appearanceStates.find((s) => s !== 'Off') ?? 'Yes';
59
75
  this.setAppearanceStream({
60
- Yes: yesAppearance,
76
+ [onState]: yesAppearance,
61
77
  Off: noAppearance,
62
78
  });
63
79
  if (options?.makeReadOnly) {
@@ -50,10 +50,7 @@ export class PdfDefaultAppearance extends PdfString {
50
50
  if (!fontMatch)
51
51
  return null;
52
52
  const fontName = fontMatch[1];
53
- let fontSize = parseFloat(fontMatch[2]);
54
- if (!fontSize || fontSize <= 0) {
55
- fontSize = 12;
56
- }
53
+ const fontSize = parseFloat(fontMatch[2]) || 0;
57
54
  let colorOp = '0 g';
58
55
  const rgMatch = da.match(/([\d.]+\s+[\d.]+\s+[\d.]+)\s+rg/);
59
56
  const gMatch = da.match(/([\d.]+)\s+g/);
@@ -107,6 +107,11 @@ export declare abstract class PdfFormField extends PdfWidgetAnnotation {
107
107
  setAppearanceStream(stream: PdfIndirectObject | {
108
108
  [key: string]: PdfIndirectObject;
109
109
  }): void;
110
+ /**
111
+ * Returns the list of appearance state names from the normal appearance
112
+ * dictionary (e.g. ["Yes", "Off"] for a checkbox).
113
+ */
114
+ get appearanceStates(): string[];
110
115
  getAppearanceStream(setting?: string): PdfIndirectObject<PdfStream> | null;
111
116
  private static _fallbackCtor?;
112
117
  private static _registry;
@@ -480,6 +480,17 @@ export class PdfFormField extends PdfWidgetAnnotation {
480
480
  this.appearanceStreamDict.set('N', dict);
481
481
  }
482
482
  }
483
+ /**
484
+ * Returns the list of appearance state names from the normal appearance
485
+ * dictionary (e.g. ["Yes", "Off"] for a checkbox).
486
+ */
487
+ get appearanceStates() {
488
+ const n = this.appearanceStreamDict?.get('N');
489
+ if (n instanceof PdfDictionary) {
490
+ return Array.from(n.entries(), ([key]) => key);
491
+ }
492
+ return [];
493
+ }
483
494
  getAppearanceStream(setting) {
484
495
  const n = this.appearanceStreamDict?.get('N');
485
496
  if (!n)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pdf-lite",
3
- "version": "1.6.1",
3
+ "version": "1.6.2",
4
4
  "main": "dist/index.js",
5
5
  "type": "module",
6
6
  "exports": {