use super::read::{acroform, fields}; use super::values::merged_appearance; use super::{Dict, Document, FieldKind, ObjRef, Object, Widget, appearance, name_bytes}; /// Whether the form asks viewers to draw its fields' appearances themselves (`/NeedAppearances`). pub fn need_appearances(doc: &Document) -> bool { acroform(doc).is_some_and(|af| matches!(af.get(b"NeedAppearances").map(|o| doc.resolve(o)).as_deref(), Some(Object::Bool(true)))) } /// A copy of `doc` in which form fields are drawn the way the file leaves to the viewer, for /// displaying the document (`None` when no widget needs it). Text and choice fields are drawn from /// their values: every one when the form sets `/NeedAppearances` (ISO 32000-2 Table 224: the /// writer left the appearances to the viewer), and otherwise each widget without a normal /// appearance (`/AP /N`), as typing the value would draw it (#3470). Check boxes and radio buttons /// with no normal appearance for their state (`/AS`) are drawn from their `/MK` background, border /// and caption, and with `/NeedAppearances` every one that has an `/MK` is, over its own /// appearance; push buttons with none are drawn from their `/MK` and `/BS` (Table 192). `doc` is /// not changed: saving writes the file as it was. pub fn with_field_appearances(doc: &Document) -> Option { with_field_appearances_cached(doc, &mut FieldAppearanceCache::default()) } /// [`with_field_appearances`], reusing the appearances `cache` drew for widgets whose inputs are /// the same objects as then (see [`FieldAppearanceCache`]). pub fn with_field_appearances_cached(doc: &Document, cache: &mut FieldAppearanceCache) -> Option { let all = need_appearances(doc); let mut copy: Option = None; for f in fields(doc) { for w in &f.widgets { let widget = doc.get(w.obj); let normal = widget .as_dict() .and_then(|d| d.get(b"AP")) .and_then(|ap| doc.resolve(ap).as_dict().and_then(|ap| ap.get(b"N").map(|n| doc.resolve(n)))); // A check box or radio button shows the appearance of its state (12.7.5.2.3): `/AS`, // else the value when it names this widget's on state (any check box value when the // widget names none), else Off. An Off state may go without one (it shows nothing); // an on state needs one. let state = match f.kind { FieldKind::CheckBox | FieldKind::Radio => { let mine = |v: &&String| w.on_state.as_ref().map_or(f.kind == FieldKind::CheckBox, |on| on == *v); let value = f.value.first().filter(|v| !v.is_empty() && v.as_str() != "Off").filter(mine); Some(w.state.clone().or_else(|| value.cloned()).unwrap_or_else(|| "Off".into())) } _ => None, }; // With /NeedAppearances a check box or radio button that describes its look in /MK is // drawn from it (Table 192), its own appearance or not; one without /MK keeps its own. let described = || widget.as_dict().and_then(|d| d.get(b"MK")).is_some_and(|mk| doc.resolve(mk).as_dict().is_some()); let missing = match f.kind { FieldKind::Text | FieldKind::Combo | FieldKind::List => normal.is_none() || all, FieldKind::PushButton => normal.is_none(), FieldKind::CheckBox | FieldKind::Radio => match (normal.as_deref(), state.as_deref()) { _ if all && described() => true, (None, _) => true, (Some(Object::Dict(states)), Some(s)) => s != "Off" && !states.contains(&name_bytes(s)), _ => false, }, FieldKind::Signature => false, }; if !missing { continue; } let inputs = appearance_inputs(doc, w); let stream = match cache.get(w.obj, &inputs) { Some(stream) => stream, None => { let stream = match f.kind { FieldKind::CheckBox | FieldKind::Radio => appearance::button_state_shown(doc, &f, w, state.as_deref() != Some("Off")), FieldKind::PushButton => appearance::push_button_shown(doc, &f, w), _ => appearance::field_appearance_drawn(doc, &f, w, &f.value, true).0, }; cache.insert(w.obj, inputs, &stream); stream } }; let target = copy.get_or_insert_with(|| doc.clone()); let mut stream = stream; // Thai glyphs are streams of their own. pdfcraft_fonts::shaped_text::store_streams(target, &mut stream.dict); let ap = target.add(Object::Stream(stream)); let mut fresh = Dict::new(); match &state { // The drawn look under its state's name, and that state as the widget's own. Some(s) => { let mut states = Dict::new(); states.set(name_bytes(s), Object::Ref(ap)); fresh.set(b"N".to_vec(), Object::Dict(states)); } None => fresh.set(b"N".to_vec(), Object::Ref(ap)), } let apd = merged_appearance(target, w.obj, &fresh); // Fails only when the widget isn't a dictionary; that widget then keeps its own look. target .update_dict(w.obj, |d| { d.set(b"AP".to_vec(), Object::Dict(apd)); if let Some(s) = &state { d.set(b"AS".to_vec(), Object::Name(name_bytes(s))); } }) .ok(); } } copy } /// The objects a widget's drawn appearance is built from, as the document holds them: the widget /// and the fields above it (value, `/DA`, `/Q`, flags, options, `/MK`, formatting actions), the /// form dictionary (`/DA`) and the fonts of its `/DR`, each with the objects its entries refer to /// (an indirect `/MK` or `/V`). Not the page: a field's rotation is its own `/MK /R`. fn appearance_inputs(doc: &Document, w: &Widget) -> Vec> { // The objects a dictionary's entries refer to. The tree links and the drawn appearance itself // are not inputs, and `/Kids` and `/Fields` can be long. let referred = |d: &Dict, inputs: &mut Vec>| { for (k, v) in d.iter() { if !matches!(&k[..], b"Kids" | b"Fields" | b"P" | b"Parent" | b"AP") && let Some(r) = v.as_ref() { inputs.push(doc.get(r)); } } }; let mut inputs = Vec::new(); let mut seen = std::collections::HashSet::new(); let mut node = Some(w.obj); // The /Parent chain; the seen-set and the cap end a cycle or an absurd depth. while let Some(r) = node.filter(|r| seen.insert(*r) && seen.len() <= 64) { let obj = doc.get(r); if let Some(d) = obj.as_dict() { node = d.reference(b"Parent"); referred(d, &mut inputs); } else { node = None; } inputs.push(obj); } let Some(root) = doc.root() else { return inputs }; let catalog = doc.get(root); let form = match catalog.as_dict().and_then(|c| c.reference(b"AcroForm")) { Some(r) => doc.get(r), None => catalog, }; if let Some(f) = form.as_dict() { referred(f, &mut inputs); // The fonts of /DR. An indirect /DR or /Font is listed through the entries that refer to // it; a direct one is part of the object that holds it. `held` only finds them: a copy // of a direct object is a new one each time, never an input. let held = |o: &Object| o.as_ref().map_or_else(|| std::sync::Arc::new(o.clone()), |r| doc.get(r)); if let Some(dr) = f.get(b"DR").map(held) && let Some(dr) = dr.as_dict() { referred(dr, &mut inputs); if let Some(font) = dr.get(b"Font").map(held) && let Some(fonts) = font.as_dict() { referred(fonts, &mut inputs); } } } inputs.push(form); inputs } /// Field appearances drawn for the display copy ([`with_field_appearances_cached`]), so a refresh /// redraws only the widgets whose inputs changed: a form that leaves every appearance to the /// viewer otherwise redrew all of its fields on each keystroke in any one of them (#3470). An /// entry holds the input objects it was drawn from and is used only while each is still the same /// object (`Arc::ptr_eq`); the document replaces an object it edits, so a changed input is a /// miss, never a stale hit. #[derive(Clone, Debug, Default)] pub struct FieldAppearanceCache { entries: std::collections::HashMap>, pdfcraft_cos::Stream)>, bytes: usize, hits: u64, builds: u64, } impl FieldAppearanceCache { /// Cached appearance data restarts past this instead of growing without bound. const MAX_BYTES: usize = 64 << 20; /// Forget every entry. pub fn clear(&mut self) { *self = Self::default(); } fn get(&mut self, w: ObjRef, inputs: &[std::sync::Arc]) -> Option { let (from, stream) = self.entries.get(&w)?; if from.len() != inputs.len() || !from.iter().zip(inputs).all(|(a, b)| std::sync::Arc::ptr_eq(a, b)) { return None; } self.hits += 1; Some(stream.clone()) } fn insert(&mut self, w: ObjRef, inputs: Vec>, stream: &pdfcraft_cos::Stream) { self.builds += 1; if self.bytes.saturating_add(stream.raw.len()) > Self::MAX_BYTES { self.clear(); } self.bytes = self.bytes.saturating_add(stream.raw.len()); if let Some((_, old)) = self.entries.insert(w, (inputs, stream.clone())) { self.bytes = self.bytes.saturating_sub(old.raw.len()); } } /// Cache hits so far (reused appearances); for tests. pub fn hits(&self) -> u64 { self.hits } /// Appearances drawn so far; for tests. pub fn builds(&self) -> u64 { self.builds } } /// The most each filter stage of the XMP metadata stream may decode for its PDF/A declaration to /// be read, the cap preflight's Standards panel reads it with: past it, metadata declares nothing /// there either. pub(crate) const MAX_XMP_BYTES: usize = 8 << 20; /// Whether the document's XMP metadata declares PDF/A conformance (the bytes `pdfaid:part`). /// Metadata a filter stage of which decodes past [`MAX_XMP_BYTES`] doesn't. pub fn declares_pdfa(doc: &Document) -> bool { let Some(root) = doc.root() else { return false }; let Some(m) = doc.get(root).as_dict().and_then(|c| c.get(b"Metadata").cloned()) else { return false }; let m = doc.resolve(&m); match &*m { // Not a `DecodeBudget`, which counts every stage against one allowance: preflight bounds // each stage, and the two must agree on metadata stored through a chain of filters. // `/DecodeParms` is read only when direct, as preflight and organize read metadata too: // PDF/A asks for it unfiltered, so only a file that breaks that reaches parameters here, // and the readers go to `with_direct_parms` together or they disagree. Object::Stream(s) => s.decoded_within(MAX_XMP_BYTES).is_ok_and(|b| b.windows(11).any(|w| w == b"pdfaid:part")), _ => false, } } /// Every field with a widget whose appearance PdfCraft drew with `?` for a character its font /// can't hold (`pdfcraft_fonts::UNDRAWN_MARK`): the field's name and that character, once per /// field. pub fn undrawn_fields(doc: &Document) -> Vec<(String, char)> { fields(doc) .into_iter() .filter_map(|f| { let c = f.widgets.iter().find_map(|w| doc.get(w.obj).as_dict().and_then(pdfcraft_fonts::undrawn_mark))?; Some((f.name, c)) }) .collect() }