1use crate::error::{JustPdfError, Result};
8use crate::object::{PdfDict, PdfObject};
9use crate::parser::PdfDocument;
10use crate::writer::modify::DocumentModifier;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum PageLabelStyle {
19 Decimal,
21 UpperRoman,
23 LowerRoman,
25 UpperAlpha,
27 LowerAlpha,
29 None,
31}
32
33impl PageLabelStyle {
34 fn from_name(name: &[u8]) -> Option<Self> {
36 match name {
37 b"D" => Some(Self::Decimal),
38 b"R" => Some(Self::UpperRoman),
39 b"r" => Some(Self::LowerRoman),
40 b"A" => Some(Self::UpperAlpha),
41 b"a" => Some(Self::LowerAlpha),
42 _ => Option::None,
43 }
44 }
45
46 fn to_name(&self) -> Option<&'static [u8]> {
48 match self {
49 Self::Decimal => Some(b"D"),
50 Self::UpperRoman => Some(b"R"),
51 Self::LowerRoman => Some(b"r"),
52 Self::UpperAlpha => Some(b"A"),
53 Self::LowerAlpha => Some(b"a"),
54 Self::None => Option::None,
55 }
56 }
57}
58
59#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct PageLabelRange {
63 pub start_page: usize,
65 pub style: PageLabelStyle,
67 pub prefix: String,
69 pub logical_start: i64,
71}
72
73impl PageLabelRange {
74 pub fn new(start_page: usize, style: PageLabelStyle) -> Self {
77 Self {
78 start_page,
79 style,
80 prefix: String::new(),
81 logical_start: 1,
82 }
83 }
84
85 pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
87 self.prefix = prefix.into();
88 self
89 }
90
91 pub fn with_logical_start(mut self, start: i64) -> Self {
93 self.logical_start = start;
94 self
95 }
96}
97
98fn parse_number_tree(
109 doc: &PdfDocument,
110 node: &PdfObject,
111 out: &mut Vec<(i64, PdfObject)>,
112) -> Result<()> {
113 let dict = match node {
114 PdfObject::Dict(d) => d.clone(),
115 PdfObject::Reference(r) => {
116 let resolved = doc.resolve(r)?;
117 match resolved {
118 PdfObject::Dict(d) => d,
119 _ => return Ok(()),
120 }
121 }
122 _ => return Ok(()),
123 };
124
125 if let Some(nums) = dict.get_array(b"Nums") {
127 let mut i = 0;
128 while i + 1 < nums.len() {
129 if let Some(key) = nums[i].as_i64() {
130 out.push((key, nums[i + 1].clone()));
131 }
132 i += 2;
133 }
134 }
135
136 if let Some(kids) = dict.get_array(b"Kids") {
138 let kids_owned: Vec<PdfObject> = kids.to_vec();
139 for kid in &kids_owned {
140 match kid {
141 PdfObject::Reference(r) => {
142 let child = doc.resolve(r)?;
143 parse_number_tree(doc, &child, out)?;
144 }
145 PdfObject::Dict(_) => {
146 parse_number_tree(doc, kid, out)?;
147 }
148 _ => {}
149 }
150 }
151 }
152
153 Ok(())
154}
155
156fn build_nums_array(entries: &[(i64, PdfObject)]) -> Vec<PdfObject> {
158 let mut arr = Vec::with_capacity(entries.len() * 2);
159 for (key, value) in entries {
160 arr.push(PdfObject::Integer(*key));
161 arr.push(value.clone());
162 }
163 arr
164}
165
166pub fn read_page_labels(doc: &PdfDocument) -> Result<Vec<PageLabelRange>> {
174 let catalog_ref = match doc.catalog_ref() {
176 Some(r) => r.clone(),
177 None => return Ok(Vec::new()),
178 };
179 let catalog = match doc.resolve(&catalog_ref)? {
180 PdfObject::Dict(d) => d,
181 _ => return Ok(Vec::new()),
182 };
183
184 let page_labels_obj = match catalog.get(b"PageLabels") {
186 Some(PdfObject::Reference(r)) => {
187 let r = r.clone();
188 doc.resolve(&r)?
189 }
190 Some(obj) => obj.clone(),
191 None => return Ok(Vec::new()),
192 };
193
194 let mut entries: Vec<(i64, PdfObject)> = Vec::new();
196 parse_number_tree(doc, &page_labels_obj, &mut entries)?;
197
198 entries.sort_by_key(|(k, _)| *k);
200
201 let mut ranges = Vec::with_capacity(entries.len());
203 for (page_index, value) in &entries {
204 let label_dict = match value {
205 PdfObject::Dict(d) => d.clone(),
206 PdfObject::Reference(r) => {
207 let r = r.clone();
208 match doc.resolve(&r)? {
209 PdfObject::Dict(d) => d,
210 _ => continue,
211 }
212 }
213 _ => continue,
214 };
215
216 let style = match label_dict.get_name(b"S") {
217 Some(name) => PageLabelStyle::from_name(name).unwrap_or(PageLabelStyle::None),
218 None => PageLabelStyle::None,
219 };
220
221 let prefix = match label_dict.get_string(b"P") {
222 Some(p) => String::from_utf8_lossy(p).into_owned(),
223 None => String::new(),
224 };
225
226 let logical_start = label_dict.get_i64(b"St").unwrap_or(1);
227
228 ranges.push(PageLabelRange {
229 start_page: *page_index as usize,
230 style,
231 prefix,
232 logical_start,
233 });
234 }
235
236 Ok(ranges)
237}
238
239pub fn label_for_page(ranges: &[PageLabelRange], page_index: usize) -> String {
249 if ranges.is_empty() {
250 return (page_index + 1).to_string();
251 }
252
253 let range = match ranges
255 .iter()
256 .rev()
257 .find(|r| r.start_page <= page_index)
258 {
259 Some(r) => r,
260 None => return (page_index + 1).to_string(),
261 };
262
263 let offset = (page_index - range.start_page) as i64;
264 let value = range.logical_start + offset;
265
266 let numeric_part = match range.style {
267 PageLabelStyle::Decimal => value.to_string(),
268 PageLabelStyle::UpperRoman => to_roman(value, true),
269 PageLabelStyle::LowerRoman => to_roman(value, false),
270 PageLabelStyle::UpperAlpha => to_alpha(value, true),
271 PageLabelStyle::LowerAlpha => to_alpha(value, false),
272 PageLabelStyle::None => String::new(),
273 };
274
275 format!("{}{}", range.prefix, numeric_part)
276}
277
278pub fn to_roman(value: i64, uppercase: bool) -> String {
286 if value <= 0 {
287 return String::new();
288 }
289
290 const TABLE: &[(i64, &str)] = &[
291 (1000, "M"),
292 (900, "CM"),
293 (500, "D"),
294 (400, "CD"),
295 (100, "C"),
296 (90, "XC"),
297 (50, "L"),
298 (40, "XL"),
299 (10, "X"),
300 (9, "IX"),
301 (5, "V"),
302 (4, "IV"),
303 (1, "I"),
304 ];
305
306 let mut result = String::new();
307 let mut remaining = value;
308
309 for &(threshold, symbol) in TABLE {
310 while remaining >= threshold {
311 result.push_str(symbol);
312 remaining -= threshold;
313 }
314 }
315
316 if uppercase {
317 result
318 } else {
319 result.to_lowercase()
320 }
321}
322
323pub fn to_alpha(value: i64, uppercase: bool) -> String {
333 if value <= 0 {
334 return String::new();
335 }
336
337 let mut result = Vec::new();
338 let mut remaining = value - 1; loop {
341 let ch = (remaining % 26) as u8;
342 let base = if uppercase { b'A' } else { b'a' };
343 result.push(base + ch);
344 remaining = remaining / 26 - 1;
345 if remaining < 0 {
346 break;
347 }
348 }
349
350 result.reverse();
351 String::from_utf8(result).unwrap_or_default()
352}
353
354pub fn set_page_labels(
361 modifier: &mut DocumentModifier,
362 ranges: &[PageLabelRange],
363) -> Result<()> {
364 let mut entries: Vec<(i64, PdfObject)> = Vec::with_capacity(ranges.len());
366
367 for range in ranges {
368 let mut label_dict = PdfDict::new();
369
370 if let Some(name) = range.style.to_name() {
371 label_dict.insert(b"S".to_vec(), PdfObject::Name(name.to_vec()));
372 }
373
374 if !range.prefix.is_empty() {
375 label_dict.insert(
376 b"P".to_vec(),
377 PdfObject::String(range.prefix.as_bytes().to_vec()),
378 );
379 }
380
381 if range.logical_start != 1 {
382 label_dict.insert(
383 b"St".to_vec(),
384 PdfObject::Integer(range.logical_start),
385 );
386 }
387
388 entries.push((range.start_page as i64, PdfObject::Dict(label_dict)));
389 }
390
391 entries.sort_by_key(|(k, _)| *k);
393
394 let nums_array = build_nums_array(&entries);
396 let mut tree_dict = PdfDict::new();
397 tree_dict.insert(b"Nums".to_vec(), PdfObject::Array(nums_array));
398
399 let tree_ref = modifier.add_object(PdfObject::Dict(tree_dict));
401
402 let catalog_ref = modifier.catalog_ref().clone();
404 let catalog_obj = modifier
405 .find_object_pub(catalog_ref.obj_num)
406 .cloned()
407 .ok_or_else(|| JustPdfError::FormError {
408 detail: "catalog object not found".into(),
409 })?;
410
411 match catalog_obj {
412 PdfObject::Dict(mut cat) => {
413 cat.insert(
414 b"PageLabels".to_vec(),
415 PdfObject::Reference(tree_ref),
416 );
417 modifier.set_object(catalog_ref.obj_num, PdfObject::Dict(cat));
418 }
419 _ => {
420 return Err(JustPdfError::FormError {
421 detail: "catalog is not a dictionary".into(),
422 });
423 }
424 }
425
426 Ok(())
427}
428
429#[cfg(test)]
434mod tests {
435 use super::*;
436 use crate::object::{PdfDict, PdfObject};
437 use crate::parser::PdfDocument;
438 use crate::writer::document::DocumentBuilder;
439 use crate::writer::modify::DocumentModifier;
440 use crate::writer::page::PageBuilder;
441
442 fn make_test_pdf(num_pages: usize) -> Vec<u8> {
444 let mut doc = DocumentBuilder::new();
445 let font = doc.add_standard_font("Helvetica");
446 for i in 0..num_pages {
447 let mut page = PageBuilder::new(612.0, 792.0);
448 page.add_font(&font, "Helvetica");
449 page.begin_text();
450 page.set_font(&font, 12.0);
451 page.move_to(72.0, 720.0);
452 page.show_text(&format!("Page {}", i + 1));
453 page.end_text();
454 doc.add_page(page);
455 }
456 doc.build().unwrap()
457 }
458
459 #[test]
462 fn test_to_roman_basic() {
463 assert_eq!(to_roman(1, true), "I");
464 assert_eq!(to_roman(4, true), "IV");
465 assert_eq!(to_roman(9, true), "IX");
466 assert_eq!(to_roman(14, true), "XIV");
467 assert_eq!(to_roman(42, true), "XLII");
468 assert_eq!(to_roman(99, true), "XCIX");
469 assert_eq!(to_roman(399, true), "CCCXCIX");
470 assert_eq!(to_roman(1994, true), "MCMXCIV");
471 assert_eq!(to_roman(3999, true), "MMMCMXCIX");
472 }
473
474 #[test]
475 fn test_to_roman_lowercase() {
476 assert_eq!(to_roman(3, false), "iii");
477 assert_eq!(to_roman(14, false), "xiv");
478 }
479
480 #[test]
481 fn test_to_roman_edge() {
482 assert_eq!(to_roman(0, true), "");
483 assert_eq!(to_roman(-5, true), "");
484 }
485
486 #[test]
489 fn test_to_alpha_basic() {
490 assert_eq!(to_alpha(1, true), "A");
491 assert_eq!(to_alpha(2, true), "B");
492 assert_eq!(to_alpha(26, true), "Z");
493 }
494
495 #[test]
496 fn test_to_alpha_multi_letter() {
497 assert_eq!(to_alpha(27, true), "AA");
498 assert_eq!(to_alpha(28, true), "AB");
499 assert_eq!(to_alpha(52, true), "AZ");
500 assert_eq!(to_alpha(53, true), "BA");
501 }
502
503 #[test]
504 fn test_to_alpha_lowercase() {
505 assert_eq!(to_alpha(1, false), "a");
506 assert_eq!(to_alpha(27, false), "aa");
507 }
508
509 #[test]
510 fn test_to_alpha_edge() {
511 assert_eq!(to_alpha(0, true), "");
512 assert_eq!(to_alpha(-1, true), "");
513 }
514
515 #[test]
518 fn test_label_for_page_empty_ranges() {
519 assert_eq!(label_for_page(&[], 0), "1");
521 assert_eq!(label_for_page(&[], 4), "5");
522 }
523
524 #[test]
525 fn test_label_for_page_decimal() {
526 let ranges = vec![PageLabelRange::new(0, PageLabelStyle::Decimal)];
527 assert_eq!(label_for_page(&ranges, 0), "1");
528 assert_eq!(label_for_page(&ranges, 9), "10");
529 }
530
531 #[test]
532 fn test_label_for_page_roman_then_decimal() {
533 let ranges = vec![
534 PageLabelRange::new(0, PageLabelStyle::LowerRoman),
535 PageLabelRange::new(4, PageLabelStyle::Decimal),
536 ];
537
538 assert_eq!(label_for_page(&ranges, 0), "i");
540 assert_eq!(label_for_page(&ranges, 1), "ii");
541 assert_eq!(label_for_page(&ranges, 2), "iii");
542 assert_eq!(label_for_page(&ranges, 3), "iv");
543
544 assert_eq!(label_for_page(&ranges, 4), "1");
546 assert_eq!(label_for_page(&ranges, 5), "2");
547 }
548
549 #[test]
550 fn test_label_for_page_with_prefix() {
551 let ranges = vec![
552 PageLabelRange::new(0, PageLabelStyle::Decimal).with_prefix("A-"),
553 ];
554 assert_eq!(label_for_page(&ranges, 0), "A-1");
555 assert_eq!(label_for_page(&ranges, 2), "A-3");
556 }
557
558 #[test]
559 fn test_label_for_page_with_logical_start() {
560 let ranges = vec![
561 PageLabelRange::new(0, PageLabelStyle::Decimal).with_logical_start(5),
562 ];
563 assert_eq!(label_for_page(&ranges, 0), "5");
564 assert_eq!(label_for_page(&ranges, 3), "8");
565 }
566
567 #[test]
568 fn test_label_for_page_none_style() {
569 let ranges = vec![
570 PageLabelRange::new(0, PageLabelStyle::None).with_prefix("Cover"),
571 ];
572 assert_eq!(label_for_page(&ranges, 0), "Cover");
573 }
574
575 #[test]
576 fn test_label_for_page_alpha() {
577 let ranges = vec![PageLabelRange::new(0, PageLabelStyle::UpperAlpha)];
578 assert_eq!(label_for_page(&ranges, 0), "A");
579 assert_eq!(label_for_page(&ranges, 25), "Z");
580 assert_eq!(label_for_page(&ranges, 26), "AA");
581 }
582
583 #[test]
584 fn test_label_for_page_single_page() {
585 let ranges = vec![
586 PageLabelRange::new(0, PageLabelStyle::None).with_prefix("Title"),
587 ];
588 assert_eq!(label_for_page(&ranges, 0), "Title");
589 }
590
591 #[test]
594 fn test_parse_page_labels_manual_structure() {
595 let bytes = make_test_pdf(5);
597 let mut doc = PdfDocument::from_bytes(bytes).unwrap();
598
599 let catalog_ref = doc.catalog_ref().unwrap().clone();
601 let catalog = doc.resolve(&catalog_ref).unwrap();
602 let mut catalog_dict = catalog.as_dict().unwrap().clone();
603
604 let mut label0 = PdfDict::new();
608 label0.insert(b"S".to_vec(), PdfObject::Name(b"r".to_vec()));
609
610 let mut label3 = PdfDict::new();
611 label3.insert(b"S".to_vec(), PdfObject::Name(b"D".to_vec()));
612 label3.insert(
613 b"P".to_vec(),
614 PdfObject::String(b"Ch-".to_vec()),
615 );
616
617 let mut tree = PdfDict::new();
618 tree.insert(
619 b"Nums".to_vec(),
620 PdfObject::Array(vec![
621 PdfObject::Integer(0),
622 PdfObject::Dict(label0),
623 PdfObject::Integer(3),
624 PdfObject::Dict(label3),
625 ]),
626 );
627
628 catalog_dict.insert(b"PageLabels".to_vec(), PdfObject::Dict(tree));
629
630 let ranges = vec![
634 PageLabelRange::new(0, PageLabelStyle::LowerRoman),
635 PageLabelRange::new(3, PageLabelStyle::Decimal).with_prefix("Ch-"),
636 ];
637
638 assert_eq!(label_for_page(&ranges, 0), "i");
639 assert_eq!(label_for_page(&ranges, 1), "ii");
640 assert_eq!(label_for_page(&ranges, 2), "iii");
641 assert_eq!(label_for_page(&ranges, 3), "Ch-1");
642 assert_eq!(label_for_page(&ranges, 4), "Ch-2");
643 }
644
645 #[test]
648 fn test_roundtrip_page_labels() {
649 let bytes = make_test_pdf(6);
650 let mut doc = PdfDocument::from_bytes(bytes).unwrap();
651 let mut modifier = DocumentModifier::from_document(&doc).unwrap();
652
653 let ranges = vec![
654 PageLabelRange::new(0, PageLabelStyle::LowerRoman),
655 PageLabelRange::new(2, PageLabelStyle::Decimal)
656 .with_prefix("P-")
657 .with_logical_start(1),
658 PageLabelRange::new(5, PageLabelStyle::UpperAlpha),
659 ];
660
661 set_page_labels(&mut modifier, &ranges).unwrap();
662
663 let new_bytes = modifier.build().unwrap();
665 let mut reparsed = PdfDocument::from_bytes(new_bytes).unwrap();
666
667 let parsed_ranges = read_page_labels(&reparsed).unwrap();
668 assert_eq!(parsed_ranges.len(), 3);
669
670 assert_eq!(parsed_ranges[0].start_page, 0);
671 assert_eq!(parsed_ranges[0].style, PageLabelStyle::LowerRoman);
672 assert_eq!(parsed_ranges[0].prefix, "");
673 assert_eq!(parsed_ranges[0].logical_start, 1);
674
675 assert_eq!(parsed_ranges[1].start_page, 2);
676 assert_eq!(parsed_ranges[1].style, PageLabelStyle::Decimal);
677 assert_eq!(parsed_ranges[1].prefix, "P-");
678 assert_eq!(parsed_ranges[1].logical_start, 1);
679
680 assert_eq!(parsed_ranges[2].start_page, 5);
681 assert_eq!(parsed_ranges[2].style, PageLabelStyle::UpperAlpha);
682
683 assert_eq!(label_for_page(&parsed_ranges, 0), "i");
685 assert_eq!(label_for_page(&parsed_ranges, 1), "ii");
686 assert_eq!(label_for_page(&parsed_ranges, 2), "P-1");
687 assert_eq!(label_for_page(&parsed_ranges, 4), "P-3");
688 assert_eq!(label_for_page(&parsed_ranges, 5), "A");
689 }
690
691 #[test]
692 fn test_roundtrip_with_logical_start() {
693 let bytes = make_test_pdf(4);
694 let mut doc = PdfDocument::from_bytes(bytes).unwrap();
695 let mut modifier = DocumentModifier::from_document(&doc).unwrap();
696
697 let ranges = vec![
698 PageLabelRange::new(0, PageLabelStyle::Decimal).with_logical_start(10),
699 ];
700
701 set_page_labels(&mut modifier, &ranges).unwrap();
702
703 let new_bytes = modifier.build().unwrap();
704 let mut reparsed = PdfDocument::from_bytes(new_bytes).unwrap();
705
706 let parsed_ranges = read_page_labels(&reparsed).unwrap();
707 assert_eq!(parsed_ranges.len(), 1);
708 assert_eq!(parsed_ranges[0].logical_start, 10);
709
710 assert_eq!(label_for_page(&parsed_ranges, 0), "10");
711 assert_eq!(label_for_page(&parsed_ranges, 3), "13");
712 }
713
714 #[test]
715 fn test_roundtrip_none_style_with_prefix() {
716 let bytes = make_test_pdf(2);
717 let mut doc = PdfDocument::from_bytes(bytes).unwrap();
718 let mut modifier = DocumentModifier::from_document(&doc).unwrap();
719
720 let ranges = vec![
721 PageLabelRange::new(0, PageLabelStyle::None).with_prefix("Cover"),
722 PageLabelRange::new(1, PageLabelStyle::Decimal),
723 ];
724
725 set_page_labels(&mut modifier, &ranges).unwrap();
726
727 let new_bytes = modifier.build().unwrap();
728 let mut reparsed = PdfDocument::from_bytes(new_bytes).unwrap();
729
730 let parsed_ranges = read_page_labels(&reparsed).unwrap();
731 assert_eq!(parsed_ranges.len(), 2);
732 assert_eq!(label_for_page(&parsed_ranges, 0), "Cover");
733 assert_eq!(label_for_page(&parsed_ranges, 1), "1");
734 }
735
736 #[test]
737 fn test_read_page_labels_no_labels() {
738 let bytes = make_test_pdf(1);
739 let mut doc = PdfDocument::from_bytes(bytes).unwrap();
740 let ranges = read_page_labels(&doc).unwrap();
741 assert!(ranges.is_empty());
742 }
743
744 #[test]
747 fn test_build_nums_array() {
748 let entries = vec![
749 (0i64, PdfObject::Dict(PdfDict::new())),
750 (5, PdfObject::Dict(PdfDict::new())),
751 ];
752 let arr = build_nums_array(&entries);
753 assert_eq!(arr.len(), 4);
754 assert_eq!(arr[0], PdfObject::Integer(0));
755 assert!(arr[1].is_dict());
756 assert_eq!(arr[2], PdfObject::Integer(5));
757 assert!(arr[3].is_dict());
758 }
759
760 #[test]
763 fn test_style_roundtrip() {
764 let styles = [
765 PageLabelStyle::Decimal,
766 PageLabelStyle::UpperRoman,
767 PageLabelStyle::LowerRoman,
768 PageLabelStyle::UpperAlpha,
769 PageLabelStyle::LowerAlpha,
770 ];
771
772 for style in &styles {
773 let name = style.to_name().unwrap();
774 let decoded = PageLabelStyle::from_name(name).unwrap();
775 assert_eq!(*style, decoded);
776 }
777
778 assert!(PageLabelStyle::None.to_name().is_none());
780 }
781}