use tracing::warn;
use zpdf_core::{ObjectId, PdfDict, PdfName, PdfObject, Rect, Result};
use zpdf_document::{generate_widget_appearance, FieldKind, FieldValue, FormField, FF_READONLY};
use crate::metadata::encode_text_string;
use crate::{invalid_data, IncrementalWriter};
pub struct FormFiller<'w> {
writer: &'w mut IncrementalWriter,
need_appearances: bool,
dr_fonts: Option<PdfDict>,
}
impl<'w> FormFiller<'w> {
pub fn new(writer: &'w mut IncrementalWriter) -> Result<Self> {
let dr_fonts = writer
.document()
.acro_form()
.and_then(|af| af.dr_fonts.clone());
if writer.document().acro_form().is_none() {
return Err(invalid_data("document has no AcroForm; cannot fill fields").into());
}
Ok(Self {
writer,
need_appearances: false,
dr_fonts,
})
}
pub fn set(&mut self, name: &str, value: &str) -> Result<()> {
let form = self
.writer
.document()
.acro_form()
.ok_or_else(|| invalid_data("no AcroForm"))?;
let field = form
.fields
.iter()
.find(|f| f.name == name)
.ok_or_else(|| invalid_data(&format!("field not found: {name}")))?
.clone();
if field.kind == FieldKind::Signature {
return Err(invalid_data("cannot set value of signature fields").into());
}
if field.flags & FF_READONLY != 0 {
warn!("field {name} has read-only flag; setting value anyway");
}
match field.kind {
FieldKind::Text | FieldKind::Choice => self.set_text_choice(&field, value)?,
FieldKind::Button => self.set_button(&field, value)?,
_ => {}
}
Ok(())
}
pub fn finish(self) -> Result<()> {
if !self.need_appearances {
return Ok(());
}
let catalog_ref = self.writer.catalog_ref;
let catalog = self.writer.resolve_current(catalog_ref)?;
let catalog_dict = catalog.as_dict()?;
let af_ref = catalog_dict
.get("AcroForm")
.ok_or_else(|| invalid_data("catalog has no /AcroForm"))?
.clone();
match af_ref {
PdfObject::Ref(r) => {
let mut af_dict = self.writer.resolve_current(r)?.as_dict()?.clone();
af_dict.insert(PdfName::new("NeedAppearances"), PdfObject::Bool(true));
self.writer.overwrite_object(r, PdfObject::Dict(af_dict));
}
PdfObject::Dict(mut d) => {
d.insert(PdfName::new("NeedAppearances"), PdfObject::Bool(true));
let mut new_catalog = catalog_dict.clone();
new_catalog.insert(PdfName::new("AcroForm"), PdfObject::Dict(d));
self.writer
.overwrite_object(catalog_ref, PdfObject::Dict(new_catalog));
}
_ => {}
}
Ok(())
}
fn set_text_choice(&mut self, field: &FormField, value: &str) -> Result<()> {
let mut field_dict = self
.writer
.resolve_current(field.field_id)?
.as_dict()?
.clone();
field_dict.insert(
PdfName::new("V"),
PdfObject::String(encode_text_string(value)),
);
if field.kind == FieldKind::Choice {
field_dict.0.remove(&PdfName::new("I"));
}
let mut filled = field.clone();
filled.value = Some(FieldValue::Text(value.to_string()));
let mut prepared_widgets = Vec::with_capacity(field.widgets.len());
let mut appearance_count = 0usize;
for &widget_id in &field.widgets {
let widget_dict = self.writer.resolve_current(widget_id)?.as_dict()?.clone();
let rect = rect_from_array(widget_dict.get("Rect"))?;
if widget_will_have_appearance(field, value, rect) {
appearance_count = appearance_count
.checked_add(1)
.ok_or_else(|| invalid_data("too many widget appearances"))?;
}
prepared_widgets.push((widget_id, widget_dict, rect));
}
self.writer.ensure_object_capacity(appearance_count)?;
let mut widget_updates = Vec::with_capacity(prepared_widgets.len());
let mut need_appearances = false;
for (widget_id, mut widget_dict, rect) in prepared_widgets {
if let Some(ap) = generate_widget_appearance(&filled, rect, self.dr_fonts.as_ref()) {
let mut ap_dict = PdfDict::new();
ap_dict.insert(
PdfName::new("Type"),
PdfObject::Name(PdfName::new("XObject")),
);
ap_dict.insert(
PdfName::new("Subtype"),
PdfObject::Name(PdfName::new("Form")),
);
ap_dict.insert(PdfName::new("FormType"), PdfObject::Integer(1));
ap_dict.insert(
PdfName::new("BBox"),
PdfObject::Array(vec![
PdfObject::Real(ap.bbox.x0),
PdfObject::Real(ap.bbox.y0),
PdfObject::Real(ap.bbox.x1),
PdfObject::Real(ap.bbox.y1),
]),
);
ap_dict.insert(PdfName::new("Resources"), PdfObject::Dict(ap.resources));
let ap_ref = self.writer.try_add_flate_stream(&ap_dict, &ap.content)?;
let mut ap_outer = PdfDict::new();
ap_outer.insert(
PdfName::new("N"),
PdfObject::Ref(ObjectId(ap_ref.0, ap_ref.1 as u16)),
);
widget_dict.insert(PdfName::new("AP"), PdfObject::Dict(ap_outer));
} else {
need_appearances = true;
widget_dict.0.remove(&PdfName::new("AP"));
}
widget_updates.push((widget_id, widget_dict));
}
self.writer
.overwrite_object(field.field_id, PdfObject::Dict(field_dict));
for (widget_id, widget_dict) in widget_updates {
self.writer
.overwrite_object(widget_id, PdfObject::Dict(widget_dict));
}
self.need_appearances |= need_appearances;
Ok(())
}
fn set_button(&mut self, field: &FormField, value: &str) -> Result<()> {
let normalized = value.to_lowercase();
let state_name = match normalized.as_str() {
"off" | "false" | "" => "Off".to_string(),
"yes" | "true" | "on" => {
let on_states = self.collect_button_on_states(field)?;
if on_states.len() == 1 {
on_states.into_iter().next().unwrap()
} else {
return Err(invalid_data(&format!(
"ambiguous 'on' value for button field {}; specify exact state name",
field.name
))
.into());
}
}
_ => {
let on_states = self.collect_button_on_states(field)?;
if on_states.iter().any(|state| state == value) {
value.to_string()
} else if !field.options.is_empty() {
let idx = field
.options
.iter()
.position(|(exp, _)| exp == value)
.ok_or_else(|| invalid_data(&format!("option not found: {value}")))?;
if idx < field.widgets.len() {
let w = field.widgets[idx];
let wdict = self.writer.resolve_current(w)?.as_dict()?.clone();
let wstates = on_states_from_widget(&wdict, self.writer)?;
wstates
.into_iter()
.next()
.unwrap_or_else(|| "Yes".to_string())
} else {
return Err(invalid_data("option index out of widget range").into());
}
} else {
value.to_string()
}
}
};
let mut field_dict = self
.writer
.resolve_current(field.field_id)?
.as_dict()?
.clone();
field_dict.insert(
PdfName::new("V"),
PdfObject::Name(PdfName::new(&state_name)),
);
self.writer
.overwrite_object(field.field_id, PdfObject::Dict(field_dict));
for &widget_id in &field.widgets {
let widget_dict = self.writer.resolve_current(widget_id)?.as_dict()?.clone();
let states = on_states_from_widget(&widget_dict, self.writer)?;
let as_state = if states.contains(&state_name) {
&state_name
} else {
"Off"
};
let mut new_widget = widget_dict.clone();
new_widget.insert(PdfName::new("AS"), PdfObject::Name(PdfName::new(as_state)));
self.writer
.overwrite_object(widget_id, PdfObject::Dict(new_widget));
}
Ok(())
}
fn collect_button_on_states(&self, field: &FormField) -> Result<Vec<String>> {
let mut states = Vec::new();
for &w in &field.widgets {
let dict = self.writer.resolve_current(w)?.as_dict()?.clone();
states.extend(on_states_from_widget(&dict, self.writer)?);
}
Ok(states
.into_iter()
.collect::<std::collections::HashSet<_>>()
.into_iter()
.collect())
}
}
fn widget_will_have_appearance(field: &FormField, value: &str, rect: Rect) -> bool {
if !matches!(field.kind, FieldKind::Text | FieldKind::Choice) || field.is_password() {
return false;
}
let display = if field.kind == FieldKind::Choice {
field
.options
.iter()
.find(|(export, _)| export == value)
.map(|(_, display)| display.as_str())
.unwrap_or(value)
} else {
value
};
let rect = rect.normalize();
!display.is_empty() && rect.width() > 1.0 && rect.height() > 1.0
}
fn on_states_from_widget(dict: &PdfDict, writer: &IncrementalWriter) -> Result<Vec<String>> {
let ap = match dict.get("AP") {
Some(obj) => writer.deref_current(obj),
None => return Ok(Vec::new()),
};
let ap_dict = ap.as_dict()?;
let n = match ap_dict.get("N") {
Some(obj) => writer.deref_current(obj),
None => return Ok(Vec::new()),
};
let n_dict = match n.as_dict() {
Ok(d) => d,
Err(_) => return Ok(Vec::new()), };
Ok(n_dict
.0
.keys()
.map(|k| k.as_str().to_string())
.filter(|s| s != "Off")
.collect())
}
fn rect_from_array(obj: Option<&PdfObject>) -> Result<Rect> {
let arr = obj
.ok_or_else(|| invalid_data("/Rect missing"))?
.as_array()?;
if arr.len() != 4 {
return Err(invalid_data("/Rect array must have 4 elements").into());
}
let number = |o: &PdfObject| match o {
PdfObject::Real(f) if f.is_finite() => Ok(*f),
PdfObject::Integer(i) => Ok(*i as f64),
PdfObject::Real(_) => Err(invalid_data("/Rect values must be finite").into()),
_ => Err(zpdf_core::Error::TypeMismatch {
expected: "number",
actual: o.type_name(),
}),
};
let nums = [
number(&arr[0])?,
number(&arr[1])?,
number(&arr[2])?,
number(&arr[3])?,
];
Ok(Rect::new(nums[0], nums[1], nums[2], nums[3]))
}
#[cfg(test)]
mod tests {
use super::*;
fn pdf_with_text_field() -> Vec<u8> {
let objects = [
"<< /Type /Catalog /Pages 2 0 R /AcroForm 5 0 R >>",
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>",
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Annots [4 0 R] >>",
"<< /Type /Annot /Subtype /Widget /FT /Tx /T (name) /Rect [0 0 100 20] /P 3 0 R /DA (/Helv 12 Tf 0 g) >>",
"<< /Fields [4 0 R] /DA (/Helv 12 Tf 0 g) >>",
];
let mut pdf = b"%PDF-1.4\n".to_vec();
let mut offsets = Vec::new();
for (index, object) in objects.iter().enumerate() {
offsets.push(pdf.len());
pdf.extend_from_slice(format!("{} 0 obj\n{}\nendobj\n", index + 1, object).as_bytes());
}
let xref = pdf.len();
pdf.extend_from_slice(format!("xref\n0 {}\n", objects.len() + 1).as_bytes());
pdf.extend_from_slice(b"0000000000 65535 f \n");
for offset in offsets {
pdf.extend_from_slice(format!("{offset:010} 00000 n \n").as_bytes());
}
pdf.extend_from_slice(
format!(
"trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n",
objects.len() + 1
)
.as_bytes(),
);
pdf
}
#[test]
fn form_appearance_exhaustion_does_not_change_field_or_widget() {
let mut writer = IncrementalWriter::new(pdf_with_text_field()).unwrap();
writer.next_obj_num = u32::MAX;
{
let mut filler = FormFiller::new(&mut writer).unwrap();
assert!(filler.set("name", "updated").is_err());
}
assert!(writer.pending.is_empty());
assert_eq!(writer.next_obj_num, u32::MAX);
}
}