Technical Article

PDF Checkbox Set to Yes but Still Unchecked in Delphi

You call SetFormFieldValue with "Yes" on a consent checkbox, save the file, and the box is still empty. In that file the on state carries a different name, and PDFlibPas switches a check box or radio widget only to an appearance state the widget really defines, so read the names with GetFormFieldSubName and write one of them

Yes is only the name the specification suggests

A check box draws itself from the normal appearance dictionary of its widget, /AP /N, which holds one form XObject per appearance state, and the widget's /AS entry picks the state currently shown (ISO 32000-1 §12.5.2 Table 164, §12.5.5). ISO 32000-1 §12.7.4.2.3 fixes a single name: the off appearance, which is optional, goes under Off, while for the on state the text only says that Yes should be used. Producers read that should as the recommendation it is, so on states named On, 1, a word in the form's own language such as Ja, or whatever a designer typed into an export-value box are all legal, and /V holds the same name with nothing in the file mapping "Yes" onto it

PDFlibPas treats the state name as data owned by the file. SetFormFieldValue stores the name you pass in /V and moves /AS to it only when the widget's appearance dictionary has an entry under that name; otherwise /AS stays Off. The reason sits in §12.5.5 itself, which asks readers for reasonable behavior, such as displaying nothing, when /AS designates a state with no appearance, so pointing it at a missing stream leaves the outcome to each reader's idea of reasonable. What users then report is a split field: /V says Yes, the widget shows Off, and code that reads the value sees a checked box while every viewer shows an empty one. Never hardcode "Yes". It works on the forms you happen to test with and fails silently on the first file from another producer

What the widget appearance dictionary holdsWhat to write to check itSpecification basis
/Off and /YesYesThe recommended on-state name, ISO 32000-1 §12.7.4.2.3
/Off and any other name (On, 1, a localized word)That exact name, case includedAny name is legal; only Yes is recommended
An on-state stream and no /Off at allThe on name to check, Off to clearThe off appearance is optional
Field carries /Opt; states named 0, 1, 2The zero-based position of the widget in /Kids§12.7.4.2.3 and §12.7.4.2.4: with /Opt, on states may be widget indices
Several widgets sharing one on-state nameThat name, and every widget carrying it turns on togetherShared names behave as one choice; this is the case /Opt indices exist to separate

List the states before you write anything

GetFormFieldSubCount and GetFormFieldSubName enumerate the possible values of a check box or radio field. The list always starts with Off, even when the file omits the off stream, followed by every state name found in the normal, rollover and down appearance dictionaries of the field's widgets with duplicates removed; field indexes and sub-indexes are both 1-based. GetFormFieldAppearanceState reads /AS from the dictionary you point it at, so a radio parent with kid widgets returns an empty string, and GetFormFieldKidTempIndex hands you each widget for the same calls

program ListButtonStates;

{$APPTYPE CONSOLE}

uses
  System.SysUtils, PDFlibrary;

const
  FieldTypeCheckBox = 3;   // GetFormFieldType return values
  FieldTypeRadio    = 4;

procedure DumpStates(Lib: TPDFlib; Index: Integer; const Prefix: string);
var
  S: Integer;
  Names: string;
begin
  Names := '';
  for S := 1 to Lib.GetFormFieldSubCount(Index) do
    Names := Names + ' [' + Lib.GetFormFieldSubName(Index, S) + ']';
  Writeln(Prefix, 'states:', Names, '   /AS=', Lib.GetFormFieldAppearanceState(Index));
end;

var
  Lib: TPDFlib;
  I, K, FieldType: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('application.pdf', '') = 0 then
      Exit;
    for I := 1 to Lib.FormFieldCount do          // 1-based field index
    begin
      FieldType := Lib.GetFormFieldType(I);
      if (FieldType <> FieldTypeCheckBox) and (FieldType <> FieldTypeRadio) then
        Continue;
      Writeln(Lib.GetFormFieldTitle(I), '   /V=', Lib.GetFormFieldValue(I));
      DumpStates(Lib, I, '  ');
      for K := 1 to Lib.GetFormFieldKidCount(I) do
        DumpStates(Lib, Lib.GetFormFieldKidTempIndex(I, K), '    widget ' + IntToStr(K) + ' ');
    end;
  finally
    Lib.Free;
  end;
end.

// Expected for a box whose producer named the on state "On":
//   agree   /V=Off
//     states: [Off] [On]   /AS=Off
// Expected for a three-button group that carries /Opt:
//   payment   /V=Off
//     states: [Off] [0] [1] [2]   /AS=
//       widget 1 states: [Off] [0]   /AS=Off

How do you check a box or pick a radio button by name?

