1use crate::graphics::Color;
4use crate::objects::{Array, Dictionary, Object, ObjectId};
5use crate::structure::destination::Destination;
6use std::collections::VecDeque;
7
8#[derive(Debug, Clone, Copy, Default)]
10pub struct OutlineFlags {
11 pub italic: bool,
13 pub bold: bool,
15}
16
17impl OutlineFlags {
18 #[allow(clippy::wrong_self_convention)]
20 pub fn to_int(&self) -> i64 {
21 let mut flags = 0;
22 if self.italic {
23 flags |= 1;
24 }
25 if self.bold {
26 flags |= 2;
27 }
28 flags
29 }
30}
31
32#[derive(Debug, Clone)]
34pub struct OutlineItem {
35 pub title: String,
37 pub destination: Option<Destination>,
39 pub children: Vec<OutlineItem>,
41 pub color: Option<Color>,
43 pub flags: OutlineFlags,
45 pub open: bool,
47}
48
49impl OutlineItem {
50 pub fn new(title: impl Into<String>) -> Self {
52 Self {
53 title: title.into(),
54 destination: None,
55 children: Vec::new(),
56 color: None,
57 flags: OutlineFlags::default(),
58 open: true,
59 }
60 }
61
62 pub fn with_destination(mut self, dest: Destination) -> Self {
64 self.destination = Some(dest);
65 self
66 }
67
68 pub fn add_child(&mut self, child: OutlineItem) {
70 self.children.push(child);
71 }
72
73 pub fn with_color(mut self, color: Color) -> Self {
75 self.color = Some(color);
76 self
77 }
78
79 pub fn bold(mut self) -> Self {
81 self.flags.bold = true;
82 self
83 }
84
85 pub fn italic(mut self) -> Self {
87 self.flags.italic = true;
88 self
89 }
90
91 pub fn closed(mut self) -> Self {
93 self.open = false;
94 self
95 }
96
97 pub fn count_all(&self) -> i64 {
99 let mut count = 1; for child in &self.children {
101 count += child.count_all();
102 }
103 count
104 }
105
106 pub fn count_visible(&self) -> i64 {
108 let mut count = 1; if self.open {
110 for child in &self.children {
111 count += child.count_visible();
112 }
113 }
114 count
115 }
116}
117
118pub struct OutlineTree {
120 pub items: Vec<OutlineItem>,
122}
123
124impl Default for OutlineTree {
125 fn default() -> Self {
126 Self::new()
127 }
128}
129
130impl OutlineTree {
131 pub fn new() -> Self {
133 Self { items: Vec::new() }
134 }
135
136 pub fn add_item(&mut self, item: OutlineItem) {
138 self.items.push(item);
139 }
140
141 pub fn total_count(&self) -> i64 {
143 self.items.iter().map(|item| item.count_all()).sum()
144 }
145
146 pub fn visible_count(&self) -> i64 {
148 self.items.iter().map(|item| item.count_visible()).sum()
149 }
150}
151
152pub struct OutlineBuilder {
154 tree: OutlineTree,
156 stack: VecDeque<OutlineItem>,
158}
159
160impl Default for OutlineBuilder {
161 fn default() -> Self {
162 Self::new()
163 }
164}
165
166impl OutlineBuilder {
167 pub fn new() -> Self {
169 Self {
170 tree: OutlineTree::new(),
171 stack: VecDeque::new(),
172 }
173 }
174
175 pub fn add_item(&mut self, item: OutlineItem) {
177 if let Some(parent) = self.stack.back_mut() {
178 parent.add_child(item);
179 } else {
180 self.tree.add_item(item);
181 }
182 }
183
184 pub fn push_item(&mut self, item: OutlineItem) {
186 self.stack.push_back(item);
187 }
188
189 pub fn pop_item(&mut self) {
191 if let Some(item) = self.stack.pop_back() {
192 if let Some(parent) = self.stack.back_mut() {
193 parent.add_child(item);
194 } else {
195 self.tree.add_item(item);
196 }
197 }
198 }
199
200 pub fn build(mut self) -> OutlineTree {
202 while !self.stack.is_empty() {
204 self.pop_item();
205 }
206 self.tree
207 }
208}
209
210pub fn outline_item_to_dict(
212 item: &OutlineItem,
213 parent_ref: ObjectId,
214 first_ref: Option<ObjectId>,
215 last_ref: Option<ObjectId>,
216 prev_ref: Option<ObjectId>,
217 next_ref: Option<ObjectId>,
218) -> Dictionary {
219 let mut dict = Dictionary::new();
220
221 dict.set("Title", Object::String(item.title.clone()));
223
224 dict.set("Parent", Object::Reference(parent_ref));
226
227 if let Some(prev) = prev_ref {
229 dict.set("Prev", Object::Reference(prev));
230 }
231 if let Some(next) = next_ref {
232 dict.set("Next", Object::Reference(next));
233 }
234
235 if !item.children.is_empty() {
237 if let Some(first) = first_ref {
238 dict.set("First", Object::Reference(first));
239 }
240 if let Some(last) = last_ref {
241 dict.set("Last", Object::Reference(last));
242 }
243
244 let count = if item.open {
246 item.count_visible() - 1 } else {
248 item.count_all() - 1 };
250 dict.set(
251 "Count",
252 Object::Integer(if item.open { count } else { -count }),
253 );
254 }
255
256 if let Some(dest) = &item.destination {
258 dict.set("Dest", Object::Array(dest.to_array().into()));
259 }
260
261 if let Some(color) = &item.color {
263 let color_array = match color {
264 Color::Rgb(r, g, b) => {
265 Array::from(vec![Object::Real(*r), Object::Real(*g), Object::Real(*b)])
266 }
267 Color::Gray(g) => {
268 Array::from(vec![Object::Real(*g), Object::Real(*g), Object::Real(*g)])
269 }
270 Color::Cmyk(c, m, y, k) => {
271 let r = (1.0 - c) * (1.0 - k);
273 let g = (1.0 - m) * (1.0 - k);
274 let b = (1.0 - y) * (1.0 - k);
275 Array::from(vec![Object::Real(r), Object::Real(g), Object::Real(b)])
276 }
277 };
278 dict.set("C", Object::Array(color_array.into()));
279 }
280
281 let flags = item.flags.to_int();
283 if flags != 0 {
284 dict.set("F", Object::Integer(flags));
285 }
286
287 dict
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293 use crate::structure::destination::PageDestination;
294
295 #[test]
296 fn test_outline_item_new() {
297 let item = OutlineItem::new("Chapter 1");
298 assert_eq!(item.title, "Chapter 1");
299 assert!(item.destination.is_none());
300 assert!(item.children.is_empty());
301 assert!(item.color.is_none());
302 assert!(!item.flags.bold);
303 assert!(!item.flags.italic);
304 assert!(item.open);
305 }
306
307 #[test]
308 fn test_outline_item_builder() {
309 let dest = Destination::fit(PageDestination::PageNumber(0));
310 let item = OutlineItem::new("Bold Chapter")
311 .with_destination(dest)
312 .with_color(Color::rgb(1.0, 0.0, 0.0))
313 .bold()
314 .closed();
315
316 assert!(item.destination.is_some());
317 assert!(item.color.is_some());
318 assert!(item.flags.bold);
319 assert!(!item.open);
320 }
321
322 #[test]
323 fn test_outline_hierarchy() {
324 let mut chapter1 = OutlineItem::new("Chapter 1");
325 chapter1.add_child(OutlineItem::new("Section 1.1"));
326 chapter1.add_child(OutlineItem::new("Section 1.2"));
327
328 assert_eq!(chapter1.children.len(), 2);
329 assert_eq!(chapter1.count_all(), 3); }
331
332 #[test]
333 fn test_outline_count() {
334 let mut root = OutlineItem::new("Book");
335
336 let mut ch1 = OutlineItem::new("Chapter 1");
337 ch1.add_child(OutlineItem::new("Section 1.1"));
338 ch1.add_child(OutlineItem::new("Section 1.2"));
339
340 let mut ch2 = OutlineItem::new("Chapter 2").closed();
341 ch2.add_child(OutlineItem::new("Section 2.1"));
342
343 root.add_child(ch1);
344 root.add_child(ch2);
345
346 assert_eq!(root.count_all(), 6); assert_eq!(root.count_visible(), 5); }
349
350 #[test]
351 fn test_outline_builder() {
352 let mut builder = OutlineBuilder::new();
353
354 builder.add_item(OutlineItem::new("Preface"));
356
357 builder.push_item(OutlineItem::new("Chapter 1"));
359 builder.add_item(OutlineItem::new("Section 1.1"));
360 builder.add_item(OutlineItem::new("Section 1.2"));
361 builder.pop_item();
362
363 builder.add_item(OutlineItem::new("Chapter 2"));
364
365 let tree = builder.build();
366 assert_eq!(tree.items.len(), 3); assert_eq!(tree.total_count(), 5); }
369
370 #[test]
371 fn test_outline_flags() {
372 let flags = OutlineFlags {
373 italic: true,
374 bold: true,
375 };
376 assert_eq!(flags.to_int(), 3);
377
378 let flags2 = OutlineFlags {
379 italic: true,
380 bold: false,
381 };
382 assert_eq!(flags2.to_int(), 1);
383
384 let flags3 = OutlineFlags::default();
385 assert_eq!(flags3.to_int(), 0);
386 }
387
388 #[test]
389 fn test_outline_flags_debug_clone_default() {
390 let flags = OutlineFlags {
391 italic: true,
392 bold: false,
393 };
394 let debug_str = format!("{flags:?}");
395 assert!(debug_str.contains("OutlineFlags"));
396 assert!(debug_str.contains("italic: true"));
397 assert!(debug_str.contains("bold: false"));
398
399 let cloned = flags;
400 assert_eq!(cloned.italic, flags.italic);
401 assert_eq!(cloned.bold, flags.bold);
402
403 let default_flags = OutlineFlags::default();
404 assert!(!default_flags.italic);
405 assert!(!default_flags.bold);
406 }
407
408 #[test]
409 fn test_outline_item_italic() {
410 let item = OutlineItem::new("Italic Text").italic();
411 assert!(item.flags.italic);
412 assert!(!item.flags.bold);
413 }
414
415 #[test]
416 fn test_outline_item_bold_italic() {
417 let item = OutlineItem::new("Bold Italic").bold().italic();
418 assert!(item.flags.italic);
419 assert!(item.flags.bold);
420 assert_eq!(item.flags.to_int(), 3);
421 }
422
423 #[test]
424 fn test_outline_item_with_complex_destination() {
425 use crate::geometry::{Point, Rectangle};
426
427 let dest = Destination::fit_r(
428 PageDestination::PageNumber(5),
429 Rectangle::new(Point::new(100.0, 200.0), Point::new(300.0, 400.0)),
430 );
431 let item = OutlineItem::new("Complex Destination").with_destination(dest.clone());
432
433 assert!(item.destination.is_some());
434 match &item.destination {
435 Some(d) => match &d.page {
436 PageDestination::PageNumber(n) => assert_eq!(*n, 5),
437 _ => panic!("Wrong destination type"),
438 },
439 None => panic!("Destination should be set"),
440 }
441 }
442
443 #[test]
444 fn test_outline_item_with_different_colors() {
445 let rgb_item = OutlineItem::new("RGB Color").with_color(Color::rgb(0.5, 0.7, 1.0));
446 assert!(rgb_item.color.is_some());
447
448 let gray_item = OutlineItem::new("Gray Color").with_color(Color::gray(0.5));
449 assert!(gray_item.color.is_some());
450
451 let cmyk_item = OutlineItem::new("CMYK Color").with_color(Color::cmyk(0.1, 0.2, 0.3, 0.4));
452 assert!(cmyk_item.color.is_some());
453 }
454
455 #[test]
456 fn test_outline_item_debug_clone() {
457 let item = OutlineItem::new("Test Item")
458 .bold()
459 .with_color(Color::rgb(1.0, 0.0, 0.0));
460
461 let debug_str = format!("{item:?}");
462 assert!(debug_str.contains("OutlineItem"));
463 assert!(debug_str.contains("Test Item"));
464
465 let cloned = item.clone();
466 assert_eq!(cloned.title, item.title);
467 assert_eq!(cloned.flags.bold, item.flags.bold);
468 assert_eq!(cloned.open, item.open);
469 }
470
471 #[test]
472 fn test_outline_tree_default() {
473 let tree = OutlineTree::default();
474 assert!(tree.items.is_empty());
475 assert_eq!(tree.total_count(), 0);
476 assert_eq!(tree.visible_count(), 0);
477 }
478
479 #[test]
480 fn test_outline_tree_add_multiple_items() {
481 let mut tree = OutlineTree::new();
482
483 tree.add_item(OutlineItem::new("First"));
484 tree.add_item(OutlineItem::new("Second"));
485 tree.add_item(OutlineItem::new("Third"));
486
487 assert_eq!(tree.items.len(), 3);
488 assert_eq!(tree.total_count(), 3);
489 assert_eq!(tree.visible_count(), 3);
490 }
491
492 #[test]
493 fn test_outline_tree_with_closed_items() {
494 let mut tree = OutlineTree::new();
495
496 let mut chapter = OutlineItem::new("Chapter").closed();
497 chapter.add_child(OutlineItem::new("Hidden Section 1"));
498 chapter.add_child(OutlineItem::new("Hidden Section 2"));
499
500 tree.add_item(chapter);
501 tree.add_item(OutlineItem::new("Visible Item"));
502
503 assert_eq!(tree.total_count(), 4); assert_eq!(tree.visible_count(), 2); }
506
507 #[test]
508 fn test_outline_builder_default() {
509 let builder = OutlineBuilder::default();
510 let tree = builder.build();
511 assert!(tree.items.is_empty());
512 }
513
514 #[test]
515 fn test_outline_builder_nested_structure() {
516 let mut builder = OutlineBuilder::new();
517
518 builder.push_item(OutlineItem::new("Part I"));
520 builder.push_item(OutlineItem::new("Chapter 1"));
521 builder.add_item(OutlineItem::new("Section 1.1"));
522 builder.add_item(OutlineItem::new("Section 1.2"));
523 builder.pop_item(); builder.push_item(OutlineItem::new("Chapter 2"));
525 builder.add_item(OutlineItem::new("Section 2.1"));
526 builder.pop_item(); builder.pop_item(); builder.add_item(OutlineItem::new("Part II"));
530
531 let tree = builder.build();
532 assert_eq!(tree.items.len(), 2); assert_eq!(tree.total_count(), 7); }
535
536 #[test]
537 fn test_outline_builder_auto_pop() {
538 let mut builder = OutlineBuilder::new();
539
540 builder.push_item(OutlineItem::new("Root"));
542 builder.push_item(OutlineItem::new("Child"));
543 builder.add_item(OutlineItem::new("Grandchild"));
544
545 let tree = builder.build();
546 assert_eq!(tree.items.len(), 1); assert_eq!(tree.total_count(), 3); }
549
550 #[test]
551 fn test_outline_item_count_deep_hierarchy() {
552 let mut root = OutlineItem::new("Root");
553
554 let mut level1 = OutlineItem::new("Level 1");
555 let mut level2 = OutlineItem::new("Level 2");
556 let mut level3 = OutlineItem::new("Level 3");
557 level3.add_child(OutlineItem::new("Level 4"));
558 level2.add_child(level3);
559 level1.add_child(level2);
560 root.add_child(level1);
561
562 assert_eq!(root.count_all(), 5); assert_eq!(root.count_visible(), 5); root.children[0].children[0].open = false;
567 assert_eq!(root.count_visible(), 3); }
569
570 #[test]
571 fn test_outline_item_to_dict_basic() {
572 let item = OutlineItem::new("Test Title");
573 let parent_ref = ObjectId::new(1, 0);
574
575 let dict = outline_item_to_dict(&item, parent_ref, None, None, None, None);
576
577 assert_eq!(
578 dict.get("Title"),
579 Some(&Object::String("Test Title".to_string()))
580 );
581 assert_eq!(dict.get("Parent"), Some(&Object::Reference(parent_ref)));
582 assert!(dict.get("Prev").is_none());
583 assert!(dict.get("Next").is_none());
584 assert!(dict.get("First").is_none());
585 assert!(dict.get("Last").is_none());
586 }
587
588 #[test]
589 fn test_outline_item_to_dict_with_siblings() {
590 let item = OutlineItem::new("Middle Child");
591 let parent_ref = ObjectId::new(1, 0);
592 let prev_ref = Some(ObjectId::new(2, 0));
593 let next_ref = Some(ObjectId::new(3, 0));
594
595 let dict = outline_item_to_dict(&item, parent_ref, None, None, prev_ref, next_ref);
596
597 assert_eq!(
598 dict.get("Prev"),
599 Some(&Object::Reference(ObjectId::new(2, 0)))
600 );
601 assert_eq!(
602 dict.get("Next"),
603 Some(&Object::Reference(ObjectId::new(3, 0)))
604 );
605 }
606
607 #[test]
608 fn test_outline_item_to_dict_with_children() {
609 let mut item = OutlineItem::new("Parent");
610 item.add_child(OutlineItem::new("Child 1"));
611 item.add_child(OutlineItem::new("Child 2"));
612
613 let parent_ref = ObjectId::new(1, 0);
614 let first_ref = Some(ObjectId::new(10, 0));
615 let last_ref = Some(ObjectId::new(11, 0));
616
617 let dict = outline_item_to_dict(&item, parent_ref, first_ref, last_ref, None, None);
618
619 assert_eq!(
620 dict.get("First"),
621 Some(&Object::Reference(ObjectId::new(10, 0)))
622 );
623 assert_eq!(
624 dict.get("Last"),
625 Some(&Object::Reference(ObjectId::new(11, 0)))
626 );
627 assert_eq!(dict.get("Count"), Some(&Object::Integer(2))); }
629
630 #[test]
631 fn test_outline_item_to_dict_closed_with_children() {
632 let mut item = OutlineItem::new("Closed Parent").closed();
633 item.add_child(OutlineItem::new("Hidden 1"));
634 item.add_child(OutlineItem::new("Hidden 2"));
635 item.add_child(OutlineItem::new("Hidden 3"));
636
637 let dict = outline_item_to_dict(
638 &item,
639 ObjectId::new(1, 0),
640 Some(ObjectId::new(10, 0)),
641 Some(ObjectId::new(12, 0)),
642 None,
643 None,
644 );
645
646 assert_eq!(dict.get("Count"), Some(&Object::Integer(-3)));
648 }
649
650 #[test]
651 fn test_outline_item_to_dict_with_destination() {
652 let dest = Destination::xyz(
653 PageDestination::PageNumber(5),
654 Some(100.0),
655 Some(200.0),
656 Some(1.5),
657 );
658 let item = OutlineItem::new("With Destination").with_destination(dest);
659
660 let dict = outline_item_to_dict(&item, ObjectId::new(1, 0), None, None, None, None);
661
662 assert!(dict.get("Dest").is_some());
663 match dict.get("Dest") {
664 Some(Object::Array(arr)) => {
665 assert!(!arr.is_empty());
667 }
668 _ => panic!("Dest should be an array"),
669 }
670 }
671
672 #[test]
673 fn test_outline_item_to_dict_with_color_rgb() {
674 let item = OutlineItem::new("Red Item").with_color(Color::rgb(1.0, 0.0, 0.0));
675
676 let dict = outline_item_to_dict(&item, ObjectId::new(1, 0), None, None, None, None);
677
678 match dict.get("C") {
679 Some(Object::Array(arr)) => {
680 assert_eq!(arr.len(), 3);
681 assert_eq!(arr.first(), Some(&Object::Real(1.0)));
682 assert_eq!(arr.get(1), Some(&Object::Real(0.0)));
683 assert_eq!(arr.get(2), Some(&Object::Real(0.0)));
684 }
685 _ => panic!("C should be an array"),
686 }
687 }
688
689 #[test]
690 fn test_outline_item_to_dict_with_color_gray() {
691 let item = OutlineItem::new("Gray Item").with_color(Color::gray(0.5));
692
693 let dict = outline_item_to_dict(&item, ObjectId::new(1, 0), None, None, None, None);
694
695 match dict.get("C") {
696 Some(Object::Array(arr)) => {
697 assert_eq!(arr.len(), 3);
698 assert_eq!(arr.first(), Some(&Object::Real(0.5)));
700 assert_eq!(arr.get(1), Some(&Object::Real(0.5)));
701 assert_eq!(arr.get(2), Some(&Object::Real(0.5)));
702 }
703 _ => panic!("C should be an array"),
704 }
705 }
706
707 #[test]
708 fn test_outline_item_to_dict_with_color_cmyk() {
709 let item = OutlineItem::new("CMYK Item").with_color(Color::cmyk(0.0, 1.0, 1.0, 0.0));
710
711 let dict = outline_item_to_dict(&item, ObjectId::new(1, 0), None, None, None, None);
712
713 match dict.get("C") {
714 Some(Object::Array(arr)) => {
715 assert_eq!(arr.len(), 3);
716 assert_eq!(arr.first(), Some(&Object::Real(1.0)));
718 assert_eq!(arr.get(1), Some(&Object::Real(0.0)));
719 assert_eq!(arr.get(2), Some(&Object::Real(0.0)));
720 }
721 _ => panic!("C should be an array"),
722 }
723 }
724
725 #[test]
726 fn test_outline_item_to_dict_with_flags() {
727 let item = OutlineItem::new("Styled Item").bold().italic();
728
729 let dict = outline_item_to_dict(&item, ObjectId::new(1, 0), None, None, None, None);
730
731 assert_eq!(dict.get("F"), Some(&Object::Integer(3))); }
733
734 #[test]
735 fn test_outline_item_to_dict_no_flags() {
736 let item = OutlineItem::new("Plain Item");
737
738 let dict = outline_item_to_dict(&item, ObjectId::new(1, 0), None, None, None, None);
739
740 assert!(dict.get("F").is_none());
742 }
743
744 #[test]
745 fn test_outline_tree_empty_counts() {
746 let tree = OutlineTree::new();
747 assert_eq!(tree.total_count(), 0);
748 assert_eq!(tree.visible_count(), 0);
749 }
750
751 #[test]
752 fn test_outline_builder_empty_pop() {
753 let mut builder = OutlineBuilder::new();
754 builder.pop_item();
756 let tree = builder.build();
757 assert!(tree.items.is_empty());
758 }
759
760 #[test]
761 fn test_outline_complex_visibility() {
762 let mut root = OutlineItem::new("Book");
763
764 let mut part1 = OutlineItem::new("Part 1"); let mut ch1 = OutlineItem::new("Chapter 1").closed();
766 ch1.add_child(OutlineItem::new("Section 1.1"));
767 ch1.add_child(OutlineItem::new("Section 1.2"));
768 part1.add_child(ch1);
769
770 let mut ch2 = OutlineItem::new("Chapter 2"); ch2.add_child(OutlineItem::new("Section 2.1"));
772 part1.add_child(ch2);
773
774 root.add_child(part1);
775
776 assert_eq!(root.count_all(), 7); assert_eq!(root.count_visible(), 5); }
788}