Skip to content

v0.6.0 — AcroForm extraction + PdfDocument fill/save SDK - #5

Merged
rrader26 merged 1 commit into
feat/v0.5-ocr-backendsfrom
feat/v0.6-acroform-support
May 10, 2026
Merged

v0.6.0 — AcroForm extraction + PdfDocument fill/save SDK#5
rrader26 merged 1 commit into
feat/v0.5-ocr-backendsfrom
feat/v0.6-acroform-support

Conversation

@rrader26

Copy link
Copy Markdown
Contributor

Summary

PDFs with AcroForm fields are now first-class. They snapshot as `kind: 'form'` with each field exposed as an AgentMark action; the new `PdfDocument` SDK class lets agents fill, save, and flatten them with the same `execute()` shape as the web `Page` SDK.

Stacked on top of #4 (v0.5 OCR). Merge #2#3#4 → this PR in order.

What's new

AcroForm extraction

`convertPdf()` now automatically reads form fields and sets `kind: 'form'` when they exist. Each AcroForm field becomes an `ActionDefinition` with the right AgentMark type:

AcroForm AgentMark Notes
Text (single / multi-line) `type: 'type'` Sensitive names redacted
Checkbox `type: 'check'` Boolean coerced from PDF "Yes"/"Off"
Radio group `type: 'select'` Options included
Dropdown `type: 'select'` Options included
Listbox single `type: 'select'`
Listbox multi (`multipleSelection`) `type: 'multi_select'`
Signature `type: 'click'` (disabled) Refused — agents can't sign
Push button `type: 'click'`

PdfDocument SDK class

import { openPdfDocument } from '@thinkfleet/agentmark'

const doc = await openPdfDocument({ data, sourceUrl: 'file:///vendor.pdf' })
const snap = await doc.snapshot()                  // kind: 'form' with all actions

await doc.execute('act_field_1', 'Acme Inc.')       // text
await doc.execute('act_field_2', true)              // checkbox
await doc.execute('act_field_3', 'NC')              // dropdown
await doc.execute('act_field_4', ['English', 'Spanish'])  // multi-select

const filled = await doc.save()                     // new PDF bytes with values
const flattened = await doc.save({ flatten: true }) // bake values into page content

await doc.close()

Mirrors the web `Page` SDK so callers' agent loops are identical regardless of surface.

Field-flag handling

`Required` and `ReadOnly` are read from page widget annotations (where pdfjs-dist surfaces `fieldFlags`) since `getFieldObjects()` doesn't expose them in v4+.

Sensitive-name redaction

Field names matching common patterns (`password`, `ssn`, `credit_card`, `cvv`, `account_num`, `token`, `secret`, `csrf`, `session`, `auth`) get `(redacted)` labels and `undefined` values.

Humanized labels

`applicant.first_name` / `firstName` / `first-name` all → `"First Name"`.

Internal type rename

The internal extraction-result interface `PdfDocument` was renamed to `ExtractedPdf` to free the `PdfDocument` name for the new public class. Internal-only — no consumer code referenced the old name through the public API.

Optional peer dependency

`pdf-lib` added as optional peer. Reading uses `pdfjs-dist` (already installed); writing requires `pdf-lib`. Throws `SnapshotError` with install instructions if missing.

Tests

  • 12 new acroform-extractor tests (all field types, redaction, flags, humanization, schema compliance)
  • 11 new `PdfDocument` round-trip tests (fill → save → re-extract for every field type, error semantics, flatten, close idempotency)
  • 199 unit + 10 real-Chromium integration = 209 total (was 188)

Known limitations

  • `pdfjs-dist`'s `getFieldObjects()` reports only the first selected value of a multi-select listbox. AgentMark's saved PDF DOES contain all values correctly (verified directly via pdf-lib in the test suite); only the snapshot view under-reports. Wait for pdfjs upstream.
  • Form-structure inference for non-AcroForm PDFs (the insurance-renewal case) deferred.
  • Signature fields refuse fill — agents can't truly sign.

Test plan

🤖 Generated with Claude Code

PDFs with AcroForm fields are now first-class. They snapshot as
kind: 'form' with each field exposed as an AgentMark action; the new
PdfDocument SDK class lets agents fill, save, and flatten them with the
same execute() shape as the web Page SDK.

AcroForm extraction
- Reads fields via pdfjs-dist's getFieldObjects() and merges with page
  annotations to recover Required/ReadOnly flags (which getFieldObjects
  doesn't surface in pdfjs-dist v4+).
- Maps AcroForm field types to AgentMark action types:
    text (single + multi-line)  → type: 'type'
    text (password flag)         → type: 'type', label: '(redacted)'
    checkbox                     → type: 'check'
    radio group                  → type: 'select' with options
    dropdown (combo)             → type: 'select' with options
    listbox single               → type: 'select'
    listbox multi (multipleSelection) → type: 'multi_select'
    signature                    → type: 'click', disabled
    push button                  → type: 'click'
- Sensitive field-name redaction (password, ssn, credit_card, cvv,
  account_num, token, secret, csrf, session, auth) — labels become
  '(redacted)' and values are dropped.
- Field-name humanization: applicant.first_name / firstName /
  first-name all → "First Name".
- Action IDs synthesized as act_field_N to satisfy AgentMark schema
  regardless of source-name irregularity. Original field names
  preserved in the binding map for fill operations.
- Filters out parent fields (empty type with kidIds) — only leaf
  widgets with real metadata are processed.
- Dedicated unit-tested coercion for checkbox values (PDF's "Yes"/"Off"
  → boolean, fallback paths for other PDF generators).

PdfDocument SDK class (new public API)
- openPdfDocument({ data, sourceUrl, ... }) factory mirrors the web
  Page SDK shape so callers' agent loops are uniform.
- snapshot()                — capture current form state (cached)
- execute(actionId, value)  — queue a field value with type validation
- save({ flatten? })         — write a new PDF with all queued values
                              applied; flatten bakes values into the
                              page content (no longer fillable)
- reset()                   — discard queued values
- close()                   — release resources, idempotent
- fields, pending, snapshotCache — read-only accessors
- All execute() type checks throw the existing AgentMark error
  hierarchy: ActionTypeError, ActionDisabledError, ActionNotFoundError,
  ExecutionError. Read-only and signature fields auto-refused.
- Defensive copy of input bytes — multiple snapshot/save calls work.

Internal type rename
- The internal extraction-result interface PdfDocument was renamed to
  ExtractedPdf to free the PdfDocument name for the new public class.
  Internal-only — no consumer code depended on the old name through
  the public API surface.

Optional peer dependency
- pdf-lib added as an optional peer dep. Reading + extracting fields
  uses pdfjs-dist (already installed); writing requires pdf-lib.
  Surface a clean SnapshotError with install instructions if missing.

Tests (199 unit + 10 real-Chromium integration = 209 total)
- 12 new acroform-extractor tests (all field types, redaction,
  required/read-only flags, humanization, ID schema compliance).
- 11 new PdfDocument round-trip tests (fill → save → re-extract for
  every field type, error semantics, flatten path, close idempotency).

Known limitations (deferred)
- pdfjs-dist's getFieldObjects() reports only the first selected value
  for multi-select listboxes. AgentMark's saved PDF DOES contain all
  values (verified via direct pdf-lib reading) — only the snapshot
  view under-reports. Wait for pdfjs-dist upstream support.
- Form-structure inference for non-AcroForm PDFs (the FB renewal case
  — visual field labels but no AcroForm dictionary) is deferred to
  a later release.
- Signature fields surface as disabled actions; AgentMark refuses to
  fulfill them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants