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