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 +289 -0
- package/README.md +148 -8
- package/dist/acroform/appearance/pdf-graphics.js +4 -3
- package/dist/acroform/appearance/pdf-text-appearance-stream.js +37 -22
- package/dist/acroform/fields/pdf-button-form-field.js +23 -7
- package/dist/acroform/fields/pdf-default-appearance.js +1 -4
- package/dist/acroform/fields/pdf-form-field.d.ts +10 -0
- package/dist/acroform/fields/pdf-form-field.js +43 -0
- package/dist/acroform/index.d.ts +1 -0
- package/dist/acroform/index.js +1 -0
- package/dist/acroform/js/index.d.ts +5 -0
- package/dist/acroform/js/index.js +5 -0
- package/dist/acroform/js/pdf-field-actions.d.ts +17 -0
- package/dist/acroform/js/pdf-field-actions.js +52 -0
- package/dist/acroform/js/pdf-javascript-action.d.ts +17 -0
- package/dist/acroform/js/pdf-javascript-action.js +38 -0
- package/dist/acroform/js/pdf-js-builtins.d.ts +12 -0
- package/dist/acroform/js/pdf-js-builtins.js +454 -0
- package/dist/acroform/js/pdf-js-engine-impl.d.ts +18 -0
- package/dist/acroform/js/pdf-js-engine-impl.js +22 -0
- package/dist/acroform/js/pdf-js-engine.d.ts +10 -0
- package/dist/acroform/js/pdf-js-engine.js +1 -0
- package/dist/acroform/pdf-acro-form.d.ts +4 -0
- package/dist/acroform/pdf-acro-form.js +58 -2
- package/dist/annotations/pdf-annotation.d.ts +20 -0
- package/dist/core/objects/pdf-dictionary.d.ts +2 -0
- package/dist/core/objects/pdf-dictionary.js +14 -0
- package/package.json +1 -1
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
|
|
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
|
|
66
|
-
|
|
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(
|
|
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
|
-
###
|
|
287
|
+
### AcroForms
|
|
288
|
+
|
|
289
|
+
Supports reading, filling, and creating AcroForm fields within PDF documents.
|
|
191
290
|
|
|
192
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
26
|
-
g.setDefaultAppearance(ctx.da);
|
|
27
|
-
|
|
28
|
-
let
|
|
29
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/);
|