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

679 statements  

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. 

30 

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) 

42 

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 

89 

90 

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]) 

97 

98 

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>`. 

104 

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 """ 

113 

114 def __init__(self) -> None: 

115 DictionaryObject.__init__(self) 

116 

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 

124 

125 @property 

126 def title(self) -> Optional[str]: 

127 """ 

128 Read-only property accessing the document's title. 

129 

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 ) 

138 

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) 

143 

144 @property 

145 def author(self) -> Optional[str]: 

146 """ 

147 Read-only property accessing the document's author. 

148 

149 Returns a ``TextStringObject`` or ``None`` if the author is not 

150 specified. 

151 """ 

152 return self._get_text(DI.AUTHOR) 

153 

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) 

158 

159 @property 

160 def subject(self) -> Optional[str]: 

161 """ 

162 Read-only property accessing the document's subject. 

163 

164 Returns a ``TextStringObject`` or ``None`` if the subject is not 

165 specified. 

166 """ 

167 return self._get_text(DI.SUBJECT) 

168 

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) 

173 

174 @property 

175 def creator(self) -> Optional[str]: 

176 """ 

177 Read-only property accessing the document's creator. 

178 

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) 

185 

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) 

190 

191 @property 

192 def producer(self) -> Optional[str]: 

193 """ 

194 Read-only property accessing the document's producer. 

195 

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) 

202 

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) 

207 

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)) 

212 

213 @property 

214 def creation_date_raw(self) -> Optional[str]: 

215 """ 

216 The "raw" version of creation date; can return a ``ByteStringObject``. 

217 

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) 

222 

223 @property 

224 def modification_date(self) -> Optional[datetime]: 

225 """ 

226 Read-only property accessing the document's modification date. 

227 

228 The date and time the document was most recently modified. 

229 """ 

230 return parse_iso8824_date(self._get_text(DI.MOD_DATE)) 

231 

232 @property 

233 def modification_date_raw(self) -> Optional[str]: 

234 """ 

235 The "raw" version of modification date; can return a 

236 ``ByteStringObject``. 

237 

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) 

242 

243 @property 

244 def keywords(self) -> Optional[str]: 

245 """ 

246 Read-only property accessing the document's keywords. 

247 

248 Returns a ``TextStringObject`` or ``None`` if keywords are not 

249 specified. 

250 """ 

251 return self._get_text(DI.KEYWORDS) 

252 

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) 

257 

258 

259class PdfDocCommon(ABC): 

260 """ 

261 Common functions from PdfWriter and PdfReader objects. 

262 

263 This root class is strongly abstracted. 

264 """ 

265 

266 strict: bool = False # default 

267 

268 flattened_pages: Optional[list[PageObject]] = None 

269 

270 _encryption: Optional[Encryption] = None 

271 

272 _readonly: bool = False 

273 

274 @property 

275 @abstractmethod 

276 def root_object(self) -> DictionaryObject: 

277 ... # pragma: no cover 

278 

279 @property 

280 @abstractmethod 

281 def pdf_header(self) -> str: 

282 ... # pragma: no cover 

283 

284 @abstractmethod 

285 def get_object( 

286 self, indirect_reference: Union[int, IndirectObject] 

287 ) -> Optional[PdfObject]: 

288 ... # pragma: no cover 

289 

290 @abstractmethod 

291 def _replace_object(self, indirect: IndirectObject, obj: PdfObject) -> PdfObject: 

292 ... # pragma: no cover 

293 

294 @property 

295 @abstractmethod 

296 def _info(self) -> Optional[DictionaryObject]: 

297 ... # pragma: no cover 

298 

299 @property 

300 def metadata(self) -> Optional[DocumentInformation]: 

301 """ 

302 Retrieve the PDF file's document information dictionary, if it exists. 

303 

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 

313 

314 @property 

315 @abstractmethod 

316 def xmp_metadata(self) -> Optional[XmpInformation]: 

317 ... # pragma: no cover 

318 

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 

333 

334 def get_num_pages(self) -> int: 

335 """ 

336 Calculate the number of pages in this PDF file. 

337 

338 Returns: 

339 The number of pages of the parsed PDF file. 

340 

341 Raises: 

342 PdfReadError: If restrictions prevent this action. 

343 

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) 

354 

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. 

359 

360 Args: 

361 page_number: The page number to retrieve 

362 (pages begin at zero) 

363 

364 Returns: 

365 A :class:`PageObject<pypdf._page.PageObject>` instance. 

366 

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] 

372 

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"]) 

382 

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.") 

405 

406 node, idx = recursive_call(top, 0) 

407 assert isinstance(node, DictionaryObject), "mypy" 

408 return node, idx 

409 

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() 

414 

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 

436 

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 

445 

446 return named_dest 

447 

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. 

458 

459 Args: 

460 tree: The current tree. 

461 retval: The previously retrieved destinations for nested calls. 

462 visited: Already known/visited objects. 

463 

464 Returns: 

465 A dictionary which maps names to destinations. 

466 

467 """ 

468 if visited is None: 

469 visited = set() 

470 if retval is None: 

471 retval = {} 

472 catalog = self.root_object 

473 

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]) 

481 

482 if is_null_or_none(tree): 

483 return retval 

484 assert tree is not None, "mypy" 

485 

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) 

491 

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 

532 

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. 

535 

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. 

545 

546 The *tree*, *retval*, *stack* parameters are for recursive use. 

547 

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. 

554 

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. 

560 

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 

585 

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) 

598 

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", "")) 

611 

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) 

653 

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) 

674 

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 ) 

680 

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 

711 

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. 

715 

716 Args: 

717 full_qualified_name: to get full name 

718 

719 Returns: 

720 A dictionary. The key is the name of the form field, 

721 the value is the content of the field. 

722 

723 If the document contains multiple form fields with the same name, the 

724 second and following will get the suffix .2, .3, ... 

725 

726 """ 

727 

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 ) 

736 

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 

749 

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. 

755 

756 Args: 

757 field: Field Object, PdfObject or IndirectObject referencing a Field 

758 

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). 

770 

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 ] 

809 

810 @property 

811 def open_destination( 

812 self, 

813 ) -> Union[None, Destination, TextStringObject, ByteStringObject]: 

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. 

818 

819 Raises: 

820 Exception: If a destination is invalid. 

821 

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 

839 

840 @open_destination.setter 

841 def open_destination(self, dest: Union[None, str, Destination, PageObject]) -> None: 

842 raise NotImplementedError("No setter for open_destination") 

843 

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() 

852 

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 

862 

863 # get the outline dictionary and named destinations 

864 if Core.OUTLINES in catalog: 

865 lines = cast(DictionaryObject, catalog[Core.OUTLINES]) 

866 

867 if isinstance(lines, NullObject): 

868 return outline 

869 

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() 

874 

875 if node is None: 

876 return outline 

877 

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) 

887 

888 outline_obj = self._build_outline_item(node) 

889 if outline_obj: 

890 outline.append(outline_obj) 

891 

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) 

904 

905 if "/Next" not in node: 

906 break 

907 node = cast(DictionaryObject, node["/Next"]) 

908 

909 return outline 

910 

911 @property 

912 def threads(self) -> Optional[ArrayObject]: 

913 """ 

914 Read-only property for the list of threads. 

915 

916 See §12.4.3 from the PDF 1.7 or 2.0 specification. 

917 

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. 

922 

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 

931 

932 @abstractmethod 

933 def _get_page_number_by_indirect( 

934 self, indirect_reference: Union[None, int, NullObject, IndirectObject] 

935 ) -> Optional[int]: 

936 ... # pragma: no cover 

937 

938 def get_page_number(self, page: PageObject) -> Optional[int]: 

939 """ 

940 Retrieve page number of a given PageObject. 

941 

942 Args: 

943 page: The page to get page number. Should be 

944 an instance of :class:`PageObject<pypdf._page.PageObject>` 

945 

946 Returns: 

947 The page number or None if page is not found 

948 

949 """ 

950 return self._get_page_number_by_indirect(page.indirect_reference) 

951 

952 def get_destination_page_number(self, destination: Destination) -> Optional[int]: 

953 """ 

954 Retrieve page number of a given Destination object. 

955 

956 Args: 

957 destination: The destination to get page number. 

958 

959 Returns: 

960 The page number or None if page is not found 

961 

962 """ 

963 return self._get_page_number_by_indirect(destination.page) 

964 

965 def _build_destination( 

966 self, 

967 title: Union[str, bytes], 

968 array: Optional[ 

969 list[ 

970 Union[NumberObject, IndirectObject, None, NullObject, DictionaryObject] 

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()) 

994 

995 def _build_outline_item(self, node: DictionaryObject) -> Optional[Destination]: 

996 dest, title, outline_item = None, None, None 

997 

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 = "" 

1006 

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"] 

1022 

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) 

1048 

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 

1080 

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. 

1086 

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. 

1094 

1095 """ 

1096 return _VirtualList(self.get_num_pages, self.get_page) # type: ignore[return-value] 

1097 

1098 @property 

1099 def page_labels(self) -> list[str]: 

1100 """ 

1101 A list of labels for the pages in this document. 

1102 

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))] 

1107 

1108 @property 

1109 def page_layout(self) -> Optional[str]: 

1110 """ 

1111 Get the page layout currently being used. 

1112 

1113 .. list-table:: Valid ``layout`` values 

1114 :widths: 50 200 

1115 

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 

1135 

1136 @property 

1137 def page_mode(self) -> Optional[PagemodeType]: 

1138 """ 

1139 Get the page mode currently being used. 

1140 

1141 .. list-table:: Valid ``mode`` values 

1142 :widths: 50 200 

1143 

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 

1161 

1162 def _flatten( 

1163 self, 

1164 list_only: bool = False, 

1165 pages: Union[None, DictionaryObject, PageObject] = 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. 

1172 

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. 

1177 

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. 

1181 

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. 

1190 

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" 

1211 

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" 

1219 

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.") 

1235 

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) 

1256 

1257 # TODO: Could flattened_pages be None at this point? 

1258 self.flattened_pages.append(page_obj) # type: ignore[union-attr] 

1259 

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. 

1267 

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. 

1274 

1275 clean: replace PageObject with NullObject to prevent annotations 

1276 or destinations to reference a detached page. 

1277 

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 

1288 

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 

1298 

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()) 

1303 

1304 def _get_indirect_object(self, num: int, gen: int) -> Optional[PdfObject]: 

1305 """ 

1306 Used to ease development. 

1307 

1308 This is equivalent to generic.IndirectObject(num,gen,self).get_object() 

1309 

1310 Args: 

1311 num: The object number of the indirect object. 

1312 gen: The generation number of the indirect object. 

1313 

1314 Returns: 

1315 A PdfObject 

1316 

1317 """ 

1318 return IndirectObject(num, gen, self).get_object() 

1319 

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 ) 

1329 

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. 

1335 

1336 .. warning:: 

1337 

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) 

1346 

1347 @property 

1348 def are_permissions_valid(self) -> Optional[bool]: 

1349 """ 

1350 Whether the ``/Perms`` integrity check passed for this document. 

1351 

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). 

1356 

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 

1366 

1367 @property 

1368 @abstractmethod 

1369 def is_encrypted(self) -> bool: 

1370 """ 

1371 Read-only boolean property showing whether this PDF file is encrypted. 

1372 

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 

1377 

1378 @property 

1379 def xfa(self) -> Optional[dict[str, Any]]: 

1380 retval: dict[str, Any] = {} 

1381 catalog = self.root_object 

1382 

1383 if "/AcroForm" not in catalog or not catalog["/AcroForm"]: 

1384 return None 

1385 

1386 tree = cast(TreeObject, catalog["/AcroForm"]) 

1387 

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 

1400 

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 ) 

1410 

1411 @property 

1412 def attachment_list(self) -> Generator[EmbeddedFile, None, None]: 

1413 """Iterable of attachment objects.""" 

1414 yield from EmbeddedFile._load(self.root_object) 

1415 

1416 def _list_attachments(self) -> list[str]: 

1417 """ 

1418 Retrieves the list of filenames of file attachments. 

1419 

1420 Returns: 

1421 list of filenames 

1422 

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 

1430 

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] 

1436 

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. 

1443 

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. 

1449 

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. 

1453 

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} 

1467 

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 

1478 

1479 @abstractmethod 

1480 def _repr_mimebundle_( 

1481 self, 

1482 include: Union[None, Iterable[str]] = None, 

1483 exclude: Union[None, Iterable[str]] = None, 

1484 ) -> dict[str, Any]: 

1485 """ 

1486 Integration into Jupyter Notebooks. 

1487 

1488 This method returns a dictionary that maps a mime-type to its 

1489 representation. 

1490 

1491 .. seealso:: 

1492 

1493 https://ipython.readthedocs.io/en/stable/config/integrating.html 

1494 """ 

1495 ... # pragma: no cover 

1496 

1497 

1498class LazyDict(Mapping[Any, Any]): 

1499 def __init__(self, *args: Any, **kwargs: Any) -> None: 

1500 self._raw_dict = dict(*args, **kwargs) 

1501 

1502 def __getitem__(self, key: str) -> Any: 

1503 func, arg = self._raw_dict.__getitem__(key) 

1504 return func(arg) 

1505 

1506 def __iter__(self) -> Iterator[Any]: 

1507 return iter(self._raw_dict) 

1508 

1509 def __len__(self) -> int: 

1510 return len(self._raw_dict) 

1511 

1512 def __str__(self) -> str: 

1513 return f"LazyDict(keys={list(self.keys())})"