Technical Article

Set Form Field Values in a Loaded PDF with Delphi

In the field list of almost any PDF viewer, the street field on a German-language claim form reads Straße. The bytes behind its /T entry tell a different story: <FEFF005300740072006100DF0065>, a hexadecimal UTF-16BE string with a byte order mark in front. Code that matches field names against those raw bytes reports the field as missing, even though a user can click straight into it. The same form may store a checkbox's on state as 2 instead of Yes and its country list as [export display] pairs, which gives you three separate ways for the value you write and the value a viewer paints to disagree

All three variations come from the same place: a form built years ago in whatever authoring tool the customer had, one your Delphi application now has to fill from a database and hand back as a file that renders the same in every reader. You cannot re-author the form, so the filling code has to follow the rules the form was written under. ISO 32000-1 has a rule for each of the three cases: §7.9.2.2 for text strings, §12.7.4.2.3 for button appearance states, Table 231 in §12.7.4.4 for choice selections. Creating fields that do not exist yet is a different job, covered in adding AcroForm fields to a loaded PDF in Delphi

Finding the field: HotPDF decodes /T before it matches names

THotPDF.SetFormFieldValue has two overloads, one taking a zero-based field index and one taking a fully qualified name, and both resolve against a name table the component builds from the AcroForm tree on first use. While that table is built, every partial name passes through a decoder. A hexadecimal /T string is turned back into bytes; if those bytes start with FE FF and have even length, the rest is read as UTF-16BE and re-encoded as UTF-8. Each decoded partial name is then joined to its parent with a period, exactly as §12.7.3.1 describes, so a kid field named City under a parent named Address is registered as Address.City. Only the lookup key is decoded. The /T object in the file keeps its hex spelling, and saving the document does not touch the identity of a field you merely filled

Keys are lower-cased before they go into the table, so 'address.city' finds Address.City. That goes beyond the standard, which treats field names as case-sensitive, and it means two fields differing only in case would collide in the lookup. For code that reads field names out of a config file someone typed by hand, a missed capital letter is a far more common failure than two fields in one form that differ only by case

How HotPDF resolves non-ASCII AcroForm names: the hexadecimal /T string is turned back into bytes, the FE FF byte order mark is decoded as UTF-16BE and re-encoded as UTF-8, and the qualified name joins its parent so Applicant.FullName and a field named Straße both land in the lookup table
Only the cache key changes: the field dictionary keeps its hexadecimal encoding, lookups normalize to lower case as a convenience beyond the standard, and saving the document never rewrites the identity of a field you merely filled

The decoded names are UTF-8 bytes carried in an AnsiString, and that is where Delphi's own string conversions get in the way. A literal such as 'Applicant.Straße' passed to an AnsiString parameter is converted through the system ANSI code page, so ß arrives as the single byte DF rather than the UTF-8 pair C3 9F, and the lookup misses. ASCII names are unaffected. For anything else, enumerate the fields once, decode each name with UTF8ToString, and write through the index overload:

// FindFieldIndex is a helper you add yourself: it compares the
// decoded UTF-8 names against a Unicode string, so no ANSI code
// page sits between your literal and the lookup
function FindFieldIndex(Pdf: THotPDF; const Name: string): Integer;
var
  I: Integer;
begin
  for I := 0 to Pdf.GetFormFieldCount - 1 do
    if SameText(UTF8ToString(Pdf.GetFormFieldName(I)), Name) then
      Exit(I);
  Result := -1;
end;

var
  Pdf: THotPDF;
  Street: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('claim-form.pdf') <= 0 then Exit;

    // ASCII names can go straight to the by-name overload
    Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');

    Street := FindFieldIndex(Pdf, 'Applicant.Straße');
    // The value is an AnsiString too, so a literal 'ß' would depend on
    // the system code page; FEFF hex (Hauptstraße 12) avoids that
    if Street >= 0 then
      Pdf.SetFormFieldValue(Street,
        'FEFF00480061007500700074007300740072006100DF0065002000310032');

    // The same FEFF-prefixed UTF-16BE hex works with the by-name
    // overload (this one is München); it is written as a hex string
    Pdf.SetFormFieldValue('Applicant.City', 'FEFF004D00FC006E006300680065006E');

    Pdf.SaveLoadedDocument('claim-form-filled.pdf');
  finally
    Pdf.Free;
  end;
end;

What SetFormFieldValue writes, step by step

