Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pypdf/_reader.py: 33%
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# 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.
30import os
31import re
32import sys
33from collections.abc import Iterable
34from io import BytesIO, UnsupportedOperation
35from operator import itemgetter
36from pathlib import Path
37from types import TracebackType
38from typing import (
39 TYPE_CHECKING,
40 Any,
41 Callable,
42 Optional,
43 Union,
44 cast,
45)
47if sys.version_info >= (3, 11):
48 from typing import Self
49else:
50 from typing_extensions import Self
52from ._configuration import apply_legacy_configuration
53from ._doc_common import PdfDocCommon, convert_to_int
54from ._encryption import Encryption, PasswordType
55from ._utils import (
56 WHITESPACES_AS_BYTES,
57 StrByteType,
58 StreamType,
59 logger_warning,
60 read_non_whitespace,
61 read_previous_line,
62 read_until_whitespace,
63 skip_over_comment,
64 skip_over_whitespace,
65)
66from .constants import TrailerKeys as TK
67from .errors import (
68 EmptyFileError,
69 FileNotDecryptedError,
70 LimitReachedError,
71 PdfReadError,
72 PdfStreamError,
73 WrongPasswordError,
74)
75from .generic import (
76 ArrayObject,
77 Destination,
78 DictionaryObject,
79 EncodedStreamObject,
80 IndirectObject,
81 NameObject,
82 NullObject,
83 NumberObject,
84 PdfObject,
85 StreamObject,
86 TextStringObject,
87 is_null_or_none,
88 read_object,
89)
90from .xmp import XmpInformation
92if TYPE_CHECKING:
93 from ._page import PageObject
96class PdfReader(PdfDocCommon):
97 """
98 Initialize a PdfReader object.
100 This operation can take some time, as the PDF stream's cross-reference
101 tables are read into memory.
103 Args:
104 stream: A File object or an object that supports the standard read
105 and seek methods similar to a File object. Could also be a
106 string representing a path to a PDF file.
107 strict: Determines whether user should be warned of all
108 problems and also causes some correctable problems to be fatal.
109 Defaults to ``False``.
110 password: Decrypt PDF file at initialization. If the
111 password is None, the file will not be decrypted.
112 Defaults to ``None``.
113 root_object_recovery_limit: The maximum number of objects to query
114 for recovering the Root object in non-strict mode. To disable
115 this security measure, pass ``None``.
117 """
119 xref: dict[int, dict[Any, Any]]
120 trailer: DictionaryObject
122 def __init__(
123 self,
124 stream: Union[StrByteType, Path],
125 strict: bool = False,
126 password: Union[str, bytes, None] = None,
127 *,
128 root_object_recovery_limit: Optional[int] = 10_000,
129 ) -> None:
130 self.strict = strict
131 self.flattened_pages: Optional[list[PageObject]] = None
133 #: Storage of parsed PDF objects.
134 self.resolved_objects: dict[tuple[Any, Any], Optional[PdfObject]] = {}
136 self._startxref: int = 0
137 self.xref_index = 0
138 self.xref = {}
139 self.xref_free_entry: dict[int, dict[Any, Any]] = {}
140 self.xref_objStm: dict[int, tuple[Any, Any]] = {}
141 self.trailer = DictionaryObject()
143 # Security parameters.
144 self._root_object_recovery_limit = (
145 root_object_recovery_limit if isinstance(root_object_recovery_limit, int) else sys.maxsize
146 )
147 apply_legacy_configuration()
149 # Map page indirect_reference number to page number
150 self._page_id2num: Optional[dict[Any, Any]] = None
152 self._validated_root: Optional[DictionaryObject] = None
154 self._initialize_stream(stream)
155 self._known_objects: set[tuple[int, int]] = set()
157 self._override_encryption = False
158 self._encryption: Optional[Encryption] = None
159 if self.is_encrypted:
160 self._handle_encryption(password)
161 elif password is not None:
162 raise PdfReadError("Not an encrypted file")
164 self._named_destinations_cache: Optional[dict[str, Destination]] = None
165 self._object_stream_resolution_stack: list[int] = []
167 def _initialize_stream(self, stream: Union[StrByteType, Path]) -> None:
168 if hasattr(stream, "mode") and "b" not in stream.mode:
169 logger_warning(
170 "PdfReader stream/file object is not in binary mode. "
171 "It may not be read correctly.",
172 source=__name__,
173 )
174 self._stream_opened = False
175 if isinstance(stream, (str, Path)):
176 with open(stream, "rb") as fh:
177 stream = BytesIO(fh.read())
178 self._stream_opened = True
179 self.read(stream)
180 self.stream = stream
182 def _handle_encryption(self, password: Optional[Union[str, bytes]]) -> None:
183 self._override_encryption = True
184 # Some documents may not have a /ID, use two empty
185 # byte strings instead. Solves
186 # https://github.com/py-pdf/pypdf/issues/608
187 id_entry = self.trailer.get(TK.ID)
188 id1_entry = id_entry[0].get_object().original_bytes if id_entry else b""
189 encrypt_entry = cast(DictionaryObject, self.trailer[TK.ENCRYPT].get_object())
190 self._encryption = Encryption.read(encrypt_entry, id1_entry)
192 # try empty password if no password provided
193 pwd = password if password is not None else b""
194 if (
195 self._encryption.verify(pwd, strict=self.strict) == PasswordType.NOT_DECRYPTED
196 and password is not None
197 ):
198 # raise if password provided
199 raise WrongPasswordError("Wrong password")
200 self._override_encryption = False
202 def __enter__(self) -> Self:
203 return self
205 def __exit__(
206 self,
207 exc_type: Optional[type[BaseException]],
208 exc_val: Optional[BaseException],
209 exc_tb: Optional[TracebackType],
210 ) -> None:
211 self.close()
213 def close(self) -> None:
214 """Close the stream if opened in __init__ and clear memory."""
215 if self._stream_opened:
216 self.stream.close()
217 self.flattened_pages = []
218 self.resolved_objects = {}
219 self.trailer = DictionaryObject()
220 self.xref = {}
221 self.xref_free_entry = {}
222 self.xref_objStm = {}
224 @property
225 def root_object(self) -> DictionaryObject:
226 """Provide access to "/Root". Standardized with PdfWriter."""
227 if self._validated_root:
228 return self._validated_root
229 root = self.trailer.get(TK.ROOT, NullObject()).get_object()
230 root_dictionary: DictionaryObject
231 if isinstance(root, DictionaryObject):
232 root_dictionary = root
233 else:
234 # The catalog has to be a dictionary. Anything else cannot be used,
235 # thus treat it like an empty one and attempt the recovery below.
236 root_dictionary = DictionaryObject()
237 if is_null_or_none(root):
238 logger_warning('Cannot find "/Root" key in trailer', source=__name__)
239 elif root_dictionary.get("/Type") == "/Catalog":
240 self._validated_root = root_dictionary
241 else:
242 logger_warning("Invalid Root object in trailer", source=__name__)
243 if self._validated_root is None:
244 logger_warning('Searching object with "/Catalog" key', source=__name__)
245 number_of_objects = cast(int, self.trailer.get("/Size", 0))
246 for i in range(number_of_objects):
247 if i >= self._root_object_recovery_limit:
248 raise LimitReachedError("Maximum Root object recovery limit reached.")
249 try:
250 obj = self.get_object(i + 1)
251 except Exception: # to be sure to capture all errors
252 obj = None
253 if isinstance(obj, DictionaryObject) and obj.get("/Type") == "/Catalog":
254 self._validated_root = obj
255 logger_warning(
256 "Root found at %(obj_reference)r",
257 source=__name__,
258 obj_reference=obj.indirect_reference,
259 )
260 break
261 if self._validated_root is None:
262 if "/Pages" in root_dictionary:
263 logger_warning(
264 "Possible root found at %(root_ref)r, but missing /Catalog key",
265 source=__name__,
266 root_ref=root_dictionary.indirect_reference,
267 )
268 self._validated_root = root_dictionary
269 else:
270 raise PdfReadError("Cannot find Root object in pdf")
271 return self._validated_root
273 @property
274 def _info(self) -> Optional[DictionaryObject]:
275 """
276 Provide access to "/Info". Standardized with PdfWriter.
278 Returns:
279 /Info Dictionary; None if the entry does not exist
281 """
282 info = self.trailer.get(TK.INFO, None)
283 if is_null_or_none(info):
284 return None
285 assert info is not None, "mypy"
286 info = info.get_object()
287 if not isinstance(info, DictionaryObject):
288 raise PdfReadError(
289 "Trailer not found or does not point to a document information dictionary"
290 )
291 return info
293 @property
294 def _ID(self) -> Optional[ArrayObject]:
295 """
296 Provide access to "/ID". Standardized with PdfWriter.
298 Returns:
299 /ID array; None if the entry does not exist
301 """
302 file_identifiers = self.trailer.get(TK.ID, None)
303 if is_null_or_none(file_identifiers):
304 return None
305 assert file_identifiers is not None, "mypy"
306 return cast(ArrayObject, file_identifiers.get_object())
308 @property
309 def pdf_header(self) -> str:
310 """
311 The first 8 bytes of the file.
313 This is typically something like ``'%PDF-1.6'`` and can be used to
314 detect if the file is actually a PDF file and which version it is.
315 """
316 # TODO: Make this return a bytes object for consistency
317 # but that needs a deprecation
318 loc = self.stream.tell()
319 self.stream.seek(0, 0)
320 pdf_file_version = self.stream.read(8).decode("utf-8", "backslashreplace")
321 self.stream.seek(loc, 0) # return to where it was
322 return pdf_file_version
324 @property
325 def xmp_metadata(self) -> Optional[XmpInformation]:
326 """XMP (Extensible Metadata Platform) data."""
327 try:
328 # The document-level metadata stream is exempt from encryption
329 # only if /EncryptMetadata is false (ISO 32000-2, Table 21).
330 self._override_encryption = (
331 self._encryption is not None and not self._encryption.EncryptMetadata
332 )
333 return cast(XmpInformation, self.root_object.xmp_metadata)
334 finally:
335 self._override_encryption = False
337 def _get_page_number_by_indirect(
338 self, indirect_reference: Union[int, NullObject, IndirectObject, None]
339 ) -> Optional[int]:
340 """
341 Retrieve the page number from an indirect reference.
343 Args:
344 indirect_reference: The indirect reference to locate.
346 Returns:
347 Page number or None.
349 """
350 if self._page_id2num is None:
351 self._page_id2num = {
352 x.indirect_reference.idnum: i for i, x in enumerate(self.pages) # type: ignore[union-attr]
353 }
355 if is_null_or_none(indirect_reference):
356 return None
357 assert isinstance(indirect_reference, (int, IndirectObject)), "mypy"
358 if isinstance(indirect_reference, int):
359 idnum = indirect_reference
360 else:
361 idnum = indirect_reference.idnum
362 assert self._page_id2num is not None, "hint for mypy"
363 return self._page_id2num.get(idnum, None)
365 def _get_object_from_stream(
366 self, indirect_reference: IndirectObject
367 ) -> PdfObject:
368 obj_id = indirect_reference.idnum
369 if obj_id in self._object_stream_resolution_stack:
370 start = self._object_stream_resolution_stack.index(obj_id)
371 cycle = [*self._object_stream_resolution_stack[start:], obj_id]
372 raise PdfReadError(
373 "Circular object-stream reference detected: " + " -> ".join(map(str, cycle))
374 )
375 self._object_stream_resolution_stack.append(obj_id)
377 try:
378 # indirect reference to object in object stream
379 # read the entire object stream into memory
380 stmnum, _idx = self.xref_objStm[indirect_reference.idnum]
381 obj_stm: EncodedStreamObject = IndirectObject(stmnum, 0, self).get_object() # type: ignore[assignment]
382 # This is an xref to a stream, so its type better be a stream
383 assert cast(str, obj_stm["/Type"]) == "/ObjStm"
384 # Parse ALL objects in this stream in one pass and cache them.
385 # This avoids O(N²) behavior when many objects from the same stream
386 # are resolved individually (each call would re-parse the header).
387 stream_data = BytesIO(obj_stm.get_data())
388 n = int(obj_stm["/N"]) # type: ignore[call-overload]
389 first_offset = int(obj_stm["/First"]) # type: ignore[call-overload]
391 # ObjStm header format: "objnum offset objnum offset ..."
392 # smallest possible entry: "0 0" = 3 bytes (1 digit + 1 space + 1 digit)
393 # using // 4 would reject a valid 3-byte single entry (3 // 4 = 0)
394 max_n = stream_data.getbuffer().nbytes // 3
395 stream_data.seek(0)
396 if n > max_n:
397 if self.strict:
398 raise LimitReachedError(f"Value /N {n} for object {stmnum} exceeds maximum allowed value {max_n}.")
399 logger_warning(
400 (
401 "Value /N %(n)d for object %(stmnum)d exceeds maximum allowed value %(max_n)d. "
402 "Limiting to %(max_n)d."
403 ),
404 source=__name__,
405 n=n,
406 stmnum=stmnum,
407 max_n=max_n,
408 )
409 n = max_n
411 # Phase 1: Read the index (objnum, offset) pairs from the header.
412 obj_index: list[tuple[int, int]] = []
413 for _i in range(n):
414 read_non_whitespace(stream_data)
415 stream_data.seek(-1, 1)
416 objnum = NumberObject.read_from_stream(stream_data)
417 read_non_whitespace(stream_data)
418 stream_data.seek(-1, 1)
419 offset = NumberObject.read_from_stream(stream_data)
420 read_non_whitespace(stream_data)
421 stream_data.seek(-1, 1)
422 obj_index.append((int(objnum), int(offset)))
424 # Phase 2: Parse each object and cache it.
425 target_obj: PdfObject = NullObject()
426 found = False
427 for i, (obj_num, obj_offset) in enumerate(obj_index):
428 # Skip objects already in the cache.
429 cached = self.cache_get_indirect_object(0, obj_num)
430 if cached is not None:
431 if obj_num == indirect_reference.idnum:
432 target_obj = cached
433 found = True
434 continue
436 stream_data.seek(first_offset + obj_offset, 0)
438 # To cope with case where the 'pointer' is on a white space
439 read_non_whitespace(stream_data)
440 stream_data.seek(-1, 1)
442 try:
443 obj = read_object(stream_data, self)
444 except PdfStreamError as exc:
445 # Stream object cannot be read. Normally, a critical error, but
446 # Adobe Reader doesn't complain, so continue (in strict mode?)
447 logger_warning(
448 "Invalid stream (index %(index)d) within object %(obj_num)d 0: %(exc)s",
449 source=__name__,
450 index=i,
451 obj_num=obj_num,
452 exc=exc,
453 )
454 if self.strict: # pragma: no cover
455 raise PdfReadError(
456 f"Cannot read object stream: {exc}"
457 ) from exc # pragma: no cover
458 obj = NullObject() # pragma: no cover
460 # Only cache if this stream is the authoritative source for the object.
461 # Incremental updates may override objects originally in the stream;
462 # caching those stale versions would shadow the newer xref entry.
463 authoritative_stm, _idx = self.xref_objStm.get(obj_num, (None, None))
464 if authoritative_stm == stmnum:
465 self.cache_indirect_object(0, obj_num, obj)
467 if obj_num == indirect_reference.idnum:
468 target_obj = obj
469 found = True
471 if not found and self.strict: # pragma: no cover
472 raise PdfReadError(
473 "This is a fatal error in strict mode."
474 ) # pragma: no cover
475 return target_obj
476 finally:
477 self._object_stream_resolution_stack.pop()
479 def get_object(
480 self, indirect_reference: Union[int, IndirectObject]
481 ) -> Optional[PdfObject]:
482 if isinstance(indirect_reference, int):
483 indirect_reference = IndirectObject(indirect_reference, 0, self)
484 retval = self.cache_get_indirect_object(
485 indirect_reference.generation, indirect_reference.idnum
486 )
487 if retval is not None:
488 return retval
489 if (
490 indirect_reference.generation == 0
491 and indirect_reference.idnum in self.xref_objStm
492 ):
493 retval = self._get_object_from_stream(indirect_reference)
494 elif (
495 indirect_reference.generation in self.xref
496 and indirect_reference.idnum in self.xref[indirect_reference.generation]
497 ):
498 if self.xref_free_entry.get(indirect_reference.generation, {}).get(
499 indirect_reference.idnum, False
500 ):
501 return NullObject()
502 start = self.xref[indirect_reference.generation][indirect_reference.idnum]
503 self.stream.seek(start, 0)
504 try:
505 idnum, generation = self.read_object_header(self.stream)
506 if (
507 idnum != indirect_reference.idnum
508 or generation != indirect_reference.generation
509 ):
510 raise PdfReadError("Not matching, we parse the file for it")
511 except Exception:
512 if hasattr(self.stream, "getbuffer"):
513 buf = bytes(self.stream.getbuffer())
514 else:
515 p = self.stream.tell()
516 self.stream.seek(0, 0)
517 buf = self.stream.read(-1)
518 self.stream.seek(p, 0)
519 m = re.search(
520 rf"\s{indirect_reference.idnum}\s+{indirect_reference.generation}\s+obj".encode(),
521 buf,
522 )
523 if m is not None:
524 logger_warning(
525 "Object ID %(idnum)d,%(generation)d ref repaired",
526 source=__name__,
527 idnum=indirect_reference.idnum,
528 generation=indirect_reference.generation,
529 )
530 self.xref[indirect_reference.generation][
531 indirect_reference.idnum
532 ] = (m.start(0) + 1)
533 self.stream.seek(m.start(0) + 1)
534 idnum, generation = self.read_object_header(self.stream)
535 else:
536 idnum = -1
537 generation = -1 # exception will be raised below
538 if idnum != indirect_reference.idnum and self.xref_index:
539 # xref table probably had bad indexes due to not being zero-indexed
540 if self.strict:
541 raise PdfReadError(
542 f"Expected object ID ({indirect_reference.idnum} {indirect_reference.generation}) "
543 f"does not match actual ({idnum} {generation}); "
544 "xref table not zero-indexed."
545 )
546 # xref table is corrected in non-strict mode
547 elif idnum != indirect_reference.idnum and self.strict:
548 # some other problem
549 raise PdfReadError(
550 f"Expected object ID ({indirect_reference.idnum} {indirect_reference.generation}) "
551 f"does not match actual ({idnum} {generation})."
552 )
553 if self.strict:
554 assert generation == indirect_reference.generation
556 current_object = (indirect_reference.idnum, indirect_reference.generation)
557 if current_object in self._known_objects:
558 raise LimitReachedError(f"Detected loop with self reference for {indirect_reference!r}.")
559 self._known_objects.add(current_object)
560 retval = read_object(self.stream, self)
561 self._known_objects.remove(current_object)
563 # override encryption is used for the /Encrypt dictionary
564 if not self._override_encryption and self._encryption is not None:
565 # if we don't have the encryption key:
566 if not self._encryption.is_decrypted():
567 raise FileNotDecryptedError("File has not been decrypted")
568 # otherwise, decrypt here...
569 retval = self._encryption.decrypt_object(
570 retval, indirect_reference.idnum, indirect_reference.generation,
571 strict=self.strict,
572 )
573 else:
574 if hasattr(self.stream, "getbuffer"):
575 buf = bytes(self.stream.getbuffer())
576 else:
577 p = self.stream.tell()
578 self.stream.seek(0, 0)
579 buf = self.stream.read(-1)
580 self.stream.seek(p, 0)
581 m = re.search(
582 rf"\s{indirect_reference.idnum}\s+{indirect_reference.generation}\s+obj".encode(),
583 buf,
584 )
585 if m is not None:
586 logger_warning(
587 "Object %(idnum)d %(generation)d found",
588 source=__name__,
589 idnum=indirect_reference.idnum,
590 generation=indirect_reference.generation,
591 )
592 if indirect_reference.generation not in self.xref:
593 self.xref[indirect_reference.generation] = {}
594 self.xref[indirect_reference.generation][indirect_reference.idnum] = (
595 m.start(0) + 1
596 )
597 self.stream.seek(m.end(0) + 1)
598 skip_over_whitespace(self.stream)
599 self.stream.seek(-1, 1)
600 retval = read_object(self.stream, self)
602 # override encryption is used for the /Encrypt dictionary
603 if not self._override_encryption and self._encryption is not None:
604 # if we don't have the encryption key:
605 if not self._encryption.is_decrypted():
606 raise FileNotDecryptedError("File has not been decrypted")
607 # otherwise, decrypt here...
608 retval = self._encryption.decrypt_object(
609 retval, indirect_reference.idnum, indirect_reference.generation,
610 strict=self.strict,
611 )
612 else:
613 logger_warning(
614 "Object %(idnum)d %(generation)d not defined.",
615 source=__name__,
616 idnum=indirect_reference.idnum,
617 generation=indirect_reference.generation,
618 )
619 if self.strict:
620 raise PdfReadError("Could not find object.")
621 # For ObjStm objects, _get_object_from_stream already cached
622 # the result during batch parsing; skip the redundant cache write
623 # to avoid "Overwriting cache" warnings. For non-ObjStm objects
624 # (including encrypted ones that need decrypted values cached),
625 # always write.
626 if not (
627 indirect_reference.generation == 0
628 and indirect_reference.idnum in self.xref_objStm
629 ):
630 self.cache_indirect_object(
631 indirect_reference.generation, indirect_reference.idnum, retval
632 )
633 return retval
635 def read_object_header(self, stream: StreamType) -> tuple[int, int]:
636 # Should never be necessary to read out whitespace, since the
637 # cross-reference table should put us in the right spot to read the
638 # object header. In reality some files have stupid cross-reference
639 # tables that are off by whitespace bytes.
640 skip_over_comment(stream)
641 extra = skip_over_whitespace(stream)
642 stream.seek(-1, 1)
643 idnum = read_until_whitespace(stream, max_bytes=IndirectObject._MAXIMUM_PART_LENGTH, strict=self.strict)
644 extra |= skip_over_whitespace(stream)
645 stream.seek(-1, 1)
646 generation = read_until_whitespace(stream, max_bytes=IndirectObject._MAXIMUM_PART_LENGTH, strict=self.strict)
647 extra |= skip_over_whitespace(stream)
648 stream.seek(-1, 1)
650 # although it's not used, it might still be necessary to read
651 _obj = stream.read(3)
653 read_non_whitespace(stream)
654 stream.seek(-1, 1)
655 if extra and self.strict:
656 logger_warning(
657 "Superfluous whitespace found in object header %(idnum)r %(generation)r",
658 source=__name__,
659 idnum=idnum,
660 generation=generation,
661 )
663 try:
664 return int(idnum), int(generation)
665 except (ValueError, OverflowError) as exception:
666 # Only raise a ValueError here as other types would break future processing.
667 raise ValueError(
668 f"Invalid indirect object reference ({idnum!r} {generation!r} R): {exception}"
669 ) from exception
671 def cache_get_indirect_object(
672 self, generation: int, idnum: int
673 ) -> Optional[PdfObject]:
674 try:
675 return self.resolved_objects.get((generation, idnum))
676 except RecursionError as exception: # pragma: no cover
677 raise PdfReadError("Maximum recursion depth reached.") from exception # pragma: no cover
679 def cache_indirect_object(
680 self, generation: int, idnum: int, obj: Optional[PdfObject]
681 ) -> Optional[PdfObject]:
682 if (generation, idnum) in self.resolved_objects:
683 msg = "Overwriting cache for %(generation)d %(idnum)d"
684 values = {"generation": generation, "idnum": idnum}
685 if self.strict:
686 raise PdfReadError(msg % values)
687 logger_warning(msg, source=__name__, **values)
688 self.resolved_objects[(generation, idnum)] = obj
689 if obj is not None:
690 obj.indirect_reference = IndirectObject(idnum, generation, self)
691 return obj
693 def _replace_object(self, indirect_reference: IndirectObject, obj: PdfObject) -> PdfObject:
694 # function reserved for future development
695 if indirect_reference.pdf != self:
696 raise ValueError("Cannot update PdfReader with external object")
697 if (indirect_reference.generation, indirect_reference.idnum) not in self.resolved_objects:
698 raise ValueError("Cannot find referenced object")
699 self.resolved_objects[(indirect_reference.generation, indirect_reference.idnum)] = obj
700 obj.indirect_reference = indirect_reference
701 return obj
703 def read(self, stream: StreamType) -> None:
704 """
705 Read and process the PDF stream, extracting necessary data.
707 Args:
708 stream: The PDF file stream.
710 """
711 self._basic_validation(stream)
712 self._find_eof_marker(stream)
713 startxref = self._find_startxref_pos(stream)
714 if startxref < 0:
715 # A negative offset cannot point into the file. Treat it like the
716 # zero case (#3157) so the xref table is repaired instead of
717 # leaking a ValueError from a negative seek.
718 logger_warning(
719 "Negative startxref pointer (%(startxref)d), treating it as zero.",
720 source=__name__,
721 startxref=startxref,
722 )
723 startxref = 0
724 self._startxref = startxref
726 # check and eventually correct the startxref only if not strict
727 xref_issue_nr = self._get_xref_issues(stream, startxref)
728 if xref_issue_nr != 0:
729 if self.strict and xref_issue_nr:
730 raise PdfReadError("Broken xref table")
731 logger_warning(
732 "incorrect startxref pointer(%(xref_issue_nr)d)",
733 source=__name__,
734 xref_issue_nr=xref_issue_nr,
735 )
737 # read all cross-reference tables and their trailers
738 self._read_xref_tables_and_trailers(stream, startxref, xref_issue_nr)
740 # if not zero-indexed, verify that the table is correct; change it if necessary
741 if self.xref_index and not self.strict:
742 loc = stream.tell()
743 for gen, xref_entry in self.xref.items():
744 if gen == 65535:
745 continue
746 xref_k = sorted(
747 xref_entry.keys()
748 ) # ensure ascending to prevent damage
749 for xref_id in xref_k:
750 stream.seek(xref_entry[xref_id], 0)
751 try:
752 pid, _pgen = self.read_object_header(stream)
753 except ValueError:
754 self._rebuild_xref_table(stream)
755 break
756 if pid == xref_id - self.xref_index:
757 # fixing index item per item is required for revised PDF.
758 self.xref[gen][pid] = self.xref[gen][xref_id]
759 del self.xref[gen][xref_id]
760 # if not, then either it's just plain wrong, or the
761 # non-zero-index is actually correct
762 stream.seek(loc, 0) # return to where it was
764 # remove wrong objects (not pointing to correct structures) - cf #2326
765 if not self.strict:
766 loc = stream.tell()
767 for gen, xref_entry in self.xref.items():
768 if gen == 65535:
769 continue
770 ids = list(xref_entry.keys())
771 for xref_id in ids:
772 stream.seek(xref_entry[xref_id], 0)
773 try:
774 self.read_object_header(stream)
775 except ValueError:
776 logger_warning(
777 "Ignoring wrong pointing object %(id)d %(gen)d (offset %(offset)d)",
778 source=__name__,
779 id=xref_id,
780 gen=gen,
781 offset=xref_entry[xref_id],
782 )
783 del xref_entry[xref_id] # we can delete the id, we are parsing ids
784 stream.seek(loc, 0) # return to where it was
786 def _basic_validation(self, stream: StreamType) -> None:
787 """Ensure the stream is valid and not empty."""
788 stream.seek(0, os.SEEK_SET)
789 try:
790 header_byte = stream.read(5)
791 except UnicodeDecodeError as exception:
792 raise UnsupportedOperation("cannot read header") from exception
793 if header_byte == b"":
794 raise EmptyFileError("Cannot read an empty file")
795 if header_byte != b"%PDF-":
796 if self.strict:
797 raise PdfReadError(
798 f"PDF starts with '{header_byte.decode('utf8')}', "
799 "but '%PDF-' expected"
800 )
801 logger_warning("invalid pdf header: %(header_byte)r", source=__name__, header_byte=header_byte)
802 stream.seek(0, os.SEEK_END)
804 def _find_eof_marker(self, stream: StreamType) -> None:
805 """
806 Jump to the %%EOF marker.
808 According to the specs, the %%EOF marker should be at the very end of
809 the file. Hence for standard-compliant PDF documents this function will
810 read only the last part (DEFAULT_BUFFER_SIZE).
812 Producers that omit the line break before the marker (``startxref
813 256%%EOF`` or ``256%%EOF``) are accepted as well (#4127).
814 """
815 header_size = 8 # to parse whole file, Header is e.g. '%PDF-1.6'
816 line = b""
817 first = True
818 # Bytes of `line` end at this position. An inline %%EOF therefore
819 # begins at end_position - len(line) + marker_index.
820 end_position = stream.tell()
821 while True:
822 marker_index = line.find(b"%%EOF")
823 if marker_index == 0:
824 break
825 if marker_index > 0 and first:
826 # Only the trailing line is allowed to carry an inline marker.
827 # Earlier lines keep the historical "starts with %%EOF" rule so
828 # a %%EOF buried in stream data is not treated as the trailer.
829 # Leading whitespace in front of the marker is ignored.
830 if line[:marker_index].lstrip(WHITESPACES_AS_BYTES):
831 logger_warning(
832 "EOF marker not at start of line",
833 source=__name__,
834 )
835 stream.seek(end_position - len(line) + marker_index)
836 break
837 if line != b"" and first:
838 if any(
839 line.strip().endswith(tr) for tr in (b"%%EO", b"%%E", b"%%", b"%")
840 ):
841 # Consider the file as truncated while
842 # having enough confidence to carry on.
843 logger_warning("EOF marker seems truncated", source=__name__)
844 break
845 first = False
846 if b"startxref" in line:
847 logger_warning(
848 "CAUTION: startxref found while searching for %%EOF. "
849 "The file might be truncated and some data might not be read.",
850 source=__name__,
851 )
852 if stream.tell() < header_size:
853 if self.strict:
854 raise PdfReadError("EOF marker not found")
855 logger_warning("EOF marker not found", source=__name__)
856 end_position = stream.tell()
857 line = read_previous_line(stream)
859 def _find_startxref_pos(self, stream: StreamType) -> int:
860 """
861 Find startxref entry - the location of the xref table.
863 Args:
864 stream:
866 Returns:
867 The bytes offset
869 """
870 line = read_previous_line(stream)
871 # Some producers append further %%EOF markers below the one that
872 # closes the last revision (#4008). _find_eof_marker() stops at the
873 # very last of them, so the line above it is another marker rather
874 # than the offset. Skip that trailing run to reach the real offset;
875 # the revision being read is unchanged, only the marker padding is
876 # ignored.
877 duplicate_markers = 0
878 while line.startswith(b"%%EOF") and stream.tell() > 0:
879 if duplicate_markers == self._MAX_STARTXREF_RECOVERY_LINES:
880 break
881 line = read_previous_line(stream)
882 duplicate_markers += 1
883 if duplicate_markers:
884 logger_warning(
885 "Duplicate %%EOF marker(s) found, skipping them", source=__name__
886 )
887 try:
888 startxref = int(line)
889 except ValueError as exception: # pragma: no cover
890 # 'startxref' may be on the same line as the location
891 if not line.startswith(b"startxref"):
892 raise PdfReadError("startxref not found") from exception
893 startxref = int(line[9:].strip())
894 logger_warning("startxref on same line as offset", source=__name__)
895 else:
896 line = read_previous_line(stream)
897 if not line.startswith(b"startxref"):
898 # The 'startxref' keyword expected just above the offset is
899 # missing or corrupt (for example a truncated 'tartxref').
900 # Some producers append a broken trailing cross-reference
901 # pointer while an earlier, intact 'startxref' from a previous
902 # revision is still present. Recovering from this violates the
903 # standard, so only attempt it in non-strict mode (#3238).
904 if self.strict:
905 raise PdfReadError("startxref not found")
906 startxref = self._find_previous_startxref_pos(stream)
907 return startxref
909 # Upper bound on the number of lines _find_previous_startxref_pos scans
910 # backwards while recovering a corrupt trailing startxref pointer. Kept
911 # fixed and non-configurable so a crafted file cannot trigger an unbounded
912 # backwards scan.
913 _MAX_STARTXREF_RECOVERY_LINES = 1000
915 @classmethod
916 def _find_previous_startxref_pos(cls, stream: StreamType) -> int:
917 """
918 Recover the most recent intact ``startxref`` pointer by scanning
919 backwards from the current position.
921 This is used as a fallback when the ``startxref`` keyword belonging to
922 the final ``%%EOF`` is corrupt (#3238). The offset always appears on
923 the line directly below the keyword, so the value read immediately
924 before encountering ``startxref`` (while moving backwards) is returned.
925 At most ``_MAX_STARTXREF_RECOVERY_LINES`` lines are inspected.
927 Args:
928 stream: The PDF byte stream, positioned just above the corrupt
929 trailing pointer.
931 Returns:
932 The bytes offset of the recovered ``startxref``.
934 """
935 offset: Optional[int] = None
936 for _ in range(cls._MAX_STARTXREF_RECOVERY_LINES):
937 if stream.tell() <= 0:
938 break
939 line = read_previous_line(stream)
940 if not line.startswith(b"startxref"):
941 try:
942 offset = int(line)
943 except ValueError:
944 offset = None
945 continue
946 if len(line) > 9:
947 # 'startxref' on the same line as the offset
948 return int(line[9:].strip())
949 if offset is not None:
950 logger_warning(
951 "found startxref pointing to a previous revision after "
952 "a corrupt one",
953 source=__name__,
954 )
955 return offset
956 break
957 raise PdfReadError("startxref not found")
959 def _load_recovery_cache(self, data: bytes) -> dict[int, tuple[int, int]]:
960 cache = {}
961 for object_number, generation_number, object_start in self._find_pdf_objects(data):
962 if object_number in cache:
963 # Always use the first match.
964 continue
965 cache[object_number] = (object_start, generation_number)
966 return cache
968 def _read_standard_xref_table(self, stream: StreamType) -> None:
969 # standard cross-reference table
970 ref = stream.read(3)
971 if ref != b"ref":
972 raise PdfReadError("xref table read error")
973 read_non_whitespace(stream)
974 stream.seek(-1, 1)
975 first_time = True # check if the first time looking at the xref table
976 recovery_cache: Optional[dict[int, tuple[int, int]]] = None
977 while True:
978 num = cast(int, read_object(stream, self))
979 if first_time and num != 0:
980 self.xref_index = num
981 if self.strict:
982 logger_warning(
983 "Xref table not zero-indexed. ID numbers for objects will be corrected.",
984 source=__name__,
985 )
986 # if table not zero indexed, could be due to error from when PDF was created
987 # which will lead to mismatched indices later on, only warned and corrected if self.strict==True
988 first_time = False
989 read_non_whitespace(stream)
990 stream.seek(-1, 1)
991 size = cast(int, read_object(stream, self))
992 if not isinstance(size, int):
993 logger_warning(
994 "Invalid/Truncated xref table. Rebuilding it.",
995 source=__name__,
996 )
997 self._rebuild_xref_table(stream)
998 stream.read()
999 return
1000 read_non_whitespace(stream)
1001 stream.seek(-1, 1)
1002 cnt = 0
1003 while cnt < size:
1004 line = stream.read(20)
1005 if not line:
1006 raise PdfReadError("Unexpected empty line in Xref table.")
1008 # It's very clear in section 3.4.3 of the PDF spec
1009 # that all cross-reference table lines are a fixed
1010 # 20 bytes (as of PDF 1.7). However, some files have
1011 # 21-byte entries (or more) due to the use of \r\n
1012 # (CRLF) EOL's. Detect that case, and adjust the line
1013 # until it does not begin with a \r (CR) or \n (LF).
1014 while line[0] in b"\x0D\x0A":
1015 stream.seek(-20 + 1, 1)
1016 line = stream.read(20)
1017 if len(line) != 20:
1018 raise PdfReadError("Unexpected EOF in Xref table.")
1020 # On the other hand, some malformed PDF files
1021 # use a single character EOL without a preceding
1022 # space. Detect that case, and seek the stream
1023 # back one character (0-9 means we've bled into
1024 # the next xref entry, t means we've bled into the
1025 # text "trailer"):
1026 if line[-1] in b"0123456789t":
1027 stream.seek(-1, 1)
1029 try:
1030 offset_b, generation_b = line[:16].split(b" ")
1031 entry_type_b = line[17:18]
1033 offset, generation = int(offset_b), int(generation_b)
1034 except Exception:
1035 if hasattr(stream, "getbuffer"):
1036 buf = bytes(stream.getbuffer())
1037 else:
1038 p = stream.tell()
1039 stream.seek(0, 0)
1040 buf = stream.read(-1)
1041 stream.seek(p)
1043 if recovery_cache is None:
1044 recovery_cache = self._load_recovery_cache(buf)
1046 if num not in recovery_cache:
1047 logger_warning(
1048 "entry %(num)d in Xref table invalid; object not found",
1049 source=__name__,
1050 num=num,
1051 )
1052 generation = 65535
1053 offset = -1
1054 entry_type_b = b"f"
1055 else:
1056 logger_warning(
1057 "entry %(num)d in Xref table invalid but object found",
1058 source=__name__,
1059 num=num,
1060 )
1061 generation, offset = recovery_cache[num]
1062 entry_type_b = b"n"
1064 if generation not in self.xref:
1065 self.xref[generation] = {}
1066 self.xref_free_entry[generation] = {}
1067 if num in self.xref[generation]:
1068 # It really seems like we should allow the last
1069 # xref table in the file to override previous
1070 # ones. Since we read the file backwards, assume
1071 # any existing key is already set correctly.
1072 pass
1073 else:
1074 if entry_type_b == b"n":
1075 self.xref[generation][num] = offset
1076 try:
1077 self.xref_free_entry[generation][num] = entry_type_b == b"f"
1078 except Exception:
1079 pass
1080 try:
1081 self.xref_free_entry[65535][num] = entry_type_b == b"f"
1082 except Exception:
1083 pass
1084 cnt += 1
1085 num += 1
1086 read_non_whitespace(stream)
1087 stream.seek(-1, 1)
1088 # Skip any PDF comments between xref entries and the trailer
1089 # keyword. Some PDF producers (e.g. Vectorizer.AI) insert
1090 # comments here which are legal per the PDF spec (§7.2.3).
1091 while stream.read(1) == b"%":
1092 stream.seek(-1, 1)
1093 skip_over_comment(stream)
1094 read_non_whitespace(stream)
1095 stream.seek(-1, 1)
1096 stream.seek(-1, 1)
1097 trailer_tag = stream.read(7)
1098 if trailer_tag != b"trailer":
1099 # more xrefs!
1100 stream.seek(-7, 1)
1101 else:
1102 break
1104 def _read_xref_tables_and_trailers(
1105 self, stream: StreamType, startxref: Optional[int], xref_issue_nr: int
1106 ) -> None:
1107 """Read the cross-reference tables and trailers in the PDF stream."""
1108 self.xref = {}
1109 self.xref_free_entry = {}
1110 self.xref_objStm = {}
1111 self.trailer = DictionaryObject()
1112 visited_xref_offsets: set[int] = set()
1113 while startxref is not None:
1114 # Detect circular /Prev references in the xref chain
1115 if startxref in visited_xref_offsets:
1116 logger_warning(
1117 "Circular xref chain detected at offset %(startxref)d, stopping",
1118 source=__name__,
1119 startxref=startxref,
1120 )
1121 break
1122 visited_xref_offsets.add(startxref)
1123 # load the xref table
1124 stream.seek(startxref, 0)
1125 x = stream.read(1)
1126 if x in b"\r\n":
1127 x = stream.read(1)
1128 if x == b"x":
1129 startxref = self._read_xref(stream)
1130 elif xref_issue_nr:
1131 try:
1132 self._rebuild_xref_table(stream)
1133 break
1134 except Exception:
1135 xref_issue_nr = 0
1136 elif x.isdigit():
1137 try:
1138 xrefstream = self._read_pdf15_xref_stream(stream)
1139 except Exception as exception: # pragma: no cover
1140 if TK.ROOT in self.trailer:
1141 logger_warning(
1142 "Previous trailer cannot be read: %(args)s",
1143 source=__name__,
1144 args=exception.args,
1145 )
1146 break
1147 raise PdfReadError(f"Trailer cannot be read: {exception!s}") from exception # pragma: no cover
1148 self._process_xref_stream(xrefstream)
1149 if "/Prev" in xrefstream:
1150 startxref = cast(int, xrefstream["/Prev"])
1151 else:
1152 break
1153 else:
1154 startxref = self._read_xref_other_error(stream, startxref)
1156 # The trailer keys a PDF 1.5+ cross-reference stream carries in place of a
1157 # `trailer` keyword (PDF 2.0 specification, table 17).
1158 _XREF_STREAM_TRAILER_KEYS = (TK.ROOT, TK.ENCRYPT, TK.INFO, TK.ID, TK.SIZE)
1160 def _process_xref_stream(self, xrefstream: DictionaryObject) -> None:
1161 """Process and handle the xref stream."""
1162 for key in self._XREF_STREAM_TRAILER_KEYS:
1163 if key in xrefstream and key not in self.trailer:
1164 self.trailer[NameObject(key)] = xrefstream.raw_get(key)
1165 if "/XRefStm" in xrefstream:
1166 p = self.stream.tell()
1167 self.stream.seek(cast(int, xrefstream["/XRefStm"]) + 1, 0)
1168 self._read_pdf15_xref_stream(self.stream)
1169 self.stream.seek(p, 0)
1171 def _read_xref(self, stream: StreamType) -> Optional[int]:
1172 self._read_standard_xref_table(stream)
1173 if stream.read(1) == b"":
1174 return None
1175 stream.seek(-1, 1)
1176 read_non_whitespace(stream)
1177 stream.seek(-1, 1)
1178 new_trailer = cast(dict[str, Any], read_object(stream, self))
1179 for key, value in new_trailer.items():
1180 if key not in self.trailer:
1181 self.trailer[key] = value
1182 if "/XRefStm" in new_trailer:
1183 p = stream.tell()
1184 stream.seek(cast(int, new_trailer["/XRefStm"]) + 1, 0)
1185 try:
1186 self._read_pdf15_xref_stream(stream)
1187 except Exception:
1188 logger_warning(
1189 "XRef object at %(xref_stm)d can not be read, some object may be missing",
1190 source=__name__,
1191 xref_stm=int(new_trailer["/XRefStm"]),
1192 )
1193 stream.seek(p, 0)
1194 if "/Prev" in new_trailer:
1195 return cast(int, new_trailer["/Prev"])
1196 return None
1198 def _read_xref_other_error(
1199 self, stream: StreamType, startxref: int
1200 ) -> Optional[int]:
1201 # some PDFs have /Prev=0 in the trailer, instead of no /Prev
1202 if startxref == 0:
1203 if self.strict:
1204 raise PdfReadError(
1205 "/Prev=0 in the trailer (try opening with strict=False)"
1206 )
1207 logger_warning(
1208 "/Prev=0 in the trailer - assuming there is no previous xref table",
1209 source=__name__,
1210 )
1211 return None
1212 # bad xref character at startxref. Let's see if we can find
1213 # the xref table nearby, as we've observed this error with an
1214 # off-by-one before.
1215 stream.seek(-11, 1)
1216 tmp = stream.read(20)
1217 xref_loc = tmp.find(b"xref")
1218 if xref_loc != -1:
1219 startxref -= 10 - xref_loc
1220 return startxref
1221 # No explicit xref table, try finding a cross-reference stream.
1222 stream.seek(startxref, 0)
1223 for look in range(25): # value extended to cope with more linearized files
1224 if stream.read(1).isdigit():
1225 # This is not a standard PDF, consider adding a warning
1226 startxref += look
1227 return startxref
1228 # no xref table found at specified location
1229 if "/Root" in self.trailer and not self.strict:
1230 # if Root has been already found, just raise warning
1231 logger_warning("Invalid parent xref., rebuild xref", source=__name__)
1232 try:
1233 self._rebuild_xref_table(stream)
1234 return None
1235 except Exception as exception: # pragma: no cover
1236 raise PdfReadError("Cannot rebuild xref") from exception # pragma: no cover
1237 raise PdfReadError("Could not find xref table at specified location")
1239 def _sanitize_pdf15_xref_stream_index_pairs(
1240 self, index_pairs: list[int], entry_sizes: list[int], xref_stream: StreamObject
1241 ) -> list[int]:
1242 # `entry_sizes` holds the byte widths for the entries. Summing determines the total number of bytes per entry.
1243 # We expect up to 3 values. `min_entry_bytes` will be the smallest plausible size of one xref entry.
1244 min_entry_bytes = sum(int(entry_sizes[i]) for i in range(min(len(entry_sizes), 3)))
1245 if min_entry_bytes == 0:
1246 message = "Cross-reference stream encodes no entry data."
1247 if self.strict:
1248 raise PdfStreamError(message)
1249 logger_warning(message, source=__name__)
1250 return []
1252 # maximum number of entries that could physically fit
1253 max_entries = len(xref_stream.get_data()) // min_entry_bytes + 1
1255 result = []
1256 total = 0
1258 for index, pair_value in enumerate(index_pairs):
1259 pair_value_int = int(pair_value)
1261 # `index_pairs` has the format `[start0, count0, start1, count1, ...]`
1262 # Only modify the counts here, but keep the start values.
1263 if index % 2 == 1:
1264 if total + pair_value_int > max_entries:
1265 if self.strict:
1266 raise LimitReachedError(
1267 f"Total XRef entries {total + pair_value_int} exceed maximum allowed value {max_entries}."
1268 )
1269 new_v = max(0, max_entries - total)
1270 logger_warning(
1271 "Clamping XRef count from %(old_count)d to %(new_count)d to fit stream size.",
1272 source=__name__,
1273 old_count=pair_value_int,
1274 new_count=new_v,
1275 )
1276 pair_value_int = new_v
1278 total += pair_value_int
1280 result.append(pair_value_int)
1282 return result
1284 def _read_pdf15_xref_stream(self, stream: StreamType) -> StreamObject:
1285 """Read the cross-reference stream for PDF 1.5+."""
1286 stream.seek(-1, 1)
1287 stream_idnum, stream_generation = self.read_object_header(stream)
1288 xref_stream = cast(StreamObject, read_object(stream, self))
1289 if cast(str, xref_stream["/Type"]) != "/XRef":
1290 raise PdfReadError(f"Unexpected type {xref_stream['/Type']!r}")
1291 self.cache_indirect_object(stream_generation, stream_idnum, xref_stream)
1293 # Index pairs specify the subsections in the dictionary.
1294 # If none, create one subsection that spans everything.
1295 if "/Size" not in xref_stream:
1296 # According to table 17 of the PDF 2.0 specification, this key is required.
1297 raise PdfReadError(f"Size missing from XRef stream {xref_stream!r}!")
1298 index_pairs = xref_stream.get("/Index", [0, xref_stream["/Size"]])
1300 entry_sizes = cast(list[int], xref_stream.get("/W"))
1301 assert len(entry_sizes) >= 3
1302 if self.strict and len(entry_sizes) > 3:
1303 raise PdfReadError(f"Too many entry sizes: {entry_sizes}")
1304 index_pairs = self._sanitize_pdf15_xref_stream_index_pairs(
1305 index_pairs=index_pairs, entry_sizes=entry_sizes, xref_stream=xref_stream
1306 )
1308 stream_data = BytesIO(xref_stream.get_data())
1310 def get_entry(i: int) -> Union[int, tuple[int, ...]]:
1311 # Reads the correct number of bytes for each entry. See the
1312 # discussion of the W parameter in PDF spec table 17.
1313 if entry_sizes[i] > 0:
1314 d = stream_data.read(entry_sizes[i])
1315 return convert_to_int(d, entry_sizes[i])
1317 # PDF Spec Table 17: A value of zero for an element in the
1318 # W array indicates...the default value shall be used
1319 if i == 0:
1320 return 1 # First value defaults to 1
1321 return 0
1323 def used_before(num: int, generation: Union[int, tuple[int, ...]]) -> bool:
1324 # We move backwards through the xrefs, don't replace any.
1325 return num in self.xref.get(generation, []) or num in self.xref_objStm # type: ignore[arg-type]
1327 # Iterate through each subsection
1328 self._read_xref_subsections(index_pairs, get_entry, used_before)
1329 return xref_stream
1331 @staticmethod
1332 def _get_xref_issues(stream: StreamType, startxref: int) -> int:
1333 """
1334 Return an int which indicates an issue. 0 means there is no issue.
1336 Args:
1337 stream:
1338 startxref:
1340 Returns:
1341 0 means no issue, other values represent specific issues.
1343 """
1344 if startxref == 0:
1345 return 4
1347 stream.seek(startxref - 1, 0) # -1 to check character before
1348 line = stream.read(1)
1349 if line == b"j":
1350 line = stream.read(1)
1351 if line not in b"\r\n \t":
1352 return 1
1353 line = stream.read(4)
1354 if line != b"xref":
1355 # not a xref so check if it is an XREF object
1356 line = b""
1357 while line in b"0123456789 \t":
1358 line = stream.read(1)
1359 if line == b"":
1360 return 2
1361 line += stream.read(2) # 1 char already read, +2 to check "obj"
1362 if line.lower() != b"obj":
1363 return 3
1364 return 0
1366 @classmethod
1367 def _find_pdf_objects(cls, data: bytes) -> Iterable[tuple[int, int, int]]:
1368 index = 0
1369 ord_0 = ord("0")
1370 ord_9 = ord("9")
1371 while True:
1372 index = data.find(b" obj", index)
1373 if index == -1:
1374 return
1376 index_before_space = index - 1
1378 # Skip whitespace backwards
1379 while index_before_space >= 0 and data[index_before_space] in WHITESPACES_AS_BYTES:
1380 index_before_space -= 1
1382 # Read generation number
1383 generation_end = index_before_space + 1
1384 while index_before_space >= 0 and ord_0 <= data[index_before_space] <= ord_9:
1385 index_before_space -= 1
1386 generation_start = index_before_space + 1
1388 # Skip whitespace
1389 while index_before_space >= 0 and data[index_before_space] in WHITESPACES_AS_BYTES:
1390 index_before_space -= 1
1392 # Read object number
1393 object_end = index_before_space + 1
1394 while index_before_space >= 0 and ord_0 <= data[index_before_space] <= ord_9:
1395 index_before_space -= 1
1396 object_start = index_before_space + 1
1398 # Validate
1399 if object_start < object_end and generation_start < generation_end:
1400 object_number = int(data[object_start:object_end])
1401 generation_number = int(data[generation_start:generation_end])
1403 yield object_number, generation_number, object_start
1405 index += 4 # len(b" obj")
1407 @classmethod
1408 def _find_pdf_trailers(cls, data: bytes) -> Iterable[int]:
1409 index = 0
1410 data_length = len(data)
1411 while True:
1412 index = data.find(b"trailer", index)
1413 if index == -1:
1414 return
1416 index_after_trailer = index + 7 # len(b"trailer")
1418 # Skip whitespace
1419 while index_after_trailer < data_length and data[index_after_trailer] in WHITESPACES_AS_BYTES:
1420 index_after_trailer += 1
1422 # Must be dictionary start
1423 if index_after_trailer + 1 < data_length and data[index_after_trailer:index_after_trailer+2] == b"<<":
1424 yield index_after_trailer # offset of '<<'
1426 index += 7 # len(b"trailer")
1428 def _rebuild_xref_table(self, stream: StreamType) -> None:
1429 self.xref = {}
1430 stream.seek(0, 0)
1431 stream_data = stream.read(-1)
1433 for object_number, generation_number, object_start in self._find_pdf_objects(stream_data):
1434 if generation_number not in self.xref:
1435 self.xref[generation_number] = {}
1436 self.xref[generation_number][object_number] = object_start
1438 logger_warning("parsing for Object Streams", source=__name__)
1439 # PDF 1.5+ files may carry the trailer keys inside a cross-reference
1440 # stream instead of behind a `trailer` keyword. Collect them here,
1441 # keyed by their offset, to merge them below in file order.
1442 xref_stream_trailers: list[tuple[int, DictionaryObject]] = []
1443 for generation_number in self.xref:
1444 for object_number in self.xref[generation_number]:
1445 # get_object in manual
1446 object_start = self.xref[generation_number][object_number]
1447 stream.seek(object_start, 0)
1448 try:
1449 _ = self.read_object_header(stream)
1450 obj = cast(StreamObject, read_object(stream, self))
1451 object_type = obj.get("/Type", "")
1452 if object_type == "/XRef":
1453 trailer = DictionaryObject()
1454 for key in self._XREF_STREAM_TRAILER_KEYS:
1455 if key in obj:
1456 trailer[NameObject(key)] = obj.raw_get(key)
1457 xref_stream_trailers.append((object_start, trailer))
1458 continue
1459 if object_type != "/ObjStm":
1460 continue
1461 object_stream = BytesIO(obj.get_data())
1462 actual_count = 0
1463 while True:
1464 current = read_until_whitespace(
1465 object_stream, max_bytes=IndirectObject._MAXIMUM_PART_LENGTH, strict=self.strict
1466 )
1467 if not current.isdigit():
1468 break
1469 inner_object_number = int(current)
1470 skip_over_whitespace(object_stream)
1471 object_stream.seek(-1, 1)
1472 current = read_until_whitespace(
1473 object_stream, max_bytes=IndirectObject._MAXIMUM_PART_LENGTH, strict=self.strict
1474 )
1475 if not current.isdigit(): # pragma: no cover
1476 break # pragma: no cover
1477 inner_offset = int(current)
1478 self.xref_objStm[inner_object_number] = (object_number, inner_offset)
1479 actual_count += 1
1480 expected_count = cast(int, obj["/N"])
1481 if actual_count != expected_count: # pragma: no cover
1482 logger_warning( # pragma: no cover
1483 (
1484 "found %(actual_count)d objects within "
1485 "Object(%(object_number)d,%(generation_number)d) "
1486 "whereas %(expected)d expected"
1487 ),
1488 source=__name__,
1489 actual_count=actual_count,
1490 object_number=object_number,
1491 generation_number=generation_number,
1492 expected=expected_count,
1493 )
1494 except LimitReachedError:
1495 # Do not let the broad recovery below bypass the token-length limit
1496 # when strict parsing is enabled.
1497 raise
1498 except Exception: # could be multiple causes
1499 pass
1501 stream.seek(0, 0)
1502 trailers: list[tuple[int, dict[Any, Any]]] = list(xref_stream_trailers)
1503 for position in self._find_pdf_trailers(stream_data):
1504 stream.seek(position, 0)
1505 trailers.append((position, cast(dict[Any, Any], read_object(stream, self))))
1506 # Here, we are parsing the file from start to end, the new data have to erase the existing.
1507 for _, new_trailer in sorted(trailers, key=itemgetter(0)):
1508 for key, value in new_trailer.items():
1509 self.trailer[key] = value
1511 def _read_xref_subsections(
1512 self,
1513 idx_pairs: list[int],
1514 get_entry: Callable[[int], Union[int, tuple[int, ...]]],
1515 used_before: Callable[[int, Union[int, tuple[int, ...]]], bool],
1516 ) -> None:
1517 """Read and process the subsections of the xref."""
1518 for start, size in self._pairs(idx_pairs):
1519 # The subsections must increase
1520 for num in range(start, start + size):
1521 # The first entry is the type
1522 xref_type = get_entry(0)
1523 # The rest of the elements depend on the xref_type
1524 if xref_type == 0:
1525 # linked list of free objects
1526 next_free_object = get_entry(1) # noqa: F841
1527 next_generation = get_entry(2) # noqa: F841
1528 elif xref_type == 1:
1529 # objects that are in use but are not compressed
1530 byte_offset = get_entry(1)
1531 generation = get_entry(2)
1532 if generation not in self.xref:
1533 self.xref[generation] = {} # type: ignore[index]
1534 if not used_before(num, generation):
1535 self.xref[generation][num] = byte_offset # type: ignore[index]
1536 elif xref_type == 2:
1537 # compressed objects
1538 objstr_num = get_entry(1)
1539 obstr_idx = get_entry(2)
1540 generation = 0 # PDF spec table 18, generation is 0
1541 if not used_before(num, generation):
1542 self.xref_objStm[num] = (objstr_num, obstr_idx)
1543 elif self.strict:
1544 raise PdfReadError(f"Unknown xref type: {xref_type}")
1546 def _pairs(self, array: list[int]) -> Iterable[tuple[int, int]]:
1547 """Iterate over pairs in the array."""
1548 i = 0
1549 while i + 1 < len(array):
1550 yield array[i], array[i + 1]
1551 i += 2
1553 def decrypt(self, password: Union[str, bytes]) -> PasswordType:
1554 """
1555 When using an encrypted / secured PDF file with the PDF Standard
1556 encryption handler, this function will allow the file to be decrypted.
1557 It checks the given password against the document's user password and
1558 owner password, and then stores the resulting decryption key if either
1559 password is correct.
1561 It does not matter which password was matched. Both passwords provide
1562 the correct decryption key that will allow the document to be used with
1563 this library.
1565 Args:
1566 password: The password to match.
1568 Returns:
1569 An indicator if the document was decrypted and whether it was the
1570 owner password or the user password.
1572 """
1573 if not self._encryption:
1574 raise PdfReadError("Not encrypted file")
1575 # TODO: raise Exception for wrong password
1576 return self._encryption.verify(password, strict=self.strict)
1578 @property
1579 def is_encrypted(self) -> bool:
1580 """
1581 Read-only boolean property showing whether this PDF file is encrypted.
1583 Note that this property, if true, will remain true even after the
1584 :meth:`decrypt()<pypdf.PdfReader.decrypt>` method is called.
1585 """
1586 return TK.ENCRYPT in self.trailer
1588 def add_form_topname(self, name: str) -> Optional[DictionaryObject]:
1589 """
1590 Add a top level form that groups all form fields below it.
1592 Args:
1593 name: text string of the "/T" Attribute of the created object
1595 Returns:
1596 The created object. ``None`` means no object was created.
1598 """
1599 catalog = self.root_object
1601 if "/AcroForm" not in catalog or not isinstance(
1602 catalog["/AcroForm"], DictionaryObject
1603 ):
1604 return None
1605 acroform = cast(DictionaryObject, catalog[NameObject("/AcroForm")])
1606 if "/Fields" not in acroform:
1607 # TODO: No error but this may be extended for XFA Forms
1608 return None
1610 interim = DictionaryObject()
1611 interim[NameObject("/T")] = TextStringObject(name)
1612 interim[NameObject("/Kids")] = acroform[NameObject("/Fields")]
1613 # Allocate the new object_id beyond the source-file range and all cached object IDs.
1614 object_id = max(
1615 cast(int, self.trailer.get("/Size", 0)) - 1,
1616 max((i for entries in self.xref.values() for i in entries), default=0),
1617 max(self.xref_objStm, default=0),
1618 max((i for _, i in self.resolved_objects), default=0),
1619 ) + 1
1620 self.cache_indirect_object(0, object_id, interim)
1621 arr = ArrayObject()
1622 arr.append(interim.indirect_reference)
1623 acroform[NameObject("/Fields")] = arr
1624 for o in cast(ArrayObject, interim["/Kids"]):
1625 obj = o.get_object()
1626 if "/Parent" in obj:
1627 logger_warning(
1628 "Top Level Form Field %(obj_ref)s has a non-expected parent",
1629 source=__name__,
1630 obj_ref=obj.indirect_reference,
1631 )
1632 obj[NameObject("/Parent")] = interim.indirect_reference
1633 return interim
1635 def rename_form_topname(self, name: str) -> Optional[DictionaryObject]:
1636 """
1637 Rename top level form field that all form fields below it.
1639 Args:
1640 name: text string of the "/T" field of the created object
1642 Returns:
1643 The modified object. ``None`` means no object was modified.
1645 """
1646 catalog = self.root_object
1648 if "/AcroForm" not in catalog or not isinstance(
1649 catalog["/AcroForm"], DictionaryObject
1650 ):
1651 return None
1652 acroform = cast(DictionaryObject, catalog[NameObject("/AcroForm")])
1653 if "/Fields" not in acroform:
1654 return None
1656 interim = cast(
1657 DictionaryObject,
1658 cast(ArrayObject, acroform[NameObject("/Fields")])[0].get_object(),
1659 )
1660 interim[NameObject("/T")] = TextStringObject(name)
1661 return interim
1663 def _repr_mimebundle_(
1664 self,
1665 include: Union[Iterable[str], None] = None,
1666 exclude: Union[Iterable[str], None] = None,
1667 ) -> dict[str, Any]:
1668 """
1669 Integration into Jupyter Notebooks.
1671 This method returns a dictionary that maps a mime-type to its
1672 representation.
1674 .. seealso::
1676 https://ipython.readthedocs.io/en/stable/config/integrating.html
1677 """
1678 self.stream.seek(0)
1679 pdf_data = self.stream.read()
1680 data = {
1681 "application/pdf": pdf_data,
1682 }
1684 if include is not None:
1685 # Filter representations based on include list
1686 data = {k: v for k, v in data.items() if k in include}
1688 if exclude is not None:
1689 # Remove representations based on exclude list
1690 data = {k: v for k, v in data.items() if k not in exclude}
1692 return data
1694 def _get_named_destinations(
1695 self,
1696 *,
1697 tree: Optional[DictionaryObject] = None,
1698 retval: Optional[dict[str, Destination]] = None,
1699 visited: Optional[set[int]] = None,
1700 ) -> dict[str, Destination]:
1701 """Override from PdfDocCommon. In the reader we can assume this is
1702 static, but not in the writer.
1703 """
1704 if tree or retval:
1705 return super()._get_named_destinations(tree=tree, retval=retval, visited=visited)
1707 if self._named_destinations_cache is None:
1708 self._named_destinations_cache = super()._get_named_destinations()
1709 return self._named_destinations_cache