use crate::capability::Capability;
use crate::error::{internal_error, Result};
use crate::license;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum FieldType {
Text,
TextArea,
Checkbox,
Radio,
Dropdown,
ListBox,
Signature,
Button,
}
#[derive(Debug, Clone)]
pub struct FormField {
pub name: String,
pub field_type: FieldType,
pub value: String,
pub required: bool,
pub read_only: bool,
}
pub struct PdfFormMut<'a> {
lopdf: &'a mut lopdf::Document,
license_override: Option<&'a str>,
}
impl std::fmt::Debug for PdfFormMut<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PdfFormMut").finish_non_exhaustive()
}
}
impl<'a> PdfFormMut<'a> {
pub(crate) fn new(lopdf: &'a mut lopdf::Document, license_override: Option<&'a str>) -> Self {
Self {
lopdf,
license_override,
}
}
pub fn set_text(&mut self, name: &str, value: &str) -> Result<&mut Self> {
self.require_fill()?;
set_field_value(self.lopdf, name, FieldKind::Text(value))?;
Ok(self)
}
pub fn set_checkbox(&mut self, name: &str, value: bool) -> Result<&mut Self> {
self.require_fill()?;
set_field_value(self.lopdf, name, FieldKind::Checkbox(value))?;
Ok(self)
}
pub fn set_radio(&mut self, name: &str, value: &str) -> Result<&mut Self> {
self.require_fill()?;
set_field_value(self.lopdf, name, FieldKind::Radio(value))?;
Ok(self)
}
pub fn set_dropdown(&mut self, name: &str, value: &str) -> Result<&mut Self> {
self.require_fill()?;
set_field_value(self.lopdf, name, FieldKind::Dropdown(value))?;
Ok(self)
}
fn require_fill(&self) -> Result<()> {
license::require_capability_with_override(Capability::AcroFormFill, self.license_override)
}
}
pub(crate) fn read_acroform_fields(doc: &lopdf::Document) -> Vec<FormField> {
use lopdf::Object;
let catalog_id = match doc.trailer.get(b"Root") {
Ok(Object::Reference(id)) => *id,
_ => return Vec::new(),
};
let catalog = match doc.get_object(catalog_id).and_then(|o| o.as_dict()) {
Ok(d) => d,
Err(_) => return Vec::new(),
};
let acroform = match catalog.get(b"AcroForm") {
Ok(Object::Reference(id)) => match doc.get_object(*id).and_then(|o| o.as_dict()) {
Ok(d) => d,
Err(_) => return Vec::new(),
},
Ok(Object::Dictionary(d)) => d,
_ => return Vec::new(),
};
let fields_array = match acroform.get(b"Fields") {
Ok(Object::Array(arr)) => arr,
_ => return Vec::new(),
};
let mut out = Vec::with_capacity(fields_array.len());
for field_obj in fields_array {
let field_dict = match field_obj {
Object::Reference(id) => match doc.get_object(*id).and_then(|o| o.as_dict()) {
Ok(d) => d,
Err(_) => continue,
},
Object::Dictionary(d) => d,
_ => continue,
};
let name = field_dict
.get(b"T")
.ok()
.and_then(|o| lopdf::decode_text_string(o).ok())
.unwrap_or_default();
let flags = field_dict
.get(b"Ff")
.ok()
.and_then(|o| match o {
Object::Integer(i) => Some(*i),
_ => None,
})
.unwrap_or(0);
let field_type = field_dict
.get(b"FT")
.ok()
.and_then(|o| match o {
Object::Name(bytes) => Some(bytes.as_slice()),
_ => None,
})
.map(|ft| classify_field_type(ft, flags))
.unwrap_or(FieldType::Text);
let value = field_dict
.get(b"V")
.ok()
.and_then(|o| match o {
Object::Name(bytes) => std::str::from_utf8(bytes).ok().map(str::to_owned),
_ => lopdf::decode_text_string(o).ok(),
})
.unwrap_or_default();
let read_only = (flags & 0x1) != 0;
let required = (flags & 0x2) != 0;
out.push(FormField {
name,
field_type,
value,
required,
read_only,
});
}
out
}
fn classify_field_type(ft: &[u8], flags: i64) -> FieldType {
match ft {
b"Tx" => {
if (flags & 0x1000) != 0 {
FieldType::TextArea
} else {
FieldType::Text
}
}
b"Btn" => {
if (flags & 0x10000) != 0 {
FieldType::Button
} else if (flags & 0x8000) != 0 {
FieldType::Radio
} else {
FieldType::Checkbox
}
}
b"Ch" => {
if (flags & 0x20000) != 0 {
FieldType::Dropdown
} else {
FieldType::ListBox
}
}
b"Sig" => FieldType::Signature,
_ => FieldType::Text,
}
}
enum FieldKind<'a> {
Text(&'a str),
Checkbox(bool),
Radio(&'a str),
Dropdown(&'a str),
}
fn set_field_value(doc: &mut lopdf::Document, name: &str, kind: FieldKind<'_>) -> Result<()> {
let target = locate_field(doc, name)?;
apply_value(doc, target, name, kind)
}
#[derive(Debug, Clone, Copy)]
enum FieldLocation {
Indirect(lopdf::ObjectId),
DirectInAcroForm {
acroform_id: lopdf::ObjectId,
index: usize,
},
}
fn locate_field(doc: &lopdf::Document, name: &str) -> Result<FieldLocation> {
use lopdf::Object;
let catalog_id = doc
.trailer
.get(b"Root")
.ok()
.and_then(|o| match o {
Object::Reference(id) => Some(*id),
_ => None,
})
.ok_or_else(|| internal_error("document has no /Root entry in trailer"))?;
let catalog = doc
.get_object(catalog_id)
.and_then(|o| o.as_dict())
.map_err(|_| internal_error("document catalog is not a dictionary"))?;
let acroform_id = match catalog.get(b"AcroForm") {
Ok(Object::Reference(id)) => *id,
Ok(Object::Dictionary(_)) => {
return Err(internal_error(
"document has an inline /AcroForm dictionary; form mutation requires an indirect AcroForm object",
));
}
_ => return Err(internal_error("document has no /AcroForm dictionary")),
};
let acroform = doc
.get_object(acroform_id)
.and_then(|o| o.as_dict())
.map_err(|_| internal_error("/AcroForm is not a dictionary"))?;
let fields_array = match acroform.get(b"Fields") {
Ok(Object::Array(arr)) => arr,
_ => return Err(internal_error("/AcroForm has no /Fields array")),
};
for (index, field_obj) in fields_array.iter().enumerate() {
let (field_dict, loc) = match field_obj {
Object::Reference(id) => {
let d = doc
.get_object(*id)
.and_then(|o| o.as_dict())
.map_err(|_| internal_error("field entry is not a dictionary"))?;
(d, FieldLocation::Indirect(*id))
}
Object::Dictionary(d) => (d, FieldLocation::DirectInAcroForm { acroform_id, index }),
_ => continue,
};
let t = field_dict
.get(b"T")
.ok()
.and_then(|o| lopdf::decode_text_string(o).ok())
.unwrap_or_default();
if t == name {
return Ok(loc);
}
}
Err(internal_error(format!("form field '{name}' not found")))
}
fn apply_value(
doc: &mut lopdf::Document,
loc: FieldLocation,
name: &str,
kind: FieldKind<'_>,
) -> Result<()> {
use lopdf::Object;
let checkbox_on_state = if let FieldKind::Checkbox(true) = kind {
Some(resolve_checkbox_on_state(doc, loc))
} else {
None
};
let field_dict: &mut lopdf::Dictionary = match loc {
FieldLocation::Indirect(id) => doc
.get_object_mut(id)
.and_then(|o| o.as_dict_mut())
.map_err(|_| internal_error("field object vanished between locate and apply"))?,
FieldLocation::DirectInAcroForm { acroform_id, index } => {
let acroform = doc
.get_object_mut(acroform_id)
.and_then(|o| o.as_dict_mut())
.map_err(|_| internal_error("/AcroForm not mutably accessible"))?;
let fields = match acroform.get_mut(b"Fields") {
Ok(Object::Array(arr)) => arr,
_ => return Err(internal_error("/AcroForm/Fields changed shape")),
};
match fields.get_mut(index) {
Some(Object::Dictionary(d)) => d,
_ => return Err(internal_error("direct field dict changed shape")),
}
}
};
let actual = field_type_of(field_dict);
match kind {
FieldKind::Text(value) => {
if !matches!(actual, FieldType::Text | FieldType::TextArea) {
return Err(wrong_type_error(name, actual, "text"));
}
field_dict.set("V", lopdf::text_string(value));
}
FieldKind::Checkbox(on) => {
if actual != FieldType::Checkbox {
return Err(wrong_type_error(name, actual, "checkbox"));
}
let state: Vec<u8> = if on {
checkbox_on_state.unwrap_or_else(|| b"Yes".to_vec())
} else {
b"Off".to_vec()
};
field_dict.set("V", Object::Name(state.clone()));
if field_dict.get(b"Kids").is_err() {
field_dict.set("AS", Object::Name(state));
}
}
FieldKind::Radio(value) => {
if actual != FieldType::Radio {
return Err(wrong_type_error(name, actual, "radio"));
}
field_dict.set("V", Object::Name(value.as_bytes().to_vec()));
}
FieldKind::Dropdown(value) => {
if !matches!(actual, FieldType::Dropdown | FieldType::ListBox) {
return Err(wrong_type_error(name, actual, "dropdown"));
}
field_dict.set("V", lopdf::text_string(value));
let _ = field_dict.remove(b"I");
}
}
Ok(())
}
fn field_type_of(field_dict: &lopdf::Dictionary) -> FieldType {
use lopdf::Object;
let flags = field_dict
.get(b"Ff")
.ok()
.and_then(|o| match o {
Object::Integer(i) => Some(*i),
_ => None,
})
.unwrap_or(0);
field_dict
.get(b"FT")
.ok()
.and_then(|o| match o {
Object::Name(bytes) => Some(bytes.as_slice()),
_ => None,
})
.map(|ft| classify_field_type(ft, flags))
.unwrap_or(FieldType::Text)
}
fn resolve_checkbox_on_state(doc: &lopdf::Document, loc: FieldLocation) -> Vec<u8> {
let default: Vec<u8> = b"Yes".to_vec();
let field_dict = match loc {
FieldLocation::Indirect(id) => match doc.get_object(id).and_then(|o| o.as_dict()) {
Ok(d) => d,
Err(_) => return default,
},
FieldLocation::DirectInAcroForm { acroform_id, index } => {
use lopdf::Object;
let acroform = match doc.get_object(acroform_id).and_then(|o| o.as_dict()) {
Ok(d) => d,
Err(_) => return default,
};
let fields = match acroform.get(b"Fields") {
Ok(Object::Array(arr)) => arr,
_ => return default,
};
match fields.get(index) {
Some(Object::Dictionary(d)) => d,
_ => return default,
}
}
};
if let Some(state) = on_state_from_ap(doc, field_dict) {
return state;
}
if let Ok(lopdf::Object::Array(kids)) = field_dict.get(b"Kids") {
for kid in kids {
let kid_dict = match kid {
lopdf::Object::Reference(id) => {
match doc.get_object(*id).and_then(|o| o.as_dict()) {
Ok(d) => d,
Err(_) => continue,
}
}
lopdf::Object::Dictionary(d) => d,
_ => continue,
};
if let Some(state) = on_state_from_ap(doc, kid_dict) {
return state;
}
}
}
default
}
fn on_state_from_ap(doc: &lopdf::Document, dict: &lopdf::Dictionary) -> Option<Vec<u8>> {
use lopdf::Object;
let ap = match dict.get(b"AP").ok()? {
Object::Reference(id) => doc.get_object(*id).and_then(|o| o.as_dict()).ok()?,
Object::Dictionary(d) => d,
_ => return None,
};
let n = match ap.get(b"N").ok()? {
Object::Reference(id) => doc.get_object(*id).and_then(|o| o.as_dict()).ok()?,
Object::Dictionary(d) => d,
_ => return None,
};
for (key, _) in n.iter() {
if key.as_slice() != b"Off" {
return Some(key.to_vec());
}
}
None
}
fn wrong_type_error(name: &str, actual: FieldType, requested: &str) -> crate::error::Error {
internal_error(format!(
"form field '{name}' has type {actual:?}; cannot apply {requested} mutation",
))
}