Technical Article

PDF Form Field Navigation in Delphi (PDFium Component)

The bug report comes with a screenshot: "Your tool filled the form, but every field in Acrobat is blank. When I click into a field, the value suddenly appears." The data is in the file — the screenshot even proves it — yet the form looks empty to everyone who receives it. This is the single most common defect in programmatic PDF form work, and it is not a bug in any library: it is what happens when field values are written without regenerating field appearances. Understanding why takes one section of the PDF specification; fixing it takes one method call. The examples below use PDFium Component, a PDFium-based VCL/LCL component for Delphi, C++Builder, and Lazarus, but the underlying file-format mechanics apply to any AcroForm tooling.

UK teams should align this pdfium component form field navigation workflow with local governance, audit, and data quality requirements before production release

For UK environments, align the pdfium component form field navigation implementation with local quality gates that include governance approval, versioned fixture baselines, and evidence retention for every publication candidate. Keep an explicit review log for accessibility, redaction policy, and data residency checks so your deployment audit is repeatable without changing output binaries

UK technical governance addendum

One field, two representations: /V and /AP

An AcroForm text field stores its value in the /V entry of the field dictionary (ISO 32000-1 §12.7.3.3). What viewers actually paint, however, is the widget's appearance stream — a small pre-rendered content stream stored under /AP (§12.5.5). Write /V without rebuilding /AP and the two diverge: the data exists, the picture of the data does not. Acrobat repaints a field's appearance when the field gains focus, which is exactly why clicking into a field 'reveals' the value in the bug report above.

The historical escape hatch, the NeedAppearances flag that asks viewers to regenerate appearances themselves, was never honored consistently across viewers and is deprecated in PDF 2.0 (ISO 32000-2). Print pipelines and thumbnail generators never honored it at all — they paint what /AP contains, which is nothing. The reliable contract is therefore: whoever writes the value also rebuilds the appearance.

Appearance generation is also where fonts and alignment make themselves felt. A regenerated stream lays the value out within the widget rectangle using the field's font, size, and quadding, which explains why a value that fits in your test form can clip or shrink in a customer's narrower copy of the same field. Auto-sized fields (font size zero) shrink text to fit; fixed-size fields clip. Both are legal outcomes, and the only way to know which one a given form produces is to inspect the regenerated output instead of the value you wrote — when a customer reports that text is cut off, this paragraph is typically the whole explanation.

Opening a form: FormFill, FormType, and the XFA question

Field access requires the form-fill subsystem, controlled by the FormFill property, to be enabled before the document is opened. Once active, FormType tells you what kind of form you are facing, and the answer changes the feature set you can promise:

Pdf.FileName := FormPath;
Pdf.FormFill := True;   // enable before Active; required for any field access
Pdf.Active := True;

case Pdf.FormType of
  ftNone:
    DisableFormPanel('This document has no interactive form');
  ftAcroForm:
    BuildFieldList;     // full field navigation and editing available
  ftXfaFull:
    ShowXfaNotice;      // XFA renders from its own XML template;
                        // treat field editing as limited
end;

Two practical notes. AcroForm is the standard ISO 32000 form model and the one every API in this article targets; XFA documents embed their own XML form architecture, and promising customers full XFA editing based on a quick AcroForm demo is a commitment you will regret. Secondly, FormFill also initialises document JavaScript — which is what you want in a data-entry viewer where calculation scripts keep totals current, and what you explicitly do not want in an untrusted-file preview. The secure PDF preview article covers the FormFill := False side of that trade-off.

Keyboard traversal users can predict

Data-entry users live on the Tab key, so field traversal has to behave like every other form they use. The focus API family — FocusFormField, FocusNextFormField, FocusPreviousFormField, FocusedFormFieldIndex, and ClearFormFieldFocus — moves the form focus without simulating mouse input:

procedure TFormViewer.HandleTabKey(Shift: TShiftState);
begin
  if ssShift in Shift then
    PdfView.FocusPreviousFormField
  else
    PdfView.FocusNextFormField;
  UpdateFieldStatus;  // e.g. "Field 4 of 17: InvoiceDate"
