use std::collections::HashMap;
use rpdfium_core::{Name, PdfSource};
use rpdfium_parser::{Object, ObjectStore};
#[derive(Debug, Clone)]
pub struct IconFit {
pub scale_method: ScaleMethod,
pub proportional: bool,
pub position_x: f32,
pub position_y: f32,
pub fitting_bounds: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScaleMethod {
Always,
Bigger,
Smaller,
Never,
}
impl IconFit {
pub fn scale_method(&self) -> ScaleMethod {
self.scale_method
}
#[inline]
pub fn get_scale_method(&self) -> ScaleMethod {
self.scale_method()
}
pub fn is_proportional_scale(&self) -> bool {
self.proportional
}
pub fn fitting_bounds(&self) -> bool {
self.fitting_bounds
}
#[inline]
pub fn get_fitting_bounds(&self) -> bool {
self.fitting_bounds()
}
pub fn icon_bottom_left_position(&self) -> (f32, f32) {
(self.position_x, self.position_y)
}
#[inline]
pub fn get_icon_bottom_left_position(&self) -> (f32, f32) {
self.icon_bottom_left_position()
}
pub fn compute_scale(
&self,
image_width: f32,
image_height: f32,
plate_width: f32,
plate_height: f32,
) -> (f32, f32) {
let iw = image_width.max(1.0);
let ih = image_height.max(1.0);
let mut h_scale = match self.scale_method {
ScaleMethod::Always => plate_width / iw,
ScaleMethod::Bigger => {
if plate_width < image_width {
plate_width / iw
} else {
1.0
}
}
ScaleMethod::Smaller => {
if plate_width > image_width {
plate_width / iw
} else {
1.0
}
}
ScaleMethod::Never => 1.0,
};
let mut v_scale = match self.scale_method {
ScaleMethod::Always => plate_height / ih,
ScaleMethod::Bigger => {
if plate_height < image_height {
plate_height / ih
} else {
1.0
}
}
ScaleMethod::Smaller => {
if plate_height > image_height {
plate_height / ih
} else {
1.0
}
}
ScaleMethod::Never => 1.0,
};
if self.proportional {
let min_scale = h_scale.min(v_scale);
h_scale = min_scale;
v_scale = min_scale;
}
(h_scale, v_scale)
}
pub fn compute_offset(
&self,
image_width: f32,
image_height: f32,
h_scale: f32,
v_scale: f32,
plate_width: f32,
plate_height: f32,
) -> (f32, f32) {
let scaled_w = image_width * h_scale;
let scaled_h = image_height * v_scale;
(
(plate_width - scaled_w) * self.position_x,
(plate_height - scaled_h) * self.position_y,
)
}
}
pub fn parse_icon_fit<S: PdfSource>(
mk_dict: &HashMap<Name, Object>,
store: &ObjectStore<S>,
) -> Option<IconFit> {
let if_obj = mk_dict.get(&Name::if_dict())?;
let resolved = store.deep_resolve(if_obj).ok()?;
let if_dict = resolved.as_dict()?;
let scale_method = if_dict
.get(&Name::sw())
.and_then(|o| o.as_name())
.map(|n| match n.as_str().as_ref() {
"B" => ScaleMethod::Bigger,
"S" => ScaleMethod::Smaller,
"N" => ScaleMethod::Never,
_ => ScaleMethod::Always,
})
.unwrap_or(ScaleMethod::Always);
let proportional = if_dict
.get(&Name::s())
.and_then(|o| o.as_name())
.map(|n| n.as_str().as_ref() != "A")
.unwrap_or(true);
let (position_x, position_y) = if_dict
.get(&Name::a())
.and_then(|o| store.deep_resolve(o).ok())
.and_then(|o| {
let arr = o.as_array()?;
let x = arr.first()?.as_f64().map(|f| f as f32).unwrap_or(0.5);
let y = arr.get(1)?.as_f64().map(|f| f as f32).unwrap_or(0.5);
Some((x, y))
})
.unwrap_or((0.5, 0.5));
let fitting_bounds = if_dict
.get(&Name::fb())
.and_then(|o| match o {
Object::Boolean(b) => Some(*b),
_ => None,
})
.unwrap_or(false);
Some(IconFit {
scale_method,
proportional,
position_x,
position_y,
fitting_bounds,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn build_store() -> ObjectStore<Vec<u8>> {
let pdf = build_minimal_pdf();
ObjectStore::open(pdf, rpdfium_core::ParsingMode::Lenient).unwrap()
}
fn build_minimal_pdf() -> Vec<u8> {
let mut pdf = Vec::new();
pdf.extend_from_slice(b"%PDF-1.4\n");
let obj1_offset = pdf.len();
pdf.extend_from_slice(b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
let obj2_offset = pdf.len();
pdf.extend_from_slice(b"2 0 obj\n<< /Type /Pages /Kids [] /Count 0 >>\nendobj\n");
let xref_offset = pdf.len();
pdf.extend_from_slice(b"xref\n0 3\n");
pdf.extend_from_slice(b"0000000000 65535 f \r\n");
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj1_offset).as_bytes());
pdf.extend_from_slice(format!("{:010} 00000 n \r\n", obj2_offset).as_bytes());
pdf.extend_from_slice(b"trailer\n<< /Size 3 /Root 1 0 R >>\n");
pdf.extend_from_slice(format!("startxref\n{}\n%%EOF", xref_offset).as_bytes());
pdf
}
#[test]
fn test_parse_icon_fit_defaults() {
let store = build_store();
let mut mk = HashMap::new();
mk.insert(Name::if_dict(), Object::Dictionary(HashMap::new()));
let result = parse_icon_fit(&mk, &store).unwrap();
assert_eq!(result.scale_method, ScaleMethod::Always);
assert!(result.proportional);
assert!((result.position_x - 0.5).abs() < 0.001);
assert!((result.position_y - 0.5).abs() < 0.001);
assert!(!result.fitting_bounds);
}
#[test]
fn test_parse_icon_fit_with_values() {
let store = build_store();
let mut if_dict = HashMap::new();
if_dict.insert(Name::sw(), Object::Name(Name::from("B")));
if_dict.insert(Name::s(), Object::Name(Name::from("A"))); if_dict.insert(
Name::a(),
Object::Array(vec![Object::Real(0.0), Object::Real(1.0)]),
);
if_dict.insert(Name::fb(), Object::Boolean(true));
let mut mk = HashMap::new();
mk.insert(Name::if_dict(), Object::Dictionary(if_dict));
let result = parse_icon_fit(&mk, &store).unwrap();
assert_eq!(result.scale_method, ScaleMethod::Bigger);
assert!(!result.proportional); assert!((result.position_x - 0.0).abs() < 0.001);
assert!((result.position_y - 1.0).abs() < 0.001);
assert!(result.fitting_bounds);
}
#[test]
fn test_parse_icon_fit_never_scale() {
let store = build_store();
let mut if_dict = HashMap::new();
if_dict.insert(Name::sw(), Object::Name(Name::from("N")));
let mut mk = HashMap::new();
mk.insert(Name::if_dict(), Object::Dictionary(if_dict));
let result = parse_icon_fit(&mk, &store).unwrap();
assert_eq!(result.scale_method, ScaleMethod::Never);
}
#[test]
fn test_parse_icon_fit_smaller_scale() {
let store = build_store();
let mut if_dict = HashMap::new();
if_dict.insert(Name::sw(), Object::Name(Name::from("S")));
let mut mk = HashMap::new();
mk.insert(Name::if_dict(), Object::Dictionary(if_dict));
let result = parse_icon_fit(&mk, &store).unwrap();
assert_eq!(result.scale_method, ScaleMethod::Smaller);
}
#[test]
fn test_parse_icon_fit_absent() {
let store = build_store();
let mk = HashMap::new();
assert!(parse_icon_fit(&mk, &store).is_none());
}
#[test]
fn test_icon_fit_always_scale() {
let fit = IconFit {
scale_method: ScaleMethod::Always,
proportional: false,
position_x: 0.5,
position_y: 0.5,
fitting_bounds: false,
};
let (h, v) = fit.compute_scale(100.0, 50.0, 200.0, 100.0);
assert!((h - 2.0).abs() < 0.001);
assert!((v - 2.0).abs() < 0.001);
}
#[test]
fn test_icon_fit_always_proportional() {
let fit = IconFit {
scale_method: ScaleMethod::Always,
proportional: true,
position_x: 0.5,
position_y: 0.5,
fitting_bounds: false,
};
let (h, v) = fit.compute_scale(100.0, 200.0, 200.0, 200.0);
assert!((h - 1.0).abs() < 0.001);
assert!((v - 1.0).abs() < 0.001);
}
#[test]
fn test_icon_fit_never_scale() {
let fit = IconFit {
scale_method: ScaleMethod::Never,
proportional: false,
position_x: 0.5,
position_y: 0.5,
fitting_bounds: false,
};
let (h, v) = fit.compute_scale(100.0, 50.0, 200.0, 100.0);
assert!((h - 1.0).abs() < 0.001);
assert!((v - 1.0).abs() < 0.001);
}
#[test]
fn test_icon_fit_bigger_icon_bigger() {
let fit = IconFit {
scale_method: ScaleMethod::Bigger,
proportional: false,
position_x: 0.5,
position_y: 0.5,
fitting_bounds: false,
};
let (h, v) = fit.compute_scale(200.0, 100.0, 100.0, 50.0);
assert!((h - 0.5).abs() < 0.001);
assert!((v - 0.5).abs() < 0.001);
}
#[test]
fn test_icon_fit_bigger_icon_smaller() {
let fit = IconFit {
scale_method: ScaleMethod::Bigger,
proportional: false,
position_x: 0.5,
position_y: 0.5,
fitting_bounds: false,
};
let (h, v) = fit.compute_scale(50.0, 25.0, 100.0, 50.0);
assert!((h - 1.0).abs() < 0.001);
assert!((v - 1.0).abs() < 0.001);
}
#[test]
fn test_icon_fit_compute_offset() {
let fit = IconFit {
scale_method: ScaleMethod::Always,
proportional: false,
position_x: 0.5,
position_y: 0.5,
fitting_bounds: false,
};
let (ox, oy) = fit.compute_offset(50.0, 30.0, 1.0, 1.0, 100.0, 60.0);
assert!((ox - 25.0).abs() < 0.001);
assert!((oy - 15.0).abs() < 0.001);
}
#[test]
fn test_icon_fit_offset_bottom_left() {
let fit = IconFit {
scale_method: ScaleMethod::Always,
proportional: false,
position_x: 0.0,
position_y: 0.0,
fitting_bounds: false,
};
let (ox, oy) = fit.compute_offset(50.0, 30.0, 1.0, 1.0, 100.0, 60.0);
assert!((ox - 0.0).abs() < 0.001);
assert!((oy - 0.0).abs() < 0.001);
}
}