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

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

836 statements  

1# Copyright (c) 2006, Mathieu Fenniak 

2# Copyright (c) 2007, Ashish Kulkarni <kulkarni.ashish@gmail.com> 

3# 

4# All rights reserved. 

5# 

6# Redistribution and use in source and binary forms, with or without 

7# modification, are permitted provided that the following conditions are 

8# met: 

9# 

10# * Redistributions of source code must retain the above copyright notice, 

11# this list of conditions and the following disclaimer. 

12# * Redistributions in binary form must reproduce the above copyright notice, 

13# this list of conditions and the following disclaimer in the documentation 

14# and/or other materials provided with the distribution. 

15# * The name of the author may not be used to endorse or promote products 

16# derived from this software without specific prior written permission. 

17# 

18# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 

19# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 

20# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 

21# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 

22# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 

23# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 

24# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 

25# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 

26# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 

27# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 

28# POSSIBILITY OF SUCH DAMAGE. 

29 

30import os 

31import re 

32import sys 

33from collections.abc import Iterable 

34from io import BytesIO, UnsupportedOperation 

35from pathlib import Path 

36from types import TracebackType 

37from typing import ( 

38 TYPE_CHECKING, 

39 Any, 

40 Callable, 

41 Optional, 

42 Union, 

43 cast, 

44) 

45 

46if sys.version_info >= (3, 11): 

47 from typing import Self 

48else: 

49 from typing_extensions import Self 

50 

51from ._doc_common import PdfDocCommon, convert_to_int 

52from ._encryption import Encryption, PasswordType 

53from ._utils import ( 

54 WHITESPACES_AS_BYTES, 

55 StrByteType, 

56 StreamType, 

57 logger_warning, 

58 read_non_whitespace, 

59 read_previous_line, 

60 read_until_whitespace, 

61 skip_over_comment, 

62 skip_over_whitespace, 

63) 

64from .constants import TrailerKeys as TK 

65from .errors import ( 

66 EmptyFileError, 

67 FileNotDecryptedError, 

68 LimitReachedError, 

69 PdfReadError, 

70 PdfStreamError, 

71 WrongPasswordError, 

72) 

73from .generic import ( 

74 ArrayObject, 

75 ContentStream, 

76 DecodedStreamObject, 

77 Destination, 

78 DictionaryObject, 

79 EncodedStreamObject, 

80 IndirectObject, 

81 NameObject, 

82 NullObject, 

83 NumberObject, 

84 PdfObject, 

85 StreamObject, 

86 TextStringObject, 

87 TreeObject, 

88 is_null_or_none, 

89 read_object, 

90) 

91from .xmp import XmpInformation 

92 

93if TYPE_CHECKING: 

94 from ._page import PageObject 

95 

96 

97class PdfReader(PdfDocCommon): 

98 """ 

99 Initialize a PdfReader object. 

100 

101 This operation can take some time, as the PDF stream's cross-reference 

102 tables are read into memory. 

103 

104 Args: 

105 stream: A File object or an object that supports the standard read 

106 and seek methods similar to a File object. Could also be a 

107 string representing a path to a PDF file. 

108 strict: Determines whether user should be warned of all 

109 problems and also causes some correctable problems to be fatal. 

110 Defaults to ``False``. 

111 password: Decrypt PDF file at initialization. If the 

112 password is None, the file will not be decrypted. 

113 Defaults to ``None``. 

114 root_object_recovery_limit: The maximum number of objects to query 

115 for recovering the Root object in non-strict mode. To disable 

116 this security measure, pass ``None``. 

117 

118 """ 

119 

120 def __init__( 

121 self, 

122 stream: Union[StrByteType, Path], 

123 strict: bool = False, 

124 password: Union[None, str, bytes] = None, 

125 *, 

126 root_object_recovery_limit: Optional[int] = 10_000, 

127 ) -> None: 

128 self.strict = strict 

129 self.flattened_pages: Optional[list[PageObject]] = None 

130 

131 #: Storage of parsed PDF objects. 

132 self.resolved_objects: dict[tuple[Any, Any], Optional[PdfObject]] = {} 

133 

134 self._startxref: int = 0 

135 self.xref_index = 0 

136 self.xref: dict[int, dict[Any, Any]] = {} 

137 self.xref_free_entry: dict[int, dict[Any, Any]] = {} 

138 self.xref_objStm: dict[int, tuple[Any, Any]] = {} 

139 self.trailer = DictionaryObject() 

140 

141 # Security parameters. 

142 self._root_object_recovery_limit = ( 

143 root_object_recovery_limit if isinstance(root_object_recovery_limit, int) else sys.maxsize 

144 ) 

145 

146 # Map page indirect_reference number to page number 

147 self._page_id2num: Optional[dict[Any, Any]] = None 

148 

149 self._validated_root: Optional[DictionaryObject] = None 

150 

151 self._initialize_stream(stream) 

152 self._known_objects: set[tuple[int, int]] = set() 

153 

154 self._override_encryption = False 

155 self._encryption: Optional[Encryption] = None 

156 if self.is_encrypted: 

157 self._handle_encryption(password) 

158 elif password is not None: 

159 raise PdfReadError("Not an encrypted file") 

160 

161 self._named_destinations_cache: Optional[dict[str, Destination]] = None 

162 

163 def _initialize_stream(self, stream: Union[StrByteType, Path]) -> None: 

164 if hasattr(stream, "mode") and "b" not in stream.mode: 

165 logger_warning( 

166 "PdfReader stream/file object is not in binary mode. " 

167 "It may not be read correctly.", 

168 source=__name__, 

169 ) 

170 self._stream_opened = False 

171 if isinstance(stream, (str, Path)): 

172 with open(stream, "rb") as fh: 

173 stream = BytesIO(fh.read()) 

174 self._stream_opened = True 

175 self.read(stream) 

176 self.stream = stream 

177 

178 def _handle_encryption(self, password: Optional[Union[str, bytes]]) -> None: 

179 self._override_encryption = True 

180 # Some documents may not have a /ID, use two empty 

181 # byte strings instead. Solves 

182 # https://github.com/py-pdf/pypdf/issues/608 

183 id_entry = self.trailer.get(TK.ID) 

184 id1_entry = id_entry[0].get_object().original_bytes if id_entry else b"" 

185 encrypt_entry = cast(DictionaryObject, self.trailer[TK.ENCRYPT].get_object()) 

186 self._encryption = Encryption.read(encrypt_entry, id1_entry) 

187 

188 # try empty password if no password provided 

189 pwd = password if password is not None else b"" 

190 if ( 

191 self._encryption.verify(pwd, strict=self.strict) == PasswordType.NOT_DECRYPTED 

192 and password is not None 

193 ): 

194 # raise if password provided 

195 raise WrongPasswordError("Wrong password") 

196 self._override_encryption = False 

197 

198 def __enter__(self) -> Self: 

199 return self 

200 

201 def __exit__( 

202 self, 

203 exc_type: Optional[type[BaseException]], 

204 exc_val: Optional[BaseException], 

205 exc_tb: Optional[TracebackType], 

206 ) -> None: 

207 self.close() 

208 

209 def close(self) -> None: 

210 """Close the stream if opened in __init__ and clear memory.""" 

211 if self._stream_opened: 

212 self.stream.close() 

213 self.flattened_pages = [] 

214 self.resolved_objects = {} 

215 self.trailer = DictionaryObject() 

216 self.xref = {} 

217 self.xref_free_entry = {} 

218 self.xref_objStm = {} 

219 

220 @property 

221 def root_object(self) -> DictionaryObject: 

222 """Provide access to "/Root". Standardized with PdfWriter.""" 

223 if self._validated_root: 

224 return self._validated_root 

225 root = self.trailer.get(TK.ROOT) 

226 if is_null_or_none(root): 

227 logger_warning('Cannot find "/Root" key in trailer', source=__name__) 

228 elif ( 

229 cast(DictionaryObject, cast(PdfObject, root).get_object()).get("/Type") 

230 == "/Catalog" 

231 ): 

