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