Skip to main content

rust_pdf_box/interactive/
form.rs

1use crate::cos::object::{COSDictionary, COSObject, COSString, ObjectId};
2use crate::error::{PdfError, PdfResult};
3use crate::pd::document::PDDocument;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub enum FieldType {
7    Text,
8    Button, // Checkbox or Pushbutton
9    Choice,
10    Signature,
11    Unknown(String),
12}
13
14/// Represents an AcroForm interactive form field.
15#[derive(Debug, Clone)]
16pub struct PDFormField {
17    pub name: String,
18    pub field_type: FieldType,
19    pub value: Option<String>,
20    pub field_id: ObjectId,
21}
22
23/// Interactive AcroForm management and form flattening.
24#[derive(Debug, Clone)]
25pub struct PDAcroForm {
26    pub acro_form_dict: COSDictionary,
27}
28
29impl PDAcroForm {
30    /// Loads the AcroForm from the document catalog if present.
31    pub fn from_doc(doc: &PDDocument) -> Option<Self> {
32        let catalog = doc.cos_doc.get_catalog()?;
33        let acro_form = catalog.get_dict("AcroForm")?.clone();
34        Some(Self {
35            acro_form_dict: acro_form,
36        })
37    }
38
39    /// Lists all form fields in the document.
40    pub fn fields(&self, doc: &PDDocument) -> Vec<PDFormField> {
41        let mut result = Vec::new();
42        if let Some(fields_arr) = self.acro_form_dict.get_array("Fields") {
43            for item in fields_arr.iter() {
44                if let Some(id) = item.as_reference() {
45                    if let Some(COSObject::Dictionary(d)) = doc.cos_doc.get_object(id) {
46                        let name = d
47                            .get_string("T")
48                            .map(|s| s.to_string_lossy())
49                            .unwrap_or_default();
50
51                        let ft_str = d.get_name("FT").unwrap_or("");
52                        let field_type = match ft_str {
53                            "Tx" => FieldType::Text,
54                            "Btn" => FieldType::Button,
55                            "Ch" => FieldType::Choice,
56                            "Sig" => FieldType::Signature,
57                            other => FieldType::Unknown(other.to_string()),
58                        };
59
60                        let value = d.get("V").map(|v| match v {
61                            COSObject::String(s) => s.to_string_lossy(),
62                            COSObject::Name(n) => n.as_str().to_string(),
63                            _ => String::new(),
64                        });
65
66                        result.push(PDFormField {
67                            name,
68                            field_type,
69                            value,
70                            field_id: id,
71                        });
72                    }
73                }
74            }
75        }
76        result
77    }
78
79    /// Sets the value of a field by its fully-qualified name (/T).
80    pub fn set_field_value(
81        &mut self,
82        doc: &mut PDDocument,
83        name: &str,
84        value: &str,
85    ) -> PdfResult<()> {
86        let fields = self.fields(doc);
87        for f in fields {
88            if f.name == name {
89                if let Some(obj) = doc.cos_doc.get_object_mut(f.field_id) {
90                    if let Some(dict) = obj.as_dict_mut() {
91                        dict.set("V", COSString::literal(value.as_bytes().to_vec()));
92                        return Ok(());
93                    }
94                }
95            }
96        }
97        Err(PdfError::MissingEntry(format!(
98            "Form field '{name}' not found"
99        )))
100    }
101
102    /// Flattens all form fields into page content streams and removes interactive fields.
103    pub fn flatten(&mut self, doc: &mut PDDocument) -> PdfResult<()> {
104        let total_pages = doc.page_count();
105        for i in 0..total_pages {
106            let mut widget_appearance_streams = Vec::new();
107            if let Ok(page) = doc.get_page(i) {
108                if let Some(annots) = page.dict.get_array("Annots") {
109                    for item in annots.iter() {
110                        if let Some(id) = item.as_reference() {
111                            if let Some(COSObject::Dictionary(d)) = doc.cos_doc.get_object(id) {
112                                if d.get_name("Subtype") == Some("Widget") {
113                                    if let Some(ap_dict) = d.get_dict("AP") {
114                                        if let Some(n_id) = ap_dict.get_reference("N") {
115                                            widget_appearance_streams.push(n_id);
116                                        }
117                                    }
118                                }
119                            }
120                        }
121                    }
122                }
123            }
124
125            // Append widget appearance streams to page contents
126            if !widget_appearance_streams.is_empty() {
127                let page = doc.get_page_mut(i)?;
128                let mut current_ids = page.content_stream_ids();
129                current_ids.extend(widget_appearance_streams);
130                page.set_contents_array(current_ids);
131            }
132
133            // Remove widget annotations from page
134            let page = doc.get_page_mut(i)?;
135            page.dict.remove("Annots");
136        }
137
138        // Remove /AcroForm from Document Catalog
139        if let Some(cat) = doc.cos_doc.get_catalog_mut() {
140            cat.remove("AcroForm");
141        }
142
143        doc.sync();
144        Ok(())
145    }
146}