rpdfium-doc 7676.6.4

Document-level features for rpdfium
Documentation
// Derived from PDFium's fpdf_formfill.h (FDF import/export concepts)
// Original: Copyright 2014 The PDFium Authors
// Licensed under BSD-3-Clause / Apache-2.0
// See pdfium-upstream/LICENSE for the original license.

//! FDF (Forms Data Format) import and export.
//!
//! Provides a lightweight in-memory representation of form field data that
//! can be exchanged between documents (ISO 32000-2 section 12.7.8).
//!
//! This implementation represents FDF as a Rust struct rather than as raw
//! FDF file bytes. It serves as a portable interchange format for form
//! field values.

use crate::error::DocResult;
use crate::form_field::{FieldValue, FormFieldType};
use crate::interactive_form::InteractiveForm;

/// In-memory FDF (Forms Data Format) data.
///
/// Contains a flat list of (field_name, value) pairs extracted from or
/// destined for an interactive form.
#[derive(Debug, Clone)]
pub struct FdfData {
    /// Field name / value pairs.
    pub fields: Vec<(String, FieldValue)>,
}

impl FdfData {
    /// Create an empty FDF dataset.
    pub fn new() -> Self {
        Self { fields: Vec::new() }
    }
}

impl Default for FdfData {
    fn default() -> Self {
        Self::new()
    }
}

/// Export all field values from an interactive form into FDF data.
///
/// Traverses the form tree and collects each field's current value.
/// Fields with no value set are skipped.
pub fn export_fdf(form: &InteractiveForm) -> FdfData {
    let all = form.all_fields();
    let mut fields = Vec::new();

    for field in all {
        if let Some(ref val_str) = field.value {
            let value = match field.field_type {
                FormFieldType::Text => FieldValue::String(val_str.clone()),
                FormFieldType::Button => {
                    let checked = field
                        .appearance_state
                        .as_deref()
                        .is_some_and(|s| s != "Off");
                    FieldValue::Bool(checked)
                }
                FormFieldType::Choice => FieldValue::String(val_str.clone()),
                FormFieldType::Signature => continue,
            };
            fields.push((field.name.clone(), value));
        }
    }

    FdfData { fields }
}

