use crate::document::{metadata_with, Metadata, Page};
use crate::geom::Rect;
use crate::hash::FastSet;
use crate::object::{Dict, ObjRef, Object};
use crate::source::AsyncObjectSource;
use crate::tree::resolved_dict;
const MAX_BEADS: usize = 100_000;
#[derive(Debug, Clone, PartialEq)]
pub struct Bead {
pub object: ObjRef,
pub page: Option<ObjRef>,
pub rect: Option<Rect>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ArticleThread {
pub object: Option<ObjRef>,
pub info: Metadata,
pub beads: Vec<Bead>,
}
pub async fn articles_with<S: AsyncObjectSource>(src: &S, trailer: &Dict) -> Vec<ArticleThread> {
let mut threads = Vec::new();
let Some(root) = trailer.get("Root") else {
return threads;
};
let Some(catalog) = resolved_dict(src, root).await else {
return threads;
};
let Some(entry) = catalog.get("Threads") else {
return threads;
};
let Ok(array) = src.resolve(entry).await else {
return threads;
};
let Some(array) = array.as_array() else {
return threads;
};
for value in array {
let Some(dict) = resolved_dict(src, value).await else {
continue;
};
let Some(first) = dict.get("F").and_then(Object::as_ref) else {
continue;
};
let info = match dict.get("I") {
Some(entry) => match resolved_dict(src, entry).await {
Some(info) => metadata_with(src, &info).await,
None => Metadata::default(),
},
None => Metadata::default(),
};
threads.push(ArticleThread {
object: value.as_ref(),
info,
beads: beads_from(src, first).await,
});
}
threads
}
pub async fn page_beads_with<S: AsyncObjectSource>(src: &S, page: &Page) -> Vec<ObjRef> {
let Some(entry) = page.dict().get("B") else {
return Vec::new();
};
let Ok(array) = src.resolve(entry).await else {
return Vec::new();
};
array
.as_array()
.map(|items| items.iter().filter_map(Object::as_ref).collect())
.unwrap_or_default()
}
async fn beads_from<S: AsyncObjectSource>(src: &S, first: ObjRef) -> Vec<Bead> {
let mut beads = Vec::new();
let mut visited: FastSet<ObjRef> = FastSet::default();
let mut next = Some(first);
while let Some(object) = next {
if beads.len() >= MAX_BEADS || !visited.insert(object) {
break;
}
let Some(bead) = resolved_dict(src, &Object::Ref(object)).await else {
break;
};
next = bead.get("N").and_then(Object::as_ref);
beads.push(Bead {
object,
page: bead.get("P").and_then(Object::as_ref),
rect: rectangle(src, bead.get("R")).await,
});
}
beads
}
async fn rectangle<S: AsyncObjectSource>(src: &S, value: Option<&Object>) -> Option<Rect> {
let array = src.resolve(value?).await.ok()?;
let items = array.as_array()?;
if items.len() != 4 {
return None;
}
let mut coords = [0.0f32; 4];
for (slot, item) in coords.iter_mut().zip(items) {
let number = src.resolve(item).await.ok()?.as_f64()?;
if !number.is_finite() {
return None;
}
*slot = number as f32;
}
Some(Rect::new(coords[0], coords[1], coords[2], coords[3]).normalize())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::document::Document;
use crate::geom::Rect;
use pdfboss_testkit::PdfBuilder;
fn r(num: u32) -> ObjRef {
ObjRef { num, gen: 0 }
}
fn doc_with(catalog_extra: &str, beads: impl FnOnce(&mut PdfBuilder)) -> Document {
let mut b = PdfBuilder::new();
b.object(
1,
&format!("<< /Type /Catalog /Pages 2 0 R {catalog_extra} >>"),
);
b.object(2, "<< /Type /Pages /Kids [8 0 R] /Count 1 >>");
b.object(
8,
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /B [23 0 R 24 0 R] >>",
);
b.object(
22,
"<< /F 23 0 R /I << /Title (Man Bites Dog) /Author (Ann) >> >>",
);
b.object(30, "<< /I << /Title (No beads) >> >>");
beads(&mut b);
Document::load(b.build(1)).unwrap()
}
fn example_beads(b: &mut PdfBuilder) {
b.object(
23,
"<< /T 22 0 R /N 24 0 R /V 25 0 R /P 8 0 R /R [158 247 318 905] >>",
);
b.object(
24,
"<< /N 25 0 R /V 23 0 R /P 8 0 R /R [322 246 486 904] >>",
);
b.object(25, "<< /N 23 0 R /V 24 0 R /P 8 0 R /R [1 2 3 4] >>");
}
#[test]
fn walks_each_threads_beads_in_order_and_reads_its_information() {
let doc = doc_with("/Threads [22 0 R 30 0 R]", example_beads);
let threads = doc.articles();
assert_eq!(threads.len(), 1);
let thread = &threads[0];
assert_eq!(thread.object, Some(r(22)));
assert_eq!(thread.info.title.as_deref(), Some("Man Bites Dog"));
assert_eq!(thread.info.author.as_deref(), Some("Ann"));
let objects: Vec<ObjRef> = thread.beads.iter().map(|bead| bead.object).collect();
assert_eq!(objects, vec![r(23), r(24), r(25)]);
assert_eq!(thread.beads[0].page, Some(r(8)));
assert_eq!(
thread.beads[0].rect,
Some(Rect::new(158.0, 247.0, 318.0, 905.0))
);
assert_eq!(thread.beads[2].rect, Some(Rect::new(1.0, 2.0, 3.0, 4.0)));
let page = doc.page(0).unwrap();
assert_eq!(doc.page_beads(&page), vec![r(23), r(24)]);
}
#[test]
fn a_broken_chain_stops_and_a_document_without_threads_has_none() {
let doc = doc_with("/Threads [22 0 R 26 0 R]", |b| {
b.object(
23,
"<< /T 22 0 R /N 24 0 R /V 25 0 R /P 8 0 R /R [0 0 1 1] >>",
);
b.object(24, "<< /N 99 0 R /V 23 0 R /P 8 0 R >>");
b.object(26, "<< /F 27 0 R >>");
b.object(27, "<< /T 26 0 R /N 28 0 R /V 28 0 R /P 8 0 R >>");
b.object(28, "<< /N 27 0 R /V 27 0 R /P 8 0 R >>");
});
let threads = doc.articles();
assert_eq!(threads.len(), 2);
let first: Vec<ObjRef> = threads[0].beads.iter().map(|bead| bead.object).collect();
assert_eq!(first, vec![r(23), r(24)]);
assert_eq!(threads[0].beads[1].rect, None);
let second: Vec<ObjRef> = threads[1].beads.iter().map(|bead| bead.object).collect();
assert_eq!(second, vec![r(27), r(28)]);
assert_eq!(threads[1].info, Metadata::default());
assert_eq!(doc_with("", example_beads).articles(), vec![]);
assert_eq!(
doc_with("/Threads 22 0 R", example_beads).articles(),
vec![]
);
}
}