With the names in hand, checking a box is a lookup followed by a write. The helper below accepts a field only when it has a single on-state name, since a field with several is a group and the caller must say which widget it means. Clearing needs no lookup: Off is the one name the specification fixes, and writing it is correct even without an off stream. The closing comparison against /AS catches the mistake from the opening paragraph, so keep it in production code, where it costs one call; a false result means the write reached /V but no widget can display it

function SingleOnState(Lib: TPDFlib; Index: Integer): WideString;
var
  S: Integer;
  Name: WideString;
begin
  Result := '';
  for S := 1 to Lib.GetFormFieldSubCount(Index) do
  begin
    Name := Lib.GetFormFieldSubName(Index, S);
    if Name = 'Off' then
      Continue;
    if Result <> '' then
      Exit('');              // several on states: a group, choose a widget instead
    Result := Name;
  end;
end;

function SetCheckBox(Lib: TPDFlib; const Title: WideString; Checked: Boolean): Boolean;
var
  Index, Probe: Integer;
  OnName: WideString;
begin
  Result := False;
  Index := Lib.FindFormFieldByTitle(Title);     // 0 when the title is not found
  if Index = 0 then
    Exit;
  if Checked then
    OnName := SingleOnState(Lib, Index)
  else
    OnName := 'Off';
  if (OnName = '') or (Lib.SetFormFieldValue(Index, OnName) <> 1) then
    Exit;                    // no usable on state, or the field is read-only
  if Lib.GetFormFieldKidCount(Index) > 0 then
    Probe := Lib.GetFormFieldKidTempIndex(Index, 1)
  else
    Probe := Index;
  Result := Lib.GetFormFieldAppearanceState(Probe) = OnName;
end;

function SelectRadioWidget(Lib: TPDFlib; GroupIndex, WidgetNo: Integer): Boolean;
var
  Kid: Integer;
  StateName: WideString;
begin
  Result := False;
  Kid := Lib.GetFormFieldKidTempIndex(GroupIndex, WidgetNo);   // WidgetNo is 1-based
  if Kid = 0 then
    Exit;
  StateName := SingleOnState(Lib, Kid);
  if StateName <> '' then
    Result := Lib.SetFormFieldRadioWidgetValue(Kid, StateName) = 1;
end;

Radio groups add a trap of their own through /Opt. When a button field carries it, each entry is the export text of one widget, while the appearance states, and therefore /V, may use the widget's zero-based position in /Kids as the name (§12.7.4.2.3, §12.7.4.2.4). Write the export text "Card" and no widget has a state called Card, so every button goes to Off. Write the position and you are counting from zero over /Kids order, which the specification does not tie to left-to-right placement on the page, one step away from a 1-based sub-index list that starts with Off: sub-index 2 is state 0, the first widget

The index names exist because, without them, two widgets with the same export value share one state name and setting the field lights both. When the PDFlibPas team worked through the button rules for the library's form validation, the obvious shortcut was to accept an /Opt export value as an alias for the state name; the specification defines no such alias, so the library maps names only through the appearance dictionaries. SetFormFieldRadioWidgetValue, used in SelectRadioWidget above, addresses one widget instead of a name: it declines a state that widget cannot draw, turns that widget on and its siblings off, and lights same-named siblings only when the group sets the RadiosInUnison flag

Why do /I and /V disagree in a list box?

When they disagree, /V wins, and the disagreement alone is not a defect: ISO 32000-1 §12.7.4.4 describes /I as a sorted array of zero-based indices into /Opt, meant for multi-select lists and for options that share an export value, and says that when /I and /V name different items, the /V entry shall be used. For export and display pairs, both ISO 32000-1 and ISO 32000-2 describe /V in terms of the display element while producers commonly store the export value, and PDFlibPas follows the producers: SetFormFieldValue matches against the export values that GetFormFieldSubName returns (GetFormFieldSubDisplayName gives the visible text), takes line-break-separated values for a multi-select list, and rewrites /V and /I together. GetFormFieldChoiceSelectedIndices reports the index array a multi-select list carries and otherwise derives indices from the value, which keeps the cross-check below short. Leave lists with duplicate export values alone, because there /I is the only thing telling the two options apart

// uses System.Classes, System.SysUtils, PDFlibrary
const
  FieldTypeChoice = 5;

function IndicesFromValue(Lib: TPDFlib; Index: Integer): string;
var
  Values: TStringList;
  S, V: Integer;
begin
  Result := '';
  Values := TStringList.Create;
  try
    Values.Text := Lib.GetFormFieldValue(Index);   // multi-select values are CR-separated
    for S := 1 to Lib.GetFormFieldSubCount(Index) do
      for V := 0 to Values.Count - 1 do
        if Lib.GetFormFieldSubName(Index, S) = Values[V] then
        begin
          if Result <> '' then
            Result := Result + ',';
          Result := Result + IntToStr(S - 1);      // /I indices are zero-based
          Break;
        end;
  finally
    Values.Free;
  end;
end;

