use crate::content::{parse_content, Op};
use crate::hash::FastSet;
use crate::object::{decode_text_string, Dict, ObjRef, Object};
use crate::source::AsyncObjectSource;
use crate::tree::{resolved_dict, Entries};
const MAX_FIELD_DEPTH: usize = 64;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct SignatureFlags {
pub signatures_exist: bool,
pub append_only: bool,
}
impl SignatureFlags {
pub fn from_bits(bits: i64) -> SignatureFlags {
SignatureFlags {
signatures_exist: bits & 1 != 0,
append_only: bits & 2 != 0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Quadding {
#[default]
Left,
Centered,
Right,
}
impl Quadding {
pub fn from_int(value: i64) -> Option<Quadding> {
match value {
0 => Some(Quadding::Left),
1 => Some(Quadding::Centered),
2 => Some(Quadding::Right),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct DefaultAppearance {
pub font: Option<String>,
pub font_size: Option<f32>,
pub fill_color: Option<Vec<f32>>,
}
impl DefaultAppearance {
pub fn parse(da: &str) -> DefaultAppearance {
let mut appearance = DefaultAppearance::default();
let Ok(ops) = parse_content(da.as_bytes()) else {
return appearance;
};
for op in ops {
match op {
Op::SetFont(name, size) => {
appearance.font = Some(name.0);
appearance.font_size = Some(size);
}
Op::SetFillGray(g) => appearance.fill_color = Some(vec![g]),
Op::SetFillRGB(r, g, b) => appearance.fill_color = Some(vec![r, g, b]),
Op::SetFillCMYK(c, m, y, k) => appearance.fill_color = Some(vec![c, m, y, k]),
_ => {}
}
}
appearance
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct InteractiveForm {
pub fields: Vec<ObjRef>,
pub need_appearances: bool,
pub signature_flags: SignatureFlags,
pub calculation_order: Vec<ObjRef>,
pub default_resources: Option<Dict>,
pub default_appearance: Option<String>,
pub quadding: Option<Quadding>,
pub xfa: bool,
}
pub async fn interactive_form_with<S: AsyncObjectSource>(
src: &S,
trailer: &Dict,
) -> Option<InteractiveForm> {
let catalog = resolved_dict(src, trailer.get("Root")?).await?;
let dict = resolved_dict(src, catalog.get("AcroForm")?).await?;
let entries = Entries { src, dict: &dict };
let fields = references(entries.value("Fields").await.as_ref());
let calculation_order = references(entries.value("CO").await.as_ref());
let need_appearances = entries.flag("NeedAppearances").await.unwrap_or(false);
let signature_flags = entries
.value("SigFlags")
.await
.and_then(|bits| bits.as_int())
.map_or_else(SignatureFlags::default, SignatureFlags::from_bits);
let default_resources = match dict.get("DR") {
Some(entry) => resolved_dict(src, entry).await,
None => None,
};
let default_appearance = entries
.value("DA")
.await
.and_then(|da| Some(String::from_utf8_lossy(da.as_str_bytes()?).into_owned()));
let quadding = entries
.value("Q")
.await
.and_then(|q| q.as_int())
.and_then(Quadding::from_int);
let xfa = matches!(
entries.value("XFA").await,
Some(Object::Stream(_)) | Some(Object::Array(_))
);
Some(InteractiveForm {
fields,
need_appearances,
signature_flags,
calculation_order,
default_resources,
default_appearance,
quadding,
xfa,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FieldType {
Button,
Text,
Choice,
Signature,
}
impl FieldType {
pub fn from_name(name: &str) -> Option<FieldType> {
match name {
"Btn" => Some(FieldType::Button),
"Tx" => Some(FieldType::Text),
"Ch" => Some(FieldType::Choice),
"Sig" => Some(FieldType::Signature),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct FieldFlags(pub u32);
impl FieldFlags {
pub fn read_only(self) -> bool {
self.0 & 1 != 0
}
pub fn required(self) -> bool {
self.0 & 2 != 0
}
pub fn no_export(self) -> bool {
self.0 & 4 != 0
}
pub fn multiline(self) -> bool {
self.bit(13)
}
pub fn password(self) -> bool {
self.bit(14)
}
pub fn file_select(self) -> bool {
self.bit(21)
}
pub fn do_not_spell_check(self) -> bool {
self.bit(23)
}
pub fn do_not_scroll(self) -> bool {
self.bit(24)
}
pub fn comb(self) -> bool {
self.bit(25)
}
pub fn rich_text(self) -> bool {
self.bit(26)
}
pub fn combo(self) -> bool {
self.bit(18)
}
pub fn edit(self) -> bool {
self.bit(19)
}
pub fn sort(self) -> bool {
self.bit(20)
}
pub fn multi_select(self) -> bool {
self.bit(22)
}
pub fn commit_on_sel_change(self) -> bool {
self.bit(27)
}
pub fn no_toggle_to_off(self) -> bool {
self.bit(15)
}
pub fn radio(self) -> bool {
self.bit(16)
}
pub fn pushbutton(self) -> bool {
self.bit(17)
}
pub fn radios_in_unison(self) -> bool {
self.bit(26)
}
fn bit(self, position: u32) -> bool {
self.0 & (1 << (position - 1)) != 0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Widget {
pub object: ObjRef,
pub appearance_state: Option<String>,
pub on_state: Option<String>,
pub characteristics: Option<AppearanceCharacteristics>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct AppearanceCharacteristics {
pub caption: Option<String>,
pub rollover_caption: Option<String>,
pub alternate_caption: Option<String>,
pub icon: Option<ObjRef>,
pub rollover_icon: Option<ObjRef>,
pub alternate_icon: Option<ObjRef>,
pub caption_position: CaptionPosition,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CaptionPosition {
#[default]
CaptionOnly,
IconOnly,
Below,
Above,
Right,
Left,
Overlaid,
}
impl CaptionPosition {
pub fn from_int(code: i64) -> Option<CaptionPosition> {
Some(match code {
0 => CaptionPosition::CaptionOnly,
1 => CaptionPosition::IconOnly,
2 => CaptionPosition::Below,
3 => CaptionPosition::Above,
4 => CaptionPosition::Right,
5 => CaptionPosition::Left,
6 => CaptionPosition::Overlaid,
_ => return None,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ButtonKind {
PushButton,
CheckBox,
RadioButtons,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Signature {
pub filter: Option<String>,
pub sub_filter: Option<String>,
pub byte_range: Vec<(u64, u64)>,
pub contents: Vec<u8>,
pub name: Option<String>,
pub signing_time: Option<String>,
pub location: Option<String>,
pub reason: Option<String>,
pub contact_info: Option<String>,
}
impl Signature {
pub fn from_dict(dict: &Dict) -> Signature {
let text = |key: &str| Some(decode_text_string(dict.get(key)?.as_str_bytes()?));
let name = |key: &str| Some(dict.get_name(key)?.0.clone());
Signature {
filter: name("Filter"),
sub_filter: name("SubFilter"),
byte_range: byte_range(dict.get("ByteRange")),
contents: dict
.get("Contents")
.and_then(Object::as_str_bytes)
.map(<[u8]>::to_vec)
.unwrap_or_default(),
name: text("Name"),
signing_time: text("M"),
location: text("Location"),
reason: text("Reason"),
contact_info: text("ContactInfo"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChoiceOption {
pub export_value: String,
pub name: String,
}
#[derive(Debug, Clone, PartialEq)]
pub struct FormField {
pub object: ObjRef,
pub parent: Option<ObjRef>,
pub kids: Vec<ObjRef>,
pub widgets: Vec<Widget>,
pub field_type: Option<FieldType>,
pub partial_name: Option<String>,
pub name: String,
pub alternate_name: Option<String>,
pub mapping_name: Option<String>,
pub flags: FieldFlags,
pub value: Option<Object>,
pub default_value: Option<Object>,
pub default_appearance: Option<String>,
pub quadding: Quadding,
pub default_style: Option<String>,
pub rich_text: Option<String>,
pub max_len: Option<u32>,
pub options: Vec<ChoiceOption>,
pub top_index: u32,
pub selected_indices: Vec<u32>,
pub additional_actions: Option<Dict>,
pub lock: Option<ObjRef>,
pub seed_value: Option<ObjRef>,
}
impl FormField {
pub fn text(&self) -> Option<String> {
if self.field_type != Some(FieldType::Text) {
return None;
}
Some(decode_text_string(self.value.as_ref()?.as_str_bytes()?))
}
pub fn button_kind(&self) -> Option<ButtonKind> {
if self.field_type != Some(FieldType::Button) {
return None;
}
Some(if self.flags.pushbutton() {
ButtonKind::PushButton
} else if self.flags.radio() {
ButtonKind::RadioButtons
} else {
ButtonKind::CheckBox
})
}
pub fn state(&self) -> Option<&str> {
match self.button_kind()? {
ButtonKind::PushButton => None,
ButtonKind::CheckBox | ButtonKind::RadioButtons => match &self.value {
None => Some("Off"),
Some(value) => Some(value.as_name()?.0.as_str()),
},
}
}
pub fn checked(&self) -> Option<bool> {
(self.button_kind()? == ButtonKind::CheckBox)
.then(|| self.state().is_some_and(|state| state != "Off"))
}
pub fn on_widgets(&self) -> Vec<usize> {
let Some(state) = self.state().filter(|state| *state != "Off") else {
return Vec::new();
};
self.widgets
.iter()
.enumerate()
.filter(|(_, widget)| widget.on_state.as_deref() == Some(state))
.map(|(index, _)| index)
.collect()
}
pub fn signature(&self) -> Option<Signature> {
if self.field_type != Some(FieldType::Signature) {
return None;
}
Some(Signature::from_dict(self.value.as_ref()?.as_dict()?))
}
pub fn selected(&self) -> Vec<String> {
if self.field_type != Some(FieldType::Choice) {
return Vec::new();
}
match &self.value {
Some(Object::String(name)) => vec![decode_text_string(name)],
Some(Object::Array(names)) => names
.iter()
.filter_map(|name| Some(decode_text_string(name.as_str_bytes()?)))
.collect(),
_ => Vec::new(),
}
}
}
pub async fn form_fields_with<S: AsyncObjectSource>(src: &S, trailer: &Dict) -> Vec<FormField> {
let mut fields = Vec::new();
let Some(form) = interactive_form_with(src, trailer).await else {
return fields;
};
let mut visited: FastSet<ObjRef> = FastSet::default();
let mut stack = Vec::new();
let defaults = Inherited {
default_appearance: form.default_appearance,
quadding: form.quadding,
..Inherited::default()
};
for object in form.fields.into_iter().rev() {
if let Some(dict) = field_dict(src, object).await {
stack.push(Pending {
object,
dict,
parent: None,
inherited: defaults.clone(),
depth: 0,
});
}
}
while let Some(pending) = stack.pop() {
if !visited.insert(pending.object) {
continue;
}
let (field, children) = read_field(src, pending, &visited).await;
fields.push(field);
stack.extend(children.into_iter().rev());
}
fields
}
#[derive(Clone, Default)]
struct Inherited {
field_type: Option<FieldType>,
flags: Option<FieldFlags>,
value: Option<Object>,
default_value: Option<Object>,
default_appearance: Option<String>,
quadding: Option<Quadding>,
name: String,
max_len: Option<u32>,
options: Vec<ChoiceOption>,
}
struct Pending {
object: ObjRef,
dict: Dict,
parent: Option<ObjRef>,
inherited: Inherited,
depth: usize,
}
async fn read_field<S: AsyncObjectSource>(
src: &S,
pending: Pending,
visited: &FastSet<ObjRef>,
) -> (FormField, Vec<Pending>) {
let Pending {
object,
dict,
parent,
inherited,
depth,
} = pending;
let entries = Entries { src, dict: &dict };
let field_type = entries
.value("FT")
.await
.and_then(|ft| FieldType::from_name(&ft.as_name()?.0))
.or(inherited.field_type);
let flags = entries
.value("Ff")
.await
.and_then(|ff| u32::try_from(ff.as_int()?).ok())
.map(FieldFlags)
.or(inherited.flags)
.unwrap_or_default();
let value = match entries.value("V").await.or(inherited.value) {
Some(Object::Stream(stream)) if field_type == Some(FieldType::Text) => {
src.stream_data(&stream).await.ok().map(Object::String)
}
value => value,
};
let default_value = entries.value("DV").await.or(inherited.default_value);
let default_appearance = entries
.value("DA")
.await
.and_then(|da| Some(String::from_utf8_lossy(da.as_str_bytes()?).into_owned()))
.or(inherited.default_appearance);
let quadding = entries
.value("Q")
.await
.and_then(|q| Quadding::from_int(q.as_int()?))
.or(inherited.quadding);
let default_style = entries.text("DS").await;
let rich_text = match entries.value("RV").await {
Some(Object::Stream(stream)) => src
.stream_data(&stream)
.await
.ok()
.map(|data| decode_text_string(&data)),
Some(value) => value.as_str_bytes().map(decode_text_string),
None => None,
};
let max_len = entries
.value("MaxLen")
.await
.and_then(|len| u32::try_from(len.as_int()?).ok())
.or(inherited.max_len);
let options = match entries.value("Opt").await {
Some(opt) => choice_options(src, &opt).await,
None => inherited.options,
};
let top_index = entries
.value("TI")
.await
.and_then(|ti| u32::try_from(ti.as_int()?).ok())
.unwrap_or(0);
let selected_indices = entries
.value("I")
.await
.and_then(|indices| {
Some(
indices
.as_array()?
.iter()
.filter_map(|index| u32::try_from(index.as_int()?).ok())
.collect(),
)
})
.unwrap_or_default();
let partial_name = entries.text("T").await;
let name = qualified_name(&inherited.name, partial_name.as_deref());
let additional_actions = match dict.get("AA") {
Some(entry) => resolved_dict(src, entry).await,
None => None,
};
let mut widgets = Vec::new();
if is_widget(&dict) {
widgets.push(widget(src, object, &dict).await);
}
let mut kids = Vec::new();
let mut children = Vec::new();
if depth < MAX_FIELD_DEPTH {
for kid in references(entries.value("Kids").await.as_ref()) {
if visited.contains(&kid) {
continue;
}
let Some(kid_dict) = field_dict(src, kid).await else {
continue;
};
if is_widget(&kid_dict) && kid_dict.get("T").is_none() {
widgets.push(widget(src, kid, &kid_dict).await);
continue;
}
kids.push(kid);
children.push(Pending {
object: kid,
dict: kid_dict,
parent: Some(object),
inherited: Inherited {
field_type,
flags: Some(flags),
value: value.clone(),
default_value: default_value.clone(),
default_appearance: default_appearance.clone(),
quadding,
name: name.clone(),
max_len,
options: options.clone(),
},
depth: depth + 1,
});
}
}
let field = FormField {
object,
parent,
kids,
widgets,
field_type,
partial_name,
name,
alternate_name: entries.text("TU").await,
mapping_name: entries.text("TM").await,
flags,
value,
default_value,
default_appearance,
quadding: quadding.unwrap_or_default(),
default_style,
rich_text,
max_len,
options,
top_index,
selected_indices,
additional_actions,
lock: dict.get("Lock").and_then(Object::as_ref),
seed_value: dict.get("SV").and_then(Object::as_ref),
};
(field, children)
}
fn qualified_name(parent: &str, partial: Option<&str>) -> String {
match partial {
Some(partial) if parent.is_empty() => partial.to_string(),
Some(partial) => format!("{parent}.{partial}"),
None => parent.to_string(),
}
}
async fn choice_options<S: AsyncObjectSource>(src: &S, opt: &Object) -> Vec<ChoiceOption> {
let Some(items) = opt.as_array() else {
return Vec::new();
};
let mut options = Vec::new();
for item in items {
let Ok(item) = src.resolve(item).await else {
continue;
};
match &item {
Object::String(name) => {
let name = decode_text_string(name);
options.push(ChoiceOption {
export_value: name.clone(),
name,
});
}
Object::Array(pair) => {
let [export_value, name] = pair.as_slice() else {
continue;
};
let (Some(export_value), Some(name)) =
(export_value.as_str_bytes(), name.as_str_bytes())
else {
continue;
};
options.push(ChoiceOption {
export_value: decode_text_string(export_value),
name: decode_text_string(name),
});
}
_ => {}
}
}
options
}
fn byte_range(value: Option<&Object>) -> Vec<(u64, u64)> {
let Some(items) = value.and_then(Object::as_array) else {
return Vec::new();
};
items
.as_chunks::<2>()
.0
.iter()
.map_while(|[offset, length]| {
Some((
u64::try_from(offset.as_int()?).ok()?,
u64::try_from(length.as_int()?).ok()?,
))
})
.collect()
}
async fn field_dict<S: AsyncObjectSource>(src: &S, object: ObjRef) -> Option<Dict> {
match src.get(object).await.ok()? {
Object::Dict(dict) => Some(dict),
_ => None,
}
}
async fn widget<S: AsyncObjectSource>(src: &S, object: ObjRef, dict: &Dict) -> Widget {
Widget {
object,
appearance_state: dict.get_name("AS").map(|state| state.0.clone()),
on_state: on_state(src, dict).await,
characteristics: characteristics(src, dict).await,
}
}
async fn characteristics<S: AsyncObjectSource>(
src: &S,
dict: &Dict,
) -> Option<AppearanceCharacteristics> {
let mk = resolved_dict(src, dict.get("MK")?).await?;
let entries = Entries { src, dict: &mk };
let reference = |key: &str| mk.get(key).and_then(Object::as_ref);
Some(AppearanceCharacteristics {
caption: entries.text("CA").await,
rollover_caption: entries.text("RC").await,
alternate_caption: entries.text("AC").await,
icon: reference("I"),
rollover_icon: reference("RI"),
alternate_icon: reference("IX"),
caption_position: entries
.value("TP")
.await
.and_then(|code| CaptionPosition::from_int(code.as_int()?))
.unwrap_or_default(),
})
}
async fn on_state<S: AsyncObjectSource>(src: &S, dict: &Dict) -> Option<String> {
let appearance = resolved_dict(src, dict.get("AP")?).await?;
let Object::Dict(states) = src.resolve(appearance.get("N")?).await.ok()? else {
return None;
};
let mut on = states
.iter()
.map(|(state, _)| state)
.filter(|state| state.0 != "Off");
let state = on.next()?;
on.next().is_none().then(|| state.0.clone())
}
fn is_widget(dict: &Dict) -> bool {
dict.get_name("Subtype")
.is_some_and(|subtype| subtype.0 == "Widget")
}
fn references(value: Option<&Object>) -> Vec<ObjRef> {
value
.and_then(Object::as_array)
.map(|items| items.iter().filter_map(Object::as_ref).collect())
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::{
AppearanceCharacteristics, ButtonKind, CaptionPosition, ChoiceOption, DefaultAppearance,
FieldFlags, FieldType, FormField, InteractiveForm, Quadding, Signature, SignatureFlags,
Widget,
};
use crate::object::{Name, ObjRef, Object};
use crate::Document;
use pdfboss_testkit::PdfBuilder;
fn doc(catalog_extra: &str, objects: &[(u32, &str)]) -> Document {
let mut b = PdfBuilder::new();
b.object(
1,
&format!("<< /Type /Catalog /Pages 2 0 R {catalog_extra} >>"),
);
b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
for (num, body) in objects {
b.object(*num, body);
}
Document::load(b.build(1)).unwrap()
}
fn r(num: u32) -> ObjRef {
ObjRef { num, gen: 0 }
}
fn widget_refs(field: &FormField) -> Vec<ObjRef> {
field.widgets.iter().map(|widget| widget.object).collect()
}
fn doc_with_stream(
catalog_extra: &str,
objects: &[(u32, &str)],
stream: (u32, &str, &[u8]),
) -> Document {
let mut b = PdfBuilder::new();
b.object(
1,
&format!("<< /Type /Catalog /Pages 2 0 R {catalog_extra} >>"),
);
b.object(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>");
b.object(3, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
for (num, body) in objects {
b.object(*num, body);
}
b.stream(stream.0, stream.1, stream.2);
Document::load(b.build(1)).unwrap()
}
#[test]
fn variable_text_entries_inherit_and_fall_back_to_the_form() {
let document = doc(
"/AcroForm << /Fields [20 0 R 23 0 R] /DA (/Helv 0 Tf 0 g) /Q 1 >>",
&[
(
20,
"<< /T (parent) /FT /Tx /DA (/TiRo 12 Tf 1 0 0 rg) /Kids [21 0 R 22 0 R] >>",
),
(21, "<< /T (plain) /Parent 20 0 R >>"),
(
22,
"<< /T (styled) /Parent 20 0 R /Q 2 /DS (font: Arial) /RV (<p>Ada</p>) >>",
),
(23, "<< /T (bare) /FT /Tx >>"),
],
);
let fields = document.form_fields();
let by_name = |name: &str| fields.iter().find(|field| field.name == name).unwrap();
let parent = by_name("parent");
assert_eq!(
parent.default_appearance.as_deref(),
Some("/TiRo 12 Tf 1 0 0 rg")
);
assert_eq!(parent.quadding, Quadding::Centered);
let plain = by_name("parent.plain");
assert_eq!(
plain.default_appearance.as_deref(),
Some("/TiRo 12 Tf 1 0 0 rg")
);
assert_eq!(plain.quadding, Quadding::Centered);
assert_eq!(plain.default_style, None);
assert_eq!(plain.rich_text, None);
let styled = by_name("parent.styled");
assert_eq!(styled.quadding, Quadding::Right);
assert_eq!(styled.default_style.as_deref(), Some("font: Arial"));
assert_eq!(styled.rich_text.as_deref(), Some("<p>Ada</p>"));
let bare = by_name("bare");
assert_eq!(bare.default_appearance.as_deref(), Some("/Helv 0 Tf 0 g"));
assert_eq!(bare.quadding, Quadding::Centered);
let unstyled = doc(
"/AcroForm << /Fields [23 0 R 24 0 R] >>",
&[
(23, "<< /T (bare) /FT /Tx >>"),
(24, "<< /T (odd) /FT /Tx /Q 7 >>"),
],
);
for field in unstyled.form_fields() {
assert_eq!(field.default_appearance, None, "{}", field.name);
assert_eq!(field.quadding, Quadding::Left, "{}", field.name);
}
}
#[test]
fn rich_text_stream_reads_as_text() {
let document = doc_with_stream(
"/AcroForm << /Fields [20 0 R] >>",
&[(20, "<< /T (rich) /FT /Tx /RV 21 0 R >>")],
(21, "", b"<p>Ada</p>"),
);
let fields = document.form_fields();
assert_eq!(fields[0].rich_text.as_deref(), Some("<p>Ada</p>"));
}
#[test]
fn default_appearance_reads_font_size_and_fill_colour() {
let da = DefaultAppearance::parse("/Helv 0 Tf 0 g");
assert_eq!(da.font.as_deref(), Some("Helv"));
assert_eq!(da.font_size, Some(0.0));
assert_eq!(da.fill_color, Some(vec![0.0]));
let da = DefaultAppearance::parse("0 0 1 rg /TiRo 12 Tf 0 0 0 1 k");
assert_eq!(da.font.as_deref(), Some("TiRo"));
assert_eq!(da.font_size, Some(12.0));
assert_eq!(da.fill_color, Some(vec![0.0, 0.0, 0.0, 1.0]));
assert_eq!(DefaultAppearance::parse(""), DefaultAppearance::default());
assert_eq!(
DefaultAppearance::parse("/Helv Tf"),
DefaultAppearance::default()
);
}
#[test]
fn the_interactive_form_dictionary_is_read() {
let found = doc(
"/AcroForm 4 0 R",
&[
(
4,
"<< /Fields [5 0 R << /T (inline) >> 6 0 R] /NeedAppearances true \
/SigFlags 7 0 R /CO [6 0 R] /DR << /Font << /Helv 8 0 R >> >> \
/DA (/Helv 0 Tf 0 g) /Q 1 /XFA [(preamble) 9 0 R] >>",
),
(5, "<< /T (a) /FT /Tx >>"),
(6, "<< /T (b) /FT /Tx >>"),
(7, "3"),
(8, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>"),
],
)
.interactive_form()
.unwrap();
assert_eq!(found.fields, vec![r(5), r(6)]);
assert!(found.need_appearances);
assert_eq!(
found.signature_flags,
SignatureFlags {
signatures_exist: true,
append_only: true,
}
);
assert_eq!(found.calculation_order, vec![r(6)]);
assert!(found
.default_resources
.as_ref()
.and_then(|dr| dr.get_dict("Font"))
.is_some());
assert_eq!(found.default_appearance.as_deref(), Some("/Helv 0 Tf 0 g"));
assert_eq!(found.quadding, Some(Quadding::Centered));
assert!(found.xfa);
}
#[test]
fn missing_and_malformed_form_entries_take_the_defaults() {
assert_eq!(doc("", &[]).interactive_form(), None);
assert_eq!(doc("/AcroForm 5", &[]).interactive_form(), None);
assert_eq!(
doc("/AcroForm << >>", &[]).interactive_form(),
Some(InteractiveForm {
fields: Vec::new(),
need_appearances: false,
signature_flags: SignatureFlags::default(),
calculation_order: Vec::new(),
default_resources: None,
default_appearance: None,
quadding: None,
xfa: false,
})
);
let odd = doc(
"/AcroForm << /Fields 5 0 R /Q 7 /SigFlags 4 >>",
&[(5, "(x)")],
)
.interactive_form()
.unwrap();
assert_eq!(odd.fields, Vec::new());
assert_eq!(odd.quadding, None);
assert_eq!(odd.signature_flags, SignatureFlags::default());
}
#[test]
fn a_merged_field_is_read_with_itself_as_its_widget() {
let fields = doc(
"/AcroForm << /Fields [5 0 R] >>",
&[
(
5,
"<< /FT /Tx /T <FEFF004E0061006D0065> /TU (Your name) /TM (name_export) \
/Ff 7 /V (Ada) /DV 6 0 R /AA << /F 7 0 R >> /Subtype /Widget \
/Rect [0 0 10 10] >>",
),
(6, "(nobody)"),
],
)
.form_fields();
assert_eq!(fields.len(), 1);
let field = &fields[0];
assert_eq!(field.object, r(5));
assert_eq!(field.parent, None);
assert_eq!(field.kids, Vec::new());
assert_eq!(widget_refs(field), vec![r(5)]);
assert_eq!(field.field_type, Some(FieldType::Text));
assert_eq!(field.partial_name.as_deref(), Some("Name"));
assert_eq!(field.alternate_name.as_deref(), Some("Your name"));
assert_eq!(field.mapping_name.as_deref(), Some("name_export"));
assert!(field.flags.read_only() && field.flags.required() && field.flags.no_export());
assert_eq!(field.value, Some(Object::String(b"Ada".to_vec())));
assert_eq!(
field.default_value,
Some(Object::String(b"nobody".to_vec()))
);
assert!(field
.additional_actions
.as_ref()
.is_some_and(|aa| aa.get("F").is_some()));
}
#[test]
fn widget_kids_belong_to_their_field_and_are_not_fields() {
let fields = doc(
"/AcroForm << /Fields [5 0 R] >>",
&[
(
5,
"<< /FT /Btn /Ff 32768 /T (card) /V /visa /Kids [6 0 R 7 0 R] >>",
),
(
6,
"<< /Subtype /Widget /Parent 5 0 R /AS /visa /Rect [0 0 1 1] >>",
),
(
7,
"<< /Subtype /Widget /Parent 5 0 R /AS /Off /Rect [0 0 1 1] >>",
),
],
)
.form_fields();
assert_eq!(fields.len(), 1);
assert_eq!(fields[0].kids, Vec::new());
assert_eq!(widget_refs(&fields[0]), vec![r(6), r(7)]);
assert_eq!(fields[0].value, Some(Object::Name(Name("visa".into()))));
}
#[test]
fn children_inherit_the_inheritable_entries_from_their_parent() {
let fields = doc(
"/AcroForm << /Fields [5 0 R 9 0 R] >>",
&[
(5, "<< /T (personal) /FT /Tx /Ff 1 /V (from parent) /DV (reset) /Kids [6 0 R 7 0 R] >>"),
(6, "<< /T (first) /Parent 5 0 R /Subtype /Widget /Rect [0 0 1 1] >>"),
(7, "<< /T (last) /Parent 5 0 R /Ff 2 /V (own) /Kids [8 0 R] >>"),
(8, "<< /Subtype /Widget /Parent 7 0 R /Rect [0 0 1 1] >>"),
(9, "<< /T (other) /FT /Ch /Subtype /Widget /Rect [0 0 1 1] >>"),
],
)
.form_fields();
let objects: Vec<ObjRef> = fields.iter().map(|f| f.object).collect();
assert_eq!(objects, vec![r(5), r(6), r(7), r(9)]);
let root = &fields[0];
assert_eq!(root.kids, vec![r(6), r(7)]);
assert_eq!(widget_refs(root), Vec::new());
let first = &fields[1];
assert_eq!(first.parent, Some(r(5)));
assert_eq!(first.partial_name.as_deref(), Some("first"));
assert_eq!(first.field_type, Some(FieldType::Text));
assert_eq!(first.flags, FieldFlags(1));
assert_eq!(first.value, Some(Object::String(b"from parent".to_vec())));
assert_eq!(first.default_value, Some(Object::String(b"reset".to_vec())));
assert_eq!(widget_refs(first), vec![r(6)]);
let last = &fields[2];
assert_eq!(last.flags, FieldFlags(2));
assert_eq!(last.value, Some(Object::String(b"own".to_vec())));
assert_eq!(last.default_value, Some(Object::String(b"reset".to_vec())));
assert_eq!(widget_refs(last), vec![r(8)]);
assert_eq!(fields[3].field_type, Some(FieldType::Choice));
assert_eq!(fields[3].flags, FieldFlags::default());
}
#[test]
fn malformed_field_trees_are_walked_without_repeats() {
assert_eq!(doc("", &[]).form_fields(), Vec::new());
assert_eq!(doc("/AcroForm << >>", &[]).form_fields(), Vec::new());
let fields = doc(
"/AcroForm << /Fields [5 0 R 6 0 R 7 0 R] >>",
&[
(5, "(not a field)"),
(6, "<< /T (loop) /FT /Bogus /Kids [6 0 R 8 0 R 7 0 R] >>"),
(7, "<< /T (leaf) /FT /Sig >>"),
(8, "42"),
],
)
.form_fields();
let objects: Vec<ObjRef> = fields.iter().map(|f| f.object).collect();
assert_eq!(objects, vec![r(6), r(7)]);
assert_eq!(fields[0].field_type, None);
assert_eq!(fields[0].kids, vec![r(7)]);
assert_eq!(fields[1].parent, Some(r(6)));
assert_eq!(fields[1].field_type, Some(FieldType::Signature));
}
#[test]
fn fully_qualified_names_join_the_partial_names_with_periods() {
let fields = doc(
"/AcroForm << /Fields [5 0 R] >>",
&[
(5, "<< /T (PersonalData) /Kids [6 0 R] >>"),
(6, "<< /T (Address) /Parent 5 0 R /Kids [7 0 R] >>"),
(7, "<< /T (ZipCode) /FT /Tx /Parent 6 0 R >>"),
],
)
.form_fields();
let names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
assert_eq!(
names,
[
"PersonalData",
"PersonalData.Address",
"PersonalData.Address.ZipCode"
]
);
}
#[test]
fn fields_without_a_partial_name_share_their_parent_s_name() {
let fields = doc(
"/AcroForm << /Fields [5 0 R 8 0 R] >>",
&[
(5, "<< /T (choice) /FT /Btn /Kids [6 0 R 7 0 R] >>"),
(6, "<< /Parent 5 0 R /Kids [9 0 R] >>"),
(7, "<< /Parent 5 0 R /Subtype /Widget /Rect [0 0 1 1] >>"),
(8, "<< /FT /Tx /Kids [10 0 R] >>"),
(9, "<< /Parent 6 0 R /Subtype /Widget /Rect [0 0 1 1] >>"),
(10, "<< /T (lonely) /Parent 8 0 R >>"),
],
)
.form_fields();
let names: Vec<(ObjRef, &str)> =
fields.iter().map(|f| (f.object, f.name.as_str())).collect();
assert_eq!(
names,
[
(r(5), "choice"),
(r(6), "choice"),
(r(8), ""),
(r(10), "lonely")
]
);
assert_eq!(fields[1].partial_name, None);
}
#[test]
fn text_fields_decode_their_value_and_read_max_len_and_their_flags() {
let fields = doc(
"/AcroForm << /Fields [5 0 R] >>",
&[
(
5,
"<< /T (box) /FT /Tx /MaxLen 8 /Ff 16781312 /Kids [6 0 R] >>",
),
(6, "<< /T (line) /Parent 5 0 R /V <FEFF00480069> >>"),
],
)
.form_fields();
let line = &fields[1];
assert_eq!(line.text().as_deref(), Some("Hi"));
assert_eq!(line.max_len, Some(8));
assert!(line.flags.multiline() && line.flags.comb());
assert!(!line.flags.password() && !line.flags.file_select());
assert!(!line.flags.do_not_spell_check() && !line.flags.do_not_scroll());
assert!(!line.flags.rich_text());
assert_eq!(fields[0].text(), None);
let all = FieldFlags((1 << 25) | (1 << 24) | (1 << 23) | (1 << 22) | (1 << 20) | (1 << 13));
assert!(all.password() && all.file_select() && all.do_not_spell_check());
assert!(all.do_not_scroll() && all.comb() && all.rich_text());
assert!(!all.multiline());
}
#[test]
fn a_stream_value_of_a_text_field_is_read_as_its_text() {
let fields = doc_with_stream(
"/AcroForm << /Fields [5 0 R] >>",
&[(5, "<< /T (essay) /FT /Tx /V 6 0 R >>")],
(6, "", b"From a stream"),
)
.form_fields();
assert_eq!(fields[0].text().as_deref(), Some("From a stream"));
assert_eq!(
fields[0].value,
Some(Object::String(b"From a stream".to_vec()))
);
}
#[test]
fn other_field_types_and_odd_values_have_no_text() {
let fields = doc(
"/AcroForm << /Fields [5 0 R 6 0 R 7 0 R] >>",
&[
(5, "<< /T (check) /FT /Btn /V /Yes >>"),
(6, "<< /T (empty) /FT /Tx >>"),
(7, "<< /T (odd) /FT /Tx /V 42 /MaxLen -1 >>"),
],
)
.form_fields();
assert_eq!(fields[0].text(), None);
assert_eq!(fields[1].text(), None);
assert_eq!(fields[2].text(), None);
assert_eq!(fields[2].max_len, None);
}
#[test]
fn choice_fields_list_their_options_top_index_and_selection() {
let fields = doc(
"/AcroForm << /Fields [5 0 R] >>",
&[(
5,
"<< /T (colour) /FT /Ch /Ff 393216 /Opt [(Red) [(b) (Blue)] <FEFF0047>] \
/TI 1 /I [1] /V (Blue) >>",
)],
)
.form_fields();
let field = &fields[0];
assert_eq!(
field.options,
vec![
ChoiceOption {
export_value: "Red".into(),
name: "Red".into(),
},
ChoiceOption {
export_value: "b".into(),
name: "Blue".into(),
},
ChoiceOption {
export_value: "G".into(),
name: "G".into(),
},
]
);
assert_eq!(field.top_index, 1);
assert_eq!(field.selected_indices, vec![1]);
assert_eq!(field.selected(), vec!["Blue".to_string()]);
assert!(field.flags.combo() && field.flags.edit());
assert!(!field.flags.sort() && !field.flags.multi_select());
assert!(!field.flags.commit_on_sel_change());
}
#[test]
fn multi_select_choice_fields_hold_several_selected_names() {
let fields = doc(
"/AcroForm << /Fields [5 0 R] >>",
&[
(
5,
"<< /T (toppings) /FT /Ch /Ff 69730304 /Opt 6 0 R /Kids [8 0 R] >>",
),
(6, "[(a) 7 0 R (c)]"),
(7, "[(bee) (b)]"),
(8, "<< /Parent 5 0 R /V [(a) (c)] /I [0 2] >>"),
],
)
.form_fields();
let kid = &fields[1];
assert_eq!(kid.options.len(), 3);
assert_eq!(kid.options[1].export_value, "bee");
assert_eq!(kid.selected(), vec!["a".to_string(), "c".to_string()]);
assert_eq!(kid.selected_indices, vec![0, 2]);
assert!(kid.flags.sort() && kid.flags.multi_select() && kid.flags.commit_on_sel_change());
assert!(!kid.flags.combo());
assert_eq!(fields[0].selected(), Vec::<String>::new());
}
#[test]
fn choice_entries_default_for_other_types_and_skip_odd_values() {
let fields = doc(
"/AcroForm << /Fields [5 0 R 6 0 R 7 0 R] >>",
&[
(5, "<< /T (text) /FT /Tx /V (x) /Opt [(y)] >>"),
(6, "<< /T (bare) /FT /Ch >>"),
(
7,
"<< /T (odd) /FT /Ch /Opt [7 [(a) (b) (c)] (ok)] /TI -3 /I [-1 0] /V 5 >>",
),
],
)
.form_fields();
assert_eq!(fields[0].selected(), Vec::<String>::new());
assert_eq!(fields[0].options.len(), 1);
assert_eq!(fields[1].options, Vec::new());
assert_eq!(fields[1].top_index, 0);
assert_eq!(fields[1].selected(), Vec::<String>::new());
assert_eq!(fields[2].options.len(), 1);
assert_eq!(fields[2].options[0].name, "ok");
assert_eq!(fields[2].top_index, 0);
assert_eq!(fields[2].selected_indices, vec![0]);
assert_eq!(fields[2].selected(), Vec::<String>::new());
}
#[test]
fn button_fields_are_told_apart_by_their_flags() {
let fields = doc(
"/AcroForm << /Fields [5 0 R 6 0 R 7 0 R 8 0 R] >>",
&[
(5, "<< /T (go) /FT /Btn /Ff 65536 >>"),
(6, "<< /T (card) /FT /Btn /Ff 33603584 /V /visa >>"),
(7, "<< /T (urgent) /FT /Btn /V /Yes >>"),
(8, "<< /T (name) /FT /Tx /V (x) >>"),
],
)
.form_fields();
assert_eq!(fields[0].button_kind(), Some(ButtonKind::PushButton));
assert!(fields[0].flags.pushbutton() && !fields[0].flags.radio());
assert_eq!(fields[1].button_kind(), Some(ButtonKind::RadioButtons));
assert!(fields[1].flags.radio() && fields[1].flags.no_toggle_to_off());
assert!(fields[1].flags.radios_in_unison() && !fields[1].flags.pushbutton());
assert_eq!(fields[2].button_kind(), Some(ButtonKind::CheckBox));
assert!(!fields[2].flags.no_toggle_to_off() && !fields[2].flags.radios_in_unison());
assert_eq!(fields[3].button_kind(), None);
}
#[test]
fn a_button_field_s_state_is_its_value_name() {
let fields = doc(
"/AcroForm << /Fields [5 0 R 6 0 R 7 0 R 8 0 R 9 0 R] >>",
&[
(5, "<< /T (urgent) /FT /Btn /V /Yes >>"),
(6, "<< /T (card) /FT /Btn /Ff 32768 /V /cardbrand1 >>"),
(7, "<< /T (blank) /FT /Btn >>"),
(8, "<< /T (go) /FT /Btn /Ff 65536 /V /Yes >>"),
(9, "<< /T (odd) /FT /Btn /V (Yes) >>"),
],
)
.form_fields();
assert_eq!(fields[0].state(), Some("Yes"));
assert_eq!(fields[1].state(), Some("cardbrand1"));
assert_eq!(fields[2].state(), Some("Off"));
assert_eq!(fields[3].state(), None);
assert_eq!(fields[4].state(), None);
}
#[test]
fn a_check_box_is_checked_when_its_state_is_not_off() {
let fields = doc(
"/AcroForm << /Fields [5 0 R 6 0 R 7 0 R 8 0 R 9 0 R] >>",
&[
(
5,
"<< /T (urgent) /FT /Btn /V /Yes /AS /Yes /Subtype /Widget /Rect [0 0 1 1] \
/AP << /N << /Yes 10 0 R /Off 11 0 R >> >> >>",
),
(
6,
"<< /T (done) /FT /Btn /V /Off /AS /Off /Subtype /Widget /Rect [0 0 1 1] >>",
),
(7, "<< /T (blank) /FT /Btn >>"),
(8, "<< /T (card) /FT /Btn /Ff 32768 /V /a >>"),
(9, "<< /T (name) /FT /Tx /V (Yes) >>"),
],
)
.form_fields();
assert_eq!(fields[0].checked(), Some(true));
assert_eq!(
fields[0].widgets,
vec![Widget {
object: r(5),
appearance_state: Some("Yes".into()),
on_state: Some("Yes".into()),
characteristics: None,
}]
);
assert_eq!(fields[0].on_widgets(), vec![0]);
assert_eq!(fields[1].checked(), Some(false));
assert_eq!(fields[1].widgets[0].on_state, None);
assert_eq!(fields[1].on_widgets(), Vec::<usize>::new());
assert_eq!(fields[2].checked(), Some(false));
assert_eq!(fields[3].checked(), None);
assert_eq!(fields[4].checked(), None);
}
#[test]
fn check_box_widgets_are_matched_to_the_state_and_their_export_values() {
let fields = doc_with_stream(
"/AcroForm << /Fields [5 0 R 9 0 R] >>",
&[
(
5,
"<< /T (size) /FT /Btn /Opt [(small) (large)] /V /1 /Kids [6 0 R 7 0 R] >>",
),
(
6,
"<< /Subtype /Widget /Parent 5 0 R /AS /Off /Rect [0 0 1 1] \
/AP << /N << /0 10 0 R /Off 10 0 R >> >> >>",
),
(
7,
"<< /Subtype /Widget /Parent 5 0 R /AS /1 /Rect [0 0 1 1] \
/AP 8 0 R >>",
),
(8, "<< /N << /1 10 0 R /Off 10 0 R >> >>"),
(9, "<< /T (odd) /FT /Btn /V /Yes /Kids [11 0 R 12 0 R] >>"),
(
11,
"<< /Subtype /Widget /Parent 9 0 R /AP << /N 10 0 R >> /Rect [0 0 1 1] >>",
),
(
12,
"<< /Subtype /Widget /Parent 9 0 R /Rect [0 0 1 1] \
/AP << /N << /Yes 10 0 R /Also 10 0 R /Off 10 0 R >> >> >>",
),
],
(10, "/Type /XObject /Subtype /Form /BBox [0 0 1 1]", b""),
)
.form_fields();
let size = &fields[0];
assert_eq!(size.checked(), Some(true));
assert_eq!(size.on_widgets(), vec![1]);
assert_eq!(size.widgets[0].on_state.as_deref(), Some("0"));
assert_eq!(size.widgets[1].on_state.as_deref(), Some("1"));
assert_eq!(size.widgets[1].appearance_state.as_deref(), Some("1"));
assert_eq!(size.options[1].export_value, "large");
let odd = &fields[1];
assert_eq!(odd.widgets[0].on_state, None);
assert_eq!(odd.widgets[1].on_state, None);
assert_eq!(odd.on_widgets(), Vec::<usize>::new());
assert_eq!(odd.checked(), Some(true));
}
#[test]
fn a_radio_button_field_names_the_widget_that_is_on() {
let fields = doc_with_stream(
"/AcroForm << /Fields [10 0 R] >>",
&[
(
10,
"<< /FT /Btn /Ff 32768 /T (Credit card) /V /cardbrand1 /Opt [(Visa) (Master)] \
/Kids [11 0 R 12 0 R] >>",
),
(
11,
"<< /Parent 10 0 R /Subtype /Widget /AS /cardbrand1 /Rect [0 0 1 1] \
/AP << /N << /cardbrand1 8 0 R /Off 8 0 R >> >> >>",
),
(
12,
"<< /Parent 10 0 R /Subtype /Widget /AS /Off /Rect [0 0 1 1] \
/AP << /N << /cardbrand2 8 0 R /Off 8 0 R >> >> >>",
),
],
(8, "/Type /XObject /Subtype /Form /BBox [0 0 1 1]", b""),
)
.form_fields();
let card = &fields[0];
assert_eq!(card.button_kind(), Some(ButtonKind::RadioButtons));
assert_eq!(card.state(), Some("cardbrand1"));
assert_eq!(card.on_widgets(), vec![0]);
assert_eq!(card.widgets[0].on_state.as_deref(), Some("cardbrand1"));
assert_eq!(card.widgets[1].on_state.as_deref(), Some("cardbrand2"));
assert_eq!(card.widgets[1].appearance_state.as_deref(), Some("Off"));
assert_eq!(card.options[0].export_value, "Visa");
assert_eq!(card.checked(), None);
}
#[test]
fn radios_in_unison_are_on_together_and_a_valueless_radio_field_is_off() {
let fields = doc(
"/AcroForm << /Fields [5 0 R 9 0 R] >>",
&[
(5, "<< /T (size) /FT /Btn /Ff 33587200 /V /a /Kids [6 0 R 7 0 R 8 0 R] >>"),
(6, "<< /Parent 5 0 R /Subtype /Widget /AS /a /AP << /N << /a 6 0 R /Off 6 0 R >> >> >>"),
(7, "<< /Parent 5 0 R /Subtype /Widget /AS /a /AP << /N << /a 6 0 R /Off 6 0 R >> >> >>"),
(8, "<< /Parent 5 0 R /Subtype /Widget /AS /Off /AP << /N << /b 6 0 R /Off 6 0 R >> >> >>"),
(9, "<< /T (none) /FT /Btn /Ff 49152 /Kids [10 0 R] >>"),
(10, "<< /Parent 9 0 R /Subtype /Widget /AS /Off /AP << /N << /x 6 0 R /Off 6 0 R >> >> >>"),
],
)
.form_fields();
let size = &fields[0];
assert!(size.flags.radio() && size.flags.radios_in_unison());
assert_eq!(size.on_widgets(), vec![0, 1]);
let none = &fields[1];
assert!(none.flags.radio() && none.flags.no_toggle_to_off());
assert_eq!(none.state(), Some("Off"));
assert_eq!(none.on_widgets(), Vec::<usize>::new());
}
#[test]
fn a_pushbutton_s_widget_carries_its_captions_and_icons() {
let fields = doc_with_stream(
"/AcroForm << /Fields [5 0 R] >>",
&[
(
5,
"<< /T (go) /FT /Btn /Ff 65536 /V /Yes /Subtype /Widget /Rect [0 0 10 10] \
/MK 7 0 R >>",
),
(
7,
"<< /CA <FEFF00530065006E0064> /RC (Go!) /AC (Sending) /I 6 0 R /RI 6 0 R \
/IX 6 0 R /TP 2 /BC [0] /BG [1 1 1] >>",
),
],
(6, "/Type /XObject /Subtype /Form /BBox [0 0 1 1]", b""),
)
.form_fields();
let go = &fields[0];
assert_eq!(go.button_kind(), Some(ButtonKind::PushButton));
assert_eq!(go.state(), None);
assert_eq!(
go.widgets[0].characteristics,
Some(AppearanceCharacteristics {
caption: Some("Send".into()),
rollover_caption: Some("Go!".into()),
alternate_caption: Some("Sending".into()),
icon: Some(r(6)),
rollover_icon: Some(r(6)),
alternate_icon: Some(r(6)),
caption_position: CaptionPosition::Below,
})
);
}
#[test]
fn captions_default_and_odd_characteristics_read_as_absent() {
let fields = doc(
"/AcroForm << /Fields [5 0 R 6 0 R 7 0 R] >>",
&[
(5, "<< /T (urgent) /FT /Btn /V /Yes /Subtype /Widget /Rect [0 0 1 1] /MK << /CA (8) >> >>"),
(6, "<< /T (plain) /FT /Btn /Ff 65536 /Subtype /Widget /Rect [0 0 1 1] >>"),
(
7,
"<< /T (odd) /FT /Btn /Ff 65536 /Subtype /Widget /Rect [0 0 1 1] \
/MK << /CA 5 /I << /Type /XObject >> /TP 9 >> >>",
),
],
)
.form_fields();
assert_eq!(
fields[0].widgets[0].characteristics,
Some(AppearanceCharacteristics {
caption: Some("8".into()),
..AppearanceCharacteristics::default()
})
);
assert_eq!(fields[1].widgets[0].characteristics, None);
assert_eq!(
fields[2].widgets[0].characteristics,
Some(AppearanceCharacteristics::default())
);
assert_eq!(
CaptionPosition::from_int(6),
Some(CaptionPosition::Overlaid)
);
assert_eq!(CaptionPosition::from_int(7), None);
}
#[test]
fn a_signed_signature_field_exposes_its_signature_dictionary() {
let fields = doc(
"/AcroForm << /Fields [5 0 R] /SigFlags 3 >>",
&[
(5, "<< /T (sig) /FT /Sig /V 6 0 R /Lock 7 0 R /SV 8 0 R /Subtype /Widget /Rect [0 0 0 0] >>"),
(
6,
"<< /Type /Sig /Filter /Adobe.PPKLite /SubFilter /adbe.pkcs7.detached \
/ByteRange [0 100 200 50] /Contents <3031> /Name <FEFF00410064> \
/M (D:20260909100000Z) /Location (Berlin) /Reason (I agree) \
/ContactInfo (ada@example.org) >>",
),
(7, "<< /Type /SigFieldLock /Action /All >>"),
(8, "<< /Type /SV /Ff 1 >>"),
],
)
.form_fields();
let sig = &fields[0];
assert_eq!(sig.field_type, Some(FieldType::Signature));
assert_eq!(
sig.signature(),
Some(Signature {
filter: Some("Adobe.PPKLite".into()),
sub_filter: Some("adbe.pkcs7.detached".into()),
byte_range: vec![(0, 100), (200, 50)],
contents: b"01".to_vec(),
name: Some("Ad".into()),
signing_time: Some("D:20260909100000Z".into()),
location: Some("Berlin".into()),
reason: Some("I agree".into()),
contact_info: Some("ada@example.org".into()),
})
);
assert_eq!(sig.lock, Some(r(7)));
assert_eq!(sig.seed_value, Some(r(8)));
assert_eq!(sig.text(), None);
assert_eq!(sig.selected(), Vec::<String>::new());
}
#[test]
fn unsigned_fields_and_odd_signature_entries_read_as_absent() {
let fields = doc(
"/AcroForm << /Fields [5 0 R 6 0 R 7 0 R] >>",
&[
(5, "<< /T (unsigned) /FT /Sig /Lock << /Action /All >> >>"),
(6, "<< /T (name) /FT /Tx /V << /Filter /X >> >>"),
(
7,
"<< /T (odd) /FT /Sig /V << /Type /Sig /ByteRange [0 10 20] /Contents 5 >> >>",
),
],
)
.form_fields();
assert_eq!(fields[0].signature(), None);
assert_eq!(fields[0].lock, None);
assert_eq!(fields[0].seed_value, None);
assert_eq!(fields[1].signature(), None);
assert_eq!(
fields[2].signature(),
Some(Signature {
byte_range: vec![(0, 10)],
..Signature::default()
})
);
}
}