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