232 self._validated_root = cast( 

233 DictionaryObject, cast(PdfObject, root).get_object() 

234 ) 

235 else: 

236 logger_warning("Invalid Root object in trailer", source=__name__) 

237 if self._validated_root is None: 

238 logger_warning('Searching object with "/Catalog" key', source=__name__) 

239 number_of_objects = cast(int, self.trailer.get("/Size", 0)) 

240 for i in range(number_of_objects): 

241 if i >= self._root_object_recovery_limit: 

242 raise LimitReachedError("Maximum Root object recovery limit reached.") 

243 try: 

244 obj = self.get_object(i + 1) 

245 except Exception: # to be sure to capture all errors 

246 obj = None 

247 if isinstance(obj, DictionaryObject) and obj.get("/Type") == "/Catalog": 

248 self._validated_root = obj 

249 logger_warning( 

250 "Root found at %(obj_reference)r", 

251 source=__name__, 

252 obj_reference=obj.indirect_reference, 

253 ) 

254 break 

255 if self._validated_root is None: 

256 if not is_null_or_none(root) and "/Pages" in cast(DictionaryObject, cast(PdfObject, root).get_object()): 

257 logger_warning( 

258 "Possible root found at %(root_ref)r, but missing /Catalog key", 

259 source=__name__, 

260 root_ref=cast(PdfObject, root).indirect_reference, 

261 ) 

262 self._validated_root = cast( 

263 DictionaryObject, cast(PdfObject, root).get_object() 

264 ) 

265 else: 

266 raise PdfReadError("Cannot find Root object in pdf") 

267 return self._validated_root 

268 

269 @property 

270 def _info(self) -> Optional[DictionaryObject]: 

271 """ 

272 Provide access to "/Info". Standardized with PdfWriter. 

273 

274 Returns: 

275 /Info Dictionary; None if the entry does not exist 

276 

277 """ 

278 info = self.trailer.get(TK.INFO, None) 

279 if is_null_or_none(info): 

280 return None 

281 assert info is not None, "mypy" 

282 info = info.get_object() 

283 if not isinstance(info, DictionaryObject): 

284 raise PdfReadError( 

285 "Trailer not found or does not point to a document information dictionary" 

286 ) 

287 return info 

288 

289 @property 

290 def _ID(self) -> Optional[ArrayObject]: 

291 """ 

292 Provide access to "/ID". Standardized with PdfWriter. 

293 

294 Returns: 

295 /ID array; None if the entry does not exist 

296 

297 """ 

298 id = self.trailer.get(TK.ID, None) 

299 if is_null_or_none(id): 

300 return None 

301 assert id is not None, "mypy" 

302 return cast(ArrayObject, id.get_object()) 

303 

304 @property 

305 def pdf_header(self) -> str: 

306 """ 

307 The first 8 bytes of the file. 

308 

309 This is typically something like ``'%PDF-1.6'`` and can be used to 

310 detect if the file is actually a PDF file and which version it is. 

311 """ 

312 # TODO: Make this return a bytes object for consistency 

313 # but that needs a deprecation 

314 loc = self.stream.tell() 

315 self.stream.seek(0, 0) 

316 pdf_file_version = self.stream.read(8).decode("utf-8", "backslashreplace") 

317 self.stream.seek(loc, 0) # return to where it was 

318 return pdf_file_version 

319 

320 @property 

321 def xmp_metadata(self) -> Optional[XmpInformation]: 

322 """XMP (Extensible Metadata Platform) data.""" 

323 try: 

324 self._override_encryption = True 

325 return cast(XmpInformation, self.root_object.xmp_metadata) 

326 finally: 

327 self._override_encryption = False 

328 

329 def _get_page_number_by_indirect( 

330 self, indirect_reference: Union[None, int, NullObject, IndirectObject] 

331 ) -> Optional[int]: 

332 """ 

333 Retrieve the page number from an indirect reference. 

334 

335 Args: 

336 indirect_reference: The indirect reference to locate. 

337 

338 Returns: 

339 Page number or None. 

340 

341 """ 

342 if self._page_id2num is None: 

343 self._page_id2num = { 

344 x.indirect_reference.idnum: i for i, x in enumerate(self.pages) # type: ignore[union-attr] 

345 } 

346 

347 if is_null_or_none(indirect_reference): 

348 return None 

349 assert isinstance(indirect_reference, (int, IndirectObject)), "mypy" 

350 if isinstance(indirect_reference, int): 

351 idnum = indirect_reference 

352 else: 

353 idnum = indirect_reference.idnum 

354 assert self._page_id2num is not None, "hint for mypy" 

355 return self._page_id2num.get(idnum, None) 

356 

357 def _get_object_from_stream( 

358 self, indirect_reference: IndirectObject 

359 ) -> Union[int, PdfObject, str]: 

360 # indirect reference to object in object stream 

361 # read the entire object stream into memory 

362 stmnum, _idx = self.xref_objStm[indirect_reference.idnum] 

363 obj_stm: EncodedStreamObject = IndirectObject(stmnum, 0, self).get_object() # type: ignore[assignment] 

364 # This is an xref to a stream, so its type better be a stream 

365 assert cast(str, obj_stm["/Type"]) == "/ObjStm" 

366 # Parse ALL objects in this stream in one pass and cache them. 

367 # This avoids O(N²) behavior when many objects from the same stream 

368 # are resolved individually (each call would re-parse the header). 

369 stream_data = BytesIO(obj_stm.get_data()) 

370 n = int(obj_stm["/N"]) # type: ignore[call-overload] 

371 first_offset = int(obj_stm["/First"]) # type: ignore[call-overload] 

372 

373 # ObjStm header format: "objnum offset objnum offset ..." 

374 # smallest possible entry: "0 0" = 3 bytes (1 digit + 1 space + 1 digit) 

375 # using // 4 would reject a valid 3-byte single entry (3 // 4 = 0) 

376 max_n = stream_data.getbuffer().nbytes // 3 

377 stream_data.seek(0) 

378 if n > max_n: 

379 if self.strict: 

380 raise LimitReachedError(f"Value /N {n} for object {stmnum} exceeds maximum allowed value {max_n}.") 

381 logger_warning( 

382 "Value /N %(n)d for object %(stmnum)d exceeds maximum allowed value %(max_n)d. Limiting to %(max_n)d.", 

383 source=__name__, 

384 n=n, 

385 stmnum=stmnum, 

386 max_n=max_n, 

387 ) 

388 n = max_n 

389 

390 # Phase 1: Read the index (objnum, offset) pairs from the header. 

391 obj_index: list[tuple[int, int]] = [] 

392 for _i in range(n): 

393 read_non_whitespace(stream_data) 

394 stream_data.seek(-1, 1) 

395 objnum = NumberObject.read_from_stream(stream_data) 

396 read_non_whitespace(stream_data) 

397 stream_data.seek(-1, 1) 

398 offset = NumberObject.read_from_stream(stream_data) 

399 read_non_whitespace(stream_data) 

400 stream_data.seek(-1, 1) 

401 obj_index.append((int(objnum), int(offset))) 

402 

403 # Phase 2: Parse each object and cache it. 

404 target_obj: Union[int, PdfObject, str] = NullObject() 

405 found = False 

406 for i, (obj_num, obj_offset) in enumerate(obj_index): 

407 # Skip objects already in the cache. 

408 cached = self.cache_get_indirect_object(0, obj_num) 

409 if cached is not None: 

410 if obj_num == indirect_reference.idnum: 

411 target_obj = cached 

412 found = True 

413 continue 

414 

415 stream_data.seek(first_offset + obj_offset, 0) 

416 

417 # To cope with case where the 'pointer' is on a white space 

418 read_non_whitespace(stream_data) 

419 stream_data.seek(-1, 1) 

420 

421 try: 

422 obj = read_object(stream_data, self) 

423 except PdfStreamError as exc: 

424 # Stream object cannot be read. Normally, a critical error, but 

425 # Adobe Reader doesn't complain, so continue (in strict mode?) 

