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}