Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

### Unreleased

- Add an `onClick` option to `formPushButton` (and other form annotation methods) for a field's mouse-up JavaScript action, replacing the previous `AA`-plus-`format` escape hatch. Accepts a plain function, called with Acrobat's `app`/`getField`/`display`/`event` as arguments and `this` bound to the Document, as well as a string. TypeScript projects can import types for this signature from the new `pdfkit/types/acrobat-js`

### [v0.20.2] - 2026-08-29

- Fix bundlers and file tracers packing the ESM copies of the standard font metrics instead of the CommonJS ones the Node build actually loads, which left `Cannot find module` errors for every standard font at runtime, by resolving the internal `#standard-fonts/*` mapping to a single file under all conditions
Expand Down
45 changes: 43 additions & 2 deletions docs/forms.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,15 +118,56 @@ These options are accepted by `formPushButton`:

- `label` [_string_] - Sets the label text. You can also set an icon, but for
this you will need to 'expert-up' and dig deeper into the PDF Reference manual.
- `onClick` [_string | function_] - JavaScript to run when the button is
clicked (its mouse-up action). If a `format` option is also given, its
keystroke/format validation actions are added alongside this one rather
than replacing it.

```js
var opts = {
backgroundColor: 'yellow',
label: 'Test Button'
label: 'Test Button',
onClick: 'app.alert("clicked");'
};
doc.formPushButton('btn1', 10, 200, 100, 30, opts);
```

`onClick` also accepts a plain function, called with Acrobat's own `app`,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplify the description. Be objective hiding internal details, just with enough information to user create correct functions

`getField`, `display` and `event` passed in as arguments, in that order (and
`this` bound to the Document, exactly as Acrobat itself binds it). Declare
only the leading parameters your handler actually uses — `function (app) {}`
or even `function () {}` are both fine, since the call always passes all of
them regardless of how many the handler declares; the rest are simply
ignored, the same way `array.map(item => ...)` can ignore the `index` and
`array` parameters its callback type also offers. This runs inside the PDF
viewer's own JavaScript engine, not wherever the PDF was generated, so it
can't close over outside variables — use only plain function syntax (not
e.g. arrow functions, which also can't bind `this`) for the widest viewer
support:

```js
doc.formPushButton('btn1', 10, 200, 100, 30, {
label: 'Test Button',
onClick: function (app, getField) {
app.alert('clicked');
this.getField('otherField').value = 'updated from btn1';
}
Comment on lines +151 to +154

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

param getField is not used. Is really necessary?

});
```

TypeScript projects can import `AcrobatOnClick` and the other types this
signature uses from `pdfkit/types/acrobat-js` — a small, best-effort set of
types for the handful of Acrobat globals most `onClick` handlers need, kept
separate from pdfkit's own types so nothing is declared globally:
Comment on lines +158 to +161

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nop. Types will be handled separately. Do not expose it for now. It can be documented internally


```ts
import type { AcrobatOnClick } from 'pdfkit/types/acrobat-js';

const onClick: AcrobatOnClick = function (app) {
app.alert('clicked');
};
```

#### Radio Button Field Options

These options are accepted by `formRadioButton`:
Expand Down Expand Up @@ -302,7 +343,7 @@ The output of this example looks like this.

### Advanced Form Field Use

Older implementations used to pass all unknown options to the internal PDF object structure. A small set of direct PDF dictionary escape hatches is still recognized: `Ff`, `MK.CA`, and `AA` when a `format` option is used but its use is discouraged and likely will be removed in future versions.
Older implementations used to pass all unknown options to the internal PDF object structure. A small set of direct PDF dictionary escape hatches is still recognized: `Ff` and `MK.CA`, but their use is discouraged and they may be removed in future versions. A previously-recognized `AA` escape hatch (only reachable together with a `format` option) has been replaced by the `onClick` option above, which needs no PDF dictionary knowledge and works on its own.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nop. This is not a changelog and no need to justify


If an option is not supported, open an issue on Github and it will be considered for addition to the API.

Expand Down
28 changes: 27 additions & 1 deletion lib/mixins/acroform.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,31 @@ function mapStrings(options, pdfObject) {
}
}

