use std::collections::HashMap;
use crate::error::PdfError;
use crate::objects::{Dict, Object, ObjectId};
use crate::outline::OutlineDestination;
use crate::reader::document::DocumentReader;
use crate::reader::outline::build_page_index_map;
#[derive(Debug, Clone)]
pub struct PdfLink {
pub source_page_index: usize,
pub rect: [f32; 4],
pub target: Option<PdfLinkTarget>,
}
#[derive(Debug, Clone)]
pub enum PdfLinkTarget {
Internal(OutlineDestination),
Uri(String),
Named(String),
}
pub fn links(reader: &mut DocumentReader<'_>) -> Result<Vec<PdfLink>, PdfError> {
let page_index_map = build_page_index_map(reader)?;
let mut pages_by_index: Vec<ObjectId> = Vec::with_capacity(page_index_map.len());
pages_by_index.resize(page_index_map.len(), ObjectId::new(0));
for (n, idx) in &page_index_map {
pages_by_index[*idx] = ObjectId::new(*n);
}
let mut out = Vec::new();
for (idx, page_id) in pages_by_index.iter().enumerate() {
if page_id.number == 0 {
continue;
}
let page = match reader.resolve(*page_id)? {
Object::Dict(d) => d,
_ => continue,
};
let annots_obj = page
.entries()
.iter()
.find(|(k, _)| k == "Annots")
.map(|(_, v)| v.clone());
let Some(annots_obj) = annots_obj else {
continue;
};
let annots_obj = reader.deref(annots_obj)?;
let Object::Array(items) = annots_obj else {
continue;
};
for item in items {
let annot = match reader.deref(item)? {
Object::Dict(d) => d,
_ => continue,
};
let is_link = matches!(
annot.entries().iter().find(|(k, _)| k == "Subtype").map(|(_, v)| v),
Some(Object::Name(s)) if s == "Link"
);
if !is_link {
continue;
}
if let Some(link) = decode_link(reader, &annot, idx, &page_index_map)? {
out.push(link);
}
}
}
Ok(out)
}
fn decode_link(
reader: &mut DocumentReader<'_>,
annot: &Dict,
page_index: usize,
page_index_map: &HashMap<u32, usize>,
) -> Result<Option<PdfLink>, PdfError> {
let rect = match annot
.entries()
.iter()
.find(|(k, _)| k == "Rect")
.map(|(_, v)| v)
{
Some(Object::Array(items)) if items.len() == 4 => {
let mut out = [0f32; 4];
for (i, it) in items.iter().enumerate() {
out[i] = match it {
Object::Real(f) => *f as f32,
Object::Integer(n) => *n as f32,
_ => return Ok(None),
};
}
out
}
_ => return Ok(None),
};
let target = decode_link_target(reader, annot, page_index_map)?;
Ok(Some(PdfLink {
source_page_index: page_index,
rect,
target,
}))
}
fn decode_link_target(
reader: &mut DocumentReader<'_>,
annot: &Dict,
page_index_map: &HashMap<u32, usize>,
) -> Result<Option<PdfLinkTarget>, PdfError> {
if let Some(dest) = annot
.entries()
.iter()
.find(|(k, _)| k == "Dest")
.map(|(_, v)| v.clone())
{
let dest = reader.deref(dest)?;
return Ok(decode_dest_value(dest, page_index_map));
}
if let Some(action) = annot
.entries()
.iter()
.find(|(k, _)| k == "A")
.map(|(_, v)| v.clone())
{
let action = reader.deref(action)?;
if let Object::Dict(adict) = action {
let s_kind =
adict
.entries()
.iter()
.find(|(k, _)| k == "S")
.and_then(|(_, v)| match v {
Object::Name(s) => Some(s.clone()),
_ => None,
});
match s_kind.as_deref() {
Some("URI") => {
let uri = adict.entries().iter().find(|(k, _)| k == "URI").and_then(
|(_, v)| match v {
Object::LiteralString(b) | Object::HexString(b) => {
Some(String::from_utf8_lossy(b).into_owned())
}
_ => None,
},
);
return Ok(uri.map(PdfLinkTarget::Uri));
}
Some("GoTo") => {
if let Some(d) = adict
.entries()
.iter()
.find(|(k, _)| k == "D")
.map(|(_, v)| v.clone())
{
let d = reader.deref(d)?;
return Ok(decode_dest_value(d, page_index_map));
}
}
_ => {}
}
}
}
Ok(None)
}
fn decode_dest_value(dest: Object, page_index_map: &HashMap<u32, usize>) -> Option<PdfLinkTarget> {
match dest {
Object::Array(items) => {
decode_explicit_dest(&items, page_index_map).map(PdfLinkTarget::Internal)
}
Object::Name(s) => Some(PdfLinkTarget::Named(s)),
Object::LiteralString(b) | Object::HexString(b) => Some(PdfLinkTarget::Named(
String::from_utf8_lossy(&b).into_owned(),
)),
_ => None,
}
}
fn decode_explicit_dest(
items: &[Object],
page_index_map: &HashMap<u32, usize>,
) -> Option<OutlineDestination> {
if items.len() < 2 {
return None;
}
let page_index = match &items[0] {
Object::Reference(id) => *page_index_map.get(&id.number)?,
_ => return None,
};
let mode = match &items[1] {
Object::Name(n) => n.as_str(),
_ => return None,
};
let opt = |o: Option<&Object>| match o {
Some(Object::Real(f)) => Some(*f as f32),
Some(Object::Integer(n)) => Some(*n as f32),
Some(Object::Null) | None => None,
_ => None,
};
let req = |o: Option<&Object>| -> Option<f32> {
match o {
Some(Object::Real(f)) => Some(*f as f32),
Some(Object::Integer(n)) => Some(*n as f32),
_ => None,
}
};
match mode {
"XYZ" => Some(OutlineDestination::Xyz {
page_index,
left: opt(items.get(2)),
top: opt(items.get(3)),
zoom: opt(items.get(4)).filter(|z| *z != 0.0),
}),
"Fit" => Some(OutlineDestination::Fit { page_index }),
"FitH" => Some(OutlineDestination::FitH {
page_index,
top: opt(items.get(2)),
}),
"FitV" => Some(OutlineDestination::FitV {
page_index,
left: opt(items.get(2)),
}),
"FitR" => Some(OutlineDestination::FitR {
page_index,
left: req(items.get(2))?,
bottom: req(items.get(3))?,
right: req(items.get(4))?,
top: req(items.get(5))?,
}),
"FitB" => Some(OutlineDestination::FitB { page_index }),
"FitBH" => Some(OutlineDestination::FitBH {
page_index,
top: opt(items.get(2)),
}),
"FitBV" => Some(OutlineDestination::FitBV {
page_index,
left: opt(items.get(2)),
}),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decode_dest_value_named_byte_string() {
let v = decode_dest_value(
Object::LiteralString(b"Chap6.begin".to_vec()),
&HashMap::new(),
);
match v {
Some(PdfLinkTarget::Named(s)) => assert_eq!(s, "Chap6.begin"),
other => panic!("expected Named, got {other:?}"),
}
}
#[test]
fn decode_dest_value_array_fit_resolves_page_index() {
let mut map = HashMap::new();
map.insert(7u32, 3usize);
let v = decode_dest_value(
Object::Array(vec![
Object::Reference(ObjectId::new(7)),
Object::Name("Fit".into()),
]),
&map,
);
match v {
Some(PdfLinkTarget::Internal(OutlineDestination::Fit { page_index })) => {
assert_eq!(page_index, 3);
}
other => panic!("expected Internal Fit, got {other:?}"),
}
}
#[test]
fn decode_explicit_dest_xyz_with_zoom_zero_is_none() {
let mut map = HashMap::new();
map.insert(2u32, 0usize);
let arr = vec![
Object::Reference(ObjectId::new(2)),
Object::Name("XYZ".into()),
Object::Real(10.0),
Object::Real(20.0),
Object::Real(0.0),
];
let d = decode_explicit_dest(&arr, &map).unwrap();
assert_eq!(
d,
OutlineDestination::Xyz {
page_index: 0,
left: Some(10.0),
top: Some(20.0),
zoom: None,
}
);
}
}