function HasDuplicateOptions(Lib: TPDFlib; Index: Integer): Boolean;
var
  A, B: Integer;
begin
  Result := False;
  for A := 1 to Lib.GetFormFieldSubCount(Index) do
    for B := A + 1 to Lib.GetFormFieldSubCount(Index) do
      if Lib.GetFormFieldSubName(Index, A) = Lib.GetFormFieldSubName(Index, B) then
        Exit(True);
end;

procedure ResyncChoiceFields(Lib: TPDFlib);
var
  I: Integer;
  FromV, FromI: string;
begin
  for I := 1 to Lib.FormFieldCount do
  begin
    if Lib.GetFormFieldType(I) <> FieldTypeChoice then
      Continue;
    FromV := IndicesFromValue(Lib, I);
    FromI := Lib.GetFormFieldChoiceSelectedIndices(I);
    if FromV = FromI then
      Continue;
    Writeln(Lib.GetFormFieldTitle(I), ': /V selects [', FromV, '], /I says [', FromI, ']');
    if not HasDuplicateOptions(Lib, I) then
      Lib.SetFormFieldValue(I, Lib.GetFormFieldValue(I));   // /V wins; /I is rebuilt from it
  end;
end;

/DA is a fragment of a content stream, and its font lives in /DR

The default appearance string is not a style description. ISO 32000-1 §12.7.3.3 defines /DA as content-stream operators spliced inside a text object when the field's appearance is built, so only operators legal inside BT/ET belong there, a /DA with path operators or its own BT is malformed, and the string must contain at least a Tf with font and size, where a size of 0 means auto-size. The font operand is a resource name that must resolve in the /Font dictionary of the AcroForm's /DR. /DA and /Q are inheritable, so the string governing a widget may sit on a parent field or on the AcroForm dictionary as the document default, which is also why fields that end up under one parent after a merge pick up each other's defaults, as covered in the article on duplicate field names when merging PDF forms

A font that changes after a refresh is that resolution failing. PDFlibPas resolves the conventional standard-font abbreviations such as Helv even when the file never declared them; for any other name that /DR does not define, UpdateAppearanceStream draws with Helvetica and writes /Helv into the field's own /DA, keeping the size and color operands, so the declared default and the drawn appearance agree afterward. GetFormFieldFontName is the diagnostic: it follows the inheritance chain to the governing /DA, looks the name up in /DR and returns the base font name, or an empty string when the lookup fails, which also flags an undeclared /Helv that renders here but still breaks the §12.7.3.3 rule

procedure RepairFieldFonts(Lib: TPDFlib);
const
  FieldTypeText   = 1;
  FieldTypeChoice = 5;
var
  I, FieldType, FontID, FormFont: Integer;
begin
  FormFont := 0;
  for I := 1 to Lib.FormFieldCount do
  begin
    FieldType := Lib.GetFormFieldType(I);
    if (FieldType <> FieldTypeText) and (FieldType <> FieldTypeChoice) then
      Continue;
    if Lib.GetFormFieldFontName(I) <> '' then
      Continue;                              // /DA resolves to a declared /DR font
    Writeln(Lib.GetFormFieldTitle(I), ': /DA font is not declared in /DR');
    if FormFont = 0 then
    begin
      FontID := Lib.AddTrueTypeFont('Arial', 1);   // embedded
      if FontID = 0 then
        Exit;
      FormFont := Lib.AddFormFont(FontID);   // returns the /DR font count, so the new entry's index
    end;
    if (FormFont > 0) and (Lib.SetFormFieldFont(I, FormFont) = 1) then
      Lib.UpdateAppearanceStream(I);
  end;
end;

For bulk filling, call SetNeedAppearances(1), change the values, and call RegenerateDirtyFormAppearances before saving; PDFlibPas rebuilds only the widgets that changed, and one whose appearance cannot be generated stays pending with NeedAppearances left true. Do not ship a file that leans on that flag. PDF 2.0 deprecates relying on it, PDF/A forbids it, and a PDF/A viewer renders the appearance streams already in the file instead of rebuilding them from /V. Flattening works the same way and keeps only the appearance /AS selects, as the guide to flattening PDF fields and widget placement explains, so a box whose /V says Yes and whose /AS says Off flattens into an empty box for good

Edition boundaries that look like corruption

ISO 32000-1 §12.7.4.2.2 says a pushbutton has no value and prohibits /V and /DV on it, but the PDF 2.0 errata delete that prohibition, so a pushbutton with a value is legal in PDF 2.0 and questionable only under PDF/A-1 to PDF/A-3, which keep the older rule. A text field's /V may likewise be a text stream instead of a string, as it has been since PDF 1.5, and GetFormFieldValue decodes either form

Values that arrive through FDF or XFDF import into existing forms meet the same state names, so run the state inventory against the target form before mapping an exported Yes onto a box that the PDFlibPas Delphi PDF Library will find named On