//! pdfcraft-forms — interactive forms (AcroForm, ISO 32000-2 §12.7), execution plan M6.1–M6.2. //! //! - [`fields`]: the field tree flattened to terminal fields, each with its widgets (page, //! rectangle, on-state), inherited attributes (`/FT`, `/Ff`, `/V`, `/DV`, `/DA`, `/Q`, //! `/MaxLen`) and choice options. //! - [`set_value`]: fill a field. Text and choice fields get new appearance streams //! ([`appearance`]); check boxes and radio buttons switch `/V` and each widget's `/AS` between //! the states their appearances already define. //! - [`reset`]: Acrobat's Clear form (back to `/DV`). //! - [`table`]: a table of text fields, laid out and named before any of it is added. //! //! Not yet: JavaScript actions (format, keystroke, validate, calculate — M6.4/M6.5) and rich text //! values (`/RV`, which is removed when a value is set so it can't contradict `/V`). #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] use pdfcraft_cos::{DecodeBudget, Dict, Document, ObjRef, Object, PdfString}; mod actions; pub mod af; pub mod appearance; mod author; mod calc_order; pub mod detect; mod scripting; pub mod table; pub use actions::{FieldAction, Trigger, field_action_chains, field_actions, set_field_actions}; pub use author::{ BorderStyle, CheckStyle, FieldFont, FieldProps, Look, LookPatch, MIN_FIELD_SIDE, NewField, add_field, check_style, delete_field, delete_field_object, duplicate_field, look, redraw_field, set_button_icon, set_props, }; pub use calc_order::{CalculatedField, CalculationIssue, CalculationOrder, calculation_order, set_calculation_order}; pub use scripting::{ FieldChange, FieldEvent, NoScripts, ScriptResult, Scripts, apply_script_changes, document_scripts, document_scripts_named, set_document_script, set_field_script, }; mod appearances; mod read; mod tab_order; mod values; pub use appearances::{FieldAppearanceCache, declares_pdfa, need_appearances, undrawn_fields, with_field_appearances, with_field_appearances_cached}; pub(crate) use read::page_refs; pub use read::{Fields, acroform, adopted_page_fields, fields, fields_with_adopted}; #[cfg(test)] use read::{MAX_SCRIPT_BYTES, PageWalk, enumerate, form_reads}; use read::{script, script_budget}; pub use tab_order::{TabOrder, move_in_tab_order, set_tab_order}; #[cfg(test)] use values::has_password_flag; use values::write_value; pub(crate) use values::{ask_viewers_to_redraw, merged_appearance}; pub use values::{ calculations_changed, clear_password_values, has_password_values, password_values, recalculate, recalculate_with, reset, restore_password_values, set_value, set_value_with, set_values, }; #[cfg(test)] mod tests; #[derive(Debug, thiserror::Error, PartialEq)] pub enum FormError { #[error("the document has no form fields")] NoForm, #[error("there is no field named {0:?}")] NoSuchField(String), #[error("{0:?} is read-only")] ReadOnly(String), #[error("{0}")] Invalid(String), /// The value doesn't fit the field's format (its keystroke check refused it), which is /// checked before any of the field's scripts run. #[error("{0}")] Format(String), #[error("{0}")] Cos(#[from] pdfcraft_cos::CosError), } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum FieldKind { Text, CheckBox, Radio, PushButton, Combo, List, Signature, } /// Field flags (§12.7.4, Tables 226, 228, 229, 231). pub mod flags { pub const READ_ONLY: u32 = 1; pub const REQUIRED: u32 = 1 << 1; pub const MULTILINE: u32 = 1 << 12; pub const PASSWORD: u32 = 1 << 13; pub const NO_TOGGLE_TO_OFF: u32 = 1 << 14; pub const RADIO: u32 = 1 << 15; pub const PUSH_BUTTON: u32 = 1 << 16; pub const COMBO: u32 = 1 << 17; pub const EDIT: u32 = 1 << 18; pub const SORT: u32 = 1 << 19; pub const FILE_SELECT: u32 = 1 << 20; pub const MULTI_SELECT: u32 = 1 << 21; pub const DO_NOT_SPELL_CHECK: u32 = 1 << 22; pub const DO_NOT_SCROLL: u32 = 1 << 23; pub const COMB: u32 = 1 << 24; /// Buttons: radios in unison. Text fields use the same bit for rich text. pub const RADIOS_IN_UNISON: u32 = 1 << 25; pub const RICH_TEXT: u32 = 1 << 25; pub const COMMIT_ON_SEL_CHANGE: u32 = 1 << 26; } /// One widget (the field's appearance on a page). #[derive(Clone, Debug, PartialEq)] pub struct Widget { pub obj: ObjRef, /// 0-based page index, when the widget is on a page. pub page: Option, /// In user space, normalized. pub rect: [f64; 4], /// Check boxes and radio buttons: the name of the "on" appearance state. pub on_state: Option, /// The current appearance state (`/AS`). pub state: Option, /// Position in the document's tab order (pages in order; within a page by its `/Tabs`: /// rows, columns, or annotation/structure order). `usize::MAX` when not on a page. pub tab: usize, /// The widget's Locked flag (`/F` bit 8): its properties can't be changed. pub locked: bool, /// The widget's Hidden or NoView flag (`/F` bit 2 or 6): it isn't shown and takes no input. pub hidden: bool, /// Read-only: the field is, or the widget's own `/Ff` says so (a radio button of a group can /// be locked on its own, as an XFA exclusion group's layout writes it). pub read_only: bool, /// `/MK /R` as 0, 90, 180 or 270 degrees counterclockwise. Anything else is stored as 0. pub rotation: i64, /// Push buttons: the normal label (`/MK /CA`), separate from the field's name. pub caption: Option, /// List boxes: where the options are drawn, so a click can pick the one under the pointer. pub list: Option, /// Drawn round: a radio button, or a check box laid out for a round XFA check button /// (`/PCXfaRound true`). pub round: bool, } /// A terminal form field. #[derive(Clone, Debug, PartialEq)] pub struct Field { /// Fully qualified name (`parent.child`). pub name: String, pub obj: ObjRef, pub kind: FieldKind, /// Text: the text. Check box / radio: the selected state (none when off). Choice: the /// selected export values. pub value: Vec, pub default: Vec, pub flags: u32, pub max_len: Option, /// Choice options: (export value, display text). pub options: Vec<(String, String)>, /// Default appearance string (`/DA`, inherited from the form if absent). pub da: String, /// Quadding: 0 left, 1 centred, 2 right. pub quadding: i64, pub tooltip: Option, pub widgets: Vec, /// Format, validate and calculate scripts (Acrobat's AF functions), see [`af`]. pub actions: af::Actions, /// Push buttons: what a click does. pub button: Option, } impl Field { pub fn has(&self, flag: u32) -> bool { self.flags & flag != 0 } pub fn read_only(&self) -> bool { self.has(flags::READ_ONLY) } /// Locked (Field Properties ▸ General ▸ Locked): its properties can't be changed. pub fn locked(&self) -> bool { self.widgets.iter().any(|w| w.locked) } /// Check boxes and radio groups with `/Opt` (PDF 1.4): one export value per widget, in widget /// order. Their on states are then often just positions (`/0`, `/1` …), as pdf-lib writes them. fn button_exports(&self) -> Option<&[(String, String)]> { (matches!(self.kind, FieldKind::CheckBox | FieldKind::Radio) && !self.options.is_empty() && self.options.len() == self.widgets.len()) .then_some(self.options.as_slice()) } /// The export value of widget `i` of a check box or radio group: its `/Opt` entry when the /// field has one per widget, otherwise the widget's on-state name. pub fn export_of(&self, i: usize) -> Option<&str> { let on = self.widgets.get(i)?.on_state.as_deref()?; Some(self.button_exports().and_then(|o| o.get(i)).map_or(on, |(e, _)| e.as_str())) } /// The on-state name that selects `choice` in a check box or radio group, where `choice` is an /// on-state name or an export value from `/Opt`. pub fn state_for(&self, choice: &str) -> Option<&str> { self.widgets.iter().filter_map(|w| w.on_state.as_deref()).find(|s| *s == choice).or_else(|| { let i = (0..self.widgets.len()).find(|&i| self.export_of(i) == Some(choice))?; self.widgets.get(i)?.on_state.as_deref() }) } /// The export value for an on-state name such as the field's value; the name itself when the /// field has no `/Opt`. pub fn export_for_state<'a>(&'a self, state: &'a str) -> &'a str { self.widgets.iter().position(|w| w.on_state.as_deref() == Some(state)).and_then(|i| self.export_of(i)).unwrap_or(state) } /// The value as one string: text, the state name, or the selected display texts. pub fn display_value(&self) -> String { match self.kind { FieldKind::Combo | FieldKind::List => self .value .iter() .map(|v| self.options.iter().find(|(e, _)| e == v).map_or(v.clone(), |(_, d)| d.clone())) .collect::>() .join(", "), _ => self.value.join(", "), } } } /// A value to put in a field. #[derive(Clone, Debug, PartialEq)] pub enum FieldValue { Text(String), /// Check box: checked or not. Check(bool), /// Radio group: the on-state to select (`None`: none selected). Radio(Option), /// Combo box or list box: export values (several only for multi-select lists). Choice(Vec), } // ── reading ───────────────────────────────────────────────────────────────────────────────── fn text_of(o: &Object) -> Option { match o { Object::String(s) => Some(s.to_text()), Object::Name(n) => Some(name_text(n)), _ => None, } } /// A name object's bytes as text that [`name_bytes`] turns back into the same bytes. UTF-8 names /// read as themselves; other bytes (Shift-JIS check box states such as 「はい」 in Japanese /// forms) and `#` are written `#XX`, as in PDF name syntax. pub fn name_text(bytes: &[u8]) -> String { let escape = |b: u8| format!("#{b:02X}"); match std::str::from_utf8(bytes) { Ok(s) => s.replace('#', "#23"), Err(_) => bytes.iter().map(|&b| if (0x21..=0x7e).contains(&b) && b != b'#' { char::from(b).to_string() } else { escape(b) }).collect(), } } /// The bytes of the name written as `text` by [`name_text`] (`#XX` is the byte XX). pub fn name_bytes(text: &str) -> Vec { let b = text.as_bytes(); let mut out = Vec::with_capacity(b.len()); let mut i = 0; while let Some(&c) = b.get(i) { let hex = |j: usize| b.get(j).and_then(|h| char::from(*h).to_digit(16)); match (c, hex(i + 1), hex(i + 2)) { // Both digits are below 16, so the byte fits. (b'#', Some(hi), Some(lo)) => { out.push(u8::try_from(hi * 16 + lo).unwrap_or(b'#')); i += 3; } _ => { out.push(c); i += 1; } } } out } /// A name object from text written by [`name_text`]. fn name_obj(text: &str) -> Object { Object::Name(name_bytes(text)) } fn invalid(m: impl Into) -> Result { Err(FormError::Invalid(m.into())) }