Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pypdf/_page_labels.py: 13%

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

115 statements  

1""" 

2Page labels are shown by PDF viewers as "the page number". 

3 

4A page has a numeric index, starting at 0. Additionally, the page 

5has a label. In the most simple case: 

6 

7 label = index + 1 

8 

9However, the title page and the table of contents might have Roman numerals as 

10page labels. This makes things more complicated. 

11 

12Example 1 

13--------- 

14 

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'} 

22 

23Example 2 

24--------- 

25The following is a document with pages labeled 

26i, ii, iii, iv, 1, 2, 3, A-8, A-9, ... 

27 

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 

44 

45 

46§12.4.2 PDF Specification 1.7 and 2.0 

47===================================== 

48 

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""" 

60 

61from collections.abc import Callable, Iterator 

62from typing import Optional, cast 

63 

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) 

73 

74 

75def number2uppercase_roman_numeral(num: int) -> str: 

76 roman = [ 

77 (1000, "M"), 

78 (900, "CM"), 

79 (500, "D"), 

80 (400, "CD"), 

81 (100, "C"), 

82 (90, "XC"), 

83 (50, "L"), 

84 (40, "XL"), 

85 (10, "X"), 

86 (9, "IX"), 

87 (5, "V"), 

88 (4, "IV"), 

89 (1, "I"), 

90 ] 

91 

92 def roman_num(num: int) -> Iterator[str]: 

93 for decimal, roman_repr in roman: 

94 x, _ = divmod(num, decimal) 

95 yield roman_repr * x 

96 num -= decimal * x 

97 if num <= 0: 

98 break 

99 

100 return "".join(list(roman_num(num))) 

101 

102 

103def number2lowercase_roman_numeral(number: int) -> str: 

104 return number2uppercase_roman_numeral(number).lower() 

105 

106 

107def number2uppercase_letter(number: int) -> str: 

108 if number <= 0: 

109 raise ValueError("Expecting a positive number") 

110 alphabet = [chr(i) for i in range(ord("A"), ord("Z") + 1)] 

111 rep = "" 

112 while number > 0: 

113 remainder = number % 26 

114 if remainder == 0: 

115 remainder = 26 

116 rep = alphabet[remainder - 1] + rep 

117 # update 

118 number -= remainder 

119 number = number // 26 

120 return rep 

121 

122 

123def number2lowercase_letter(number: int) -> str: 

124 return number2uppercase_letter(number).lower() 

125 

126 

127def get_label_from_nums(dictionary_object: DictionaryObject, index: int) -> str: 

128 # [Nums] shall be an array of the form 

129 # [ key_1 value_1 key_2 value_2 ... key_n value_n ] 

130 # where each key_i is an integer and the corresponding 

131 # value_i shall be the object associated with that key. 

132 # The keys shall be sorted in numerical order, 

133 # analogously to the arrangement of keys in a name tree 

134 # as described in 7.9.6, "Name Trees." 

135 nums = cast(ArrayObject, dictionary_object["/Nums"]) 

136 nums_length = len(nums) 

137 i = 0 

138 value = None 

139 start_index = 0 

140 while i < nums_length: 

141 if i + 1 >= nums_length: 

142 logger_warning( 

143 "Ignoring last /Nums key without a value.", source=__name__ 

144 ) 

145 break 

146 start_index = nums[i] 

147 value = nums[i + 1].get_object() 

148 if i + 2 == nums_length: 

149 break 

150 if nums[i + 2] > index: 

151 break 

152 i += 2 

153 m: dict[Optional[str], Callable[[int], str]] = { 

154 None: lambda _: "", 

155 "/D": str, 

156 "/R": number2uppercase_roman_numeral, 

157 "/r": number2lowercase_roman_numeral, 

158 "/A": number2uppercase_letter, 

159 "/a": number2lowercase_letter, 

160 } 

161 # if /Nums array is not following the specification or if /Nums is empty 

162 if not isinstance(value, dict): 

163 return str(index + 1) # Fallback 

164 start = value.get("/St", 1) 

165 prefix = cast(str, value.get("/P", "")) 

166 mapping_function = m.get(value.get("/S")) 

167 if mapping_function is None: 

168 # Unknown /S numbering style; fall back to the page position. 

169 logger_warning( 

170 "Ignoring unknown page label numbering style %(style)r in /Nums.", 

171 source=__name__, 

172 style=value.get("/S"), 

173 ) 

174 return str(index + 1) # Fallback 

175 try: 

176 return prefix + mapping_function(index - start_index + start) 

177 except (TypeError, ValueError): 

178 # Malformed /St or /P value; fall back to the page position. 

179 logger_warning( 

180 "Ignoring malformed page label entry in /Nums (/St=%(start)r, /P=%(prefix)r).", 

181 source=__name__, 

182 start=start, 

183 prefix=prefix, 

184 ) 

