use std::collections::HashMap;
use rpdfium_core::{Name, PdfSource};
use rpdfium_parser::{Object, ObjectStore};
use crate::action::{Action, parse_action};
use crate::destination::{Destination, parse_destination};
use crate::error::DocResult;
#[derive(Debug, Clone)]
pub struct Link {
pub rect: [f32; 4],
pub destination: Option<Destination>,
pub action: Option<Action>,
}
pub fn parse_link<S: PdfSource>(
dict: &HashMap<Name, Object>,
store: &ObjectStore<S>,
) -> DocResult<Link> {
let rect_obj = dict
.get(&Name::rect())
.ok_or_else(|| crate::error::DocError::MissingKey("/Rect".into()))?;
let resolved = store
.deep_resolve(rect_obj)
.map_err(|e| crate::error::DocError::Parser(e.to_string()))?;
let arr = resolved
.as_array()
.ok_or(crate::error::DocError::UnexpectedType)?;
let get = |idx: usize| -> f32 {
arr.get(idx)
.and_then(|o| o.as_f64())
.map(|f| f as f32)
.unwrap_or(0.0)
};
let rect = [get(0), get(1), get(2), get(3)];
let destination = dict
.get(&Name::dest())
.and_then(|o| parse_destination(o, store).ok());
let action = dict
.get(&Name::a())
.and_then(|o| parse_action(o, store).ok());
Ok(Link {
rect,
destination,
action,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::action::Action;
use crate::destination::Destination;
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_link_with_rect_only() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(
Name::rect(),
Object::Array(vec![
Object::Real(10.0),
Object::Real(20.0),
Object::Real(100.0),
Object::Real(50.0),
]),
);
let link = parse_link(&dict, &store).unwrap();
assert_eq!(link.rect, [10.0, 20.0, 100.0, 50.0]);
assert!(link.destination.is_none());
assert!(link.action.is_none());
}
#[test]
fn test_parse_link_with_dest() {
let store = build_store();
let mut dict = HashMap::new();
dict.insert(
Name::rect(),
Object::Array(vec![
Object::Real(0.0),
Object::Real(0.0),
Object::Real(200.0),
Object::Real(20.0),
]),
);
dict.insert(
Name::dest(),
Object::String(PdfString::from_bytes(b"page2".to_vec())),
);
let link = parse_link(&dict, &store).unwrap();
match &link.destination {
Some(Destination::Named(n)) => assert_eq!(n, "page2"),
_ => panic!("expected named destination"),
}
assert!(link.action.is_none());
}
#[test]
fn test_parse_link_with_uri_action() {
let store = build_store();
let mut action_dict = HashMap::new();
action_dict.insert(Name::s(), Object::Name(Name::from("URI")));
action_dict.insert(
Name::uri(),
Object::String(PdfString::from_bytes(b"https://example.com".to_vec())),
);
let mut dict = HashMap::new();
dict.insert(
Name::rect(),
Object::Array(vec![
Object::Real(0.0),
Object::Real(0.0),
Object::Real(100.0),
Object::Real(20.0),
]),
);
dict.insert(Name::a(), Object::Dictionary(action_dict));
let link = parse_link(&dict, &store).unwrap();
assert!(link.destination.is_none());
match &link.action {
Some(Action::Uri(uri)) => assert_eq!(uri, "https://example.com"),
_ => panic!("expected URI action"),
}
}
#[test]
fn test_parse_link_missing_rect_returns_error() {
let store = build_store();
let dict = HashMap::new();
let result = parse_link(&dict, &store);
assert!(result.is_err());
}
}