426 logger_warning( 

427 "Invalid stream (index %(index)d) within object %(obj_num)d 0: %(exc)s", 

428 source=__name__, 

429 index=i, 

430 obj_num=obj_num, 

431 exc=exc, 

432 ) 

433 if self.strict: # pragma: no cover 

434 raise PdfReadError( 

435 f"Cannot read object stream: {exc}" 

436 ) # pragma: no cover 

437 obj = NullObject() # pragma: no cover 

438 

439 # Only cache if this stream is the authoritative source for the object. 

440 # Incremental updates may override objects originally in the stream; 

441 # caching those stale versions would shadow the newer xref entry. 

442 authoritative_stm, _idx = self.xref_objStm.get(obj_num, (None, None)) 

443 if authoritative_stm == stmnum: 

444 self.cache_indirect_object(0, obj_num, obj) # type: ignore[arg-type] 

445 

446 if obj_num == indirect_reference.idnum: 

447 target_obj = obj 

448 found = True 

449 

450 if not found and self.strict: # pragma: no cover 

451 raise PdfReadError( 

452 "This is a fatal error in strict mode." 

453 ) # pragma: no cover 

454 return target_obj 

455 

456 def get_object( 

457 self, indirect_reference: Union[int, IndirectObject] 

458 ) -> Optional[PdfObject]: 

459 if isinstance(indirect_reference, int): 

460 indirect_reference = IndirectObject(indirect_reference, 0, self) 

461 retval = self.cache_get_indirect_object( 

462 indirect_reference.generation, indirect_reference.idnum 

463 ) 

464 if retval is not None: 

465 return retval 

466 if ( 

467 indirect_reference.generation == 0 

468 and indirect_reference.idnum in self.xref_objStm 

469 ): 

470 retval = self._get_object_from_stream(indirect_reference) # type: ignore 

471 elif ( 

472 indirect_reference.generation in self.xref 

473 and indirect_reference.idnum in self.xref[indirect_reference.generation] 

474 ): 

475 if self.xref_free_entry.get(indirect_reference.generation, {}).get( 

476 indirect_reference.idnum, False 

477 ): 

478 return NullObject() 

479 start = self.xref[indirect_reference.generation][indirect_reference.idnum] 

480 self.stream.seek(start, 0) 

481 try: 

482 idnum, generation = self.read_object_header(self.stream) 

483 if ( 

484 idnum != indirect_reference.idnum 

485 or generation != indirect_reference.generation 

486 ): 

487 raise PdfReadError("Not matching, we parse the file for it") 

488 except Exception: 

489 if hasattr(self.stream, "getbuffer"): 

490 buf = bytes(self.stream.getbuffer()) 

491 else: 

492 p = self.stream.tell() 

493 self.stream.seek(0, 0) 

494 buf = self.stream.read(-1) 

495 self.stream.seek(p, 0) 

496 m = re.search( 

497 rf"\s{indirect_reference.idnum}\s+{indirect_reference.generation}\s+obj".encode(), 

498 buf, 

499 ) 

500 if m is not None: 

501 logger_warning( 

502 "Object ID %(idnum)d,%(generation)d ref repaired", 

503 source=__name__, 

504 idnum=indirect_reference.idnum, 

505 generation=indirect_reference.generation, 

506 ) 

507 self.xref[indirect_reference.generation][ 

508 indirect_reference.idnum 

509 ] = (m.start(0) + 1) 

510 self.stream.seek(m.start(0) + 1) 

511 idnum, generation = self.read_object_header(self.stream) 

512 else: 

513 idnum = -1 

514 generation = -1 # exception will be raised below 

515 if idnum != indirect_reference.idnum and self.xref_index: 

516 # xref table probably had bad indexes due to not being zero-indexed 

517 if self.strict: 

518 raise PdfReadError( 

519 f"Expected object ID ({indirect_reference.idnum} {indirect_reference.generation}) " 

520 f"does not match actual ({idnum} {generation}); " 

521 "xref table not zero-indexed." 

522 ) 

523 # xref table is corrected in non-strict mode 

524 elif idnum != indirect_reference.idnum and self.strict: 

525 # some other problem 

526 raise PdfReadError( 

527 f"Expected object ID ({indirect_reference.idnum} {indirect_reference.generation}) " 

528 f"does not match actual ({idnum} {generation})." 

529 ) 

530 if self.strict: 

531 assert generation == indirect_reference.generation 

532 

533 current_object = (indirect_reference.idnum, indirect_reference.generation) 

534 if current_object in self._known_objects: 

535 raise LimitReachedError(f"Detected loop with self reference for {indirect_reference!r}.") 

536 self._known_objects.add(current_object) 

537 retval = read_object(self.stream, self) # type: ignore[assignment] 

538 self._known_objects.remove(current_object) 

539 

540 # override encryption is used for the /Encrypt dictionary 

541 if not self._override_encryption and self._encryption is not None: 

542 # if we don't have the encryption key: 

543 if not self._encryption.is_decrypted(): 

544 raise FileNotDecryptedError("File has not been decrypted") 

545 # otherwise, decrypt here... 

546 retval = cast(PdfObject, retval) 

547 retval = self._encryption.decrypt_object( 

548 retval, indirect_reference.idnum, indirect_reference.generation, 

549 strict=self.strict, 

550 ) 

551 else: 

552 if hasattr(self.stream, "getbuffer"): 

553 buf = bytes(self.stream.getbuffer()) 

554 else: 

555 p = self.stream.tell() 

556 self.stream.seek(0, 0) 

557 buf = self.stream.read(-1) 

558 self.stream.seek(p, 0) 

559 m = re.search( 

560 rf"\s{indirect_reference.idnum}\s+{indirect_reference.generation}\s+obj".encode(), 

561 buf, 

562 ) 

563 if m is not None: 

564 logger_warning( 

565 "Object %(idnum)d %(generation)d found", 

566 source=__name__, 

567 idnum=indirect_reference.idnum, 

568 generation=indirect_reference.generation, 

569 ) 

570 if indirect_reference.generation not in self.xref: 

571 self.xref[indirect_reference.generation] = {} 

572 self.xref[indirect_reference.generation][indirect_reference.idnum] = ( 

573 m.start(0) + 1 

574 ) 

575 self.stream.seek(m.end(0) + 1) 

576 skip_over_whitespace(self.stream) 

577 self.stream.seek(-1, 1) 

578 retval = read_object(self.stream, self) # type: ignore[assignment] 

579 

580 # override encryption is used for the /Encrypt dictionary 

581 if not self._override_encryption and self._encryption is not None: 

582 # if we don't have the encryption key: 

583 if not self._encryption.is_decrypted(): 

584 raise FileNotDecryptedError("File has not been decrypted") 

585 # otherwise, decrypt here... 

586 retval = cast(PdfObject, retval) 

587 retval = self._encryption.decrypt_object( 

588 retval, indirect_reference.idnum, indirect_reference.generation, 

589 strict=self.strict, 

590 ) 

591 else: 

592 logger_warning( 

593 "Object %(idnum)d %(generation)d not defined.", 

594 source=__name__, 

595 idnum=indirect_reference.idnum, 

596 generation=indirect_reference.generation, 

597 ) 

598 if self.strict: 

599 raise PdfReadError("Could not find object.") 

600 # For ObjStm objects, _get_object_from_stream already cached 

601 # the result during batch parsing; skip the redundant cache write 

602 # to avoid "Overwriting cache" warnings. For non-ObjStm objects 

603 # (including encrypted ones that need decrypted values cached), 

604 # always write. 

605 if not ( 

606 indirect_reference.generation == 0 

607 and indirect_reference.idnum in self.xref_objStm 

608 ): 

609 self.cache_indirect_object( 

610 indirect_reference.generation, indirect_reference.idnum, retval 

611 ) 

612 return retval 

613 

614 def read_object_header(self, stream: StreamType) -> tuple[int, int]: 

615 # Should never be necessary to read out whitespace, since the 

616 # cross-reference table should put us in the right spot to read the 

617 # object header. In reality some files have stupid cross-reference 

