pdf-lite 1.6.2 → 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 +33 -9
- package/dist/acroform/fields/pdf-form-field.d.ts +5 -0
- package/dist/acroform/fields/pdf-form-field.js +32 -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
|
|
@@ -113,7 +113,7 @@ form.setValues({
|
|
|
113
113
|
|
|
114
114
|
// Or work with individual fields
|
|
115
115
|
const field = form.fields.find((f) => f.name === 'name')
|
|
116
|
-
field.value = 'Jane Doe'
|
|
116
|
+
if (field) field.value = 'Jane Doe'
|
|
117
117
|
|
|
118
118
|
// Export all current values
|
|
119
119
|
const values = form.exportData()
|
|
@@ -127,6 +127,14 @@ await writeFile('filled.pdf', doc.toBytes())
|
|
|
127
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
128
|
|
|
129
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
|
+
|
|
130
138
|
// Auto-generate appearances when setting values (default behavior)
|
|
131
139
|
field.value = 'Hello' // appearance is generated automatically
|
|
132
140
|
|
|
@@ -138,12 +146,16 @@ field.fontSize = 14
|
|
|
138
146
|
field.fontName = 'Helv'
|
|
139
147
|
|
|
140
148
|
// For checkbox fields
|
|
141
|
-
const checkbox = form.fields.find(
|
|
149
|
+
const checkbox = form.fields.find(
|
|
150
|
+
(f) => f.name === 'agree',
|
|
151
|
+
) as PdfButtonFormField
|
|
142
152
|
checkbox.checked = true
|
|
143
153
|
checkbox.generateAppearance()
|
|
144
154
|
|
|
145
155
|
// For choice fields (dropdowns/listboxes)
|
|
146
|
-
const dropdown = form.fields.find(
|
|
156
|
+
const dropdown = form.fields.find(
|
|
157
|
+
(f) => f.name === 'country',
|
|
158
|
+
) as PdfChoiceFormField
|
|
147
159
|
dropdown.value = 'US'
|
|
148
160
|
dropdown.generateAppearance()
|
|
149
161
|
```
|
|
@@ -152,6 +164,8 @@ dropdown.generateAppearance()
|
|
|
152
164
|
|
|
153
165
|
```typescript
|
|
154
166
|
import { PdfFont } from 'pdf-lite'
|
|
167
|
+
import { PdfDocument } from 'pdf-lite/pdf/pdf-document'
|
|
168
|
+
import { readFileSync } from 'fs'
|
|
155
169
|
|
|
156
170
|
// Standard PDF fonts (built into all PDF readers)
|
|
157
171
|
const helvetica = PdfFont.fromStandardFont('Helvetica')
|
|
@@ -159,7 +173,6 @@ const timesBold = PdfFont.fromStandardFont('Times-Bold')
|
|
|
159
173
|
const courier = PdfFont.fromStandardFont('Courier')
|
|
160
174
|
|
|
161
175
|
// Embed custom fonts from file bytes (auto-detects TTF, OTF, WOFF)
|
|
162
|
-
import { readFileSync } from 'fs'
|
|
163
176
|
const fontData = readFileSync('MyFont.ttf')
|
|
164
177
|
const customFont = PdfFont.fromBytes(fontData)
|
|
165
178
|
|
|
@@ -170,7 +183,8 @@ const font = PdfFont.HELVETICA_BOLD
|
|
|
170
183
|
customFont.resourceName = 'F1'
|
|
171
184
|
|
|
172
185
|
// Add to document
|
|
173
|
-
|
|
186
|
+
const doc = new PdfDocument()
|
|
187
|
+
doc.add(customFont)
|
|
174
188
|
```
|
|
175
189
|
|
|
176
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
|
|
@@ -289,6 +303,16 @@ Supports reading, filling, and creating AcroForm fields within PDF documents.
|
|
|
289
303
|
- [x] Read individual field properties (name, value, type, flags)
|
|
290
304
|
- [x] Hierarchical field support (parent/child/sibling fields)
|
|
291
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
|
+
|
|
292
316
|
### Appearance Streams
|
|
293
317
|
|
|
294
318
|
Automatic generation of visual appearance streams for form fields, so filled forms render correctly in all PDF viewers without relying on `NeedAppearances`.
|
|
@@ -10,6 +10,8 @@ import type { PdfFieldType } from './types.js';
|
|
|
10
10
|
import { PdfFormFieldFlags } from './pdf-form-field-flags.js';
|
|
11
11
|
import { PdfDefaultResourcesDictionary } from '../../annotations/pdf-default-resources.js';
|
|
12
12
|
import type { PdfAcroForm } from '../pdf-acro-form.js';
|
|
13
|
+
import { PdfFieldActions } from '../js/pdf-field-actions.js';
|
|
14
|
+
import { PdfJavaScriptAction } from '../js/pdf-javascript-action.js';
|
|
13
15
|
/**
|
|
14
16
|
* Abstract base form field class. Extends PdfWidgetAnnotation with form-specific properties:
|
|
15
17
|
* FT, V, DA, Ff, T (name), field hierarchy (parent/children/siblings).
|
|
@@ -100,6 +102,9 @@ export declare abstract class PdfFormField extends PdfWidgetAnnotation {
|
|
|
100
102
|
set maxLen(maxLen: number | null);
|
|
101
103
|
get kids(): PdfArray<PdfObjectReference> | undefined;
|
|
102
104
|
set kids(kids: PdfObjectReference[]);
|
|
105
|
+
private _resolveActionDict;
|
|
106
|
+
get actions(): PdfFieldActions | null;
|
|
107
|
+
get activateAction(): PdfJavaScriptAction | null;
|
|
103
108
|
abstract generateAppearance(options?: {
|
|
104
109
|
makeReadOnly?: boolean;
|
|
105
110
|
textYOffset?: number;
|
|
@@ -12,6 +12,8 @@ import { PdfWidgetAnnotation } from '../../annotations/pdf-widget-annotation.js'
|
|
|
12
12
|
import { PdfDefaultAppearance } from './pdf-default-appearance.js';
|
|
13
13
|
import { PdfFieldType as PdfFieldTypeConst } from './types.js';
|
|
14
14
|
import { PdfFormFieldFlags } from './pdf-form-field-flags.js';
|
|
15
|
+
import { PdfFieldActions } from '../js/pdf-field-actions.js';
|
|
16
|
+
import { PdfJavaScriptAction } from '../js/pdf-javascript-action.js';
|
|
15
17
|
/**
|
|
16
18
|
* Abstract base form field class. Extends PdfWidgetAnnotation with form-specific properties:
|
|
17
19
|
* FT, V, DA, Ff, T (name), field hierarchy (parent/children/siblings).
|
|
@@ -467,6 +469,36 @@ export class PdfFormField extends PdfWidgetAnnotation {
|
|
|
467
469
|
const kidsArray = new PdfArray(kids);
|
|
468
470
|
this.content.set('Kids', kidsArray);
|
|
469
471
|
}
|
|
472
|
+
_resolveActionDict(key) {
|
|
473
|
+
const entry = this.content.get(key);
|
|
474
|
+
if (entry instanceof PdfObjectReference) {
|
|
475
|
+
const resolved = entry.resolve();
|
|
476
|
+
if (resolved?.content instanceof PdfDictionary) {
|
|
477
|
+
return resolved.content;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
else if (entry instanceof PdfDictionary) {
|
|
481
|
+
return entry;
|
|
482
|
+
}
|
|
483
|
+
return undefined;
|
|
484
|
+
}
|
|
485
|
+
get actions() {
|
|
486
|
+
const aaDict = this._resolveActionDict('AA');
|
|
487
|
+
if (!aaDict)
|
|
488
|
+
return null;
|
|
489
|
+
return aaDict.becomes(PdfFieldActions, {
|
|
490
|
+
activateDict: this._resolveActionDict('A'),
|
|
491
|
+
engine: this._form?.jsEngine,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
get activateAction() {
|
|
495
|
+
const aDict = this._resolveActionDict('A');
|
|
496
|
+
if (!aDict)
|
|
497
|
+
return null;
|
|
498
|
+
return aDict.becomes(PdfJavaScriptAction, {
|
|
499
|
+
engine: this._form?.jsEngine,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
470
502
|
setAppearanceStream(stream) {
|
|
471
503
|
this.appearanceStreamDict ||= new PdfDictionary();
|
|
472
504
|
if (stream instanceof PdfIndirectObject) {
|
package/dist/acroform/index.d.ts
CHANGED
package/dist/acroform/index.js
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { PdfDictionary } from '../../core/objects/pdf-dictionary.js';
|
|
2
|
+
import { PdfJavaScriptAction } from './pdf-javascript-action.js';
|
|
3
|
+
import type { PdfJsEngine } from './pdf-js-engine.js';
|
|
4
|
+
export declare class PdfFieldActions extends PdfDictionary {
|
|
5
|
+
private readonly activateDict?;
|
|
6
|
+
readonly engine?: PdfJsEngine;
|
|
7
|
+
constructor(dict: PdfDictionary, options?: {
|
|
8
|
+
activateDict?: PdfDictionary;
|
|
9
|
+
engine?: PdfJsEngine;
|
|
10
|
+
});
|
|
11
|
+
private _resolve;
|
|
12
|
+
get keystroke(): PdfJavaScriptAction | null;
|
|
13
|
+
get validate(): PdfJavaScriptAction | null;
|
|
14
|
+
get calculate(): PdfJavaScriptAction | null;
|
|
15
|
+
get format(): PdfJavaScriptAction | null;
|
|
16
|
+
get activate(): PdfJavaScriptAction | null;
|
|
17
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { PdfDictionary } from '../../core/objects/pdf-dictionary.js';
|
|
2
|
+
import { PdfObjectReference } from '../../core/objects/pdf-object-reference.js';
|
|
3
|
+
import { PdfJavaScriptAction } from './pdf-javascript-action.js';
|
|
4
|
+
export class PdfFieldActions extends PdfDictionary {
|
|
5
|
+
activateDict;
|
|
6
|
+
engine;
|
|
7
|
+
constructor(dict, options) {
|
|
8
|
+
super();
|
|
9
|
+
this.copyFrom(dict);
|
|
10
|
+
this.activateDict = options?.activateDict;
|
|
11
|
+
this.engine = options?.engine;
|
|
12
|
+
}
|
|
13
|
+
_resolve(key) {
|
|
14
|
+
const entry = this.get(key);
|
|
15
|
+
if (!entry)
|
|
16
|
+
return null;
|
|
17
|
+
let actionDict;
|
|
18
|
+
if (entry instanceof PdfObjectReference) {
|
|
19
|
+
const resolved = entry.resolve();
|
|
20
|
+
if (resolved?.content instanceof PdfDictionary) {
|
|
21
|
+
actionDict = resolved.content;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
else if (entry instanceof PdfDictionary) {
|
|
25
|
+
actionDict = entry;
|
|
26
|
+
}
|
|
27
|
+
if (!actionDict)
|
|
28
|
+
return null;
|
|
29
|
+
return actionDict.becomes(PdfJavaScriptAction, {
|
|
30
|
+
engine: this.engine,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
get keystroke() {
|
|
34
|
+
return this._resolve('K');
|
|
35
|
+
}
|
|
36
|
+
get validate() {
|
|
37
|
+
return this._resolve('V');
|
|
38
|
+
}
|
|
39
|
+
get calculate() {
|
|
40
|
+
return this._resolve('C');
|
|
41
|
+
}
|
|
42
|
+
get format() {
|
|
43
|
+
return this._resolve('F');
|
|
44
|
+
}
|
|
45
|
+
get activate() {
|
|
46
|
+
if (!this.activateDict)
|
|
47
|
+
return null;
|
|
48
|
+
return this.activateDict.becomes(PdfJavaScriptAction, {
|
|
49
|
+
engine: this.engine,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { PdfDictionary } from '../../core/objects/pdf-dictionary.js';
|
|
2
|
+
import { PdfString } from '../../core/objects/pdf-string.js';
|
|
3
|
+
import { PdfStream } from '../../core/objects/pdf-stream.js';
|
|
4
|
+
import { PdfObjectReference } from '../../core/objects/pdf-object-reference.js';
|
|
5
|
+
import { PdfName } from '../../core/objects/pdf-name.js';
|
|
6
|
+
import type { PdfJsEngine, PdfJsEvent } from './pdf-js-engine.js';
|
|
7
|
+
export declare class PdfJavaScriptAction extends PdfDictionary<{
|
|
8
|
+
S?: PdfName;
|
|
9
|
+
JS?: PdfString | PdfStream | PdfObjectReference;
|
|
10
|
+
}> {
|
|
11
|
+
engine?: PdfJsEngine;
|
|
12
|
+
constructor(dict: PdfDictionary, options?: {
|
|
13
|
+
engine?: PdfJsEngine;
|
|
14
|
+
});
|
|
15
|
+
get code(): string | null;
|
|
16
|
+
execute(event: PdfJsEvent): void;
|
|
17
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { PdfDictionary } from '../../core/objects/pdf-dictionary.js';
|
|
2
|
+
import { PdfString } from '../../core/objects/pdf-string.js';
|
|
3
|
+
import { PdfStream } from '../../core/objects/pdf-stream.js';
|
|
4
|
+
import { PdfObjectReference } from '../../core/objects/pdf-object-reference.js';
|
|
5
|
+
import { bytesToString } from '../../utils/bytesToString.js';
|
|
6
|
+
export class PdfJavaScriptAction extends PdfDictionary {
|
|
7
|
+
engine;
|
|
8
|
+
constructor(dict, options) {
|
|
9
|
+
super();
|
|
10
|
+
this.copyFrom(dict);
|
|
11
|
+
this.engine = options?.engine;
|
|
12
|
+
}
|
|
13
|
+
get code() {
|
|
14
|
+
const js = this.get('JS');
|
|
15
|
+
if (js instanceof PdfString) {
|
|
16
|
+
return js.value;
|
|
17
|
+
}
|
|
18
|
+
if (js instanceof PdfObjectReference) {
|
|
19
|
+
const resolved = js.resolve();
|
|
20
|
+
if (resolved?.content instanceof PdfStream) {
|
|
21
|
+
return bytesToString(resolved.content.data);
|
|
22
|
+
}
|
|
23
|
+
if (resolved?.content instanceof PdfString) {
|
|
24
|
+
return resolved.content.value;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (js instanceof PdfStream) {
|
|
28
|
+
return bytesToString(js.data);
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
execute(event) {
|
|
33
|
+
const code = this.code;
|
|
34
|
+
if (code && this.engine) {
|
|
35
|
+
this.engine.execute(code, event);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { PdfJsEvent } from './pdf-js-engine.js';
|
|
2
|
+
export declare function printd(fmt: string, date: Date): string;
|
|
3
|
+
export declare function scand(fmt: string, str: string): Date | null;
|
|
4
|
+
export declare function printf(fmt: string, ...args: unknown[]): string;
|
|
5
|
+
export declare const util: {
|
|
6
|
+
printd: typeof printd;
|
|
7
|
+
scand: typeof scand;
|
|
8
|
+
printf: typeof printf;
|
|
9
|
+
};
|
|
10
|
+
type GetFieldValue = (name: string) => string;
|
|
11
|
+
export declare function createBuiltins(event: PdfJsEvent, getFieldValue?: GetFieldValue): Record<string, unknown>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// util.printd — format a Date as a string using Acrobat date tokens
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
const MONTH_NAMES = [
|
|
5
|
+
'January',
|
|
6
|
+
'February',
|
|
7
|
+
'March',
|
|
8
|
+
'April',
|
|
9
|
+
'May',
|
|
10
|
+
'June',
|
|
11
|
+
'July',
|
|
12
|
+
'August',
|
|
13
|
+
'September',
|
|
14
|
+
'October',
|
|
15
|
+
'November',
|
|
16
|
+
'December',
|
|
17
|
+
];
|
|
18
|
+
const MONTH_ABBR = [
|
|
19
|
+
'Jan',
|
|
20
|
+
'Feb',
|
|
21
|
+
'Mar',
|
|
22
|
+
'Apr',
|
|
23
|
+
'May',
|
|
24
|
+
'Jun',
|
|
25
|
+
'Jul',
|
|
26
|
+
'Aug',
|
|
27
|
+
'Sep',
|
|
28
|
+
'Oct',
|
|
29
|
+
'Nov',
|
|
30
|
+
'Dec',
|
|
31
|
+
];
|
|
32
|
+
const DAY_NAMES = [
|
|
33
|
+
'Sunday',
|
|
34
|
+
'Monday',
|
|
35
|
+
'Tuesday',
|
|
36
|
+
'Wednesday',
|
|
37
|
+
'Thursday',
|
|
38
|
+
'Friday',
|
|
39
|
+
'Saturday',
|
|
40
|
+
];
|
|
41
|
+
const DAY_ABBR = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
42
|
+
function pad(n, len) {
|
|
43
|
+
return String(n).padStart(len, '0');
|
|
44
|
+
}
|
|
45
|
+
export function printd(fmt, date) {
|
|
46
|
+
const y = date.getFullYear();
|
|
47
|
+
const M = date.getMonth(); // 0-based
|
|
48
|
+
const d = date.getDate();
|
|
49
|
+
const dow = date.getDay();
|
|
50
|
+
const H = date.getHours();
|
|
51
|
+
const m = date.getMinutes();
|
|
52
|
+
const s = date.getSeconds();
|
|
53
|
+
const h12 = H % 12 || 12;
|
|
54
|
+
// Replace tokens longest-first to avoid partial matches
|
|
55
|
+
return fmt
|
|
56
|
+
.replace(/yyyy/g, String(y))
|
|
57
|
+
.replace(/yy/g, pad(y % 100, 2))
|
|
58
|
+
.replace(/mmmm/g, MONTH_NAMES[M])
|
|
59
|
+
.replace(/mmm/g, MONTH_ABBR[M])
|
|
60
|
+
.replace(/mm/g, pad(M + 1, 2))
|
|
61
|
+
.replace(/(?<![\w])m(?![\w])/g, String(M + 1))
|
|
62
|
+
.replace(/dddd/g, DAY_NAMES[dow])
|
|
63
|
+
.replace(/ddd/g, DAY_ABBR[dow])
|
|
64
|
+
.replace(/dd/g, pad(d, 2))
|
|
65
|
+
.replace(/(?<![\w])d(?![\w])/g, String(d))
|
|
66
|
+
.replace(/HH/g, pad(H, 2))
|
|
67
|
+
.replace(/hh/g, pad(h12, 2))
|
|
68
|
+
.replace(/(?<![\w])h(?![\w])/g, String(h12))
|
|
69
|
+
.replace(/MM/g, pad(m, 2))
|
|
70
|
+
.replace(/ss/g, pad(s, 2))
|
|
71
|
+
.replace(/tt/g, H < 12 ? 'AM' : 'PM')
|
|
72
|
+
.replace(/(?<![\w])t(?![\w])/g, H < 12 ? 'am' : 'pm');
|
|
73
|
+
}
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// util.scand — parse a date string using an Acrobat format
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
export function scand(fmt, str) {
|
|
78
|
+
// Build a regex from the format, capturing groups for each token
|
|
79
|
+
const tokens = [];
|
|
80
|
+
let pattern = '';
|
|
81
|
+
let i = 0;
|
|
82
|
+
while (i < fmt.length) {
|
|
83
|
+
let matched = false;
|
|
84
|
+
for (const tok of [
|
|
85
|
+
'yyyy',
|
|
86
|
+
'yy',
|
|
87
|
+
'mmmm',
|
|
88
|
+
'mmm',
|
|
89
|
+
'mm',
|
|
90
|
+
'm',
|
|
91
|
+
'dddd',
|
|
92
|
+
'ddd',
|
|
93
|
+
'dd',
|
|
94
|
+
'd',
|
|
95
|
+
'HH',
|
|
96
|
+
'hh',
|
|
97
|
+
'h',
|
|
98
|
+
'MM',
|
|
99
|
+
'ss',
|
|
100
|
+
]) {
|
|
101
|
+
if (fmt.substring(i, i + tok.length) === tok) {
|
|
102
|
+
tokens.push({ token: tok, index: tokens.length });
|
|
103
|
+
switch (tok) {
|
|
104
|
+
case 'yyyy':
|
|
105
|
+
pattern += '(\\d{4})';
|
|
106
|
+
break;
|
|
107
|
+
case 'yy':
|
|
108
|
+
pattern += '(\\d{2})';
|
|
109
|
+
break;
|
|
110
|
+
case 'mmmm':
|
|
111
|
+
pattern += '([A-Za-z]+)';
|
|
112
|
+
break;
|
|
113
|
+
case 'mmm':
|
|
114
|
+
pattern += '([A-Za-z]{3})';
|
|
115
|
+
break;
|
|
116
|
+
case 'mm':
|
|
117
|
+
pattern += '(\\d{2})';
|
|
118
|
+
break;
|
|
119
|
+
case 'm':
|
|
120
|
+
pattern += '(\\d{1,2})';
|
|
121
|
+
break;
|
|
122
|
+
case 'dddd':
|
|
123
|
+
pattern += '([A-Za-z]+)';
|
|
124
|
+
break;
|
|
125
|
+
case 'ddd':
|
|
126
|
+
pattern += '([A-Za-z]{3})';
|
|
127
|
+
break;
|
|
128
|
+
case 'dd':
|
|
129
|
+
pattern += '(\\d{2})';
|
|
130
|
+
break;
|
|
131
|
+
case 'd':
|
|
132
|
+
pattern += '(\\d{1,2})';
|
|
133
|
+
break;
|
|
134
|
+
case 'HH':
|
|
135
|
+
case 'hh':
|
|
136
|
+
pattern += '(\\d{2})';
|
|
137
|
+
break;
|
|
138
|
+
case 'h':
|
|
139
|
+
pattern += '(\\d{1,2})';
|
|
140
|
+
break;
|
|
141
|
+
case 'MM':
|
|
142
|
+
pattern += '(\\d{2})';
|
|
143
|
+
break;
|
|
144
|
+
case 'ss':
|
|
145
|
+
pattern += '(\\d{2})';
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
i += tok.length;
|
|
149
|
+
matched = true;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (!matched) {
|
|
154
|
+
pattern += fmt[i].replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
155
|
+
i++;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const re = new RegExp('^' + pattern + '$');
|
|
159
|
+
const match = str.match(re);
|
|
160
|
+
if (!match)
|
|
161
|
+
return null;
|
|
162
|
+
let year = 0, month = 0, day = 1, hours = 0, minutes = 0, seconds = 0;
|
|
163
|
+
for (const { token, index } of tokens) {
|
|
164
|
+
const val = match[index + 1];
|
|
165
|
+
switch (token) {
|
|
166
|
+
case 'yyyy':
|
|
167
|
+
year = parseInt(val, 10);
|
|
168
|
+
break;
|
|
169
|
+
case 'yy': {
|
|
170
|
+
const n = parseInt(val, 10);
|
|
171
|
+
year = n < 50 ? 2000 + n : 1900 + n;
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
case 'mmmm': {
|
|
175
|
+
const idx = MONTH_NAMES.findIndex((m) => m.toLowerCase() === val.toLowerCase());
|
|
176
|
+
if (idx >= 0)
|
|
177
|
+
month = idx;
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
case 'mmm': {
|
|
181
|
+
const idx = MONTH_ABBR.findIndex((m) => m.toLowerCase() === val.toLowerCase());
|
|
182
|
+
if (idx >= 0)
|
|
183
|
+
month = idx;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
case 'mm':
|
|
187
|
+
case 'm':
|
|
188
|
+
month = parseInt(val, 10) - 1;
|
|
189
|
+
break;
|
|
190
|
+
case 'dd':
|
|
191
|
+
case 'd':
|
|
192
|
+
day = parseInt(val, 10);
|
|
193
|
+
break;
|
|
194
|
+
case 'HH':
|
|
195
|
+
case 'hh':
|
|
196
|
+
case 'h':
|
|
197
|
+
hours = parseInt(val, 10);
|
|
198
|
+
break;
|
|
199
|
+
case 'MM':
|
|
200
|
+
minutes = parseInt(val, 10);
|
|
201
|
+
break;
|
|
202
|
+
case 'ss':
|
|
203
|
+
seconds = parseInt(val, 10);
|
|
204
|
+
break;
|
|
205
|
+
// dddd/ddd are day-of-week names — informational only, ignored
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return new Date(year, month, day, hours, minutes, seconds);
|
|
209
|
+
}
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// util.printf — sprintf subset: %d, %f, %s with width/precision/flags
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
export function printf(fmt, ...args) {
|
|
214
|
+
let argIdx = 0;
|
|
215
|
+
return fmt.replace(/%([0 #+-]*)(\d+)?(?:\.(\d+))?([dfsxXo%])/g, (_match, flags, widthStr, precStr, type) => {
|
|
216
|
+
if (type === '%')
|
|
217
|
+
return '%';
|
|
218
|
+
const val = args[argIdx++];
|
|
219
|
+
const width = widthStr ? parseInt(widthStr, 10) : 0;
|
|
220
|
+
const prec = precStr !== undefined ? parseInt(precStr, 10) : undefined;
|
|
221
|
+
const leftAlign = flags.includes('-');
|
|
222
|
+
const zeroPad = flags.includes('0') && !leftAlign;
|
|
223
|
+
const plusSign = flags.includes('+');
|
|
224
|
+
const spaceSign = flags.includes(' ');
|
|
225
|
+
let result;
|
|
226
|
+
switch (type) {
|
|
227
|
+
case 'd': {
|
|
228
|
+
const n = Math.trunc(Number(val));
|
|
229
|
+
const sign = n < 0 ? '-' : plusSign ? '+' : spaceSign ? ' ' : '';
|
|
230
|
+
const digits = String(Math.abs(n));
|
|
231
|
+
result =
|
|
232
|
+
sign +
|
|
233
|
+
(zeroPad
|
|
234
|
+
? digits.padStart(Math.max(0, width - sign.length), '0')
|
|
235
|
+
: digits);
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
case 'f': {
|
|
239
|
+
const n = Number(val);
|
|
240
|
+
const p = prec !== undefined ? prec : 6;
|
|
241
|
+
const sign = n < 0 ? '-' : plusSign ? '+' : spaceSign ? ' ' : '';
|
|
242
|
+
const formatted = Math.abs(n).toFixed(p);
|
|
243
|
+
result =
|
|
244
|
+
sign +
|
|
245
|
+
(zeroPad
|
|
246
|
+
? formatted.padStart(Math.max(0, width - sign.length), '0')
|
|
247
|
+
: formatted);
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
case 'x':
|
|
251
|
+
result = (Math.trunc(Number(val)) >>> 0).toString(16);
|
|
252
|
+
break;
|
|
253
|
+
case 'X':
|
|
254
|
+
result = (Math.trunc(Number(val)) >>> 0)
|
|
255
|
+
.toString(16)
|
|
256
|
+
.toUpperCase();
|
|
257
|
+
break;
|
|
258
|
+
case 'o':
|
|
259
|
+
result = (Math.trunc(Number(val)) >>> 0).toString(8);
|
|
260
|
+
break;
|
|
261
|
+
case 's':
|
|
262
|
+
default:
|
|
263
|
+
result = String(val);
|
|
264
|
+
if (prec !== undefined)
|
|
265
|
+
result = result.slice(0, prec);
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
if (result.length < width) {
|
|
269
|
+
result = leftAlign
|
|
270
|
+
? result.padEnd(width)
|
|
271
|
+
: result.padStart(width, zeroPad ? '0' : ' ');
|
|
272
|
+
}
|
|
273
|
+
return result;
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
// util object
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
export const util = { printd, scand, printf };
|
|
280
|
+
function parseNumericValue(str) {
|
|
281
|
+
// Strip currency symbols, spaces, commas
|
|
282
|
+
const cleaned = str.replace(/[^0-9.\-eE]/g, '');
|
|
283
|
+
const n = parseFloat(cleaned);
|
|
284
|
+
return isNaN(n) ? 0 : n;
|
|
285
|
+
}
|
|
286
|
+
function formatNumber(value, nDec, sepStyle, negStyle, _currStyle, strCurrency, bCurrencyPrepend) {
|
|
287
|
+
const absVal = Math.abs(value);
|
|
288
|
+
const fixed = absVal.toFixed(nDec);
|
|
289
|
+
// sepStyle: 0 = 1,234.56 1 = 1234.56 2 = 1.234,56 3 = 1234,56
|
|
290
|
+
let [intPart, decPart] = fixed.split('.');
|
|
291
|
+
const decSep = sepStyle >= 2 ? ',' : '.';
|
|
292
|
+
const thousandSep = sepStyle === 0 ? ',' : sepStyle === 2 ? '.' : '';
|
|
293
|
+
if (thousandSep) {
|
|
294
|
+
intPart = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, thousandSep);
|
|
295
|
+
}
|
|
296
|
+
let formatted = decPart !== undefined ? intPart + decSep + decPart : intPart;
|
|
297
|
+
// Currency
|
|
298
|
+
if (strCurrency) {
|
|
299
|
+
if (bCurrencyPrepend) {
|
|
300
|
+
formatted = strCurrency + formatted;
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
formatted = formatted + strCurrency;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// Negative style: 0 = -val 1 = red text (we just use -) 2 = (val) 3 = (val) red
|
|
307
|
+
if (value < 0) {
|
|
308
|
+
if (negStyle === 0 || negStyle === 1) {
|
|
309
|
+
formatted = '-' + formatted;
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
formatted = '(' + formatted + ')';
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return formatted;
|
|
316
|
+
}
|
|
317
|
+
export function createBuiltins(event, getFieldValue) {
|
|
318
|
+
// --- AF functions that close over event ---
|
|
319
|
+
function AFDate_FormatEx(fmt) {
|
|
320
|
+
if (!event.value)
|
|
321
|
+
return;
|
|
322
|
+
// Try parsing the current value as a date
|
|
323
|
+
const d = new Date(event.value);
|
|
324
|
+
if (isNaN(d.getTime()))
|
|
325
|
+
return;
|
|
326
|
+
event.value = printd(fmt, d);
|
|
327
|
+
}
|
|
328
|
+
function AFDate_KeystrokeEx(fmt) {
|
|
329
|
+
if (event.willCommit) {
|
|
330
|
+
if (!event.value)
|
|
331
|
+
return;
|
|
332
|
+
const d = scand(fmt, event.value);
|
|
333
|
+
if (!d) {
|
|
334
|
+
event.rc = false;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// On non-commit keystrokes, allow all input
|
|
338
|
+
}
|
|
339
|
+
function AFNumber_Format(nDec, sepStyle, negStyle, _currStyle, strCurrency, bCurrencyPrepend) {
|
|
340
|
+
if (!event.value && event.value !== '0')
|
|
341
|
+
return;
|
|
342
|
+
const num = parseNumericValue(event.value);
|
|
343
|
+
event.value = formatNumber(num, nDec, sepStyle, negStyle, _currStyle, strCurrency, bCurrencyPrepend);
|
|
344
|
+
}
|
|
345
|
+
function AFNumber_Keystroke(nDec, _sepStyle, _negStyle, _currStyle, _strCurrency, _bCurrencyPrepend) {
|
|
346
|
+
if (event.willCommit) {
|
|
347
|
+
if (!event.value)
|
|
348
|
+
return;
|
|
349
|
+
const num = parseFloat(event.value);
|
|
350
|
+
if (isNaN(num)) {
|
|
351
|
+
event.rc = false;
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
// Check decimal places
|
|
355
|
+
const parts = event.value.split('.');
|
|
356
|
+
if (nDec === 0 && parts.length > 1) {
|
|
357
|
+
event.rc = false;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
function AFSimple_Calculate(op, fields) {
|
|
362
|
+
if (!getFieldValue)
|
|
363
|
+
return;
|
|
364
|
+
const values = fields.map((name) => parseNumericValue(getFieldValue(name)));
|
|
365
|
+
let result;
|
|
366
|
+
switch (op.toUpperCase()) {
|
|
367
|
+
case 'SUM':
|
|
368
|
+
result = values.reduce((a, b) => a + b, 0);
|
|
369
|
+
break;
|
|
370
|
+
case 'AVG':
|
|
371
|
+
result = values.length
|
|
372
|
+
? values.reduce((a, b) => a + b, 0) / values.length
|
|
373
|
+
: 0;
|
|
374
|
+
break;
|
|
375
|
+
case 'PRD':
|
|
376
|
+
result = values.reduce((a, b) => a * b, 1);
|
|
377
|
+
break;
|
|
378
|
+
case 'MIN':
|
|
379
|
+
result = values.length ? Math.min(...values) : 0;
|
|
380
|
+
break;
|
|
381
|
+
case 'MAX':
|
|
382
|
+
result = values.length ? Math.max(...values) : 0;
|
|
383
|
+
break;
|
|
384
|
+
default:
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
event.value = String(result);
|
|
388
|
+
}
|
|
389
|
+
function AFSpecial_Format(psf) {
|
|
390
|
+
if (!event.value)
|
|
391
|
+
return;
|
|
392
|
+
const digits = event.value.replace(/\D/g, '');
|
|
393
|
+
switch (psf) {
|
|
394
|
+
case 0: // zip: 12345
|
|
395
|
+
event.value = digits.slice(0, 5).padEnd(5, '0');
|
|
396
|
+
break;
|
|
397
|
+
case 1: // ssn: 123-45-6789
|
|
398
|
+
event.value =
|
|
399
|
+
digits.slice(0, 3) +
|
|
400
|
+
'-' +
|
|
401
|
+
digits.slice(3, 5) +
|
|
402
|
+
'-' +
|
|
403
|
+
digits.slice(5, 9);
|
|
404
|
+
break;
|
|
405
|
+
case 2: // phone: (123) 456-7890
|
|
406
|
+
event.value =
|
|
407
|
+
'(' +
|
|
408
|
+
digits.slice(0, 3) +
|
|
409
|
+
') ' +
|
|
410
|
+
digits.slice(3, 6) +
|
|
411
|
+
'-' +
|
|
412
|
+
digits.slice(6, 10);
|
|
413
|
+
break;
|
|
414
|
+
case 3: // zip+4: 12345-6789
|
|
415
|
+
event.value = digits.slice(0, 5) + '-' + digits.slice(5, 9);
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
function AFSpecial_Keystroke(psf) {
|
|
420
|
+
if (!event.willCommit)
|
|
421
|
+
return;
|
|
422
|
+
if (!event.value)
|
|
423
|
+
return;
|
|
424
|
+
const digits = event.value.replace(/\D/g, '');
|
|
425
|
+
switch (psf) {
|
|
426
|
+
case 0: // zip
|
|
427
|
+
if (digits.length !== 5)
|
|
428
|
+
event.rc = false;
|
|
429
|
+
break;
|
|
430
|
+
case 1: // ssn
|
|
431
|
+
if (digits.length !== 9)
|
|
432
|
+
event.rc = false;
|
|
433
|
+
break;
|
|
434
|
+
case 2: // phone
|
|
435
|
+
if (digits.length !== 10 && digits.length !== 7)
|
|
436
|
+
event.rc = false;
|
|
437
|
+
break;
|
|
438
|
+
case 3: // zip+4
|
|
439
|
+
if (digits.length !== 9)
|
|
440
|
+
event.rc = false;
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
util,
|
|
446
|
+
AFDate_FormatEx,
|
|
447
|
+
AFDate_KeystrokeEx,
|
|
448
|
+
AFNumber_Format,
|
|
449
|
+
AFNumber_Keystroke,
|
|
450
|
+
AFSimple_Calculate,
|
|
451
|
+
AFSpecial_Format,
|
|
452
|
+
AFSpecial_Keystroke,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { PdfJsEngine, PdfJsEvent } from './pdf-js-engine.js';
|
|
2
|
+
export interface PdfJavaScriptEngineOptions {
|
|
3
|
+
getFieldValue?: (name: string) => string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Default JavaScript engine that executes PDF JS actions via `new Function()`.
|
|
7
|
+
*
|
|
8
|
+
* **Security note:** This engine runs PDF-sourced JavaScript with access to the
|
|
9
|
+
* ambient JS environment (e.g. `globalThis`, constructors, network APIs). It is
|
|
10
|
+
* NOT sandboxed and should only be used with trusted PDF documents. For untrusted
|
|
11
|
+
* documents, provide your own `PdfJsEngine` implementation that evaluates code
|
|
12
|
+
* in an isolated context (e.g. Node `vm`, a dedicated realm, or a Web Worker).
|
|
13
|
+
*/
|
|
14
|
+
export declare class PdfJavaScriptEngine implements PdfJsEngine {
|
|
15
|
+
private _getFieldValue?;
|
|
16
|
+
constructor(options?: PdfJavaScriptEngineOptions);
|
|
17
|
+
execute(code: string, event: PdfJsEvent): void;
|
|
18
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createBuiltins } from './pdf-js-builtins.js';
|
|
2
|
+
/**
|
|
3
|
+
* Default JavaScript engine that executes PDF JS actions via `new Function()`.
|
|
4
|
+
*
|
|
5
|
+
* **Security note:** This engine runs PDF-sourced JavaScript with access to the
|
|
6
|
+
* ambient JS environment (e.g. `globalThis`, constructors, network APIs). It is
|
|
7
|
+
* NOT sandboxed and should only be used with trusted PDF documents. For untrusted
|
|
8
|
+
* documents, provide your own `PdfJsEngine` implementation that evaluates code
|
|
9
|
+
* in an isolated context (e.g. Node `vm`, a dedicated realm, or a Web Worker).
|
|
10
|
+
*/
|
|
11
|
+
export class PdfJavaScriptEngine {
|
|
12
|
+
_getFieldValue;
|
|
13
|
+
constructor(options) {
|
|
14
|
+
this._getFieldValue = options?.getFieldValue;
|
|
15
|
+
}
|
|
16
|
+
execute(code, event) {
|
|
17
|
+
const builtins = createBuiltins(event, this._getFieldValue);
|
|
18
|
+
const names = Object.keys(builtins);
|
|
19
|
+
const fn = new Function('event', 'app', ...names, code);
|
|
20
|
+
fn(event, Object.freeze({ alert() { } }), ...names.map((k) => builtins[k]));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -12,6 +12,7 @@ import './fields/pdf-choice-form-field.js';
|
|
|
12
12
|
import './fields/pdf-signature-form-field.js';
|
|
13
13
|
import { PdfDefaultResourcesDictionary } from '../annotations/pdf-default-resources.js';
|
|
14
14
|
import { PdfXfaForm } from './xfa/pdf-xfa-form.js';
|
|
15
|
+
import type { PdfJsEngine } from './js/pdf-js-engine.js';
|
|
15
16
|
export declare class PdfAcroForm<T extends Record<string, string> = Record<string, string>> extends PdfIndirectObject<PdfDictionary<{
|
|
16
17
|
Fields: PdfArray<PdfObjectReference> | PdfObjectReference;
|
|
17
18
|
NeedAppearances?: PdfBoolean;
|
|
@@ -38,7 +39,10 @@ export declare class PdfAcroForm<T extends Record<string, string> = Record<strin
|
|
|
38
39
|
set fields(newFields: PdfFormField[]);
|
|
39
40
|
setValues(values: Partial<T>): void;
|
|
40
41
|
importData(fields: T): void;
|
|
42
|
+
private _fireValidate;
|
|
43
|
+
private _fireCalculate;
|
|
41
44
|
exportData(): Partial<T>;
|
|
45
|
+
jsEngine?: PdfJsEngine;
|
|
42
46
|
fontEncodingMaps: Map<string, Map<number, string>>;
|
|
43
47
|
getFontEncodingMap(fontName: string): Map<number, string> | null;
|
|
44
48
|
get xfa(): PdfXfaForm | null;
|
|
@@ -122,15 +122,70 @@ export class PdfAcroForm extends PdfIndirectObject {
|
|
|
122
122
|
for (const field of this.fields) {
|
|
123
123
|
const name = field.name;
|
|
124
124
|
if (name in values && values[name] !== undefined) {
|
|
125
|
-
|
|
125
|
+
const result = this._fireValidate(field, values[name]);
|
|
126
|
+
if (!result.rc)
|
|
127
|
+
continue;
|
|
128
|
+
field.value = result.value;
|
|
126
129
|
}
|
|
127
130
|
}
|
|
131
|
+
this._fireCalculate();
|
|
128
132
|
}
|
|
129
133
|
importData(fields) {
|
|
130
134
|
for (const field of this.fields) {
|
|
131
135
|
const name = field.name;
|
|
132
136
|
if (name && name in fields) {
|
|
133
|
-
|
|
137
|
+
const result = this._fireValidate(field, fields[name]);
|
|
138
|
+
if (!result.rc)
|
|
139
|
+
continue;
|
|
140
|
+
field.value = result.value;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
this._fireCalculate();
|
|
144
|
+
}
|
|
145
|
+
_fireValidate(field, value) {
|
|
146
|
+
const validateAction = field.actions?.validate;
|
|
147
|
+
if (!validateAction?.code)
|
|
148
|
+
return { rc: true, value };
|
|
149
|
+
const event = {
|
|
150
|
+
fieldName: field.name,
|
|
151
|
+
value,
|
|
152
|
+
willCommit: true,
|
|
153
|
+
rc: true,
|
|
154
|
+
};
|
|
155
|
+
validateAction.execute(event);
|
|
156
|
+
return { rc: event.rc, value: event.value };
|
|
157
|
+
}
|
|
158
|
+
_fireCalculate() {
|
|
159
|
+
const co = this.content.get('CO');
|
|
160
|
+
if (!(co instanceof PdfArray))
|
|
161
|
+
return;
|
|
162
|
+
const allFields = this.fields;
|
|
163
|
+
const fieldsByObjNum = new Map();
|
|
164
|
+
for (const field of allFields) {
|
|
165
|
+
if (field.objectNumber != null) {
|
|
166
|
+
fieldsByObjNum.set(field.objectNumber, field);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
for (const item of co.items) {
|
|
170
|
+
if (!(item instanceof PdfObjectReference))
|
|
171
|
+
continue;
|
|
172
|
+
const resolved = item.resolve();
|
|
173
|
+
if (!resolved)
|
|
174
|
+
continue;
|
|
175
|
+
const field = fieldsByObjNum.get(resolved.objectNumber);
|
|
176
|
+
if (!field)
|
|
177
|
+
continue;
|
|
178
|
+
const calcAction = field.actions?.calculate;
|
|
179
|
+
if (!calcAction?.code)
|
|
180
|
+
continue;
|
|
181
|
+
const event = {
|
|
182
|
+
fieldName: field.name,
|
|
183
|
+
value: field.value,
|
|
184
|
+
rc: true,
|
|
185
|
+
};
|
|
186
|
+
calcAction.execute(event);
|
|
187
|
+
if (event.rc && event.value !== field.value) {
|
|
188
|
+
field.value = event.value;
|
|
134
189
|
}
|
|
135
190
|
}
|
|
136
191
|
}
|
|
@@ -144,6 +199,7 @@ export class PdfAcroForm extends PdfIndirectObject {
|
|
|
144
199
|
}
|
|
145
200
|
return result;
|
|
146
201
|
}
|
|
202
|
+
jsEngine;
|
|
147
203
|
fontEncodingMaps = new Map();
|
|
148
204
|
getFontEncodingMap(fontName) {
|
|
149
205
|
if (this.fontEncodingMaps.has(fontName)) {
|
|
@@ -34,6 +34,26 @@ export declare class PdfAnnotation extends PdfIndirectObject<PdfDictionary<{
|
|
|
34
34
|
AP?: PdfAppearanceStreamDictionary;
|
|
35
35
|
P?: PdfObjectReference;
|
|
36
36
|
Parent?: PdfObjectReference<PdfPage>;
|
|
37
|
+
A?: PdfDictionary<{
|
|
38
|
+
Type?: PdfName;
|
|
39
|
+
S?: PdfName;
|
|
40
|
+
D?: PdfArray | PdfDictionary;
|
|
41
|
+
T?: PdfString;
|
|
42
|
+
F?: PdfNumber;
|
|
43
|
+
Win?: PdfDictionary;
|
|
44
|
+
JS?: PdfString;
|
|
45
|
+
}>;
|
|
46
|
+
AA?: PdfDictionary<{
|
|
47
|
+
D?: PdfObjectReference | PdfDictionary;
|
|
48
|
+
U?: PdfObjectReference | PdfDictionary;
|
|
49
|
+
Fo?: PdfObjectReference | PdfDictionary;
|
|
50
|
+
Bl?: PdfObjectReference | PdfDictionary;
|
|
51
|
+
PO?: PdfObjectReference | PdfDictionary;
|
|
52
|
+
K?: PdfObjectReference | PdfDictionary;
|
|
53
|
+
V?: PdfObjectReference | PdfDictionary;
|
|
54
|
+
C?: PdfObjectReference | PdfDictionary;
|
|
55
|
+
F?: PdfObjectReference | PdfDictionary;
|
|
56
|
+
}>;
|
|
37
57
|
}>> {
|
|
38
58
|
private _annotationFlags?;
|
|
39
59
|
private get flags_();
|
|
@@ -22,6 +22,8 @@ export declare class PdfDictionary<T extends PdfDictionaryEntries = PdfDictionar
|
|
|
22
22
|
entries(): IterableIterator<[string, PdfObject | undefined]>;
|
|
23
23
|
get isTrailingDelimited(): boolean;
|
|
24
24
|
protected tokenize(): PdfToken[];
|
|
25
|
+
/** Factory-style type conversion: constructs a new instance passing `this` as the first argument */
|
|
26
|
+
becomes<U extends PdfDictionary, A extends unknown[]>(cls: new (source: PdfDictionary, ...args: A) => U, ...args: A): U;
|
|
25
27
|
copyFrom(other: PdfDictionary<any>): void;
|
|
26
28
|
cloneImpl(): this;
|
|
27
29
|
setModified(modified?: boolean): void;
|
|
@@ -131,6 +131,20 @@ export class PdfDictionary extends PdfObject {
|
|
|
131
131
|
new PdfEndDictionaryToken(),
|
|
132
132
|
];
|
|
133
133
|
}
|
|
134
|
+
/** Factory-style type conversion: constructs a new instance passing `this` as the first argument */
|
|
135
|
+
becomes(cls, ...args) {
|
|
136
|
+
if (this instanceof cls)
|
|
137
|
+
return this;
|
|
138
|
+
const donor = new cls(this, ...args);
|
|
139
|
+
Object.setPrototypeOf(this, cls.prototype);
|
|
140
|
+
for (const key of Object.getOwnPropertyNames(donor)) {
|
|
141
|
+
if (!Object.prototype.hasOwnProperty.call(this, key)) {
|
|
142
|
+
;
|
|
143
|
+
this[key] = donor[key];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return this;
|
|
147
|
+
}
|
|
134
148
|
copyFrom(other) {
|
|
135
149
|
for (const [key, value] of other.#entries) {
|
|
136
150
|
this.#entries.set(key, value);
|