1# SPDX-FileCopyrightText: 2022 James R. Barlow
2# SPDX-License-Identifier: MPL-2.0
3
4"""Implement some features in Python and monkey-patch them onto C++ classes.
5
6In several cases the implementation of some higher levels features might as
7well be in Python. Fortunately we can attach Python methods to C++ class
8bindings after the fact.
9
10We can also move the implementation to C++ if desired.
11"""
12
13from __future__ import annotations
14
15import datetime
16import mimetypes
17import shutil
18from collections.abc import ItemsView, Iterator, KeysView, MutableMapping, ValuesView
19from contextlib import ExitStack, suppress
20from decimal import Decimal
21from io import BytesIO, RawIOBase
22from pathlib import Path
23from subprocess import run
24from tempfile import NamedTemporaryFile
25from typing import BinaryIO, Callable, TypeVar
26from warnings import warn
27
28from pikepdf._augments import augment_override_cpp, augments
29from pikepdf._core import (
30 AccessMode,
31 AttachedFile,
32 AttachedFileSpec,
33 Attachments,
34 NameTree,
35 NumberTree,
36 ObjectStreamMode,
37 Page,
38 Pdf,
39 Rectangle,
40 StreamDecodeLevel,
41 StreamParser,
42 Token,
43 _ObjectMapping,
44)
45from pikepdf._io import atomic_overwrite, check_different_files, check_stream_is_usable
46from pikepdf.models import Encryption, EncryptionInfo, Outline, Permissions
47from pikepdf.models.metadata import PdfMetadata, decode_pdf_date, encode_pdf_date
48from pikepdf.objects import Array, Dictionary, Name, Object, Stream
49
50# pylint: disable=no-member,unsupported-membership-test,unsubscriptable-object
51# mypy: ignore-errors
52
53__all__ = []
54
55Numeric = TypeVar('Numeric', int, float, Decimal)
56
57
58def _single_page_pdf(page: Page) -> bytes:
59 """Construct a single page PDF from the provided page in memory."""
60 pdf = Pdf.new()
61 pdf.pages.append(page)
62 bio = BytesIO()
63 pdf.save(bio)
64 bio.seek(0)
65 return bio.read()
66
67
68def _mudraw(buffer, fmt) -> bytes:
69 """Use mupdf draw to rasterize the PDF in the memory buffer."""
70 # mudraw cannot read from stdin so NamedTemporaryFile is required
71 with NamedTemporaryFile(suffix='.pdf') as tmp_in, NamedTemporaryFile() as tmp_out:
72 tmp_in.write(buffer)
73 tmp_in.seek(0)
74 tmp_in.flush()
75 run(
76 ['mutool', 'draw', '-F', fmt, '-o', tmp_out.name, tmp_in.name],
77 check=True,
78 )
79 return tmp_out.read()
80
81
82@augments(Object)
83class Extend_Object:
84 def _ipython_key_completions_(self):
85 if isinstance(self, (Dictionary, Stream)):
86 return self.keys()
87 return None
88
89 def emplace(self, other: Object, retain=(Name.Parent,)):
90 if not self.same_owner_as(other):
91 raise TypeError("Objects must have the same owner for emplace()")
92
93 # .keys() returns strings, so make all strings
94 retain = {str(k) for k in retain}
95 self_keys = set(self.keys())
96 other_keys = set(other.keys())
97
98 assert all(isinstance(k, str) for k in (retain | self_keys | other_keys))
99
100 del_keys = self_keys - other_keys - retain
101 for k in (k for k in other_keys if k not in retain):
102 self[k] = other[k] # pylint: disable=unsupported-assignment-operation
103 for k in del_keys:
104 del self[k] # pylint: disable=unsupported-delete-operation
105
106 def _type_check_write(self, filter_, decode_parms):
107 if isinstance(filter_, list):
108 filter_ = Array(filter_)
109 filter_ = filter_.wrap_in_array()
110
111 if isinstance(decode_parms, list):
112 decode_parms = Array(decode_parms)
113 elif decode_parms is None:
114 decode_parms = Array([])
115 else:
116 decode_parms = decode_parms.wrap_in_array()
117
118 if not all(isinstance(item, Name) for item in filter_):
119 raise TypeError(
120 "filter must be: pikepdf.Name or pikepdf.Array([pikepdf.Name])"
121 )
122 if not all(
123 (isinstance(item, Dictionary) or item is None) for item in decode_parms
124 ):
125 raise TypeError(
126 "decode_parms must be: pikepdf.Dictionary or "
127 "pikepdf.Array([pikepdf.Dictionary])"
128 )
129 if len(decode_parms) != 0 and len(filter_) != len(decode_parms):
130 raise ValueError(
131 f"filter ({repr(filter_)}) and decode_parms "
132 f"({repr(decode_parms)}) must be arrays of same length"
133 )
134 if len(filter_) == 1:
135 filter_ = filter_[0]
136 if len(decode_parms) == 0:
137 decode_parms = None
138 elif len(decode_parms) == 1:
139 decode_parms = decode_parms[0]
140 return filter_, decode_parms
141
142 def write(
143 self,
144 data: bytes,
145 *,
146 filter: Name | Array | None = None,
147 decode_parms: Dictionary | Array | None = None,
148 type_check: bool = True,
149 ): # pylint: disable=redefined-builtin
150 if type_check and filter is not None:
151 filter, decode_parms = self._type_check_write(filter, decode_parms)
152
153 self._write(data, filter=filter, decode_parms=decode_parms)
154
155
156@augments(Pdf)
157class Extend_Pdf:
158 def _quick_save(self):
159 bio = BytesIO()
160 self.save(bio)
161 bio.seek(0)
162 return bio
163
164 def _repr_mimebundle_(self, include=None, exclude=None): # pylint: disable=unused-argument
165 pdf_data = self._quick_save().read()
166 data = {
167 'application/pdf': pdf_data,
168 }
169 with suppress(FileNotFoundError, RuntimeError):
170 data['image/svg+xml'] = _mudraw(pdf_data, 'svg').decode('utf-8')
171 return data
172
173 @property
174 def docinfo(self) -> Dictionary:
175 if Name.Info not in self.trailer or not isinstance(
176 self.trailer.Info, Dictionary
177 ):
178 self.trailer.Info = self.make_indirect(Dictionary())
179 if not self.trailer.Info.is_indirect:
180 self.trailer.Info = self.make_indirect(self.trailer.Info)
181 return self.trailer.Info
182
183 @docinfo.setter
184 def docinfo(self, new_docinfo: Dictionary):
185 if not new_docinfo.is_indirect:
186 raise ValueError(
187 "docinfo must be an indirect object - use Pdf.make_indirect"
188 )
189 self.trailer.Info = new_docinfo
190
191 @docinfo.deleter
192 def docinfo(self):
193 if Name.Info in self.trailer:
194 del self.trailer.Info
195
196 def open_metadata(
197 self,
198 set_pikepdf_as_editor: bool = True,
199 update_docinfo: bool = True,
200 strict: bool = False,
201 ) -> PdfMetadata:
202 return PdfMetadata(
203 self,
204 pikepdf_mark=set_pikepdf_as_editor,
205 sync_docinfo=update_docinfo,
206 overwrite_invalid_xml=not strict,
207 )
208
209 def open_outline(self, max_depth: int = 15, strict: bool = False) -> Outline:
210 return Outline(self, max_depth=max_depth, strict=strict)
211
212 def make_stream(self, data: bytes, d=None, **kwargs) -> Stream:
213 return Stream(self, data, d, **kwargs)
214
215 def add_blank_page(
216 self, *, page_size: tuple[Numeric, Numeric] = (612.0, 792.0)
217 ) -> Page:
218 for dim in page_size:
219 if not (3 <= dim <= 14400):
220 raise ValueError('Page size must be between 3 and 14400 PDF units')
221
222 page_dict = Dictionary(
223 Type=Name.Page,
224 MediaBox=Array([0, 0, page_size[0], page_size[1]]),
225 Contents=self.make_stream(b''),
226 Resources=Dictionary(),
227 )
228 page_obj = self.make_indirect(page_dict)
229 self._add_page(page_obj, first=False)
230 return Page(page_obj)
231
232 def close(self) -> None:
233 self._close()
234 if getattr(self, '_tmp_stream', None):
235 self._tmp_stream.close()
236
237 def __enter__(self):
238 return self
239
240 def __exit__(self, exc_type, exc_value, traceback):
241 self.close()
242
243 @property
244 def allow(self) -> Permissions:
245 results = {}
246 for field in Permissions._fields:
247 results[field] = getattr(self, '_allow_' + field)
248 return Permissions(**results)
249
250 @property
251 def encryption(self) -> EncryptionInfo:
252 return EncryptionInfo(self._encryption_data)
253
254 def check(self) -> list[str]:
255 class DiscardingParser(StreamParser):
256 def __init__(self): # pylint: disable=useless-super-delegation
257 super().__init__() # required for C++
258
259 def handle_object(self, *_args):
260 pass
261
262 def handle_eof(self):
263 pass
264
265 problems: list[str] = []
266
267 self._decode_all_streams_and_discard()
268
269 discarding_parser = DiscardingParser()
270 for page in self.pages:
271 page.parse_contents(discarding_parser)
272
273 for warning in self.get_warnings():
274 problems.append("WARNING: " + warning)
275
276 return problems
277
278 def save(
279 self,
280 filename_or_stream: Path | str | BinaryIO | None = None,
281 *,
282 static_id: bool = False,
283 preserve_pdfa: bool = True,
284 min_version: str | tuple[str, int] = "",
285 force_version: str | tuple[str, int] = "",
286 fix_metadata_version: bool = True,
287 compress_streams: bool = True,
288 stream_decode_level: StreamDecodeLevel | None = None,
289 object_stream_mode: ObjectStreamMode = ObjectStreamMode.preserve,
290 normalize_content: bool = False,
291 linearize: bool = False,
292 qdf: bool = False,
293 progress: Callable[[int], None] = None,
294 encryption: Encryption | bool | None = None,
295 recompress_flate: bool = False,
296 deterministic_id: bool = False,
297 ) -> None:
298 if not filename_or_stream and getattr(self, '_original_filename', None):
299 filename_or_stream = self._original_filename
300 if not filename_or_stream:
301 raise ValueError(
302 "Cannot save to original filename because the original file was "
303 "not opening using Pdf.open(..., allow_overwriting_input=True). "
304 "Either specify a new destination filename/file stream or open "
305 "with allow_overwriting_input=True. If this Pdf was created using "
306 "Pdf.new(), you must specify a destination object since there is "
307 "no original filename to save to."
308 )
309 with ExitStack() as stack:
310 if hasattr(filename_or_stream, 'seek'):
311 stream = filename_or_stream
312 check_stream_is_usable(filename_or_stream)
313 else:
314 if not isinstance(filename_or_stream, (str, bytes, Path)):
315 raise TypeError("expected str, bytes or os.PathLike object")
316 filename = Path(filename_or_stream)
317 if (
318 not getattr(self, '_tmp_stream', None)
319 and getattr(self, '_original_filename', None) is not None
320 ):
321 check_different_files(self._original_filename, filename)
322 stream = stack.enter_context(atomic_overwrite(filename))
323 self._save(
324 stream,
325 static_id=static_id,
326 preserve_pdfa=preserve_pdfa,
327 min_version=min_version,
328 force_version=force_version,
329 fix_metadata_version=fix_metadata_version,
330 compress_streams=compress_streams,
331 stream_decode_level=stream_decode_level,
332 object_stream_mode=object_stream_mode,
333 normalize_content=normalize_content,
334 linearize=linearize,
335 qdf=qdf,
336 progress=progress,
337 encryption=encryption,
338 samefile_check=getattr(self, '_tmp_stream', None) is None,
339 recompress_flate=recompress_flate,
340 deterministic_id=deterministic_id,
341 )
342
343 @staticmethod
344 def open(
345 filename_or_stream: Path | str | BinaryIO,
346 *,
347 password: str | bytes = "",
348 hex_password: bool = False,
349 ignore_xref_streams: bool = False,
350 suppress_warnings: bool = True,
351 attempt_recovery: bool = True,
352 inherit_page_attributes: bool = True,
353 access_mode: AccessMode = AccessMode.default,
354 allow_overwriting_input: bool = False,
355 ) -> Pdf:
356 if isinstance(filename_or_stream, bytes) and filename_or_stream.startswith(
357 b'%PDF-'
358 ):
359 warn(
360 "It looks like you called with Pdf.open(data) with a bytes-like object "
361 "containing a PDF. This will probably fail because this function "
362 "expects a filename or opened file-like object. Instead, please use "
363 "Pdf.open(BytesIO(data))."
364 )
365 if isinstance(filename_or_stream, (int, float)):
366 # Attempted to open with integer file descriptor?
367 # TODO improve error
368 raise TypeError("expected str, bytes or os.PathLike object")
369
370 stream: RawIOBase | None = None
371 closing_stream: bool = False
372 original_filename: Path | None = None
373
374 if allow_overwriting_input:
375 try:
376 Path(filename_or_stream)
377 except TypeError as error:
378 raise ValueError(
379 '"allow_overwriting_input=True" requires "open" first argument '
380 'to be a file path'
381 ) from error
382 original_filename = Path(filename_or_stream)
383 with open(original_filename, 'rb') as pdf_file:
384 stream = BytesIO()
385 shutil.copyfileobj(pdf_file, stream)
386 stream.seek(0)
387 # description = f"memory copy of {original_filename}"
388 description = str(original_filename)
389 elif hasattr(filename_or_stream, 'read') and hasattr(
390 filename_or_stream, 'seek'
391 ):
392 stream = filename_or_stream
393 description = f"stream {stream}"
394 else:
395 stream = open(filename_or_stream, 'rb')
396 original_filename = Path(filename_or_stream)
397 description = str(filename_or_stream)
398 closing_stream = True
399
400 try:
401 check_stream_is_usable(stream)
402 pdf = Pdf._open(
403 stream,
404 password=password,
405 hex_password=hex_password,
406 ignore_xref_streams=ignore_xref_streams,
407 suppress_warnings=suppress_warnings,
408 attempt_recovery=attempt_recovery,
409 inherit_page_attributes=inherit_page_attributes,
410 access_mode=access_mode,
411 description=description,
412 closing_stream=closing_stream,
413 )
414 except Exception:
415 if stream is not None and closing_stream:
416 stream.close()
417 raise
418 pdf._tmp_stream = stream if allow_overwriting_input else None
419 pdf._original_filename = original_filename
420 return pdf
421
422
423@augments(_ObjectMapping)
424class Extend_ObjectMapping:
425 def get(self, key, default=None) -> Object:
426 try:
427 return self[key]
428 except KeyError:
429 return default
430
431 @augment_override_cpp
432 def __contains__(self, key: Name | str) -> bool:
433 if isinstance(key, Name):
434 key = str(key)
435 return _ObjectMapping._cpp__contains__(self, key)
436
437 @augment_override_cpp
438 def __getitem__(self, key: Name | str) -> Object:
439 if isinstance(key, Name):
440 key = str(key)
441 return _ObjectMapping._cpp__getitem__(self, key)
442
443
444def check_is_box(obj) -> None:
445 with suppress(AttributeError):
446 if obj.is_rectangle:
447 return
448 try:
449 pdfobj = Array(obj)
450 if pdfobj.is_rectangle:
451 return
452 except Exception as e:
453 raise ValueError("object is not a rectangle") from e
454 raise ValueError("object is not a rectangle")
455
456
457@augments(Page)
458class Extend_Page:
459 @property
460 def mediabox(self):
461 return self._get_mediabox(True)
462
463 @mediabox.setter
464 def mediabox(self, value):
465 check_is_box(value)
466 self.obj['/MediaBox'] = value
467
468 @property
469 def artbox(self):
470 return self._get_artbox(True, False)
471
472 @artbox.setter
473 def artbox(self, value):
474 check_is_box(value)
475 self.obj['/ArtBox'] = value
476
477 @property
478 def bleedbox(self):
479 return self._get_bleedbox(True, False)
480
481 @bleedbox.setter
482 def bleedbox(self, value):
483 check_is_box(value)
484 self.obj['/BleedBox'] = value
485
486 @property
487 def cropbox(self):
488 return self._get_cropbox(True, False)
489
490 @cropbox.setter
491 def cropbox(self, value):
492 check_is_box(value)
493 self.obj['/CropBox'] = value
494
495 @property
496 def trimbox(self):
497 return self._get_trimbox(True, False)
498
499 @trimbox.setter
500 def trimbox(self, value):
501 check_is_box(value)
502 self.obj['/TrimBox'] = value
503
504 @property
505 def images(self) -> _ObjectMapping:
506 return self._images
507
508 @property
509 def form_xobjects(self) -> _ObjectMapping:
510 return self._form_xobjects
511
512 @property
513 def resources(self) -> Dictionary:
514 if Name.Resources not in self.obj:
515 self.obj.Resources = Dictionary()
516 elif not isinstance(self.obj.Resources, Dictionary):
517 raise TypeError("Page /Resources exists but is not a dictionary")
518 return self.obj.Resources
519
520 def add_resource(
521 self,
522 res: Object,
523 res_type: Name,
524 name: Name | None = None,
525 *,
526 prefix: str = '',
527 replace_existing: bool = True,
528 ) -> Name:
529 resources = self.resources
530 if res_type not in resources:
531 resources[res_type] = Dictionary()
532
533 if name is not None and prefix:
534 raise ValueError("Must specify one of name= or prefix=")
535 if name is None:
536 name = Name.random(prefix=prefix)
537
538 for res_dict in resources.as_dict().values():
539 if not isinstance(res_dict, Dictionary):
540 continue
541 if name in res_dict:
542 if replace_existing:
543 del res_dict[name]
544 else:
545 raise ValueError(f"Name {name} already exists in page /Resources")
546
547 resources[res_type][name] = res.with_same_owner_as(self.obj)
548 return name
549
550 def _over_underlay(
551 self,
552 other,
553 rect: Rectangle | None,
554 under: bool,
555 push_stack: bool,
556 shrink: bool,
557 expand: bool,
558 ) -> Name:
559 formx = None
560 if isinstance(other, Page):
561 formx = other.as_form_xobject()
562 elif isinstance(other, Dictionary) and other.get(Name.Type) == Name.Page:
563 formx = Page(other).as_form_xobject()
564 elif (
565 isinstance(other, Stream)
566 and other.get(Name.Type) == Name.XObject
567 and other.get(Name.Subtype) == Name.Form
568 ):
569 formx = other
570
571 if formx is None:
572 raise TypeError(
573 "other object is not something we can convert to Form XObject"
574 )
575
576 if rect is None:
577 rect = Rectangle(self.trimbox)
578
579 formx_placed_name = self.add_resource(formx, Name.XObject)
580 cs = self.calc_form_xobject_placement(
581 formx, formx_placed_name, rect, allow_shrink=shrink, allow_expand=expand
582 )
583
584 if push_stack:
585 self.contents_add(b'q\n', prepend=True) # prepend q
586 self.contents_add(b'Q\n', prepend=False) # i.e. append Q
587
588 self.contents_add(cs, prepend=under)
589 self.contents_coalesce()
590 return formx_placed_name
591
592 def add_overlay(
593 self,
594 other: Object | Page,
595 rect: Rectangle | None = None,
596 *,
597 push_stack: bool = True,
598 shrink: bool = True,
599 expand: bool = True,
600 ) -> Name:
601 return self._over_underlay(
602 other,
603 rect,
604 under=False,
605 push_stack=push_stack,
606 expand=expand,
607 shrink=shrink,
608 )
609
610 def add_underlay(
611 self,
612 other: Object | Page,
613 rect: Rectangle | None = None,
614 *,
615 shrink: bool = True,
616 expand: bool = True,
617 ) -> Name:
618 return self._over_underlay(
619 other, rect, under=True, push_stack=False, expand=expand, shrink=shrink
620 )
621
622 def contents_add(self, contents: Stream | bytes, *, prepend: bool = False):
623 return self._contents_add(contents, prepend=prepend)
624
625 def __getattr__(self, name):
626 return getattr(self.obj, name)
627
628 @augment_override_cpp
629 def __setattr__(self, name, value):
630 if hasattr(self.__class__, name):
631 object.__setattr__(self, name, value)
632 else:
633 setattr(self.obj, name, value)
634
635 @augment_override_cpp
636 def __delattr__(self, name):
637 if hasattr(self.__class__, name):
638 object.__delattr__(self, name)
639 else:
640 delattr(self.obj, name)
641
642 def __getitem__(self, key):
643 return self.obj[key]
644
645 def __setitem__(self, key, value):
646 self.obj[key] = value
647
648 def __delitem__(self, key):
649 del self.obj[key]
650
651 def __contains__(self, key):
652 return key in self.obj
653
654 def get(self, key, default=None):
655 try:
656 return self[key]
657 except KeyError:
658 return default
659
660 def emplace(self, other: Page, retain=(Name.Parent,)):
661 return self.obj.emplace(other.obj, retain=retain)
662
663 def __repr__(self):
664 return (
665 repr(self.obj)
666 .replace('Dictionary', 'Page', 1)
667 .replace('(Type="/Page")', '', 1)
668 )
669
670 def _repr_mimebundle_(self, include=None, exclude=None):
671 data = {}
672 bundle = {'application/pdf', 'image/svg+xml'}
673 if include:
674 bundle = {k for k in bundle if k in include}
675 if exclude:
676 bundle = {k for k in bundle if k not in exclude}
677 pagedata = _single_page_pdf(self)
678 if 'application/pdf' in bundle:
679 data['application/pdf'] = pagedata
680 if 'image/svg+xml' in bundle:
681 with suppress(FileNotFoundError, RuntimeError):
682 data['image/svg+xml'] = _mudraw(pagedata, 'svg').decode('utf-8')
683 return data
684
685
686@augments(Token)
687class Extend_Token:
688 def __repr__(self):
689 return f'pikepdf.Token({self.type_}, {self.raw_value})'
690
691
692@augments(Rectangle)
693class Extend_Rectangle:
694 def __repr__(self):
695 return f'pikepdf.Rectangle({self.llx}, {self.lly}, {self.urx}, {self.ury})'
696
697 def __hash__(self):
698 return hash((self.llx, self.lly, self.urx, self.ury))
699
700 def to_bbox(self) -> Rectangle:
701 """Returns the origin-centred bounding box that encloses this rectangle.
702
703 Create a new rectangle with the same width and height as this one, but located
704 at the origin (0, 0).
705
706 Bounding boxes represent independent coordinate systems, such as for Form
707 XObjects.
708 """
709 return Rectangle(0, 0, self.width, self.height)
710
711
712@augments(Attachments)
713class Extend_Attachments(MutableMapping):
714 def __getitem__(self, k: str) -> AttachedFileSpec:
715 filespec = self._get_filespec(k)
716 if filespec is None:
717 raise KeyError(k)
718 return filespec
719
720 def __setitem__(self, k: str, v: AttachedFileSpec | bytes) -> None:
721 if isinstance(v, bytes):
722 return self._attach_data(k, v)
723 if not v.filename:
724 v.filename = k
725 return self._add_replace_filespec(k, v)
726
727 def __delitem__(self, k: str) -> None:
728 return self._remove_filespec(k)
729
730 def __len__(self):
731 return len(self._get_all_filespecs())
732
733 def __iter__(self) -> Iterator[str]:
734 yield from self._get_all_filespecs()
735
736 def __repr__(self):
737 return f"<pikepdf._core.Attachments: {list(self)}>"
738
739
740@augments(AttachedFileSpec)
741class Extend_AttachedFileSpec:
742 @staticmethod
743 def from_filepath(
744 pdf: Pdf,
745 path: Path | str,
746 *,
747 description: str = '',
748 relationship: Name | None = Name.Unspecified,
749 ):
750 mime, _ = mimetypes.guess_type(str(path))
751 if mime is None:
752 mime = ''
753 if not isinstance(path, Path):
754 path = Path(path)
755
756 stat = path.stat()
757 return AttachedFileSpec(
758 pdf,
759 path.read_bytes(),
760 description=description,
761 filename=str(path.name),
762 mime_type=mime,
763 creation_date=encode_pdf_date(
764 datetime.datetime.fromtimestamp(stat.st_ctime)
765 ),
766 mod_date=encode_pdf_date(datetime.datetime.fromtimestamp(stat.st_mtime)),
767 relationship=relationship,
768 )
769
770 @property
771 def relationship(self) -> Name | None:
772 return self.obj.get(Name.AFRelationship)
773
774 @relationship.setter
775 def relationship(self, value: Name | None):
776 if value is None:
777 del self.obj[Name.AFRelationship]
778 else:
779 self.obj[Name.AFRelationship] = value
780
781 def __repr__(self):
782 if self.filename:
783 return (
784 f"<pikepdf._core.AttachedFileSpec for {self.filename!r}, "
785 f"description {self.description!r}>"
786 )
787 return f"<pikepdf._core.AttachedFileSpec description {self.description!r}>"
788
789
790@augments(AttachedFile)
791class Extend_AttachedFile:
792 @property
793 def creation_date(self) -> datetime.datetime | None:
794 if not self._creation_date:
795 return None
796 return decode_pdf_date(self._creation_date)
797
798 @creation_date.setter
799 def creation_date(self, value: datetime.datetime):
800 self._creation_date = encode_pdf_date(value)
801
802 @property
803 def mod_date(self) -> datetime.datetime | None:
804 if not self._mod_date:
805 return None
806 return decode_pdf_date(self._mod_date)
807
808 @mod_date.setter
809 def mod_date(self, value: datetime.datetime):
810 self._mod_date = encode_pdf_date(value)
811
812 def read_bytes(self) -> bytes:
813 return self.obj.read_bytes()
814
815 def __repr__(self):
816 return (
817 f'<pikepdf._core.AttachedFile objid={self.obj.objgen} size={self.size} '
818 f'mime_type={self.mime_type} creation_date={self.creation_date} '
819 f'mod_date={self.mod_date}>'
820 )
821
822
823@augments(NameTree)
824class Extend_NameTree:
825 def keys(self):
826 return KeysView(self._as_map())
827
828 def values(self):
829 return ValuesView(self._as_map())
830
831 def items(self):
832 return ItemsView(self._as_map())
833
834 get = MutableMapping.get
835 pop = MutableMapping.pop
836 popitem = MutableMapping.popitem
837 clear = MutableMapping.clear
838 update = MutableMapping.update
839 setdefault = MutableMapping.setdefault
840
841
842MutableMapping.register(NameTree)
843
844
845@augments(NumberTree)
846class Extend_NumberTree:
847 def keys(self):
848 return KeysView(self._as_map())
849
850 def values(self):
851 return ValuesView(self._as_map())
852
853 def items(self):
854 return ItemsView(self._as_map())
855
856 get = MutableMapping.get
857 pop = MutableMapping.pop
858 popitem = MutableMapping.popitem
859 clear = MutableMapping.clear
860 update = MutableMapping.update
861 setdefault = MutableMapping.setdefault
862
863
864MutableMapping.register(NumberTree)