Skip to main content

oxidize_pdf/page_labels/
page_label_tree.rs

1//! Page label tree structure for managing page numbering
2
3use crate::objects::{Array, Dictionary, Object};
4use crate::page_labels::PageLabel;
5use std::collections::BTreeMap;
6
7/// Page label tree - manages custom page numbering for a document
8#[derive(Debug, Clone)]
9pub struct PageLabelTree {
10    /// Page label ranges, sorted by starting page
11    ranges: BTreeMap<u32, PageLabel>,
12}
13
14impl PageLabelTree {
15    /// Create a new empty page label tree
16    pub fn new() -> Self {
17        Self {
18            ranges: BTreeMap::new(),
19        }
20    }
21
22    /// Add a page label range
23    pub fn add_range(&mut self, start_page: u32, label: PageLabel) {
24        self.ranges.insert(start_page, label);
25    }
26
27    /// Get the page label for a specific page
28    pub fn get_label(&self, page_index: u32) -> Option<String> {
29        // Find the applicable range
30        let mut applicable_range = None;
31        let mut range_start = 0;
32
33        for (&start, label) in &self.ranges {
34            if start <= page_index {
35                applicable_range = Some(label);
36                range_start = start;
37            } else {
38                break;
39            }
40        }
41
42        // Format the label if found
43        applicable_range.map(|label| {
44            let offset = page_index - range_start;
45            label.format_label(offset)
46        })
47    }
48
49    /// Get all page labels for a document
50    pub fn get_all_labels(&self, total_pages: u32) -> Vec<String> {
51        (0..total_pages)
52            .map(|i| self.get_label(i).unwrap_or_else(|| (i + 1).to_string()))
53            .collect()
54    }
55
56    /// Convert to PDF number tree dictionary
57    pub fn to_dict(&self) -> Dictionary {
58        let mut dict = Dictionary::new();
59
60        // Create nums array [key1 val1 key2 val2 ...]
61        let mut nums = Array::new();
62
63        for (&start_page, label) in &self.ranges {
64            nums.push(Object::Integer(start_page as i64));
65            nums.push(Object::Dictionary(label.to_dict()));
66        }
67
68        dict.set("Nums", Object::Array(nums.into()));
69
70        dict
71    }
72
73    /// Create from PDF dictionary
74    pub fn from_dict(dict: &Dictionary) -> Option<Self> {
75        let nums_array = match dict.get("Nums")? {
76            Object::Array(arr) => arr,
77            _ => return None,
78        };
79        let mut tree = Self::new();
80
81        // Parse pairs of [page_index, label_dict]
82        let elements: Vec<&Object> = nums_array.iter().collect();
83        for i in (0..elements.len()).step_by(2) {
84            if i + 1 >= elements.len() {
85                break;
86            }
87
88            let page_index = match elements[i] {
89                Object::Integer(n) => *n as u32,
90                _ => continue,
91            };
92            let label_dict = match elements[i + 1] {
93                Object::Dictionary(d) => d,
94                _ => continue,
95            };
96
97            // Parse numbering style per ISO 32000-1 §12.4.2 Table 159.
98            // Spec key is /S; accept a legacy /Type-carrying-style as a
99            // tolerant fallback so documents written by older versions of
100            // this crate still round-trip.
101            let style_name = match label_dict.get("S") {
102                Some(Object::Name(s)) => Some(s.as_str()),
103                _ => match label_dict.get("Type") {
104                    Some(Object::Name(t)) if t != "PageLabel" => Some(t.as_str()),
105                    _ => None,
106                },
107            };
108            // Per ISO 32000-1 §12.4.2 Table 159 the /S value is case
109            // sensitive: /R = uppercase Roman, /r = lowercase Roman;
110            // /A = uppercase letters, /a = lowercase letters.
111            let style = match style_name {
112                Some("D") => PageLabelStyle::DecimalArabic,
113                Some("R") => PageLabelStyle::UppercaseRoman,
114                Some("r") => PageLabelStyle::LowercaseRoman,
115                Some("A") => PageLabelStyle::UppercaseLetters,
116                Some("a") => PageLabelStyle::LowercaseLetters,
117                _ => PageLabelStyle::None,
118            };
119
120            let mut label = PageLabel::new(style);
121
122            if let Some(Object::String(prefix)) = label_dict.get("P") {
123                label = label.with_prefix(prefix);
124            }
125
126            if let Some(Object::Integer(start)) = label_dict.get("St") {
127                label = label.starting_at(*start as u32);
128            }
129
130            tree.add_range(page_index, label);
131        }
132
133        Some(tree)
134    }
135}
136
137impl Default for PageLabelTree {
138    fn default() -> Self {
139        Self::new()
140    }
141}
142
143/// Builder for creating page label trees
144pub struct PageLabelBuilder {
145    tree: PageLabelTree,
146    current_page: u32,
147}
148
149impl Default for PageLabelBuilder {
150    fn default() -> Self {
151        Self::new()
152    }
153}
154
155impl PageLabelBuilder {
156    /// Create a new page label builder
157    pub fn new() -> Self {
158        Self {
159            tree: PageLabelTree::new(),
160            current_page: 0,
161        }
162    }
163
164    /// Add a range with a specific label
165    pub fn add_range(mut self, num_pages: u32, label: PageLabel) -> Self {
166        self.tree.add_range(self.current_page, label);
167        self.current_page += num_pages;
168        self
169    }
170
171    /// Add pages with decimal numbering
172    pub fn decimal_pages(self, num_pages: u32) -> Self {
173        self.add_range(num_pages, PageLabel::decimal())
174    }
175
176    /// Add pages with roman numbering
177    pub fn roman_pages(self, num_pages: u32, uppercase: bool) -> Self {
178        let label = if uppercase {
179            PageLabel::roman_uppercase()
180        } else {
181            PageLabel::roman_lowercase()
182        };
183        self.add_range(num_pages, label)
184    }
185
186    /// Add pages with letter numbering
187    pub fn letter_pages(self, num_pages: u32, uppercase: bool) -> Self {
188        let label = if uppercase {
189            PageLabel::letters_uppercase()
190        } else {
191            PageLabel::letters_lowercase()
192        };
193        self.add_range(num_pages, label)
194    }
195
196    /// Add pages with only a prefix
197    pub fn prefix_pages(self, num_pages: u32, prefix: impl Into<String>) -> Self {
198        self.add_range(num_pages, PageLabel::prefix_only(prefix))
199    }
200
201    /// Build the page label tree
202    pub fn build(self) -> PageLabelTree {
203        self.tree
204    }
205}
206
207// Import PageLabelStyle from the other module
208use crate::page_labels::PageLabelStyle;
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn test_page_label_tree() {
216        let mut tree = PageLabelTree::new();
217
218        // Add roman numerals for first 3 pages
219        tree.add_range(0, PageLabel::roman_lowercase());
220
221        // Add decimal starting at page 3
222        tree.add_range(3, PageLabel::decimal());
223
224        // Test labels
225        assert_eq!(tree.get_label(0), Some("i".to_string()));
226        assert_eq!(tree.get_label(1), Some("ii".to_string()));
227        assert_eq!(tree.get_label(2), Some("iii".to_string()));
228        assert_eq!(tree.get_label(3), Some("1".to_string()));
229        assert_eq!(tree.get_label(4), Some("2".to_string()));
230        assert_eq!(tree.get_label(5), Some("3".to_string()));
231    }
232
233    #[test]
234    fn test_page_label_with_prefix() {
235        let mut tree = PageLabelTree::new();
236
237        // Preface with prefix
238        tree.add_range(0, PageLabel::prefix_only("Cover"));
239        tree.add_range(1, PageLabel::roman_lowercase().with_prefix("p. "));
240        tree.add_range(4, PageLabel::decimal().with_prefix("Chapter "));
241
242        assert_eq!(tree.get_label(0), Some("Cover".to_string()));
243        assert_eq!(tree.get_label(1), Some("p. i".to_string()));
244        assert_eq!(tree.get_label(2), Some("p. ii".to_string()));
245        assert_eq!(tree.get_label(3), Some("p. iii".to_string()));
246        assert_eq!(tree.get_label(4), Some("Chapter 1".to_string()));
247        assert_eq!(tree.get_label(5), Some("Chapter 2".to_string()));
248    }
249
250    #[test]
251    fn test_page_label_with_start() {
252        let mut tree = PageLabelTree::new();
253
254        // Start numbering at 10
255        tree.add_range(0, PageLabel::decimal().starting_at(10));
256
257        assert_eq!(tree.get_label(0), Some("10".to_string()));
258        assert_eq!(tree.get_label(1), Some("11".to_string()));
259        assert_eq!(tree.get_label(2), Some("12".to_string()));
260    }
261
262    #[test]
263    fn test_get_all_labels() {
264        let mut tree = PageLabelTree::new();
265        tree.add_range(0, PageLabel::roman_lowercase());
266        tree.add_range(2, PageLabel::decimal());
267
268        let labels = tree.get_all_labels(5);
269        assert_eq!(labels, vec!["i", "ii", "1", "2", "3"]);
270    }
271
272    #[test]
273    fn test_page_label_builder() {
274        let tree = PageLabelBuilder::new()
275            .prefix_pages(1, "Cover")
276            .roman_pages(3, false)
277            .decimal_pages(10)
278            .letter_pages(3, true)
279            .build();
280
281        assert_eq!(tree.get_label(0), Some("Cover".to_string()));
282        assert_eq!(tree.get_label(1), Some("i".to_string()));
283        assert_eq!(tree.get_label(2), Some("ii".to_string()));
284        assert_eq!(tree.get_label(3), Some("iii".to_string()));
285        assert_eq!(tree.get_label(4), Some("1".to_string()));
286        assert_eq!(tree.get_label(13), Some("10".to_string()));
287        assert_eq!(tree.get_label(14), Some("A".to_string()));
288        assert_eq!(tree.get_label(15), Some("B".to_string()));
289        assert_eq!(tree.get_label(16), Some("C".to_string()));
290    }
291
292    #[test]
293    fn test_to_dict() {
294        let mut tree = PageLabelTree::new();
295        tree.add_range(0, PageLabel::roman_lowercase());
296        tree.add_range(3, PageLabel::decimal().with_prefix("Page "));
297
298        let dict = tree.to_dict();
299        assert!(dict.get("Nums").is_some());
300    }
301
302    #[test]
303    fn test_page_label_tree_default() {
304        let tree = PageLabelTree::default();
305        // Empty tree should return None for any page
306        assert!(tree.get_label(0).is_none());
307        assert!(tree.get_label(100).is_none());
308    }
309
310    #[test]
311    fn test_page_label_tree_clone() {
312        let mut tree = PageLabelTree::new();
313        tree.add_range(0, PageLabel::decimal());
314        let cloned = tree.clone();
315        assert_eq!(tree.get_label(0), cloned.get_label(0));
316    }
317
318    #[test]
319    fn test_page_label_tree_debug() {
320        let tree = PageLabelTree::new();
321        let debug_str = format!("{:?}", tree);
322        assert!(debug_str.contains("PageLabelTree"));
323    }
324
325    #[test]
326    fn test_page_label_builder_default() {
327        let builder = PageLabelBuilder::default();
328        let tree = builder.build();
329        // Empty tree
330        assert!(tree.get_label(0).is_none());
331    }
332
333    #[test]
334    fn test_page_label_builder_roman_uppercase() {
335        let tree = PageLabelBuilder::new().roman_pages(5, true).build();
336
337        assert_eq!(tree.get_label(0), Some("I".to_string()));
338        assert_eq!(tree.get_label(1), Some("II".to_string()));
339        assert_eq!(tree.get_label(2), Some("III".to_string()));
340        assert_eq!(tree.get_label(3), Some("IV".to_string()));
341        assert_eq!(tree.get_label(4), Some("V".to_string()));
342    }
343
344    #[test]
345    fn test_page_label_builder_letter_lowercase() {
346        let tree = PageLabelBuilder::new().letter_pages(3, false).build();
347
348        assert_eq!(tree.get_label(0), Some("a".to_string()));
349        assert_eq!(tree.get_label(1), Some("b".to_string()));
350        assert_eq!(tree.get_label(2), Some("c".to_string()));
351    }
352
353    #[test]
354    fn test_get_all_labels_empty_tree() {
355        let tree = PageLabelTree::new();
356        // Empty tree should return default numbering
357        let labels = tree.get_all_labels(3);
358        assert_eq!(labels, vec!["1", "2", "3"]);
359    }
360
361    #[test]
362    fn test_from_dict_empty() {
363        let mut dict = Dictionary::new();
364        dict.set("Nums", Object::Array(Array::new().into()));
365
366        let tree = PageLabelTree::from_dict(&dict);
367        assert!(tree.is_some());
368        let tree = tree.unwrap();
369        assert!(tree.get_label(0).is_none());
370    }
371
372    #[test]
373    fn test_from_dict_missing_nums() {
374        let dict = Dictionary::new();
375        let tree = PageLabelTree::from_dict(&dict);
376        assert!(tree.is_none());
377    }
378
379    #[test]
380    fn test_from_dict_invalid_nums_type() {
381        let mut dict = Dictionary::new();
382        dict.set("Nums", Object::Integer(42));
383
384        let tree = PageLabelTree::from_dict(&dict);
385        assert!(tree.is_none());
386    }
387
388    #[test]
389    fn test_from_dict_with_decimal_labels() {
390        let mut label_dict = Dictionary::new();
391        label_dict.set("Type", Object::Name("D".to_string()));
392
393        let mut nums = Array::new();
394        nums.push(Object::Integer(0));
395        nums.push(Object::Dictionary(label_dict));
396
397        let mut dict = Dictionary::new();
398        dict.set("Nums", Object::Array(nums.into()));
399
400        let tree = PageLabelTree::from_dict(&dict);
401        assert!(tree.is_some());
402    }
403
404    #[test]
405    fn test_from_dict_reads_roman_case_per_iso_spec() {
406        // ISO 32000-1 §12.4.2 Table 159 binds /S /R to uppercase Roman
407        // numerals and /S /r to lowercase. from_dict() must therefore
408        // map /R → UppercaseRoman and /r → LowercaseRoman. The v2.5.5
409        // reader was mirror-inverted to compensate for the inverted
410        // writer, so /R was read back as LowercaseRoman; external PDFs
411        // (written by other tools per spec) were interpreted upside
412        // down.
413        //
414        // We can't see the style enum directly (it is private to the
415        // PageLabel fields) so we assert on the rendered label, which
416        // calls format() under the hood — format() is the one thing
417        // that has always been correct.
418        fn build_tree(style_name: &str) -> PageLabelTree {
419            let mut label_dict = Dictionary::new();
420            label_dict.set("Type", Object::Name("PageLabel".to_string()));
421            label_dict.set("S", Object::Name(style_name.to_string()));
422
423            let mut nums = Array::new();
424            nums.push(Object::Integer(0));
425            nums.push(Object::Dictionary(label_dict));
426
427            let mut dict = Dictionary::new();
428            dict.set("Nums", Object::Array(nums.into()));
429            PageLabelTree::from_dict(&dict).expect("valid page label tree")
430        }
431
432        let upper = build_tree("R");
433        assert_eq!(
434            upper.get_label(0),
435            Some("I".to_string()),
436            "/S /R must be parsed as UppercaseRoman per ISO 32000-1 §12.4.2 \
437             Table 159 (viewer renders 'I', not 'i')"
438        );
439
440        let lower = build_tree("r");
441        assert_eq!(
442            lower.get_label(0),
443            Some("i".to_string()),
444            "/S /r must be parsed as LowercaseRoman per ISO 32000-1 §12.4.2 \
445             Table 159 (viewer renders 'i', not 'I')"
446        );
447    }
448
449    #[test]
450    fn test_from_dict_with_prefix_and_start() {
451        let mut label_dict = Dictionary::new();
452        label_dict.set("Type", Object::Name("D".to_string()));
453        label_dict.set("P", Object::String("Page ".to_string()));
454        label_dict.set("St", Object::Integer(10));
455
456        let mut nums = Array::new();
457        nums.push(Object::Integer(0));
458        nums.push(Object::Dictionary(label_dict));
459
460        let mut dict = Dictionary::new();
461        dict.set("Nums", Object::Array(nums.into()));
462
463        let tree = PageLabelTree::from_dict(&dict);
464        assert!(tree.is_some());
465    }
466
467    #[test]
468    fn test_from_dict_invalid_page_index() {
469        // Test with non-integer page index
470        let label_dict = Dictionary::new();
471
472        let mut nums = Array::new();
473        nums.push(Object::String("not_an_integer".to_string()));
474        nums.push(Object::Dictionary(label_dict));
475
476        let mut dict = Dictionary::new();
477        dict.set("Nums", Object::Array(nums.into()));
478
479        // Should skip invalid entries
480        let tree = PageLabelTree::from_dict(&dict);
481        assert!(tree.is_some());
482    }
483
484    #[test]
485    fn test_from_dict_invalid_label_dict() {
486        // Test with non-dictionary label
487        let mut nums = Array::new();
488        nums.push(Object::Integer(0));
489        nums.push(Object::String("not_a_dict".to_string()));
490
491        let mut dict = Dictionary::new();
492        dict.set("Nums", Object::Array(nums.into()));
493
494        // Should skip invalid entries
495        let tree = PageLabelTree::from_dict(&dict);
496        assert!(tree.is_some());
497    }
498
499    #[test]
500    fn test_from_dict_odd_length_array() {
501        // Array with odd length - last element should be ignored
502        let label_dict = Dictionary::new();
503
504        let mut nums = Array::new();
505        nums.push(Object::Integer(0));
506        nums.push(Object::Dictionary(label_dict));
507        nums.push(Object::Integer(5)); // Missing pair
508
509        let mut dict = Dictionary::new();
510        dict.set("Nums", Object::Array(nums.into()));
511
512        let tree = PageLabelTree::from_dict(&dict);
513        assert!(tree.is_some());
514    }
515
516    #[test]
517    fn test_from_dict_all_style_types() {
518        // Exercises the legacy /Type-carrying-style fallback for each
519        // ISO 32000-1 §12.4.2 Table 159 style name. Asserts only that
520        // parsing succeeds; for the case-sensitive semantics of /R vs
521        // /r and /A vs /a see `test_from_dict_reads_roman_case_per_iso_spec`.
522
523        // Test r (lowercase roman)
524        let mut label_dict1 = Dictionary::new();
525        label_dict1.set("Type", Object::Name("r".to_string()));
526
527        // Test R (uppercase roman)
528        let mut label_dict2 = Dictionary::new();
529        label_dict2.set("Type", Object::Name("R".to_string()));
530
531        // Test A (uppercase letters)
532        let mut label_dict3 = Dictionary::new();
533        label_dict3.set("Type", Object::Name("A".to_string()));
534
535        // Test a (lowercase letters)
536        let mut label_dict4 = Dictionary::new();
537        label_dict4.set("Type", Object::Name("a".to_string()));
538
539        // Test unknown type
540        let mut label_dict5 = Dictionary::new();
541        label_dict5.set("Type", Object::Name("unknown".to_string()));
542
543        let mut nums = Array::new();
544        nums.push(Object::Integer(0));
545        nums.push(Object::Dictionary(label_dict1));
546        nums.push(Object::Integer(5));
547        nums.push(Object::Dictionary(label_dict2));
548        nums.push(Object::Integer(10));
549        nums.push(Object::Dictionary(label_dict3));
550        nums.push(Object::Integer(15));
551        nums.push(Object::Dictionary(label_dict4));
552        nums.push(Object::Integer(20));
553        nums.push(Object::Dictionary(label_dict5));
554
555        let mut dict = Dictionary::new();
556        dict.set("Nums", Object::Array(nums.into()));
557
558        let tree = PageLabelTree::from_dict(&dict);
559        assert!(tree.is_some());
560    }
561
562    #[test]
563    fn test_get_label_no_applicable_range() {
564        let mut tree = PageLabelTree::new();
565        // Add range starting at page 5
566        tree.add_range(5, PageLabel::decimal());
567
568        // Pages before the first range should return None
569        assert!(tree.get_label(0).is_none());
570        assert!(tree.get_label(4).is_none());
571
572        // Pages at or after the range should have labels
573        assert_eq!(tree.get_label(5), Some("1".to_string()));
574        assert_eq!(tree.get_label(6), Some("2".to_string()));
575    }
576
577    #[test]
578    fn test_page_label_builder_chained() {
579        let tree = PageLabelBuilder::new()
580            .prefix_pages(1, "TOC")
581            .roman_pages(2, false)
582            .roman_pages(2, true)
583            .letter_pages(2, false)
584            .letter_pages(2, true)
585            .decimal_pages(5)
586            .build();
587
588        assert_eq!(tree.get_label(0), Some("TOC".to_string()));
589        assert_eq!(tree.get_label(1), Some("i".to_string()));
590        assert_eq!(tree.get_label(3), Some("I".to_string()));
591        assert_eq!(tree.get_label(5), Some("a".to_string()));
592        assert_eq!(tree.get_label(7), Some("A".to_string()));
593        assert_eq!(tree.get_label(9), Some("1".to_string()));
594    }
595
596    #[test]
597    fn test_to_dict_round_trip() {
598        let mut original = PageLabelTree::new();
599        original.add_range(0, PageLabel::roman_lowercase());
600        original.add_range(5, PageLabel::decimal());
601
602        let dict = original.to_dict();
603        let restored = PageLabelTree::from_dict(&dict);
604
605        assert!(restored.is_some());
606        // Note: exact round-trip may not work perfectly due to style mapping differences
607        // but basic structure should be preserved
608    }
609}