use pdfrum_object::{Array, Dict, Name, ObjRef, Object, PdfString, Resolve, encode_text};
use crate::names;
use crate::{annot::AnnotGoToView, doc::EditDoc, error::Error};
type Result<T> = core::result::Result<T, Error>;
#[derive(Debug, Clone, PartialEq)]
pub enum BookmarkTarget {
Page {
page: ObjRef,
view: AnnotGoToView,
},
Named(String),
None,
}
#[derive(Debug, Clone, PartialEq)]
pub struct BookmarkSpec {
pub title: String,
pub depth: usize,
pub target: BookmarkTarget,
pub color: Option<(f32, f32, f32)>,
pub style: Option<i64>,
pub open: bool,
}
impl BookmarkSpec {
#[must_use]
pub fn new(title: impl Into<String>) -> Self {
Self {
title: title.into(),
depth: 0,
target: BookmarkTarget::None,
color: None,
style: None,
open: true,
}
}
#[must_use]
pub fn depth(mut self, depth: usize) -> Self {
self.depth = depth;
self
}
#[must_use]
pub fn page(mut self, page: ObjRef, view: AnnotGoToView) -> Self {
self.target = BookmarkTarget::Page { page, view };
self
}
#[must_use]
pub fn named(mut self, name: impl Into<String>) -> Self {
self.target = BookmarkTarget::Named(name.into());
self
}
#[must_use]
pub fn color(mut self, rgb: (f32, f32, f32)) -> Self {
self.color = Some(rgb);
self
}
#[must_use]
pub fn style(mut self, style: i64) -> Self {
self.style = Some(style);
self
}
#[must_use]
pub fn open(mut self, open: bool) -> Self {
self.open = open;
self
}
}
pub fn set_outline(dest: &mut EditDoc<'_>, items: &[BookmarkSpec]) -> Result<()> {
let Some(root) = dest.base().trailer().reference(names::ROOT) else {
return Err(Error::NoDestinationCatalog);
};
let Some(mut catalog) = dest
.fetch(root)
.ok()
.and_then(|object| object.as_dict().cloned())
else {
return Err(Error::NoDestinationCatalog);
};
if items.is_empty() {
catalog.remove(names::OUTLINES);
dest.replace(root, Object::Dict(catalog));
return Ok(());
}
let refs: Vec<ObjRef> = items
.iter()
.map(|_| dest.add(Object::Dict(Dict::new())))
.collect();
let outlines_ref = dest.add(Object::Dict(Dict::new()));
let tree = Tree::of(items);
let reference_of = |index: usize| refs.get(index).copied();
for (index, item) in items.iter().enumerate() {
let (Some(node), Some(self_ref)) = (tree.nodes.get(index), reference_of(index)) else {
continue;
};
let dict = item_dict(item, node, index, &tree, items, outlines_ref, reference_of);
dest.replace(self_ref, Object::Dict(dict));
}
let mut outlines = Dict::new();
outlines.insert(names::TYPE.clone(), Object::Name(Name::from("Outlines")));
if let (Some(first), Some(last)) = (
tree.roots.first().copied().and_then(reference_of),
tree.roots.last().copied().and_then(reference_of),
) {
outlines.insert(names::FIRST.clone(), Object::Ref(first));
outlines.insert(names::LAST.clone(), Object::Ref(last));
}
outlines.insert(names::COUNT.clone(), Object::Int(tree.visible_total(items)));
dest.replace(outlines_ref, Object::Dict(outlines));
catalog.insert(names::OUTLINES.clone(), Object::Ref(outlines_ref));
dest.replace(root, Object::Dict(catalog));
Ok(())
}
#[derive(Default)]
struct Node {
parent: Option<usize>,
prev: Option<usize>,
next: Option<usize>,
first: Option<usize>,
last: Option<usize>,
children: Vec<usize>,
}
struct Tree {
nodes: Vec<Node>,
roots: Vec<usize>,
}
impl Tree {
fn of(items: &[BookmarkSpec]) -> Self {
let mut nodes: Vec<Node> = (0..items.len()).map(|_| Node::default()).collect();
let mut roots = Vec::new();
let mut at_depth: Vec<usize> = Vec::new();
for (index, item) in items.iter().enumerate() {
let depth = item.depth.min(at_depth.len());
at_depth.truncate(depth);
let link = |prev: usize, next: usize, nodes: &mut Vec<Node>| {
if let Some(node) = nodes.get_mut(next) {
node.prev = Some(prev);
}
if let Some(node) = nodes.get_mut(prev) {
node.next = Some(next);
}
};
if depth == 0 {
if let Some(&prev) = roots.last() {
link(prev, index, &mut nodes);
}
roots.push(index);
} else if let Some(&parent) = at_depth.get(depth - 1) {
if let Some(node) = nodes.get_mut(index) {
node.parent = Some(parent);
}
let last_child = nodes.get(parent).and_then(|p| p.children.last().copied());
if let Some(prev) = last_child {
link(prev, index, &mut nodes);
}
if let Some(node) = nodes.get_mut(parent) {
node.children.push(index);
}
}
at_depth.push(index);
}
for node in &mut nodes {
node.first = node.children.first().copied();
node.last = node.children.last().copied();
}
Self { nodes, roots }
}
fn visible_descendants(&self, index: usize, items: &[BookmarkSpec]) -> i64 {
let mut total = 0;
let Some(node) = self.nodes.get(index) else {
return 0;
};
for &child in &node.children {
total += 1;
if items.get(child).is_some_and(|item| item.open) {
total += self.visible_descendants(child, items);
}
}
total
}
fn visible_total(&self, items: &[BookmarkSpec]) -> i64 {
let mut total = 0;
for &root in &self.roots {
total += 1;
if items.get(root).is_some_and(|item| item.open) {
total += self.visible_descendants(root, items);
}
}
total
}
}
fn item_dict(
item: &BookmarkSpec,
node: &Node,
index: usize,
tree: &Tree,
items: &[BookmarkSpec],
outlines_ref: ObjRef,
reference_of: impl Fn(usize) -> Option<ObjRef>,
) -> Dict {
let mut dict = Dict::new();
dict.insert(
names::TITLE.clone(),
Object::Str(PdfString::literal(encode_text(&item.title))),
);
dict.insert(
names::PARENT.clone(),
Object::Ref(node.parent.and_then(&reference_of).unwrap_or(outlines_ref)),
);
if let Some(prev) = node.prev.and_then(&reference_of) {
dict.insert(names::PREV.clone(), Object::Ref(prev));
}
if let Some(next) = node.next.and_then(&reference_of) {
dict.insert(names::NEXT.clone(), Object::Ref(next));
}
if let (Some(first), Some(last)) = (
node.first.and_then(&reference_of),
node.last.and_then(&reference_of),
) {
dict.insert(names::FIRST.clone(), Object::Ref(first));
dict.insert(names::LAST.clone(), Object::Ref(last));
let visible = tree.visible_descendants(index, items);
dict.insert(
names::COUNT.clone(),
Object::Int(if item.open { visible } else { -visible }),
);
}
match &item.target {
BookmarkTarget::Page { page, view } => {
dict.insert(
names::DEST.clone(),
Object::Array(crate::annot::goto_dest_array(*page, *view)),
);
}
BookmarkTarget::Named(name) => {
dict.insert(
names::DEST.clone(),
Object::Str(PdfString::literal(name.as_bytes())),
);
}
BookmarkTarget::None => {}
}
if let Some((r, g, b)) = item.color {
dict.insert(
names::C.clone(),
Object::Array(Array::of([
Object::Real(r.clamp(0.0, 1.0)),
Object::Real(g.clamp(0.0, 1.0)),
Object::Real(b.clamp(0.0, 1.0)),
])),
);
}
if let Some(style) = item.style {
dict.insert(names::F.clone(), Object::Int(style));
}
dict
}