use pdfrum_object::{Dict, Name, ObjRef, Object, Resolve};
use crate::names;
#[derive(Debug, Clone, PartialEq)]
pub enum Kid {
Invalid,
Element {
dict: Dict,
reference: Option<ObjRef>,
slot: usize,
linked: Option<usize>,
},
PageContent {
content_id: i64,
},
StreamContent {
stream_obj_num: u32,
content_id: i64,
},
Object {
obj_num: u32,
},
}
#[derive(Debug, Clone, PartialEq)]
pub struct StructElement {
pub dict: Dict,
pub reference: Option<ObjRef>,
pub kind: Vec<u8>,
pub kids: Vec<Kid>,
pub parent: Option<usize>,
}
impl StructElement {
pub fn load_kids<R: Resolve>(dict: &Dict, page_obj_num: u32, r: &R) -> Vec<Kid> {
let own_page = dict
.raw(names::PG)
.and_then(Object::as_ref_id)
.map_or(0, |reference| reference.num);
let Some(kids_obj) = dict.get(names::K, r).map(|k| k.get().clone()) else {
return Vec::new();
};
match &kids_obj {
Object::Array(array) => array
.iter()
.enumerate()
.map(|(slot, element)| {
let direct = element.resolve(r).ok().map(|d| d.get().clone());
load_kid(direct.as_ref(), element, own_page, page_obj_num, slot, r)
})
.collect(),
other => vec![load_kid(Some(other), other, own_page, page_obj_num, 0, r)],
}
}
pub fn link_kid(&mut self, target: &Dict, reference: Option<ObjRef>, element: usize) -> bool {
let mut matched = false;
for kid in &mut self.kids {
if let Kid::Element {
dict,
reference: kid_ref,
linked,
..
} = kid
{
let same = match (*kid_ref, reference) {
(Some(a), Some(b)) => a == b,
_ => dict == target,
};
if same {
*linked = Some(element);
matched = true;
}
}
}
matched
}
#[must_use]
pub fn obj_type<R: Resolve>(&self, r: &R) -> Vec<u8> {
self.dict.byte_string(names::TYPE, r).unwrap_or_default()
}
#[must_use]
pub fn alt_text<R: Resolve>(&self, r: &R) -> String {
self.dict.text(names::ALT, r).unwrap_or_default()
}
#[must_use]
pub fn actual_text<R: Resolve>(&self, r: &R) -> String {
self.dict.text(names::ACTUAL_TEXT, r).unwrap_or_default()
}
#[must_use]
pub fn expansion<R: Resolve>(&self, r: &R) -> String {
self.dict.text(names::E, r).unwrap_or_default()
}
#[must_use]
pub fn title<R: Resolve>(&self, r: &R) -> String {
self.dict.text(names::T, r).unwrap_or_default()
}
#[must_use]
pub fn id(&self) -> Option<Vec<u8>> {
self.dict
.raw(names::ID)
.and_then(Object::as_string)
.map(|s| s.bytes.to_vec())
}
#[must_use]
pub fn lang(&self) -> Option<Vec<u8>> {
self.dict
.raw(names::LANG)
.and_then(Object::as_string)
.map(|s| s.bytes.to_vec())
}
#[must_use]
pub fn kid_content_id(&self, index: usize) -> Option<i64> {
match self.kids.get(index) {
Some(Kid::PageContent { content_id } | Kid::StreamContent { content_id, .. }) => {
Some(*content_id)
}
_ => None,
}
}
}
fn load_kid<R: Resolve>(
direct: Option<&Object>,
raw: &Object,
own_page: u32,
page_obj_num: u32,
slot: usize,
r: &R,
) -> Kid {
let Some(direct) = direct else {
return Kid::Invalid;
};
match direct {
Object::Int(_) | Object::Real(_) => {
if own_page == page_obj_num {
Kid::PageContent {
content_id: direct.as_int().unwrap_or(0),
}
} else {
Kid::Invalid
}
}
Object::Dict(dict) => {
let page = dict
.raw(names::PG)
.and_then(Object::as_ref_id)
.map_or(own_page, |reference| reference.num);
let kind = dict.name(names::TYPE);
match kind {
Some(k) if k == names::MCR => {
if page != page_obj_num {
return Kid::Invalid;
}
Kid::StreamContent {
stream_obj_num: dict
.raw(names::STM)
.and_then(Object::as_ref_id)
.map_or(0, |reference| reference.num),
content_id: dict.int(names::MCID, r).unwrap_or(0),
}
}
Some(k) if k == names::OBJR => {
if page != page_obj_num {
return Kid::Invalid;
}
Kid::Object {
obj_num: dict
.raw(names::OBJ)
.and_then(Object::as_ref_id)
.map_or(0, |reference| reference.num),
}
}
_ => Kid::Element {
dict: dict.clone(),
reference: raw.as_ref_id(),
slot,
linked: None,
},
}
}
_ => Kid::Invalid,
}
}
#[must_use]
pub(crate) fn marked_content_id_count<R: Resolve>(dict: &Dict, r: &R) -> Option<usize> {
match dict.get(names::K, r).map(|k| k.get().clone()) {
Some(Object::Int(_) | Object::Real(_) | Object::Dict(_)) => Some(1),
Some(Object::Array(array)) => Some(array.len()),
None | Some(_) => None,
}
}
#[must_use]
pub(crate) fn marked_content_id_at<R: Resolve>(dict: &Dict, index: usize, r: &R) -> Option<i64> {
match dict.get(names::K, r).map(|k| k.get().clone()) {
Some(obj @ (Object::Int(_) | Object::Real(_))) => {
if index == 0 {
obj.as_int()
} else {
None
}
}
Some(Object::Dict(dict)) => mcid_from_dict(&dict, r),
Some(Object::Array(array)) => match array.get(index, r).map(|e| e.get().clone()) {
Some(obj @ (Object::Int(_) | Object::Real(_))) => obj.as_int(),
Some(Object::Dict(dict)) => mcid_from_dict(&dict, r),
_ => None,
},
_ => None,
}
}
fn mcid_from_dict<R: Resolve>(dict: &Dict, r: &R) -> Option<i64> {
if dict.name(names::TYPE) != Some(names::MCR) {
return None;
}
dict.get(names::MCID, r)
.and_then(|v| v.get().as_number().and_then(Object::as_int))
}
#[must_use]
pub fn map_role(role_map: Option<&Dict>, kind: &[u8]) -> Vec<u8> {
role_map
.and_then(|map| map.name(&Name::new(kind.to_vec())))
.filter(|mapped| !mapped.as_bytes().is_empty())
.map_or_else(|| kind.to_vec(), |mapped| mapped.as_bytes().to_vec())
}
#[cfg(test)]
mod tests {
use super::{Kid, StructElement, map_role, marked_content_id_at, marked_content_id_count};
use pdfrum_object::{Array, Dict, Name, NoResolve, Object, PdfString};
use crate::names;
fn dict(pairs: &[(&str, Object)]) -> Dict {
Dict::from_pairs(
pairs
.iter()
.map(|(k, v)| (Name::from(*k), v.clone()))
.collect::<Vec<_>>(),
)
}
#[test]
fn a_number_kid_is_page_content_only_for_its_own_page() {
let element = dict(&[("K", Object::Int(7))]);
assert_eq!(
StructElement::load_kids(&element, 0, &NoResolve),
vec![Kid::PageContent { content_id: 7 }]
);
assert_eq!(
StructElement::load_kids(&element, 4, &NoResolve),
vec![Kid::Invalid]
);
}
#[test]
fn a_dict_kid_with_no_recognized_type_is_a_nested_element() {
let inner = dict(&[("S", Object::Name(Name::from("P")))]);
let element = dict(&[("K", Object::Dict(inner.clone()))]);
let kids = StructElement::load_kids(&element, 0, &NoResolve);
assert_eq!(
kids,
vec![Kid::Element {
dict: inner,
reference: None,
slot: 0,
linked: None,
}]
);
}
#[test]
fn an_element_kid_is_never_page_tested() {
let inner = dict(&[("S", Object::Name(Name::from("P")))]);
let element = dict(&[("K", Object::Dict(inner))]);
assert!(matches!(
StructElement::load_kids(&element, 9, &NoResolve).as_slice(),
[Kid::Element { .. }]
));
}
#[test]
fn a_marked_content_reference_needs_its_page_to_match() {
let mcr = dict(&[
("Type", Object::Name(names::MCR.clone())),
("MCID", Object::Int(3)),
]);
let element = dict(&[("K", Object::Dict(mcr))]);
assert_eq!(
StructElement::load_kids(&element, 0, &NoResolve),
vec![Kid::StreamContent {
stream_obj_num: 0,
content_id: 3,
}]
);
assert_eq!(
StructElement::load_kids(&element, 1, &NoResolve),
vec![Kid::Invalid]
);
}
#[test]
fn the_unfiltered_count_ignores_the_page_and_answers_none_when_absent() {
assert_eq!(marked_content_id_count(&Dict::new(), &NoResolve), None);
assert_eq!(
marked_content_id_count(&dict(&[("K", Object::Int(0))]), &NoResolve),
Some(1)
);
assert_eq!(
marked_content_id_count(
&dict(&[(
"K",
Object::Array(Array::of([Object::Int(2), Object::Int(3)]))
)]),
&NoResolve
),
Some(2)
);
assert_eq!(
marked_content_id_count(
&dict(&[("K", Object::Str(PdfString::literal(b"nope")))]),
&NoResolve
),
None
);
}
#[test]
fn a_marked_content_reference_needs_the_name_type_and_a_number_mcid() {
let good = dict(&[(
"K",
Object::Dict(dict(&[
("Type", Object::Name(names::MCR.clone())),
("MCID", Object::Int(5)),
])),
)]);
assert_eq!(marked_content_id_at(&good, 0, &NoResolve), Some(5));
assert_eq!(marked_content_id_at(&good, 9, &NoResolve), Some(5));
let wrong_type = dict(&[(
"K",
Object::Dict(dict(&[
("Type", Object::Name(Name::from("Other"))),
("MCID", Object::Int(5)),
])),
)]);
assert_eq!(marked_content_id_at(&wrong_type, 0, &NoResolve), None);
}
#[test]
fn linking_a_kid_fills_its_element_index_and_leaves_its_slot_alone() {
let inner = dict(&[("S", Object::Name(Name::from("P")))]);
let mut element = StructElement {
dict: Dict::new(),
reference: None,
kind: b"Document".to_vec(),
kids: StructElement::load_kids(
&dict(&[("K", Object::Dict(inner.clone()))]),
0,
&NoResolve,
),
parent: None,
};
assert!(element.link_kid(&inner, None, 7));
assert_eq!(
element.kids,
vec![Kid::Element {
dict: inner,
reference: None,
slot: 0,
linked: Some(7),
}]
);
}
#[test]
fn an_unmatched_kid_keeps_no_element_index_at_all() {
let inner = dict(&[("S", Object::Name(Name::from("P")))]);
let mut element = StructElement {
dict: Dict::new(),
reference: None,
kind: b"Document".to_vec(),
kids: StructElement::load_kids(&dict(&[("K", Object::Dict(inner))]), 0, &NoResolve),
parent: None,
};
let stranger = dict(&[("S", Object::Name(Name::from("Span")))]);
assert!(!element.link_kid(&stranger, None, 7));
assert!(matches!(
element.kids.first(),
Some(Kid::Element { linked: None, .. })
));
}
#[test]
fn the_role_map_replaces_a_type_only_when_it_names_a_nonempty_one() {
let map = dict(&[("Quote", Object::Name(Name::from("BlockQuote")))]);
assert_eq!(map_role(Some(&map), b"Quote"), b"BlockQuote");
assert_eq!(map_role(Some(&map), b"P"), b"P");
assert_eq!(map_role(None, b"P"), b"P");
}
}