Fill, read, flatten, and convert PDF forms

A practical guide for developers using the PDFluent SDK to handle both AcroForm and XFA-based PDF forms.

Fill PDF form fields in Rust

Set text field values, check checkboxes, select radio buttons, and choose dropdown options programmatically in any AcroForm PDF.

rust
use pdfluent::prelude::*;

fn main() -> pdfluent::Result<()> {
    let mut doc = PdfDocument::open("application.pdf")?;

    if doc.form_fields()?.is_empty() {
        eprintln!("This PDF has no form fields.");
        return Ok(());
    }

    let mut form = doc.form_mut();
    form.set_text("first_name", "Jane")?
        .set_text("last_name", "Smith")?
        .set_text("email", "jane@example.com")?;
    form.set_checkbox("agree_terms", true)?
        .set_checkbox("newsletter", false)?
        .set_radio("payment_method", "credit_card")?;

    doc.save("filled-application.pdf")?;
    Ok(())
}

Complete program, compiled against the published crate: Code on GitHub →

The same code, typed and run on screen. Watch on YouTube

  1. Open the form PDF

    Open the PDF with a mutable binding. Use form_fields() to check whether the document has any form fields before trying to fill them.

    rust
    use pdfluent::prelude::*;
    
    let mut doc = PdfDocument::open("application.pdf")?;
    
    if doc.form_fields()?.is_empty() {
        eprintln!("This PDF has no form fields.");
        return Ok(());
    }
  2. Get a mutable form handle

    Call form_mut() to get a PdfFormMut value that lets you write field values. The accessor is infallible — errors surface on the individual set_* calls when a field does not exist or has a different type.

    rust
    let mut form = doc.form_mut();
  3. Set text field values

    Use set_text() with the field name as it appears in the PDF. Field names are case-sensitive. The setters chain via Result<&mut Self>.

    rust
    form.set_text("first_name", "Jane")?
        .set_text("last_name", "Smith")?
        .set_text("email", "jane@example.com")?;
  4. Set checkboxes and radio buttons

    Pass true or false to set_checkbox(). For radio buttons, pass the export value of the option you want selected. PDFluent writes the field value (/V) correctly. Note: in 1.0 the widget appearance state (/AS) on kid annotations is not yet synced — viewers that honour /AS may show stale visual state until they rebuild appearance from /V.

    rust
    form.set_checkbox("agree_terms", true)?
        .set_checkbox("newsletter", false)?
        // Radio button: pass the export value, not the display label
        .set_radio("payment_method", "credit_card")?;
  5. Save the filled form

    Drop the form handle scope and call save() on the document. flatten_forms() is on the 1.0 surface but currently returns Error::MissingDependency — the flatten runtime is tracked for a 1.x MINOR.

    rust
    doc.save("application_filled.pdf")?;
  • Field names are the partial names of top-level AcroForm fields. In 1.0 only the top-level /Fields array is addressable; fully-qualified names like "Address.Street" that require walking /Kids are not yet supported.
  • Use doc.form_fields() to list every form field with its name, type, and current value.
  • Multiline text fields accept newline characters ("\n") in the value string.
  • set_text() and set_dropdown() encode values with lopdf::text_string — ASCII values use PDFDocEncoding, non-ASCII values switch to UTF-16BE with BOM so Unicode round-trips correctly.
  • Kid-widget checkbox and radio appearance sync (/AS on /Kids[*]) is tracked as a 1.1 follow-up; today /V is written correctly but the visual state may lag in viewers that don't rebuild appearances from /V.

Read PDF form field values in Rust

List all form fields in a PDF and read their current values. Covers text fields, checkboxes, radio buttons, dropdowns, and list boxes.

rust
use pdfluent::PdfDocument;

