1use crate::object::{decode_text_string, Dict};
5use crate::source::AsyncObjectSource;
6use crate::tree::{self, resolved_dict};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum LabelStyle {
13 Decimal,
15 RomanUpper,
17 RomanLower,
19 LettersUpper,
21 LettersLower,
23}
24
25impl LabelStyle {
26 pub fn code(self) -> &'static str {
28 match self {
29 LabelStyle::Decimal => "D",
30 LabelStyle::RomanUpper => "R",
31 LabelStyle::RomanLower => "r",
32 LabelStyle::LettersUpper => "A",
33 LabelStyle::LettersLower => "a",
34 }
35 }
36
37 pub fn from_code(code: &str) -> Option<LabelStyle> {
39 Some(match code {
40 "D" => LabelStyle::Decimal,
41 "R" => LabelStyle::RomanUpper,
42 "r" => LabelStyle::RomanLower,
43 "A" => LabelStyle::LettersUpper,
44 "a" => LabelStyle::LettersLower,
45 _ => return None,
46 })
47 }
48
49 pub fn numeral(self, n: u32) -> String {
56 match self {
57 LabelStyle::Decimal => n.to_string(),
58 LabelStyle::RomanUpper => roman(n),
59 LabelStyle::RomanLower => roman(n).to_lowercase(),
60 LabelStyle::LettersUpper => letters(n, b'A'),
61 LabelStyle::LettersLower => letters(n, b'a'),
62 }
63 }
64}
65
66fn roman(mut n: u32) -> String {
69 const STEPS: [(u32, &str); 13] = [
70 (1000, "M"),
71 (900, "CM"),
72 (500, "D"),
73 (400, "CD"),
74 (100, "C"),
75 (90, "XC"),
76 (50, "L"),
77 (40, "XL"),
78 (10, "X"),
79 (9, "IX"),
80 (5, "V"),
81 (4, "IV"),
82 (1, "I"),
83 ];
84 let mut out = String::new();
85 for (value, numeral) in STEPS {
86 while n >= value {
87 out.push_str(numeral);
88 n -= value;
89 }
90 }
91 out
92}
93
94fn letters(n: u32, first: u8) -> String {
97 if n == 0 {
98 return String::new();
99 }
100 let letter = char::from(first + ((n - 1) % 26) as u8);
101 let count = (n - 1) / 26 + 1;
102 std::iter::repeat_n(letter, count as usize).collect()
103}
104
105#[derive(Debug, Clone, PartialEq)]
112pub struct PageLabel {
113 pub first_page: usize,
115 pub style: Option<LabelStyle>,
117 pub prefix: Option<String>,
119 pub start_at: u32,
121}
122
123impl PageLabel {
124 pub fn label(&self, index: usize) -> String {
128 let mut label = self.prefix.clone().unwrap_or_default();
129 if let Some(style) = self.style {
130 let offset = u32::try_from(index.saturating_sub(self.first_page)).unwrap_or(u32::MAX);
131 label.push_str(&style.numeral(self.start_at.saturating_add(offset)));
132 }
133 label
134 }
135}
136
137pub async fn page_labels_with<S: AsyncObjectSource>(
144 src: &S,
145 trailer: &Dict,
146) -> Option<Vec<PageLabel>> {
147 let catalog = resolved_dict(src, trailer.get("Root")?).await?;
148 let root = resolved_dict(src, catalog.get("PageLabels")?).await?;
149 let mut ranges = Vec::new();
150 for (first_page, value) in tree::entries::<i64, S>(src, &root).await {
151 let Ok(first_page) = usize::try_from(first_page) else {
152 continue;
153 };
154 let Some(dict) = resolved_dict(src, &value).await else {
155 continue;
156 };
157 ranges.push(PageLabel {
158 first_page,
159 style: match dict.get("S") {
160 Some(s) => src
161 .resolve(s)
162 .await
163 .ok()
164 .and_then(|s| LabelStyle::from_code(&s.as_name()?.0)),
165 None => None,
166 },
167 prefix: match dict.get("P") {
168 Some(p) => src
169 .resolve(p)
170 .await
171 .ok()
172 .and_then(|p| p.as_str_bytes().map(decode_text_string)),
173 None => None,
174 },
175 start_at: match dict.get("St") {
176 Some(st) => src
177 .resolve(st)
178 .await
179 .ok()
180 .and_then(|st| st.as_int())
181 .and_then(|st| u32::try_from(st).ok())
182 .filter(|st| *st >= 1)
183 .unwrap_or(1),
184 None => 1,
185 },
186 });
187 }
188 ranges.sort_by_key(|range| range.first_page);
189 Some(ranges)
190}
191
192pub fn page_label(ranges: &[PageLabel], index: usize) -> String {
198 match ranges.iter().rev().find(|range| range.first_page <= index) {
199 Some(range) => range.label(index),
200 None => (index + 1).to_string(),
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 use crate::Document;
208 use pdfboss_testkit::PdfBuilder;
209
210 #[test]
212 fn styles_number_pages_as_table_159_says() {
213 assert_eq!(LabelStyle::Decimal.numeral(1), "1");
214 assert_eq!(LabelStyle::Decimal.numeral(1040), "1040");
215 for (n, roman) in [
216 (1, "i"),
217 (4, "iv"),
218 (9, "ix"),
219 (14, "xiv"),
220 (40, "xl"),
221 (90, "xc"),
222 (400, "cd"),
223 (1994, "mcmxciv"),
224 (3999, "mmmcmxcix"),
225 (4000, "mmmm"),
226 ] {
227 assert_eq!(LabelStyle::RomanLower.numeral(n), roman, "{n}");
228 assert_eq!(
229 LabelStyle::RomanUpper.numeral(n),
230 roman.to_uppercase(),
231 "{n}"
232 );
233 }
234 for (n, letters) in [(1, "a"), (26, "z"), (27, "aa"), (52, "zz"), (53, "aaa")] {
235 assert_eq!(LabelStyle::LettersLower.numeral(n), letters, "{n}");
236 assert_eq!(
237 LabelStyle::LettersUpper.numeral(n),
238 letters.to_uppercase(),
239 "{n}"
240 );
241 }
242 assert_eq!(LabelStyle::RomanLower.numeral(0), "");
243 assert_eq!(LabelStyle::LettersUpper.numeral(0), "");
244 assert_eq!(LabelStyle::Decimal.numeral(0), "0");
245 for style in [
246 LabelStyle::Decimal,
247 LabelStyle::RomanUpper,
248 LabelStyle::RomanLower,
249 LabelStyle::LettersUpper,
250 LabelStyle::LettersLower,
251 ] {
252 assert_eq!(LabelStyle::from_code(style.code()), Some(style));
253 }
254 assert_eq!(LabelStyle::from_code("X"), None);
255 }
256
257 fn doc(pages: usize, catalog_extra: &str) -> Document {
259 let mut b = PdfBuilder::new();
260 b.object(
261 1,
262 &format!("<< /Type /Catalog /Pages 2 0 R {catalog_extra} >>"),
263 );
264 let kids: Vec<String> = (0..pages).map(|i| format!("{} 0 R", 10 + i)).collect();
265 b.object(
266 2,
267 &format!(
268 "<< /Type /Pages /Kids [{}] /Count {pages} >>",
269 kids.join(" ")
270 ),
271 );
272 for i in 0..pages {
273 b.object(
274 10 + i as u32,
275 "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>",
276 );
277 }
278 Document::load(b.build(1)).expect("load")
279 }
280
281 fn labels(doc: &Document) -> Vec<String> {
282 (0..doc.page_count())
283 .map(|i| doc.page_label(i).unwrap())
284 .collect()
285 }
286
287 #[test]
289 fn the_clauses_example_labels_its_pages() {
290 let doc = doc(
293 9,
294 "/PageLabels << /Nums [0 << /S /r >> 4 << /S /D >> 7 << /S /D /P (A-) /St 8 >>] >>",
295 );
296 assert_eq!(
297 labels(&doc),
298 ["i", "ii", "iii", "iv", "1", "2", "3", "A-8", "A-9"]
299 );
300 let ranges = doc.page_labels().unwrap();
301 assert_eq!(ranges.len(), 3);
302 assert_eq!(
303 ranges[2],
304 PageLabel {
305 first_page: 7,
306 style: Some(LabelStyle::Decimal),
307 prefix: Some("A-".to_string()),
308 start_at: 8,
309 }
310 );
311 assert_eq!(doc.page_label(9), None, "past the last page");
312 }
313
314 #[test]
316 fn ranges_without_a_style_or_a_page_zero_still_label() {
317 let mut b = PdfBuilder::new();
322 b.object(1, "<< /Type /Catalog /Pages 2 0 R /PageLabels 30 0 R >>");
323 b.object(
324 2,
325 "<< /Type /Pages /Kids [10 0 R 11 0 R 12 0 R 13 0 R 14 0 R] /Count 5 >>",
326 );
327 for i in 10..15 {
328 b.object(i, "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] >>");
329 }
330 b.object(
331 30,
332 "<< /Nums [1 << /P (Cover) >> 2 << /S /D /St 0 >> 3 << /S /Q /P <FEFF00A7> >> 4 (junk)] >>",
333 );
334 let doc = Document::load(b.build(1)).unwrap();
335 assert_eq!(labels(&doc), ["1", "Cover", "1", "§", "§"]);
336 let plain = self::doc(2, "");
338 assert_eq!(plain.page_labels(), None);
339 assert_eq!(plain.page_label(0), None);
340 }
341}