618 # tables that are off by whitespace bytes. 

619 skip_over_comment(stream) 

620 extra = skip_over_whitespace(stream) 

621 stream.seek(-1, 1) 

622 idnum = read_until_whitespace(stream) 

623 extra |= skip_over_whitespace(stream) 

624 stream.seek(-1, 1) 

625 generation = read_until_whitespace(stream) 

626 extra |= skip_over_whitespace(stream) 

627 stream.seek(-1, 1) 

628 

629 # although it's not used, it might still be necessary to read 

630 _obj = stream.read(3) 

631 

632 read_non_whitespace(stream) 

633 stream.seek(-1, 1) 

634 if extra and self.strict: 

635 logger_warning( 

636 "Superfluous whitespace found in object header %(idnum)r %(generation)r", 

637 source=__name__, 

638 idnum=idnum, 

639 generation=generation, 

640 ) 

641 return int(idnum), int(generation) 

642 

643 def cache_get_indirect_object( 

644 self, generation: int, idnum: int 

645 ) -> Optional[PdfObject]: 

646 try: 

647 return self.resolved_objects.get((generation, idnum)) 

648 except RecursionError: 

649 raise PdfReadError("Maximum recursion depth reached.") 

650 

651 def cache_indirect_object( 

652 self, generation: int, idnum: int, obj: Optional[PdfObject] 

653 ) -> Optional[PdfObject]: 

654 if (generation, idnum) in self.resolved_objects: 

655 msg = "Overwriting cache for %(generation)d %(idnum)d" 

656 values = {"generation": generation, "idnum": idnum} 

657 if self.strict: 

658 raise PdfReadError(msg % values) 

659 logger_warning(msg, source=__name__, **values) 

660 self.resolved_objects[(generation, idnum)] = obj 

661 if obj is not None: 

662 obj.indirect_reference = IndirectObject(idnum, generation, self) 

663 return obj 

664 

665 def _replace_object(self, indirect_reference: IndirectObject, obj: PdfObject) -> PdfObject: 

666 # function reserved for future development 

667 if indirect_reference.pdf != self: 

668 raise ValueError("Cannot update PdfReader with external object") 

669 if (indirect_reference.generation, indirect_reference.idnum) not in self.resolved_objects: 

670 raise ValueError("Cannot find referenced object") 

671 self.resolved_objects[(indirect_reference.generation, indirect_reference.idnum)] = obj 

672 obj.indirect_reference = indirect_reference 

673 return obj 

674 

675 def read(self, stream: StreamType) -> None: 

676 """ 

677 Read and process the PDF stream, extracting necessary data. 

678 

679 Args: 

680 stream: The PDF file stream. 

681 

682 """ 

683 self._basic_validation(stream) 

684 self._find_eof_marker(stream) 

685 startxref = self._find_startxref_pos(stream) 

686 self._startxref = startxref 

687 

688 # check and eventually correct the startxref only if not strict 

689 xref_issue_nr = self._get_xref_issues(stream, startxref) 

690 if xref_issue_nr != 0: 

691 if self.strict and xref_issue_nr: 

692 raise PdfReadError("Broken xref table") 

693 logger_warning( 

694 "incorrect startxref pointer(%(xref_issue_nr)d)", 

695 source=__name__, 

696 xref_issue_nr=xref_issue_nr, 

697 ) 

698 

699 # read all cross-reference tables and their trailers 

700 self._read_xref_tables_and_trailers(stream, startxref, xref_issue_nr) 

701 

702 # if not zero-indexed, verify that the table is correct; change it if necessary 

703 if self.xref_index and not self.strict: 

704 loc = stream.tell() 

705 for gen, xref_entry in self.xref.items(): 

706 if gen == 65535: 

707 continue 

708 xref_k = sorted( 

709 xref_entry.keys() 

710 ) # ensure ascending to prevent damage 

711 for id in xref_k: 

712 stream.seek(xref_entry[id], 0) 

713 try: 

714 pid, _pgen = self.read_object_header(stream) 

715 except ValueError: 

716 self._rebuild_xref_table(stream) 

717 break 

718 if pid == id - self.xref_index: 

719 # fixing index item per item is required for revised PDF. 

720 self.xref[gen][pid] = self.xref[gen][id] 

721 del self.xref[gen][id] 

722 # if not, then either it's just plain wrong, or the 

723 # non-zero-index is actually correct 

724 stream.seek(loc, 0) # return to where it was 

725 

726 # remove wrong objects (not pointing to correct structures) - cf #2326 

727 if not self.strict: 

728 loc = stream.tell() 

729 for gen, xref_entry in self.xref.items(): 

730 if gen == 65535: 

731 continue 

732 ids = list(xref_entry.keys()) 

733 for id in ids: 

734 stream.seek(xref_entry[id], 0) 

735 try: 

736 self.read_object_header(stream) 

737 except ValueError: 

738 logger_warning( 

739 "Ignoring wrong pointing object %(id)d %(gen)d (offset %(offset)d)", 

740 source=__name__, 

741 id=id, 

742 gen=gen, 

743 offset=xref_entry[id], 

744 ) 

745 del xref_entry[id] # we can delete the id, we are parsing ids 

746 stream.seek(loc, 0) # return to where it was 

747 

748 def _basic_validation(self, stream: StreamType) -> None: 

749 """Ensure the stream is valid and not empty.""" 

750 stream.seek(0, os.SEEK_SET) 

751 try: 

752 header_byte = stream.read(5) 

753 except UnicodeDecodeError: 

754 raise UnsupportedOperation("cannot read header") 

755 if header_byte == b"": 

756 raise EmptyFileError("Cannot read an empty file") 

757 if header_byte != b"%PDF-": 

758 if self.strict: 

759 raise PdfReadError( 

760 f"PDF starts with '{header_byte.decode('utf8')}', " 

761 "but '%PDF-' expected" 

762 ) 

763 logger_warning("invalid pdf header: %(header_byte)r", source=__name__, header_byte=header_byte) 

764 stream.seek(0, os.SEEK_END) 

765 

766 def _find_eof_marker(self, stream: StreamType) -> None: 

767 """ 

768 Jump to the %%EOF marker. 

769 

770 According to the specs, the %%EOF marker should be at the very end of 

771 the file. Hence for standard-compliant PDF documents this function will 

772 read only the last part (DEFAULT_BUFFER_SIZE). 

773 """ 

774 HEADER_SIZE = 8 # to parse whole file, Header is e.g. '%PDF-1.6' 

775 line = b"" 

776 first = True 

777 while not line.startswith(b"%%EOF"): 

778 if line != b"" and first: 

779 if any( 

780 line.strip().endswith(tr) for tr in (b"%%EO", b"%%E", b"%%", b"%") 

781 ): 

782 # Consider the file as truncated while 

783 # having enough confidence to carry on. 

784 logger_warning("EOF marker seems truncated", source=__name__) 

785 break 

786 first = False 

787 if b"startxref" in line: 

788 logger_warning( 

789 "CAUTION: startxref found while searching for %%EOF. " 

790 "The file might be truncated and some data might not be read.", 

791 source=__name__, 

792 ) 

793 if stream.tell() < HEADER_SIZE: 

794 if self.strict: 

795 raise PdfReadError("EOF marker not found") 

796 logger_warning("EOF marker not found", source=__name__) 

797 line = read_previous_line(stream) 

798 

799 def _find_startxref_pos(self, stream: StreamType) -> int: 

800 """ 

801 Find startxref entry - the location of the xref table. 

802 

803 Args: 

804 stream: 

805 

806 Returns: 

807 The bytes offset 

808 

809 """ 

810 line = read_previous_line(stream) 

811 try: 

812 startxref = int(line) 

813 except ValueError: 

814 # 'startxref' may be on the same line as the location 

815 if not line.startswith(b"startxref"): 

816 raise PdfReadError("startxref not found") 

817 startxref = int(line[9:].strip()) 

818 logger_warning("startxref on same line as offset", source=__name__) 

819 else: 