fn main() -> pdfluent::Result<()> {
    let doc = PdfDocument::open("form.pdf")?;
    for field in doc.form_fields()? {
        println!("{} = {} ({:?})", field.name, field.value, field.field_type);
    }
    Ok(())
}
  1. Open the PDF

    Open the document and check that it contains an AcroForm.

    rust
    use pdfluent::PdfDocument;
    
    let doc = PdfDocument::open("application.pdf")?;
    if doc.form_fields()?.is_empty() {
        println!("No form fields.");
        return Ok(());
    }
  2. Get the form handle

    Call doc.form() to get an immutable PdfForm handle.

    rust
    let fields = doc.form_fields()?;
    println!("{} form field(s)", fields.len());
  3. List all fields

    doc.form_fields() returns a slice of FormField values. Each entry has a name, field_type, and value.

    rust
    for field in doc.form_fields()? {
        println!("name: {}", field.name);
        println!("type: {:?}", field.field_type);
        println!("value: {:?}", field.value);
        println!();
    }
  4. Read a specific field by name

    Use form.field("name") to get a single field. Returns None if the field is not found.

    rust
    if let Some(f) = doc.form_fields()?.iter().find(|f| f.name == "email") {
        println!("Email: {}", f.value);
    }
  5. Export all field values to a HashMap

    Use form.values_as_map() to get all field names and their string representations in one call. Useful for logging or serialising form data.

    rust
    use std::collections::HashMap;
    
    let values: HashMap<String, String> = doc
        .form_fields()?
        .into_iter()
        .map(|f| (f.name, f.value))
        .collect();
    
    for (name, val) in &values {
        println!("{} = {}", name, val);
    }
  • FormField.value is a FieldValue enum. Match on its variants: Text, Checked, Selected, and None.
  • Hierarchical field names use a dot separator, e.g. "address.city". doc.form_fields() returns the full dotted name.
  • Read-only and hidden fields are included in the field list. Check field.is_read_only and field.is_hidden.

Flatten PDF form fields in Rust

Convert interactive form fields into static page content. Flattening bakes the current field values into the page so they cannot be edited.

rust
use pdfluent::PdfDocument;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut doc = PdfDocument::open("filled_form.pdf")?;
    doc.flatten_form()?;
    doc.save("flattened.pdf")?;
    println!("Form fields removed, values baked into page content.");
    Ok(())
}
  1. Open the filled form PDF

    Open the document that has form fields with values already set. Flattening works on whatever values are currently in the fields.

    rust
    use pdfluent::PdfDocument;
    
    let mut doc = PdfDocument::open("filled_application.pdf")?;
  2. Flatten all form fields

    Call flatten_form() to convert every field into static page content. The AcroForm dictionary is removed from the document.

    rust
    doc.flatten_form()?;
  3. Flatten only specific fields

    To flatten a subset of fields while leaving others editable, use flatten_forms() with a list of field names.

    rust
    doc.flatten_fields(&["first_name", "last_name", "date_signed"])?;
  4. Verify the form is gone

    After flattening, !doc.form_fields()?.is_empty() returns false and the page text includes the values that were in the fields.

    rust
    assert!(doc.form_fields()?.is_empty());
    let text = doc.page(1)?.text()?;
    assert!(text.contains("Jane Smith"));
  5. Save the flattened document

    Save to a new file to keep the original editable version intact.

    rust
    doc.save("application_final.pdf")?;
  • Flattening is irreversible. Keep the original filled-form PDF if you might need to re-extract the field values later.
  • Invisible fields (fields with no value and no visible appearance) are simply removed, not rendered.
  • Some complex field appearances use JavaScript to generate their display. PDFluent renders the last known appearance stream, not the JS output.

Fill XFA dynamic form fields with data in Rust

Write field values into the XFA datasets packet to populate a dynamic XFA form with application data.

rust
use pdfluent::{PdfDocument, XfaFieldValue};

