1use crate::objects::{Dictionary, Object};
4
5#[derive(Debug, Clone, Copy, PartialEq)]
7pub enum PageLabelStyle {
8 DecimalArabic,
10 UppercaseRoman,
12 LowercaseRoman,
14 UppercaseLetters,
16 LowercaseLetters,
18 None,
20}
21
22impl PageLabelStyle {
23 pub fn to_pdf_name(&self) -> Option<&'static str> {
37 match self {
38 PageLabelStyle::DecimalArabic => Some("D"),
39 PageLabelStyle::UppercaseRoman => Some("R"),
40 PageLabelStyle::LowercaseRoman => Some("r"),
41 PageLabelStyle::UppercaseLetters => Some("A"),
42 PageLabelStyle::LowercaseLetters => Some("a"),
43 PageLabelStyle::None => None,
44 }
45 }
46
47 pub fn format(&self, number: u32) -> String {
49 match self {
50 PageLabelStyle::DecimalArabic => number.to_string(),
51 PageLabelStyle::UppercaseRoman => to_roman(number).to_uppercase(),
52 PageLabelStyle::LowercaseRoman => to_roman(number),
53 PageLabelStyle::UppercaseLetters => to_letters(number, true),
54 PageLabelStyle::LowercaseLetters => to_letters(number, false),
55 PageLabelStyle::None => String::new(),
56 }
57 }
58}
59
60#[derive(Debug, Clone)]
62pub struct PageLabel {
63 pub style: PageLabelStyle,
65 pub prefix: Option<String>,
67 pub start: u32,
69}
70
71impl PageLabel {
72 pub fn new(style: PageLabelStyle) -> Self {
74 Self {
75 style,
76 prefix: None,
77 start: 1,
78 }
79 }
80
81 pub fn decimal() -> Self {
83 Self::new(PageLabelStyle::DecimalArabic)
84 }
85
86 pub fn roman_uppercase() -> Self {
88 Self::new(PageLabelStyle::UppercaseRoman)
89 }
90
91 pub fn roman_lowercase() -> Self {
93 Self::new(PageLabelStyle::LowercaseRoman)
94 }
95
96 pub fn letters_uppercase() -> Self {
98 Self::new(PageLabelStyle::UppercaseLetters)
99 }
100
101 pub fn letters_lowercase() -> Self {
103 Self::new(PageLabelStyle::LowercaseLetters)
104 }
105
106 pub fn prefix_only(prefix: impl Into<String>) -> Self {
108 Self {
109 style: PageLabelStyle::None,
110 prefix: Some(prefix.into()),
111 start: 1,
112 }
113 }
114
115 pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
117 self.prefix = Some(prefix.into());
118 self
119 }
120
121 pub fn starting_at(mut self, start: u32) -> Self {
123 self.start = start;
124 self
125 }
126
127 pub fn format_label(&self, offset: u32) -> String {
129 let mut label = String::new();
130
131 if let Some(prefix) = &self.prefix {
132 label.push_str(prefix);
133 }
134
135 if self.style != PageLabelStyle::None {
136 let number = self.start + offset;
137 label.push_str(&self.style.format(number));
138 }
139
140 label
141 }
142
143 pub fn to_dict(&self) -> Dictionary {
152 let mut dict = Dictionary::new();
153
154 dict.set("Type", Object::Name("PageLabel".to_string()));
155
156 if let Some(style_name) = self.style.to_pdf_name() {
157 dict.set("S", Object::Name(style_name.to_string()));
158 }
159
160 if let Some(prefix) = &self.prefix {
161 dict.set("P", Object::String(prefix.clone()));
162 }
163
164 if self.start != 1 {
165 dict.set("St", Object::Integer(self.start as i64));
166 }
167
168 dict
169 }
170}
171
172#[derive(Debug, Clone)]
174pub struct PageLabelRange {
175 pub start_page: u32,
177 pub label: PageLabel,
179}
180
181impl PageLabelRange {
182 pub fn new(start_page: u32, label: PageLabel) -> Self {
184 Self { start_page, label }
185 }
186}
187
188fn to_roman(mut num: u32) -> String {
190 if num == 0 {
191 return String::new();
192 }
193
194 let values = [
195 (1000, "m"),
196 (900, "cm"),
197 (500, "d"),
198 (400, "cd"),
199 (100, "c"),
200 (90, "xc"),
201 (50, "l"),
202 (40, "xl"),
203 (10, "x"),
204 (9, "ix"),
205 (5, "v"),
206 (4, "iv"),
207 (1, "i"),
208 ];
209
210 let mut result = String::new();
211
212 for (value, numeral) in values.iter() {
213 while num >= *value {
214 result.push_str(numeral);
215 num -= value;
216 }
217 }
218
219 result
220}
221
222fn to_letters(num: u32, uppercase: bool) -> String {
224 if num == 0 {
225 return String::new();
226 }
227
228 let mut result = String::new();
229 let mut n = num;
230
231 while n > 0 {
232 let remainder = ((n - 1) % 26) as u8;
233 let letter = if uppercase {
234 (b'A' + remainder) as char
235 } else {
236 (b'a' + remainder) as char
237 };
238 result.insert(0, letter);
239 n = (n - 1) / 26;
240 }
241
242 result
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248
249 #[test]
250 fn test_page_label_styles() {
251 assert_eq!(PageLabelStyle::DecimalArabic.format(1), "1");
252 assert_eq!(PageLabelStyle::DecimalArabic.format(42), "42");
253
254 assert_eq!(PageLabelStyle::UppercaseRoman.format(1), "I");
255 assert_eq!(PageLabelStyle::UppercaseRoman.format(4), "IV");
256 assert_eq!(PageLabelStyle::UppercaseRoman.format(9), "IX");
257 assert_eq!(PageLabelStyle::UppercaseRoman.format(58), "LVIII");
258
259 assert_eq!(PageLabelStyle::LowercaseRoman.format(1), "i");
260 assert_eq!(PageLabelStyle::LowercaseRoman.format(4), "iv");
261
262 assert_eq!(PageLabelStyle::UppercaseLetters.format(1), "A");
263 assert_eq!(PageLabelStyle::UppercaseLetters.format(26), "Z");
264 assert_eq!(PageLabelStyle::UppercaseLetters.format(27), "AA");
265 assert_eq!(PageLabelStyle::UppercaseLetters.format(52), "AZ");
266
267 assert_eq!(PageLabelStyle::LowercaseLetters.format(1), "a");
268 assert_eq!(PageLabelStyle::LowercaseLetters.format(26), "z");
269 assert_eq!(PageLabelStyle::LowercaseLetters.format(27), "aa");
270
271 assert_eq!(PageLabelStyle::None.format(1), "");
272 assert_eq!(PageLabelStyle::None.format(100), "");
273 }
274
275 #[test]
276 fn test_page_label_creation() {
277 let label = PageLabel::decimal();
278 assert_eq!(label.style, PageLabelStyle::DecimalArabic);
279 assert_eq!(label.start, 1);
280 assert!(label.prefix.is_none());
281
282 let label = PageLabel::roman_lowercase()
283 .with_prefix("Page ")
284 .starting_at(5);
285 assert_eq!(label.style, PageLabelStyle::LowercaseRoman);
286 assert_eq!(label.start, 5);
287 assert_eq!(label.prefix, Some("Page ".to_string()));
288 }
289
290 #[test]
291 fn test_format_label() {
292 let label = PageLabel::decimal().with_prefix("Chapter ");
293 assert_eq!(label.format_label(0), "Chapter 1");
294 assert_eq!(label.format_label(1), "Chapter 2");
295
296 let label = PageLabel::roman_uppercase().starting_at(1);
297 assert_eq!(label.format_label(0), "I");
298 assert_eq!(label.format_label(3), "IV");
299
300 let label = PageLabel::prefix_only("Appendix");
301 assert_eq!(label.format_label(0), "Appendix");
302 assert_eq!(label.format_label(10), "Appendix");
303 }
304
305 #[test]
306 fn test_to_dict() {
307 let label = PageLabel::decimal();
310 let dict = label.to_dict();
311 assert_eq!(
312 dict.get("Type"),
313 Some(&Object::Name("PageLabel".to_string()))
314 );
315 assert_eq!(dict.get("S"), Some(&Object::Name("D".to_string())));
316 assert!(dict.get("P").is_none());
317 assert!(dict.get("St").is_none());
318
319 let label = PageLabel::roman_lowercase()
320 .with_prefix("p. ")
321 .starting_at(5);
322 let dict = label.to_dict();
323 assert_eq!(
324 dict.get("Type"),
325 Some(&Object::Name("PageLabel".to_string()))
326 );
327 assert_eq!(dict.get("S"), Some(&Object::Name("r".to_string())));
331 assert_eq!(dict.get("P"), Some(&Object::String("p. ".to_string())));
332 assert_eq!(dict.get("St"), Some(&Object::Integer(5)));
333 }
334
335 #[test]
336 fn test_roman_conversion() {
337 assert_eq!(to_roman(1), "i");
338 assert_eq!(to_roman(3), "iii");
339 assert_eq!(to_roman(4), "iv");
340 assert_eq!(to_roman(5), "v");
341 assert_eq!(to_roman(9), "ix");
342 assert_eq!(to_roman(10), "x");
343 assert_eq!(to_roman(40), "xl");
344 assert_eq!(to_roman(50), "l");
345 assert_eq!(to_roman(90), "xc");
346 assert_eq!(to_roman(100), "c");
347 assert_eq!(to_roman(400), "cd");
348 assert_eq!(to_roman(500), "d");
349 assert_eq!(to_roman(900), "cm");
350 assert_eq!(to_roman(1000), "m");
351 assert_eq!(to_roman(1984), "mcmlxxxiv");
352 assert_eq!(to_roman(3999), "mmmcmxcix");
353 }
354
355 #[test]
356 fn test_letter_conversion() {
357 assert_eq!(to_letters(1, true), "A");
358 assert_eq!(to_letters(26, true), "Z");
359 assert_eq!(to_letters(27, true), "AA");
360 assert_eq!(to_letters(52, true), "AZ");
361 assert_eq!(to_letters(53, true), "BA");
362 assert_eq!(to_letters(702, true), "ZZ");
363 assert_eq!(to_letters(703, true), "AAA");
364
365 assert_eq!(to_letters(1, false), "a");
366 assert_eq!(to_letters(26, false), "z");
367 assert_eq!(to_letters(27, false), "aa");
368 }
369
370 #[test]
371 fn test_page_label_style_to_pdf_name() {
372 assert_eq!(PageLabelStyle::DecimalArabic.to_pdf_name(), Some("D"));
374 assert_eq!(PageLabelStyle::UppercaseRoman.to_pdf_name(), Some("R"));
375 assert_eq!(PageLabelStyle::LowercaseRoman.to_pdf_name(), Some("r"));
376 assert_eq!(PageLabelStyle::UppercaseLetters.to_pdf_name(), Some("A"));
377 assert_eq!(PageLabelStyle::LowercaseLetters.to_pdf_name(), Some("a"));
378 assert_eq!(PageLabelStyle::None.to_pdf_name(), None);
379 }
380
381 #[test]
382 fn test_roman_format_and_pdf_name_agree_on_case() {
383 let cases: &[(PageLabelStyle, &str, &str)] = &[
394 (PageLabelStyle::UppercaseRoman, "I", "R"),
395 (PageLabelStyle::LowercaseRoman, "i", "r"),
396 (PageLabelStyle::UppercaseLetters, "A", "A"),
397 (PageLabelStyle::LowercaseLetters, "a", "a"),
398 ];
399 for (style, expected_format_1, expected_pdf_name) in cases {
400 let formatted = style.format(1);
401 let pdf_name = style.to_pdf_name().expect("style has /S name");
402 assert_eq!(&formatted, expected_format_1, "format(1) for {:?}", style);
403 assert_eq!(pdf_name, *expected_pdf_name, "/S name for {:?}", style);
404 let format_upper = formatted == formatted.to_uppercase();
405 let pdf_upper = pdf_name == pdf_name.to_uppercase();
406 assert_eq!(
407 format_upper, pdf_upper,
408 "style {:?}: format(1)={:?} (upper={}) disagrees with /S /{} (upper={})",
409 style, formatted, format_upper, pdf_name, pdf_upper
410 );
411 }
412 }
413
414 #[test]
415 fn test_page_label_with_all_styles() {
416 let decimal = PageLabel::decimal();
418 assert_eq!(decimal.style, PageLabelStyle::DecimalArabic);
419
420 let roman_upper = PageLabel::roman_uppercase();
421 assert_eq!(roman_upper.style, PageLabelStyle::UppercaseRoman);
422
423 let roman_lower = PageLabel::roman_lowercase();
424 assert_eq!(roman_lower.style, PageLabelStyle::LowercaseRoman);
425
426 let letters_upper = PageLabel::letters_uppercase();
427 assert_eq!(letters_upper.style, PageLabelStyle::UppercaseLetters);
428
429 let letters_lower = PageLabel::letters_lowercase();
430 assert_eq!(letters_lower.style, PageLabelStyle::LowercaseLetters);
431
432 let prefix_only = PageLabel::prefix_only("Prefix");
433 assert_eq!(prefix_only.style, PageLabelStyle::None);
434 assert_eq!(prefix_only.prefix, Some("Prefix".to_string()));
435 }
436
437 #[test]
438 fn test_page_label_chaining() {
439 let label = PageLabel::decimal().with_prefix("Page ").starting_at(10);
440
441 assert_eq!(label.style, PageLabelStyle::DecimalArabic);
442 assert_eq!(label.prefix, Some("Page ".to_string()));
443 assert_eq!(label.start, 10);
444
445 assert_eq!(label.format_label(0), "Page 10");
447 assert_eq!(label.format_label(5), "Page 15");
448 }
449
450 #[test]
451 fn test_format_label_edge_cases() {
452 let label = PageLabel::decimal().with_prefix("");
454 assert_eq!(label.format_label(0), "1");
455
456 let long_prefix = "This is a very long prefix that might appear in some documents: ";
458 let label = PageLabel::roman_uppercase().with_prefix(long_prefix);
459 assert_eq!(label.format_label(0), format!("{}I", long_prefix));
460
461 let label = PageLabel::decimal().starting_at(9999);
463 assert_eq!(label.format_label(0), "9999");
464 assert_eq!(label.format_label(1), "10000");
465 }
466
467 #[test]
468 fn test_roman_edge_cases() {
469 assert_eq!(to_roman(49), "xlix");
471 assert_eq!(to_roman(99), "xcix");
472 assert_eq!(to_roman(499), "cdxcix");
473 assert_eq!(to_roman(999), "cmxcix");
474 assert_eq!(to_roman(1444), "mcdxliv");
475 assert_eq!(to_roman(1994), "mcmxciv");
476 assert_eq!(to_roman(2023), "mmxxiii");
477
478 assert_eq!(PageLabelStyle::UppercaseRoman.format(49), "XLIX");
480 assert_eq!(PageLabelStyle::LowercaseRoman.format(49), "xlix");
481 }
482
483 #[test]
484 fn test_letter_edge_cases() {
485 assert_eq!(to_letters(78, true), "BZ"); assert_eq!(to_letters(104, true), "CZ"); assert_eq!(to_letters(701, true), "ZY"); assert_eq!(to_letters(728, true), "AAZ"); assert_eq!(to_letters(1378, true), "AZZ"); assert_eq!(to_letters(78, false), "bz");
494 assert_eq!(to_letters(104, false), "cz");
495 assert_eq!(to_letters(701, false), "zy");
496 }
497
498 #[test]
499 fn test_prefix_only_variations() {
500 let label1 = PageLabel::prefix_only("Cover");
502 assert_eq!(label1.format_label(0), "Cover");
503 assert_eq!(label1.format_label(100), "Cover"); let label2 = PageLabel::prefix_only("Appendix A");
506 assert_eq!(label2.format_label(0), "Appendix A");
507
508 let label3 = PageLabel::prefix_only("附录");
510 assert_eq!(label3.format_label(0), "附录");
511
512 let label4 = PageLabel::prefix_only("§1");
514 assert_eq!(label4.format_label(0), "§1");
515 }
516
517 #[test]
518 fn test_to_dict_comprehensive() {
519 let label1 = PageLabel::new(PageLabelStyle::DecimalArabic);
521 let dict1 = label1.to_dict();
522 assert_eq!(
523 dict1.get("Type"),
524 Some(&Object::Name("PageLabel".to_string()))
525 );
526 assert_eq!(dict1.get("S"), Some(&Object::Name("D".to_string())));
527 assert!(dict1.get("P").is_none()); assert!(dict1.get("St").is_none()); let label2 = PageLabel::new(PageLabelStyle::UppercaseLetters)
531 .with_prefix("Section ")
532 .starting_at(10);
533 let dict2 = label2.to_dict();
534 assert_eq!(
535 dict2.get("Type"),
536 Some(&Object::Name("PageLabel".to_string()))
537 );
538 assert_eq!(dict2.get("S"), Some(&Object::Name("A".to_string())));
539 assert_eq!(
540 dict2.get("P"),
541 Some(&Object::String("Section ".to_string()))
542 );
543 assert_eq!(dict2.get("St"), Some(&Object::Integer(10)));
544
545 let label3 = PageLabel::prefix_only("Index");
548 let dict3 = label3.to_dict();
549 assert_eq!(
550 dict3.get("Type"),
551 Some(&Object::Name("PageLabel".to_string()))
552 );
553 assert!(dict3.get("S").is_none());
554 assert_eq!(dict3.get("P"), Some(&Object::String("Index".to_string())));
555 }
556
557 #[test]
558 fn test_sequential_page_labels() {
559 let front_matter = PageLabel::roman_lowercase().with_prefix("");
561 let main_content = PageLabel::decimal().starting_at(1);
562 let appendix = PageLabel::letters_uppercase().with_prefix("Appendix ");
563
564 assert_eq!(front_matter.format_label(0), "i");
566 assert_eq!(front_matter.format_label(1), "ii");
567 assert_eq!(front_matter.format_label(2), "iii");
568 assert_eq!(front_matter.format_label(3), "iv");
569
570 assert_eq!(main_content.format_label(0), "1");
572 assert_eq!(main_content.format_label(99), "100");
573
574 assert_eq!(appendix.format_label(0), "Appendix A");
576 assert_eq!(appendix.format_label(1), "Appendix B");
577 assert_eq!(appendix.format_label(25), "Appendix Z");
578 }
579
580 #[test]
581 fn test_large_number_formatting() {
582 let label = PageLabel::decimal().starting_at(999999);
584 assert_eq!(label.format_label(0), "999999");
585 assert_eq!(label.format_label(1), "1000000");
586
587 assert_eq!(to_roman(4000), "mmmm"); assert_eq!(to_roman(5000), "mmmmm");
590
591 assert_eq!(to_letters(18278, true).len() > 0, true); }
594
595 #[test]
596 fn test_special_prefix_combinations() {
597 let combinations = vec![
599 (PageLabel::decimal().with_prefix("№"), 0, "№1"),
600 (
601 PageLabel::roman_uppercase().with_prefix("Chapter "),
602 0,
603 "Chapter I",
604 ),
605 (
606 PageLabel::letters_lowercase()
607 .with_prefix("(")
608 .with_prefix(")"),
609 0,
610 ")a",
611 ),
612 (
613 PageLabel::decimal().with_prefix("Page ").starting_at(100),
614 0,
615 "Page 100",
616 ),
617 ];
618
619 for (label, offset, expected) in combinations {
620 assert_eq!(label.format_label(offset), expected);
621 }
622 }
623
624 #[test]
625 fn test_clone_and_equality() {
626 let label1 = PageLabel::decimal().with_prefix("Page ");
627 let label2 = label1.clone();
628
629 assert_eq!(label1.style, label2.style);
630 assert_eq!(label1.prefix, label2.prefix);
631 assert_eq!(label1.start, label2.start);
632 }
633}