820 line = read_previous_line(stream) 

821 if not line.startswith(b"startxref"): 

822 # The 'startxref' keyword expected just above the offset is 

823 # missing or corrupt (for example a truncated 'tartxref'). 

824 # Some producers append a broken trailing cross-reference 

825 # pointer while an earlier, intact 'startxref' from a previous 

826 # revision is still present. Recovering from this violates the 

827 # standard, so only attempt it in non-strict mode (#3238). 

828 if self.strict: 

829 raise PdfReadError("startxref not found") 

830 startxref = self._find_previous_startxref_pos(stream) 

831 return startxref 

832 

833 # Upper bound on the number of lines _find_previous_startxref_pos scans 

834 # backwards while recovering a corrupt trailing startxref pointer. Kept 

835 # fixed and non-configurable so a crafted file cannot trigger an unbounded 

836 # backwards scan. 

837 _MAX_STARTXREF_RECOVERY_LINES = 1000 

838 

839 @classmethod 

840 def _find_previous_startxref_pos(cls, stream: StreamType) -> int: 

841 """ 

842 Recover the most recent intact ``startxref`` pointer by scanning 

843 backwards from the current position. 

844 

845 This is used as a fallback when the ``startxref`` keyword belonging to 

846 the final ``%%EOF`` is corrupt (#3238). The offset always appears on 

847 the line directly below the keyword, so the value read immediately 

848 before encountering ``startxref`` (while moving backwards) is returned. 

849 At most ``_MAX_STARTXREF_RECOVERY_LINES`` lines are inspected. 

850 

851 Args: 

852 stream: The PDF byte stream, positioned just above the corrupt 

853 trailing pointer. 

854 

855 Returns: 

856 The bytes offset of the recovered ``startxref``. 

857 

858 """ 

859 offset: Optional[int] = None 

860 for _ in range(cls._MAX_STARTXREF_RECOVERY_LINES): 

861 if stream.tell() <= 0: 

862 break 

863 line = read_previous_line(stream) 

864 if not line.startswith(b"startxref"): 

865 try: 

866 offset = int(line) 

867 except ValueError: 

868 offset = None 

869 continue 

870 if len(line) > 9: 

871 # 'startxref' on the same line as the offset 

872 return int(line[9:].strip()) 

873 if offset is not None: 

874 logger_warning( 

875 "found startxref pointing to a previous revision after " 

876 "a corrupt one", 

877 source=__name__, 

878 ) 

879 return offset 

880 break 

881 raise PdfReadError("startxref not found") 

882 

883 def _load_recovery_cache(self, data: bytes) -> dict[int, tuple[int, int]]: 

884 cache = {} 

885 for object_number, generation_number, object_start in self._find_pdf_objects(data): 

886 if object_number in cache: 

887 # Always use the first match. 

888 continue 

889 cache[object_number] = (object_start, generation_number) 

890 return cache 

891 

892 def _read_standard_xref_table(self, stream: StreamType) -> None: 

893 # standard cross-reference table 

894 ref = stream.read(3) 

895 if ref != b"ref": 

896 raise PdfReadError("xref table read error") 

897 read_non_whitespace(stream) 

898 stream.seek(-1, 1) 

899 first_time = True # check if the first time looking at the xref table 

900 recovery_cache: Optional[dict[int, tuple[int, int]]] = None 

901 while True: 

902 num = cast(int, read_object(stream, self)) 

903 if first_time and num != 0: 

904 self.xref_index = num 

905 if self.strict: 

906 logger_warning( 

907 "Xref table not zero-indexed. ID numbers for objects will be corrected.", 

908 source=__name__, 

909 ) 

910 # if table not zero indexed, could be due to error from when PDF was created 

911 # which will lead to mismatched indices later on, only warned and corrected if self.strict==True 

912 first_time = False 

913 read_non_whitespace(stream) 

914 stream.seek(-1, 1) 

915 size = cast(int, read_object(stream, self)) 

916 if not isinstance(size, int): 

917 logger_warning( 

918 "Invalid/Truncated xref table. Rebuilding it.", 

919 source=__name__, 

920 ) 

921 self._rebuild_xref_table(stream) 

922 stream.read() 

923 return 

924 read_non_whitespace(stream) 

925 stream.seek(-1, 1) 

926 cnt = 0 

927 while cnt < size: 

928 line = stream.read(20) 

929 if not line: 

930 raise PdfReadError("Unexpected empty line in Xref table.") 

931 

932 # It's very clear in section 3.4.3 of the PDF spec 

933 # that all cross-reference table lines are a fixed 

934 # 20 bytes (as of PDF 1.7). However, some files have 

935 # 21-byte entries (or more) due to the use of \r\n 

936 # (CRLF) EOL's. Detect that case, and adjust the line 

937 # until it does not begin with a \r (CR) or \n (LF). 

938 while line[0] in b"\x0D\x0A": 

939 stream.seek(-20 + 1, 1) 

940 line = stream.read(20) 

941 

942 # On the other hand, some malformed PDF files 

943 # use a single character EOL without a preceding 

944 # space. Detect that case, and seek the stream 

945 # back one character (0-9 means we've bled into 

946 # the next xref entry, t means we've bled into the 

947 # text "trailer"): 

948 if line[-1] in b"0123456789t": 

949 stream.seek(-1, 1) 

950 

951 try: 

952 offset_b, generation_b = line[:16].split(b" ") 

953 entry_type_b = line[17:18] 

954 

955 offset, generation = int(offset_b), int(generation_b) 

956 except Exception: 

957 if hasattr(stream, "getbuffer"): 

958 buf = bytes(stream.getbuffer()) 

959 else: 

960 p = stream.tell() 

961 stream.seek(0, 0) 

962 buf = stream.read(-1) 

963 stream.seek(p) 

964 

965 if recovery_cache is None: 

966 recovery_cache = self._load_recovery_cache(buf) 

967 

968 if num not in recovery_cache: 

969 logger_warning( 

970 "entry %(num)d in Xref table invalid; object not found", 

971 source=__name__, 

972 num=num, 

973 ) 

974 generation = 65535 

975 offset = -1 

976 entry_type_b = b"f" 

977 else: 

978 logger_warning( 

979 "entry %(num)d in Xref table invalid but object found", 

980 source=__name__, 

981 num=num, 

982 ) 

983 generation, offset = recovery_cache[num] 

984 entry_type_b = b"n" 

985 

986 if generation not in self.xref: 

987 self.xref[generation] = {} 

988 self.xref_free_entry[generation] = {} 

989 if num in self.xref[generation]: 

990 # It really seems like we should allow the last 

991 # xref table in the file to override previous 

992 # ones. Since we read the file backwards, assume 

993 # any existing key is already set correctly. 

994 pass 

995 else: 

996 if entry_type_b == b"n": 

997 self.xref[generation][num] = offset 

998 try: 

999 self.xref_free_entry[generation][num] = entry_type_b == b"f" 

1000 except Exception: 

1001 pass 

1002 try: 

1003 self.xref_free_entry[65535][num] = entry_type_b == b"f" 

1004 except Exception: 

1005 pass 

1006 cnt += 1 

1007 num += 1 

1008 read_non_whitespace(stream) 

1009 stream.seek(-1, 1) 

1010 # Skip any PDF comments between xref entries and the trailer 

1011 # keyword. Some PDF producers (e.g. Vectorizer.AI) insert 

1012 # comments here which are legal per the PDF spec (§7.2.3). 

1013 while stream.read(1) == b"%": 

1014 stream.seek(-1, 1) 

1015 skip_over_comment(stream) 

1016 read_non_whitespace(stream) 

1017 stream.seek(-1, 1) 

1018 stream.seek(-1, 1) 

1019 trailer_tag = stream.read(7) 

1020 if trailer_tag != b"trailer": 

1021 # more xrefs! 

1022 stream.seek(-7, 1) 

1023 else: 

1024 break 

1025 

1026 def _read_xref_tables_and_trailers( 

1027 self, stream: StreamType, startxref: Optional[int], xref_issue_nr: int 

1028 ) -> None: 