Both overloads run the same sequence after the field is found: write /V, rebuild the choice selection index, mark the field changed, switch button appearance states, regenerate the text or choice appearance, and record the field in a dirty set. That last step feeds the parameterless RecalculateLoadedFormFieldsIncremental, which re-runs only the calculation scripts that read a changed field, directly or through a chain such as line item, subtotal, total

The object type of the new /V depends on the field. For a checkbox or radio button, SetFormFieldValue always writes a name object, because §12.7.4.2.3 stores the selected state as a name and PDF names are plain bytes with no text-string encoding. Every other field gets a string object, and the value you pass is inspected first. A string that starts with FEFF, has even length and contains only hex digits is taken to be the UTF-16BE form from §7.9.2.2 and serialized as <FEFF...>. That is what the City line above relies on. Anything else is stored as a literal string with exactly the bytes you supplied, so plain Latin text goes in as plain text

Why does a checkbox keep its old tick after the value changes?

For a button field the value does not decide what gets drawn. A checkbox widget carries an /AS entry that names which stream in /AP /N is currently showing, and viewers paint from /AS, not from /V. Change /V to Yes while /AS still says Off and the file contradicts itself; a flattener will bake the unchecked box into the page while the form data claims it is checked. SetFormFieldValue therefore visits the button field and every entry in its /Kids array, reads the on-state name from each widget's /AP /N, and sets /AS to that name when it matches the field value or to Off when it does not

Why a HotPDF checkbox keeps its old tick when only /V changes: viewers paint from the /AS appearance state into /AP /N, so SetFormFieldValue visits the field and every kid, reads the on-state name as the first key other than Off, and rewrites /AS on match or to Off otherwise
Radio groups compare each kid against the value inherited from the parent field through the /Parent chain, so setting the group to one export value turns exactly that widget on and every sibling off

Real forms bend that rule in two ways. The off appearance is named Off in the standard, yet authoring tools often leave its stream out entirely and let the viewer draw nothing, so a normal appearance dictionary with a single entry is perfectly legal. The on-state name is whatever the author picked: Yes, On, 2, a word in the form's own language. So the on state is taken as the first key in /AP /N that is not Off, and the comparison with your value ignores case. Radio buttons add the arrangement from §12.7.4.2.4: the selected value lives in /V on the parent field, while each kid widget usually has no /V of its own. Each kid is compared against the value inherited through its /Parent chain

// Checkbox: the value must equal the on-state key in /AP /N
// ('Yes' here; the form designer may have chosen '2' or 'On')
Pdf.SetFormFieldValue('Consent', 'Yes');

// Radio group: /V goes on the parent as a name; every kid widget
// gets /AS set to its own on-state name or to Off
Pdf.SetFormFieldValue('PaymentMethod', 'Card');

// Clearing a checkbox: a value that matches no on state yields /AS Off
Pdf.SetFormFieldValue('Newsletter', 'Off');

The flip side is that a typo produces no exception. 'yes ' with a trailing space matches nothing, so the box ends up Off and the file is still perfectly valid. If your values come from a database column, trim them before the call

Choice fields: keeping /I in step with /V

A combo box or list box records its selection in two places. Next to /V, Table 231 defines /I as an array of zero-based indices into /Opt, and a viewer that finds /I pointing at option 0 while /V names option 3 may highlight the wrong row. SetFormFieldValue rebuilds /I on every call to a choice field. It deletes the field's own /I entry before anything else and never edits the old array in place: an indirect array can be shared with another field, and mutating it would change that field's selection too. It then resolves /Opt through the /Parent chain, since options may be inherited, and walks the entries. A bare string option is compared directly, an [export display] pair on its export element, and a pair with fewer than two elements is skipped

How HotPDF keeps a choice field consistent: SetFormFieldValue deletes the local /I array before touching it, resolves /Opt through the /Parent chain, compares the export half of each option as decoded text, writes a one-element /I on the first match and writes nothing when an editable combo value has no index
A bare string option is compared directly and an export display pair on its export element, while a value outside /Opt correctly leaves no index — a stale /I pointing at the wrong row would be worse than none

The comparison runs on decoded text, so an option stored in PDFDocEncoding matches a value written as UTF-16BE hex and vice versa. A scalar value produces at most one index; whatever multi-selection the field held before is replaced. When nothing matches, no /I is written at all. For an editable combo box that is the correct outcome, because §12.7.4.4 lets the user type a value outside the list and such a value has no index to point at

