Skip to main content

rpdfium_doc/
fdf.rs

1// Derived from PDFium's fpdf_formfill.h (FDF import/export concepts)
2// Original: Copyright 2014 The PDFium Authors
3// Licensed under BSD-3-Clause / Apache-2.0
4// See pdfium-upstream/LICENSE for the original license.
5
6//! FDF (Forms Data Format) import and export.
7//!
8//! Provides a lightweight in-memory representation of form field data that
9//! can be exchanged between documents (ISO 32000-2 section 12.7.8).
10//!
11//! This implementation represents FDF as a Rust struct rather than as raw
12//! FDF file bytes. It serves as a portable interchange format for form
13//! field values.
14
15use crate::error::DocResult;
16use crate::form_field::{FieldValue, FormFieldType};
17use crate::interactive_form::InteractiveForm;
18
19/// In-memory FDF (Forms Data Format) data.
20///
21/// Contains a flat list of (field_name, value) pairs extracted from or
22/// destined for an interactive form.
23#[derive(Debug, Clone)]
24pub struct FdfData {
25    /// Field name / value pairs.
26    pub fields: Vec<(String, FieldValue)>,
27}
28
29impl FdfData {
30    /// Create an empty FDF dataset.
31    pub fn new() -> Self {
32        Self { fields: Vec::new() }
33    }
34}
35
36impl Default for FdfData {
37    fn default() -> Self {
38        Self::new()
39    }
40}
41
42/// Export all field values from an interactive form into FDF data.
43///
44/// Traverses the form tree and collects each field's current value.
45/// Fields with no value set are skipped.
46pub fn export_fdf(form: &InteractiveForm) -> FdfData {
47    let all = form.all_fields();
48    let mut fields = Vec::new();
49
50    for field in all {
51        if let Some(ref val_str) = field.value {
52            let value = match field.field_type {
53                FormFieldType::Text => FieldValue::String(val_str.clone()),
54                FormFieldType::Button => {
55                    let checked = field
56                        .appearance_state
57                        .as_deref()
58                        .is_some_and(|s| s != "Off");
59                    FieldValue::Bool(checked)
60                }
61                FormFieldType::Choice => FieldValue::String(val_str.clone()),
62                FormFieldType::Signature => continue,
63            };
64            fields.push((field.name.clone(), value));
65        }
66    }
67
68    FdfData { fields }
69}
70
71/// Import FDF data into an interactive form, updating matching fields.
72///
73/// For each entry in the FDF data, looks up the field by name and applies
74/// the value. Fields not found in the form are silently skipped.
75///
76/// Returns the number of fields successfully updated.
77pub fn import_fdf(form: &mut InteractiveForm, fdf: &FdfData) -> DocResult<usize> {
78    let mut count = 0;
79
80    for (name, value) in &fdf.fields {
81        let field = match form.field_by_name_mut(name) {
82            Some(f) => f,
83            None => continue,
84        };
85        field.set_value(value.clone())?;
86        count += 1;
87    }
88
89    Ok(count)
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use crate::form_field::{ChoiceOption, FormField, FormFieldFlags};
96
97    fn make_text_field(name: &str, value: Option<&str>) -> FormField {
98        FormField {
99            name: name.to_string(),
100            field_type: FormFieldType::Text,
101            value: value.map(|s| s.to_string()),
102            default_value: None,
103            flags: FormFieldFlags::from_bits(0),
104            tooltip: None,
105            alternate_name: None,
106            mapping_name: None,
107            max_len: None,
108            options: Vec::new(),
109            appearance_state: None,
110            children: Vec::new(),
111            controls: Vec::new(),
112            dirty: false,
113            selected_indices: Vec::new(),
114            additional_actions: None,
115        }
116    }
117
118    fn make_button_field(name: &str, state: &str) -> FormField {
119        FormField {
120            name: name.to_string(),
121            field_type: FormFieldType::Button,
122            value: Some(state.to_string()),
123            default_value: None,
124            flags: FormFieldFlags::from_bits(0),
125            tooltip: None,
126            alternate_name: None,
127            mapping_name: None,
128            max_len: None,
129            options: Vec::new(),
130            appearance_state: Some(state.to_string()),
131            children: Vec::new(),
132            controls: Vec::new(),
133            dirty: false,
134            selected_indices: Vec::new(),
135            additional_actions: None,
136        }
137    }
138
139    fn make_choice_field(name: &str, options: &[&str], value: Option<&str>) -> FormField {
140        FormField {
141            name: name.to_string(),
142            field_type: FormFieldType::Choice,
143            value: value.map(|s| s.to_string()),
144            default_value: None,
145            flags: FormFieldFlags::from_bits(0),
146            tooltip: None,
147            alternate_name: None,
148            mapping_name: None,
149            max_len: None,
150            options: options
151                .iter()
152                .map(|s| ChoiceOption {
153                    export_value: s.to_string(),
154                    display_value: s.to_string(),
155                })
156                .collect(),
157            appearance_state: None,
158            children: Vec::new(),
159            controls: Vec::new(),
160            dirty: false,
161            selected_indices: Vec::new(),
162            additional_actions: None,
163        }
164    }
165
166    #[test]
167    fn test_export_collects_all_fields() {
168        let form = InteractiveForm {
169            calculation_order: Vec::new(),
170            default_appearance: None,
171            default_alignment: crate::variable_text::Alignment::Left,
172            fields: vec![
173                make_text_field("name", Some("Alice")),
174                make_button_field("agree", "Yes"),
175                make_text_field("empty", None),
176            ],
177        };
178
179        let fdf = export_fdf(&form);
180        // "empty" field has no value, so should be skipped
181        assert_eq!(fdf.fields.len(), 2);
182        assert_eq!(fdf.fields[0].0, "name");
183        assert_eq!(fdf.fields[0].1, FieldValue::String("Alice".to_string()));
184        assert_eq!(fdf.fields[1].0, "agree");
185        assert_eq!(fdf.fields[1].1, FieldValue::Bool(true));
186    }
187
188    #[test]
189    fn test_import_updates_matching_fields() {
190        let mut form = InteractiveForm {
191            calculation_order: Vec::new(),
192            default_appearance: None,
193            default_alignment: crate::variable_text::Alignment::Left,
194            fields: vec![
195                make_text_field("name", Some("Alice")),
196                make_text_field("email", Some("a@b.com")),
197            ],
198        };
199
200        let fdf = FdfData {
201            fields: vec![
202                ("name".to_string(), FieldValue::String("Bob".to_string())),
203                (
204                    "email".to_string(),
205                    FieldValue::String("bob@example.com".to_string()),
206                ),
207                (
208                    "missing".to_string(),
209                    FieldValue::String("skip".to_string()),
210                ),
211            ],
212        };
213
214        let count = import_fdf(&mut form, &fdf).unwrap();
215        assert_eq!(count, 2);
216        assert_eq!(
217            form.field_by_name("name").unwrap().value.as_deref(),
218            Some("Bob")
219        );
220        assert_eq!(
221            form.field_by_name("email").unwrap().value.as_deref(),
222            Some("bob@example.com")
223        );
224    }
225
226    #[test]
227    fn test_import_type_mismatch_returns_error() {
228        let mut form = InteractiveForm {
229            calculation_order: Vec::new(),
230            default_appearance: None,
231            default_alignment: crate::variable_text::Alignment::Left,
232            fields: vec![make_text_field("name", Some("Alice"))],
233        };
234
235        let fdf = FdfData {
236            fields: vec![("name".to_string(), FieldValue::Bool(true))],
237        };
238
239        let result = import_fdf(&mut form, &fdf);
240        assert!(result.is_err());
241    }
242
243    #[test]
244    fn test_roundtrip_export_import() {
245        let form = InteractiveForm {
246            calculation_order: Vec::new(),
247            default_appearance: None,
248            default_alignment: crate::variable_text::Alignment::Left,
249            fields: vec![
250                make_text_field("name", Some("Alice")),
251                make_choice_field("color", &["Red", "Green", "Blue"], Some("Green")),
252            ],
253        };
254
255        let fdf = export_fdf(&form);
256
257        // Create a new form with empty values and import
258        let mut new_form = InteractiveForm {
259            calculation_order: Vec::new(),
260            default_appearance: None,
261            default_alignment: crate::variable_text::Alignment::Left,
262            fields: vec![
263                make_text_field("name", None),
264                make_choice_field("color", &["Red", "Green", "Blue"], None),
265            ],
266        };
267
268        let count = import_fdf(&mut new_form, &fdf).unwrap();
269        assert_eq!(count, 2);
270        assert_eq!(
271            new_form.field_by_name("name").unwrap().value.as_deref(),
272            Some("Alice")
273        );
274    }
275}