Skip to main content

pdfplumber_core/
form_field.rs

1//! PDF form field types for AcroForm extraction.
2//!
3//! Provides [`FormField`] and [`FieldType`] for representing PDF interactive
4//! form fields (AcroForms) such as text inputs, checkboxes, dropdowns, and
5//! signature fields.
6
7use crate::BBox;
8
9/// The type of a PDF form field.
10///
11/// Corresponds to the `/FT` entry in a field dictionary (PDF 1.7 Table 220).
12#[derive(Debug, Clone, PartialEq, Eq)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14pub enum FieldType {
15    /// Text field (`/FT /Tx`) — accepts text input.
16    Text,
17    /// Button field (`/FT /Btn`) — checkboxes, radio buttons, push buttons.
18    Button,
19    /// Choice field (`/FT /Ch`) — dropdowns, list boxes.
20    Choice,
21    /// Signature field (`/FT /Sig`) — digital signature.
22    Signature,
23}
24
25impl FieldType {
26    /// Parse a field type from its PDF name string.
27    ///
28    /// Returns `None` if the string is not a recognized field type.
29    pub fn from_pdf_name(name: &str) -> Option<Self> {
30        match name {
31            "Tx" => Some(Self::Text),
32            "Btn" => Some(Self::Button),
33            "Ch" => Some(Self::Choice),
34            "Sig" => Some(Self::Signature),
35            _ => None,
36        }
37    }
38
39    /// Return the PDF name string for this field type.
40    pub fn as_pdf_name(&self) -> &'static str {
41        match self {
42            Self::Text => "Tx",
43            Self::Button => "Btn",
44            Self::Choice => "Ch",
45            Self::Signature => "Sig",
46        }
47    }
48}
49
50impl std::fmt::Display for FieldType {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        match self {
53            Self::Text => write!(f, "Text"),
54            Self::Button => write!(f, "Button"),
55            Self::Choice => write!(f, "Choice"),
56            Self::Signature => write!(f, "Signature"),
57        }
58    }
59}
60
61/// A PDF form field extracted from the document's AcroForm dictionary.
62///
63/// Represents a single interactive form field with its name, type, value,
64/// and visual position on the page.
65#[derive(Debug, Clone, PartialEq)]
66#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
67pub struct FormField {
68    /// Field name from `/T` entry. Hierarchical fields join names with `.`.
69    pub name: String,
70    /// Field type from `/FT` entry.
71    pub field_type: FieldType,
72    /// Current value from `/V` entry.
73    pub value: Option<String>,
74    /// Default value from `/DV` entry.
75    pub default_value: Option<String>,
76    /// Bounding box from `/Rect` entry.
77    pub bbox: BBox,
78    /// Options for choice fields from `/Opt` entry.
79    pub options: Vec<String>,
80    /// Field flags from `/Ff` entry (bitmask).
81    pub flags: u32,
82    /// The 0-based page index this field belongs to, if determinable.
83    pub page_index: Option<usize>,
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn field_type_from_pdf_name_text() {
92        assert_eq!(FieldType::from_pdf_name("Tx"), Some(FieldType::Text));
93    }
94
95    #[test]
96    fn field_type_from_pdf_name_button() {
97        assert_eq!(FieldType::from_pdf_name("Btn"), Some(FieldType::Button));
98    }
99
100    #[test]
101    fn field_type_from_pdf_name_choice() {
102        assert_eq!(FieldType::from_pdf_name("Ch"), Some(FieldType::Choice));
103    }
104
105    #[test]
106    fn field_type_from_pdf_name_signature() {
107        assert_eq!(FieldType::from_pdf_name("Sig"), Some(FieldType::Signature));
108    }
109
110    #[test]
111    fn field_type_from_pdf_name_unknown() {
112        assert_eq!(FieldType::from_pdf_name("Unknown"), None);
113    }
114
115    #[test]
116    fn field_type_as_pdf_name() {
117        assert_eq!(FieldType::Text.as_pdf_name(), "Tx");
118        assert_eq!(FieldType::Button.as_pdf_name(), "Btn");
119        assert_eq!(FieldType::Choice.as_pdf_name(), "Ch");
120        assert_eq!(FieldType::Signature.as_pdf_name(), "Sig");
121    }
122
123    #[test]
124    fn field_type_display() {
125        assert_eq!(format!("{}", FieldType::Text), "Text");
126        assert_eq!(format!("{}", FieldType::Button), "Button");
127        assert_eq!(format!("{}", FieldType::Choice), "Choice");
128        assert_eq!(format!("{}", FieldType::Signature), "Signature");
129    }
130
131    #[test]
132    fn form_field_text_with_value() {
133        let field = FormField {
134            name: "full_name".to_string(),
135            field_type: FieldType::Text,
136            value: Some("John Doe".to_string()),
137            default_value: None,
138            bbox: BBox::new(50.0, 100.0, 200.0, 120.0),
139            options: vec![],
140            flags: 0,
141            page_index: Some(0),
142        };
143        assert_eq!(field.name, "full_name");
144        assert_eq!(field.field_type, FieldType::Text);
145        assert_eq!(field.value.as_deref(), Some("John Doe"));
146        assert!(field.default_value.is_none());
147        assert!(field.options.is_empty());
148        assert_eq!(field.flags, 0);
149        assert_eq!(field.page_index, Some(0));
150    }
151
152    #[test]
153    fn form_field_checkbox() {
154        let field = FormField {
155            name: "agree".to_string(),
156            field_type: FieldType::Button,
157            value: Some("Yes".to_string()),
158            default_value: Some("Off".to_string()),
159            bbox: BBox::new(30.0, 200.0, 50.0, 220.0),
160            options: vec![],
161            flags: 0,
162            page_index: Some(0),
163        };
164        assert_eq!(field.field_type, FieldType::Button);
165        assert_eq!(field.value.as_deref(), Some("Yes"));
166        assert_eq!(field.default_value.as_deref(), Some("Off"));
167    }
168
169    #[test]
170    fn form_field_dropdown_with_options() {
171        let field = FormField {
172            name: "country".to_string(),
173            field_type: FieldType::Choice,
174            value: Some("US".to_string()),
175            default_value: None,
176            bbox: BBox::new(50.0, 300.0, 200.0, 320.0),
177            options: vec!["US".to_string(), "UK".to_string(), "FR".to_string()],
178            flags: 0,
179            page_index: Some(0),
180        };
181        assert_eq!(field.field_type, FieldType::Choice);
182        assert_eq!(field.options.len(), 3);
183        assert_eq!(field.options[0], "US");
184        assert_eq!(field.options[2], "FR");
185    }
186
187    #[test]
188    fn form_field_with_no_value() {
189        let field = FormField {
190            name: "email".to_string(),
191            field_type: FieldType::Text,
192            value: None,
193            default_value: None,
194            bbox: BBox::new(50.0, 400.0, 200.0, 420.0),
195            options: vec![],
196            flags: 0,
197            page_index: None,
198        };
199        assert!(field.value.is_none());
200        assert!(field.page_index.is_none());
201    }
202
203    #[test]
204    fn form_field_signature() {
205        let field = FormField {
206            name: "signature".to_string(),
207            field_type: FieldType::Signature,
208            value: None,
209            default_value: None,
210            bbox: BBox::new(100.0, 500.0, 300.0, 600.0),
211            options: vec![],
212            flags: 0,
213            page_index: Some(1),
214        };
215        assert_eq!(field.field_type, FieldType::Signature);
216    }
217
218    #[test]
219    fn form_field_with_flags() {
220        let field = FormField {
221            name: "readonly_field".to_string(),
222            field_type: FieldType::Text,
223            value: Some("Cannot edit".to_string()),
224            default_value: None,
225            bbox: BBox::new(50.0, 100.0, 200.0, 120.0),
226            options: vec![],
227            flags: 1, // ReadOnly
228            page_index: Some(0),
229        };
230        assert_eq!(field.flags, 1);
231    }
232
233    #[test]
234    fn form_field_clone_and_eq() {
235        let field1 = FormField {
236            name: "test".to_string(),
237            field_type: FieldType::Text,
238            value: Some("val".to_string()),
239            default_value: None,
240            bbox: BBox::new(0.0, 0.0, 100.0, 20.0),
241            options: vec![],
242            flags: 0,
243            page_index: Some(0),
244        };
245        let field2 = field1.clone();
246        assert_eq!(field1, field2);
247    }
248}