1use std::collections::HashMap;
13
14use rpdfium_core::{Name, PdfSource};
15use rpdfium_parser::{Object, ObjectStore};
16
17#[derive(Debug, Clone)]
22pub struct IconFit {
23 pub scale_method: ScaleMethod,
25 pub proportional: bool,
27 pub position_x: f32,
29 pub position_y: f32,
31 pub fitting_bounds: bool,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum ScaleMethod {
38 Always,
40 Bigger,
42 Smaller,
44 Never,
46}
47
48impl IconFit {
49 pub fn scale_method(&self) -> ScaleMethod {
53 self.scale_method
54 }
55
56 #[inline]
60 pub fn get_scale_method(&self) -> ScaleMethod {
61 self.scale_method()
62 }
63
64 pub fn is_proportional_scale(&self) -> bool {
68 self.proportional
69 }
70
71 pub fn fitting_bounds(&self) -> bool {
75 self.fitting_bounds
76 }
77
78 #[inline]
82 pub fn get_fitting_bounds(&self) -> bool {
83 self.fitting_bounds()
84 }
85
86 pub fn icon_bottom_left_position(&self) -> (f32, f32) {
91 (self.position_x, self.position_y)
92 }
93
94 #[inline]
98 pub fn get_icon_bottom_left_position(&self) -> (f32, f32) {
99 self.icon_bottom_left_position()
100 }
101
102 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 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
176pub 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"))); 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); 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 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 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 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 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}