Per-product direction: AcroForm widget to Documenso field creation
should not happen automatically on upload. It must be a deliberate,
opt-in action on a draft envelope.
- Revert AcroForm extraction from create-envelope (route) and
create-envelope-items upload paths. They no longer thread
acroFormFields into envelope items or run the extractor.
- Stop flattening on upload (flattenForm: false) so widgets survive
in the stored PDF until the user opts in.
- New tRPC mutation envelope.field.importFromPdf is the single entry
point. It loads each item's stored PDF, extracts widgets, creates
Field rows assigned to the first signable recipient (creating a
placeholder Recipient 1 SIGNER when none exist), flattens the PDF
in place, swaps documentDataId, and emits FIELD_CREATED audit log
entries on DOCUMENT envelopes.
- Editor fields panel gains an "Import from PDF form" button next to
"Detect with AI", gated to DRAFT envelopes. Success toasts the
count and revalidates the editor.
- Rewrite acroform-import.spec.ts e2e to the new flow: upload
preserves widgets and creates zero fields; service call creates
fields, flattens PDF, audits, and cleans up old DocumentData.
- Invert four DOCUMENT-upload assertions in form-flattening.spec.ts
to match the new preserve-widgets, no-auto-flatten contract.
Template and template-to-doc flatten behavior is unchanged.
Eight findings from PR review of feat/acroform-field-import, all verified
against actual source in plan mode before fixing.
P1.1 — getTextFieldFormatHint never produced a hint in practice. PdfDict.get()
returns PdfRef for indirect entries (Adobe almost always emits /AA and /F as
indirect), and String(js) returned "[object Object]" because PdfString and
PdfStream don't override toString. Now we thread a RefResolver via
PDF.context.resolve through every dict lookup, use PdfDict.getDict(key,
resolver) so refs are auto-deref'd, and decode JS bodies via asString() (for
strings) or TextDecoder + getDecodedData() (for streams). The MaxLen probe had
the same bug — also fixed via getNumber(key, resolver). Branch ordering is
restructured so format actions take precedence over every name token, not
just within their own type bucket.
P1.2 — Signed-signature path had no test. Added a stub-based mock test that
drives extractAcroFormFieldsFromPDF against a SignatureField whose isSigned()
returns true and asserts hasSignedSignature: true, fields: [], and an
unsupported entry with reason: 'signed-signature'. Mirrored with a negative
control where isSigned() returns false.
P2.1 — hasXfa reached into PDFForm._acroForm (private readonly) via a cast.
Replaced with public catalog access: pdf.context.catalog.getDict() →
getDict('AcroForm', resolver) → has('XFA'). Removed the
fields.length === 0 short-circuit so pure-XFA docs with no /Fields surface
as xfa-hybrid instead of falling through.
P2.2 — When no signable recipient resolved, the AcroForm branch threw
AppError(NOT_FOUND) inside prisma.$transaction and tore down the entire
envelope creation. Replaced with logger.warn + early skip from the AcroForm
branch only. Matches UNSAFE_createEnvelopeItems' silent-skip behaviour.
P2.6 — Coverage gaps closed with 9 new test cases: encrypted (mock), xfa-
hybrid (mock), signed signature (mock + negative control), listbox unsupported
(mock), no-page-match (mock), format-action precedence (extends fixture),
/TU label fallback (extends fixture), required CHECKBOX (extends fixture
with Ff bit 2), hidden + off-page widgets (extends fixture).
P2.3 / P2.4 / P2.5 — Plan doc updated to match shipped implementation: rot=180
y formula corrected from `top` to `bottom`, heuristic regexes documented as
the lenient substring patterns actually shipped (with explicit false-positive
acknowledgements), signed-signature detection mechanism updated from raw /V
probe to SignatureField.isSigned().
Verification: 26/26 unit tests pass (was 17, +9). lib + trpc + remix
typecheck clean (lib retains 5 pre-existing unrelated errors). biome clean.
Detect AcroForm widgets (text, checkbox, radio, dropdown, signature) at upload
time and reuse their geometry as Documenso fields instead of stripping them via
form.flatten(). Imported fields land in the editor as ordinary Field rows
assigned to the first signable recipient, removing the manual re-placement step
users hit when preparing PDFs in Adobe Acrobat.
Extraction runs before normalizePdf so widget geometry is still readable.
Text fields go through a name+format heuristic that maps DATE/NUMBER/EMAIL/
NAME/INITIALS/TEXT, with AcroForm /AA format actions taking precedence over
name tokens. Coordinates are converted via per-rotation transforms (0/90/180/
270) against the rendered page dimensions; widgets fully off-page are
dropped, partial overlap is clamped. Signed signatures (SignatureField.
isSigned()) are detected and skip both the import and the form flatten so
the signature stays valid. Encrypted PDFs, XFA hybrids, malformed PDFs, and
internal extractor errors all return an empty result with skipReason set so
the upload proceeds untouched.
Every imported field carries fieldMeta.source = 'acroform' (new optional on
ZBaseFieldMeta) for future provenance queries. DOCUMENT envelopes emit a
per-field FIELD_CREATED audit entry matching create-envelope-fields.ts.
Recipient assignment picks the first Recipient with role SIGNER or APPROVER
sorted by (signingOrder asc nulls last, id asc); when no signable recipient
exists, a placeholder Recipient 1 SIGNER is created mirroring the
placeholder-pipeline behaviour.
## Description
Corrected API endpoint path from /api/v2/documents to /api/v2/document
The current example in the docs(/api/v2/documents) returns a 404
NOT_FOUND object.
Uploaded .docx files are converted to PDF on the server using a
Gotenberg
sidecar before entering the normal envelope pipeline. The feature is
opt-in via NEXT_PRIVATE_DOCUMENT_CONVERSION_URL; when unset, only PDF
uploads are accepted.
A per-process circuit breaker opens for 30s after a conversion failure
to shed load.
Ships a dev Dockerfile that layers Microsoft Core Fonts and additional
language fonts
onto the upstream Gotenberg image for better fidelity.
Co-authored-by: Ephraim Duncan
<55143799+ephraimduncan@users.noreply.github.com>
Co-authored-by: Ephraim Duncan <55143799+ephraimduncan@users.noreply.github.com>