1use pdfrum_object::{Array, Dict, Name, ObjRef, Object, PdfString, Resolve, encode_text};
14
15use crate::names;
16
17use crate::{annot::AnnotGoToView, doc::EditDoc, error::Error};
18
19type Result<T> = core::result::Result<T, Error>;
21
22#[derive(Debug, Clone, PartialEq)]
24pub enum BookmarkTarget {
25 Page {
27 page: ObjRef,
29 view: AnnotGoToView,
31 },
32 Named(String),
36 None,
38}
39
40#[derive(Debug, Clone, PartialEq)]
47pub struct BookmarkSpec {
48 pub title: String,
50 pub depth: usize,
52 pub target: BookmarkTarget,
54 pub color: Option<(f32, f32, f32)>,
56 pub style: Option<i64>,
58 pub open: bool,
63}
64
65impl BookmarkSpec {
66 #[must_use]
68 pub fn new(title: impl Into<String>) -> Self {
69 Self {
70 title: title.into(),
71 depth: 0,
72 target: BookmarkTarget::None,
73 color: None,
74 style: None,
75 open: true,
76 }
77 }
78
79 #[must_use]
81 pub fn depth(mut self, depth: usize) -> Self {
82 self.depth = depth;
83 self
84 }
85
86 #[must_use]
88 pub fn page(mut self, page: ObjRef, view: AnnotGoToView) -> Self {
89 self.target = BookmarkTarget::Page { page, view };
90 self
91 }
92
93 #[must_use]
95 pub fn named(mut self, name: impl Into<String>) -> Self {
96 self.target = BookmarkTarget::Named(name.into());
97 self
98 }
99
100 #[must_use]
102 pub fn color(mut self, rgb: (f32, f32, f32)) -> Self {
103 self.color = Some(rgb);
104 self
105 }
106
107 #[must_use]
109 pub fn style(mut self, style: i64) -> Self {
110 self.style = Some(style);
111 self
112 }
113
114 #[must_use]
116 pub fn open(mut self, open: bool) -> Self {
117 self.open = open;
118 self
119 }
120}
121
122pub fn set_outline(dest: &mut EditDoc<'_>, items: &[BookmarkSpec]) -> Result<()> {
158 let Some(root) = dest.base().trailer().reference(names::ROOT) else {
159 return Err(Error::NoDestinationCatalog);
160 };
161 let Some(mut catalog) = dest
162 .fetch(root)
163 .ok()
164 .and_then(|object| object.as_dict().cloned())
165 else {
166 return Err(Error::NoDestinationCatalog);
167 };
168
169 if items.is_empty() {
170 catalog.remove(names::OUTLINES);
171 dest.replace(root, Object::Dict(catalog));
172 return Ok(());
173 }
174
175 let refs: Vec<ObjRef> = items
179 .iter()
180 .map(|_| dest.add(Object::Dict(Dict::new())))
181 .collect();
182 let outlines_ref = dest.add(Object::Dict(Dict::new()));
183
184 let tree = Tree::of(items);
185 let reference_of = |index: usize| refs.get(index).copied();
189 for (index, item) in items.iter().enumerate() {
190 let (Some(node), Some(self_ref)) = (tree.nodes.get(index), reference_of(index)) else {
191 continue;
192 };
193 let dict = item_dict(item, node, index, &tree, items, outlines_ref, reference_of);
194 dest.replace(self_ref, Object::Dict(dict));
195 }
196
197 let mut outlines = Dict::new();
198 outlines.insert(names::TYPE.clone(), Object::Name(Name::from("Outlines")));
199 if let (Some(first), Some(last)) = (
200 tree.roots.first().copied().and_then(reference_of),
201 tree.roots.last().copied().and_then(reference_of),
202 ) {
203 outlines.insert(names::FIRST.clone(), Object::Ref(first));
204 outlines.insert(names::LAST.clone(), Object::Ref(last));
205 }
206 outlines.insert(names::COUNT.clone(), Object::Int(tree.visible_total(items)));
207 dest.replace(outlines_ref, Object::Dict(outlines));
208
209 catalog.insert(names::OUTLINES.clone(), Object::Ref(outlines_ref));
210 dest.replace(root, Object::Dict(catalog));
211 Ok(())
212}
213
214#[derive(Default)]
216struct Node {
217 parent: Option<usize>,
218 prev: Option<usize>,
219 next: Option<usize>,
220 first: Option<usize>,
221 last: Option<usize>,
222 children: Vec<usize>,
223}
224
225struct Tree {
227 nodes: Vec<Node>,
228 roots: Vec<usize>,
229}
230
231impl Tree {
232 fn of(items: &[BookmarkSpec]) -> Self {
238 let mut nodes: Vec<Node> = (0..items.len()).map(|_| Node::default()).collect();
239 let mut roots = Vec::new();
240 let mut at_depth: Vec<usize> = Vec::new();
243
244 for (index, item) in items.iter().enumerate() {
245 let depth = item.depth.min(at_depth.len());
246 at_depth.truncate(depth);
247
248 let link = |prev: usize, next: usize, nodes: &mut Vec<Node>| {
252 if let Some(node) = nodes.get_mut(next) {
253 node.prev = Some(prev);
254 }
255 if let Some(node) = nodes.get_mut(prev) {
256 node.next = Some(next);
257 }
258 };
259
260 if depth == 0 {
261 if let Some(&prev) = roots.last() {
262 link(prev, index, &mut nodes);
263 }
264 roots.push(index);
265 } else if let Some(&parent) = at_depth.get(depth - 1) {
266 if let Some(node) = nodes.get_mut(index) {
267 node.parent = Some(parent);
268 }
269 let last_child = nodes.get(parent).and_then(|p| p.children.last().copied());
270 if let Some(prev) = last_child {
271 link(prev, index, &mut nodes);
272 }
273 if let Some(node) = nodes.get_mut(parent) {
274 node.children.push(index);
275 }
276 }
277 at_depth.push(index);
278 }
279
280 for node in &mut nodes {
281 node.first = node.children.first().copied();
282 node.last = node.children.last().copied();
283 }
284 Self { nodes, roots }
285 }
286
287 fn visible_descendants(&self, index: usize, items: &[BookmarkSpec]) -> i64 {
290 let mut total = 0;
291 let Some(node) = self.nodes.get(index) else {
292 return 0;
293 };
294 for &child in &node.children {
295 total += 1;
296 if items.get(child).is_some_and(|item| item.open) {
297 total += self.visible_descendants(child, items);
298 }
299 }
300 total
301 }
302
303 fn visible_total(&self, items: &[BookmarkSpec]) -> i64 {
305 let mut total = 0;
306 for &root in &self.roots {
307 total += 1;
308 if items.get(root).is_some_and(|item| item.open) {
309 total += self.visible_descendants(root, items);
310 }
311 }
312 total
313 }
314}
315
316fn item_dict(
319 item: &BookmarkSpec,
320 node: &Node,
321 index: usize,
322 tree: &Tree,
323 items: &[BookmarkSpec],
324 outlines_ref: ObjRef,
325 reference_of: impl Fn(usize) -> Option<ObjRef>,
326) -> Dict {
327 let mut dict = Dict::new();
328 dict.insert(
329 names::TITLE.clone(),
330 Object::Str(PdfString::literal(encode_text(&item.title))),
331 );
332 dict.insert(
333 names::PARENT.clone(),
334 Object::Ref(node.parent.and_then(&reference_of).unwrap_or(outlines_ref)),
335 );
336 if let Some(prev) = node.prev.and_then(&reference_of) {
337 dict.insert(names::PREV.clone(), Object::Ref(prev));
338 }
339 if let Some(next) = node.next.and_then(&reference_of) {
340 dict.insert(names::NEXT.clone(), Object::Ref(next));
341 }
342 if let (Some(first), Some(last)) = (
343 node.first.and_then(&reference_of),
344 node.last.and_then(&reference_of),
345 ) {
346 dict.insert(names::FIRST.clone(), Object::Ref(first));
347 dict.insert(names::LAST.clone(), Object::Ref(last));
348 let visible = tree.visible_descendants(index, items);
351 dict.insert(
352 names::COUNT.clone(),
353 Object::Int(if item.open { visible } else { -visible }),
354 );
355 }
356 match &item.target {
357 BookmarkTarget::Page { page, view } => {
358 dict.insert(
359 names::DEST.clone(),
360 Object::Array(crate::annot::goto_dest_array(*page, *view)),
361 );
362 }
363 BookmarkTarget::Named(name) => {
364 dict.insert(
365 names::DEST.clone(),
366 Object::Str(PdfString::literal(name.as_bytes())),
367 );
368 }
369 BookmarkTarget::None => {}
370 }
371 if let Some((r, g, b)) = item.color {
372 dict.insert(
373 names::C.clone(),
374 Object::Array(Array::of([
375 Object::Real(r.clamp(0.0, 1.0)),
376 Object::Real(g.clamp(0.0, 1.0)),
377 Object::Real(b.clamp(0.0, 1.0)),
378 ])),
379 );
380 }
381 if let Some(style) = item.style {
382 dict.insert(names::F.clone(), Object::Int(style));
383 }
384 dict
385}