use std::collections::HashMap;
use rpdfium_core::{Name, PdfSource};
use rpdfium_parser::{Object, ObjectId, ObjectStore};
use crate::aaction::{AdditionalActions, parse_additional_actions};
use crate::error::DocError;
use crate::form_control::{FormControl, HighlightingMode, TextPosition};
use crate::icon_fit::{IconFit, parse_icon_fit};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FormFieldType {
Text,
Button,
Choice,
Signature,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChoiceOption {
pub export_value: String,
pub display_value: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum FieldValue {
String(String),
Bool(bool),
Choice(usize),
Indices(Vec<usize>),
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct FormFieldFlags(u32);
impl FormFieldFlags {
pub fn from_bits(bits: u32) -> Self {
Self(bits)
}
pub fn bits(self) -> u32 {
self.0
}
pub fn is_read_only(self) -> bool {
self.0 & (1 << 0) != 0
}
pub fn is_required(self) -> bool {
self.0 & (1 << 1) != 0
}
pub fn is_no_export(self) -> bool {
self.0 & (1 << 2) != 0
}
pub fn is_no_toggle_to_off(self) -> bool {
self.0 & (1 << 14) != 0
}
pub fn is_radio(self) -> bool {
self.0 & (1 << 15) != 0
}
pub fn is_push_button(self) -> bool {
self.0 & (1 << 16) != 0
}
pub fn is_radios_in_unison(self) -> bool {
self.0 & (1 << 25) != 0
}
pub fn is_multiline(self) -> bool {
self.0 & (1 << 12) != 0
}
pub fn is_password(self) -> bool {
self.0 & (1 << 13) != 0
}
pub fn is_file_select(self) -> bool {
self.0 & (1 << 20) != 0
}
pub fn is_do_not_spell_check(self) -> bool {
self.0 & (1 << 22) != 0
}
pub fn is_do_not_scroll(self) -> bool {
self.0 & (1 << 23) != 0
}
pub fn is_comb(self) -> bool {
self.0 & (1 << 24) != 0
}
pub fn is_rich_text(self) -> bool {
self.0 & (1 << 25) != 0
}
pub fn is_combo(self) -> bool {
self.0 & (1 << 17) != 0
}
pub fn is_combo_editable(self) -> bool {
self.0 & (1 << 18) != 0
}
pub fn is_sort(self) -> bool {
self.0 & (1 << 19) != 0
}
pub fn is_multi_select(self) -> bool {
self.0 & (1 << 21) != 0
}
pub fn is_commit_on_sel_change(self) -> bool {
self.0 & (1 << 26) != 0
}
pub fn set_read_only(&mut self, v: bool) {
if v {
self.0 |= 1 << 0;
} else {
self.0 &= !(1 << 0);
}
}
pub fn set_required(&mut self, v: bool) {
if v {
self.0 |= 1 << 1;
} else {
self.0 &= !(1 << 1);
}
}
pub fn set_no_export(&mut self, v: bool) {
if v {
self.0 |= 1 << 2;
} else {
self.0 &= !(1 << 2);
}
}
}
#[derive(Debug, Clone)]
pub struct FormField {
pub name: String,
pub field_type: FormFieldType,
pub value: Option<String>,
pub default_value: Option<String>,
pub flags: FormFieldFlags,
pub tooltip: Option<String>,
pub alternate_name: Option<String>,
pub mapping_name: Option<String>,
pub max_len: Option<u32>,
pub options: Vec<ChoiceOption>,
pub appearance_state: Option<String>,
pub children: Vec<FormField>,
pub controls: Vec<FormControl>,
pub dirty: bool,
pub selected_indices: Vec<usize>,
pub additional_actions: Option<AdditionalActions>,
}
impl FormField {
pub(crate) fn from_dict<S: PdfSource>(
dict: &HashMap<Name, Object>,
store: &ObjectStore<S>,
parent_ft: Option<&str>,
parent_name: &str,
) -> Option<Self> {
let partial_name = dict
.get(&Name::t())
.and_then(|o| store.deep_resolve(o).ok())
.and_then(|o| o.as_string().map(|s| s.to_string_lossy()));
let name = if let Some(store_name) = resolve_full_field_name_from_store(dict, store) {
store_name
} else {
match (&partial_name, parent_name.is_empty()) {
(Some(pn), true) => pn.clone(),
(Some(pn), false) => format!("{parent_name}.{pn}"),
(None, _) => parent_name.to_string(),
}
};
let ft_obj = get_field_attr_inherited(dict, &Name::ft(), store);
let ft_str = ft_obj
.as_ref()
.and_then(|o| store.deep_resolve(o).ok())
.and_then(|o| o.as_name().map(|n| n.as_str().into_owned()));
let ft_ref = ft_str.as_deref().or(parent_ft);
let field_type = match ft_ref {
Some("Tx") => FormFieldType::Text,
Some("Btn") => FormFieldType::Button,
Some("Ch") => FormFieldType::Choice,
Some("Sig") => FormFieldType::Signature,
_ => {
return None;
}
};
let value = extract_inherited_string_or_name(dict, &Name::v(), store);
let default_value = extract_inherited_string_or_name(dict, &Name::dv(), store);
let ff_obj = get_field_attr_inherited(dict, &Name::ff(), store);
let flags = FormFieldFlags::from_bits(
ff_obj
.as_ref()
.and_then(|o| store.deep_resolve(o).ok())
.and_then(|o| o.as_i64())
.unwrap_or(0) as u32,
);
let tooltip = dict
.get(&Name::tu())
.and_then(|o| store.deep_resolve(o).ok())
.and_then(|o| o.as_string().map(|s| s.to_string_lossy()));
let alternate_name = tooltip.clone();
let mapping_name = dict
.get(&Name::tm())
.and_then(|o| store.deep_resolve(o).ok())
.and_then(|o| o.as_string().map(|s| s.to_string_lossy()));
let max_len = dict
.get(&Name::max_len())
.and_then(|o| store.deep_resolve(o).ok())
.and_then(|o| o.as_i64())
.map(|v| v as u32);
let options = parse_options(dict, store);
let appearance_state = dict
.get(&Name::as_name())
.and_then(|o| store.deep_resolve(o).ok())
.and_then(|o| o.as_name().map(|n| n.as_str().into_owned()));
let selected_indices = parse_selected_indices(dict, store);
let controls = parse_widget_controls(dict, store, &name);
let additional_actions = dict
.get(&Name::aa())
.and_then(|o| parse_additional_actions(o, store).ok());
Some(FormField {
name,
field_type,
value,
default_value,
flags,
tooltip,
alternate_name,
mapping_name,
max_len,
options,
appearance_state,
children: Vec::new(),
controls,
dirty: false,
selected_indices,
additional_actions,
})
}
pub fn set_value(&mut self, value: FieldValue) -> Result<(), DocError> {
match (&self.field_type, &value) {
(FormFieldType::Text, FieldValue::String(s)) => {
if let Some(max) = self.max_len {
if s.len() > max as usize {
return Err(DocError::ValueTooLong { len: s.len(), max });
}
}
self.value = Some(s.clone());
}
(FormFieldType::Button, FieldValue::Bool(checked)) => {
self.appearance_state = Some(if *checked {
"Yes".to_string()
} else {
"Off".to_string()
});
self.value = Some(self.appearance_state.as_ref().unwrap().clone());
}
(FormFieldType::Choice, FieldValue::Choice(idx)) => {
if *idx >= self.options.len() {
return Err(DocError::InvalidChoiceIndex {
index: *idx,
count: self.options.len(),
});
}
self.value = Some(self.options[*idx].export_value.clone());
}
(FormFieldType::Choice, FieldValue::String(s)) => {
self.value = Some(s.clone());
}
(FormFieldType::Choice, FieldValue::Indices(indices)) => {
for &idx in indices {
if idx >= self.options.len() {
return Err(DocError::InvalidChoiceIndex {
index: idx,
count: self.options.len(),
});
}
}
let vals: Vec<&str> = indices
.iter()
.map(|&i| self.options[i].export_value.as_str())
.collect();
self.value = Some(vals.join(","));
}
(field_type, val) => {
let expected = match field_type {
FormFieldType::Text => "String",
FormFieldType::Button => "Bool",
FormFieldType::Choice => "Choice or Indices",
FormFieldType::Signature => "Signature (read-only)",
};
let got = match val {
FieldValue::String(_) => "String",
FieldValue::Bool(_) => "Bool",
FieldValue::Choice(_) => "Choice",
FieldValue::Indices(_) => "Indices",
};
return Err(DocError::TypeMismatch {
expected: expected.to_string(),
got: got.to_string(),
});
}
}
self.dirty = true;
Ok(())
}
pub fn controls(&self) -> &[FormControl] {
&self.controls
}
pub fn needs_appearance(&self) -> bool {
self.dirty
}
pub fn reset_to_default(&mut self) {
self.value = self.default_value.clone();
self.dirty = true;
}
pub fn selected_item_count(&self) -> usize {
self.selected_indices.len()
}
#[inline]
pub fn count_selected_items(&self) -> usize {
self.selected_item_count()
}
pub fn is_item_selected(&self, index: usize) -> bool {
self.selected_indices.contains(&index)
}
pub fn selected_index(&self, sel_index: usize) -> Option<usize> {
self.selected_indices.get(sel_index).copied()
}
#[inline]
pub fn get_selected_index(&self, sel_index: usize) -> Option<usize> {
self.selected_index(sel_index)
}
pub fn clear_selection(&mut self) -> Result<(), DocError> {
self.selected_indices.clear();
Ok(())
}
pub fn set_read_only(&mut self, v: bool) {
self.flags.set_read_only(v);
}
pub fn set_required(&mut self, v: bool) {
self.flags.set_required(v);
}
pub fn set_no_export(&mut self, v: bool) {
self.flags.set_no_export(v);
}
pub fn additional_actions(&self) -> Option<&AdditionalActions> {
self.additional_actions.as_ref()
}
#[inline]
pub fn get_additional_actions(&self) -> Option<&AdditionalActions> {
self.additional_actions()
}
pub fn option_count(&self) -> usize {
self.options.len()
}
#[inline]
pub fn count_options(&self) -> usize {
self.option_count()
}
pub fn option_label(&self, index: usize) -> Option<&str> {
self.options.get(index).map(|o| o.display_value.as_str())
}
#[inline]
pub fn get_option_label(&self, index: usize) -> Option<&str> {
self.option_label(index)
}
pub fn option_value(&self, index: usize) -> Option<&str> {
self.options.get(index).map(|o| o.export_value.as_str())
}
#[inline]
pub fn get_option_value(&self, index: usize) -> Option<&str> {
self.option_value(index)
}
pub fn control_count(&self) -> usize {
self.controls.len()
}
#[inline]
pub fn count_controls(&self) -> usize {
self.control_count()
}
pub fn control_at(&self, index: usize) -> Option<&FormControl> {
self.controls.get(index)
}
#[inline]
pub fn get_control(&self, index: usize) -> Option<&FormControl> {
self.control_at(index)
}
}
const MAX_FIELD_ATTR_DEPTH: usize = 32;
fn get_field_attr_inherited<S: PdfSource>(
start_dict: &HashMap<Name, Object>,
name: &Name,
store: &ObjectStore<S>,
) -> Option<Object> {
if let Some(val) = start_dict.get(name) {
return Some(val.clone());
}
let mut current_id: Option<ObjectId> = None;
let mut parent_ref = start_dict
.get(&Name::parent())
.and_then(|v| v.as_reference());
for _ in 0..MAX_FIELD_ATTR_DEPTH {
let parent_id = parent_ref?;
if current_id == Some(parent_id) {
break;
}
current_id = Some(parent_id);
let parent_obj = store.resolve(parent_id).ok()?;
let parent_dict = parent_obj.as_dict()?;
if let Some(val) = parent_dict.get(name) {
return Some(val.clone());
}
parent_ref = parent_dict
.get(&Name::parent())
.and_then(|v| v.as_reference());
}
None
}
fn resolve_full_field_name_from_store<S: PdfSource>(
start_dict: &HashMap<Name, Object>,
store: &ObjectStore<S>,
) -> Option<String> {
start_dict.get(&Name::parent())?.as_reference()?;
let mut parts: Vec<String> = Vec::new();
if let Some(t_obj) = start_dict.get(&Name::t()) {
if let Ok(resolved) = store.deep_resolve(t_obj) {
if let Some(s) = resolved.as_string() {
parts.push(s.to_string_lossy());
}
}
}
let mut parent_ref = start_dict
.get(&Name::parent())
.and_then(|v| v.as_reference());
let mut current_id: Option<ObjectId> = None;
for _ in 0..MAX_FIELD_ATTR_DEPTH {
let pid = match parent_ref {
Some(id) => id,
None => break,
};
if current_id == Some(pid) {
break;
}
current_id = Some(pid);
let pobj = match store.resolve(pid).ok() {
Some(o) => o,
None => break,
};
let pdict = match pobj.as_dict() {
Some(d) => d,
None => break,
};
if let Some(t_obj) = pdict.get(&Name::t()) {
if let Ok(resolved) = store.deep_resolve(t_obj) {
if let Some(s) = resolved.as_string() {
parts.push(s.to_string_lossy());
}
}
}
parent_ref = pdict.get(&Name::parent()).and_then(|v| v.as_reference());
}
if parts.is_empty() {
return None;
}
parts.reverse();
Some(parts.join("."))
}
fn extract_inherited_string_or_name<S: PdfSource>(
dict: &HashMap<Name, Object>,
key: &Name,
store: &ObjectStore<S>,
) -> Option<String> {
let obj = get_field_attr_inherited(dict, key, store)?;
let resolved = store.deep_resolve(&obj).ok()?;
if let Some(s) = resolved.as_string() {
Some(s.to_string_lossy())
} else {
resolved.as_name().map(|n| n.as_str().into_owned())
}
}
fn parse_options<S: PdfSource>(
dict: &HashMap<Name, Object>,
store: &ObjectStore<S>,
) -> Vec<ChoiceOption> {
let opt_obj = match dict.get(&Name::opt()) {
Some(o) => o,
None => return Vec::new(),
};
let resolved = match store.deep_resolve(opt_obj).ok() {
Some(o) => o,
None => return Vec::new(),
};
let arr = match resolved.as_array() {
Some(a) => a,
None => return Vec::new(),
};
let mut options = Vec::with_capacity(arr.len());
for item in arr {
let resolved_item = match store.deep_resolve(item).ok() {
Some(o) => o,
None => continue,
};
if let Some(sub_arr) = resolved_item.as_array() {
if sub_arr.len() >= 2 {
let export = store
.deep_resolve(&sub_arr[0])
.ok()
.and_then(|o| o.as_string().map(|s| s.to_string_lossy()))
.unwrap_or_default();
let display = store
.deep_resolve(&sub_arr[1])
.ok()
.and_then(|o| o.as_string().map(|s| s.to_string_lossy()))
.unwrap_or_default();
options.push(ChoiceOption {
export_value: export,
display_value: display,
});
}
} else if let Some(s) = resolved_item.as_string() {
let val = s.to_string_lossy();
options.push(ChoiceOption {
export_value: val.clone(),
display_value: val,
});
}
}
options
}
fn parse_selected_indices<S: PdfSource>(
dict: &HashMap<Name, Object>,
store: &ObjectStore<S>,
) -> Vec<usize> {
let i_obj = match dict.get(&Name::i()) {
Some(o) => o,
None => return Vec::new(),
};
let resolved = match store.deep_resolve(i_obj).ok() {
Some(o) => o,
None => return Vec::new(),
};
let arr = match resolved.as_array() {
Some(a) => a,
None => return Vec::new(),
};
arr.iter()
.filter_map(|item| item.as_i64().map(|n| n as usize))
.collect()
}
fn parse_widget_controls<S: PdfSource>(
dict: &HashMap<Name, Object>,
store: &ObjectStore<S>,
field_name: &str,
) -> Vec<FormControl> {
let kids_obj = match dict.get(&Name::kids()) {
Some(o) => o,
None => return Vec::new(),
};
let kids_resolved = match store.deep_resolve(kids_obj).ok() {
Some(o) => o,
None => return Vec::new(),
};
let kids_arr = match kids_resolved.as_array() {
Some(a) => a,
None => return Vec::new(),
};
let mut controls = Vec::new();
for kid in kids_arr {
let resolved = match store.deep_resolve(kid).ok() {
Some(o) => o,
None => continue,
};
let kid_dict = match resolved.as_dict() {
Some(d) => d,
None => continue,
};
let is_widget = kid_dict
.get(&Name::subtype())
.and_then(|o| store.deep_resolve(o).ok())
.and_then(|o| o.as_name().map(|n| n.as_str().into_owned()))
.is_some_and(|s| s == "Widget");
let has_ft = kid_dict.get(&Name::ft()).is_some();
if is_widget && !has_ft {
let rect = parse_rect(kid_dict, store);
let appearance_state = kid_dict
.get(&Name::as_name())
.and_then(|o| store.deep_resolve(o).ok())
.and_then(|o| o.as_name().map(|n| n.as_str().into_owned()));
let highlighting_mode = kid_dict
.get(&Name::h())
.and_then(|o| store.deep_resolve(o).ok())
.and_then(|o| {
o.as_name()
.map(|n| HighlightingMode::from_name(&n.as_str()))
})
.unwrap_or_default();
let default_appearance = kid_dict
.get(&Name::da())
.and_then(|o| store.deep_resolve(o).ok())
.and_then(|o| o.as_string().map(|s| s.to_string_lossy()));
let mk = parse_mk_fields(kid_dict, store);
controls.push(FormControl {
field_name: field_name.to_string(),
rect,
appearance_state,
page_index: None,
highlighting_mode,
rotation: mk.rotation,
border_color: mk.border_color,
background_color: mk.background_color,
caption: mk.caption,
rollover_caption: mk.rollover_caption,
alt_caption: mk.alt_caption,
default_appearance,
normal_icon: mk.normal_icon,
rollover_icon: mk.rollover_icon,
down_icon: mk.down_icon,
icon_fit: mk.icon_fit,
text_position: mk.text_position,
});
}
}
controls
}
#[derive(Default)]
struct MkFields {
rotation: u32,
border_color: Option<Vec<f32>>,
background_color: Option<Vec<f32>>,
caption: Option<String>,
rollover_caption: Option<String>,
alt_caption: Option<String>,
normal_icon: Option<ObjectId>,
rollover_icon: Option<ObjectId>,
down_icon: Option<ObjectId>,
icon_fit: Option<IconFit>,
text_position: TextPosition,
}
fn parse_mk_fields<S: PdfSource>(dict: &HashMap<Name, Object>, store: &ObjectStore<S>) -> MkFields {
let mk_obj = match dict.get(&Name::mk()) {
Some(o) => o,
None => return MkFields::default(),
};
let resolved = match store.deep_resolve(mk_obj).ok() {
Some(o) => o,
None => return MkFields::default(),
};
let mk_dict = match resolved.as_dict() {
Some(d) => d,
None => return MkFields::default(),
};
let rotation = mk_dict
.get(&Name::r())
.and_then(|o| o.as_i64())
.unwrap_or(0) as u32;
let border_color = parse_color_array(mk_dict, &Name::bc(), store);
let background_color = parse_color_array(mk_dict, &Name::bg_color(), store);
let caption = mk_dict
.get(&Name::ca_display())
.and_then(|o| store.deep_resolve(o).ok())
.and_then(|o| o.as_string().map(|s| s.to_string_lossy()));
let rollover_caption = mk_dict
.get(&Name::rc())
.and_then(|o| store.deep_resolve(o).ok())
.and_then(|o| o.as_string().map(|s| s.to_string_lossy()));
let alt_caption = mk_dict
.get(&Name::ac())
.and_then(|o| store.deep_resolve(o).ok())
.and_then(|o| o.as_string().map(|s| s.to_string_lossy()));
let normal_icon = mk_dict.get(&Name::i()).and_then(|o| o.as_reference());
let rollover_icon = mk_dict.get(&Name::ri()).and_then(|o| o.as_reference());
let down_icon = mk_dict.get(&Name::ix()).and_then(|o| o.as_reference());
let text_position = mk_dict
.get(&Name::tp())
.and_then(|o| o.as_i64())
.map(|v| TextPosition::from_value(v as u32))
.unwrap_or_default();
let icon_fit = parse_icon_fit(mk_dict, store);
MkFields {
rotation,
border_color,
background_color,
caption,
rollover_caption,
alt_caption,
normal_icon,
rollover_icon,
down_icon,
icon_fit,
text_position,
}
}
fn parse_color_array<S: PdfSource>(
dict: &HashMap<Name, Object>,
key: &Name,
store: &ObjectStore<S>,
) -> Option<Vec<f32>> {
let obj = dict.get(key)?;
let resolved = store.deep_resolve(obj).ok()?;
let arr = resolved.as_array()?;
let colors: Vec<f32> = arr
.iter()
.filter_map(|o| o.as_f64().map(|v| v as f32))
.collect();
if colors.is_empty() {
None
} else {
Some(colors)
}
}
fn parse_rect<S: PdfSource>(
dict: &HashMap<Name, Object>,
store: &ObjectStore<S>,
) -> rpdfium_core::Rect {
let default = rpdfium_core::Rect {
left: 0.0,
bottom: 0.0,
right: 0.0,
top: 0.0,
};
let rect_obj = match dict.get(&Name::rect()) {
Some(o) => o,
None => return default,
};
let resolved = match store.deep_resolve(rect_obj).ok() {
Some(o) => o,
None => return default,
};
let arr = match resolved.as_array() {
Some(a) if a.len() >= 4 => a,
_ => return default,
};
let get_f64 = |idx: usize| -> f64 { arr[idx].as_f64().unwrap_or(0.0) };
rpdfium_core::Rect {
left: get_f64(0),
bottom: get_f64(1),
right: get_f64(2),
top: get_f64(3),
}
}
#[cfg(test)]
mod tests {
use super::*;
use rpdfium_core::PdfString;
fn build_store() -> ObjectStore<Vec<u8>> {
let pdf = build_minimal_pdf();
ObjectStore::open(pdf, rpdfium_core::ParsingMode::Lenient).unwrap()
}
fn build_minimal_pdf() -> Vec<u8> {
let mut pdf = Vec::new();
pdf.extend_from_slice(b"%PDF-1.4\n");
let obj1_offset = pdf.len();
pdf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
let obj2_offset = pdf.len();
pdf.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n");
let xref_offset = pdf.len();
pdf.extend_from_slice(b"xref\n0 3\n");
pdf.extend_from_slice(b"0000000000 65535 f \r\n");
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj1_offset).as_bytes());
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj2_offset).as_bytes());
pdf.extend_from_slice(b"trailer\n<< /Size 3 /Root 1 0 R >>\n");
pdf.extend_from_slice(format!("startxref\n{}\n%%EOF", xref_offset).as_bytes());
pdf
}
fn str_obj(s: &str) -> Object {
Object::String(PdfString::from_bytes(s.as_bytes().to_vec()))
}
#[test]
fn test_parse_text_field() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::ft(), Object::Name(Name::from("Tx")));
dict.insert(Name::t(), str_obj("username"));
dict.insert(Name::v(), str_obj("john"));
dict.insert(Name::max_len(), Object::Integer(50));
dict.insert(Name::tu(), str_obj("Enter your name"));
let field = FormField::from_dict(&dict, &store, None, "").unwrap();
assert_eq!(field.field_type, FormFieldType::Text);
assert_eq!(field.name, "username");
assert_eq!(field.value.as_deref(), Some("john"));
assert_eq!(field.max_len, Some(50));
assert_eq!(field.tooltip.as_deref(), Some("Enter your name"));
}
#[test]
fn test_parse_button_field_checkbox() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::ft(), Object::Name(Name::from("Btn")));
dict.insert(Name::t(), str_obj("agree"));
dict.insert(Name::as_name(), Object::Name(Name::from("Yes")));
dict.insert(Name::ff(), Object::Integer(0));
let field = FormField::from_dict(&dict, &store, None, "").unwrap();
assert_eq!(field.field_type, FormFieldType::Button);
assert_eq!(field.name, "agree");
assert_eq!(field.appearance_state.as_deref(), Some("Yes"));
assert_eq!(field.flags.bits(), 0);
}
#[test]
fn test_parse_choice_field_with_options() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::ft(), Object::Name(Name::from("Ch")));
dict.insert(Name::t(), str_obj("color"));
let opts = Object::Array(vec![str_obj("Red"), str_obj("Green"), str_obj("Blue")]);
dict.insert(Name::opt(), opts);
let field = FormField::from_dict(&dict, &store, None, "").unwrap();
assert_eq!(field.field_type, FormFieldType::Choice);
assert_eq!(field.options.len(), 3);
assert_eq!(field.options[0].export_value, "Red");
assert_eq!(field.options[0].display_value, "Red");
assert_eq!(field.options[2].export_value, "Blue");
}
#[test]
fn test_parse_choice_field_with_export_display_pairs() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::ft(), Object::Name(Name::from("Ch")));
dict.insert(Name::t(), str_obj("country"));
let opts = Object::Array(vec![Object::Array(vec![
str_obj("US"),
str_obj("United States"),
])]);
dict.insert(Name::opt(), opts);
let field = FormField::from_dict(&dict, &store, None, "").unwrap();
assert_eq!(field.options.len(), 1);
assert_eq!(field.options[0].export_value, "US");
assert_eq!(field.options[0].display_value, "United States");
}
#[test]
fn test_hierarchical_field_name() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::ft(), Object::Name(Name::from("Tx")));
dict.insert(Name::t(), str_obj("city"));
let field = FormField::from_dict(&dict, &store, None, "address").unwrap();
assert_eq!(field.name, "address.city");
}
#[test]
fn test_inherit_ft_from_parent() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::t(), str_obj("line1"));
dict.insert(Name::v(), str_obj("123 Main St"));
let field = FormField::from_dict(&dict, &store, Some("Tx"), "address").unwrap();
assert_eq!(field.field_type, FormFieldType::Text);
assert_eq!(field.name, "address.line1");
assert_eq!(field.value.as_deref(), Some("123 Main St"));
}
#[test]
fn test_field_flags_parsing() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::ft(), Object::Name(Name::from("Tx")));
dict.insert(Name::t(), str_obj("notes"));
dict.insert(Name::ff(), Object::Integer(4096));
let field = FormField::from_dict(&dict, &store, None, "").unwrap();
assert_eq!(field.flags.bits(), 4096);
}
#[test]
fn test_no_ft_returns_none() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::t(), str_obj("orphan"));
let result = FormField::from_dict(&dict, &store, None, "");
assert!(result.is_none());
}
fn make_field(ft: FormFieldType) -> FormField {
FormField {
name: "test".to_string(),
field_type: ft,
value: None,
default_value: Some("default_val".to_string()),
flags: FormFieldFlags::from_bits(0),
tooltip: None,
alternate_name: None,
mapping_name: None,
max_len: None,
options: Vec::new(),
appearance_state: None,
children: Vec::new(),
controls: Vec::new(),
dirty: false,
selected_indices: Vec::new(),
additional_actions: None,
}
}
#[test]
fn test_set_value_text_field() {
let mut field = make_field(FormFieldType::Text);
field.max_len = Some(10);
field
.set_value(FieldValue::String("hello".to_string()))
.unwrap();
assert_eq!(field.value.as_deref(), Some("hello"));
assert!(field.dirty);
}
#[test]
fn test_set_value_text_field_too_long() {
let mut field = make_field(FormFieldType::Text);
field.max_len = Some(3);
let result = field.set_value(FieldValue::String("toolong".to_string()));
assert!(result.is_err());
assert!(!field.dirty);
}
#[test]
fn test_set_value_button_checked() {
let mut field = make_field(FormFieldType::Button);
field.set_value(FieldValue::Bool(true)).unwrap();
assert_eq!(field.appearance_state.as_deref(), Some("Yes"));
assert_eq!(field.value.as_deref(), Some("Yes"));
assert!(field.dirty);
}
#[test]
fn test_set_value_button_unchecked() {
let mut field = make_field(FormFieldType::Button);
field.set_value(FieldValue::Bool(false)).unwrap();
assert_eq!(field.appearance_state.as_deref(), Some("Off"));
}
#[test]
fn test_set_value_choice_by_index() {
let mut field = make_field(FormFieldType::Choice);
field.options = vec![
ChoiceOption {
export_value: "R".into(),
display_value: "Red".into(),
},
ChoiceOption {
export_value: "G".into(),
display_value: "Green".into(),
},
ChoiceOption {
export_value: "B".into(),
display_value: "Blue".into(),
},
];
field.set_value(FieldValue::Choice(1)).unwrap();
assert_eq!(field.value.as_deref(), Some("G"));
}
#[test]
fn test_set_value_choice_out_of_bounds() {
let mut field = make_field(FormFieldType::Choice);
field.options = vec![ChoiceOption {
export_value: "R".into(),
display_value: "Red".into(),
}];
let result = field.set_value(FieldValue::Choice(5));
assert!(result.is_err());
}
#[test]
fn test_set_value_type_mismatch() {
let mut field = make_field(FormFieldType::Text);
let result = field.set_value(FieldValue::Bool(true));
assert!(result.is_err());
assert!(!field.dirty);
}
#[test]
fn test_reset_to_default() {
let mut field = make_field(FormFieldType::Text);
field.value = Some("modified".to_string());
field.reset_to_default();
assert_eq!(field.value.as_deref(), Some("default_val"));
assert!(field.dirty);
}
#[test]
fn test_needs_appearance_after_set() {
let mut field = make_field(FormFieldType::Text);
assert!(!field.needs_appearance());
field
.set_value(FieldValue::String("x".to_string()))
.unwrap();
assert!(field.needs_appearance());
}
#[test]
fn test_controls_accessor_empty() {
let field = make_field(FormFieldType::Text);
assert!(field.controls().is_empty());
}
#[test]
fn test_widget_kids_parsed_as_controls() {
let store = build_store();
let mut widget_dict = HashMap::new();
widget_dict.insert(Name::subtype(), Object::Name(Name::from("Widget")));
widget_dict.insert(
Name::rect(),
Object::Array(vec![
Object::Real(10.0),
Object::Real(20.0),
Object::Real(110.0),
Object::Real(40.0),
]),
);
widget_dict.insert(Name::as_name(), Object::Name(Name::from("Yes")));
let mut field_dict = HashMap::new();
field_dict.insert(Name::ft(), Object::Name(Name::from("Btn")));
field_dict.insert(Name::t(), str_obj("checkbox"));
field_dict.insert(
Name::kids(),
Object::Array(vec![Object::Dictionary(widget_dict)]),
);
let field = FormField::from_dict(&field_dict, &store, None, "").unwrap();
assert_eq!(field.controls().len(), 1);
assert_eq!(field.controls()[0].field_name, "checkbox");
assert_eq!(field.controls()[0].rect.left, 10.0);
assert_eq!(field.controls()[0].rect.right, 110.0);
assert_eq!(field.controls()[0].appearance_state.as_deref(), Some("Yes"));
}
#[test]
fn test_flag_helpers_default() {
let field = make_field(FormFieldType::Text);
assert!(!field.flags.is_read_only());
assert!(!field.flags.is_required());
assert!(!field.flags.is_no_export());
assert!(!field.flags.is_multiline());
assert!(!field.flags.is_password());
}
#[test]
fn test_flag_read_only() {
let mut field = make_field(FormFieldType::Text);
field.flags = FormFieldFlags::from_bits(1 << 0);
assert!(field.flags.is_read_only());
}
#[test]
fn test_flag_required() {
let mut field = make_field(FormFieldType::Text);
field.flags = FormFieldFlags::from_bits(1 << 1);
assert!(field.flags.is_required());
}
#[test]
fn test_flag_no_export() {
let mut field = make_field(FormFieldType::Text);
field.flags = FormFieldFlags::from_bits(1 << 2);
assert!(field.flags.is_no_export());
}
#[test]
fn test_flag_multiline() {
let mut field = make_field(FormFieldType::Text);
field.flags = FormFieldFlags::from_bits(1 << 12);
assert!(field.flags.is_multiline());
}
#[test]
fn test_flag_password() {
let mut field = make_field(FormFieldType::Text);
field.flags = FormFieldFlags::from_bits(1 << 13);
assert!(field.flags.is_password());
}
#[test]
fn test_flag_no_toggle_to_off() {
let mut field = make_field(FormFieldType::Button);
field.flags = FormFieldFlags::from_bits(1 << 14);
assert!(field.flags.is_no_toggle_to_off());
}
#[test]
fn test_flag_radio() {
let mut field = make_field(FormFieldType::Button);
field.flags = FormFieldFlags::from_bits(1 << 15);
assert!(field.flags.is_radio());
}
#[test]
fn test_flag_push_button() {
let mut field = make_field(FormFieldType::Button);
field.flags = FormFieldFlags::from_bits(1 << 16);
assert!(field.flags.is_push_button());
}
#[test]
fn test_flag_combo() {
let mut field = make_field(FormFieldType::Choice);
field.flags = FormFieldFlags::from_bits(1 << 17);
assert!(field.flags.is_combo());
}
#[test]
fn test_flag_combo_editable() {
let mut field = make_field(FormFieldType::Choice);
field.flags = FormFieldFlags::from_bits(1 << 18);
assert!(field.flags.is_combo_editable());
}
#[test]
fn test_flag_sort() {
let mut field = make_field(FormFieldType::Choice);
field.flags = FormFieldFlags::from_bits(1 << 19);
assert!(field.flags.is_sort());
}
#[test]
fn test_flag_file_select() {
let mut field = make_field(FormFieldType::Text);
field.flags = FormFieldFlags::from_bits(1 << 20);
assert!(field.flags.is_file_select());
}
#[test]
fn test_flag_multi_select() {
let mut field = make_field(FormFieldType::Choice);
field.flags = FormFieldFlags::from_bits(1 << 21);
assert!(field.flags.is_multi_select());
}
#[test]
fn test_flag_do_not_spell_check() {
let mut field = make_field(FormFieldType::Text);
field.flags = FormFieldFlags::from_bits(1 << 22);
assert!(field.flags.is_do_not_spell_check());
}
#[test]
fn test_flag_do_not_scroll() {
let mut field = make_field(FormFieldType::Text);
field.flags = FormFieldFlags::from_bits(1 << 23);
assert!(field.flags.is_do_not_scroll());
}
#[test]
fn test_flag_comb() {
let mut field = make_field(FormFieldType::Text);
field.flags = FormFieldFlags::from_bits(1 << 24);
assert!(field.flags.is_comb());
}
#[test]
fn test_flag_rich_text_and_radios_in_unison() {
let mut field = make_field(FormFieldType::Text);
field.flags = FormFieldFlags::from_bits(1 << 25);
assert!(field.flags.is_rich_text());
assert!(field.flags.is_radios_in_unison());
}
#[test]
fn test_flag_commit_on_sel_change() {
let mut field = make_field(FormFieldType::Choice);
field.flags = FormFieldFlags::from_bits(1 << 26);
assert!(field.flags.is_commit_on_sel_change());
}
#[test]
fn test_selected_indices_empty() {
let field = make_field(FormFieldType::Choice);
assert_eq!(field.selected_item_count(), 0);
assert!(!field.is_item_selected(0));
assert!(field.selected_index(0).is_none());
}
#[test]
fn test_selected_indices_populated() {
let mut field = make_field(FormFieldType::Choice);
field.selected_indices = vec![1, 3];
assert_eq!(field.selected_item_count(), 2);
assert!(!field.is_item_selected(0));
assert!(field.is_item_selected(1));
assert!(!field.is_item_selected(2));
assert!(field.is_item_selected(3));
assert_eq!(field.selected_index(0), Some(1));
assert_eq!(field.selected_index(1), Some(3));
assert!(field.selected_index(2).is_none());
}
#[test]
fn test_alternate_name_from_tooltip() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::ft(), Object::Name(Name::from("Tx")));
dict.insert(Name::t(), str_obj("field1"));
dict.insert(Name::tu(), str_obj("Enter your name"));
let field = FormField::from_dict(&dict, &store, None, "").unwrap();
assert_eq!(field.alternate_name.as_deref(), Some("Enter your name"));
assert_eq!(field.tooltip.as_deref(), Some("Enter your name"));
}
#[test]
fn test_mapping_name_parsed() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::ft(), Object::Name(Name::from("Tx")));
dict.insert(Name::t(), str_obj("field1"));
dict.insert(Name::tm(), str_obj("export_field1"));
let field = FormField::from_dict(&dict, &store, None, "").unwrap();
assert_eq!(field.mapping_name.as_deref(), Some("export_field1"));
}
#[test]
fn test_selected_indices_parsed_from_dict() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::ft(), Object::Name(Name::from("Ch")));
dict.insert(Name::t(), str_obj("colors"));
let opts = Object::Array(vec![str_obj("Red"), str_obj("Green"), str_obj("Blue")]);
dict.insert(Name::opt(), opts);
dict.insert(
Name::i(),
Object::Array(vec![Object::Integer(0), Object::Integer(2)]),
);
let field = FormField::from_dict(&dict, &store, None, "").unwrap();
assert_eq!(field.selected_indices, vec![0, 2]);
assert!(field.is_item_selected(0));
assert!(!field.is_item_selected(1));
assert!(field.is_item_selected(2));
}
#[test]
fn test_kids_with_ft_not_treated_as_controls() {
let store = build_store();
let mut child_dict = HashMap::new();
child_dict.insert(Name::subtype(), Object::Name(Name::from("Widget")));
child_dict.insert(Name::ft(), Object::Name(Name::from("Tx")));
child_dict.insert(Name::t(), str_obj("child"));
let mut field_dict = HashMap::new();
field_dict.insert(Name::ft(), Object::Name(Name::from("Tx")));
field_dict.insert(Name::t(), str_obj("parent"));
field_dict.insert(
Name::kids(),
Object::Array(vec![Object::Dictionary(child_dict)]),
);
let field = FormField::from_dict(&field_dict, &store, None, "").unwrap();
assert!(field.controls().is_empty());
}
#[test]
fn test_clear_selection_clears_indices() {
let mut field = make_field(FormFieldType::Choice);
field.selected_indices = vec![0, 2];
let result = field.clear_selection();
assert!(result.is_ok());
assert!(field.selected_indices.is_empty());
}
#[test]
fn test_additional_actions_none_by_default() {
let field = make_field(FormFieldType::Text);
assert!(field.additional_actions().is_none());
}
#[test]
fn test_option_count_returns_options_length() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::ft(), Object::Name(Name::from("Ch")));
dict.insert(Name::t(), str_obj("colors"));
dict.insert(
Name::opt(),
Object::Array(vec![str_obj("Red"), str_obj("Green"), str_obj("Blue")]),
);
let field = FormField::from_dict(&dict, &store, None, "").unwrap();
assert_eq!(field.option_count(), 3);
}
#[test]
fn test_get_option_label_returns_display_value() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::ft(), Object::Name(Name::from("Ch")));
dict.insert(Name::t(), str_obj("sizes"));
dict.insert(
Name::opt(),
Object::Array(vec![
Object::Array(vec![str_obj("sm"), str_obj("Small")]),
Object::Array(vec![str_obj("lg"), str_obj("Large")]),
]),
);
let field = FormField::from_dict(&dict, &store, None, "").unwrap();
assert_eq!(field.get_option_label(0), Some("Small"));
assert_eq!(field.get_option_label(1), Some("Large"));
assert_eq!(field.get_option_label(2), None);
}
#[test]
fn test_get_option_value_returns_export_value() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(Name::ft(), Object::Name(Name::from("Ch")));
dict.insert(Name::t(), str_obj("sizes"));
dict.insert(
Name::opt(),
Object::Array(vec![
Object::Array(vec![str_obj("sm"), str_obj("Small")]),
Object::Array(vec![str_obj("lg"), str_obj("Large")]),
]),
);
let field = FormField::from_dict(&dict, &store, None, "").unwrap();
assert_eq!(field.get_option_value(0), Some("sm"));
assert_eq!(field.get_option_value(1), Some("lg"));
assert_eq!(field.get_option_value(99), None);
}
#[test]
fn test_control_count_returns_controls_length() {
let mut field = make_field(FormFieldType::Choice);
assert_eq!(field.control_count(), 0);
use crate::form_control::{FormControl, HighlightingMode, TextPosition};
use rpdfium_core::Rect;
field.controls.push(FormControl {
field_name: "test".to_string(),
rect: Rect::new(0.0, 0.0, 100.0, 20.0),
appearance_state: None,
page_index: None,
highlighting_mode: HighlightingMode::Invert,
rotation: 0,
border_color: None,
background_color: None,
caption: None,
rollover_caption: None,
alt_caption: None,
default_appearance: None,
normal_icon: None,
rollover_icon: None,
down_icon: None,
icon_fit: None,
text_position: TextPosition::CaptionOnly,
});
assert_eq!(field.control_count(), 1);
}
#[test]
fn test_get_control_returns_correct_control() {
let mut field = make_field(FormFieldType::Text);
use crate::form_control::{FormControl, HighlightingMode, TextPosition};
use rpdfium_core::Rect;
let ctrl = FormControl {
field_name: "f".to_string(),
rect: Rect::new(10.0, 10.0, 110.0, 30.0),
appearance_state: None,
page_index: Some(0),
highlighting_mode: HighlightingMode::None,
rotation: 0,
border_color: None,
background_color: None,
caption: None,
rollover_caption: None,
alt_caption: None,
default_appearance: None,
normal_icon: None,
rollover_icon: None,
down_icon: None,
icon_fit: None,
text_position: TextPosition::CaptionOnly,
};
field.controls.push(ctrl);
assert!(field.get_control(0).is_some());
assert_eq!(field.get_control(0).unwrap().page_index, Some(0));
assert!(field.get_control(1).is_none());
}
#[test]
fn test_cpdf_form_field_is_item_selected() {
let options = vec![
ChoiceOption {
export_value: "Alpha".into(),
display_value: "Alpha".into(),
},
ChoiceOption {
export_value: "Beta".into(),
display_value: "Beta".into(),
},
ChoiceOption {
export_value: "Gamma".into(),
display_value: "Gamma".into(),
},
ChoiceOption {
export_value: "Delta".into(),
display_value: "Delta".into(),
},
ChoiceOption {
export_value: "Epsilon".into(),
display_value: "Epsilon".into(),
},
];
let make_choice_field = |selected: Vec<usize>| -> FormField {
FormField {
name: "multi".to_string(),
field_type: FormFieldType::Choice,
value: None,
default_value: None,
flags: FormFieldFlags::from_bits(1 << 21), tooltip: None,
alternate_name: None,
mapping_name: None,
max_len: None,
options: options.clone(),
appearance_state: None,
children: Vec::new(),
controls: Vec::new(),
dirty: false,
selected_indices: selected,
additional_actions: None,
}
};
{
let field = make_choice_field(vec![]);
for i in 0..5 {
assert!(!field.is_item_selected(i));
}
assert!(!field.is_item_selected(5));
}
{
let field = make_choice_field(vec![2]); assert!(!field.is_item_selected(0));
assert!(!field.is_item_selected(1));
assert!(field.is_item_selected(2));
assert!(!field.is_item_selected(3));
assert!(!field.is_item_selected(4));
}
{
let field = make_choice_field(vec![0, 2, 3]);
assert!(field.is_item_selected(0));
assert!(!field.is_item_selected(1));
assert!(field.is_item_selected(2));
assert!(field.is_item_selected(3));
assert!(!field.is_item_selected(4));
}
{
let field = make_choice_field(vec![0, 2, 3, 12, 42]);
assert!(field.is_item_selected(0));
assert!(!field.is_item_selected(1));
assert!(field.is_item_selected(2));
assert!(field.is_item_selected(3));
assert!(!field.is_item_selected(4));
assert!(!field.is_item_selected(5));
assert!(field.is_item_selected(12));
assert!(field.is_item_selected(42));
}
{
let field = make_choice_field(vec![1, 4]);
assert_eq!(field.selected_item_count(), 2);
assert_eq!(field.count_selected_items(), 2);
}
{
let field = make_choice_field(vec![1, 4]);
assert_eq!(field.selected_index(0), Some(1));
assert_eq!(field.selected_index(1), Some(4));
assert_eq!(field.selected_index(2), None);
}
}
}