_page_labels.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. """
  2. Page labels are shown by PDF viewers as "the page number".
  3. A page has a numeric index, starting at 0. Additionally, the page
  4. has a label. In the most simple case:
  5. label = index + 1
  6. However, the title page and the table of contents might have Roman numerals as
  7. page labels. This makes things more complicated.
  8. Example 1
  9. ---------
  10. >>> reader.root_object["/PageLabels"]["/Nums"]
  11. [0, IndirectObject(18, 0, 139929798197504),
  12. 8, IndirectObject(19, 0, 139929798197504)]
  13. >>> reader.get_object(reader.root_object["/PageLabels"]["/Nums"][1])
  14. {'/S': '/r'}
  15. >>> reader.get_object(reader.root_object["/PageLabels"]["/Nums"][3])
  16. {'/S': '/D'}
  17. Example 2
  18. ---------
  19. The following is a document with pages labeled
  20. i, ii, iii, iv, 1, 2, 3, A-8, A-9, ...
  21. 1 0 obj
  22. << /Type /Catalog
  23. /PageLabels << /Nums [
  24. 0 << /S /r >>
  25. 4 << /S /D >>
  26. 7 << /S /D
  27. /P ( A- )
  28. /St 8
  29. >>
  30. % A number tree containing
  31. % three page label dictionaries
  32. ]
  33. >>
  34. ...
  35. >>
  36. endobj
  37. §12.4.2 PDF Specification 1.7 and 2.0
  38. =====================================
  39. Entries in a page label dictionary
  40. ----------------------------------
  41. The /S key:
  42. D Decimal Arabic numerals
  43. R Uppercase Roman numerals
  44. r Lowercase Roman numerals
  45. A Uppercase letters (A to Z for the first 26 pages,
  46. AA to ZZ for the next 26, and so on)
  47. a Lowercase letters (a to z for the first 26 pages,
  48. aa to zz for the next 26, and so on)
  49. """
  50. from collections.abc import Callable, Iterator
  51. from typing import Optional, cast
  52. from ._protocols import PdfCommonDocProtocol
  53. from ._utils import logger_warning
  54. from .generic import (
  55. ArrayObject,
  56. DictionaryObject,
  57. NullObject,
  58. NumberObject,
  59. is_null_or_none,
  60. )
  61. def number2uppercase_roman_numeral(num: int) -> str:
  62. roman = [
  63. (1000, "M"),
  64. (900, "CM"),
  65. (500, "D"),
  66. (400, "CD"),
  67. (100, "C"),
  68. (90, "XC"),
  69. (50, "L"),
  70. (40, "XL"),
  71. (10, "X"),
  72. (9, "IX"),
  73. (5, "V"),
  74. (4, "IV"),
  75. (1, "I"),
  76. ]
  77. def roman_num(num: int) -> Iterator[str]:
  78. for decimal, roman_repr in roman:
  79. x, _ = divmod(num, decimal)
  80. yield roman_repr * x
  81. num -= decimal * x
  82. if num <= 0:
  83. break
  84. return "".join(list(roman_num(num)))
  85. def number2lowercase_roman_numeral(number: int) -> str:
  86. return number2uppercase_roman_numeral(number).lower()
  87. def number2uppercase_letter(number: int) -> str:
  88. if number <= 0:
  89. raise ValueError("Expecting a positive number")
  90. alphabet = [chr(i) for i in range(ord("A"), ord("Z") + 1)]
  91. rep = ""
  92. while number > 0:
  93. remainder = number % 26
  94. if remainder == 0:
  95. remainder = 26
  96. rep = alphabet[remainder - 1] + rep
  97. # update
  98. number -= remainder
  99. number = number // 26
  100. return rep
  101. def number2lowercase_letter(number: int) -> str:
  102. return number2uppercase_letter(number).lower()
  103. def get_label_from_nums(dictionary_object: DictionaryObject, index: int) -> str:
  104. # [Nums] shall be an array of the form
  105. # [ key_1 value_1 key_2 value_2 ... key_n value_n ]
  106. # where each key_i is an integer and the corresponding
  107. # value_i shall be the object associated with that key.
  108. # The keys shall be sorted in numerical order,
  109. # analogously to the arrangement of keys in a name tree
  110. # as described in 7.9.6, "Name Trees."
  111. nums = cast(ArrayObject, dictionary_object["/Nums"])
  112. i = 0
  113. value = None
  114. start_index = 0
  115. while i < len(nums):
  116. start_index = nums[i]
  117. value = nums[i + 1].get_object()
  118. if i + 2 == len(nums):
  119. break
  120. if nums[i + 2] > index:
  121. break
  122. i += 2
  123. m: dict[Optional[str], Callable[[int], str]] = {
  124. None: lambda _: "",
  125. "/D": str,
  126. "/R": number2uppercase_roman_numeral,
  127. "/r": number2lowercase_roman_numeral,
  128. "/A": number2uppercase_letter,
  129. "/a": number2lowercase_letter,
  130. }
  131. # if /Nums array is not following the specification or if /Nums is empty
  132. if not isinstance(value, dict):
  133. return str(index + 1) # Fallback
  134. start = value.get("/St", 1)
  135. prefix = value.get("/P", "")
  136. mapping_function = m[value.get("/S")]
  137. return prefix + mapping_function(index - start_index + start)
  138. def index2label(reader: PdfCommonDocProtocol, index: int) -> str:
  139. """
  140. See 7.9.7 "Number Trees".
  141. Args:
  142. reader: The PdfReader
  143. index: The index of the page
  144. Returns:
  145. The label of the page, e.g. "iv" or "4".
  146. """
  147. root = cast(DictionaryObject, reader.root_object)
  148. if "/PageLabels" not in root:
  149. return str(index + 1) # Fallback
  150. number_tree = cast(DictionaryObject, root["/PageLabels"].get_object())
  151. if "/Nums" in number_tree:
  152. return get_label_from_nums(number_tree, index)
  153. if "/Kids" in number_tree and not isinstance(number_tree["/Kids"], NullObject):
  154. # number_tree = {'/Kids': [IndirectObject(7333, 0, 140132998195856), ...]}
  155. # Limit maximum depth.
  156. level = 0
  157. while level < 100:
  158. kids = cast(list[DictionaryObject], number_tree["/Kids"])
  159. for kid in kids:
  160. # kid = {'/Limits': [0, 63], '/Nums': [0, {'/P': 'C1'}, ...]}
  161. limits = cast(list[int], kid["/Limits"])
  162. if limits[0] <= index <= limits[1]:
  163. if not is_null_or_none(kid.get("/Kids", None)):
  164. # Recursive definition.
  165. level += 1
  166. if level == 100: # pragma: no cover
  167. raise NotImplementedError(
  168. "Too deep nesting is not supported."
  169. )
  170. number_tree = kid
  171. # Exit the inner `for` loop and continue at the next level with the
  172. # next iteration of the `while` loop.
  173. break
  174. return get_label_from_nums(kid, index)
  175. else:
  176. # When there are no kids, make sure to exit the `while` loop directly
  177. # and continue with the fallback.
  178. break
  179. logger_warning(f"Could not reliably determine page label for {index}.", __name__)
  180. return str(index + 1) # Fallback if neither /Nums nor /Kids is in the number_tree
  181. def nums_insert(
  182. key: NumberObject,
  183. value: DictionaryObject,
  184. nums: ArrayObject,
  185. ) -> None:
  186. """
  187. Insert a key, value pair in a Nums array.
  188. See 7.9.7 "Number Trees".
  189. Args:
  190. key: number key of the entry
  191. value: value of the entry
  192. nums: Nums array to modify
  193. """
  194. if len(nums) % 2 != 0:
  195. raise ValueError("A nums like array must have an even number of elements")
  196. i = len(nums)
  197. while i != 0 and key <= nums[i - 2]:
  198. i = i - 2
  199. if i < len(nums) and key == nums[i]:
  200. nums[i + 1] = value
  201. else:
  202. nums.insert(i, key)
  203. nums.insert(i + 1, value)
  204. def nums_clear_range(
  205. key: NumberObject,
  206. page_index_to: int,
  207. nums: ArrayObject,
  208. ) -> None:
  209. """
  210. Remove all entries in a number tree in a range after an entry.
  211. See 7.9.7 "Number Trees".
  212. Args:
  213. key: number key of the entry before the range
  214. page_index_to: The page index of the upper limit of the range
  215. nums: Nums array to modify
  216. """
  217. if len(nums) % 2 != 0:
  218. raise ValueError("A nums like array must have an even number of elements")
  219. if page_index_to < key:
  220. raise ValueError("page_index_to must be greater or equal than key")
  221. i = nums.index(key) + 2
  222. while i < len(nums) and nums[i] <= page_index_to:
  223. nums.pop(i)
  224. nums.pop(i)
  225. def nums_next(
  226. key: NumberObject,
  227. nums: ArrayObject,
  228. ) -> tuple[Optional[NumberObject], Optional[DictionaryObject]]:
  229. """
  230. Return the (key, value) pair of the entry after the given one.
  231. See 7.9.7 "Number Trees".
  232. Args:
  233. key: number key of the entry
  234. nums: Nums array
  235. """
  236. if len(nums) % 2 != 0:
  237. raise ValueError("A nums like array must have an even number of elements")
  238. i = nums.index(key) + 2
  239. if i < len(nums):
  240. return (nums[i], nums[i + 1])
  241. return (None, None)