fn main() -> pdfluent::Result<()> {
    let mut doc = PdfDocument::open("blank_form.pdf")?;

    doc.set_xfa_field_value("form1.subform1.firstName", XfaFieldValue::Text("Alice"))?;
    doc.set_xfa_field_value("form1.subform1.lastName", XfaFieldValue::Text("Dupont"))?;
    doc.set_xfa_field_value("form1.subform1.dob", XfaFieldValue::Text("1985-06-15"))?;

    doc.save("filled_form.pdf")?;
    Ok(())
}
  1. Open the blank XFA form

    Open the template form file for mutation. The XFA structure must already exist; PDFluent cannot create an XFA template from scratch.

    rust
    let mut doc = PdfDocument::open("blank_form.pdf")?;
    
    if !doc.has_xfa_form() {
        eprintln!("this document carries no XFA form");
    }
  2. List the fields the form actually has

    xfa_form_model() parses the template and the saved form state into a flat list. Each XfaField carries both the dotted name and the fully-indexed SOM path, and set_xfa_field_value accepts either.

    rust
    let model = doc.xfa_form_model()?;
    
    for field in &model.fields {
        println!(
            "{}  type={:?}  value={:?}  read_only={}",
            field.name, field.field_type, field.value, field.read_only,
        );
    }
  3. Set individual field values

    Values are typed. XfaFieldValue::Text covers text, numeric, date/time and dropdown fields; Checkbox takes a bool; Radio takes the on-value of the member to select. Field paths are dot-separated and case-sensitive.

    rust
    use pdfluent::XfaFieldValue;
    
    doc.set_xfa_field_value("form1.personal.firstName", XfaFieldValue::Text("Alice"))?;
    doc.set_xfa_field_value("form1.personal.newsletter", XfaFieldValue::Checkbox(true))?;
    doc.set_xfa_field_value("form1.personal.title", XfaFieldValue::Radio("Ms"))?;
  4. Fill multiple fields from a map

    Each call applies immediately and returns an XfaSetOutcome. persisted_to_datasets tells you whether the value survives a save and reopen -- it is false only for bind="none" fields, which live in the form state and not in the datasets packet.

    rust
    use pdfluent::XfaFieldValue;
    use std::collections::HashMap;
    
    let mut values: HashMap<&str, &str> = HashMap::new();
    values.insert("form1.address.street", "123 Main St");
    values.insert("form1.address.city", "Amsterdam");
    values.insert("form1.address.postcode", "1234AB");
    
    for (path, value) in &values {
        let outcome = doc.set_xfa_field_value(path, XfaFieldValue::Text(value))?;
        if !outcome.persisted_to_datasets {
            eprintln!("{path} was set but is not bound to the datasets packet");
        }
    }
  5. Save the filled form

    The modified datasets XML is written back into the PDF. Save to a new file to preserve the original blank template.

    rust
    doc.save("filled_form.pdf")?;
  • XFA field paths are case-sensitive. "form1.Name" and "form1.name" are different paths.
  • Date fields expect ISO 8601 format (YYYY-MM-DD) by default. Check the field binding in the XFA template if a different format is required.
  • Adding a new instance of a repeated subform is not part of the fill surface. You can set values on the subform instances the template already carries; creating a new table row is form authoring, which PDFluent does not do.
  • XFA forms are rendered by the PDF viewer at display time. Saving filled data does not produce a static appearance; the viewer renders from the template and data. Call flatten_xfa() if you need a document that looks the same everywhere.

Extract data from an XFA PDF form in Rust

Read the XFA data packet from a dynamic XFA form and access field values as typed Rust data.

rust
use pdfluent::PdfDocument;

fn main() -> pdfluent::Result<()> {
    let doc = PdfDocument::open("form.pdf")?;

    let xfa = doc.xfa().ok_or(pdfluent::Error::NoXfa)?;
    let datasets = xfa.datasets()?;

    // Read a field value by its fully-qualified name
    let first_name = datasets.field_value("form1.subform1.firstName")?;
    println!("First name: {}", first_name.as_str().unwrap_or(""));

    Ok(())
}
  1. Open the PDF and access the XFA root

    doc.has_xfa_form() returns true when the PDF carries an XFA form. doc.xfa_form_model()? then returns an XfaFormModel with the layout page count and the fields in document order; it returns Error::Unsupported when the document has no XFA form.

    rust
    let doc = PdfDocument::open("form.pdf")?;
    let xfa = doc.xfa().ok_or(pdfluent::Error::NoXfa)?;
  2. Access the datasets packet

    XFA forms store submitted data in the xfa:datasets XML packet. xfa.datasets() parses that XML into a queryable tree.

    rust
    let datasets = xfa.datasets()?;
  3. Read a field value by name

    Field names are dot-separated paths from the root node. The path mirrors the XFA form template hierarchy.

    rust
    let val = datasets.field_value("form1.subform1.firstName")?;
    match val {
        pdfluent::xfa::FieldType::Str(s)  => println!("string: {}", s),
        pdfluent::xfa::FieldType::Date(d) => println!("date: {:?}", d),
        pdfluent::xfa::FieldType::Num(n)  => println!("number: {}", n),
        pdfluent::xfa::FieldType::Empty   => println!("(empty)"),
    }
  4. Iterate all field nodes

    Use datasets.fields() to get every leaf node in the data tree.

    rust
    for field in datasets.fields() {
        println!("{} = {:?}", field.path(), field.value());
    }
  5. Export the raw datasets XML

    If you need the raw XML for custom processing, access the bytes directly.

    rust
    let xml_bytes = datasets.to_xml_bytes()?;
    std::fs::write("form_data.xml", &xml_bytes)?;
  • XFA forms come in two variants: static XFA (fixed layout) and dynamic XFA (auto-layout). Both use the same data model. PDFluent parses both.
  • The XFA template and datasets are separate XML streams. Modifying datasets without updating the template rendering may produce inconsistent results.
  • Adobe Reader is the primary renderer for dynamic XFA. Most other viewers (Foxit, Chrome PDF) do not support dynamic XFA fully.
  • XFA is deprecated in PDF 2.0. New forms should use AcroForm instead. PDFluent supports both for reading existing documents.