use std::collections::HashMap;
use rpdfium_core::{Name, PdfSource};
use rpdfium_parser::{Object, ObjectId, ObjectStore};
use crate::icon_fit::{IconFit, parse_icon_fit};
#[derive(Debug, Clone)]
pub struct MkDict {
pub rotation: u32,
pub border_color: Option<Vec<f32>>,
pub background_color: Option<Vec<f32>>,
pub caption: Option<String>,
pub rollover_caption: Option<String>,
pub alt_caption: Option<String>,
pub icon_normal: Option<ObjectId>,
pub icon_rollover: Option<ObjectId>,
pub icon_alt: Option<ObjectId>,
pub text_position: u32,
pub icon_fit: Option<IconFit>,
}
pub fn parse_mk_dict<S: PdfSource>(
dict: &HashMap<Name, Object>,
store: &ObjectStore<S>,
) -> Option<MkDict> {
let mk_obj = dict.get(&Name::mk())?;
let resolved = store.deep_resolve(mk_obj).ok()?;
let mk_dict = resolved.as_dict()?;
let rotation = mk_dict
.get(&Name::r())
.and_then(|o| o.as_i64())
.map(|v| v as u32)
.unwrap_or(0);
let border_color = mk_dict
.get(&Name::bc())
.and_then(|o| store.deep_resolve(o).ok())
.and_then(parse_float_array);
let background_color = mk_dict
.get(&Name::bg_color())
.and_then(|o| store.deep_resolve(o).ok())
.and_then(parse_float_array);
let caption = mk_dict
.get(&Name::ca_display())
.and_then(|o| store.deep_resolve(o).ok())
.and_then(|o| o.as_string().map(|s| s.to_string_lossy()));
let rollover_caption = mk_dict
.get(&Name::rc())
.and_then(|o| store.deep_resolve(o).ok())
.and_then(|o| o.as_string().map(|s| s.to_string_lossy()));
let alt_caption = mk_dict
.get(&Name::ac())
.and_then(|o| store.deep_resolve(o).ok())
.and_then(|o| o.as_string().map(|s| s.to_string_lossy()));
let icon_normal = mk_dict.get(&Name::i()).and_then(|o| o.as_reference());
let icon_rollover = mk_dict.get(&Name::ri()).and_then(|o| o.as_reference());
let icon_alt = mk_dict.get(&Name::ix()).and_then(|o| o.as_reference());
let text_position = mk_dict
.get(&Name::tp())
.and_then(|o| o.as_i64())
.map(|v| v as u32)
.unwrap_or(0);
let icon_fit = parse_icon_fit(mk_dict, store);
Some(MkDict {
rotation,
border_color,
background_color,
caption,
rollover_caption,
alt_caption,
icon_normal,
icon_rollover,
icon_alt,
text_position,
icon_fit,
})
}
impl MkDict {
pub fn border_color_components(&self) -> usize {
self.border_color.as_ref().map_or(0, |c| c.len())
}
pub fn background_color_components(&self) -> usize {
self.background_color.as_ref().map_or(0, |c| c.len())
}
}
#[derive(Debug, Clone)]
pub struct IconProperties {
pub width: f32,
pub height: f32,
pub matrix: [f32; 6],
pub alias: Option<String>,
}
pub fn parse_icon_properties<S: PdfSource>(
icon_id: ObjectId,
store: &ObjectStore<S>,
) -> Option<IconProperties> {
let obj = store.resolve(icon_id).ok()?;
let dict = match obj {
Object::Stream { dict, .. } => dict,
Object::Dictionary(d) => d,
_ => return None,
};
let (width, height) = dict
.get(&Name::b_box())
.and_then(|o| store.deep_resolve(o).ok())
.and_then(|o| {
let arr = o.as_array()?;
if arr.len() >= 4 {
let left = arr[0].as_f64().unwrap_or(0.0) as f32;
let bottom = arr[1].as_f64().unwrap_or(0.0) as f32;
let right = arr[2].as_f64().unwrap_or(0.0) as f32;
let top = arr[3].as_f64().unwrap_or(0.0) as f32;
Some((right - left, top - bottom))
} else {
None
}
})
.unwrap_or((0.0, 0.0));
let matrix = dict
.get(&Name::matrix())
.and_then(|o| store.deep_resolve(o).ok())
.and_then(|o| {
let arr = o.as_array()?;
if arr.len() >= 6 {
Some([
arr[0].as_f64().unwrap_or(1.0) as f32,
arr[1].as_f64().unwrap_or(0.0) as f32,
arr[2].as_f64().unwrap_or(0.0) as f32,
arr[3].as_f64().unwrap_or(1.0) as f32,
arr[4].as_f64().unwrap_or(0.0) as f32,
arr[5].as_f64().unwrap_or(0.0) as f32,
])
} else {
None
}
})
.unwrap_or([1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
let alias = dict
.get(&Name::from("Name"))
.and_then(|o| o.as_name())
.map(|n| n.as_str().into_owned());
Some(IconProperties {
width,
height,
matrix,
alias,
})
}
fn parse_float_array(obj: &Object) -> Option<Vec<f32>> {
let arr = obj.as_array()?;
let values: Vec<f32> = arr
.iter()
.filter_map(|o| o.as_f64().map(|f| f as f32))
.collect();
if values.is_empty() && !arr.is_empty() {
return None;
}
Some(values)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::icon_fit::ScaleMethod;
use rpdfium_core::PdfString;
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_mk_dict_with_colors_and_caption() {
let store = build_store();
let mut mk = HashMap::new();
mk.insert(Name::r(), Object::Integer(90));
mk.insert(
Name::bc(),
Object::Array(vec![
Object::Real(1.0),
Object::Real(0.0),
Object::Real(0.0),
]),
);
mk.insert(
Name::bg_color(),
Object::Array(vec![
Object::Real(0.9),
Object::Real(0.9),
Object::Real(0.9),
]),
);
mk.insert(
Name::ca_display(),
Object::String(PdfString::from_bytes(b"Submit".to_vec())),
);
mk.insert(
Name::rc(),
Object::String(PdfString::from_bytes(b"Hover".to_vec())),
);
mk.insert(
Name::ac(),
Object::String(PdfString::from_bytes(b"Pressed".to_vec())),
);
mk.insert(Name::tp(), Object::Integer(2));
let mut dict = HashMap::new();
dict.insert(Name::mk(), Object::Dictionary(mk));
let result = parse_mk_dict(&dict, &store).unwrap();
assert_eq!(result.rotation, 90);
assert_eq!(result.border_color, Some(vec![1.0, 0.0, 0.0]));
assert_eq!(result.background_color, Some(vec![0.9, 0.9, 0.9]));
assert_eq!(result.caption.as_deref(), Some("Submit"));
assert_eq!(result.rollover_caption.as_deref(), Some("Hover"));
assert_eq!(result.alt_caption.as_deref(), Some("Pressed"));
assert_eq!(result.text_position, 2);
}
#[test]
fn test_parse_mk_dict_defaults() {
let store = build_store();
let mk = HashMap::new();
let mut dict = HashMap::new();
dict.insert(Name::mk(), Object::Dictionary(mk));
let result = parse_mk_dict(&dict, &store).unwrap();
assert_eq!(result.rotation, 0);
assert!(result.border_color.is_none());
assert!(result.background_color.is_none());
assert!(result.caption.is_none());
assert_eq!(result.text_position, 0);
}
#[test]
fn test_parse_mk_dict_with_icon_refs() {
let store = build_store();
let mut mk = HashMap::new();
mk.insert(Name::i(), Object::Reference(ObjectId::new(10, 0)));
mk.insert(Name::ri(), Object::Reference(ObjectId::new(11, 0)));
mk.insert(Name::ix(), Object::Reference(ObjectId::new(12, 0)));
let mut dict = HashMap::new();
dict.insert(Name::mk(), Object::Dictionary(mk));
let result = parse_mk_dict(&dict, &store).unwrap();
assert_eq!(result.icon_normal, Some(ObjectId::new(10, 0)));
assert_eq!(result.icon_rollover, Some(ObjectId::new(11, 0)));
assert_eq!(result.icon_alt, Some(ObjectId::new(12, 0)));
}
#[test]
fn test_parse_mk_dict_absent_returns_none() {
let store = build_store();
let dict = HashMap::new();
assert!(parse_mk_dict(&dict, &store).is_none());
}
#[test]
fn test_mk_dict_includes_icon_fit() {
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 mut dict = HashMap::new();
dict.insert(Name::mk(), Object::Dictionary(mk));
let result = parse_mk_dict(&dict, &store).unwrap();
assert!(result.icon_fit.is_some());
assert_eq!(result.icon_fit.unwrap().scale_method, ScaleMethod::Never);
}
#[test]
fn test_mk_dict_border_color_components() {
let store = build_store();
let mut mk = HashMap::new();
mk.insert(
Name::bc(),
Object::Array(vec![
Object::Real(1.0),
Object::Real(0.0),
Object::Real(0.0),
]),
);
let mut dict = HashMap::new();
dict.insert(Name::mk(), Object::Dictionary(mk));
let result = parse_mk_dict(&dict, &store).unwrap();
assert_eq!(result.border_color_components(), 3); }
#[test]
fn test_mk_dict_no_color_components() {
let store = build_store();
let mk = HashMap::new();
let mut dict = HashMap::new();
dict.insert(Name::mk(), Object::Dictionary(mk));
let result = parse_mk_dict(&dict, &store).unwrap();
assert_eq!(result.border_color_components(), 0); assert_eq!(result.background_color_components(), 0);
}
#[test]
fn test_mk_dict_gray_color_components() {
let store = build_store();
let mut mk = HashMap::new();
mk.insert(Name::bg_color(), Object::Array(vec![Object::Real(0.5)]));
let mut dict = HashMap::new();
dict.insert(Name::mk(), Object::Dictionary(mk));
let result = parse_mk_dict(&dict, &store).unwrap();
assert_eq!(result.background_color_components(), 1); }
}