/// Import FDF data into an interactive form, updating matching fields.
///
/// For each entry in the FDF data, looks up the field by name and applies
/// the value. Fields not found in the form are silently skipped.
///
/// Returns the number of fields successfully updated.
pub fn import_fdf(form: &mut InteractiveForm, fdf: &FdfData) -> DocResult<usize> {
    let mut count = 0;

    for (name, value) in &fdf.fields {
        let field = match form.field_by_name_mut(name) {
            Some(f) => f,
            None => continue,
        };
        field.set_value(value.clone())?;
        count += 1;
    }

    Ok(count)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::form_field::{ChoiceOption, FormField, FormFieldFlags};

    fn make_text_field(name: &str, value: Option<&str>) -> FormField {
        FormField {
            name: name.to_string(),
            field_type: FormFieldType::Text,
            value: value.map(|s| s.to_string()),
            default_value: None,
            flags: FormFieldFlags::from_bits(0),
            tooltip: None,
            alternate_name: None,
            mapping_name: None,
            max_len: None,
            options: Vec::new(),
            appearance_state: None,
            children: Vec::new(),
            controls: Vec::new(),
            dirty: false,
            selected_indices: Vec::new(),
            additional_actions: None,
        }
    }

    fn make_button_field(name: &str, state: &str) -> FormField {
        FormField {
            name: name.to_string(),
            field_type: FormFieldType::Button,
            value: Some(state.to_string()),
            default_value: None,
            flags: FormFieldFlags::from_bits(0),
            tooltip: None,
            alternate_name: None,
            mapping_name: None,
            max_len: None,
            options: Vec::new(),
            appearance_state: Some(state.to_string()),
            children: Vec::new(),
            controls: Vec::new(),
            dirty: false,
            selected_indices: Vec::new(),
            additional_actions: None,
        }
    }

    fn make_choice_field(name: &str, options: &[&str], value: Option<&str>) -> FormField {
        FormField {
            name: name.to_string(),
            field_type: FormFieldType::Choice,
            value: value.map(|s| s.to_string()),
            default_value: None,
            flags: FormFieldFlags::from_bits(0),
            tooltip: None,
            alternate_name: None,
            mapping_name: None,
            max_len: None,
            options: options
                .iter()
                .map(|s| ChoiceOption {
                    export_value: s.to_string(),
                    display_value: s.to_string(),
                })
                .collect(),
            appearance_state: None,
            children: Vec::new(),
            controls: Vec::new(),
            dirty: false,
            selected_indices: Vec::new(),
            additional_actions: None,
        }
    }

    #[test]
    fn test_export_collects_all_fields() {
        let form = InteractiveForm {
            calculation_order: Vec::new(),
            default_appearance: None,
            default_alignment: crate::variable_text::Alignment::Left,
            fields: vec![
                make_text_field("name", Some("Alice")),
                make_button_field("agree", "Yes"),
                make_text_field("empty", None),
            ],
        };

        let fdf = export_fdf(&form);
        // "empty" field has no value, so should be skipped
        assert_eq!(fdf.fields.len(), 2);
        assert_eq!(fdf.fields[0].0, "name");
        assert_eq!(fdf.fields[0].1, FieldValue::String("Alice".to_string()));
        assert_eq!(fdf.fields[1].0, "agree");
        assert_eq!(fdf.fields[1].1, FieldValue::Bool(true));
    }

    #[test]
    fn test_import_updates_matching_fields() {
        let mut form = InteractiveForm {
            calculation_order: Vec::new(),
            default_appearance: None,
            default_alignment: crate::variable_text::Alignment::Left,
            fields: vec![
                make_text_field("name", Some("Alice")),
                make_text_field("email", Some("a@b.com")),
            ],
        };

        let fdf = FdfData {
            fields: vec![
                ("name".to_string(), FieldValue::String("Bob".to_string())),
                (
                    "email".to_string(),
                    FieldValue::String("bob@example.com".to_string()),
                ),
                (
                    "missing".to_string(),
                    FieldValue::String("skip".to_string()),
                ),
            ],
        };

        let count = import_fdf(&mut form, &fdf).unwrap();
        assert_eq!(count, 2);
        assert_eq!(
            form.field_by_name("name").unwrap().value.as_deref(),
            Some("Bob")
        );
        assert_eq!(
            form.field_by_name("email").unwrap().value.as_deref(),
            Some("bob@example.com")
        );
    }

    #[test]
    fn test_import_type_mismatch_returns_error() {
        let mut form = InteractiveForm {
            calculation_order: Vec::new(),
            default_appearance: None,
            default_alignment: crate::variable_text::Alignment::Left,
            fields: vec![make_text_field("name", Some("Alice"))],
        };

        let fdf = FdfData {
            fields: vec![("name".to_string(), FieldValue::Bool(true))],
        };

        let result = import_fdf(&mut form, &fdf);
        assert!(result.is_err());
    }

    #[test]
    fn test_roundtrip_export_import() {
        let form = InteractiveForm {
            calculation_order: Vec::new(),
            default_appearance: None,
            default_alignment: crate::variable_text::Alignment::Left,
            fields: vec![
                make_text_field("name", Some("Alice")),
                make_choice_field("color", &["Red", "Green", "Blue"], Some("Green")),
            ],
        };

        let fdf = export_fdf(&form);

        // Create a new form with empty values and import
        let mut new_form = InteractiveForm {
            calculation_order: Vec::new(),
            default_appearance: None,
            default_alignment: crate::variable_text::Alignment::Left,
            fields: vec![
                make_text_field("name", None),
                make_choice_field("color", &["Red", "Green", "Blue"], None),
            ],
        };

        let count = import_fdf(&mut new_form, &fdf).unwrap();
        assert_eq!(count, 2);
        assert_eq!(
            new_form.field_by_name("name").unwrap().value.as_deref(),
            Some("Alice")
        );
    }
}