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