use crate::object::{decode_text_string, Dict};
use crate::source::AsyncObjectSource;
use crate::tree::{self, resolved_dict};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LabelStyle {
Decimal,
RomanUpper,
RomanLower,
LettersUpper,
LettersLower,
}
impl LabelStyle {
pub fn code(self) -> &'static str {
match self {
LabelStyle::Decimal => "D",
LabelStyle::RomanUpper => "R",
LabelStyle::RomanLower => "r",
LabelStyle::LettersUpper => "A",
LabelStyle::LettersLower => "a",
}
}
pub fn from_code(code: &str) -> Option<LabelStyle> {
Some(match code {
"D" => LabelStyle::Decimal,
"R" => LabelStyle::RomanUpper,
"r" => LabelStyle::RomanLower,
"A" => LabelStyle::LettersUpper,
"a" => LabelStyle::LettersLower,
_ => return None,
})
}
pub fn numeral(self, n: u32) -> String {
match self {
LabelStyle::Decimal => n.to_string(),
LabelStyle::RomanUpper => roman(n),
LabelStyle::RomanLower => roman(n).to_lowercase(),
LabelStyle::LettersUpper => letters(n, b'A'),
LabelStyle::LettersLower => letters(n, b'a'),
}
}
}
fn roman(mut n: u32) -> String {
const STEPS: [(u32, &str); 13] = [
(1000, "M"),
(900, "CM"),
(500, "D"),
(400, "CD"),
(100, "C"),
(90, "XC"),
(50, "L"),
(40, "XL"),
(10, "X"),
(9, "IX"),
(5, "V"),
(4, "IV"),
(1, "I"),
];
let mut out = String::new();
for (value, numeral) in STEPS {
while n >= value {
out.push_str(numeral);
n -= value;
}
}
out
}
fn letters(n: u32, first: u8) -> String {
if n == 0 {
return String::new();
}
let letter = char::from(first + ((n - 1) % 26) as u8);
let count = (n - 1) / 26 + 1;
std::iter::repeat_n(letter, count as usize).collect()
}
#[derive(Debug, Clone, PartialEq)]
pub struct PageLabel {
pub first_page: usize,
pub style: Option<LabelStyle>,
pub prefix: Option<String>,
pub start_at: u32,
}
impl PageLabel {
pub fn label(&self, index: usize) -> String {
let mut label = self.prefix.clone().unwrap_or_default();
if let Some(style) = self.style {
let offset = u32::try_from(index.saturating_sub(self.first_page)).unwrap_or(u32::MAX);
label.push_str(&style.numeral(self.start_at.saturating_add(offset)));
}
label
}
}
pub async fn page_labels_with<S: AsyncObjectSource>(
src: &S,
trailer: &Dict,
) -> Option<Vec<PageLabel>> {
let catalog = resolved_dict(src, trailer.get("Root")?).await?;
let root = resolved_dict(src, catalog.get("PageLabels")?).await?;
let mut ranges = Vec::new();
for (first_page, value) in tree::entries::<i64, S>(src, &root).await {
let Ok(first_page) = usize::try_from(first_page) else {
continue;
};
let Some(dict) = resolved_dict(src, &value).await else {
continue;
};
ranges.push(PageLabel {
first_page,
style: match dict.get("S") {
Some(s) => src
.resolve(s)
.await
.ok()
.and_then(|s| LabelStyle::from_code(&s.as_name()?.0)),
None => None,
},
prefix: match dict.get("P") {
Some(p) => src
.resolve(p)
.await
.ok()
.and_then(|p| p.as_str_bytes().map(decode_text_string)),
None => None,
},
start_at: match dict.get("St") {
Some(st) => src
.resolve(st)
.await
.ok()
.and_then(|st| st.as_int())
.and_then(|st| u32::try_from(st).ok())
.filter(|st| *st >= 1)
.unwrap_or(1),
None => 1,
},
});
}
ranges.sort_by_key(|range| range.first_page);
Some(ranges)
}
pub fn page_label(ranges: &[PageLabel], index: usize) -> String {
match ranges.iter().rev().find(|range| range.first_page <= index) {
Some(range) => range.label(index),
None => (index + 1).to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Document;
use pdfboss_testkit::PdfBuilder;
#[test]
fn styles_number_pages_as_table_159_says() {
assert_eq!(LabelStyle::Decimal.numeral(1), "1");
assert_eq!(LabelStyle::Decimal.numeral(1040), "1040");
for (n, roman) in [
(1, "i"),
(4, "iv"),
(9, "ix"),
(14, "xiv"),
(40, "xl"),
(90, "xc"),
(400, "cd"),
(1994, "mcmxciv"),
(3999, "mmmcmxcix"),
(4000, "mmmm"),
] {
assert_eq!(LabelStyle::RomanLower.numeral(n), roman, "{n}");
assert_eq!(
LabelStyle::RomanUpper.numeral(n),
roman.to_uppercase(),
"{n}"
);
}
for (n, letters) in [(1, "a"), (26, "z"), (27, "aa"), (52, "zz"), (53, "aaa")] {
assert_eq!(LabelStyle::LettersLower.numeral(n), letters, "{n}");
assert_eq!(
LabelStyle::LettersUpper.numeral(n),
letters.to_uppercase(),
"{n}"
);
}
assert_eq!(LabelStyle::RomanLower.numeral(0), "");
assert_eq!(LabelStyle::LettersUpper.numeral(0), "");
assert_eq!(LabelStyle::Decimal.numeral(0), "0");
for style in [
LabelStyle::Decimal,
LabelStyle::RomanUpper,
LabelStyle::RomanLower,
LabelStyle::LettersUpper,
LabelStyle::LettersLower,
] {
assert_eq!(LabelStyle::from_code(style.code()), Some(style));
}
assert_eq!(LabelStyle::from_code("X"), None);
}
fn doc(pages: usize, catalog_extra: &str) -> Document {
let mut b = PdfBuilder::new();
b.object(
1,
&format!("<< /Type /Catalog /Pages 2 0 R {catalog_extra} >>"),
);
let kids: Vec<String> = (0..pages).map(|i| format!("{} 0 R", 10 + i)).collect();
b.object(
2,
&format!(
"<< /Type /Pages /Kids [{}] /Count {pages} >>",
kids.join(" ")
),
);
for i in 0..pages {
b.object(
10 + i as u32,
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>",
);
}
Document::load(b.build(1)).expect("load")
}
fn labels(doc: &Document) -> Vec<String> {
(0..doc.page_count())
.map(|i| doc.page_label(i).unwrap())
.collect()
}
#[test]
fn the_clauses_example_labels_its_pages() {
let doc = doc(
9,
"/PageLabels << /Nums [0 << /S /r >> 4 << /S /D >> 7 << /S /D /P (A-) /St 8 >>] >>",
);
assert_eq!(
labels(&doc),
["i", "ii", "iii", "iv", "1", "2", "3", "A-8", "A-9"]
);
let ranges = doc.page_labels().unwrap();
assert_eq!(ranges.len(), 3);
assert_eq!(
ranges[2],
PageLabel {
first_page: 7,
style: Some(LabelStyle::Decimal),
prefix: Some("A-".to_string()),
start_at: 8,
}
);
assert_eq!(doc.page_label(9), None, "past the last page");
}
#[test]
fn ranges_without_a_style_or_a_page_zero_still_label() {
let mut b = PdfBuilder::new();
b.object(1, "<< /Type /Catalog /Pages 2 0 R /PageLabels 30 0 R >>");
b.object(
2,
"<< /Type /Pages /Kids [10 0 R 11 0 R 12 0 R 13 0 R 14 0 R] /Count 5 >>",
);
for i in 10..15 {
b.object(i, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
}
b.object(
30,
"<< /Nums [1 << /P (Cover) >> 2 << /S /D /St 0 >> 3 << /S /Q /P <FEFF00A7> >> 4 (junk)] >>",
);
let doc = Document::load(b.build(1)).unwrap();
assert_eq!(labels(&doc), ["1", "Cover", "1", "§", "§"]);
let plain = self::doc(2, "");
assert_eq!(plain.page_labels(), None);
assert_eq!(plain.page_label(0), None);
}
}