185 return str(index + 1) # Fallback 

186 

187 

188def index2label(reader: PdfCommonDocProtocol, index: int) -> str: 

189 """ 

190 See 7.9.7 "Number Trees". 

191 

192 Args: 

193 reader: The PdfReader 

194 index: The index of the page 

195 

196 Returns: 

197 The label of the page, e.g. "iv" or "4". 

198 

199 """ 

200 root = cast(DictionaryObject, reader.root_object) 

201 if "/PageLabels" not in root: 

202 return str(index + 1) # Fallback 

203 number_tree = cast(DictionaryObject, root["/PageLabels"].get_object()) 

204 if "/Nums" in number_tree: 

205 return get_label_from_nums(number_tree, index) 

206 if "/Kids" in number_tree and not isinstance(number_tree["/Kids"], NullObject): 

207 # number_tree = {'/Kids': [IndirectObject(7333, 0, 140132998195856), ...]} 

208 # Limit maximum depth. 

209 level = 0 

210 while level < 100: 

211 kids = cast(list[DictionaryObject], number_tree["/Kids"]) 

212 for kid in kids: 

213 # kid = {'/Limits': [0, 63], '/Nums': [0, {'/P': 'C1'}, ...]} 

214 limits = kid.get("/Limits", NullObject()).get_object() 

215 if not isinstance(limits, list) or len(limits) < 2: 

216 # Skip kids whose /Limits range is missing or malformed. 

217 logger_warning( 

218 "Ignoring kid with missing or malformed /Limits in /PageLabels.", 

219 source=__name__, 

220 ) 

221 continue 

222 if limits[0] <= index <= limits[1]: 

223 if not is_null_or_none(kid.get("/Kids", None)): 

224 # Recursive definition. 

225 level += 1 

226 if level == 100: # pragma: no cover 

227 raise NotImplementedError( 

228 "Too deep nesting is not supported." 

229 ) 

230 number_tree = kid 

231 # Exit the inner `for` loop and continue at the next level with the 

232 # next iteration of the `while` loop. 

233 break 

234 return get_label_from_nums(kid, index) 

235 else: 

236 # When there are no kids, make sure to exit the `while` loop directly 

237 # and continue with the fallback. 

238 break 

239 

240 logger_warning("Could not reliably determine page label for %(index)d.", source=__name__, index=index) 

241 return str(index + 1) # Fallback if neither /Nums nor /Kids is in the number_tree 

242 

243 

244def nums_insert( 

245 key: NumberObject, 

246 value: DictionaryObject, 

247 nums: ArrayObject, 

248) -> None: 

249 """ 

250 Insert a key, value pair in a Nums array. 

251 

252 See 7.9.7 "Number Trees". 

253 

254 Args: 

255 key: number key of the entry 

256 value: value of the entry 

257 nums: Nums array to modify 

258 

259 """ 

260 if len(nums) % 2 != 0: 

261 raise ValueError("A nums like array must have an even number of elements") 

262 

263 i = len(nums) 

264 while i != 0 and key <= nums[i - 2]: 

265 i = i - 2 

266 

267 if i < len(nums) and key == nums[i]: 

268 nums[i + 1] = value 

269 else: 

270 nums.insert(i, key) 

271 nums.insert(i + 1, value) 

272 

273 

274def nums_clear_range( 

275 key: NumberObject, 

276 page_index_to: int, 

277 nums: ArrayObject, 

278) -> None: 

279 """ 

280 Remove all entries in a number tree in a range after an entry. 

281 

282 See 7.9.7 "Number Trees". 

283 

284 Args: 

285 key: number key of the entry before the range 

286 page_index_to: The page index of the upper limit of the range 

287 nums: Nums array to modify 

288 

289 """ 

290 if len(nums) % 2 != 0: 

291 raise ValueError("A nums like array must have an even number of elements") 

292 if page_index_to < key: 

293 raise ValueError("page_index_to must be greater or equal than key") 

294 

295 i = nums.index(key) + 2 

296 while i < len(nums) and nums[i] <= page_index_to: 

297 nums.pop(i) 

298 nums.pop(i) 

299 

300 

301def nums_next( 

302 key: NumberObject, 

303 nums: ArrayObject, 

304) -> tuple[Optional[NumberObject], Optional[DictionaryObject]]: 

305 """ 

306 Return the (key, value) pair of the entry after the given one. 

307 

308 See 7.9.7 "Number Trees". 

309 

310 Args: 

311 key: number key of the entry 

312 nums: Nums array 

313 

314 """ 

315 if len(nums) % 2 != 0: 

316 raise ValueError("A nums like array must have an even number of elements") 

317 

318 i = nums.index(key) + 2 

319 if i < len(nums): 

320 return (nums[i], nums[i + 1]) 

321 return (None, None)