Recursive AI

Fill a PDF form in C#

All onepdf recipes

Open the form with editing allowed, call SetValue or SetChecked with each field’s name, and save. The program below fills in the IRS Form W-9, a public fillable PDF you can download to try it.

Terminal
dotnet new console -o FillForm && cd FillForm
dotnet add package RecursiveAI.OnePdf --version 1.14.0
curl -LO https://www.irs.gov/pub/irs-pdf/fw9.pdf

Put this in Program.cs and run dotnet run:

C#
using OnePdf;
 
using PdfDocument doc = PdfDocument.Open("fw9.pdf",
    new PdfOpenOptions { AllowEditing = true });
PdfAcroForm form = doc.Form ?? throw new InvalidOperationException("No form here.");
 
// Fields are found by their full names. Every field of the W-9 starts with this.
const string page1 = "topmostSubform[0].Page1[0].";
 
// 1: name
form.SetValue(page1 + "f1_01[0]", "Ada Lovelace");
// 3a: individual
form.SetChecked(page1 + "Boxes3a-b_ReadOrder[0].c1_1[0]", true);
// 5: address
form.SetValue(page1 + "Address_ReadOrder[0].f1_07[0]", "100 Main Street");
form.SetValue(page1 + "Address_ReadOrder[0].f1_08[0]", "Springfield, IL 62701");
 
// A value that doesn't fit is refused, and nothing is changed.
try
{
    form.SetValue(page1 + "f1_11[0]", "1234");   // the first SSN box takes 3 digits
}
catch (PdfFormValueException e)
{
    Console.WriteLine($"Refused: {e.Reason}");
}
 
// The W-9 also carries an XFA copy of the form, which OnePDF doesn't fill.
// Remove it, so viewers such as Adobe Reader show the values set above.
form.Dictionary.RemoveEntry("XFA");
 
doc.Save("fw9-filled.pdf");
Console.WriteLine($"Name: {form.FindField(page1 + "f1_01[0]")!.Value}");
Text
Refused: TooLong
Name: Ada Lovelace

fw9-filled.pdf shows the name, the checked box and the address in any viewer, because OnePDF draws each filled field’s appearance and stores it in the file. It is still a form: anyone can change the values. To make them permanent, flatten it.

Find out which field is which

Field names are often codes like f1_01. List them, then fill every text field with its own name and open the copy: each box shows what to call it.

C#
using OnePdf;
 
using PdfDocument doc = PdfDocument.Open("fw9.pdf",
    new PdfOpenOptions { AllowEditing = true });
PdfAcroForm form = doc.Form!;
 
foreach (PdfFormField field in form.TerminalFields)
{
    string max = field.MaxLength is int n ? $", max {n}" : "";
    Console.WriteLine($"{field.FullName} ({field.Kind}{max})");
    if (field.Kind == PdfFieldKind.Text)
    {
        string label = field.PartialName.Replace("[0]", "");
        field.SetValue(label.Length <= (field.MaxLength ?? 99) ? label : "#");
    }
}
form.Dictionary.RemoveEntry("XFA");
doc.Save("fw9-names.pdf");
Text
topmostSubform[0].Page1[0].f1_01[0] (Text)
topmostSubform[0].Page1[0].f1_02[0] (Text)
topmostSubform[0].Page1[0].Boxes3a-b_ReadOrder[0].c1_1[0] (CheckBox)
topmostSubform[0].Page1[0].Boxes3a-b_ReadOrder[0].c1_1[1] (CheckBox)
topmostSubform[0].Page1[0].Boxes3a-b_ReadOrder[0].c1_1[2] (CheckBox)
topmostSubform[0].Page1[0].Boxes3a-b_ReadOrder[0].c1_1[3] (CheckBox)
topmostSubform[0].Page1[0].Boxes3a-b_ReadOrder[0].c1_1[4] (CheckBox)
topmostSubform[0].Page1[0].Boxes3a-b_ReadOrder[0].c1_1[5] (CheckBox)
topmostSubform[0].Page1[0].Boxes3a-b_ReadOrder[0].f1_03[0] (Text, max 1)
topmostSubform[0].Page1[0].Boxes3a-b_ReadOrder[0].c1_1[6] (CheckBox)
topmostSubform[0].Page1[0].Boxes3a-b_ReadOrder[0].f1_04[0] (Text)
topmostSubform[0].Page1[0].Boxes3a-b_ReadOrder[0].c1_2[0] (CheckBox)
topmostSubform[0].Page1[0].f1_05[0] (Text)
topmostSubform[0].Page1[0].f1_06[0] (Text)
topmostSubform[0].Page1[0].Address_ReadOrder[0].f1_07[0] (Text)
topmostSubform[0].Page1[0].Address_ReadOrder[0].f1_08[0] (Text)
topmostSubform[0].Page1[0].f1_09[0] (Text)
topmostSubform[0].Page1[0].f1_10[0] (Text)
topmostSubform[0].Page1[0].f1_11[0] (Text, max 3)
topmostSubform[0].Page1[0].f1_12[0] (Text, max 2)
topmostSubform[0].Page1[0].f1_13[0] (Text, max 4)
topmostSubform[0].Page1[0].f1_14[0] (Text, max 2)
topmostSubform[0].Page1[0].f1_15[0] (Text, max 7)

In fw9-names.pdf, box 1 reads “f1_01”, box 5 “f1_07” and so on. A check box or radio button is set with SetChecked(name, true), or with SetValue(name, state) where the state is one of the names in its widgets’ /AP /N dictionary, here 1 to 7.

What each kind of field takes

FieldCallValue
TextSetValue(name, text)Any text up to its maximum length; null clears it.
Check boxSetChecked(name, true)Or SetValue(name, state) with an on-state, or Off.
Radio groupSetValue(name, state)One of the group’s on-states.
Combo or list boxSetValue(name, choice)An option’s export value or display text.
Multi-select listSetValues(name, choices)Several options.

A refused value throws PdfFormValueException with a Reason: UnknownField, WrongType, ReadOnly, NotInOptions, TooLong, NotMultiSelect or NotTerminal. PdfFillOptions can fill read-only fields or cut text to the maximum length instead.

Limits

  • Scripts never run. Calculated fields aren’t recomputed and format scripts aren’t applied: values are stored exactly as you give them, so format dates and totals yourself.
  • XFA isn’t filled. OnePDF fills AcroForm fields. A form that is XFA only, with no AcroForm fields, can’t be filled. On hybrid forms like the W-9, remove the XFA as above.
  • No new fields. OnePDF fills the fields a form has; it doesn’t add fields.
  • Signature fields are signed, not filled: see Sign a PDF.

Every rule for each field type is in Forms, and how values are drawn in Appearance generation.