Skip to main content

rpdfium_doc/
icon_fit.rs

1// Derived from PDFium's cpdf_iconfit.h/cpp
2// Original: Copyright 2014 The PDFium Authors
3// Licensed under BSD-3-Clause / Apache-2.0
4// See pdfium-upstream/LICENSE for the original license.
5
6//! Icon fit parameters for widget annotation buttons (`/IF` dictionary).
7//!
8//! Controls how a button icon is scaled and positioned within the annotation
9//! rectangle (ISO 32000-2 section 12.5.6.19, Table 196).
10//! Corresponds to upstream `CPDF_IconFit`.
11
12use std::collections::HashMap;
13
14use rpdfium_core::{Name, PdfSource};
15use rpdfium_parser::{Object, ObjectStore};
16
17/// Icon fit parameters parsed from an `/IF` dictionary inside `/MK`.
18///
19/// Controls how a button icon is scaled and positioned within the annotation
20/// rectangle (ISO 32000-2 section 12.5.6.19).
21#[derive(Debug, Clone)]
22pub struct IconFit {
23    /// How the icon should be scaled.
24    pub scale_method: ScaleMethod,
25    /// If `true`, scale proportionally; if `false`, scale anamorphically.
26    pub proportional: bool,
27    /// Horizontal position fraction (0.0 = left, 0.5 = center, 1.0 = right).
28    pub position_x: f32,
29    /// Vertical position fraction (0.0 = bottom, 0.5 = center, 1.0 = top).
30    pub position_y: f32,
31    /// If `true`, the button appearance is clipped to the annotation rectangle.
32    pub fitting_bounds: bool,
33}
34
35/// Icon scale method (from `/SW` key in IconFit dictionary).
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum ScaleMethod {
38    /// Always scale to fit (`A`, default).
39    Always,
40    /// Scale only if the icon is bigger than the annotation (`B`).
41    Bigger,
42    /// Scale only if the icon is smaller than the annotation (`S`).
43    Smaller,
44    /// Never scale (`N`).
45    Never,
46}
47
48impl IconFit {
49    /// Returns how the icon should be scaled.
50    ///
51    /// Corresponds to `CPDF_IconFit::GetScaleMethod()` in PDFium.
52    pub fn scale_method(&self) -> ScaleMethod {
53        self.scale_method
54    }
55
56    /// Upstream-aligned alias for [`scale_method()`](Self::scale_method).
57    ///
58    /// Corresponds to `CPDF_IconFit::GetScaleMethod()` in PDFium.
59    #[inline]
60    pub fn get_scale_method(&self) -> ScaleMethod {
61        self.scale_method()
62    }
63
64    /// Returns whether the icon is scaled proportionally.
65    ///
66    /// Corresponds to `CPDF_IconFit::IsProportionalScale()` in PDFium.
67    pub fn is_proportional_scale(&self) -> bool {
68        self.proportional
69    }
70
71    /// Returns whether the button appearance is clipped to the annotation rectangle.
72    ///
73    /// Corresponds to `CPDF_IconFit::GetFittingBounds()` in PDFium.
74    pub fn fitting_bounds(&self) -> bool {
75        self.fitting_bounds
76    }
77
78    /// Upstream-aligned alias for [`fitting_bounds()`](Self::fitting_bounds).
79    ///
80    /// Corresponds to `CPDF_IconFit::GetFittingBounds()` in PDFium.
81    #[inline]
82    pub fn get_fitting_bounds(&self) -> bool {
83        self.fitting_bounds()
84    }
85
86    /// Returns the icon bottom-left position fraction as `(x, y)`.
87    ///
88    /// `x` is 0.0 (left) to 1.0 (right); `y` is 0.0 (bottom) to 1.0 (top).
89    /// Corresponds to `CPDF_IconFit::GetIconBottomLeftPosition()` in PDFium.
90    pub fn icon_bottom_left_position(&self) -> (f32, f32) {
91        (self.position_x, self.position_y)
92    }
93
94    /// Upstream-aligned alias for [`icon_bottom_left_position()`](Self::icon_bottom_left_position).
95    ///
96    /// Corresponds to `CPDF_IconFit::GetIconBottomLeftPosition()` in PDFium.
97    #[inline]
98    pub fn get_icon_bottom_left_position(&self) -> (f32, f32) {
99        self.icon_bottom_left_position()
100    }
101
102    /// Compute scale factors for an icon of `image_width` x `image_height`
103    /// within a plate (annotation rect) of `plate_width` x `plate_height`.
104    pub fn compute_scale(
105        &self,
106        image_width: f32,
107        image_height: f32,
108        plate_width: f32,
109        plate_height: f32,
110    ) -> (f32, f32) {
111        let iw = image_width.max(1.0);
112        let ih = image_height.max(1.0);
113        let mut h_scale = match self.scale_method {
114            ScaleMethod::Always => plate_width / iw,
115            ScaleMethod::Bigger => {
116                if plate_width < image_width {
117                    plate_width / iw
118                } else {
119                    1.0
120                }
121            }
122            ScaleMethod::Smaller => {
123                if plate_width > image_width {
124                    plate_width / iw
125                } else {
126                    1.0
127                }
128            }
129            ScaleMethod::Never => 1.0,
130        };
131        let mut v_scale = match self.scale_method {
132            ScaleMethod::Always => plate_height / ih,
133            ScaleMethod::Bigger => {
134                if plate_height < image_height {
135                    plate_height / ih
136                } else {
137                    1.0
138                }
139            }
140            ScaleMethod::Smaller => {
141                if plate_height > image_height {
142                    plate_height / ih
143                } else {
144                    1.0
145                }
146            }
147            ScaleMethod::Never => 1.0,
148        };
149        if self.proportional {
150            let min_scale = h_scale.min(v_scale);
151            h_scale = min_scale;
152            v_scale = min_scale;
153        }
154        (h_scale, v_scale)
155    }
156
157    /// Compute the offset for positioning the icon within the plate.
158    pub fn compute_offset(
159        &self,
160        image_width: f32,
161        image_height: f32,
162        h_scale: f32,
163        v_scale: f32,
164        plate_width: f32,
165        plate_height: f32,
166    ) -> (f32, f32) {
167        let scaled_w = image_width * h_scale;
168        let scaled_h = image_height * v_scale;
169        (
170            (plate_width - scaled_w) * self.position_x,
171            (plate_height - scaled_h) * self.position_y,
172        )
173    }
174}
175
176/// Parse an IconFit dictionary from the `/IF` sub-key of an MK dictionary.
177pub fn parse_icon_fit<S: PdfSource>(
178    mk_dict: &HashMap<Name, Object>,
179    store: &ObjectStore<S>,
180) -> Option<IconFit> {
181    let if_obj = mk_dict.get(&Name::if_dict())?;
182    let resolved = store.deep_resolve(if_obj).ok()?;
183    let if_dict = resolved.as_dict()?;
184
185    let scale_method = if_dict
186        .get(&Name::sw())
187        .and_then(|o| o.as_name())
188        .map(|n| match n.as_str().as_ref() {
189            "B" => ScaleMethod::Bigger,
190            "S" => ScaleMethod::Smaller,
191            "N" => ScaleMethod::Never,
192            _ => ScaleMethod::Always,
193        })
194        .unwrap_or(ScaleMethod::Always);
195
196    let proportional = if_dict
197        .get(&Name::s())
198        .and_then(|o| o.as_name())
199        .map(|n| n.as_str().as_ref() != "A")
200        .unwrap_or(true);
201
202    let (position_x, position_y) = if_dict
203        .get(&Name::a())
204        .and_then(|o| store.deep_resolve(o).ok())
205        .and_then(|o| {
206            let arr = o.as_array()?;
207            let x = arr.first()?.as_f64().map(|f| f as f32).unwrap_or(0.5);
208            let y = arr.get(1)?.as_f64().map(|f| f as f32).unwrap_or(0.5);
209            Some((x, y))
210        })
211        .unwrap_or((0.5, 0.5));
212
213    let fitting_bounds = if_dict
214        .get(&Name::fb())
215        .and_then(|o| match o {
216            Object::Boolean(b) => Some(*b),
217            _ => None,
218        })
219        .unwrap_or(false);
220
221    Some(IconFit {
222        scale_method,
223        proportional,
224        position_x,
225        position_y,
226        fitting_bounds,
227    })
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    fn build_store() -> ObjectStore<Vec<u8>> {
235        let pdf = build_minimal_pdf();
236        ObjectStore::open(pdf, rpdfium_core::ParsingMode::Lenient).unwrap()
237    }
238
239    fn build_minimal_pdf() -> Vec<u8> {
240        let mut pdf = Vec::new();
241        pdf.extend_from_slice(b"%PDF-1.4\n");
242        let obj1_offset = pdf.len();
243        pdf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
244        let obj2_offset = pdf.len();
245        pdf.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n");
246        let xref_offset = pdf.len();
247        pdf.extend_from_slice(b"xref\n0 3\n");
248        pdf.extend_from_slice(b"0000000000 65535 f \r\n");
249        pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj1_offset).as_bytes());
250        pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj2_offset).as_bytes());
251        pdf.extend_from_slice(b"trailer\n<< /Size 3 /Root 1 0 R >>\n");
252        pdf.extend_from_slice(format!("startxref\n{}\n%%EOF", xref_offset).as_bytes());
253        pdf
254    }
255
256    #[test]
257    fn test_parse_icon_fit_defaults() {
258        let store = build_store();
259        let mut mk = HashMap::new();
260        mk.insert(Name::if_dict(), Object::Dictionary(HashMap::new()));
261        let result = parse_icon_fit(&mk, &store).unwrap();
262        assert_eq!(result.scale_method, ScaleMethod::Always);
263        assert!(result.proportional);
264        assert!((result.position_x - 0.5).abs() < 0.001);
265        assert!((result.position_y - 0.5).abs() < 0.001);
266        assert!(!result.fitting_bounds);
267    }
268
269    #[test]
270    fn test_parse_icon_fit_with_values() {
271        let store = build_store();
272        let mut if_dict = HashMap::new();
273        if_dict.insert(Name::sw(), Object::Name(Name::from("B")));
274        if_dict.insert(Name::s(), Object::Name(Name::from("A"))); // anamorphic
275        if_dict.insert(
276            Name::a(),
277            Object::Array(vec![Object::Real(0.0), Object::Real(1.0)]),
278        );
279        if_dict.insert(Name::fb(), Object::Boolean(true));
280
281        let mut mk = HashMap::new();
282        mk.insert(Name::if_dict(), Object::Dictionary(if_dict));
283        let result = parse_icon_fit(&mk, &store).unwrap();
284        assert_eq!(result.scale_method, ScaleMethod::Bigger);
285        assert!(!result.proportional); // "A" = anamorphic
286        assert!((result.position_x - 0.0).abs() < 0.001);
287        assert!((result.position_y - 1.0).abs() < 0.001);
288        assert!(result.fitting_bounds);
289    }
290
291    #[test]
292    fn test_parse_icon_fit_never_scale() {
293        let store = build_store();
294        let mut if_dict = HashMap::new();
295        if_dict.insert(Name::sw(), Object::Name(Name::from("N")));
296
297        let mut mk = HashMap::new();
298        mk.insert(Name::if_dict(), Object::Dictionary(if_dict));
299        let result = parse_icon_fit(&mk, &store).unwrap();
300        assert_eq!(result.scale_method, ScaleMethod::Never);
301    }
302
303    #[test]
304    fn test_parse_icon_fit_smaller_scale() {
305        let store = build_store();
306        let mut if_dict = HashMap::new();
307        if_dict.insert(Name::sw(), Object::Name(Name::from("S")));
308
309        let mut mk = HashMap::new();
310        mk.insert(Name::if_dict(), Object::Dictionary(if_dict));
311        let result = parse_icon_fit(&mk, &store).unwrap();
312        assert_eq!(result.scale_method, ScaleMethod::Smaller);
313    }
314
315    #[test]
316    fn test_parse_icon_fit_absent() {
317        let store = build_store();
318        let mk = HashMap::new();
319        assert!(parse_icon_fit(&mk, &store).is_none());
320    }
321
322    #[test]
323    fn test_icon_fit_always_scale() {
324        let fit = IconFit {
325            scale_method: ScaleMethod::Always,
326            proportional: false,
327            position_x: 0.5,
328            position_y: 0.5,
329            fitting_bounds: false,
330        };
331        let (h, v) = fit.compute_scale(100.0, 50.0, 200.0, 100.0);
332        assert!((h - 2.0).abs() < 0.001);
333        assert!((v - 2.0).abs() < 0.001);
334    }
335
336    #[test]
337    fn test_icon_fit_always_proportional() {
338        let fit = IconFit {
339            scale_method: ScaleMethod::Always,
340            proportional: true,
341            position_x: 0.5,
342            position_y: 0.5,
343            fitting_bounds: false,
344        };
345        let (h, v) = fit.compute_scale(100.0, 200.0, 200.0, 200.0);
346        // h_scale = 2.0, v_scale = 1.0, min = 1.0
347        assert!((h - 1.0).abs() < 0.001);
348        assert!((v - 1.0).abs() < 0.001);
349    }
350
351    #[test]
352    fn test_icon_fit_never_scale() {
353        let fit = IconFit {
354            scale_method: ScaleMethod::Never,
355            proportional: false,
356            position_x: 0.5,
357            position_y: 0.5,
358            fitting_bounds: false,
359        };
360        let (h, v) = fit.compute_scale(100.0, 50.0, 200.0, 100.0);
361        assert!((h - 1.0).abs() < 0.001);
362        assert!((v - 1.0).abs() < 0.001);
363    }
364
365    #[test]
366    fn test_icon_fit_bigger_icon_bigger() {
367        let fit = IconFit {
368            scale_method: ScaleMethod::Bigger,
369            proportional: false,
370            position_x: 0.5,
371            position_y: 0.5,
372            fitting_bounds: false,
373        };
374        // Icon (200x100) bigger than plate (100x50) → scale down
375        let (h, v) = fit.compute_scale(200.0, 100.0, 100.0, 50.0);
376        assert!((h - 0.5).abs() < 0.001);
377        assert!((v - 0.5).abs() < 0.001);
378    }
379
380    #[test]
381    fn test_icon_fit_bigger_icon_smaller() {
382        let fit = IconFit {
383            scale_method: ScaleMethod::Bigger,
384            proportional: false,
385            position_x: 0.5,
386            position_y: 0.5,
387            fitting_bounds: false,
388        };
389        // Icon (50x25) smaller than plate (100x50) → no scale
390        let (h, v) = fit.compute_scale(50.0, 25.0, 100.0, 50.0);
391        assert!((h - 1.0).abs() < 0.001);
392        assert!((v - 1.0).abs() < 0.001);
393    }
394
395    #[test]
396    fn test_icon_fit_compute_offset() {
397        let fit = IconFit {
398            scale_method: ScaleMethod::Always,
399            proportional: false,
400            position_x: 0.5,
401            position_y: 0.5,
402            fitting_bounds: false,
403        };
404        // Image 50x30 at scale 1.0 in plate 100x60 → centered
405        let (ox, oy) = fit.compute_offset(50.0, 30.0, 1.0, 1.0, 100.0, 60.0);
406        assert!((ox - 25.0).abs() < 0.001);
407        assert!((oy - 15.0).abs() < 0.001);
408    }
409
410    #[test]
411    fn test_icon_fit_offset_bottom_left() {
412        let fit = IconFit {
413            scale_method: ScaleMethod::Always,
414            proportional: false,
415            position_x: 0.0,
416            position_y: 0.0,
417            fitting_bounds: false,
418        };
419        let (ox, oy) = fit.compute_offset(50.0, 30.0, 1.0, 1.0, 100.0, 60.0);
420        assert!((ox - 0.0).abs() < 0.001);
421        assert!((oy - 0.0).abs() < 0.001);
422    }
423}