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

482 statements  

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)