Skip to main content

pdfrum_edit/
form.rs

1//! The interactive form dictionary (ISO 32000-1 §12.7.2) on the write side,
2//! and the appearance characteristics (§12.5.6.19) a widget is drawn from.
3//!
4//! The catalog's `/AcroForm` is where a document says how its fields should be
5//! presented, as opposed to what any one of them holds; `/MK` is where one
6//! widget says how it should look. Values themselves go through
7//! `Document::save_form`, which lays each one out as it writes.
8
9use pdfrum_object::{Array, Dict, Name, ObjRef, Object, Resolve, names};
10use peniko::Color;
11
12use crate::{doc::EditDoc, error::Error};
13
14/// A form write either applies or names why it could not.
15type Result<T> = core::result::Result<T, Error>;
16
17/// A widget's appearance characteristics — its `/MK` dictionary.
18///
19/// The generator reads `/MK` on **every** regeneration, so writing one is
20/// enough to change how a field is drawn: there is no separate appearance to
21/// keep in step. An unset field leaves that key alone rather than writing a
22/// default, which is what lets this edit one characteristic of a widget
23/// without flattening the rest.
24///
25/// ```
26/// use pdfrum_edit::WidgetAppearance;
27/// use peniko::Color;
28///
29/// let mk = WidgetAppearance::new()
30///     .background(Color::from_rgb8(240, 240, 240))
31///     .border(Color::from_rgb8(0, 0, 0));
32/// // Characteristics left unset keep whatever the widget already had.
33/// assert_eq!(mk, mk.clone());
34/// ```
35#[derive(Debug, Clone, Default, PartialEq)]
36pub struct WidgetAppearance {
37    background: Option<Color>,
38    border: Option<Color>,
39    rotation: Option<i64>,
40    caption: Option<String>,
41}
42
43impl WidgetAppearance {
44    /// Characteristics that change nothing.
45    #[must_use]
46    pub fn new() -> Self {
47        Self::default()
48    }
49
50    /// `/BG`: the colour behind the field.
51    #[must_use]
52    pub fn background(mut self, color: Color) -> Self {
53        self.background = Some(color);
54        self
55    }
56
57    /// `/BC`: the border colour.
58    ///
59    /// A widget's border is drawn only when this names one — the `/BS` width
60    /// alone does not produce a visible edge.
61    #[must_use]
62    pub fn border(mut self, color: Color) -> Self {
63        self.border = Some(color);
64        self
65    }
66
67    /// `/R`: the widget's rotation within its rectangle, in degrees.
68    ///
69    /// Read as a quarter turn; anything else rounds down to one.
70    #[must_use]
71    pub fn rotation(mut self, degrees: i64) -> Self {
72        self.rotation = Some(degrees);
73        self
74    }
75
76    /// `/CA`: the caption a push button shows.
77    #[must_use]
78    pub fn caption(mut self, text: impl Into<String>) -> Self {
79        self.caption = Some(text.into());
80        self
81    }
82
83    /// These characteristics alone, as a fresh `/MK`.
84    ///
85    /// A created widget has nothing to merge into, so it takes this; an
86    /// existing one goes through [`Self::merge_into`] instead and keeps the
87    /// keys the builder says nothing about.
88    pub(crate) fn to_dict(&self) -> Dict {
89        self.merge_into(Dict::new())
90    }
91
92    /// Merges these characteristics into an existing `/MK`, keeping keys the
93    /// builder says nothing about.
94    fn merge_into(&self, mut mk: Dict) -> Dict {
95        if let Some(color) = self.background {
96            mk.insert(names::BG.clone(), color_object(color));
97        }
98        if let Some(color) = self.border {
99            mk.insert(names::BC.clone(), color_object(color));
100        }
101        if let Some(degrees) = self.rotation {
102            mk.insert(Name::from("R"), Object::Int(degrees));
103        }
104        if let Some(caption) = &self.caption {
105            mk.insert(
106                names::CA.clone(),
107                Object::Str(pdfrum_object::PdfString::literal(caption.as_bytes())),
108            );
109        }
110        mk
111    }
112}
113
114/// The `/MK` key. `pdfrum-doc` owns the constant; this crate spells it here
115/// rather than depending on that module's name table for one entry.
116pub(crate) fn mk_key() -> Name {
117    Name::from("MK")
118}
119
120/// An `/MK` colour: three clamped components, as the reader's `from_array`
121/// expects.
122fn color_object(color: Color) -> Object {
123    let [r, g, b, _] = color.components;
124    Object::Array(Array::of([
125        Object::Real(r.clamp(0.0, 1.0)),
126        Object::Real(g.clamp(0.0, 1.0)),
127        Object::Real(b.clamp(0.0, 1.0)),
128    ]))
129}
130
131/// Sets a widget annotation's `/MK` appearance characteristics.
132///
133/// `annot` is the widget's own object — the reference an
134/// [`add_annotation`](crate::add_annotation) returned, or one found by walking
135/// a page's `/Annots`. Characteristics the builder leaves unset keep whatever
136/// the widget already had.
137///
138/// The generated appearance follows automatically: `/BG` and `/BC` are read
139/// every time a widget's appearance is rebuilt, so this needs no companion
140/// call to redraw. A widget whose `/AP` is stale and which the document does
141/// not mark with [`set_need_appearances`] may still show its old face in a
142/// reader that trusts the stream, which is the ordinary appearance-staleness
143/// question rather than anything specific to `/MK`.
144///
145/// # Errors
146///
147/// [`Error::UnresolvedRef`](crate::Error) when `annot` names no dictionary.
148pub fn set_widget_appearance(
149    dest: &mut EditDoc<'_>,
150    annot: ObjRef,
151    appearance: &WidgetAppearance,
152) -> Result<()> {
153    let Some(dict) = dest
154        .fetch(annot)
155        .ok()
156        .and_then(|object| object.as_dict().cloned())
157    else {
158        return Err(Error::Object(pdfrum_object::Error::UnresolvedRef(annot)));
159    };
160
161    // `/MK` is usually direct, but an indirect one is edited in place so any
162    // other widget sharing it keeps the same characteristics.
163    match dict.raw(&mk_key()).cloned() {
164        Some(Object::Ref(mk_ref)) => {
165            let mk = dest
166                .fetch(mk_ref)
167                .ok()
168                .and_then(|object| object.as_dict().cloned())
169                .unwrap_or_default();
170            dest.replace(mk_ref, Object::Dict(appearance.merge_into(mk)));
171        }
172        existing => {
173            let mk = match existing {
174                Some(Object::Dict(mk)) => mk,
175                _ => Dict::new(),
176            };
177            let mut dict = dict;
178            dict.insert(mk_key(), Object::Dict(appearance.merge_into(mk)));
179            dest.replace(annot, Object::Dict(dict));
180        }
181    }
182    Ok(())
183}
184
185/// Sets or clears `/AcroForm /NeedAppearances`.
186///
187/// The flag asks a reader to build every field's appearance from its value and
188/// `/DA` rather than trust the `/AP` in the file. pdfrum draws appearances as
189/// it fills, so a saved form shows its values without this — but a document
190/// whose fields were filled by something else, or whose appearances are known
191/// to be stale, wants it set, and setting it is the one way to make a reader
192/// that disagrees with pdfrum's layout use its own.
193///
194/// `false` removes the key rather than writing `false`, which is the same
195/// thing to a reader — [`pdfrum_doc`](../pdfrum_doc/index.html) reads it
196/// strictly as a boolean, so a missing key and an explicit `false` both mean
197/// "trust the `/AP`".
198///
199/// A document with no `/AcroForm` gains one, because a flag with no form to
200/// hang on would be dropped by the next reader that rewrites the catalog.
201///
202/// # Errors
203///
204/// [`Error::NoDestinationCatalog`] when the document has no catalog to hold
205/// the form.
206///
207/// ```
208/// use std::sync::Arc;
209/// use pdfrum_edit::{EditDoc, SaveOptions, save, set_need_appearances};
210/// use pdfrum_object::{Resolve, names};
211/// use pdfrum_parser::{LoadOptions, load};
212///
213/// let bytes: Arc<[u8]> = Arc::from(&include_bytes!("../tests/files/hello.pdf")[..]);
214/// let doc = load(bytes, &LoadOptions::default())?;
215/// let mut edit = EditDoc::new(&doc);
216/// set_need_appearances(&mut edit, true)?;
217///
218/// let mut out = Vec::new();
219/// save(&edit, &SaveOptions::default(), &mut out)?;
220/// let reloaded = load(Arc::from(&out[..]), &LoadOptions::default())?;
221/// let catalog = reloaded.catalog().expect("a catalog");
222/// let form = catalog
223///     .dict(&names::ACRO_FORM, &reloaded)
224///     .expect("an /AcroForm");
225/// assert_eq!(
226///     form.get(&pdfrum_object::Name::from("NeedAppearances"), &reloaded)
227///         .and_then(|v| v.as_direct().and_then(pdfrum_object::Object::as_bool)),
228///     Some(true),
229/// );
230/// # Ok::<(), Box<dyn std::error::Error>>(())
231/// ```
232pub fn set_need_appearances(dest: &mut EditDoc<'_>, needed: bool) -> Result<()> {
233    let key = Name::from("NeedAppearances");
234    let Some(root) = dest.base().trailer().reference(names::ROOT) else {
235        return Err(Error::NoDestinationCatalog);
236    };
237    let Ok(fetched) = dest.fetch(root) else {
238        return Err(Error::NoDestinationCatalog);
239    };
240    let Some(mut catalog) = fetched.as_dict().cloned() else {
241        return Err(Error::NoDestinationCatalog);
242    };
243
244    // `/AcroForm` is usually indirect, and editing it in place keeps every
245    // other reference to it — the field tree's, most of all — pointing at the
246    // dictionary that now carries the flag.
247    match catalog.raw(names::ACRO_FORM).cloned() {
248        Some(Object::Ref(form_ref)) => {
249            let mut form = dest
250                .fetch(form_ref)
251                .ok()
252                .and_then(|object| object.as_dict().cloned())
253                .unwrap_or_default();
254            set_flag(&mut form, &key, needed);
255            dest.replace(form_ref, Object::Dict(form));
256        }
257        Some(Object::Dict(mut form)) => {
258            set_flag(&mut form, &key, needed);
259            catalog.insert(names::ACRO_FORM.clone(), Object::Dict(form));
260            dest.replace(root, Object::Dict(catalog));
261        }
262        _ => {
263            let mut form = Dict::new();
264            set_flag(&mut form, &key, needed);
265            catalog.insert(names::ACRO_FORM.clone(), Object::Dict(form));
266            dest.replace(root, Object::Dict(catalog));
267        }
268    }
269    Ok(())
270}
271
272/// Writes the flag, or removes it when it would say `false`.
273fn set_flag(form: &mut Dict, key: &Name, needed: bool) {
274    if needed {
275        form.insert(key.clone(), Object::Bool(true));
276    } else {
277        form.remove(key);
278    }
279}