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 holds | What to write to check it | Specification basis |
|---|---|---|
/Off and /Yes | Yes | The 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 included | Any name is legal; only Yes is recommended |
An on-state stream and no /Off at all | The on name to check, Off to clear | The off appearance is optional |
Field carries /Opt; states named 0, 1, 2 | The 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 name | That name, and every widget carrying it turns on together | Shared 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