1029 """Read the cross-reference tables and trailers in the PDF stream.""" 

1030 self.xref = {} 

1031 self.xref_free_entry = {} 

1032 self.xref_objStm = {} 

1033 self.trailer = DictionaryObject() 

1034 visited_xref_offsets: set[int] = set() 

1035 while startxref is not None: 

1036 # Detect circular /Prev references in the xref chain 

1037 if startxref in visited_xref_offsets: 

1038 logger_warning( 

1039 "Circular xref chain detected at offset %(startxref)d, stopping", 

1040 source=__name__, 

1041 startxref=startxref, 

1042 ) 

1043 break 

1044 visited_xref_offsets.add(startxref) 

1045 # load the xref table 

1046 stream.seek(startxref, 0) 

1047 x = stream.read(1) 

1048 if x in b"\r\n": 

1049 x = stream.read(1) 

1050 if x == b"x": 

1051 startxref = self._read_xref(stream) 

1052 elif xref_issue_nr: 

1053 try: 

1054 self._rebuild_xref_table(stream) 

1055 break 

1056 except Exception: 

1057 xref_issue_nr = 0 

1058 elif x.isdigit(): 

1059 try: 

1060 xrefstream = self._read_pdf15_xref_stream(stream) 

1061 except Exception as e: 

1062 if TK.ROOT in self.trailer: 

1063 logger_warning( 

1064 "Previous trailer cannot be read: %(args)s", 

1065 source=__name__, 

1066 args=e.args, 

1067 ) 

1068 break 

1069 raise PdfReadError(f"Trailer cannot be read: {e!s}") 

1070 self._process_xref_stream(xrefstream) 

1071 if "/Prev" in xrefstream: 

1072 startxref = cast(int, xrefstream["/Prev"]) 

1073 else: 

1074 break 

1075 else: 

1076 startxref = self._read_xref_other_error(stream, startxref) 

1077 

1078 def _process_xref_stream(self, xrefstream: DictionaryObject) -> None: 

1079 """Process and handle the xref stream.""" 

1080 trailer_keys = TK.ROOT, TK.ENCRYPT, TK.INFO, TK.ID, TK.SIZE 

1081 for key in trailer_keys: 

1082 if key in xrefstream and key not in self.trailer: 

1083 self.trailer[NameObject(key)] = xrefstream.raw_get(key) 

1084 if "/XRefStm" in xrefstream: 

1085 p = self.stream.tell() 

1086 self.stream.seek(cast(int, xrefstream["/XRefStm"]) + 1, 0) 

1087 self._read_pdf15_xref_stream(self.stream) 

1088 self.stream.seek(p, 0) 

1089 

1090 def _read_xref(self, stream: StreamType) -> Optional[int]: 

1091 self._read_standard_xref_table(stream) 

1092 if stream.read(1) == b"": 

1093 return None 

1094 stream.seek(-1, 1) 

1095 read_non_whitespace(stream) 

1096 stream.seek(-1, 1) 

1097 new_trailer = cast(dict[str, Any], read_object(stream, self)) 

1098 for key, value in new_trailer.items(): 

1099 if key not in self.trailer: 

1100 self.trailer[key] = value 

1101 if "/XRefStm" in new_trailer: 

1102 p = stream.tell() 

1103 stream.seek(cast(int, new_trailer["/XRefStm"]) + 1, 0) 

1104 try: 

1105 self._read_pdf15_xref_stream(stream) 

1106 except Exception: 

1107 logger_warning( 

1108 "XRef object at %(xref_stm)d can not be read, some object may be missing", 

1109 source=__name__, 

1110 xref_stm=int(new_trailer["/XRefStm"]), 

1111 ) 

1112 stream.seek(p, 0) 

1113 if "/Prev" in new_trailer: 

1114 return cast(int, new_trailer["/Prev"]) 

1115 return None 

1116 

1117 def _read_xref_other_error( 

1118 self, stream: StreamType, startxref: int 

1119 ) -> Optional[int]: 

1120 # some PDFs have /Prev=0 in the trailer, instead of no /Prev 

1121 if startxref == 0: 

1122 if self.strict: 

1123 raise PdfReadError( 

1124 "/Prev=0 in the trailer (try opening with strict=False)" 

1125 ) 

1126 logger_warning( 

1127 "/Prev=0 in the trailer - assuming there is no previous xref table", 

1128 source=__name__, 

1129 ) 

1130 return None 

1131 # bad xref character at startxref. Let's see if we can find 

1132 # the xref table nearby, as we've observed this error with an 

1133 # off-by-one before. 

1134 stream.seek(-11, 1) 

1135 tmp = stream.read(20) 

1136 xref_loc = tmp.find(b"xref") 

1137 if xref_loc != -1: 

1138 startxref -= 10 - xref_loc 

1139 return startxref 

1140 # No explicit xref table, try finding a cross-reference stream. 

1141 stream.seek(startxref, 0) 

1142 for look in range(25): # value extended to cope with more linearized files 

1143 if stream.read(1).isdigit(): 

1144 # This is not a standard PDF, consider adding a warning 

1145 startxref += look 

1146 return startxref 

1147 # no xref table found at specified location 

1148 if "/Root" in self.trailer and not self.strict: 

1149 # if Root has been already found, just raise warning 

1150 logger_warning("Invalid parent xref., rebuild xref", source=__name__) 

1151 try: 

1152 self._rebuild_xref_table(stream) 

1153 return None 

1154 except Exception: 

1155 raise PdfReadError("Cannot rebuild xref") 

1156 raise PdfReadError("Could not find xref table at specified location") 

1157 

1158 def _sanitize_pdf15_xref_stream_index_pairs( 

1159 self, index_pairs: list[int], entry_sizes: list[int], xref_stream: ContentStream 

1160 ) -> list[int]: 

1161 # `entry_sizes` holds the byte widths for the entries. Summing determines the total number of bytes per entry. 

1162 # We expect up to 3 values. `min_entry_bytes` will be the smallest plausible size of one xref entry. 

1163 min_entry_bytes = sum(int(entry_sizes[i]) for i in range(min(len(entry_sizes), 3))) 

1164 if min_entry_bytes == 0: 

1165 message = "Cross-reference stream encodes no entry data." 

1166 if self.strict: 

1167 raise PdfStreamError(message) 

1168 logger_warning(message, source=__name__) 

1169 return [] 

1170 

1171 # maximum number of entries that could physically fit 

1172 max_entries = len(xref_stream.get_data()) // min_entry_bytes + 1 

1173 

1174 result = [] 

1175 total = 0 

1176 

1177 for index, pair_value in enumerate(index_pairs): 

1178 pair_value_int = int(pair_value) 

1179 

1180 # `index_pairs` has the format `[start0, count0, start1, count1, ...]` 

1181 # Only modify the counts here, but keep the start values. 

1182 if index % 2 == 1: 

1183 if total + pair_value_int > max_entries: 

1184 if self.strict: 

1185 raise LimitReachedError( 

1186 f"Total XRef entries {total + pair_value_int} exceed maximum allowed value {max_entries}." 

1187 ) 

1188 new_v = max(0, max_entries - total) 

1189 logger_warning( 

1190 "Clamping XRef count from %(old_count)d to %(new_count)d to fit stream size.", 

1191 source=__name__, 

1192 old_count=pair_value_int, 

1193 new_count=new_v, 

1194 ) 

1195 pair_value_int = new_v 

1196 

1197 total += pair_value_int 

1198 

1199 result.append(pair_value_int) 

1200 

1201 return result 

1202 

1203 def _read_pdf15_xref_stream( 

1204 self, stream: StreamType 

1205 ) -> Union[ContentStream, EncodedStreamObject, DecodedStreamObject]: 

1206 """Read the cross-reference stream for PDF 1.5+.""" 

1207 stream.seek(-1, 1) 

1208 stream_idnum, stream_generation = self.read_object_header(stream) 

1209 xref_stream = cast(ContentStream, read_object(stream, self)) 