end;

Know the boundary behaviour: the traversal calls work through the current page's tab order and wrap around it — advancing past the last field returns to the first, and both functions return the new field index (or -1 when the page has no fields). Crossing to the next page is your decision: detect the wrap by comparing indices and advance PageNumber yourself if document-wide traversal is the goal. Pair the traversal with the OnFormFieldEnter event (and, on the viewer, OnFormFieldFocusChange) to keep a side panel synchronised with the document, and use the FormFieldAt indexed property when you need hit-testing — mapping a mouse position to a field value for tooltip previews or click-to-edit panels. Screen-reader users get the traversal benefit free of charge: focus moves through the document's own field order, so the path you build for the Tab key is also the path assistive technology follows.

For metadata-driven UIs, the FormFieldInfo[] property returns a TPdfFormFieldInfo record per index, which is how you label fields in a navigation list instead of showing bare index numbers. Radio groups deserve a regression file of their own here: several widgets share one field name, so a list built naively from widgets shows apparent duplicates that confuse users.

The fill-and-save sequence that survives Acrobat

Everything above converges on a three-step sequence whose middle step is the one teams skip:

procedure TFormViewer.FillAndSave(const Values: array of WString;
  const OutputPath: string);
var
  i: Integer;
begin
  for i := 0 to Pdf.FormFieldCount - 1 do
    Pdf.FormField[i] := Values[i];   // writes /V only

  // Rebuild the /AP appearance streams; without this the form
  // looks blank in Acrobat until each field is clicked
  Pdf.GenerateFormAppearances;

  Pdf.SaveAs(OutputPath);
end;

GenerateFormAppearances is the entire fix for the opening bug report. It rebuilds the widget appearance streams from the current values, fonts, and quadding, so every viewer — including ones that never execute focus events, like print servers and thumbnailers — paints the filled state. Call it once after the batch of assignments instead of per field; appearance generation touches fonts and layout, and per-field calls multiply that cost across large forms for no benefit.

Verification belongs in the definition of done: open the saved file in Acrobat and confirm values are visible without clicking any field, then print to PDF or image from a second viewer and confirm the values survive a pipeline that ignores form logic entirely. Those two checks together catch every variant of the /V-versus-/AP divergence.

Production forms that break clean implementations

A short list of field configurations that pass demo testing and fail with customer files:

  • Checkbox export values. The 'on' state is not universally Yes — forms define arbitrary export values, and writing the wrong one leaves the box visually unchecked whilst your code believes it succeeded.
  • Shared-name radio groups. One field, many widgets. Value assignment selects which widget shows as checked, and per-widget UI code that assumes one-name-one-rectangle draws the wrong focus ring.
  • Calculated fields. Totals computed by document JavaScript update on field events. A programmatic fill that bypasses events should either trigger recalculation or overwrite the calculated fields explicitly — shipping a form where line items and total disagree is worse than either option.
  • Hidden required fields. Conditional forms hide fields that remain marked required. Decide whether your validation honors visibility or the raw flag, and document the decision where support can find it.

FAQ

Why do my filled values only appear when a field is clicked?

The values were written to /V but the /AP appearance streams were never regenerated, so viewers paint the stale (empty) appearance until a focus event forces a rebuild. Call GenerateFormAppearances after assigning values and before SaveAs.

Does field navigation work on XFA forms?

Check FormType first. ftAcroForm gives you the full navigation and editing surface described here; ftXfaFull means the document renders from its own XML template and field-level interaction is limited. Detect and message it instead of letting users discover it.

Is flattening the same as generating appearances?

No. GenerateFormAppearances keeps fields interactive whilst making their values visible everywhere. Flattening converts the appearance into static page content and removes interactivity permanently — right for archival output, wrong for a form the next person must edit.

The form-fill subsystem, focus traversal, and appearance generation shown here are part of PDFium Component for Delphi, C++Builder, and Lazarus/FPC. If your viewer also handles reviewer markup alongside form data, the annotation review article covers that adjacent model.