function mapActions(options, pdfObject) {
if (options.onClick) {
// A function is stringified and immediately invoked with `this` bound to
// the Document (exactly as Acrobat itself binds it in any field action)
// and Acrobat's own `app`, `getField`, `display` and `event` globals
// passed in as arguments, so authors can write the action as a real,
// typed function (see types/acrobat-js.d.ts) instead of a hand-built
// string or relying on ambient global declarations that risk colliding
// with an unrelated identifier elsewhere in their project. It still runs
// inside Acrobat's own JavaScript engine, not wherever the PDF was
// generated, so it can't close over outside variables, and only plain
// function syntax (not arrow functions or other syntax Acrobat's engine
// may not support) should be relied on.
const js =

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep it simple, pass the base minimum arguments to get it working

typeof options.onClick === 'function'
? `(${options.onClick}).call(this, app, getField, display, event);`
: options.onClick;
pdfObject.AA = pdfObject.AA ?? {};
pdfObject.AA.U = {
S: 'JavaScript',
JS: new String(js),
};
}
}

function mapFormat(options, pdfObject) {
const f = options.format;
if (f?.type) {
Expand Down Expand Up @@ -148,7 +173,7 @@ function mapFormat(options, pdfObject) {
params = String([String(p.nDec), p.sepComma ? '0' : '1'].join(','));
}
}
pdfObject.AA = options.AA ?? {};
pdfObject.AA = pdfObject.AA ?? {};
pdfObject.AA.K = {
S: 'JavaScript',
JS: new String(`${fnKeystroke}(${params});`),
Expand Down Expand Up @@ -311,6 +336,7 @@ export default {
this._mapFont(options, pdfObject);
mapStrings(options, pdfObject);
this._mapColors(options, pdfObject);
mapActions(options, pdfObject);
mapFormat(options, pdfObject);

pdfObject.T = new String(name);
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@
"require": "./js/output.cjs",
"default": "./js/output.mjs"
},
"./types/acrobat-js": {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nop

"types": "./types/acrobat-js.d.ts",
"default": "./types/acrobat-js.d.ts"
},
"./standard-fonts/Courier": {
"require": "./js/standard-fonts/Courier.cjs",
"default": "./js/standard-fonts/Courier.mjs"
Expand Down
60 changes: 60 additions & 0 deletions tests/unit/acroform.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,66 @@ describe('acroform', () => {
expect(docData[2]).toBe(expected[2]);
});

test('push button with an onClick action', () => {
const expected = [
'10 0 obj',
'<<\n/FT /Btn\n/Ff 65536\n/AA <<\n/U <<\n/S /JavaScript\n/JS (app.alert\\(1\\);)\n>>\n>>\n' +
'/T (btn1)\n/Subtype /Widget\n/F 4\n/Type /Annot\n/Rect [20 742 120 772]\n/Border [0 0 0]\n/C [0 0 0]\n>>',
'endobj',
];
doc.initForm();
const docData = logData(doc);
doc.formPushButton('btn1', 20, 20, 100, 30, { onClick: 'app.alert(1);' });
expect(docData.length).toBe(3);
expect(docData).toContainChunk(expected);
});

test('push button with an onClick action given as a function', () => {
doc.initForm();
const docData = logData(doc);
// Written the way types/acrobat-js.d.ts's AcrobatOnClick expects: app
// and the other Acrobat globals arrive as parameters, not references to
// ambient globals, so nothing here needs pdfkit-specific lint/type setup.
function onClick(app) {
app.alert('clicked');
}
doc.formPushButton('btn1', 20, 20, 100, 30, { onClick });

// The function is stringified and invoked with Acrobat's globals; PDF
// string literals escape parens and newlines, so build the expectation
// the same way rather than hardcoding the exact whitespace
// `Function.prototype.toString()` happens to use (see lib/object.js's
// `escapable` map).
const expectedJs =
`(${onClick}).call(this, app, getField, display, event);`.replace(
/[\n\r\t\b\f()\\]/g,
(char) => ({ '\n': '\\n', '\r': '\\r', '(': '\\(', ')': '\\)' })[char],
);
expect(docData[1]).toContain('/S /JavaScript');
expect(docData[1]).toContain(expectedJs);
});

test('an onClick action and text formatting combine into one AA dictionary', () => {
doc.initForm();
const docData = logData(doc);
let opts = {
value: 32.98,
onClick: 'app.alert(1);',
format: {
type: 'number',
nDec: 2,
},
};
doc.formText('dollars', 20, 20, 50, 20, opts);
// The onClick action survives...
expect(docData[1]).toContain(
'/U <<\n/S /JavaScript\n/JS (app.alert\\(1\\);)\n>>',
);
// ...alongside the format-validation actions mapFormat() adds.
expect(docData[1]).toContain('/K <<\n/S /JavaScript');
expect(docData[1]).toContain('/F <<\n/S /JavaScript');
});

test('type flags do not leak implementation markers', () => {
doc.initForm();
const docData = logData(doc);
Expand Down
96 changes: 96 additions & 0 deletions types/acrobat-js.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* Minimal, best-effort types for the small subset of Adobe Acrobat's own
* JavaScript API commonly needed to write a form field's `onClick` action
* (see docs/forms.md). This is not a full Acrobat SDK type surface -- only
* the handful of globals most `onClick` handlers reach for. Contributions
* extending it are welcome.
*
* These aren't ambient/global declarations: `app`, `getField`, `display` and
* `event` only exist inside a PDF viewer's own JavaScript engine at the
* moment the action runs, never in the Node or browser code that builds the
* PDF, so declaring them as globals would risk colliding with unrelated
* identifiers elsewhere in a project (a bare global `event`, for example,
* collides with the DOM lib's own deprecated `window.event`).
*
* Instead, write `onClick` as a function that takes them as parameters --
* pdfkit calls the generated action with the real Acrobat globals in that
* position (and `this` bound to the Document, exactly as Acrobat itself
* binds it in any field action), so this works exactly like referencing
* them as globals would, without ever declaring one:
*
* import type { AcrobatOnClick } from 'pdfkit/types/acrobat-js';
*
* const onClick: AcrobatOnClick = function (app, getField, display) {
* app.alert('clicked');
* this.getField('otherField').value = 'updated from btn1';
* };
*
* Declare only the leading parameters your handler actually uses --
* `function (app) {}` or even `function () {}` are both valid AcrobatOnClick
* values. The call always passes all of them (`this`, `app`, `getField`,
* `display`, `event`, in that order); a handler that declares fewer simply
* never sees the rest, the same way `array.map(item => ...)` can ignore the
* `index` and `array` parameters its callback type also offers.
*
* (`this` isn't available in an arrow function, and Acrobat's own JS engine
* may not support arrow function syntax at all -- write `onClick` as a
* plain `function` for the widest viewer support.)
*/

/**
* Partial: Acrobat's real `app` object has many more methods (`execDialog`,
* `launchURL`, `response`, `thermometer`, ...). Only the ones common enough
* to include here are listed.
*/
export interface AcrobatApp {
alert(
message: string,
icon?: number,
type?: number,
title?: string,
): number;
execMenuItem(name: string): void;
}

/** Partial: a real field object has many more properties than these. */
export interface AcrobatField {
value: string | number;
display: number;
readonly: boolean;
hidden: boolean;
}

/** Complete: this is Acrobat's full, fixed set of `display` constants. */
export interface AcrobatDisplay {
visible: 0;
hidden: 1;
noPrint: 2;
noView: 3;
}

export type AcrobatGetField = (name: string) => AcrobatField;

/** Partial: a real field-action event object has more properties than these. */
export interface AcrobatEvent {
target: AcrobatField;
value: string | number;
rc: boolean;
willCommit: boolean;
}

/**
* Partial: the Document object Acrobat binds `this` to in any field action.
* A real Document has hundreds of members; only these two are declared here.
*/
export interface AcrobatDocument {
getField: AcrobatGetField;
numPages: number;
}

export type AcrobatOnClick = (
this: AcrobatDocument,
app: AcrobatApp,
getField: AcrobatGetField,
display: AcrobatDisplay,
event: AcrobatEvent,
) => void;