1210 if cast(str, xref_stream["/Type"]) != "/XRef": 

1211 raise PdfReadError(f"Unexpected type {xref_stream['/Type']!r}") 

1212 self.cache_indirect_object(stream_generation, stream_idnum, xref_stream) 

1213 

1214 # Index pairs specify the subsections in the dictionary. 

1215 # If none, create one subsection that spans everything. 

1216 if "/Size" not in xref_stream: 

1217 # According to table 17 of the PDF 2.0 specification, this key is required. 

1218 raise PdfReadError(f"Size missing from XRef stream {xref_stream!r}!") 

1219 index_pairs = xref_stream.get("/Index", [0, xref_stream["/Size"]]) 

1220 

1221 entry_sizes = cast(list[int], xref_stream.get("/W")) 

1222 assert len(entry_sizes) >= 3 

1223 if self.strict and len(entry_sizes) > 3: 

1224 raise PdfReadError(f"Too many entry sizes: {entry_sizes}") 

1225 index_pairs = self._sanitize_pdf15_xref_stream_index_pairs( 

1226 index_pairs=index_pairs, entry_sizes=entry_sizes, xref_stream=xref_stream 

1227 ) 

1228 

1229 stream_data = BytesIO(xref_stream.get_data()) 

1230 

1231 def get_entry(i: int) -> Union[int, tuple[int, ...]]: 

1232 # Reads the correct number of bytes for each entry. See the 

1233 # discussion of the W parameter in PDF spec table 17. 

1234 if entry_sizes[i] > 0: 

1235 d = stream_data.read(entry_sizes[i]) 

1236 return convert_to_int(d, entry_sizes[i]) 

1237 

1238 # PDF Spec Table 17: A value of zero for an element in the 

1239 # W array indicates...the default value shall be used 

1240 if i == 0: 

1241 return 1 # First value defaults to 1 

1242 return 0 

1243 

1244 def used_before(num: int, generation: Union[int, tuple[int, ...]]) -> bool: 

1245 # We move backwards through the xrefs, don't replace any. 

1246 return num in self.xref.get(generation, []) or num in self.xref_objStm # type: ignore[arg-type] 

1247 

1248 # Iterate through each subsection 

1249 self._read_xref_subsections(index_pairs, get_entry, used_before) 

1250 return xref_stream 

1251 

1252 @staticmethod 

1253 def _get_xref_issues(stream: StreamType, startxref: int) -> int: 

1254 """ 

1255 Return an int which indicates an issue. 0 means there is no issue. 

1256 

1257 Args: 

1258 stream: 

1259 startxref: 

1260 

1261 Returns: 

1262 0 means no issue, other values represent specific issues. 

1263 

1264 """ 

1265 if startxref == 0: 

1266 return 4 

1267 

1268 stream.seek(startxref - 1, 0) # -1 to check character before 

1269 line = stream.read(1) 

1270 if line == b"j": 

1271 line = stream.read(1) 

1272 if line not in b"\r\n \t": 

1273 return 1 

1274 line = stream.read(4) 

1275 if line != b"xref": 

1276 # not a xref so check if it is an XREF object 

1277 line = b"" 

1278 while line in b"0123456789 \t": 

1279 line = stream.read(1) 

1280 if line == b"": 

1281 return 2 

1282 line += stream.read(2) # 1 char already read, +2 to check "obj" 

1283 if line.lower() != b"obj": 

1284 return 3 

1285 return 0 

1286 

1287 @classmethod 

1288 def _find_pdf_objects(cls, data: bytes) -> Iterable[tuple[int, int, int]]: 

1289 index = 0 

1290 ord_0 = ord("0") 

1291 ord_9 = ord("9") 

1292 while True: 

1293 index = data.find(b" obj", index) 

1294 if index == -1: 

1295 return 

1296 

1297 index_before_space = index - 1 

1298 

1299 # Skip whitespace backwards 

1300 while index_before_space >= 0 and data[index_before_space] in WHITESPACES_AS_BYTES: 

1301 index_before_space -= 1 

1302 

1303 # Read generation number 

1304 generation_end = index_before_space + 1 

1305 while index_before_space >= 0 and ord_0 <= data[index_before_space] <= ord_9: 

1306 index_before_space -= 1 

1307 generation_start = index_before_space + 1 

1308 

1309 # Skip whitespace 

1310 while index_before_space >= 0 and data[index_before_space] in WHITESPACES_AS_BYTES: 

1311 index_before_space -= 1 

1312 

1313 # Read object number 

1314 object_end = index_before_space + 1 

1315 while index_before_space >= 0 and ord_0 <= data[index_before_space] <= ord_9: 

1316 index_before_space -= 1 

1317 object_start = index_before_space + 1 

1318 

1319 # Validate 

1320 if object_start < object_end and generation_start < generation_end: 

1321 object_number = int(data[object_start:object_end]) 

1322 generation_number = int(data[generation_start:generation_end]) 

1323 

1324 yield object_number, generation_number, object_start 

1325 

1326 index += 4 # len(b" obj") 

1327 

1328 @classmethod 

1329 def _find_pdf_trailers(cls, data: bytes) -> Iterable[int]: 

1330 index = 0 

1331 data_length = len(data) 

1332 while True: 

1333 index = data.find(b"trailer", index) 

1334 if index == -1: 

1335 return 

1336 

1337 index_after_trailer = index + 7 # len(b"trailer") 

1338 

1339 # Skip whitespace 

1340 while index_after_trailer < data_length and data[index_after_trailer] in WHITESPACES_AS_BYTES: 

1341 index_after_trailer += 1 

1342 

1343 # Must be dictionary start 

1344 if index_after_trailer + 1 < data_length and data[index_after_trailer:index_after_trailer+2] == b"<<": 

1345 yield index_after_trailer # offset of '<<' 

1346 

1347 index += 7 # len(b"trailer") 

1348 

1349 def _rebuild_xref_table(self, stream: StreamType) -> None: 

1350 self.xref = {} 

1351 stream.seek(0, 0) 

1352 stream_data = stream.read(-1) 

1353 

1354 for object_number, generation_number, object_start in self._find_pdf_objects(stream_data): 

1355 if generation_number not in self.xref: 

1356 self.xref[generation_number] = {} 

1357 self.xref[generation_number][object_number] = object_start 

1358 

1359 logger_warning("parsing for Object Streams", source=__name__) 

1360 for generation_number in self.xref: 

1361 for object_number in self.xref[generation_number]: 

1362 # get_object in manual 

1363 stream.seek(self.xref[generation_number][object_number], 0) 

1364 try: 

1365 _ = self.read_object_header(stream) 

1366 obj = cast(StreamObject, read_object(stream, self)) 

1367 if obj.get("/Type", "") != "/ObjStm": 

1368 continue 

1369 object_stream = BytesIO(obj.get_data()) 

1370 actual_count = 0 

1371 while True: 

1372 current = read_until_whitespace(object_stream) 

1373 if not current.isdigit(): 

1374 break 

1375 inner_object_number = int(current) 

1376 skip_over_whitespace(object_stream) 

1377 object_stream.seek(-1, 1) 

1378 current = read_until_whitespace(object_stream) 

1379 if not current.isdigit(): # pragma: no cover 

1380 break # pragma: no cover 

1381 inner_generation_number = int(current) 

1382 self.xref_objStm[inner_object_number] = (object_number, inner_generation_number) 

1383 actual_count += 1 

1384 expected_count = cast(int, obj["/N"]) 

1385 if actual_count != expected_count: # pragma: no cover 

1386 logger_warning( # pragma: no cover 

1387 ( 

1388 "found %(actual_count)d objects within " 

1389 "Object(%(object_number)d,%(generation_number)d) " 

1390 "whereas %(expected)d expected" 

1391 ), 

1392 source=__name__, 

1393 actual_count=actual_count, 

1394 object_number=object_number, 

1395 generation_number=generation_number, 

1396 expected=expected_count, 

1397 ) 

1398 except Exception: # could be multiple causes 

1399 pass 

1400 

1401 stream.seek(0, 0) 

