Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pypdf/_doc_common.py: 21%
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# Copyright (c) 2024, Pubpub-ZZ
4#
5# All rights reserved.
6#
7# Redistribution and use in source and binary forms, with or without
8# modification, are permitted provided that the following conditions are
9# met:
10#
11# * Redistributions of source code must retain the above copyright notice,
12# this list of conditions and the following disclaimer.
13# * Redistributions in binary form must reproduce the above copyright notice,
14# this list of conditions and the following disclaimer in the documentation
15# and/or other materials provided with the distribution.
16# * The name of the author may not be used to endorse or promote products
17# derived from this software without specific prior written permission.
18#
19# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
23# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29# POSSIBILITY OF SUCH DAMAGE.
31import struct
32from abc import ABC, abstractmethod
33from collections.abc import Iterable, Iterator, Mapping, Sequence
34from datetime import datetime
35from typing import (
36 Any,
37 NamedTuple,
38 NoReturn,
39 Optional,
40 Union,
41 cast,
42)
44from ._configuration import get_configuration
45from ._encryption import Encryption
46from ._page import PageObject, _VirtualList
47from ._page_labels import index2label as page_index2page_label
48from ._utils import (
49 _TraversalState,
50 deprecate_no_replacement,
51 deprecation_with_replacement,
52 logger_warning,
53 parse_iso8824_date,
54)
55from .constants import CatalogAttributes as CA
56from .constants import (
57 CheckboxRadioButtonAttributes,
58 Core,
59 GoToActionArguments,
60 PagesAttributes,
61 UserAccessPermissions,
62)
63from .constants import DocumentInformationAttributes as DI
64from .constants import FieldDictionaryAttributes as FA
65from .constants import PageAttributes as PG
66from .errors import LimitReachedError, PdfReadError, PyPdfError
67from .filters import _decompress_with_limit
68from .generic import (
69 ArrayObject,
70 BooleanObject,
71 ByteStringObject,
72 Destination,
73 DictionaryObject,
74 EncodedStreamObject,
75 Field,
76 Fit,
77 FloatObject,
78 IndirectObject,
79 NameObject,
80 NullObject,
81 PdfObject,
82 TextStringObject,
83 TreeObject,
84 ViewerPreferences,
85 create_string_object,
86 is_null_or_none,
87)
88from .generic._files import EmbeddedFile
89from .types import OutlineType, PagemodeType
90from .xmp import XmpInformation
93def convert_to_int(d: bytes, size: int) -> Union[int, tuple[Any, ...]]:
94 if size > 8:
95 raise PdfReadError("Invalid size in convert_to_int")
96 d = b"\x00\x00\x00\x00\x00\x00\x00\x00" + d
97 d = d[-8:]
98 return cast(int, struct.unpack(">Q", d)[0])
101# Attributes a page inherits from its ancestors in the page tree.
102_INHERITABLE_PAGE_ATTRIBUTES = (
103 NameObject(PG.RESOURCES),
104 NameObject(PG.MEDIABOX),
105 NameObject(PG.CROPBOX),
106 NameObject(PG.ROTATE),
107)
110class _PageTreeItem(NamedTuple):
111 """A page tree node waiting to be flattened by :meth:`PdfDocCommon._flatten`."""
113 node: DictionaryObject
114 #: Inherited attributes. Shared read-only between the kids of one node.
115 inherit: dict[str, Any]
116 indirect_reference: Optional[IndirectObject]
117 depth: int
120#: A ``/Pages`` node currently being walked: the node - carrying the attributes its
121#: kids inherit - and the part of its ``/Kids`` which has not been reached yet.
122_PageTreeFrame = tuple[_PageTreeItem, Iterator[PdfObject]]
124_UNSET = object()
127class DocumentInformation(DictionaryObject):
128 """
129 A class representing the basic document metadata provided in a PDF File.
130 This class is accessible through
131 :py:class:`PdfReader.metadata<pypdf.PdfReader.metadata>`.
133 All text properties of the document metadata have
134 *two* properties, e.g. author and author_raw. The non-raw property will
135 always return a ``TextStringObject``, making it ideal for a case where the
136 metadata is being displayed. The raw property can sometimes return a
137 ``ByteStringObject``, if pypdf was unable to decode the string's text
138 encoding; this requires additional safety in the caller and therefore is not
139 as commonly accessed.
140 """
142 def __init__(self) -> None:
143 DictionaryObject.__init__(self)
145 def _get_text(self, key: str) -> Optional[str]:
146 retval = self.get(key, None)
147 if isinstance(retval, TextStringObject):
148 return retval
149 if isinstance(retval, ByteStringObject):
150 return str(retval)
151 return None
153 @property
154 def title(self) -> Optional[str]:
155 """
156 Read-only property accessing the document's title.
158 Returns a ``TextStringObject`` or ``None`` if the title is not
159 specified.
160 """
161 return (
162 self._get_text(DI.TITLE) or self.get(DI.TITLE).get_object() # type: ignore[union-attr]
163 if self.get(DI.TITLE)
164 else None
165 )
167 @property
168 def title_raw(self) -> Optional[str]:
169 """The "raw" version of title; can return a ``ByteStringObject``."""
170 return self.get(DI.TITLE)
172 @property
173 def author(self) -> Optional[str]:
174 """
175 Read-only property accessing the document's author.
177 Returns a ``TextStringObject`` or ``None`` if the author is not
178 specified.
179 """
180 return self._get_text(DI.AUTHOR)
182 @property
183 def author_raw(self) -> Optional[str]:
184 """The "raw" version of author; can return a ``ByteStringObject``."""
185 return self.get(DI.AUTHOR)
187 @property
188 def subject(self) -> Optional[str]:
189 """
190 Read-only property accessing the document's subject.
192 Returns a ``TextStringObject`` or ``None`` if the subject is not
193 specified.
194 """
195 return self._get_text(DI.SUBJECT)
197 @property
198 def subject_raw(self) -> Optional[str]:
199 """The "raw" version of subject; can return a ``ByteStringObject``."""
200 return self.get(DI.SUBJECT)
202 @property
203 def creator(self) -> Optional[str]:
204 """
205 Read-only property accessing the document's creator.
207 If the document was converted to PDF from another format, this is the
208 name of the application (e.g. OpenOffice) that created the original
209 document from which it was converted. Returns a ``TextStringObject`` or
210 ``None`` if the creator is not specified.
211 """
212 return self._get_text(DI.CREATOR)
214 @property
215 def creator_raw(self) -> Optional[str]:
216 """The "raw" version of creator; can return a ``ByteStringObject``."""
217 return self.get(DI.CREATOR)
219 @property
220 def producer(self) -> Optional[str]:
221 """
222 Read-only property accessing the document's producer.
224 If the document was converted to PDF from another format, this is the
225 name of the application (for example, macOS Quartz) that converted it to
226 PDF. Returns a ``TextStringObject`` or ``None`` if the producer is not
227 specified.
228 """
229 return self._get_text(DI.PRODUCER)
231 @property
232 def producer_raw(self) -> Optional[str]:
233 """The "raw" version of producer; can return a ``ByteStringObject``."""
234 return self.get(DI.PRODUCER)
236 @property
237 def creation_date(self) -> Optional[datetime]:
238 """Read-only property accessing the document's creation date."""
239 return parse_iso8824_date(self._get_text(DI.CREATION_DATE))
241 @property
242 def creation_date_raw(self) -> Optional[str]:
243 """
244 The "raw" version of creation date; can return a ``ByteStringObject``.
246 Typically in the format ``D:YYYYMMDDhhmmss[+Z-]hh'mm`` where the suffix
247 is the offset from UTC.
248 """
249 return self.get(DI.CREATION_DATE)
251 @property
252 def modification_date(self) -> Optional[datetime]:
253 """
254 Read-only property accessing the document's modification date.
256 The date and time the document was most recently modified.
257 """
258 return parse_iso8824_date(self._get_text(DI.MOD_DATE))
260 @property
261 def modification_date_raw(self) -> Optional[str]:
262 """
263 The "raw" version of modification date; can return a
264 ``ByteStringObject``.
266 Typically, in the format ``D:YYYYMMDDhhmmss[+Z-]hh'mm`` where the suffix
267 is the offset from UTC.
268 """
269 return self.get(DI.MOD_DATE)
271 @property
272 def keywords(self) -> Optional[str]:
273 """
274 Read-only property accessing the document's keywords.
276 Returns a ``TextStringObject`` or ``None`` if keywords are not
277 specified.
278 """
279 return self._get_text(DI.KEYWORDS)
281 @property
282 def keywords_raw(self) -> Optional[str]:
283 """The "raw" version of keywords; can return a ``ByteStringObject``."""
284 return self.get(DI.KEYWORDS)
287class PdfDocCommon(ABC):
288 """
289 Common functions from PdfWriter and PdfReader objects.
291 This root class is strongly abstracted.
292 """
294 strict: bool = False # default
296 flattened_pages: Optional[list[PageObject]] = None
298 _encryption: Optional[Encryption] = None
300 _readonly: bool = False
302 @property
303 @abstractmethod
304 def root_object(self) -> DictionaryObject:
305 ... # pragma: no cover
307 @property
308 @abstractmethod
309 def pdf_header(self) -> str:
310 ... # pragma: no cover
312 @abstractmethod
313 def get_object(
314 self, indirect_reference: Union[int, IndirectObject]
315 ) -> Optional[PdfObject]:
316 ... # pragma: no cover
318 @abstractmethod
319 def _replace_object(self, indirect: IndirectObject, obj: PdfObject) -> PdfObject:
320 ... # pragma: no cover
322 @property
323 @abstractmethod
324 def _info(self) -> Optional[DictionaryObject]:
325 ... # pragma: no cover
327 @property
328 def metadata(self) -> Optional[DocumentInformation]:
329 """
330 Retrieve the PDF file's document information dictionary, if it exists.
332 Note that some PDF files use metadata streams instead of document
333 information dictionaries, and these metadata streams will not be
334 accessed by this function.
335 """
336 retval = DocumentInformation()
337 if self._info is None:
338 return None
339 retval.update(self._info)
340 return retval
342 @property
343 @abstractmethod
344 def xmp_metadata(self) -> Optional[XmpInformation]:
345 ... # pragma: no cover
347 @property
348 def viewer_preferences(self) -> Optional[ViewerPreferences]:
349 """Returns the existing ViewerPreferences as an overloaded dictionary."""
350 o = self.root_object.get(CA.VIEWER_PREFERENCES, None)
351 if o is None:
352 return None
353 o = o.get_object()
354 if not isinstance(o, DictionaryObject):
355 logger_warning(
356 "Viewer preferences are not a dictionary: %(preferences)s",
357 source=__name__,
358 preferences=o,
359 )
360 return None
361 if not isinstance(o, ViewerPreferences):
362 o = ViewerPreferences(o)
363 if hasattr(o, "indirect_reference") and o.indirect_reference is not None:
364 self._replace_object(o.indirect_reference, o)
365 else:
366 self.root_object[NameObject(CA.VIEWER_PREFERENCES)] = o
367 return o
369 def get_num_pages(self) -> int:
370 """
371 Calculate the number of pages in this PDF file.
373 Returns:
374 The number of pages of the parsed PDF file.
376 Raises:
377 PdfReadError: If restrictions prevent this action.
379 """
380 # Flattened pages will not work on an encrypted PDF;
381 # the PDF file's page count is used in this case. Otherwise,
382 # the original method (flattened page count) is used.
383 if self.is_encrypted:
384 return self.root_object["/Pages"]["/Count"] # type: ignore[no-any-return, index]
385 if self.flattened_pages is None:
386 self._flatten(self._readonly)
387 assert self.flattened_pages is not None
388 return len(self.flattened_pages)
390 def get_page(self, page_number: int) -> PageObject:
391 """
392 Retrieve a page by number from this PDF file.
393 Most of the time ``.pages[page_number]`` is preferred.
395 Args:
396 page_number: The page number to retrieve
397 (pages begin at zero)
399 Returns:
400 A :class:`PageObject<pypdf._page.PageObject>` instance.
402 """
403 if self.flattened_pages is None:
404 self._flatten(self._readonly)
405 assert self.flattened_pages is not None, "mypy"
406 return self.flattened_pages[page_number]
408 def _get_page_in_node(
409 self,
410 page_number: int,
411 ) -> tuple[DictionaryObject, int]:
412 """
413 Retrieve the node and position within the /Kids containing the page.
414 If page_number is greater than the number of pages, it returns the top node, -1.
415 """
416 top = cast(DictionaryObject, self.root_object["/Pages"])
417 visited: set[int] = set()
419 def recursive_call(
420 _node: DictionaryObject, mi: int
421 ) -> tuple[Optional[PdfObject], int]:
422 _node_id = id(_node)
423 if _node_id in visited:
424 raise LimitReachedError("Detected cycle in /Pages hierarchy when retrieving page.")
425 visited.add(_node_id)
426 ma = cast(int, _node.get("/Count", 1)) # default 1 for /Page types
427 if _node.get("/Type") == "/Page":
428 if page_number == mi:
429 return _node, -1
430 return None, mi + 1
431 if (page_number - mi) >= ma: # not in nodes below
432 if _node == top:
433 return top, -1
434 return None, mi + ma
435 for _idx, kid in enumerate(cast(ArrayObject, _node["/Kids"])):
436 kid = cast(DictionaryObject, kid.get_object())
437 n, i = recursive_call(kid, mi)
438 if n is not None: # page has just been found ...
439 if i < 0: # ... just below!
440 return _node, _idx
441 # ... at lower levels
442 return n, i
443 mi = i
444 raise PyPdfError("Unexpectedly cannot find the node.")
446 node, idx = recursive_call(top, 0)
447 assert isinstance(node, DictionaryObject), "mypy"
448 return node, idx
450 @property
451 def named_destinations(self) -> dict[str, Destination]:
452 """A read-only dictionary which maps names to destinations."""
453 return self._get_named_destinations()
455 def get_named_dest_root(self) -> ArrayObject:
456 named_dest = ArrayObject()
457 if CA.NAMES in self.root_object and isinstance(
458 self.root_object[CA.NAMES], DictionaryObject
459 ):
460 names = cast(DictionaryObject, self.root_object[CA.NAMES])
461 if CA.DESTS in names and isinstance(names[CA.DESTS], DictionaryObject):
462 # §3.6.3 Name Dictionary (PDF spec 1.7)
463 dests = cast(DictionaryObject, names[CA.DESTS])
464 if CA.NAMES in dests:
465 # §7.9.6, entries in a name tree node dictionary
466 named_dest = cast(ArrayObject, dests[CA.NAMES])
467 else:
468 named_dest = ArrayObject()
469 dests[NameObject(CA.NAMES)] = named_dest
470 elif hasattr(self, "_add_object"):
471 dests = DictionaryObject()
472 dests_ref = self._add_object(dests)
473 names[NameObject(CA.DESTS)] = dests_ref
474 dests[NameObject(CA.NAMES)] = named_dest
476 elif hasattr(self, "_add_object"):
477 names = DictionaryObject()
478 names_ref = self._add_object(names)
479 self.root_object[NameObject(CA.NAMES)] = names_ref
480 dests = DictionaryObject()
481 dests_ref = self._add_object(dests)
482 names[NameObject(CA.DESTS)] = dests_ref
483 dests[NameObject(CA.NAMES)] = named_dest
485 return named_dest
487 ## common
488 def _get_named_destinations(
489 self,
490 *,
491 tree: Optional[DictionaryObject] = None,
492 retval: Optional[dict[str, Destination]] = None,
493 visited: Optional[set[int]] = None,
494 ) -> dict[str, Destination]:
495 """
496 Retrieve the named destinations present in the document.
498 Args:
499 tree: The current tree.
500 retval: The previously retrieved destinations for nested calls.
501 visited: Already known/visited objects.
503 Returns:
504 A dictionary which maps names to destinations.
506 """
507 if visited is None:
508 visited = set()
509 if retval is None:
510 retval = {}
511 catalog = self.root_object
513 # get the name tree
514 candidate: Optional[PdfObject] = None
515 if CA.DESTS in catalog:
516 candidate = catalog[CA.DESTS].get_object()
517 elif CA.NAMES in catalog:
518 names = catalog[CA.NAMES].get_object()
519 if isinstance(names, DictionaryObject) and CA.DESTS in names:
520 candidate = names[CA.DESTS].get_object()
521 if candidate is not None and not isinstance(candidate, DictionaryObject):
522 logger_warning(
523 "Destination tree is not a dictionary: %(tree)s",
524 source=__name__,
525 tree=candidate,
526 )
527 return retval
528 if candidate is not None:
529 tree = candidate
531 if is_null_or_none(tree):
532 return retval
533 assert tree is not None, "mypy"
535 tree_id = id(tree)
536 if tree_id in visited:
537 logger_warning("Detected cycle in destination tree.", source=__name__)
538 return retval
539 visited.add(tree_id)
541 if PagesAttributes.KIDS in tree:
542 kids = tree[PagesAttributes.KIDS].get_object()
543 if not isinstance(kids, ArrayObject):
544 logger_warning(
545 "Destination tree kids are not an array: %(kids)s",
546 source=__name__,
547 kids=kids,
548 )
549 return retval
550 # recurse down the tree
551 for kid in kids:
552 self._get_named_destinations(tree=kid.get_object(), retval=retval, visited=visited)
553 # §7.9.6, entries in a name tree node dictionary
554 elif CA.NAMES in tree: # /Kids and /Names are exclusives (§7.9.6)
555 names = tree[CA.NAMES].get_object()
556 if not isinstance(names, ArrayObject):
557 logger_warning(
558 "Destination tree names are not an array: %(names)s",
559 source=__name__,
560 names=names,
561 )
562 return retval
563 i = 0
564 while i < len(names):
565 key = names[i].get_object()
566 i += 1
567 if not isinstance(key, (bytes, str)):
568 continue
569 try:
570 value = names[i].get_object()
571 except IndexError:
572 break
573 i += 1
574 if isinstance(value, DictionaryObject):
575 if "/D" in value:
576 value = value["/D"]
577 else:
578 continue
579 dest = self._build_destination(key, value)
580 if dest is not None:
581 retval[cast(str, dest["/Title"])] = dest
582 # Remain backwards-compatible.
583 retval[str(key)] = dest
584 else: # case where /Dests is in the document's catalog dictionary (PDF 1.7 specs, §2 about PDF 1.1)
585 for k__, v__ in tree.items():
586 val = v__.get_object()
587 if isinstance(val, DictionaryObject):
588 if "/D" in val:
589 val = val["/D"].get_object()
590 else:
591 continue
592 dest = self._build_destination(k__, val)
593 if dest is not None:
594 retval[k__] = dest
595 return retval
597 # A select group of relevant field attributes. For the complete list,
598 # see §12.3.2 of the PDF 1.7 or PDF 2.0 specification.
600 def get_fields(
601 self,
602 tree: Optional[DictionaryObject] = None,
603 retval: Optional[dict[Any, Any]] = None,
604 fileobj: Optional[Any] = None,
605 stack: Any = _UNSET,
606 *,
607 _visited: Optional[set[int]] = None,
608 _depth: int = 0,
609 _traversal_state: Optional[_TraversalState] = None,
610 ) -> Optional[dict[str, Any]]:
611 """
612 Extract field data if this PDF contains interactive form fields.
614 The *tree*, *retval*, *visited*, *depth* parameters are for internal use.
616 Args:
617 tree: Current object to parse.
618 retval: In-progress list of fields.
619 fileobj: A file object (usually a text file) to write
620 a report to on all interactive form fields found.
621 stack: The stack to use. Deprecated.
622 _visited: Set of already visited IDs. Callers should usually omit this.
623 _depth: The current field depth.
624 _traversal_state: The current traversal state.
626 Returns:
627 A dictionary where each key is a field name, and each
628 value is a :class:`Field<pypdf.generic.Field>` object. By
629 default, the mapping name is used for keys.
630 ``None`` if form data could not be located.
632 """
633 if stack is not _UNSET:
634 deprecate_no_replacement(name="stack", removed_in="7.0.0")
635 if retval is not None:
636 if _visited is None:
637 _visited = set()
638 _visited.update(set(map(id, stack or [])))
640 configuration = get_configuration()
641 if _depth >= configuration.form_fields_maximum_depth:
642 raise LimitReachedError("Reached maximum allowed form field depth.")
643 if _traversal_state is None:
644 _traversal_state = _TraversalState()
646 def _check_field_limit(_increment: int) -> None:
647 _traversal_state.entry_count += _increment
648 if _traversal_state.entry_count > configuration.form_fields_maximum_entries:
649 raise LimitReachedError("Exceeded maximum number of fields allowed.")
651 field_attributes = FA.attributes_dict()
652 field_attributes.update(CheckboxRadioButtonAttributes.attributes_dict())
653 if retval is None:
654 retval = {}
655 catalog = self.root_object
656 _visited = set()
657 # get the AcroForm tree
658 if CA.ACRO_FORM in catalog:
659 entry = catalog[CA.ACRO_FORM]
660 acro_form = None if entry is None else entry.get_object()
661 if acro_form is not None and not isinstance(
662 acro_form, DictionaryObject
663 ):
664 logger_warning(
665 "AcroForm is not a dictionary: %(acro_form)s",
666 source=__name__,
667 acro_form=acro_form,
668 )
669 return None
670 tree = acro_form
671 else:
672 return None
673 if tree is None:
674 return retval
675 assert _visited is not None
676 if "/Fields" in tree:
677 fields = tree["/Fields"].get_object()
678 if not isinstance(fields, ArrayObject):
679 logger_warning(
680 "AcroForm /Fields is not an array: %(fields)s",
681 source=__name__,
682 fields=fields,
683 )
684 return retval
685 _check_field_limit(len(fields))
686 for f in fields:
687 field = f.get_object()
688 self._build_field(
689 field=field, retval=retval, fileobj=fileobj, field_attributes=field_attributes,
690 visited=_visited, depth=_depth, traversal_state=_traversal_state,
691 )
692 elif any(attr in tree for attr in field_attributes):
693 # Tree is a field
694 _check_field_limit(1)
695 self._build_field(
696 field=tree, retval=retval, fileobj=fileobj, field_attributes=field_attributes,
697 visited=_visited, depth=_depth, traversal_state=_traversal_state,
698 )
699 return retval
701 def _get_qualified_field_name(
702 self,
703 *,
704 parent: DictionaryObject,
705 visited: Optional[set[int]] = None
706 ) -> str:
707 if visited is None:
708 visited = set()
709 parent_id = id(parent)
710 if parent_id in visited:
711 raise LimitReachedError("Detected cycle in /Parent hierarchy when retrieving qualified field name.")
712 visited.add(parent_id)
714 if "/TM" in parent:
715 return cast(str, parent["/TM"])
716 if "/Parent" in parent:
717 return (
718 self._get_qualified_field_name(
719 parent=cast(DictionaryObject, parent["/Parent"]),
720 visited=visited,
721 )
722 + "."
723 + cast(str, parent.get("/T", ""))
724 )
725 return cast(str, parent.get("/T", ""))
727 @staticmethod
728 def _normal_appearance(appearance: Any) -> DictionaryObject:
729 """Return the /N normal-appearance sub-dictionary of an /AP entry."""
730 appearance = appearance.get_object()
731 if not isinstance(appearance, DictionaryObject):
732 raise PdfReadError(f"Expected appearance dictionary, got {appearance!r}")
733 normal = appearance.get("/N")
734 if normal is not None:
735 normal = normal.get_object()
736 if not isinstance(normal, DictionaryObject):
737 raise PdfReadError(f"Expected /N appearance dictionary, got {normal!r}")
738 return normal
740 def _build_field(
741 self,
742 *,
743 field: Union[TreeObject, DictionaryObject],
744 retval: dict[Any, Any],
745 fileobj: Any,
746 field_attributes: Any,
747 visited: set[int],
748 depth: int,
749 traversal_state: _TraversalState,
750 ) -> None:
751 if not isinstance(field, DictionaryObject):
752 logger_warning(
753 "Form field is not a dictionary: %(field)s",
754 source=__name__,
755 field=field,
756 )
757 return
758 if all(attr not in field for attr in ("/T", "/TM")):
759 return
760 key = self._get_qualified_field_name(parent=field)
761 if fileobj:
762 self._write_field(fileobj, field, field_attributes)
763 fileobj.write("\n")
764 retval[key] = Field(field)
765 obj = retval[key].indirect_reference.get_object() # to get the full object
766 if obj.get(FA.FT, "") == "/Ch" and obj.get(NameObject(FA.Opt)):
767 retval[key][NameObject("/_States_")] = obj[NameObject(FA.Opt)]
768 if obj.get(FA.FT, "") == "/Btn" and "/AP" in obj:
769 # Checkbox
770 normal = self._normal_appearance(obj["/AP"])
771 retval[key][NameObject("/_States_")] = ArrayObject(list(normal.keys()))
772 if "/Off" not in retval[key]["/_States_"]:
773 retval[key][NameObject("/_States_")].append(NameObject("/Off"))
774 elif obj.get(FA.FT, "") == "/Btn" and obj.get(FA.Ff, 0) & FA.FfBits.Radio != 0:
775 states: list[str] = []
776 retval[key][NameObject("/_States_")] = ArrayObject(states)
777 for k in obj.get(FA.Kids, {}):
778 k = k.get_object()
779 if "/AP" not in k:
780 raise PdfReadError(f"Button field kid missing /AP: {k!r}")
781 normal = self._normal_appearance(k["/AP"])
782 for s in list(normal.keys()):
783 if s not in states:
784 states.append(s)
785 retval[key][NameObject("/_States_")] = ArrayObject(states)
786 if (
787 obj.get(FA.Ff, 0) & FA.FfBits.NoToggleToOff != 0
788 and "/Off" in retval[key]["/_States_"]
789 ):
790 del retval[key]["/_States_"][retval[key]["/_States_"].index("/Off")]
791 # at last for order
792 self._check_kids(
793 tree=field, retval=retval, fileobj=fileobj, visited=visited, depth=depth, traversal_state=traversal_state
794 )
796 def _check_kids(
797 self,
798 *,
799 tree: Union[TreeObject, DictionaryObject],
800 retval: Any,
801 fileobj: Any,
802 visited: set[int],
803 depth: int,
804 traversal_state: _TraversalState,
805 ) -> None:
806 tree_id = id(tree)
807 if tree_id in visited:
808 logger_warning(
809 "%(field_name)s already parsed",
810 source=__name__,
811 field_name=self._get_qualified_field_name(parent=tree),
812 )
813 return
814 visited.add(tree_id)
815 if PagesAttributes.KIDS in tree:
816 # recurse down the tree
817 for kid in tree[PagesAttributes.KIDS]: # type: ignore[attr-defined]
818 kid = kid.get_object()
819 self.get_fields(
820 tree=kid, retval=retval, fileobj=fileobj,
821 _visited=visited, _depth=depth + 1, _traversal_state=traversal_state
822 )
824 def _write_field(self, fileobj: Any, field: Any, field_attributes: Any) -> None:
825 field_attributes_tuple = FA.attributes()
826 field_attributes_tuple = (
827 field_attributes_tuple + CheckboxRadioButtonAttributes.attributes()
828 )
830 for attr in field_attributes_tuple:
831 if attr in (
832 FA.Kids,
833 FA.AA,
834 ):
835 continue
836 attr_name = field_attributes[attr]
837 try:
838 if attr == FA.FT:
839 # Make the field type value clearer
840 types = {
841 "/Btn": "Button",
842 "/Tx": "Text",
843 "/Ch": "Choice",
844 "/Sig": "Signature",
845 }
846 if field[attr] in types:
847 fileobj.write(f"{attr_name}: {types[field[attr]]}\n")
848 elif attr == FA.Parent:
849 # Let's just write the name of the parent
850 try:
851 name = field[attr][FA.TM]
852 except KeyError:
853 name = field[attr][FA.T]
854 fileobj.write(f"{attr_name}: {name}\n")
855 else:
856 fileobj.write(f"{attr_name}: {field[attr]}\n")
857 except KeyError:
858 # Field attribute is N/A or unknown, so don't write anything
859 pass
861 def get_form_text_fields(self, full_qualified_name: bool = False) -> dict[str, Any]:
862 """
863 Retrieve form fields from the document with textual data.
865 Args:
866 full_qualified_name: to get full name
868 Returns:
869 A dictionary. The key is the name of the form field,
870 the value is the content of the field.
872 If the document contains multiple form fields with the same name, the
873 second and following will get the suffix .2, .3, ...
875 """
877 def indexed_key(k: str, fields: dict[Any, Any]) -> str:
878 if k not in fields:
879 return k
880 return (
881 k
882 + "."
883 + str(sum(1 for kk in fields if kk.startswith(k + ".")) + 2)
884 )
886 # Retrieve document form fields
887 form_fields = self.get_fields()
888 if form_fields is None:
889 return {}
890 ff = {}
891 for field, value in form_fields.items():
892 if value.get("/FT") == "/Tx":
893 if full_qualified_name:
894 ff[field] = value.get("/V")
895 else:
896 ff[indexed_key(cast(str, value.get("/T", field)), ff)] = value.get("/V")
897 return ff
899 def get_pages_showing_field(
900 self, field: Union[Field, PdfObject, IndirectObject]
901 ) -> list[PageObject]:
902 """
903 Provides list of pages where the field is called.
905 Args:
906 field: Field Object, PdfObject or IndirectObject referencing a Field
908 Returns:
909 List of pages:
910 - Empty list:
911 The field has no widgets attached
912 (either hidden field or ancestor field).
913 - Single page list:
914 Page where the widget is present
915 (most common).
916 - Multi-page list:
917 Field with multiple kids widgets
918 (example: radio buttons, field repeated on multiple pages).
920 """
921 try:
922 # to cope with all types
923 field = cast(DictionaryObject, field.indirect_reference.get_object()) # type: ignore[union-attr]
924 except Exception as exc:
925 raise ValueError("Field type is invalid") from exc
926 if is_null_or_none(field.get_inherited(key="/FT", default=None)):
927 raise ValueError("Field is not valid")
928 ret = []
929 if field.get("/Subtype", "") == "/Widget":
930 if "/P" in field:
931 ret = [field["/P"].get_object()]
932 else:
933 ret = [
934 p
935 for p in self.pages
936 if field.indirect_reference in p.get("/Annots", "")
937 ]
938 else:
939 kids = field.get("/Kids", ())
940 for k in kids:
941 k = k.get_object()
942 if (k.get("/Subtype", "") == "/Widget") and ("/T" not in k):
943 # Kid that is just a widget, not a field:
944 if "/P" in k:
945 ret += [k["/P"].get_object()]
946 else:
947 ret += [
948 p
949 for p in self.pages
950 if k.indirect_reference in p.get("/Annots", "")
951 ]
952 return [
953 x
954 if isinstance(x, PageObject)
955 else (self.pages[self._get_page_number_by_indirect(x.indirect_reference)]) # type: ignore[index, union-attr]
956 for x in ret
957 ]
959 @property
960 def open_destination(
961 self,
962 ) -> Union[Destination, TextStringObject, ByteStringObject, None]:
963 """
964 Property to access the opening destination (``/OpenAction`` entry in
965 the PDF catalog). It returns ``None`` if the entry does not exist
966 or is not set.
968 Raises:
969 Exception: If a destination is invalid.
971 """
972 if "/OpenAction" not in self.root_object:
973 return None
974 oa: Any = self.root_object["/OpenAction"]
975 if isinstance(oa, bytes): # pragma: no cover
976 oa = oa.decode()
977 if isinstance(oa, str):
978 return create_string_object(oa)
979 if isinstance(oa, ArrayObject):
980 try:
981 page, typ, *array = oa
982 fit = Fit(typ, tuple(array))
983 return Destination("OpenAction", page, fit)
984 except Exception as exc:
985 raise Exception(f"Invalid Destination {oa}: {exc}") from exc
986 else:
987 return None
989 @open_destination.setter
990 def open_destination(self, dest: Union[str, Destination, PageObject, None]) -> None:
991 raise NotImplementedError("No setter for open_destination")
993 @property
994 def outline(self) -> OutlineType:
995 """
996 Read-only property for the outline present in the document
997 (i.e., a collection of 'outline items' which are also known as
998 'bookmarks').
999 """
1000 return self._get_outline()
1002 def _get_outline(
1003 self,
1004 *,
1005 node: Optional[DictionaryObject] = None,
1006 outline: Optional[Any] = None,
1007 visited: Optional[set[int]] = None,
1008 depth: int = 0,
1009 traversal_state: Optional[_TraversalState] = None
1010 ) -> OutlineType:
1011 if traversal_state is None:
1012 traversal_state = _TraversalState()
1014 if outline is None:
1015 outline = []
1016 catalog = self.root_object
1018 # get the outline dictionary and named destinations
1019 if Core.OUTLINES in catalog:
1020 lines = catalog[Core.OUTLINES]
1022 # A reference to an object which is missing or free in the xref table
1023 # resolves to None.
1024 if is_null_or_none(lines):
1025 return outline
1027 if not isinstance(lines, DictionaryObject):
1028 logger_warning(
1029 "Outlines are not a dictionary: %(lines)s",
1030 source=__name__,
1031 lines=lines,
1032 )
1033 return outline
1035 # §12.3.3 Document outline, entries in the outline dictionary
1036 if "/First" in lines:
1037 node = cast(DictionaryObject, lines["/First"])
1038 self._named_destinations = self._get_named_destinations()
1040 if node is None:
1041 return outline
1043 configuration = get_configuration()
1044 if depth > configuration.outline_maximum_depth:
1045 raise LimitReachedError(f"Maximum outline depth reached: {depth} > {configuration.outline_maximum_depth}.")
1047 # see if there are any more outline items
1048 if visited is None:
1049 visited = set()
1050 while True:
1051 node_id = id(node)
1052 if node_id in visited:
1053 logger_warning("Detected cycle in outline structure for %(node)s", source=__name__, node=node)
1054 break
1055 visited.add(node_id)
1056 traversal_state.entry_count += 1
1057 if traversal_state.entry_count > configuration.outline_maximum_entries:
1058 raise LimitReachedError(
1059 f"Maximum outline entry limit reached: "
1060 f"{traversal_state.entry_count} > {configuration.outline_maximum_entries}."
1061 )
1063 if not isinstance(node, DictionaryObject):
1064 logger_warning(
1065 "Outline node is not a dictionary: %(node)s",
1066 source=__name__,
1067 node=node,
1068 )
1069 break
1071 outline_obj = self._build_outline_item(node)
1072 if outline_obj:
1073 outline.append(outline_obj)
1075 # check for sub-outline
1076 if "/First" in node:
1077 sub_outline: list[Any] = []
1078 # Pass a copy to allow multiple outer entries to reference the same inner one.
1079 inner_visited = visited.copy()
1080 self._get_outline(
1081 node=cast(DictionaryObject, node["/First"]),
1082 outline=sub_outline,
1083 visited=inner_visited,
1084 depth=depth + 1,
1085 traversal_state=traversal_state,
1086 )
1087 if sub_outline:
1088 outline.append(sub_outline)
1090 if "/Next" not in node:
1091 break
1092 node = cast(DictionaryObject, node["/Next"])
1094 return outline
1096 @property
1097 def threads(self) -> Optional[ArrayObject]:
1098 """
1099 Read-only property for the list of threads.
1101 See §12.4.3 from the PDF 1.7 or 2.0 specification.
1103 It is an array of dictionaries with "/F" (the first bead in the thread)
1104 and "/I" (a thread information dictionary containing information about
1105 the thread, such as its title, author, and creation date) properties or
1106 None if there are no articles.
1108 Since PDF 2.0 it can also contain an indirect reference to a metadata
1109 stream containing information about the thread, such as its title,
1110 author, and creation date.
1111 """
1112 catalog = self.root_object
1113 if Core.THREADS in catalog:
1114 return cast("ArrayObject", catalog[Core.THREADS])
1115 return None
1117 @abstractmethod
1118 def _get_page_number_by_indirect(
1119 self, indirect_reference: Union[int, NullObject, IndirectObject, None]
1120 ) -> Optional[int]:
1121 ... # pragma: no cover
1123 def get_page_number(self, page: PageObject) -> Optional[int]:
1124 """
1125 Retrieve page number of a given PageObject.
1127 Args:
1128 page: The page to get page number. Should be
1129 an instance of :class:`PageObject<pypdf._page.PageObject>`
1131 Returns:
1132 The page number or None if page is not found
1134 """
1135 return self._get_page_number_by_indirect(page.indirect_reference)
1137 def get_destination_page_number(self, destination: Destination) -> Optional[int]:
1138 """
1139 Retrieve page number of a given Destination object.
1141 Args:
1142 destination: The destination to get page number.
1144 Returns:
1145 The page number or None if page is not found
1147 """
1148 return self._get_page_number_by_indirect(destination.page)
1150 def _build_destination(
1151 self,
1152 title: Union[str, bytes],
1153 array: Optional[ArrayObject],
1154 ) -> Destination:
1155 page, typ = None, None
1156 # A valid destination is an array of at least a page and a fit type.
1157 # Anything else (a name, a bare number, a NullObject, None, ...) cannot
1158 # be unpacked below, so treat it as a missing destination.
1159 if not isinstance(array, ArrayObject) or len(array) < 2:
1160 page = NullObject()
1161 return Destination(title, page, Fit.fit())
1162 page, typ, *fit_args = array
1163 try:
1164 return Destination(title, page, Fit(fit_type=typ, fit_args=fit_args))
1165 except PdfReadError:
1166 logger_warning(
1167 "Unknown destination: %(title)r %(fit_args)s",
1168 source=__name__,
1169 title=title,
1170 fit_args=fit_args,
1171 )
1172 if self.strict:
1173 raise
1174 # create a link to first Page
1175 tmp = self.pages[0].indirect_reference
1176 indirect_reference = NullObject() if tmp is None else tmp
1177 return Destination(title, indirect_reference, Fit.fit())
1179 def _build_outline_item(self, node: DictionaryObject) -> Optional[Destination]:
1180 dest, title, outline_item = None, None, None
1182 # title required for valid outline
1183 # §12.3.3, entries in an outline item dictionary
1184 if "/Title" in node:
1185 title = cast("str", node["/Title"])
1186 else:
1187 if self.strict:
1188 raise PdfReadError(f"Outline Entry Missing /Title attribute: {node!r}")
1189 title = ""
1191 if "/A" in node:
1192 # Action, PDF 1.7 and PDF 2.0 §12.6 (only type GoTo supported)
1193 action = cast(DictionaryObject, node["/A"])
1194 if GoToActionArguments.S in action:
1195 action_type = cast(NameObject, action[GoToActionArguments.S])
1196 if action_type == "/GoTo":
1197 if GoToActionArguments.D in action:
1198 dest = action[GoToActionArguments.D]
1199 elif self.strict:
1200 raise PdfReadError(f"Outline Action Missing /D attribute: {node!r}")
1201 elif self.strict:
1202 raise PdfReadError(f"Outline Action Missing /S attribute: {node!r}")
1203 elif "/Dest" in node:
1204 # Destination, PDF 1.7 and PDF 2.0 §12.3.2
1205 dest = node["/Dest"]
1206 # if array was referenced in another object, will be a dict w/ key "/D"
1207 if isinstance(dest, DictionaryObject) and "/D" in dest:
1208 dest = dest["/D"]
1210 if isinstance(dest, ArrayObject):
1211 outline_item = self._build_destination(title, dest)
1212 elif isinstance(dest, str):
1213 # named destination, addresses NameObject Issue #193
1214 # TODO: Keep named destination instead of replacing it?
1215 try:
1216 outline_item = self._build_destination(
1217 title, self._named_destinations[dest].dest_array
1218 )
1219 except KeyError:
1220 # named destination not found in Name Dict
1221 outline_item = self._build_destination(title, None)
1222 elif dest is None:
1223 # outline item not required to have destination or action
1224 # PDFv1.7 Table 153
1225 outline_item = self._build_destination(title, dest)
1226 else:
1227 if self.strict:
1228 raise PdfReadError(f"Unexpected destination {dest!r}")
1229 logger_warning(
1230 "Removed unexpected destination %(dest)r from destination",
1231 source=__name__,
1232 dest=dest,
1233 )
1234 outline_item = self._build_destination(title, None)
1236 # if outline item created, add color, format, and child count if present
1237 if outline_item:
1238 if "/C" in node:
1239 # Color of outline item font in (R, G, B) with values ranging 0.0-1.0
1240 color = node["/C"]
1241 if isinstance(color, list):
1242 outline_item[NameObject("/C")] = ArrayObject(FloatObject(c) for c in color)
1243 else:
1244 logger_warning(
1245 "Ignoring non-array outline color %(color)r",
1246 source=__name__,
1247 color=color,
1248 )
1249 if "/F" in node:
1250 # specifies style characteristics bold and/or italic
1251 # with 1=italic, 2=bold, 3=both
1252 outline_item[NameObject("/F")] = node["/F"]
1253 if "/Count" in node:
1254 # absolute value = num. visible children
1255 # with positive = open/unfolded, negative = closed/folded
1256 outline_item[NameObject("/Count")] = node["/Count"]
1257 # if count is 0 we will consider it as open (to have available is_open)
1258 outline_item[NameObject("/%is_open%")] = BooleanObject(
1259 node.get("/Count", 0) >= 0
1260 )
1261 outline_item.node = node
1262 try:
1263 outline_item.indirect_reference = node.indirect_reference
1264 except AttributeError:
1265 pass
1266 return outline_item
1268 @property
1269 def pages(self) -> Sequence[PageObject]:
1270 """
1271 Property that emulates a list of :class:`PageObject<pypdf._page.PageObject>`.
1272 This property allows to get a page or a range of pages.
1274 The returned object supports indexing, slicing, ``len()``, iteration and
1275 (for PdfWriter) ``del``, but it is not a :class:`list` - pages are looked
1276 up on demand rather than materialised up front, so list-only operations
1277 such as ``append()`` or concatenation with ``+`` are not available.
1279 Note:
1280 For PdfWriter only: Provides the capability to remove a page/range of
1281 page from the list (using the del operator). Remember: Only the page
1282 entry is removed, as the objects beneath can be used elsewhere. A
1283 solution to completely remove them - if they are not used anywhere - is
1284 to write to a buffer/temporary file and then load it into a new
1285 PdfWriter.
1287 """
1288 return _VirtualList(self.get_num_pages, self.get_page)
1290 @property
1291 def page_labels(self) -> list[str]:
1292 """
1293 A list of labels for the pages in this document.
1295 This property is read-only. The labels are in the order that the pages
1296 appear in the document.
1297 """
1298 return [page_index2page_label(self, i) for i in range(len(self.pages))]
1300 @property
1301 def page_layout(self) -> Optional[str]:
1302 """
1303 Get the page layout currently being used.
1305 .. list-table:: Valid ``layout`` values
1306 :widths: 50 200
1308 * - /NoLayout
1309 - Layout explicitly not specified
1310 * - /SinglePage
1311 - Show one page at a time
1312 * - /OneColumn
1313 - Show one column at a time
1314 * - /TwoColumnLeft
1315 - Show pages in two columns, odd-numbered pages on the left
1316 * - /TwoColumnRight
1317 - Show pages in two columns, odd-numbered pages on the right
1318 * - /TwoPageLeft
1319 - Show two pages at a time, odd-numbered pages on the left
1320 * - /TwoPageRight
1321 - Show two pages at a time, odd-numbered pages on the right
1322 """
1323 try:
1324 return cast(NameObject, self.root_object[CA.PAGE_LAYOUT])
1325 except KeyError:
1326 return None
1328 @property
1329 def page_mode(self) -> Optional[PagemodeType]:
1330 """
1331 Get the page mode currently being used.
1333 .. list-table:: Valid ``mode`` values
1334 :widths: 50 200
1336 * - /UseNone
1337 - Do not show outline or thumbnails panels
1338 * - /UseOutlines
1339 - Show outline (aka bookmarks) panel
1340 * - /UseThumbs
1341 - Show page thumbnails panel
1342 * - /FullScreen
1343 - Fullscreen view
1344 * - /UseOC
1345 - Show Optional Content Group (OCG) panel
1346 * - /UseAttachments
1347 - Show attachments panel
1348 """
1349 try:
1350 return self.root_object["/PageMode"] # type: ignore[return-value]
1351 except KeyError:
1352 return None
1354 def _flatten(self, list_only: bool = False) -> None:
1355 """
1356 Process the document pages to ease searching.
1358 Attributes of a page may inherit from ancestor nodes
1359 in the page tree. Flattening means moving
1360 any inheritance data into descendant nodes,
1361 effectively removing the inheritance dependency.
1362 The result is stored in ``self.flattened_pages``.
1364 Note: It is distinct from another use of "flattening" applied to PDFs.
1365 Flattening a PDF also means combining all the contents into one single layer
1366 and making the file less editable.
1368 The page tree is walked with an explicit stack instead of recursion, so the
1369 traversal is bound by ``Configuration.page_tree_maximum_depth`` and
1370 ``Configuration.page_tree_maximum_entries`` only, not by the interpreter
1371 recursion limit.
1373 Args:
1374 list_only: If True, the page's own entries are not copied into the
1375 generated :class:`PageObject`. Attributes inherited from ancestor
1376 nodes are applied either way. Note that a page reached through an
1377 indirect reference is already populated by :class:`PageObject`
1378 itself, so this only has an effect for ``/Kids`` entries that are
1379 inline dictionaries.
1381 """
1382 configuration = get_configuration()
1383 self.flattened_pages = None
1384 pages = self.root_object.get("/Pages", NullObject()).get_object()
1385 if not isinstance(pages, DictionaryObject):
1386 raise PdfReadError("Invalid object in /Pages")
1388 flattened_pages: list[PageObject] = []
1389 traversal_state = _TraversalState()
1390 # id() values of the nodes on the path from the root to the current node.
1391 # Detects multi-hop cycles such as A→B→C→A that the single-parent check misses.
1392 ancestor_ids: set[int] = set()
1393 # The /Pages nodes of that path whose /Kids are not exhausted yet.
1394 stack: list[_PageTreeFrame] = []
1395 item: Optional[_PageTreeItem] = _PageTreeItem(
1396 node=pages, inherit={}, indirect_reference=None, depth=0
1397 )
1398 while item is not None:
1399 if item.depth > configuration.page_tree_maximum_depth:
1400 raise LimitReachedError(
1401 f"Maximum page tree depth reached: {item.depth} > {configuration.page_tree_maximum_depth}."
1402 )
1404 # Get the current node_type
1405 node = item.node
1406 if PagesAttributes.TYPE in node:
1407 node_type = cast(str, node[PagesAttributes.TYPE])
1408 # if the page tree node has no /Type, consider as a page if /Kids is also missing
1409 elif PagesAttributes.KIDS not in node:
1410 # Without /Type, only accept it as a page if it carries a structural page key.
1411 if self.strict and not any(key in node for key in (PG.CONTENTS, PG.MEDIABOX, PG.PARENT)):
1412 raise PdfReadError(f"Non-page object reached through /Kids: {node!r}")
1413 node_type = "/Page"
1414 else:
1415 node_type = "/Pages"
1417 # Flatten that type of node
1418 if node_type == "/Pages":
1419 ancestor_ids.add(id(node))
1420 stack.append(self._enter_page_tree_node(item))
1421 elif node_type == "/Page":
1422 flattened_pages.append(self._flatten_leaf_page(item, list_only))
1424 item = self._pop_next_page_tree_kid(stack, ancestor_ids, traversal_state)
1426 # Assigned only here, so that a failed traversal does not leave a partial page list behind.
1427 self.flattened_pages = flattened_pages
1429 def _enter_page_tree_node(self, item: _PageTreeItem) -> _PageTreeFrame:
1430 """
1431 Start walking an intermediate ``/Pages`` node: propagate its inheritable
1432 attributes and pair it with an iterator over its ``/Kids``.
1433 """
1434 pages = item.node
1435 # Copied before being modified, as the kids of the enclosing node share it.
1436 inherit = dict(item.inherit)
1437 for attr in _INHERITABLE_PAGE_ATTRIBUTES:
1438 if attr in pages:
1439 inherit[attr] = pages[attr]
1441 kids = pages.get(PagesAttributes.KIDS, ArrayObject()).get_object()
1442 if isinstance(kids, NullObject):
1443 kids = ArrayObject()
1444 elif not isinstance(kids, ArrayObject):
1445 raise PdfReadError(f"Expected /Kids to be an array, got {type(kids).__name__}.")
1447 return item._replace(inherit=inherit), iter(kids)
1449 def _pop_next_page_tree_kid(
1450 self,
1451 stack: list[_PageTreeFrame],
1452 ancestor_ids: set[int],
1453 traversal_state: _TraversalState,
1454 ) -> Optional[_PageTreeItem]:
1455 """
1456 Return the next page tree node to flatten, leaving the ``/Pages`` nodes whose
1457 ``/Kids`` are exhausted, or ``None`` when the traversal is complete.
1459 Cyclic references and the configured page-tree entry limit are enforced here.
1460 """
1461 configuration = get_configuration()
1462 while stack:
1463 parent, kids = stack[-1]
1464 pages_reference = getattr(parent.node, "indirect_reference", object())
1465 for page in kids:
1466 if getattr(page, "indirect_reference", object()) == pages_reference:
1467 raise PdfReadError("Detected cyclic page references.")
1469 obj = page.get_object()
1470 if not isinstance(obj, DictionaryObject):
1471 if not is_null_or_none(obj):
1472 logger_warning(
1473 "Ignoring page tree entry that is not a dictionary: %(entry)s",
1474 source=__name__,
1475 entry=obj,
1476 )
1477 # damaged file may have invalid child in /Pages
1478 continue
1479 if not obj:
1480 # An empty dictionary would otherwise be read as a blank page.
1481 continue
1482 if id(obj) in ancestor_ids:
1483 raise PdfReadError("Detected cyclic page references.")
1484 traversal_state.entry_count += 1
1485 if traversal_state.entry_count > configuration.page_tree_maximum_entries:
1486 raise LimitReachedError(
1487 "Maximum page tree entry limit reached: "
1488 f"{traversal_state.entry_count} > {configuration.page_tree_maximum_entries}."
1489 )
1490 return _PageTreeItem(
1491 node=obj,
1492 inherit=parent.inherit,
1493 indirect_reference=page if isinstance(page, IndirectObject) else None,
1494 depth=parent.depth + 1,
1495 )
1496 stack.pop()
1497 ancestor_ids.discard(id(parent.node))
1498 return None
1500 def _flatten_leaf_page(self, item: _PageTreeItem, list_only: bool) -> PageObject:
1501 """
1502 Build the :class:`PageObject` for a leaf ``/Page`` node.
1504 ``list_only`` suppresses copying the page's own entries; the inherited
1505 attributes are applied regardless. When the node has an indirect reference,
1506 :class:`PageObject` has already copied those entries in its constructor,
1507 so the flag only matters for inline ``/Kids`` dictionaries.
1508 """
1509 page_obj = PageObject(self, item.indirect_reference)
1510 if not list_only:
1511 page_obj.update(item.node)
1512 for attr, value in item.inherit.items():
1513 # if the page has its own value, it does not inherit the parent's value
1514 if attr not in page_obj:
1515 page_obj[attr] = value
1517 return page_obj
1519 def remove_page(
1520 self,
1521 page: Union[int, PageObject, IndirectObject],
1522 clean: bool = False,
1523 ) -> None:
1524 """
1525 Remove page from pages list.
1527 Args:
1528 page:
1529 * :class:`int`: Page number to be removed.
1530 * :class:`~pypdf._page.PageObject`: page to be removed. If the page appears many times
1531 only the first one will be removed.
1532 * :class:`~pypdf.generic.IndirectObject`: Reference to page to be removed.
1534 clean: replace PageObject with NullObject to prevent annotations
1535 or destinations to reference a detached page.
1537 """
1538 if self.flattened_pages is None:
1539 self._flatten(self._readonly)
1540 assert self.flattened_pages is not None
1541 if isinstance(page, IndirectObject):
1542 p = page.get_object()
1543 if not isinstance(p, PageObject):
1544 logger_warning("IndirectObject is not referencing a page", source=__name__)
1545 return
1546 page = p
1548 if not isinstance(page, int):
1549 try:
1550 page = self.flattened_pages.index(page)
1551 except ValueError:
1552 logger_warning("Cannot find page in pages", source=__name__)
1553 return
1554 if not (0 <= page < len(self.flattened_pages)):
1555 logger_warning("Page number is out of range", source=__name__)
1556 return
1558 ind = self.pages[page].indirect_reference
1559 # `pages` is typed as a Sequence because it is not a list, but the
1560 # concrete _VirtualList does implement deletion.
1561 del cast(_VirtualList, self.pages)[page]
1562 if clean and ind is not None:
1563 self._replace_object(ind, NullObject())
1565 def _get_indirect_object(self, num: int, gen: int) -> Optional[PdfObject]:
1566 """
1567 Used to ease development.
1569 This is equivalent to generic.IndirectObject(num,gen,self).get_object()
1571 Args:
1572 num: The object number of the indirect object.
1573 gen: The generation number of the indirect object.
1575 Returns:
1576 A PdfObject
1578 """
1579 return IndirectObject(num, gen, self).get_object()
1581 def decode_permissions(
1582 self, permissions_code: int
1583 ) -> NoReturn: # pragma: no cover
1584 """Take the permissions as an integer, return the allowed access."""
1585 deprecation_with_replacement(
1586 old_name="decode_permissions",
1587 new_name="user_access_permissions",
1588 removed_in="5.0.0",
1589 )
1591 @property
1592 def user_access_permissions(self) -> Optional[UserAccessPermissions]:
1593 """
1594 Get the user access permissions for encrypted documents.
1595 Returns None if not encrypted.
1597 .. warning::
1599 For AES-256 encrypted documents (R=5/R=6), the returned
1600 permissions are derived from the ``/P`` field, which is
1601 only trustworthy if the ``/Perms`` integrity check passed.
1602 Check :attr:`are_permissions_valid` to verify.
1603 """
1604 if self._encryption is None:
1605 return None
1606 return UserAccessPermissions(self._encryption.P)
1608 @property
1609 def are_permissions_valid(self) -> Optional[bool]:
1610 """
1611 Whether the ``/Perms`` integrity check passed for this document.
1613 For AES-256 encrypted documents (R=5/R=6), the ``/Perms`` field
1614 is an encrypted copy of the permissions that can be verified
1615 independently. Returns ``False`` if this check fails (the ``/P``
1616 permissions may have been tampered with).
1618 Returns ``None`` if the document is not encrypted or has not yet
1619 been decrypted via :meth:`decrypt()<pypdf.PdfReader.decrypt>`.
1620 Returns ``True`` for non-AES-256 encryption (no ``/Perms`` to check).
1621 """
1622 if self._encryption is None:
1623 return None
1624 if not self._encryption.is_decrypted():
1625 return None
1626 return self._encryption._are_permissions_valid
1628 @property
1629 @abstractmethod
1630 def is_encrypted(self) -> bool:
1631 """
1632 Read-only boolean property showing whether this PDF file is encrypted.
1634 Note that this property, if true, will remain true even after the
1635 :meth:`decrypt()<pypdf.PdfReader.decrypt>` method is called.
1636 """
1637 ... # pragma: no cover
1639 @property
1640 def xfa(self) -> Optional[dict[str, Any]]:
1641 retval: dict[str, Any] = {}
1642 catalog = self.root_object
1644 if "/AcroForm" not in catalog or not catalog["/AcroForm"]:
1645 return None
1647 tree = cast(TreeObject, catalog["/AcroForm"])
1649 if "/XFA" in tree:
1650 fields = tree["/XFA"].get_object()
1651 if not isinstance(fields, ArrayObject):
1652 logger_warning(
1653 "XFA entry is not an array: %(fields)s",
1654 source=__name__,
1655 fields=fields,
1656 )
1657 return retval
1658 i = iter(fields)
1659 for f in i:
1660 tag = f
1661 f = next(i, None)
1662 if isinstance(f, IndirectObject):
1663 field = cast(Optional[EncodedStreamObject], f.get_object())
1664 if field:
1665 es = _decompress_with_limit(field._data)
1666 retval[tag] = es
1667 return retval
1669 @property
1670 def attachments(self) -> Mapping[str, list[bytes]]:
1671 """Mapping of attachment filenames to their content."""
1672 entries: dict[str, list[EmbeddedFile]] = {}
1674 for entry in self.attachment_list:
1675 for name in entry._names:
1676 entries.setdefault(name, []).append(entry)
1678 return LazyDict(
1679 {
1680 name: (self._get_attachment_contents, attachment_entries)
1681 for name, attachment_entries in entries.items()
1682 }
1683 )
1685 @classmethod
1686 def _get_attachment_contents(cls, entries: list[EmbeddedFile]) -> list[bytes]:
1687 return [entry.content for entry in entries]
1689 @property
1690 def attachment_list(self) -> Iterator[EmbeddedFile]:
1691 """Iterable of attachment objects."""
1692 yield from EmbeddedFile._load(self.root_object, strict=self.strict)
1694 @abstractmethod
1695 def _repr_mimebundle_(
1696 self,
1697 include: Union[Iterable[str], None] = None,
1698 exclude: Union[Iterable[str], None] = None,
1699 ) -> dict[str, Any]:
1700 """
1701 Integration into Jupyter Notebooks.
1703 This method returns a dictionary that maps a mime-type to its
1704 representation.
1706 .. seealso::
1708 https://ipython.readthedocs.io/en/stable/config/integrating.html
1709 """
1710 ... # pragma: no cover
1713class LazyDict(Mapping[Any, Any]):
1714 def __init__(self, *args: Any, **kwargs: Any) -> None:
1715 self._raw_dict = dict(*args, **kwargs)
1717 def __getitem__(self, key: str) -> Any:
1718 func, arg = self._raw_dict.__getitem__(key)
1719 return func(arg)
1721 def __iter__(self) -> Iterator[Any]:
1722 return iter(self._raw_dict)
1724 def __len__(self) -> int:
1725 return len(self._raw_dict)
1727 def __str__(self) -> str:
1728 return f"LazyDict(keys={list(self.keys())})"