Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pypdf/_page_labels.py: 12%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2Page labels are shown by PDF viewers as "the page number".
4A page has a numeric index, starting at 0. Additionally, the page
5has a label. In the most simple case:
7 label = index + 1
9However, the title page and the table of contents might have Roman numerals as
10page labels. This makes things more complicated.
12Example 1
13---------
15>>> reader.root_object["/PageLabels"]["/Nums"]
16[0, IndirectObject(18, 0, 139929798197504),
17 8, IndirectObject(19, 0, 139929798197504)]
18>>> reader.get_object(reader.root_object["/PageLabels"]["/Nums"][1])
19{'/S': '/r'}
20>>> reader.get_object(reader.root_object["/PageLabels"]["/Nums"][3])
21{'/S': '/D'}
23Example 2
24---------
25The following is a document with pages labeled
26i, ii, iii, iv, 1, 2, 3, A-8, A-9, ...
281 0 obj
29 << /Type /Catalog
30 /PageLabels << /Nums [
31 0 << /S /r >>
32 4 << /S /D >>
33 7 << /S /D
34 /P ( A- )
35 /St 8
36 >>
37 % A number tree containing
38 % three page label dictionaries
39 ]
40 >>
41 ...
42 >>
43endobj
46§12.4.2 PDF Specification 1.7 and 2.0
47=====================================
49Entries in a page label dictionary
50----------------------------------
51The /S key:
52D Decimal Arabic numerals
53R Uppercase Roman numerals
54r Lowercase Roman numerals
55A Uppercase letters (A to Z for the first 26 pages,
56 AA to ZZ for the next 26, and so on)
57a Lowercase letters (a to z for the first 26 pages,
58 aa to zz for the next 26, and so on)
59"""
61from collections.abc import Callable, Iterator
62from typing import Optional, cast
64from ._protocols import PdfCommonDocProtocol
65from ._utils import logger_warning
66from .generic import (
67 ArrayObject,
68 DictionaryObject,
69 NullObject,
70 NumberObject,
71 is_null_or_none,
72)
75def number2uppercase_roman_numeral(num: int) -> str:
76 if num <= 0:
77 raise ValueError("Expecting a positive number")
78 roman = [
79 (1000, "M"),
80 (900, "CM"),
81 (500, "D"),
82 (400, "CD"),
83 (100, "C"),
84 (90, "XC"),
85 (50, "L"),
86 (40, "XL"),
87 (10, "X"),
88 (9, "IX"),
89 (5, "V"),
90 (4, "IV"),
91 (1, "I"),
92 ]
94 def roman_num(num: int) -> Iterator[str]:
95 for decimal, roman_repr in roman:
96 x, _ = divmod(num, decimal)
97 yield roman_repr * x
98 num -= decimal * x
99 if num <= 0:
100 break
102 return "".join(list(roman_num(num)))
105def number2lowercase_roman_numeral(number: int) -> str:
106 return number2uppercase_roman_numeral(number).lower()
109def number2uppercase_letter(number: int) -> str:
110 if number <= 0:
111 raise ValueError("Expecting a positive number")
112 alphabet = [chr(i) for i in range(ord("A"), ord("Z") + 1)]
113 rep = ""
114 while number > 0:
115 remainder = number % 26
116 if remainder == 0:
117 remainder = 26
118 rep = alphabet[remainder - 1] + rep
119 # update
120 number -= remainder
121 number = number // 26
122 return rep
125def number2lowercase_letter(number: int) -> str:
126 return number2uppercase_letter(number).lower()
129def get_label_from_nums(dictionary_object: DictionaryObject, index: int) -> str:
130 # [Nums] shall be an array of the form
131 # [ key_1 value_1 key_2 value_2 ... key_n value_n ]
132 # where each key_i is an integer and the corresponding
133 # value_i shall be the object associated with that key.
134 # The keys shall be sorted in numerical order,
135 # analogously to the arrangement of keys in a name tree
136 # as described in 7.9.6, "Name Trees."
137 nums = cast(ArrayObject, dictionary_object["/Nums"])
138 nums_length = len(nums)
139 i = 0
140 value = None
141 start_index = 0
142 while i < nums_length:
143 if i + 1 >= nums_length:
144 logger_warning(
145 "Ignoring last /Nums key without a value.", source=__name__
146 )
147 break
148 start_index = nums[i]
149 value = nums[i + 1].get_object()
150 if i + 2 == nums_length:
151 break
152 if nums[i + 2] > index:
153 break
154 i += 2
155 m: dict[Optional[str], Callable[[int], str]] = {
156 None: lambda _: "",
157 "/D": str,
158 "/R": number2uppercase_roman_numeral,
159 "/r": number2lowercase_roman_numeral,
160 "/A": number2uppercase_letter,
161 "/a": number2lowercase_letter,
162 }
163 # if /Nums array is not following the specification or if /Nums is empty
164 if not isinstance(value, dict):
165 return str(index + 1) # Fallback
166 start = value.get("/St", 1)
167 prefix = cast(str, value.get("/P", ""))
168 mapping_function = m.get(value.get("/S"))
169 if mapping_function is None:
170 # Unknown /S numbering style; fall back to the page position.
171 logger_warning(
172 "Ignoring unknown page label numbering style %(style)r in /Nums.",
173 source=__name__,
174 style=value.get("/S"),
175 )
176 return str(index + 1) # Fallback
177 try:
178 return prefix + mapping_function(index - start_index + start)
179 except (TypeError, ValueError):
180 # Malformed /St or /P value; fall back to the page position.
181 logger_warning(
182 "Ignoring malformed page label entry in /Nums (/St=%(start)r, /P=%(prefix)r).",
183 source=__name__,
184 start=start,
185 prefix=prefix,
186 )
187 return str(index + 1) # Fallback
190def index2label(reader: PdfCommonDocProtocol, index: int) -> str:
191 """
192 See 7.9.7 "Number Trees".
194 Args:
195 reader: The PdfReader
196 index: The index of the page
198 Returns:
199 The label of the page, e.g. "iv" or "4".
201 """
202 root = cast(DictionaryObject, reader.root_object)
203 if "/PageLabels" not in root:
204 return str(index + 1) # Fallback
205 number_tree = root["/PageLabels"].get_object()
206 if not isinstance(number_tree, DictionaryObject):
207 logger_warning(
208 "Page labels are not a dictionary: %(number_tree)s",
209 source=__name__,
210 number_tree=number_tree,
211 )
212 return str(index + 1) # Fallback
213 if "/Nums" in number_tree:
214 return get_label_from_nums(number_tree, index)
215 if "/Kids" in number_tree and not isinstance(number_tree["/Kids"], NullObject):
216 # number_tree = {'/Kids': [IndirectObject(7333, 0, 140132998195856), ...]}
217 # Limit maximum depth.
218 level = 0
219 while level < 100:
220 kids = cast(list[DictionaryObject], number_tree["/Kids"])
221 for kid in kids:
222 # kid = {'/Limits': [0, 63], '/Nums': [0, {'/P': 'C1'}, ...]}
223 limits = kid.get("/Limits", NullObject()).get_object()
224 if not isinstance(limits, list) or len(limits) < 2:
225 # Skip kids whose /Limits range is missing or malformed.
226 logger_warning(
227 "Ignoring kid with missing or malformed /Limits in /PageLabels.",
228 source=__name__,
229 )
230 continue
231 if limits[0] <= index <= limits[1]:
232 if not is_null_or_none(kid.get("/Kids", None)):
233 # Recursive definition.
234 level += 1
235 if level == 100: # pragma: no cover
236 raise NotImplementedError(
237 "Too deep nesting is not supported."
238 )
239 number_tree = kid
240 # Exit the inner `for` loop and continue at the next level with the
241 # next iteration of the `while` loop.
242 break
243 return get_label_from_nums(kid, index)
244 else:
245 # When there are no kids, make sure to exit the `while` loop directly
246 # and continue with the fallback.
247 break
249 logger_warning("Could not reliably determine page label for %(index)d.", source=__name__, index=index)
250 return str(index + 1) # Fallback if neither /Nums nor /Kids is in the number_tree
253def nums_insert(
254 key: NumberObject,
255 value: DictionaryObject,
256 nums: ArrayObject,
257) -> None:
258 """
259 Insert a key, value pair in a Nums array.
261 See 7.9.7 "Number Trees".
263 Args:
264 key: number key of the entry
265 value: value of the entry
266 nums: Nums array to modify
268 """
269 if len(nums) % 2 != 0:
270 raise ValueError("A nums like array must have an even number of elements")
272 i = len(nums)
273 while i != 0 and key <= nums[i - 2]:
274 i = i - 2
276 if i < len(nums) and key == nums[i]:
277 nums[i + 1] = value
278 else:
279 nums.insert(i, key)
280 nums.insert(i + 1, value)
283def nums_clear_range(
284 key: NumberObject,
285 page_index_to: int,
286 nums: ArrayObject,
287) -> None:
288 """
289 Remove all entries in a number tree in a range after an entry.
291 See 7.9.7 "Number Trees".
293 Args:
294 key: number key of the entry before the range
295 page_index_to: The page index of the upper limit of the range
296 nums: Nums array to modify
298 """
299 if len(nums) % 2 != 0:
300 raise ValueError("A nums like array must have an even number of elements")
301 if page_index_to < key:
302 raise ValueError("page_index_to must be greater or equal than key")
304 i = nums.index(key) + 2
305 while i < len(nums) and nums[i] <= page_index_to:
306 nums.pop(i)
307 nums.pop(i)
310def nums_next(
311 key: NumberObject,
312 nums: ArrayObject,
313) -> tuple[Optional[NumberObject], Optional[DictionaryObject]]:
314 """
315 Return the (key, value) pair of the entry after the given one.
317 See 7.9.7 "Number Trees".
319 Args:
320 key: number key of the entry
321 nums: Nums array
323 """
324 if len(nums) % 2 != 0:
325 raise ValueError("A nums like array must have an even number of elements")
327 i = nums.index(key) + 2
328 if i < len(nums):
329 return (nums[i], nums[i + 1])
330 return (None, None)