1402 for position in self._find_pdf_trailers(stream_data): 

1403 stream.seek(position, 0) 

1404 new_trailer = cast(dict[Any, Any], read_object(stream, self)) 

1405 # Here, we are parsing the file from start to end, the new data have to erase the existing. 

1406 for key, value in new_trailer.items(): 

1407 self.trailer[key] = value 

1408 

1409 def _read_xref_subsections( 

1410 self, 

1411 idx_pairs: list[int], 

1412 get_entry: Callable[[int], Union[int, tuple[int, ...]]], 

1413 used_before: Callable[[int, Union[int, tuple[int, ...]]], bool], 

1414 ) -> None: 

1415 """Read and process the subsections of the xref.""" 

1416 for start, size in self._pairs(idx_pairs): 

1417 # The subsections must increase 

1418 for num in range(start, start + size): 

1419 # The first entry is the type 

1420 xref_type = get_entry(0) 

1421 # The rest of the elements depend on the xref_type 

1422 if xref_type == 0: 

1423 # linked list of free objects 

1424 next_free_object = get_entry(1) # noqa: F841 

1425 next_generation = get_entry(2) # noqa: F841 

1426 elif xref_type == 1: 

1427 # objects that are in use but are not compressed 

1428 byte_offset = get_entry(1) 

1429 generation = get_entry(2) 

1430 if generation not in self.xref: 

1431 self.xref[generation] = {} # type: ignore[index] 

1432 if not used_before(num, generation): 

1433 self.xref[generation][num] = byte_offset # type: ignore[index] 

1434 elif xref_type == 2: 

1435 # compressed objects 

1436 objstr_num = get_entry(1) 

1437 obstr_idx = get_entry(2) 

1438 generation = 0 # PDF spec table 18, generation is 0 

1439 if not used_before(num, generation): 

1440 self.xref_objStm[num] = (objstr_num, obstr_idx) 

1441 elif self.strict: 

1442 raise PdfReadError(f"Unknown xref type: {xref_type}") 

1443 

1444 def _pairs(self, array: list[int]) -> Iterable[tuple[int, int]]: 

1445 """Iterate over pairs in the array.""" 

1446 i = 0 

1447 while i + 1 < len(array): 

1448 yield array[i], array[i + 1] 

1449 i += 2 

1450 

1451 def decrypt(self, password: Union[str, bytes]) -> PasswordType: 

1452 """ 

1453 When using an encrypted / secured PDF file with the PDF Standard 

1454 encryption handler, this function will allow the file to be decrypted. 

1455 It checks the given password against the document's user password and 

1456 owner password, and then stores the resulting decryption key if either 

1457 password is correct. 

1458 

1459 It does not matter which password was matched. Both passwords provide 

1460 the correct decryption key that will allow the document to be used with 

1461 this library. 

1462 

1463 Args: 

1464 password: The password to match. 

1465 

1466 Returns: 

1467 An indicator if the document was decrypted and whether it was the 

1468 owner password or the user password. 

1469 

1470 """ 

1471 if not self._encryption: 

1472 raise PdfReadError("Not encrypted file") 

1473 # TODO: raise Exception for wrong password 

1474 return self._encryption.verify(password, strict=self.strict) 

1475 

1476 @property 

1477 def is_encrypted(self) -> bool: 

1478 """ 

1479 Read-only boolean property showing whether this PDF file is encrypted. 

1480 

1481 Note that this property, if true, will remain true even after the 

1482 :meth:`decrypt()<pypdf.PdfReader.decrypt>` method is called. 

1483 """ 

1484 return TK.ENCRYPT in self.trailer 

1485 

1486 def add_form_topname(self, name: str) -> Optional[DictionaryObject]: 

1487 """ 

1488 Add a top level form that groups all form fields below it. 

1489 

1490 Args: 

1491 name: text string of the "/T" Attribute of the created object 

1492 

1493 Returns: 

1494 The created object. ``None`` means no object was created. 

1495 

1496 """ 

1497 catalog = self.root_object 

1498 

1499 if "/AcroForm" not in catalog or not isinstance( 

1500 catalog["/AcroForm"], DictionaryObject 

1501 ): 

1502 return None 

1503 acroform = cast(DictionaryObject, catalog[NameObject("/AcroForm")]) 

1504 if "/Fields" not in acroform: 

1505 # TODO: No error but this may be extended for XFA Forms 

1506 return None 

1507 

1508 interim = DictionaryObject() 

1509 interim[NameObject("/T")] = TextStringObject(name) 

1510 interim[NameObject("/Kids")] = acroform[NameObject("/Fields")] 

1511 self.cache_indirect_object( 

1512 0, 

1513 max(i for (g, i) in self.resolved_objects if g == 0) + 1, 

1514 interim, 

1515 ) 

1516 arr = ArrayObject() 

1517 arr.append(interim.indirect_reference) 

1518 acroform[NameObject("/Fields")] = arr 

1519 for o in cast(ArrayObject, interim["/Kids"]): 

1520 obj = o.get_object() 

1521 if "/Parent" in obj: 

1522 logger_warning( 

1523 "Top Level Form Field %(obj_ref)s has a non-expected parent", 

1524 source=__name__, 

1525 obj_ref=obj.indirect_reference, 

1526 ) 

1527 obj[NameObject("/Parent")] = interim.indirect_reference 

1528 return interim 

1529 

1530 def rename_form_topname(self, name: str) -> Optional[DictionaryObject]: 

1531 """ 

1532 Rename top level form field that all form fields below it. 

1533 

1534 Args: 

1535 name: text string of the "/T" field of the created object 

1536 

1537 Returns: 

1538 The modified object. ``None`` means no object was modified. 

1539 

1540 """ 

1541 catalog = self.root_object 

1542 

1543 if "/AcroForm" not in catalog or not isinstance( 

1544 catalog["/AcroForm"], DictionaryObject 

1545 ): 

1546 return None 

1547 acroform = cast(DictionaryObject, catalog[NameObject("/AcroForm")]) 

1548 if "/Fields" not in acroform: 

1549 return None 

1550 

1551 interim = cast( 

1552 DictionaryObject, 

1553 cast(ArrayObject, acroform[NameObject("/Fields")])[0].get_object(), 

1554 ) 

1555 interim[NameObject("/T")] = TextStringObject(name) 

1556 return interim 

1557 

1558 def _repr_mimebundle_( 

1559 self, 

1560 include: Union[None, Iterable[str]] = None, 

1561 exclude: Union[None, Iterable[str]] = None, 

1562 ) -> dict[str, Any]: 

1563 """ 

1564 Integration into Jupyter Notebooks. 

1565 

1566 This method returns a dictionary that maps a mime-type to its 

1567 representation. 

1568 

1569 .. seealso:: 

1570 

1571 https://ipython.readthedocs.io/en/stable/config/integrating.html 

1572 """ 

1573 self.stream.seek(0) 

1574 pdf_data = self.stream.read() 

1575 data = { 

1576 "application/pdf": pdf_data, 

1577 } 

1578 

1579 if include is not None: 

1580 # Filter representations based on include list 

1581 data = {k: v for k, v in data.items() if k in include} 

1582 

1583 if exclude is not None: 

1584 # Remove representations based on exclude list 

1585 data = {k: v for k, v in data.items() if k not in exclude} 

1586 

1587 return data 

1588 

1589 def _get_named_destinations( 

1590 self, 

1591 *, 

1592 tree: Union[TreeObject, None] = None, 

1593 retval: Optional[dict[str, Destination]] = None, 

1594 visited: Optional[set[int]] = None, 

1595 ) -> dict[str, Destination]: 

1596 """Override from PdfDocCommon. In the reader we can assume this is 

1597 static, but not in the writer. 

1598 """ 

1599 if tree or retval: 

1600 return super()._get_named_destinations(tree=tree, retval=retval, visited=visited) 

1601 

1602 if self._named_destinations_cache is None: 

1603 self._named_destinations_cache = super()._get_named_destinations() 

1604 return self._named_destinations_cache