Skip to content

Form annotations: TU has no mapping and structParent is dropped, so form fields cannot be made PDF/UA compliant #1803

Description

@Robert-Krueger

Bug Report

Description of the problem

0.20.0 restricted AcroForm options to documented mappings. Three things a tagged, PDF/UA-compliant form field needs became unreachable through the public API. All three worked in 0.19.1 through the documented passthrough, so this is a regression rather than a feature request:

v0.19.1 docs/forms.md — "Consult the PDF Reference and set these attributes in the options object. Any options that are not listed above will be added directly to the corresponding PDF Object."

v0.20.2's docs/forms.md asks that unsupported options be raised as issues, which is what this is.

1. TU has no mapping. PDF/UA-1 requires an alternate field name on form fields. mapStrings recognises only VALUE_MAP = { value: 'V', defaultValue: 'DV' } plus label → MK.CA, so no option produces TU.

2. structParent is dropped between formAnnotation and annotate. annotate() already accepts and consumes structParent — it destructures it and calls structParent.add(new PDFAnnotationReference(ref)). But formAnnotation() passes annotate() the dictionary returned by _fieldDict(), which builds a fresh object, so a structParent given to formText() never reaches annotate(). The widget is therefore never nested in its Form structure element.

This one looks like plumbing rather than an intentional removal: the capability exists on both sides, only the hand-off is missing.

3. fontSize is documented but silently ignored when the field font matches the form's default font. docs/forms.md lists it as a common option — "Sets the font size used in the field appearance string. The default, 0, means auto sizing." _mapFont only writes DA when _acroform.defaultFont !== _font.name, so a field using the same font as initForm ignores fontSize and inherits the form-level /DA … 0 Tf, i.e. auto sizing. Nothing documents that condition.

Together, (1) and (2) make a form field fail PDF/UA-1 validation with no supported workaround. veraPDF 1.28.2 (ua1) on the output of the sample below reports these three form-related failures:

7.18.1-3  A form field shall have a TU key present or all its Widget annotations
          shall have alternative descriptions
7.18.4-1  A Widget annotation shall be nested within a Form tag
7.18.4-2  If the Form element omits a Role attribute, it shall have only one child:
          an object reference identifying the widget annotation

(The minimal sample also fails 7.1-9, 7.1-10 and 5-1, because it omits the document title, viewer preferences and the PDF/UA identifier. Those are artifacts of keeping the sample short and are unrelated to this report. In our real document, which sets all of them, the three above are the only failures.)

Code sample

import PDFDocument from 'pdfkit';
import { writeFileSync } from 'node:fs';

const doc = new PDFDocument({ pdfVersion: '1.7', tagged: true, autoFirstPage: false });
const chunks = [];
doc.on('data', (c) => chunks.push(c));
doc.on('end', () => {
  const pdf = Buffer.concat(chunks);
  writeFileSync('out.pdf', pdf);
  const s = pdf.toString('latin1');
  console.log('has /TU :', /\/TU\s*\(/.test(s));
  console.log('/S /Form:', /\/S\s*\/Form[\s\S]{0,200}?\/K\s*(\[[^\]]*\])/.exec(s)?.[1]);
});

doc.addPage();
doc.font('Helvetica');
doc.initForm();

const formStruct = doc.struct('Form', { title: 'Contract number' });
doc.addStructure(formStruct);

doc.font('Helvetica').fontSize(9);
doc.formText('contractNumber', 100, 100, 120, 12, {
  align: 'right',
  fontSize: 9,                                  // (3) ignored: field font == default font
  TU: new String('Contract No.'),               // (1) dropped
  structParent: formStruct,                     // (2) dropped
});

doc.end();

Prints:

has /TU : false
/S /Form: []

Expected /TU (Contract No.) and a /K holding an /OBJR that references the widget.

To isolate (3), change the field's font to one other than the form's default — doc.font('Helvetica-Bold') before formText — and /DA (/F2 9 Tf 0 g) appears. With Helvetica on both, the only /DA in the file is the form-level /DA (/F1 0 Tf 0 g).

Your environment

  • pdfkit version: 0.20.2 (the AcroForm change landed in 0.20.0)
  • Node version: 24.18.0
  • Browser version (if applicable): n/a — Node
  • Operating System: macOS 27.0
  • Validator: veraPDF 1.28.2, profile ua1

Proposed fix

For (1), a documented option mapping to TU. The PDF spec calls it the alternate field name, so alternateName would match the existing naming style (defaultValue, backgroundColor); tooltip is what people are more likely to search for. Happy to follow whichever you prefer — it is one more entry in VALUE_MAP.

For (2), forward options.structParent onto the dictionary formAnnotation() hands to annotate(). annotate() needs no change.

For (3), I have not proposed a patch, because always emitting DA would change output for existing users who pass fontSize with a matching font, and I do not know whether the defaultFont !== _font.name guard is deliberate. Reporting it rather than guessing.

I am happy to open a pull request for (1) and (2) if the approach and the option name sound right.

Context

We generate a tagged, PDF/UA-1 vehicle sales contract with a single text field on AWS Lambda, and validate it in CI with veraPDF. On 0.19.1 the field is compliant; on 0.20.2 it is not, with no supported way to fix it. We are holding at 0.19.1 for now.

Separately, we independently hit #1801 before finding it already reported — same TypeError: Invalid URL from a CJS esbuild bundle, also via AWS CDK's NodejsFunction. Mentioning it only as a second data point; it is a different bug from this one, and either alone is enough to keep us on 0.19.1.


Disclosure: this issue was written with an AI coding assistant (Claude Code). The reproduction above was run, not only written.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions