Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pypdf/_page_labels.py: 14%
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"""
60import string
61from typing import TYPE_CHECKING, Optional, cast
63from ._protocols import PdfCommonDocProtocol
64from ._utils import logger_warning
65from .generic import (
66 ArrayObject,
67 DictionaryObject,
68 NullObject,
69 NumberObject,
70 is_null_or_none,
71)
73if TYPE_CHECKING:
74 from collections.abc import Callable
76# The largest number one can represent using the usual symbols
77# and subtractive notation without additional conventions.
78# Aligns with https://github.com/AA-Turner/roman-numerals/blob/master/python/roman_numerals/__init__.py
79MAXIMUM_ROMAN_NUMERAL = 3_999
81# Allow values of up to 512 * 26 = 13_312 for the `/A` and `/a`.
82# Please note that this limit is somehow arbitrary and some trade-off
83# between rather restrictive <= 256 output bytes per label and the possibly
84# unrealistic 1000 output bytes per label.
85MAXIMUM_PAGE_LABEL_LENGTH = 512
88def number2uppercase_roman_numeral(num: int) -> str:
89 if num <= 0:
90 raise ValueError("Expecting a positive number.")
91 if num > MAXIMUM_ROMAN_NUMERAL:
92 raise ValueError("Number is out of range.")
93 roman = [
94 (1000, "M"),
95 (900, "CM"),
96 (500, "D"),
97 (400, "CD"),
98 (100, "C"),
99 (90, "XC"),
100 (50, "L"),
101 (40, "XL"),
102 (10, "X"),
103 (9, "IX"),
104 (5, "V"),
105 (4, "IV"),
106 (1, "I"),
107 ]
109 result = []
110 for value, symbol in roman:
111 count, num = divmod(num, value)
112 result.append(symbol * count)
114 return "".join(result)
117def number2lowercase_roman_numeral(number: int) -> str:
118 return number2uppercase_roman_numeral(number).lower()
121def number2uppercase_letter(number: int) -> str:
122 if number <= 0:
123 raise ValueError("Expecting a positive number.")
124 # A to Z, then the same letter repeated: AA to ZZ, AAA to ZZZ, and so on.
125 repetitions, position = divmod(number - 1, 26)
126 length = repetitions + 1
127 if length > MAXIMUM_PAGE_LABEL_LENGTH:
128 raise ValueError("Number is too large.")
129 return string.ascii_uppercase[position] * (repetitions + 1)
132def number2lowercase_letter(number: int) -> str:
133 return number2uppercase_letter(number).lower()
136def get_label_from_nums(dictionary_object: DictionaryObject, index: int) -> str:
137 # [Nums] shall be an array of the form
138 # [ key_1 value_1 key_2 value_2 ... key_n value_n ]
139 # where each key_i is an integer and the corresponding
140 # value_i shall be the object associated with that key.
141 # The keys shall be sorted in numerical order,
142 # analogously to the arrangement of keys in a name tree
143 # as described in 7.9.6, "Name Trees."
144 nums = cast(ArrayObject, dictionary_object["/Nums"])
145 nums_length = len(nums)
146 i = 0
147 value = None
148 start_index = 0
149 while i < nums_length:
150 if i + 1 >= nums_length:
151 logger_warning(
152 "Ignoring last /Nums key without a value.", source=__name__
153 )
154 break
155 start_index = nums[i]
156 value = nums[i + 1].get_object()
157 if i + 2 == nums_length:
158 break
159 if nums[i + 2] > index:
160 break
161 i += 2
162 m: dict[Optional[str], Callable[[int], str]] = {
163 None: lambda _: "",
164 "/D": str,
165 "/R": number2uppercase_roman_numeral,
166 "/r": number2lowercase_roman_numeral,
167 "/A": number2uppercase_letter,
168 "/a": number2lowercase_letter,
169 }
170 # if /Nums array is not following the specification or if /Nums is empty
171 if not isinstance(value, dict):
172 return str(index + 1) # Fallback
173 start = value.get("/St", 1)
174 prefix = cast(str, value.get("/P", ""))
175 mapping_function = m.get(value.get("/S"))
176 if mapping_function is None:
177 # Unknown /S numbering style; fall back to the page position.
178 logger_warning(
179 "Ignoring unknown page label numbering style %(style)r in /Nums.",
180 source=__name__,
181 style=value.get("/S"),
182 )
183 return str(index + 1) # Fallback
184 try:
185 return prefix + mapping_function(index - start_index + start)
186 except (TypeError, ValueError) as exception:
187 # Malformed /St or /P value; fall back to the page position.
188 logger_warning(
189 "Ignoring malformed page label entry in /Nums (/St=%(start)r, /P=%(prefix)r): %(exception)s",
190 source=__name__,
191 start=start,
192 prefix=prefix,
193 exception=exception,
194 )
195 return str(index + 1) # Fallback
198def index2label(reader: PdfCommonDocProtocol, index: int) -> str:
199 """
200 See 7.9.7 "Number Trees".
202 Args:
203 reader: The PdfReader
204 index: The index of the page
206 Returns:
207 The label of the page, e.g. "iv" or "4".
209 """
210 root = cast(DictionaryObject, reader.root_object)
211 if "/PageLabels" not in root:
212 return str(index + 1) # Fallback
213 number_tree = root["/PageLabels"].get_object()
214 if not isinstance(number_tree, DictionaryObject):
215 logger_warning(
216 "Page labels are not a dictionary: %(number_tree)s",
217 source=__name__,
218 number_tree=number_tree,
219 )
220 return str(index + 1) # Fallback
221 if "/Nums" in number_tree:
222 return get_label_from_nums(number_tree, index)
223 if "/Kids" in number_tree and not isinstance(number_tree["/Kids"], NullObject):
224 # number_tree = {'/Kids': [IndirectObject(7333, 0, 140132998195856), ...]}
225 # Limit maximum depth.
226 level = 0
227 while level < 100:
228 kids = number_tree["/Kids"].get_object()
229 if not isinstance(kids, ArrayObject):
230 logger_warning(
231 "Page label kids are not an array: %(kids)s",
232 source=__name__,
233 kids=kids,
234 )
235 break
236 for kid in kids:
237 # kid = {'/Limits': [0, 63], '/Nums': [0, {'/P': 'C1'}, ...]}
238 kid = kid.get_object()
239 if not isinstance(kid, DictionaryObject):
240 logger_warning(
241 "Ignoring kid which is not a dictionary in /PageLabels.",
242 source=__name__,
243 )
244 continue
245 limits = kid.get("/Limits", NullObject()).get_object()
246 if not isinstance(limits, list) or len(limits) < 2:
247 # Skip kids whose /Limits range is missing or malformed.
248 logger_warning(
249 "Ignoring kid with missing or malformed /Limits in /PageLabels.",
250 source=__name__,
251 )
252 continue
253 if limits[0] <= index <= limits[1]:
254 if not is_null_or_none(kid.get("/Kids", None)):
255 # Recursive definition.
256 level += 1
257 if level == 100: # pragma: no cover
258 raise NotImplementedError(
259 "Too deep nesting is not supported."
260 )
261 number_tree = kid
262 # Exit the inner `for` loop and continue at the next level with the
263 # next iteration of the `while` loop.
264 break
265 return get_label_from_nums(kid, index)
266 else:
267 # When there are no kids, make sure to exit the `while` loop directly
268 # and continue with the fallback.
269 break
271 logger_warning("Could not reliably determine page label for %(index)d.", source=__name__, index=index)
272 return str(index + 1) # Fallback if neither /Nums nor /Kids is in the number_tree
275def nums_insert(
276 key: NumberObject,
277 value: DictionaryObject,
278 nums: ArrayObject,
279) -> None:
280 """
281 Insert a key, value pair in a Nums array.
283 See 7.9.7 "Number Trees".
285 Args:
286 key: number key of the entry
287 value: value of the entry
288 nums: Nums array to modify
290 """
291 if len(nums) % 2 != 0:
292 raise ValueError("A nums like array must have an even number of elements")
294 i = len(nums)
295 while i != 0 and key <= nums[i - 2]:
296 i = i - 2
298 if i < len(nums) and key == nums[i]:
299 nums[i + 1] = value
300 else:
301 nums.insert(i, key)
302 nums.insert(i + 1, value)
305def nums_clear_range(
306 key: NumberObject,
307 page_index_to: int,
308 nums: ArrayObject,
309) -> None:
310 """
311 Remove all entries in a number tree in a range after an entry.
313 See 7.9.7 "Number Trees".
315 Args:
316 key: number key of the entry before the range
317 page_index_to: The page index of the upper limit of the range
318 nums: Nums array to modify
320 """
321 if len(nums) % 2 != 0:
322 raise ValueError("A nums like array must have an even number of elements")
323 if page_index_to < key:
324 raise ValueError("page_index_to must be greater or equal than key")
326 i = nums.index(key) + 2
327 while i < len(nums) and nums[i] <= page_index_to:
328 nums.pop(i)
329 nums.pop(i)
332def nums_next(
333 key: NumberObject,
334 nums: ArrayObject,
335) -> tuple[Optional[NumberObject], Optional[DictionaryObject]]:
336 """
337 Return the (key, value) pair of the entry after the given one.
339 See 7.9.7 "Number Trees".
341 Args:
342 key: number key of the entry
343 nums: Nums array
345 """
346 if len(nums) % 2 != 0:
347 raise ValueError("A nums like array must have an even number of elements")
349 i = nums.index(key) + 2
350 if i < len(nums):
351 return nums[i], nums[i + 1]
352 return None, None