The same rule is behind the most common complaint about combo boxes: the call succeeds, /V holds the new text, and the viewer shows nothing selected. Usually the caller passed the display label (Canada) to a list built from [CA Canada] pairs. The component cannot treat that as an error without breaking editable combos, so the check belongs in your code. GetFormFieldOptionValue returns the export element of each option, which makes a guard short:

// SetChoiceByExport is your own guard, not part of the component:
// it refuses values that match no export element in /Opt
procedure SetChoiceByExport(Pdf: THotPDF; const FieldName,
  ExportValue: AnsiString);
var
  I: Integer;
begin
  for I := 0 to Pdf.GetFormFieldOptionCount(FieldName) - 1 do
    if Pdf.GetFormFieldOptionValue(FieldName, I) = ExportValue then
    begin
      // /Opt is [[US United States] [CA Canada] [MX Mexico]]:
      // 'CA' matches the export element and /I becomes [1]
      Pdf.SetFormFieldValue(FieldName, ExportValue);
      Exit;
    end;
  raise Exception.CreateFmt('%s is not an export value of %s',
    [string(ExportValue), string(FieldName)]);
end;

// Editable combo with free text: write it directly. /V is set,
// /I is removed, and no index is invented
Pdf.SetFormFieldValue('Title', 'Principal Engineer');

Does the new value show up without /NeedAppearances?

Yes, for text and choice fields. A viewer draws the stored /AP stream and does not look at /V, so a new value with an old appearance leaves the old text on screen and in print unless the AcroForm dictionary carries /NeedAppearances true (§12.7.3.3) and the viewer chooses to honor it. Flatteners and thumbnail generators frequently ignore that flag. SetFormFieldValue regenerates the appearance stream for text and choice fields as part of the call, using the same builder as the public EnsureLoadedFieldAppearanceStream. The stream is built from the inherited /DA string, /Q alignment, /MaxLen for comb fields and the value itself. The font named in /DA is resolved through the AcroForm /DR resources, so a Type0 font keeps its own glyphs instead of falling back to Helvetica. Buttons keep the state appearances their designer drew; for them only /AS moves

SetFormFieldValue is a procedure and does not tell you whether anything was painted. A widget without a usable /Rect gets no stream, and broken forms with orphaned or rectangle-less widgets do exist. When the rendered result matters, call EnsureLoadedFieldAppearanceStream yourself after setting the value. It returns True when at least one widget received a stream. GetFormField hands you a THPDFLoadedFormField whose Index property is what the call expects; you own that object and must free it

var
  Field: THPDFLoadedFormField;
begin
  Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
  Field := Pdf.GetFormField('Applicant.FullName');
  try
    // False means no widget of this field had a rectangle to paint
    // into; the value is stored, but nothing will show it
    if not Pdf.EnsureLoadedFieldAppearanceStream(Field.Index) then
      raise Exception.Create('No widget rectangle to paint into');
  finally
    Field.Free;
  end;
  Pdf.SaveLoadedDocument('claim-form-filled.pdf');
end;

Checking that /V changed proves very little about what a user will see. A test that means something renders the page before and after and confirms that pixels inside the widget rectangle changed while pixels outside it did not. We keep that check because appearance bytes alone have misled us before: a stream held every right byte while a viewer honoring /NeedAppearances true rebuilt the display from /V and showed none of it

Reading values back, and what usually comes next

GetFormFieldValue returns the inherited /V of the field. A value stored as FEFF-prefixed hex comes back decoded to UTF-8, so the City field above reads back as the UTF-8 bytes of München and UTF8ToString turns it into a Delphi string. A literal string comes back with the bytes it was stored with. For round-trip checks, compare decoded text on both sides rather than the spelling you passed in

Moving field data in bulk between the form and an external system, instead of one call per field, is the subject of XFDF import and export in Delphi. Locking a finished form so nobody can edit it is flattening AcroForm and XFA fields in Delphi, which turns exactly the /AS states and appearance streams described here into static page content. Whatever is inconsistent at that point stays inconsistent for good, so fill, check, then flatten

SetFormFieldValue, EnsureLoadedFieldAppearanceStream and the incremental recalculation API are part of the HotPDF Delphi Component, next to the calls that rename, remove and flatten fields in a document you loaded from Delphi or C++Builder