Skip to main content

pdfluent_forms/
model.rs

1//! Complete AcroForm model export for editor/binding consumption (C-chain).
2//!
3//! [`build_form_model`] flattens a parsed [`FieldTree`] into a list of
4//! [`FormFieldModel`] values — one per *logical* field — with everything an
5//! interactive form UI needs: typed kind, per-page widget rectangles, radio
6//! on-state values, checkbox on-states, choice options, comb/multiline
7//! flags, length limits, access flags and resolved default-appearance info.
8//!
9//! The model is deliberately plain data (no lifetimes, no arena ids) so
10//! facades and language bindings can serialize it as-is.
11
12use crate::appearance::parse_da;
13use crate::button::{button_kind, ButtonKind};
14use crate::flags::FieldFlags;
15use crate::tree::{ChoiceOption, FieldId, FieldTree, FieldType, FieldValue, Quadding};
16
17/// The kind of a logical form field, with kind-specific data inline.
18#[derive(Debug, Clone, PartialEq)]
19pub enum FormFieldKind {
20    /// Single-line or multiline text input.
21    Text {
22        /// Multiline flag (`/Ff` bit 13).
23        multiline: bool,
24        /// Comb flag (`/Ff` bit 25) — fixed character cells; only meaningful
25        /// together with `max_len`.
26        comb: bool,
27        /// Password flag (`/Ff` bit 14) — render input masked.
28        password: bool,
29    },
30    /// A two-state checkbox.
31    Checkbox {
32        /// The widget's on-state name (`/AP /N` key); `Yes` when the widget
33        /// declares none. Pass to setters; `Off` is always the off state.
34        on_state: String,
35        /// Whether the box is currently checked.
36        checked: bool,
37    },
38    /// A radio group: one logical field, one widget per option.
39    RadioGroup {
40        /// Export (on-state) value per option, in widget order. The widget
41        /// at `widgets[i]` toggles on when the group value is `options[i]`.
42        options: Vec<String>,
43    },
44    /// Combo box (dropdown), optionally editable.
45    ComboBox {
46        /// Edit flag (`/Ff` bit 19) — free text allowed besides options.
47        editable: bool,
48        /// `/Opt` entries.
49        options: Vec<ChoiceOption>,
50    },
51    /// List box, optionally multi-select.
52    ListBox {
53        /// MultiSelect flag (`/Ff` bit 22).
54        multi_select: bool,
55        /// `/Opt` entries.
56        options: Vec<ChoiceOption>,
57    },
58    /// Push button — no persistent value.
59    PushButton,
60    /// Signature field.
61    Signature,
62}
63
64/// One widget annotation of a logical field.
65#[derive(Debug, Clone, PartialEq)]
66pub struct WidgetModel {
67    /// 0-based page index, when the widget was found on a page.
68    pub page_index: Option<usize>,
69    /// Widget rectangle `[x0, y0, x1, y1]` in PDF user space.
70    pub rect: [f32; 4],
71    /// Button widgets: this widget's on-state name from its `/AP /N`.
72    pub on_state: Option<String>,
73    /// Current appearance state (`/AS`), when present.
74    pub appearance_state: Option<String>,
75}
76
77/// Resolved default-appearance info for a field (from `/DA`, inherited).
78#[derive(Debug, Clone, PartialEq)]
79pub struct DaInfo {
80    /// Font resource name (e.g. `Helv`), without the leading slash.
81    pub font_name: Option<String>,
82    /// Font size in points; `0` means auto-size.
83    pub font_size: f32,
84    /// Text color components (1 = gray, 3 = RGB, 4 = CMYK).
85    pub color: Vec<f32>,
86}
87
88/// A logical form field with everything an editor UI needs.
89#[derive(Debug, Clone, PartialEq)]
90pub struct FormFieldModel {
91    /// Fully-qualified field name — the handle for all get/set operations.
92    pub name: String,
93    /// Typed kind with kind-specific data.
94    pub kind: FormFieldKind,
95    /// Current value: text fields the text, buttons the on-state name or
96    /// `Off`, choice fields the selected export value (joined with `", "` for
97    /// multi-select list boxes — use `selected_values` for the array form).
98    pub value: Option<String>,
99    /// Selected export values for multi-select list boxes (`/Ff` bit 22 set).
100    /// `None` for all other field types. Empty vec means nothing selected.
101    pub selected_values: Option<Vec<String>>,
102    /// Default value (`/DV`).
103    pub default_value: Option<String>,
104    /// User-facing label (`/TU`, the accessibility/tooltip name).
105    pub tooltip: Option<String>,
106    /// ReadOnly flag (`/Ff` bit 1, inherited).
107    pub read_only: bool,
108    /// Required flag (`/Ff` bit 2, inherited).
109    pub required: bool,
110    /// `/MaxLen` for text fields (inherited).
111    pub max_len: Option<u32>,
112    /// Text alignment.
113    pub quadding: Quadding,
114    /// Resolved `/DA` font/size/color info.
115    pub da: DaInfo,
116    /// All widget annotations of this field (one per visual occurrence).
117    pub widgets: Vec<WidgetModel>,
118}
119
120/// Build the editor-facing model from a parsed field tree.
121///
122/// Logical fields are tree nodes that carry (or inherit) a field type and
123/// have no *named* children: a radio group whose kids are unnamed widgets is
124/// ONE logical field; a container with named children contributes its
125/// children, not itself.
126pub fn build_form_model(tree: &FieldTree) -> Vec<FormFieldModel> {
127    let mut out = Vec::new();
128    for id in tree.all_ids() {
129        if !is_logical_field(tree, id) {
130            continue;
131        }
132        if let Some(model) = field_model(tree, id) {
133            out.push(model);
134        }
135    }
136    out
137}
138
139/// A node is a logical field when it resolves to a field type and none of
140/// its children are named fields themselves.
141fn is_logical_field(tree: &FieldTree, id: FieldId) -> bool {
142    if tree.effective_field_type(id).is_none() {
143        return false;
144    }
145    let node = tree.get(id);
146    // Unnamed nodes are widgets of their parent, not fields.
147    if node.partial_name.is_empty() && node.parent.is_some() {
148        return false;
149    }
150    // A node with named children is a container.
151    !node
152        .children
153        .iter()
154        .any(|&c| !tree.get(c).partial_name.is_empty())
155}
156
157fn field_model(tree: &FieldTree, id: FieldId) -> Option<FormFieldModel> {
158    let node = tree.get(id);
159    let ft = tree.effective_field_type(id)?;
160    let flags = effective_flags_deep(tree, id);
161
162    let widgets = collect_widgets(tree, id);
163    let raw_value = tree.effective_value(id);
164    let selected_values = match &raw_value {
165        Some(FieldValue::StringArray(arr)) => Some(arr.clone()),
166        _ => None,
167    };
168    let value = raw_value.map(value_to_string);
169    let default_value = node.default_value.as_ref().map(value_to_string);
170
171    let kind = match ft {
172        FieldType::Text => FormFieldKind::Text {
173            multiline: flags.multiline(),
174            comb: flags.comb(),
175            password: flags.password(),
176        },
177        FieldType::Button => match button_kind(flags) {
178            ButtonKind::PushButton => FormFieldKind::PushButton,
179            ButtonKind::Checkbox => {
180                let on_state = widgets
181                    .iter()
182                    .find_map(|w| w.on_state.clone())
183                    .unwrap_or_else(|| "Yes".to_string());
184                let checked = value.as_deref().is_some_and(|v| v != "Off")
185                    || widgets
186                        .iter()
187                        .any(|w| w.appearance_state.as_deref().is_some_and(|s| s != "Off"));
188                FormFieldKind::Checkbox { on_state, checked }
189            }
190            ButtonKind::Radio => FormFieldKind::RadioGroup {
191                options: widgets
192                    .iter()
193                    .map(|w| w.on_state.clone().unwrap_or_default())
194                    .collect(),
195            },
196        },
197        FieldType::Choice => {
198            if flags.combo() {
199                FormFieldKind::ComboBox {
200                    editable: flags.edit(),
201                    options: node.options.clone(),
202                }
203            } else {
204                FormFieldKind::ListBox {
205                    multi_select: flags.multi_select(),
206                    options: node.options.clone(),
207                }
208            }
209        }
210        FieldType::Signature => FormFieldKind::Signature,
211    };
212
213    let da_str = tree.effective_da(id).unwrap_or("/Helv 0 Tf 0 g");
214    let da = parse_da(da_str);
215
216    Some(FormFieldModel {
217        name: tree.fully_qualified_name(id),
218        kind,
219        value,
220        selected_values,
221        default_value,
222        tooltip: node.alternate_name.clone(),
223        read_only: flags.read_only(),
224        required: flags.required(),
225        max_len: tree.effective_max_len(id),
226        quadding: tree.effective_quadding(id),
227        da: DaInfo {
228            font_name: da.font_name,
229            font_size: da.font_size,
230            color: da.color,
231        },
232        widgets,
233    })
234}
235
236/// Effective flags: the node's own, else the nearest ancestor's (the /Ff
237/// entry is inheritable per ISO 32000-1 Table 220).
238fn effective_flags_deep(tree: &FieldTree, id: FieldId) -> FieldFlags {
239    let mut cur = Some(id);
240    while let Some(cid) = cur {
241        let node = tree.get(cid);
242        if node.flags.bits() != 0 {
243            return node.flags;
244        }
245        cur = node.parent;
246    }
247    FieldFlags::empty()
248}
249
250fn collect_widgets(tree: &FieldTree, id: FieldId) -> Vec<WidgetModel> {
251    let node = tree.get(id);
252    let mut widgets = Vec::new();
253    if node.children.is_empty() {
254        if let Some(rect) = node.rect {
255            widgets.push(WidgetModel {
256                page_index: node.page_index,
257                rect,
258                on_state: node.on_state.clone(),
259                appearance_state: node.appearance_state.clone(),
260            });
261        }
262    } else {
263        for &kid in &node.children {
264            let k = tree.get(kid);
265            if let Some(rect) = k.rect {
266                widgets.push(WidgetModel {
267                    page_index: k.page_index,
268                    rect,
269                    on_state: k.on_state.clone(),
270                    appearance_state: k.appearance_state.clone(),
271                });
272            }
273        }
274    }
275    widgets
276}
277
278fn value_to_string(v: &FieldValue) -> String {
279    match v {
280        FieldValue::Text(s) => s.clone(),
281        FieldValue::StringArray(arr) => arr.join(", "),
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288    use crate::tree::FieldNode;
289
290    fn blank_node(name: &str) -> FieldNode {
291        FieldNode {
292            partial_name: name.into(),
293            alternate_name: None,
294            mapping_name: None,
295            field_type: None,
296            flags: FieldFlags::empty(),
297            value: None,
298            default_value: None,
299            default_appearance: None,
300            quadding: None,
301            max_len: None,
302            options: vec![],
303            top_index: None,
304            rect: None,
305            appearance_state: None,
306            on_state: None,
307            page_index: None,
308            parent: None,
309            children: vec![],
310            object_id: None,
311            has_actions: false,
312            mk: None,
313            border_style: None,
314        }
315    }
316
317    #[test]
318    fn radio_group_is_one_logical_field_with_widget_options() {
319        let mut tree = FieldTree::new();
320        let mut group = blank_node("kleur");
321        group.field_type = Some(FieldType::Button);
322        group.flags = FieldFlags::from_bits(1 << 15); // Radio
323        let gid = tree.alloc(group);
324        for (i, state) in ["Rood", "Blauw"].iter().enumerate() {
325            let mut w = blank_node("");
326            w.parent = Some(gid);
327            w.rect = Some([0.0, i as f32 * 20.0, 10.0, i as f32 * 20.0 + 10.0]);
328            w.on_state = Some(state.to_string());
329            w.appearance_state = Some("Off".into());
330            let wid = tree.alloc(w);
331            tree.get_mut(gid).children.push(wid);
332        }
333
334        let model = build_form_model(&tree);
335        assert_eq!(model.len(), 1, "group + 2 widgets must be ONE field");
336        let f = &model[0];
337        assert_eq!(f.name, "kleur");
338        assert_eq!(
339            f.kind,
340            FormFieldKind::RadioGroup {
341                options: vec!["Rood".into(), "Blauw".into()]
342            }
343        );
344        assert_eq!(f.widgets.len(), 2);
345        assert_eq!(f.widgets[1].on_state.as_deref(), Some("Blauw"));
346    }
347
348    #[test]
349    fn comb_and_multiline_flags_surface_in_kind() {
350        let mut tree = FieldTree::new();
351        let mut n = blank_node("bsn");
352        n.field_type = Some(FieldType::Text);
353        n.flags = FieldFlags::from_bits(1 << 24); // Comb
354        n.max_len = Some(9);
355        n.rect = Some([0.0, 0.0, 90.0, 12.0]);
356        tree.alloc(n);
357
358        let model = build_form_model(&tree);
359        assert_eq!(model.len(), 1);
360        assert_eq!(
361            model[0].kind,
362            FormFieldKind::Text {
363                multiline: false,
364                comb: true,
365                password: false
366            }
367        );
368        assert_eq!(model[0].max_len, Some(9));
369    }
370
371    #[test]
372    fn container_with_named_children_is_not_a_field() {
373        let mut tree = FieldTree::new();
374        let mut parent = blank_node("adres");
375        parent.field_type = Some(FieldType::Text);
376        let pid = tree.alloc(parent);
377        let mut kid = blank_node("straat");
378        kid.parent = Some(pid);
379        kid.rect = Some([0.0, 0.0, 100.0, 12.0]);
380        let kid_id = tree.alloc(kid);
381        tree.get_mut(pid).children.push(kid_id);
382
383        let model = build_form_model(&tree);
384        assert_eq!(model.len(), 1);
385        assert_eq!(model[0].name, "adres.straat");
386    }
387
388    #[test]
389    fn checkbox_reports_on_state_and_checked() {
390        let mut tree = FieldTree::new();
391        let mut n = blank_node("akkoord");
392        n.field_type = Some(FieldType::Button);
393        n.rect = Some([0.0, 0.0, 12.0, 12.0]);
394        n.on_state = Some("Akkoord".into());
395        n.appearance_state = Some("Akkoord".into());
396        n.value = Some(FieldValue::Text("Akkoord".into()));
397        tree.alloc(n);
398
399        let model = build_form_model(&tree);
400        assert_eq!(
401            model[0].kind,
402            FormFieldKind::Checkbox {
403                on_state: "Akkoord".into(),
404                checked: true
405            }
406        );
407    }
408}