pdf-lite 1.6.1 → 1.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/EXAMPLES.md CHANGED
@@ -1989,3 +1989,292 @@ async function main() {
1989
1989
 
1990
1990
  main().catch(console.error)
1991
1991
  ```
1992
+
1993
+ ## JavaScript Actions example - Executing JS actions in PDF form fields
1994
+
1995
+ ```typescript
1996
+ //
1997
+ // PDF forms generated by Adobe Acrobat and LiveCycle often include JavaScript
1998
+ // actions for formatting, validation, and field calculations. The
1999
+ // PdfJavaScriptEngine executes these automatically when setting field values,
2000
+ // with built-in support for common Acrobat JS functions.
2001
+
2002
+ import { PdfArray } from 'pdf-lite/core/objects/pdf-array'
2003
+ import { PdfBoolean } from 'pdf-lite/core/objects/pdf-boolean'
2004
+ import { PdfDictionary } from 'pdf-lite/core/objects/pdf-dictionary'
2005
+ import { PdfIndirectObject } from 'pdf-lite/core/objects/pdf-indirect-object'
2006
+ import { PdfName } from 'pdf-lite/core/objects/pdf-name'
2007
+ import { PdfNumber } from 'pdf-lite/core/objects/pdf-number'
2008
+ import { PdfObjectReference } from 'pdf-lite/core/objects/pdf-object-reference'
2009
+ import { PdfStream } from 'pdf-lite/core/objects/pdf-stream'
2010
+ import { PdfString } from 'pdf-lite/core/objects/pdf-string'
2011
+ import { PdfDocument } from 'pdf-lite/pdf/pdf-document'
2012
+ import { PdfJavaScriptEngine } from 'pdf-lite/acroform/js/pdf-js-engine-impl'
2013
+ import fs from 'fs/promises'
2014
+
2015
+ const tmpFolder = `${import.meta.dirname}/tmp`
2016
+ await fs.mkdir(tmpFolder, { recursive: true })
2017
+
2018
+ // ============================================
2019
+ // Helpers
2020
+ // ============================================
2021
+
2022
+ function createPage(
2023
+ contentStreamRef: PdfObjectReference,
2024
+ ): PdfIndirectObject<PdfDictionary> {
2025
+ const d = new PdfDictionary()
2026
+ d.set('Type', new PdfName('Page'))
2027
+ d.set(
2028
+ 'MediaBox',
2029
+ new PdfArray([
2030
+ new PdfNumber(0),
2031
+ new PdfNumber(0),
2032
+ new PdfNumber(612),
2033
+ new PdfNumber(792),
2034
+ ]),
2035
+ )
2036
+ d.set('Contents', contentStreamRef)
2037
+ return new PdfIndirectObject({ content: d })
2038
+ }
2039
+
2040
+ function createPages(
2041
+ pages: PdfIndirectObject<PdfDictionary>[],
2042
+ ): PdfIndirectObject<PdfDictionary> {
2043
+ const d = new PdfDictionary()
2044
+ d.set('Type', new PdfName('Pages'))
2045
+ d.set('Kids', new PdfArray(pages.map((x) => x.reference)))
2046
+ d.set('Count', new PdfNumber(pages.length))
2047
+ return new PdfIndirectObject({ content: d })
2048
+ }
2049
+
2050
+ function createCatalog(
2051
+ pagesRef: PdfObjectReference,
2052
+ ): PdfIndirectObject<PdfDictionary> {
2053
+ const d = new PdfDictionary()
2054
+ d.set('Type', new PdfName('Catalog'))
2055
+ d.set('Pages', pagesRef)
2056
+ return new PdfIndirectObject({ content: d })
2057
+ }
2058
+
2059
+ /** Create a text field with an optional JavaScript format action. */
2060
+ function createTextField(
2061
+ fieldName: string,
2062
+ pageRef: PdfObjectReference,
2063
+ rect: [number, number, number, number],
2064
+ jsFormatCode?: string,
2065
+ jsCalcCode?: string,
2066
+ ): PdfIndirectObject<PdfDictionary> {
2067
+ const d = new PdfDictionary()
2068
+ d.set('Type', new PdfName('Annot'))
2069
+ d.set('Subtype', new PdfName('Widget'))
2070
+ d.set('FT', new PdfName('Tx'))
2071
+ d.set('T', new PdfString(fieldName))
2072
+ d.set(
2073
+ 'Rect',
2074
+ new PdfArray([
2075
+ new PdfNumber(rect[0]),
2076
+ new PdfNumber(rect[1]),
2077
+ new PdfNumber(rect[2]),
2078
+ new PdfNumber(rect[3]),
2079
+ ]),
2080
+ )
2081
+ d.set('F', new PdfNumber(4))
2082
+ d.set('P', pageRef)
2083
+ d.set('DA', new PdfString('/Helv 12 Tf 0 g'))
2084
+
2085
+ // Add /AA (additional actions) dictionary with JS format/calculate actions
2086
+ if (jsFormatCode || jsCalcCode) {
2087
+ const aa = new PdfDictionary()
2088
+ if (jsFormatCode) {
2089
+ const fmtAction = new PdfDictionary()
2090
+ fmtAction.set('S', new PdfName('JavaScript'))
2091
+ fmtAction.set('JS', new PdfString(jsFormatCode))
2092
+ aa.set('F', fmtAction)
2093
+ }
2094
+ if (jsCalcCode) {
2095
+ const calcAction = new PdfDictionary()
2096
+ calcAction.set('S', new PdfName('JavaScript'))
2097
+ calcAction.set('JS', new PdfString(jsCalcCode))
2098
+ aa.set('C', calcAction)
2099
+ }
2100
+ d.set('AA', aa)
2101
+ }
2102
+
2103
+ return new PdfIndirectObject({ content: d })
2104
+ }
2105
+
2106
+ // ============================================
2107
+ // Build a PDF with JS-powered form fields
2108
+ // ============================================
2109
+
2110
+ const document = new PdfDocument()
2111
+
2112
+ // Font
2113
+ const font = new PdfDictionary()
2114
+ font.set('Type', new PdfName('Font'))
2115
+ font.set('Subtype', new PdfName('Type1'))
2116
+ font.set('BaseFont', new PdfName('Helvetica'))
2117
+ const fontObj = new PdfIndirectObject({ content: font })
2118
+ document.add(fontObj)
2119
+
2120
+ // Resources
2121
+ const resources = new PdfDictionary()
2122
+ const fontDict = new PdfDictionary()
2123
+ fontDict.set('F1', fontObj.reference)
2124
+ resources.set('Font', fontDict)
2125
+ const resourcesObj = new PdfIndirectObject({ content: resources })
2126
+ document.add(resourcesObj)
2127
+
2128
+ // Content stream with labels
2129
+ const contentStream = new PdfIndirectObject({
2130
+ content: new PdfStream({
2131
+ header: new PdfDictionary(),
2132
+ original: `BT
2133
+ /F1 18 Tf 72 720 Td (Invoice - JS Actions Demo) Tj
2134
+ /F1 12 Tf 0 -50 Td (Price:) Tj
2135
+ 0 -30 Td (Quantity:) Tj
2136
+ 0 -30 Td (Total:) Tj
2137
+ 0 -40 Td (Phone:) Tj
2138
+ ET `,
2139
+ }),
2140
+ })
2141
+ document.add(contentStream)
2142
+
2143
+ // Page
2144
+ const page = createPage(contentStream.reference)
2145
+ page.content.set('Resources', resourcesObj.reference)
2146
+ document.add(page)
2147
+
2148
+ // Form fields with JavaScript actions:
2149
+
2150
+ // Price field — formats as currency via AFNumber_Format
2151
+ const priceField = createTextField(
2152
+ 'Price',
2153
+ page.reference,
2154
+ [150, 655, 350, 675],
2155
+ 'AFNumber_Format(2, 0, 0, 0, "$", true)',
2156
+ )
2157
+ document.add(priceField)
2158
+
2159
+ // Quantity field — plain number format
2160
+ const qtyField = createTextField(
2161
+ 'Quantity',
2162
+ page.reference,
2163
+ [150, 625, 350, 645],
2164
+ 'AFNumber_Format(0, 0, 0, 0, "", false)',
2165
+ )
2166
+ document.add(qtyField)
2167
+
2168
+ // Total field — calculated as Price * Quantity via AFSimple_Calculate
2169
+ const totalField = createTextField(
2170
+ 'Total',
2171
+ page.reference,
2172
+ [150, 595, 350, 615],
2173
+ 'AFNumber_Format(2, 0, 0, 0, "$", true)',
2174
+ 'AFSimple_Calculate("PRD", ["Price", "Quantity"])',
2175
+ )
2176
+ document.add(totalField)
2177
+
2178
+ // Phone field — formatted via AFSpecial_Format (phone)
2179
+ const phoneField = createTextField(
2180
+ 'Phone',
2181
+ page.reference,
2182
+ [150, 555, 350, 575],
2183
+ 'AFSpecial_Format(2)',
2184
+ )
2185
+ document.add(phoneField)
2186
+
2187
+ // Annotations on page
2188
+ page.content.set(
2189
+ 'Annots',
2190
+ new PdfArray([
2191
+ priceField.reference,
2192
+ qtyField.reference,
2193
+ totalField.reference,
2194
+ phoneField.reference,
2195
+ ]),
2196
+ )
2197
+
2198
+ // Pages / catalog
2199
+ const pages = createPages([page])
2200
+ page.content.set('Parent', pages.reference)
2201
+ document.add(pages)
2202
+
2203
+ const catalog = createCatalog(pages.reference)
2204
+
2205
+ // AcroForm with fields and calculation order
2206
+ const acroForm = new PdfDictionary()
2207
+ acroForm.set(
2208
+ 'Fields',
2209
+ new PdfArray([
2210
+ priceField.reference,
2211
+ qtyField.reference,
2212
+ totalField.reference,
2213
+ phoneField.reference,
2214
+ ]),
2215
+ )
2216
+ // Calculation order — Total depends on Price and Quantity
2217
+ acroForm.set('CO', new PdfArray([totalField.reference]))
2218
+ acroForm.set('NeedAppearances', new PdfBoolean(true))
2219
+
2220
+ // Form default resources
2221
+ const formResources = new PdfDictionary()
2222
+ const formFontDict = new PdfDictionary()
2223
+ const helvetica = new PdfDictionary()
2224
+ helvetica.set('Type', new PdfName('Font'))
2225
+ helvetica.set('Subtype', new PdfName('Type1'))
2226
+ helvetica.set('BaseFont', new PdfName('Helvetica'))
2227
+ const helveticaObj = new PdfIndirectObject({ content: helvetica })
2228
+ document.add(helveticaObj)
2229
+ formFontDict.set('Helv', helveticaObj.reference)
2230
+ formResources.set('Font', formFontDict)
2231
+ acroForm.set('DR', formResources)
2232
+ acroForm.set('DA', new PdfString('/Helv 12 Tf 0 g'))
2233
+
2234
+ const acroFormObj = new PdfIndirectObject({ content: acroForm })
2235
+ document.add(acroFormObj)
2236
+ catalog.content.set('AcroForm', acroFormObj.reference)
2237
+ document.add(catalog)
2238
+ document.trailerDict.set('Root', catalog.reference)
2239
+
2240
+ await document.finalize()
2241
+ await fs.writeFile(`${tmpFolder}/js-actions-form.pdf`, document.toBytes())
2242
+ console.log('Created js-actions-form.pdf with JavaScript action fields')
2243
+
2244
+ // ============================================
2245
+ // Fill the form with the JS engine enabled
2246
+ // ============================================
2247
+
2248
+ const formBytes = await fs.readFile(`${tmpFolder}/js-actions-form.pdf`)
2249
+ const filledDoc = await PdfDocument.fromBytes([formBytes])
2250
+ const form = filledDoc.acroform!
2251
+
2252
+ // Attach the JS engine with a field value resolver for AFSimple_Calculate
2253
+ form.jsEngine = new PdfJavaScriptEngine({
2254
+ getFieldValue: (name) => {
2255
+ const field = form.fields.find((f) => f.name === name)
2256
+ return field?.value ?? ''
2257
+ },
2258
+ })
2259
+
2260
+ // Set values — JS format actions fire automatically
2261
+ form.setValues({
2262
+ Price: '49.99',
2263
+ Quantity: '3',
2264
+ Phone: '5551234567',
2265
+ })
2266
+
2267
+ // Print resulting values (after JS actions have run)
2268
+ console.log('\nField values after JS actions:')
2269
+ for (const field of form.fields) {
2270
+ console.log(` ${field.name}: "${field.value}"`)
2271
+ }
2272
+ // Expected output:
2273
+ // Price: "$49.99"
2274
+ // Quantity: "3"
2275
+ // Total: "$149.97" (calculated via AFSimple_Calculate PRD)
2276
+ // Phone: "(555) 123-4567" (formatted via AFSpecial_Format)
2277
+
2278
+ await fs.writeFile(`${tmpFolder}/js-actions-filled.pdf`, filledDoc.toBytes())
2279
+ console.log('\nCreated js-actions-filled.pdf with JS-formatted values')
2280
+ ```
package/README.md CHANGED
@@ -52,7 +52,7 @@ import { PdfArray } from 'pdf-lite/core/objects/pdf-array'
52
52
  import { PdfNumber } from 'pdf-lite/core/objects/pdf-number'
53
53
 
54
54
  // Create the document
55
- const document = new PdfDocument()
55
+ const doc = new PdfDocument()
56
56
 
57
57
  // Create content stream
58
58
  const contentStream = new PdfIndirectObject({
@@ -62,12 +62,12 @@ const contentStream = new PdfIndirectObject({
62
62
  }),
63
63
  })
64
64
 
65
- // Create and commit objects
66
- document.commit(contentStream)
65
+ // Create and add objects
66
+ doc.add(contentStream)
67
67
  // ... create pages, catalog, etc.
68
68
 
69
69
  // Output the PDF
70
- console.log(document.toString())
70
+ console.log(doc.toString())
71
71
  ```
72
72
 
73
73
  ### Working with Encryption
@@ -92,6 +92,103 @@ 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
+ if (field) 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
+ import { PdfButtonFormField } from 'pdf-lite/acroform/fields/pdf-button-form-field'
131
+ import { PdfChoiceFormField } from 'pdf-lite/acroform/fields/pdf-choice-form-field'
132
+ import { PdfAcroForm } from 'pdf-lite/acroform/pdf-acro-form'
133
+ import { PdfTextFormField } from 'pdf-lite/acroform/fields/pdf-text-form-field'
134
+
135
+ declare const form: PdfAcroForm
136
+ declare const field: PdfTextFormField
137
+
138
+ // Auto-generate appearances when setting values (default behavior)
139
+ field.value = 'Hello' // appearance is generated automatically
140
+
141
+ // Or generate manually with options
142
+ field.generateAppearance({ makeReadOnly: true })
143
+
144
+ // Configure font and size for a field
145
+ field.fontSize = 14
146
+ field.fontName = 'Helv'
147
+
148
+ // For checkbox fields
149
+ const checkbox = form.fields.find(
150
+ (f) => f.name === 'agree',
151
+ ) as PdfButtonFormField
152
+ checkbox.checked = true
153
+ checkbox.generateAppearance()
154
+
155
+ // For choice fields (dropdowns/listboxes)
156
+ const dropdown = form.fields.find(
157
+ (f) => f.name === 'country',
158
+ ) as PdfChoiceFormField
159
+ dropdown.value = 'US'
160
+ dropdown.generateAppearance()
161
+ ```
162
+
163
+ ### Working with Fonts
164
+
165
+ ```typescript
166
+ import { PdfFont } from 'pdf-lite'
167
+ import { PdfDocument } from 'pdf-lite/pdf/pdf-document'
168
+ import { readFileSync } from 'fs'
169
+
170
+ // Standard PDF fonts (built into all PDF readers)
171
+ const helvetica = PdfFont.fromStandardFont('Helvetica')
172
+ const timesBold = PdfFont.fromStandardFont('Times-Bold')
173
+ const courier = PdfFont.fromStandardFont('Courier')
174
+
175
+ // Embed custom fonts from file bytes (auto-detects TTF, OTF, WOFF)
176
+ const fontData = readFileSync('MyFont.ttf')
177
+ const customFont = PdfFont.fromBytes(fontData)
178
+
179
+ // Use pre-defined font constants
180
+ const font = PdfFont.HELVETICA_BOLD
181
+
182
+ // Assign resource names for use in content streams
183
+ customFont.resourceName = 'F1'
184
+
185
+ // Add to document
186
+ const doc = new PdfDocument()
187
+ doc.add(customFont)
188
+ ```
189
+
190
+ **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
191
+
95
192
  ### Signing PDFs
96
193
 
97
194
  ```typescript
@@ -187,14 +284,57 @@ Long-Term Validation (LTV) support ensures that digital signatures remain valid
187
284
  - [x] Timestamping
188
285
  - [x] Verification of existing signatures
189
286
 
190
- ### AcroForm filling
287
+ ### AcroForms
288
+
289
+ Supports reading, filling, and creating AcroForm fields within PDF documents.
191
290
 
192
- Supports filling out AcroForm forms within PDF documents, allowing for dynamic content generation and user interaction.
291
+ **Field types:**
193
292
 
194
- - [x] Text fields
293
+ - [x] Text fields (single-line, multi-line, comb, password)
195
294
  - [x] Checkboxes
196
295
  - [x] Radio buttons
197
- - [x] Dropdowns
296
+ - [x] Dropdowns (combo boxes)
297
+ - [x] List boxes
298
+ - [x] Signature fields
299
+
300
+ **Form operations:**
301
+
302
+ - [x] Import/export field values (`importData`, `exportData`, `setValues`)
303
+ - [x] Read individual field properties (name, value, type, flags)
304
+ - [x] Hierarchical field support (parent/child/sibling fields)
305
+
306
+ **JavaScript actions:**
307
+
308
+ - [x] JavaScript action execution via `PdfJavaScriptEngine`
309
+ - [x] Validate, keystroke, calculate, and format action triggers
310
+ - [x] Built-in Acrobat JS functions: `util.printd`, `util.scand`, `util.printf`
311
+ - [x] `AFNumber_Format` / `AFNumber_Keystroke` — number formatting and validation
312
+ - [x] `AFDate_FormatEx` / `AFDate_KeystrokeEx` — date formatting and validation
313
+ - [x] `AFSimple_Calculate` — SUM, AVG, PRD, MIN, MAX across fields
314
+ - [x] `AFSpecial_Format` / `AFSpecial_Keystroke` — zip, SSN, phone formatting
315
+
316
+ ### Appearance Streams
317
+
318
+ Automatic generation of visual appearance streams for form fields, so filled forms render correctly in all PDF viewers without relying on `NeedAppearances`.
319
+
320
+ - [x] Text field rendering (word wrap, auto-sizing, comb layout)
321
+ - [x] Checkbox/radio button rendering
322
+ - [x] Dropdown/list box rendering
323
+ - [x] Font resolution from field, form, or document resources
324
+ - [x] Auto-size font (fontSize=0) for single-line text fields
325
+ - [x] Custom font color via Default Appearance strings
326
+
327
+ ### Fonts
328
+
329
+ Supports standard PDF fonts and embedding custom fonts.
330
+
331
+ - [x] All 14 standard PDF fonts (Helvetica, Times, Courier, Symbol, ZapfDingbats + variants)
332
+ - [x] TrueType font embedding (TTF)
333
+ - [x] OpenType font embedding (OTF, non-CFF)
334
+ - [x] WOFF font embedding
335
+ - [x] Auto-detect font format via `PdfFont.fromBytes()`
336
+ - [x] Font encoding maps (WinAnsi, Unicode/Identity-H)
337
+ - [x] Character width metrics
198
338
 
199
339
  ### XFA Forms
200
340
 
@@ -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/);