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 Generator, Iterable, Iterator, Mapping, Sequence
34from datetime import datetime
35from typing import (
36 Any,
37 NoReturn,
38 Optional,
39 Union,
40 cast,
41)
43from ._encryption import Encryption
44from ._page import PageObject, _VirtualList
45from ._page_labels import index2label as page_index2page_label
46from ._utils import (
47 _TraversalState,
48 deprecation_with_replacement,
49 logger_warning,
50 parse_iso8824_date,
51)
52from .constants import CatalogAttributes as CA
53from .constants import (
54 CheckboxRadioButtonAttributes,
55 Core,
56 GoToActionArguments,
57 PagesAttributes,
58 UserAccessPermissions,
59)
60from .constants import DocumentInformationAttributes as DI
61from .constants import FieldDictionaryAttributes as FA
62from .constants import PageAttributes as PG
63from .errors import LimitReachedError, PdfReadError, PyPdfError
64from .filters import _decompress_with_limit
65from .generic import (
66 ArrayObject,
67 BooleanObject,
68 ByteStringObject,
69 Destination,
70 DictionaryObject,
71 EncodedStreamObject,
72 Field,
73 Fit,
74 FloatObject,
75 IndirectObject,
76 NameObject,
77 NullObject,
78 PdfObject,
79 TextStringObject,
80 TreeObject,
81 ViewerPreferences,
82 create_string_object,
83 is_null_or_none,
84)
85from .generic._files import EmbeddedFile
86from .types import OutlineType, PagemodeType
87from .xmp import XmpInformation
89# TODO: Make configurable.
90OUTLINE_MAX_ENTRIES = 100_000
91OUTLINE_MAX_DEPTH = 100
92PAGE_TREE_MAX_ENTRIES = 100_000
93PAGE_TREE_MAX_DEPTH = 100
96def convert_to_int(d: bytes, size: int) -> Union[int, tuple[Any, ...]]:
97 if size > 8:
98 raise PdfReadError("Invalid size in convert_to_int")
99 d = b"\x00\x00\x00\x00\x00\x00\x00\x00" + d
100 d = d[-8:]
101 return cast(int, struct.unpack(">Q", d)[0])
104class DocumentInformation(DictionaryObject):
105 """
106 A class representing the basic document metadata provided in a PDF File.
107 This class is accessible through
108 :py:class:`PdfReader.metadata<pypdf.PdfReader.metadata>`.
110 All text properties of the document metadata have
111 *two* properties, e.g. author and author_raw. The non-raw property will
112 always return a ``TextStringObject``, making it ideal for a case where the
113 metadata is being displayed. The raw property can sometimes return a
114 ``ByteStringObject``, if pypdf was unable to decode the string's text
115 encoding; this requires additional safety in the caller and therefore is not
116 as commonly accessed.
117 """
119 def __init__(self) -> None:
120 DictionaryObject.__init__(self)
122 def _get_text(self, key: str) -> Optional[str]:
123 retval = self.get(key, None)
124 if isinstance(retval, TextStringObject):
125 return retval
126 if isinstance(retval, ByteStringObject):
127 return str(retval)
128 return None
130 @property
131 def title(self) -> Optional[str]:
132 """
133 Read-only property accessing the document's title.
135 Returns a ``TextStringObject`` or ``None`` if the title is not
136 specified.
137 """
138 return (
139 self._get_text(DI.TITLE) or self.get(DI.TITLE).get_object() # type: ignore[union-attr]
140 if self.get(DI.TITLE)
141 else None
142 )
144 @property
145 def title_raw(self) -> Optional[str]:
146 """The "raw" version of title; can return a ``ByteStringObject``."""
147 return self.get(DI.TITLE)
149 @property
150 def author(self) -> Optional[str]:
151 """
152 Read-only property accessing the document's author.
154 Returns a ``TextStringObject`` or ``None`` if the author is not
155 specified.
156 """
157 return self._get_text(DI.AUTHOR)
159 @property
160 def author_raw(self) -> Optional[str]:
161 """The "raw" version of author; can return a ``ByteStringObject``."""
162 return self.get(DI.AUTHOR)
164 @property
165 def subject(self) -> Optional[str]:
166 """
167 Read-only property accessing the document's subject.
169 Returns a ``TextStringObject`` or ``None`` if the subject is not
170 specified.
171 """
172 return self._get_text(DI.SUBJECT)
174 @property
175 def subject_raw(self) -> Optional[str]:
176 """The "raw" version of subject; can return a ``ByteStringObject``."""
177 return self.get(DI.SUBJECT)
179 @property
180 def creator(self) -> Optional[str]:
181 """
182 Read-only property accessing the document's creator.
184 If the document was converted to PDF from another format, this is the
185 name of the application (e.g. OpenOffice) that created the original
186 document from which it was converted. Returns a ``TextStringObject`` or
187 ``None`` if the creator is not specified.
188 """
189 return self._get_text(DI.CREATOR)
191 @property
192 def creator_raw(self) -> Optional[str]:
193 """The "raw" version of creator; can return a ``ByteStringObject``."""
194 return self.get(DI.CREATOR)
196 @property
197 def producer(self) -> Optional[str]:
198 """
199 Read-only property accessing the document's producer.
201 If the document was converted to PDF from another format, this is the
202 name of the application (for example, macOS Quartz) that converted it to
203 PDF. Returns a ``TextStringObject`` or ``None`` if the producer is not
204 specified.
205 """
206 return self._get_text(DI.PRODUCER)
208 @property
209 def producer_raw(self) -> Optional[str]:
210 """The "raw" version of producer; can return a ``ByteStringObject``."""
211 return self.get(DI.PRODUCER)
213 @property
214 def creation_date(self) -> Optional[datetime]:
215 """Read-only property accessing the document's creation date."""
216 return parse_iso8824_date(self._get_text(DI.CREATION_DATE))
218 @property
219 def creation_date_raw(self) -> Optional[str]:
220 """
221 The "raw" version of creation date; can return a ``ByteStringObject``.
223 Typically in the format ``D:YYYYMMDDhhmmss[+Z-]hh'mm`` where the suffix
224 is the offset from UTC.
225 """
226 return self.get(DI.CREATION_DATE)
228 @property
229 def modification_date(self) -> Optional[datetime]:
230 """
231 Read-only property accessing the document's modification date.
233 The date and time the document was most recently modified.
234 """
235 return parse_iso8824_date(self._get_text(DI.MOD_DATE))
237 @property
238 def modification_date_raw(self) -> Optional[str]:
239 """
240 The "raw" version of modification date; can return a
241 ``ByteStringObject``.
243 Typically, in the format ``D:YYYYMMDDhhmmss[+Z-]hh'mm`` where the suffix
244 is the offset from UTC.
245 """
246 return self.get(DI.MOD_DATE)
248 @property
249 def keywords(self) -> Optional[str]:
250 """
251 Read-only property accessing the document's keywords.
253 Returns a ``TextStringObject`` or ``None`` if keywords are not
254 specified.
255 """
256 return self._get_text(DI.KEYWORDS)
258 @property
259 def keywords_raw(self) -> Optional[str]:
260 """The "raw" version of keywords; can return a ``ByteStringObject``."""
261 return self.get(DI.KEYWORDS)
264class PdfDocCommon(ABC):
265 """
266 Common functions from PdfWriter and PdfReader objects.
268 This root class is strongly abstracted.
269 """
271 strict: bool = False # default
273 flattened_pages: Optional[list[PageObject]] = None
275 _encryption: Optional[Encryption] = None
277 _readonly: bool = False
279 @property
280 @abstractmethod
281 def root_object(self) -> DictionaryObject:
282 ... # pragma: no cover
284 @property
285 @abstractmethod
286 def pdf_header(self) -> str:
287 ... # pragma: no cover
289 @abstractmethod
290 def get_object(
291 self, indirect_reference: Union[int, IndirectObject]
292 ) -> Optional[PdfObject]:
293 ... # pragma: no cover
295 @abstractmethod
296 def _replace_object(self, indirect: IndirectObject, obj: PdfObject) -> PdfObject:
297 ... # pragma: no cover
299 @property
300 @abstractmethod
301 def _info(self) -> Optional[DictionaryObject]:
302 ... # pragma: no cover
304 @property
305 def metadata(self) -> Optional[DocumentInformation]:
306 """
307 Retrieve the PDF file's document information dictionary, if it exists.
309 Note that some PDF files use metadata streams instead of document
310 information dictionaries, and these metadata streams will not be
311 accessed by this function.
312 """
313 retval = DocumentInformation()
314 if self._info is None:
315 return None
316 retval.update(self._info)
317 return retval
319 @property
320 @abstractmethod
321 def xmp_metadata(self) -> Optional[XmpInformation]:
322 ... # pragma: no cover
324 @property
325 def viewer_preferences(self) -> Optional[ViewerPreferences]:
326 """Returns the existing ViewerPreferences as an overloaded dictionary."""
327 o = self.root_object.get(CA.VIEWER_PREFERENCES, None)
328 if o is None:
329 return None
330 o = o.get_object()
331 if not isinstance(o, DictionaryObject):
332 logger_warning(
333 "Viewer preferences are not a dictionary: %(preferences)s",
334 source=__name__,
335 preferences=o,
336 )
337 return None
338 if not isinstance(o, ViewerPreferences):
339 o = ViewerPreferences(o)
340 if hasattr(o, "indirect_reference") and o.indirect_reference is not None:
341 self._replace_object(o.indirect_reference, o)
342 else:
343 self.root_object[NameObject(CA.VIEWER_PREFERENCES)] = o
344 return o
346 def get_num_pages(self) -> int:
347 """
348 Calculate the number of pages in this PDF file.
350 Returns:
351 The number of pages of the parsed PDF file.
353 Raises:
354 PdfReadError: If restrictions prevent this action.
356 """
357 # Flattened pages will not work on an encrypted PDF;
358 # the PDF file's page count is used in this case. Otherwise,
359 # the original method (flattened page count) is used.
360 if self.is_encrypted:
361 return self.root_object["/Pages"]["/Count"] # type: ignore[no-any-return, index]
362 if self.flattened_pages is None:
363 self._flatten(self._readonly)
364 assert self.flattened_pages is not None
365 return len(self.flattened_pages)
367 def get_page(self, page_number: int) -> PageObject:
368 """
369 Retrieve a page by number from this PDF file.
370 Most of the time ``.pages[page_number]`` is preferred.
372 Args:
373 page_number: The page number to retrieve
374 (pages begin at zero)
376 Returns:
377 A :class:`PageObject<pypdf._page.PageObject>` instance.
379 """
380 if self.flattened_pages is None:
381 self._flatten(self._readonly)
382 assert self.flattened_pages is not None, "mypy"
383 return self.flattened_pages[page_number]
385 def _get_page_in_node(
386 self,
387 page_number: int,
388 ) -> tuple[DictionaryObject, int]:
389 """
390 Retrieve the node and position within the /Kids containing the page.
391 If page_number is greater than the number of pages, it returns the top node, -1.
392 """
393 top = cast(DictionaryObject, self.root_object["/Pages"])
394 visited: set[int] = set()
396 def recursive_call(
397 _node: DictionaryObject, mi: int
398 ) -> tuple[Optional[PdfObject], int]:
399 _node_id = id(_node)
400 if _node_id in visited:
401 raise LimitReachedError("Detected cycle in /Pages hierarchy when retrieving page.")
402 visited.add(_node_id)
403 ma = cast(int, _node.get("/Count", 1)) # default 1 for /Page types
404 if _node.get("/Type") == "/Page":
405 if page_number == mi:
406 return _node, -1
407 return None, mi + 1
408 if (page_number - mi) >= ma: # not in nodes below
409 if _node == top:
410 return top, -1
411 return None, mi + ma
412 for _idx, kid in enumerate(cast(ArrayObject, _node["/Kids"])):
413 kid = cast(DictionaryObject, kid.get_object())
414 n, i = recursive_call(kid, mi)
415 if n is not None: # page has just been found ...
416 if i < 0: # ... just below!
417 return _node, _idx
418 # ... at lower levels
419 return n, i
420 mi = i
421 raise PyPdfError("Unexpectedly cannot find the node.")
423 node, idx = recursive_call(top, 0)
424 assert isinstance(node, DictionaryObject), "mypy"
425 return node, idx
427 @property
428 def named_destinations(self) -> dict[str, Destination]:
429 """A read-only dictionary which maps names to destinations."""
430 return self._get_named_destinations()
432 def get_named_dest_root(self) -> ArrayObject:
433 named_dest = ArrayObject()
434 if CA.NAMES in self.root_object and isinstance(
435 self.root_object[CA.NAMES], DictionaryObject
436 ):
437 names = cast(DictionaryObject, self.root_object[CA.NAMES])
438 if CA.DESTS in names and isinstance(names[CA.DESTS], DictionaryObject):
439 # §3.6.3 Name Dictionary (PDF spec 1.7)
440 dests = cast(DictionaryObject, names[CA.DESTS])
441 dests_ref = dests.indirect_reference
442 if CA.NAMES in dests:
443 # §7.9.6, entries in a name tree node dictionary
444 named_dest = cast(ArrayObject, dests[CA.NAMES])
445 else:
446 named_dest = ArrayObject()
447 dests[NameObject(CA.NAMES)] = named_dest
448 elif hasattr(self, "_add_object"):
449 dests = DictionaryObject()
450 dests_ref = self._add_object(dests)
451 names[NameObject(CA.DESTS)] = dests_ref
452 dests[NameObject(CA.NAMES)] = named_dest
454 elif hasattr(self, "_add_object"):
455 names = DictionaryObject()
456 names_ref = self._add_object(names)
457 self.root_object[NameObject(CA.NAMES)] = names_ref
458 dests = DictionaryObject()
459 dests_ref = self._add_object(dests)
460 names[NameObject(CA.DESTS)] = dests_ref
461 dests[NameObject(CA.NAMES)] = named_dest
463 return named_dest
465 ## common
466 def _get_named_destinations(
467 self,
468 *,
469 tree: Optional[DictionaryObject] = None,
470 retval: Optional[dict[str, Destination]] = None,
471 visited: Optional[set[int]] = None,
472 ) -> dict[str, Destination]:
473 """
474 Retrieve the named destinations present in the document.
476 Args:
477 tree: The current tree.
478 retval: The previously retrieved destinations for nested calls.
479 visited: Already known/visited objects.
481 Returns:
482 A dictionary which maps names to destinations.
484 """
485 if visited is None:
486 visited = set()
487 if retval is None:
488 retval = {}
489 catalog = self.root_object
491 # get the name tree
492 if CA.DESTS in catalog:
493 tree = cast(DictionaryObject, catalog[CA.DESTS])
494 elif CA.NAMES in catalog:
495 names = cast(DictionaryObject, catalog[CA.NAMES])
496 if CA.DESTS in names:
497 tree = cast(DictionaryObject, names[CA.DESTS])
499 if is_null_or_none(tree):
500 return retval
501 assert tree is not None, "mypy"
503 tree_id = id(tree)
504 if tree_id in visited:
505 logger_warning("Detected cycle in destination tree.", source=__name__)
506 return retval
507 visited.add(tree_id)
509 if PagesAttributes.KIDS in tree:
510 # recurse down the tree
511 for kid in cast(ArrayObject, tree[PagesAttributes.KIDS]):
512 self._get_named_destinations(tree=kid.get_object(), retval=retval, visited=visited)
513 # §7.9.6, entries in a name tree node dictionary
514 elif CA.NAMES in tree: # /Kids and /Names are exclusives (§7.9.6)
515 names = cast(DictionaryObject, tree[CA.NAMES])
516 i = 0
517 while i < len(names):
518 key = names[i].get_object()
519 i += 1
520 if not isinstance(key, (bytes, str)):
521 continue
522 try:
523 value = names[i].get_object()
524 except IndexError:
525 break
526 i += 1
527 if isinstance(value, DictionaryObject):
528 if "/D" in value:
529 value = value["/D"]
530 else:
531 continue
532 dest = self._build_destination(key, value)
533 if dest is not None:
534 retval[cast(str, dest["/Title"])] = dest
535 # Remain backwards-compatible.
536 retval[str(key)] = dest
537 else: # case where /Dests is in the document's catalog dictionary (PDF 1.7 specs, §2 about PDF 1.1)
538 for k__, v__ in tree.items():
539 val = v__.get_object()
540 if isinstance(val, DictionaryObject):
541 if "/D" in val:
542 val = val["/D"].get_object()
543 else:
544 continue
545 dest = self._build_destination(k__, val)
546 if dest is not None:
547 retval[k__] = dest
548 return retval
550 # A select group of relevant field attributes. For the complete list,
551 # see §12.3.2 of the PDF 1.7 or PDF 2.0 specification.
553 def get_fields(
554 self,
555 tree: Optional[DictionaryObject] = None,
556 retval: Optional[dict[Any, Any]] = None,
557 fileobj: Optional[Any] = None,
558 stack: Optional[list[PdfObject]] = None,
559 ) -> Optional[dict[str, Any]]:
560 """
561 Extract field data if this PDF contains interactive form fields.
563 The *tree*, *retval*, *stack* parameters are for recursive use.
565 Args:
566 tree: Current object to parse.
567 retval: In-progress list of fields.
568 fileobj: A file object (usually a text file) to write
569 a report to on all interactive form fields found.
570 stack: List of already parsed objects.
572 Returns:
573 A dictionary where each key is a field name, and each
574 value is a :class:`Field<pypdf.generic.Field>` object. By
575 default, the mapping name is used for keys.
576 ``None`` if form data could not be located.
578 """
579 field_attributes = FA.attributes_dict()
580 field_attributes.update(CheckboxRadioButtonAttributes.attributes_dict())
581 if retval is None:
582 retval = {}
583 catalog = self.root_object
584 stack = []
585 # get the AcroForm tree
586 if CA.ACRO_FORM in catalog:
587 tree = cast(Optional[DictionaryObject], catalog[CA.ACRO_FORM])
588 else:
589 return None
590 if tree is None:
591 return retval
592 assert stack is not None
593 if "/Fields" in tree:
594 fields = tree["/Fields"].get_object()
595 if not isinstance(fields, ArrayObject):
596 logger_warning(
597 "AcroForm /Fields is not an array: %(fields)s",
598 source=__name__,
599 fields=fields,
600 )
601 return retval
602 for f in fields:
603 field = f.get_object()
604 self._build_field(field, retval, fileobj, field_attributes, stack)
605 elif any(attr in tree for attr in field_attributes):
606 # Tree is a field
607 self._build_field(tree, retval, fileobj, field_attributes, stack)
608 return retval
610 def _get_qualified_field_name(
611 self,
612 *,
613 parent: DictionaryObject,
614 visited: Optional[set[int]] = None
615 ) -> str:
616 if visited is None:
617 visited = set()
618 parent_id = id(parent)
619 if parent_id in visited:
620 raise LimitReachedError("Detected cycle in /Parent hierarchy when retrieving qualified field name.")
621 visited.add(parent_id)
623 if "/TM" in parent:
624 return cast(str, parent["/TM"])
625 if "/Parent" in parent:
626 return (
627 self._get_qualified_field_name(
628 parent=cast(DictionaryObject, parent["/Parent"]),
629 visited=visited,
630 )
631 + "."
632 + cast(str, parent.get("/T", ""))
633 )
634 return cast(str, parent.get("/T", ""))
636 @staticmethod
637 def _normal_appearance(appearance: Any) -> DictionaryObject:
638 """Return the /N normal-appearance sub-dictionary of an /AP entry."""
639 appearance = appearance.get_object()
640 if not isinstance(appearance, DictionaryObject):
641 raise PdfReadError(f"Expected appearance dictionary, got {appearance!r}")
642 normal = appearance.get("/N")
643 if normal is not None:
644 normal = normal.get_object()
645 if not isinstance(normal, DictionaryObject):
646 raise PdfReadError(f"Expected /N appearance dictionary, got {normal!r}")
647 return normal
649 def _build_field(
650 self,
651 field: Union[TreeObject, DictionaryObject],
652 retval: dict[Any, Any],
653 fileobj: Any,
654 field_attributes: Any,
655 stack: list[PdfObject],
656 ) -> None:
657 if all(attr not in field for attr in ("/T", "/TM")):
658 return
659 key = self._get_qualified_field_name(parent=field)
660 if fileobj:
661 self._write_field(fileobj, field, field_attributes)
662 fileobj.write("\n")
663 retval[key] = Field(field)
664 obj = retval[key].indirect_reference.get_object() # to get the full object
665 if obj.get(FA.FT, "") == "/Ch" and obj.get(NameObject(FA.Opt)):
666 retval[key][NameObject("/_States_")] = obj[NameObject(FA.Opt)]
667 if obj.get(FA.FT, "") == "/Btn" and "/AP" in obj:
668 # Checkbox
669 normal = self._normal_appearance(obj["/AP"])
670 retval[key][NameObject("/_States_")] = ArrayObject(list(normal.keys()))
671 if "/Off" not in retval[key]["/_States_"]:
672 retval[key][NameObject("/_States_")].append(NameObject("/Off"))
673 elif obj.get(FA.FT, "") == "/Btn" and obj.get(FA.Ff, 0) & FA.FfBits.Radio != 0:
674 states: list[str] = []
675 retval[key][NameObject("/_States_")] = ArrayObject(states)
676 for k in obj.get(FA.Kids, {}):
677 k = k.get_object()
678 if "/AP" not in k:
679 raise PdfReadError(f"Button field kid missing /AP: {k!r}")
680 normal = self._normal_appearance(k["/AP"])
681 for s in list(normal.keys()):
682 if s not in states:
683 states.append(s)
684 retval[key][NameObject("/_States_")] = ArrayObject(states)
685 if (
686 obj.get(FA.Ff, 0) & FA.FfBits.NoToggleToOff != 0
687 and "/Off" in retval[key]["/_States_"]
688 ):
689 del retval[key]["/_States_"][retval[key]["/_States_"].index("/Off")]
690 # at last for order
691 self._check_kids(field, retval, fileobj, stack)
693 def _check_kids(
694 self,
695 tree: Union[TreeObject, DictionaryObject],
696 retval: Any,
697 fileobj: Any,
698 stack: list[PdfObject],
699 ) -> None:
700 if tree in stack:
701 logger_warning(
702 "%(field_name)s already parsed",
703 source=__name__,
704 field_name=self._get_qualified_field_name(parent=tree),
705 )
706 return
707 stack.append(tree)
708 if PagesAttributes.KIDS in tree:
709 # recurse down the tree
710 for kid in tree[PagesAttributes.KIDS]: # type: ignore[attr-defined]
711 kid = kid.get_object()
712 self.get_fields(kid, retval, fileobj, stack)
714 def _write_field(self, fileobj: Any, field: Any, field_attributes: Any) -> None:
715 field_attributes_tuple = FA.attributes()
716 field_attributes_tuple = (
717 field_attributes_tuple + CheckboxRadioButtonAttributes.attributes()
718 )
720 for attr in field_attributes_tuple:
721 if attr in (
722 FA.Kids,
723 FA.AA,
724 ):
725 continue
726 attr_name = field_attributes[attr]
727 try:
728 if attr == FA.FT:
729 # Make the field type value clearer
730 types = {
731 "/Btn": "Button",
732 "/Tx": "Text",
733 "/Ch": "Choice",
734 "/Sig": "Signature",
735 }
736 if field[attr] in types:
737 fileobj.write(f"{attr_name}: {types[field[attr]]}\n")
738 elif attr == FA.Parent:
739 # Let's just write the name of the parent
740 try:
741 name = field[attr][FA.TM]
742 except KeyError:
743 name = field[attr][FA.T]
744 fileobj.write(f"{attr_name}: {name}\n")
745 else:
746 fileobj.write(f"{attr_name}: {field[attr]}\n")
747 except KeyError:
748 # Field attribute is N/A or unknown, so don't write anything
749 pass
751 def get_form_text_fields(self, full_qualified_name: bool = False) -> dict[str, Any]:
752 """
753 Retrieve form fields from the document with textual data.
755 Args:
756 full_qualified_name: to get full name
758 Returns:
759 A dictionary. The key is the name of the form field,
760 the value is the content of the field.
762 If the document contains multiple form fields with the same name, the
763 second and following will get the suffix .2, .3, ...
765 """
767 def indexed_key(k: str, fields: dict[Any, Any]) -> str:
768 if k not in fields:
769 return k
770 return (
771 k
772 + "."
773 + str(sum(1 for kk in fields if kk.startswith(k + ".")) + 2)
774 )
776 # Retrieve document form fields
777 form_fields = self.get_fields()
778 if form_fields is None:
779 return {}
780 ff = {}
781 for field, value in form_fields.items():
782 if value.get("/FT") == "/Tx":
783 if full_qualified_name:
784 ff[field] = value.get("/V")
785 else:
786 ff[indexed_key(cast(str, value.get("/T", field)), ff)] = value.get("/V")
787 return ff
789 def get_pages_showing_field(
790 self, field: Union[Field, PdfObject, IndirectObject]
791 ) -> list[PageObject]:
792 """
793 Provides list of pages where the field is called.
795 Args:
796 field: Field Object, PdfObject or IndirectObject referencing a Field
798 Returns:
799 List of pages:
800 - Empty list:
801 The field has no widgets attached
802 (either hidden field or ancestor field).
803 - Single page list:
804 Page where the widget is present
805 (most common).
806 - Multi-page list:
807 Field with multiple kids widgets
808 (example: radio buttons, field repeated on multiple pages).
810 """
811 try:
812 # to cope with all types
813 field = cast(DictionaryObject, field.indirect_reference.get_object()) # type: ignore[union-attr]
814 except Exception as exc:
815 raise ValueError("Field type is invalid") from exc
816 if is_null_or_none(field.get_inherited(key="/FT", default=None)):
817 raise ValueError("Field is not valid")
818 ret = []
819 if field.get("/Subtype", "") == "/Widget":
820 if "/P" in field:
821 ret = [field["/P"].get_object()]
822 else:
823 ret = [
824 p
825 for p in self.pages
826 if field.indirect_reference in p.get("/Annots", "")
827 ]
828 else:
829 kids = field.get("/Kids", ())
830 for k in kids:
831 k = k.get_object()
832 if (k.get("/Subtype", "") == "/Widget") and ("/T" not in k):
833 # Kid that is just a widget, not a field:
834 if "/P" in k:
835 ret += [k["/P"].get_object()]
836 else:
837 ret += [
838 p
839 for p in self.pages
840 if k.indirect_reference in p.get("/Annots", "")
841 ]
842 return [
843 x
844 if isinstance(x, PageObject)
845 else (self.pages[self._get_page_number_by_indirect(x.indirect_reference)]) # type: ignore[index, union-attr]
846 for x in ret
847 ]
849 @property
850 def open_destination(
851 self,
852 ) -> Union[Destination, TextStringObject, ByteStringObject, None]:
853 """
854 Property to access the opening destination (``/OpenAction`` entry in
855 the PDF catalog). It returns ``None`` if the entry does not exist
856 or is not set.
858 Raises:
859 Exception: If a destination is invalid.
861 """
862 if "/OpenAction" not in self.root_object:
863 return None
864 oa: Any = self.root_object["/OpenAction"]
865 if isinstance(oa, bytes): # pragma: no cover
866 oa = oa.decode()
867 if isinstance(oa, str):
868 return create_string_object(oa)
869 if isinstance(oa, ArrayObject):
870 try:
871 page, typ, *array = oa
872 fit = Fit(typ, tuple(array))
873 return Destination("OpenAction", page, fit)
874 except Exception as exc:
875 raise Exception(f"Invalid Destination {oa}: {exc}")
876 else:
877 return None
879 @open_destination.setter
880 def open_destination(self, dest: Union[str, Destination, PageObject, None]) -> None:
881 raise NotImplementedError("No setter for open_destination")
883 @property
884 def outline(self) -> OutlineType:
885 """
886 Read-only property for the outline present in the document
887 (i.e., a collection of 'outline items' which are also known as
888 'bookmarks').
889 """
890 return self._get_outline()
892 def _get_outline(
893 self,
894 *,
895 node: Optional[DictionaryObject] = None,
896 outline: Optional[Any] = None,
897 visited: Optional[set[int]] = None,
898 depth: int = 0,
899 traversal_state: Optional[_TraversalState] = None
900 ) -> OutlineType:
901 if traversal_state is None:
902 traversal_state = _TraversalState()
904 if outline is None:
905 outline = []
906 catalog = self.root_object
908 # get the outline dictionary and named destinations
909 if Core.OUTLINES in catalog:
910 lines = cast(DictionaryObject, catalog[Core.OUTLINES])
912 if isinstance(lines, NullObject):
913 return outline
915 # §12.3.3 Document outline, entries in the outline dictionary
916 if not is_null_or_none(lines) and "/First" in lines:
917 node = cast(DictionaryObject, lines["/First"])
918 self._named_destinations = self._get_named_destinations()
920 if node is None:
921 return outline
923 if depth > OUTLINE_MAX_DEPTH:
924 raise LimitReachedError(f"Maximum outline depth reached: {depth} > {OUTLINE_MAX_DEPTH}.")
926 # see if there are any more outline items
927 if visited is None:
928 visited = set()
929 while True:
930 node_id = id(node)
931 if node_id in visited:
932 logger_warning("Detected cycle in outline structure for %(node)s", source=__name__, node=node)
933 break
934 visited.add(node_id)
935 traversal_state.entry_count += 1
936 if traversal_state.entry_count > OUTLINE_MAX_ENTRIES:
937 raise LimitReachedError(
938 f"Maximum outline entry limit reached: {traversal_state.entry_count} > {OUTLINE_MAX_ENTRIES}."
939 )
941 if not isinstance(node, DictionaryObject):
942 logger_warning(
943 "Outline node is not a dictionary: %(node)s",
944 source=__name__,
945 node=node,
946 )
947 break
949 outline_obj = self._build_outline_item(node)
950 if outline_obj:
951 outline.append(outline_obj)
953 # check for sub-outline
954 if "/First" in node:
955 sub_outline: list[Any] = []
956 # Pass a copy to allow multiple outer entries to reference the same inner one.
957 inner_visited = visited.copy()
958 self._get_outline(
959 node=cast(DictionaryObject, node["/First"]),
960 outline=sub_outline,
961 visited=inner_visited,
962 depth=depth + 1,
963 traversal_state=traversal_state,
964 )
965 if sub_outline:
966 outline.append(sub_outline)
968 if "/Next" not in node:
969 break
970 node = cast(DictionaryObject, node["/Next"])
972 return outline
974 @property
975 def threads(self) -> Optional[ArrayObject]:
976 """
977 Read-only property for the list of threads.
979 See §12.4.3 from the PDF 1.7 or 2.0 specification.
981 It is an array of dictionaries with "/F" (the first bead in the thread)
982 and "/I" (a thread information dictionary containing information about
983 the thread, such as its title, author, and creation date) properties or
984 None if there are no articles.
986 Since PDF 2.0 it can also contain an indirect reference to a metadata
987 stream containing information about the thread, such as its title,
988 author, and creation date.
989 """
990 catalog = self.root_object
991 if Core.THREADS in catalog:
992 return cast("ArrayObject", catalog[Core.THREADS])
993 return None
995 @abstractmethod
996 def _get_page_number_by_indirect(
997 self, indirect_reference: Union[int, NullObject, IndirectObject, None]
998 ) -> Optional[int]:
999 ... # pragma: no cover
1001 def get_page_number(self, page: PageObject) -> Optional[int]:
1002 """
1003 Retrieve page number of a given PageObject.
1005 Args:
1006 page: The page to get page number. Should be
1007 an instance of :class:`PageObject<pypdf._page.PageObject>`
1009 Returns:
1010 The page number or None if page is not found
1012 """
1013 return self._get_page_number_by_indirect(page.indirect_reference)
1015 def get_destination_page_number(self, destination: Destination) -> Optional[int]:
1016 """
1017 Retrieve page number of a given Destination object.
1019 Args:
1020 destination: The destination to get page number.
1022 Returns:
1023 The page number or None if page is not found
1025 """
1026 return self._get_page_number_by_indirect(destination.page)
1028 def _build_destination(
1029 self,
1030 title: Union[str, bytes],
1031 array: Optional[ArrayObject],
1032 ) -> Destination:
1033 page, typ = None, None
1034 # A valid destination is an array of at least a page and a fit type.
1035 # Anything else (a name, a bare number, a NullObject, None, ...) cannot
1036 # be unpacked below, so treat it as a missing destination.
1037 if not isinstance(array, ArrayObject) or len(array) < 2:
1038 page = NullObject()
1039 return Destination(title, page, Fit.fit())
1040 page, typ, *array = array # type: ignore[assignment]
1041 try:
1042 return Destination(title, page, Fit(fit_type=typ, fit_args=array))
1043 except PdfReadError:
1044 logger_warning("Unknown destination: %(title)r %(array)s", source=__name__, title=title, array=array)
1045 if self.strict:
1046 raise
1047 # create a link to first Page
1048 tmp = self.pages[0].indirect_reference
1049 indirect_reference = NullObject() if tmp is None else tmp
1050 return Destination(title, indirect_reference, Fit.fit())
1052 def _build_outline_item(self, node: DictionaryObject) -> Optional[Destination]:
1053 dest, title, outline_item = None, None, None
1055 # title required for valid outline
1056 # §12.3.3, entries in an outline item dictionary
1057 try:
1058 title = cast("str", node["/Title"])
1059 except KeyError:
1060 if self.strict:
1061 raise PdfReadError(f"Outline Entry Missing /Title attribute: {node!r}")
1062 title = ""
1064 if "/A" in node:
1065 # Action, PDF 1.7 and PDF 2.0 §12.6 (only type GoTo supported)
1066 action = cast(DictionaryObject, node["/A"])
1067 action_type = cast(NameObject, action[GoToActionArguments.S])
1068 if action_type == "/GoTo":
1069 if GoToActionArguments.D in action:
1070 dest = action[GoToActionArguments.D]
1071 elif self.strict:
1072 raise PdfReadError(f"Outline Action Missing /D attribute: {node!r}")
1073 elif "/Dest" in node:
1074 # Destination, PDF 1.7 and PDF 2.0 §12.3.2
1075 dest = node["/Dest"]
1076 # if array was referenced in another object, will be a dict w/ key "/D"
1077 if isinstance(dest, DictionaryObject) and "/D" in dest:
1078 dest = dest["/D"]
1080 if isinstance(dest, ArrayObject):
1081 outline_item = self._build_destination(title, dest)
1082 elif isinstance(dest, str):
1083 # named destination, addresses NameObject Issue #193
1084 # TODO: Keep named destination instead of replacing it?
1085 try:
1086 outline_item = self._build_destination(
1087 title, self._named_destinations[dest].dest_array
1088 )
1089 except KeyError:
1090 # named destination not found in Name Dict
1091 outline_item = self._build_destination(title, None)
1092 elif dest is None:
1093 # outline item not required to have destination or action
1094 # PDFv1.7 Table 153
1095 outline_item = self._build_destination(title, dest)
1096 else:
1097 if self.strict:
1098 raise PdfReadError(f"Unexpected destination {dest!r}")
1099 logger_warning(
1100 "Removed unexpected destination %(dest)r from destination",
1101 source=__name__,
1102 dest=dest,
1103 )
1104 outline_item = self._build_destination(title, None)
1106 # if outline item created, add color, format, and child count if present
1107 if outline_item:
1108 if "/C" in node:
1109 # Color of outline item font in (R, G, B) with values ranging 0.0-1.0
1110 color = node["/C"]
1111 if isinstance(color, list):
1112 outline_item[NameObject("/C")] = ArrayObject(FloatObject(c) for c in color)
1113 else:
1114 logger_warning(
1115 "Ignoring non-array outline color %(color)r",
1116 source=__name__,
1117 color=color,
1118 )
1119 if "/F" in node:
1120 # specifies style characteristics bold and/or italic
1121 # with 1=italic, 2=bold, 3=both
1122 outline_item[NameObject("/F")] = node["/F"]
1123 if "/Count" in node:
1124 # absolute value = num. visible children
1125 # with positive = open/unfolded, negative = closed/folded
1126 outline_item[NameObject("/Count")] = node["/Count"]
1127 # if count is 0 we will consider it as open (to have available is_open)
1128 outline_item[NameObject("/%is_open%")] = BooleanObject(
1129 node.get("/Count", 0) >= 0
1130 )
1131 outline_item.node = node
1132 try:
1133 outline_item.indirect_reference = node.indirect_reference
1134 except AttributeError:
1135 pass
1136 return outline_item
1138 @property
1139 def pages(self) -> Sequence[PageObject]:
1140 """
1141 Property that emulates a list of :class:`PageObject<pypdf._page.PageObject>`.
1142 This property allows to get a page or a range of pages.
1144 The returned object supports indexing, slicing, ``len()``, iteration and
1145 (for PdfWriter) ``del``, but it is not a :class:`list` - pages are looked
1146 up on demand rather than materialised up front, so list-only operations
1147 such as ``append()`` or concatenation with ``+`` are not available.
1149 Note:
1150 For PdfWriter only: Provides the capability to remove a page/range of
1151 page from the list (using the del operator). Remember: Only the page
1152 entry is removed, as the objects beneath can be used elsewhere. A
1153 solution to completely remove them - if they are not used anywhere - is
1154 to write to a buffer/temporary file and then load it into a new
1155 PdfWriter.
1157 """
1158 return _VirtualList(self.get_num_pages, self.get_page)
1160 @property
1161 def page_labels(self) -> list[str]:
1162 """
1163 A list of labels for the pages in this document.
1165 This property is read-only. The labels are in the order that the pages
1166 appear in the document.
1167 """
1168 return [page_index2page_label(self, i) for i in range(len(self.pages))]
1170 @property
1171 def page_layout(self) -> Optional[str]:
1172 """
1173 Get the page layout currently being used.
1175 .. list-table:: Valid ``layout`` values
1176 :widths: 50 200
1178 * - /NoLayout
1179 - Layout explicitly not specified
1180 * - /SinglePage
1181 - Show one page at a time
1182 * - /OneColumn
1183 - Show one column at a time
1184 * - /TwoColumnLeft
1185 - Show pages in two columns, odd-numbered pages on the left
1186 * - /TwoColumnRight
1187 - Show pages in two columns, odd-numbered pages on the right
1188 * - /TwoPageLeft
1189 - Show two pages at a time, odd-numbered pages on the left
1190 * - /TwoPageRight
1191 - Show two pages at a time, odd-numbered pages on the right
1192 """
1193 try:
1194 return cast(NameObject, self.root_object[CA.PAGE_LAYOUT])
1195 except KeyError:
1196 return None
1198 @property
1199 def page_mode(self) -> Optional[PagemodeType]:
1200 """
1201 Get the page mode currently being used.
1203 .. list-table:: Valid ``mode`` values
1204 :widths: 50 200
1206 * - /UseNone
1207 - Do not show outline or thumbnails panels
1208 * - /UseOutlines
1209 - Show outline (aka bookmarks) panel
1210 * - /UseThumbs
1211 - Show page thumbnails panel
1212 * - /FullScreen
1213 - Fullscreen view
1214 * - /UseOC
1215 - Show Optional Content Group (OCG) panel
1216 * - /UseAttachments
1217 - Show attachments panel
1218 """
1219 try:
1220 return self.root_object["/PageMode"] # type: ignore[return-value]
1221 except KeyError:
1222 return None
1224 def _flatten(
1225 self,
1226 list_only: bool = False,
1227 pages: Union[DictionaryObject, PageObject, None] = None,
1228 inherit: Optional[dict[str, Any]] = None,
1229 indirect_reference: Optional[IndirectObject] = None,
1230 visited: Optional[set[int]] = None,
1231 depth: int = 0,
1232 traversal_state: Optional[_TraversalState] = None,
1233 ) -> None:
1234 """
1235 Process the document pages to ease searching.
1237 Attributes of a page may inherit from ancestor nodes
1238 in the page tree. Flattening means moving
1239 any inheritance data into descendant nodes,
1240 effectively removing the inheritance dependency.
1242 Note: It is distinct from another use of "flattening" applied to PDFs.
1243 Flattening a PDF also means combining all the contents into one single layer
1244 and making the file less editable.
1246 Args:
1247 list_only: Will only list the pages within _flatten_pages.
1248 pages:
1249 inherit:
1250 indirect_reference: Used recursively to flatten the /Pages object.
1251 visited: Set of id() values on the active page-tree traversal path.
1252 Detects multi-hop cycles such as A→B→C→A that the single-parent
1253 check misses.
1254 depth: Current page-tree traversal depth.
1255 traversal_state: State shared across the complete traversal.
1257 """
1258 inheritable_page_attributes = (
1259 NameObject(PG.RESOURCES),
1260 NameObject(PG.MEDIABOX),
1261 NameObject(PG.CROPBOX),
1262 NameObject(PG.ROTATE),
1263 )
1264 if inherit is None:
1265 inherit = {}
1266 if visited is None:
1267 visited = set()
1268 if traversal_state is None:
1269 traversal_state = _TraversalState()
1270 if depth > PAGE_TREE_MAX_DEPTH:
1271 raise LimitReachedError(f"Maximum page tree depth reached: {depth} > {PAGE_TREE_MAX_DEPTH}.")
1272 if is_null_or_none(pages):
1273 # Fix issue 327: set flattened_pages attribute only for
1274 # decrypted file
1275 catalog = self.root_object
1276 pages = catalog.get("/Pages").get_object() # type: ignore[union-attr]
1277 if not isinstance(pages, DictionaryObject):
1278 raise PdfReadError("Invalid object in /Pages")
1279 self.flattened_pages = []
1280 assert pages is not None, "mypy"
1282 if PagesAttributes.TYPE in pages:
1283 t = cast(str, pages[PagesAttributes.TYPE])
1284 # if the page tree node has no /Type, consider as a page if /Kids is also missing
1285 elif PagesAttributes.KIDS not in pages:
1286 # Without /Type, only accept it as a page if it carries a structural page key.
1287 if self.strict and not any(
1288 key in pages for key in (PG.CONTENTS, PG.MEDIABOX, PG.PARENT)
1289 ):
1290 raise PdfReadError(f"Non-page object reached through /Kids: {pages!r}")
1291 t = "/Page"
1292 else:
1293 t = "/Pages"
1295 if t == "/Pages":
1296 for attr in inheritable_page_attributes:
1297 if attr in pages:
1298 inherit[attr] = pages[attr]
1299 pages_reference = getattr(pages, "indirect_reference", object())
1300 kids = pages.get(PagesAttributes.KIDS, ArrayObject()).get_object()
1301 if isinstance(kids, NullObject):
1302 kids = ArrayObject()
1303 elif not isinstance(kids, ArrayObject):
1304 raise PdfReadError(
1305 f"Expected /Kids to be an array, got {type(kids).__name__}."
1306 )
1307 for page in kids:
1308 if getattr(page, "indirect_reference", object()) == pages_reference:
1309 raise PdfReadError("Detected cyclic page references.")
1311 additional_arguments = {}
1312 if isinstance(page, IndirectObject):
1313 additional_arguments["indirect_reference"] = page
1314 obj = page.get_object()
1315 if obj:
1316 # damaged file may have invalid child in /Pages
1317 obj_id = id(obj)
1318 if obj_id in visited:
1319 raise PdfReadError("Detected cyclic page references.")
1320 traversal_state.entry_count += 1
1321 if traversal_state.entry_count > PAGE_TREE_MAX_ENTRIES:
1322 raise LimitReachedError(
1323 "Maximum page tree entry limit reached: "
1324 f"{traversal_state.entry_count} > {PAGE_TREE_MAX_ENTRIES}."
1325 )
1326 visited.add(obj_id)
1327 try:
1328 self._flatten(
1329 list_only,
1330 obj,
1331 inherit.copy(),
1332 visited=visited,
1333 depth=depth + 1,
1334 traversal_state=traversal_state,
1335 **additional_arguments,
1336 )
1337 finally:
1338 visited.remove(obj_id)
1339 elif t == "/Page":
1340 page_obj = PageObject(self, indirect_reference)
1341 if not list_only:
1342 page_obj.update(pages)
1343 for attr_in, value in inherit.items():
1344 # if the page has its own value, it does not inherit the
1345 # parent's value
1346 if attr_in not in page_obj:
1347 page_obj[attr_in] = value
1349 # TODO: Could flattened_pages be None at this point?
1350 self.flattened_pages.append(page_obj) # type: ignore[union-attr]
1352 def remove_page(
1353 self,
1354 page: Union[int, PageObject, IndirectObject],
1355 clean: bool = False,
1356 ) -> None:
1357 """
1358 Remove page from pages list.
1360 Args:
1361 page:
1362 * :class:`int`: Page number to be removed.
1363 * :class:`~pypdf._page.PageObject`: page to be removed. If the page appears many times
1364 only the first one will be removed.
1365 * :class:`~pypdf.generic.IndirectObject`: Reference to page to be removed.
1367 clean: replace PageObject with NullObject to prevent annotations
1368 or destinations to reference a detached page.
1370 """
1371 if self.flattened_pages is None:
1372 self._flatten(self._readonly)
1373 assert self.flattened_pages is not None
1374 if isinstance(page, IndirectObject):
1375 p = page.get_object()
1376 if not isinstance(p, PageObject):
1377 logger_warning("IndirectObject is not referencing a page", source=__name__)
1378 return
1379 page = p
1381 if not isinstance(page, int):
1382 try:
1383 page = self.flattened_pages.index(page)
1384 except ValueError:
1385 logger_warning("Cannot find page in pages", source=__name__)
1386 return
1387 if not (0 <= page < len(self.flattened_pages)):
1388 logger_warning("Page number is out of range", source=__name__)
1389 return
1391 ind = self.pages[page].indirect_reference
1392 # `pages` is typed as a Sequence because it is not a list, but the
1393 # concrete _VirtualList does implement deletion.
1394 del cast(_VirtualList, self.pages)[page]
1395 if clean and ind is not None:
1396 self._replace_object(ind, NullObject())
1398 def _get_indirect_object(self, num: int, gen: int) -> Optional[PdfObject]:
1399 """
1400 Used to ease development.
1402 This is equivalent to generic.IndirectObject(num,gen,self).get_object()
1404 Args:
1405 num: The object number of the indirect object.
1406 gen: The generation number of the indirect object.
1408 Returns:
1409 A PdfObject
1411 """
1412 return IndirectObject(num, gen, self).get_object()
1414 def decode_permissions(
1415 self, permissions_code: int
1416 ) -> NoReturn: # pragma: no cover
1417 """Take the permissions as an integer, return the allowed access."""
1418 deprecation_with_replacement(
1419 old_name="decode_permissions",
1420 new_name="user_access_permissions",
1421 removed_in="5.0.0",
1422 )
1424 @property
1425 def user_access_permissions(self) -> Optional[UserAccessPermissions]:
1426 """
1427 Get the user access permissions for encrypted documents.
1428 Returns None if not encrypted.
1430 .. warning::
1432 For AES-256 encrypted documents (R=5/R=6), the returned
1433 permissions are derived from the ``/P`` field, which is
1434 only trustworthy if the ``/Perms`` integrity check passed.
1435 Check :attr:`are_permissions_valid` to verify.
1436 """
1437 if self._encryption is None:
1438 return None
1439 return UserAccessPermissions(self._encryption.P)
1441 @property
1442 def are_permissions_valid(self) -> Optional[bool]:
1443 """
1444 Whether the ``/Perms`` integrity check passed for this document.
1446 For AES-256 encrypted documents (R=5/R=6), the ``/Perms`` field
1447 is an encrypted copy of the permissions that can be verified
1448 independently. Returns ``False`` if this check fails (the ``/P``
1449 permissions may have been tampered with).
1451 Returns ``None`` if the document is not encrypted or has not yet
1452 been decrypted via :meth:`decrypt()<pypdf.PdfReader.decrypt>`.
1453 Returns ``True`` for non-AES-256 encryption (no ``/Perms`` to check).
1454 """
1455 if self._encryption is None:
1456 return None
1457 if not self._encryption.is_decrypted():
1458 return None
1459 return self._encryption._are_permissions_valid
1461 @property
1462 @abstractmethod
1463 def is_encrypted(self) -> bool:
1464 """
1465 Read-only boolean property showing whether this PDF file is encrypted.
1467 Note that this property, if true, will remain true even after the
1468 :meth:`decrypt()<pypdf.PdfReader.decrypt>` method is called.
1469 """
1470 ... # pragma: no cover
1472 @property
1473 def xfa(self) -> Optional[dict[str, Any]]:
1474 retval: dict[str, Any] = {}
1475 catalog = self.root_object
1477 if "/AcroForm" not in catalog or not catalog["/AcroForm"]:
1478 return None
1480 tree = cast(TreeObject, catalog["/AcroForm"])
1482 if "/XFA" in tree:
1483 fields = cast(ArrayObject, tree["/XFA"])
1484 i = iter(fields)
1485 for f in i:
1486 tag = f
1487 f = next(i)
1488 if isinstance(f, IndirectObject):
1489 field = cast(Optional[EncodedStreamObject], f.get_object())
1490 if field:
1491 es = _decompress_with_limit(field._data)
1492 retval[tag] = es
1493 return retval
1495 @property
1496 def attachments(self) -> Mapping[str, list[bytes]]:
1497 """Mapping of attachment filenames to their content."""
1498 return LazyDict(
1499 {
1500 name: (self._get_attachment_list, name)
1501 for name in self._list_attachments()
1502 }
1503 )
1505 @property
1506 def attachment_list(self) -> Generator[EmbeddedFile, None, None]:
1507 """Iterable of attachment objects."""
1508 yield from EmbeddedFile._load(self.root_object)
1510 def _list_attachments(self) -> list[str]:
1511 """
1512 Retrieves the list of filenames of file attachments.
1514 Returns:
1515 list of filenames
1517 """
1518 names = []
1519 for entry in self.attachment_list:
1520 names.append(entry.name)
1521 if (name := entry.alternative_name) != entry.name and name:
1522 names.append(name)
1523 return names
1525 def _get_attachment_list(self, name: str) -> list[bytes]:
1526 out = self._get_attachments(name)[name]
1527 if isinstance(out, list):
1528 return out
1529 return [out]
1531 def _get_attachments(
1532 self, filename: Optional[str] = None
1533 ) -> dict[str, Union[bytes, list[bytes]]]:
1534 """
1535 Retrieves all or selected file attachments of the PDF as a dictionary of file names
1536 and the file data as a bytestring.
1538 Args:
1539 filename: If filename is None, then a dictionary of all attachments
1540 will be returned, where the key is the filename and the value
1541 is the content. Otherwise, a dictionary with just a single key
1542 - the filename - and its content will be returned.
1544 Returns:
1545 dictionary of filename -> Union[bytestring or List[ByteString]]
1546 If the filename exists multiple times a list of the different versions will be provided.
1548 """
1549 attachments: dict[str, Union[bytes, list[bytes]]] = {}
1550 for entry in self.attachment_list:
1551 names = set()
1552 alternative_name = entry.alternative_name
1553 if filename is not None:
1554 if filename in {entry.name, alternative_name}:
1555 name = entry.name if filename == entry.name else alternative_name
1556 names.add(name)
1557 else:
1558 continue
1559 else:
1560 names = {entry.name, alternative_name}
1562 for name in names:
1563 if name is None:
1564 continue
1565 if name in attachments:
1566 if not isinstance(attachments[name], list):
1567 attachments[name] = [attachments[name]] # type:ignore
1568 attachments[name].append(entry.content) # type:ignore
1569 else:
1570 attachments[name] = entry.content
1571 return attachments
1573 @abstractmethod
1574 def _repr_mimebundle_(
1575 self,
1576 include: Union[Iterable[str], None] = None,
1577 exclude: Union[Iterable[str], None] = None,
1578 ) -> dict[str, Any]:
1579 """
1580 Integration into Jupyter Notebooks.
1582 This method returns a dictionary that maps a mime-type to its
1583 representation.
1585 .. seealso::
1587 https://ipython.readthedocs.io/en/stable/config/integrating.html
1588 """
1589 ... # pragma: no cover
1592class LazyDict(Mapping[Any, Any]):
1593 def __init__(self, *args: Any, **kwargs: Any) -> None:
1594 self._raw_dict = dict(*args, **kwargs)
1596 def __getitem__(self, key: str) -> Any:
1597 func, arg = self._raw_dict.__getitem__(key)
1598 return func(arg)
1600 def __iter__(self) -> Iterator[Any]:
1601 return iter(self._raw_dict)
1603 def __len__(self) -> int:
1604 return len(self._raw_dict)
1606 def __str__(self) -> str:
1607 return f"LazyDict(keys={list(self.keys())})"