Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pypdf/_page.py: 16%

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

1037 statements  

1# Copyright (c) 2006, Mathieu Fenniak 

2# Copyright (c) 2007, Ashish Kulkarni <kulkarni.ashish@gmail.com> 

3# 

4# All rights reserved. 

5# 

6# Redistribution and use in source and binary forms, with or without 

7# modification, are permitted provided that the following conditions are 

8# met: 

9# 

10# * Redistributions of source code must retain the above copyright notice, 

11# this list of conditions and the following disclaimer. 

12# * Redistributions in binary form must reproduce the above copyright notice, 

13# this list of conditions and the following disclaimer in the documentation 

14# and/or other materials provided with the distribution. 

15# * The name of the author may not be used to endorse or promote products 

16# derived from this software without specific prior written permission. 

17# 

18# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 

19# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 

20# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 

21# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 

22# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 

23# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 

24# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 

25# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 

26# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 

27# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 

28# POSSIBILITY OF SUCH DAMAGE. 

29 

30import math 

31from collections.abc import Iterable, Iterator, Sequence 

32from copy import deepcopy 

33from dataclasses import asdict, dataclass 

34from decimal import Decimal 

35from io import BytesIO 

36from pathlib import Path 

37from typing import ( 

38 Any, 

39 Callable, 

40 Literal, 

41 Optional, 

42 Union, 

43 cast, 

44 overload, 

45) 

46 

47from ._configuration import get_configuration 

48from ._protocols import PdfCommonDocProtocol 

49from ._text_extraction import ( 

50 _layout_mode, 

51) 

52from ._text_extraction._text_extractor import TextExtraction 

53from ._utils import ( 

54 CompressedTransformationMatrix, 

55 TransformationMatrixType, 

56 _human_readable_bytes, 

57 _TraversalState, 

58 deprecate, 

59 deprecate_no_replacement, 

60 deprecate_with_replacement, 

61 logger_warning, 

62 matrix_multiply, 

63) 

64from .actions import Action, PageTrigger 

65from .constants import ( 

66 _INLINE_IMAGE_KEY_MAPPING, 

67 _INLINE_IMAGE_VALUE_MAPPING, 

68 AnnotationDictionaryAttributes, 

69 ImageAttributes, 

70) 

71from .constants import PageAttributes as PG 

72from .constants import Resources as RES 

73from .errors import PageSizeNotDefinedError, PdfReadError 

74from .generic import ( 

75 ArrayObject, 

76 ContentStream, 

77 DictionaryObject, 

78 EncodedStreamObject, 

79 FloatObject, 

80 IndirectObject, 

81 NameObject, 

82 NullObject, 

83 NumberObject, 

84 PdfObject, 

85 RectangleObject, 

86 StreamObject, 

87 is_null_or_none, 

88) 

89from .generic._font import Font 

90 

91try: 

92 from PIL.Image import Image 

93 

94 pil_not_imported = False 

95except ImportError: 

96 Image = object # type: ignore[assignment,misc,unused-ignore] # TODO: Remove unused-ignore on Python 3.10 

97 pil_not_imported = True # error will be raised only when using images 

98 

99MERGE_CROP_BOX = "cropbox" # DEPRECATED: Use pypdf.Configuration. 

100 

101 

102def _get_page_resources(obj: Any) -> DictionaryObject: 

103 """Return the inherited /Resources, or an empty dictionary if malformed.""" 

104 resources = obj.get_inherited(key=PG.RESOURCES, default=DictionaryObject()) 

105 if is_null_or_none(resources): 

106 return DictionaryObject() 

107 if not isinstance(resources, DictionaryObject): 

108 logger_warning( 

109 "Page resources are not a dictionary: %(resources)s", 

110 source=__name__, 

111 resources=resources, 

112 ) 

113 return DictionaryObject() 

114 return resources 

115 

116 

117def _get_font_resources(resources: Any) -> DictionaryObject: 

118 """Return the /Font resources, or an empty dictionary if missing or malformed.""" 

119 fonts = resources.get(RES.FONT) 

120 if is_null_or_none(fonts): 

121 return DictionaryObject() 

122 fonts = fonts.get_object() 

123 if not isinstance(fonts, DictionaryObject): 

124 logger_warning( 

125 "Font resources are not a dictionary: %(fonts)s", 

126 source=__name__, 

127 fonts=fonts, 

128 ) 

129 return DictionaryObject() 

130 return fonts 

131 

132 

133def _get_rectangle(self: Any, name: str, defaults: Iterable[str]) -> RectangleObject: 

134 retval: Optional[PdfObject] = self.get(name) 

135 if isinstance(retval, RectangleObject): 

136 return retval 

137 if is_null_or_none(retval): 

138 for d in defaults: 

139 retval = self.get(d) 

140 if retval is not None: 

141 break 

142 if isinstance(retval, IndirectObject): 

143 # Pages of a writer have no `pdf`, the reference knows its own document. 

144 retval = retval.get_object() 

145 if not isinstance(retval, ArrayObject): 

146 raise ValueError(f"Expected an array of four values for {name}, got {retval}") 

147 if (length := len(retval)) != 4: 

148 if length > 4: 

149 # Keep backwards-compatibility with files previously written in a 

150 # broken way by pypdf, which carried more than four values. 

151 logger_warning( 

152 "Expected four values, got %(length)d: %(retval)s", 

153 source=__name__, 

154 length=length, 

155 retval=retval, 

156 ) 

157 retval = RectangleObject(tuple(retval[:4])) 

158 else: 

159 raise ValueError( 

160 f"Expected four values for {name}, got {length}: {retval}" 

161 ) 

162 else: 

163 retval = RectangleObject(retval) 

164 _set_rectangle(self, name, retval) 

165 return retval 

166 

167 

168def _set_rectangle(self: Any, name: str, value: Union[RectangleObject, float]) -> None: 

169 if isinstance(value, (list, tuple)) and len(value) < 4: 

170 # The getter tolerates more than four values for backwards compatibility 

171 # but cannot do anything with fewer, so writing them would produce a page 

172 # whose box cannot be read back. 

173 raise ValueError( 

174 f"Expected four values for {name}, got {len(value)}: {value}" 

175 ) 

176 self[NameObject(name)] = value 

177 

178 

179def _delete_rectangle(self: Any, name: str) -> None: 

180 del self[name] 

181 

182 

183def _create_rectangle_accessor(name: str, fallback: Iterable[str]) -> property: 

184 return property( 

185 lambda self: _get_rectangle(self, name, fallback), 

186 lambda self, value: _set_rectangle(self, name, value), 

187 lambda self: _delete_rectangle(self, name), 

188 ) 

189 

190 

191class Transformation: 

192 """ 

193 Represent a 2D transformation. 

194 

195 The transformation between two coordinate systems is represented by a 3-by-3 

196 transformation matrix with the following form:: 

197 

198 a b 0 

199 c d 0 

200 e f 1 

201 

202 Because a transformation matrix has only six elements that can be changed, 

203 it is usually specified in PDF as the six-element array [ a b c d e f ]. 

204 

205 Coordinate transformations are expressed as matrix multiplications:: 

206 

207 a b 0 

208 [ x′ y′ 1 ] = [ x y 1 ] × c d 0 

209 e f 1 

210 

211 

212 Example: 

213 >>> from pypdf import PdfWriter, Transformation 

214 >>> page = PdfWriter().add_blank_page(800, 600) 

215 >>> op = Transformation().scale(sx=2, sy=3).translate(tx=10, ty=20) 

216 >>> page.add_transformation(op) 

217 

218 """ 

219 

220 def __init__(self, ctm: CompressedTransformationMatrix = (1, 0, 0, 1, 0, 0)) -> None: 

221 self.ctm = ctm 

222 

223 @property 

224 def matrix(self) -> TransformationMatrixType: 

225 """ 

226 Return the transformation matrix as a tuple of tuples in the form: 

227 

228 ((a, b, 0), (c, d, 0), (e, f, 1)) 

229 """ 

230 return ( 

231 (self.ctm[0], self.ctm[1], 0), 

232 (self.ctm[2], self.ctm[3], 0), 

233 (self.ctm[4], self.ctm[5], 1), 

234 ) 

235 

236 @staticmethod 

237 def compress(matrix: TransformationMatrixType) -> CompressedTransformationMatrix: 

238 """ 

239 Compresses the transformation matrix into a tuple of (a, b, c, d, e, f). 

240 

241 Args: 

242 matrix: The transformation matrix as a tuple of tuples. 

243 

244 Returns: 

245 A tuple representing the transformation matrix as (a, b, c, d, e, f) 

246 

247 """ 

248 return ( 

249 matrix[0][0], 

250 matrix[0][1], 

251 matrix[1][0], 

252 matrix[1][1], 

253 matrix[2][0], 

254 matrix[2][1], 

255 ) 

256 

257 def _to_cm(self) -> str: 

258 # Returns the cm operation string for the given transformation matrix 

259 return ( 

260 f"{self.ctm[0]:.4f} {self.ctm[1]:.4f} {self.ctm[2]:.4f} " 

261 f"{self.ctm[3]:.4f} {self.ctm[4]:.4f} {self.ctm[5]:.4f} cm" 

262 ) 

263 

264 def transform(self, m: "Transformation") -> "Transformation": 

265 """ 

266 Apply one transformation to another. 

267 

268 Args: 

269 m: a Transformation to apply. 

270 

271 Returns: 

272 A new ``Transformation`` instance 

273 

274 Example: 

275 >>> from pypdf import PdfWriter, Transformation 

276 >>> height, width = 40, 50 

277 >>> page = PdfWriter().add_blank_page(800, 600) 

278 >>> op = Transformation((1, 0, 0, -1, 0, height)) # vertical mirror 

279 >>> op = Transformation().transform(Transformation((-1, 0, 0, 1, width, 0))) # horizontal mirror 

280 >>> page.add_transformation(op) 

281 

282 """ 

283 ctm = Transformation.compress(matrix_multiply(self.matrix, m.matrix)) 

284 return Transformation(ctm) 

285 

286 def translate(self, tx: float = 0, ty: float = 0) -> "Transformation": 

287 """ 

288 Translate the contents of a page. 

289 

290 Args: 

291 tx: The translation along the x-axis. 

292 ty: The translation along the y-axis. 

293 

294 Returns: 

295 A new ``Transformation`` instance 

296 

297 """ 

298 m = self.ctm 

299 return Transformation(ctm=(m[0], m[1], m[2], m[3], m[4] + tx, m[5] + ty)) 

300 

301 def scale( 

302 self, sx: Optional[float] = None, sy: Optional[float] = None 

303 ) -> "Transformation": 

304 """ 

305 Scale the contents of a page towards the origin of the coordinate system. 

306 

307 Typically, that is the lower-left corner of the page. That can be 

308 changed by translating the contents / the page boxes. 

309 

310 Args: 

311 sx: The scale factor along the x-axis. 

312 sy: The scale factor along the y-axis. 

313 

314 Returns: 

315 A new Transformation instance with the scaled matrix. 

316 

317 """ 

318 if sx is None and sy is None: 

319 raise ValueError("Either sx or sy must be specified") 

320 if sx is None: 

321 sx = sy 

322 if sy is None: 

323 sy = sx 

324 assert sx is not None 

325 assert sy is not None 

326 op: TransformationMatrixType = ((sx, 0, 0), (0, sy, 0), (0, 0, 1)) 

327 ctm = Transformation.compress(matrix_multiply(self.matrix, op)) 

328 return Transformation(ctm) 

329 

330 def rotate(self, rotation: float) -> "Transformation": 

331 """ 

332 Rotate the contents of a page. 

333 

334 Args: 

335 rotation: The angle of rotation in degrees. 

336 

337 Returns: 

338 A new ``Transformation`` instance with the rotated matrix. 

339 

340 """ 

341 rotation = math.radians(rotation) 

342 op: TransformationMatrixType = ( 

343 (math.cos(rotation), math.sin(rotation), 0), 

344 (-math.sin(rotation), math.cos(rotation), 0), 

345 (0, 0, 1), 

346 ) 

347 ctm = Transformation.compress(matrix_multiply(self.matrix, op)) 

348 return Transformation(ctm) 

349 

350 def __repr__(self) -> str: 

351 return f"Transformation(ctm={self.ctm})" 

352 

353 @overload 

354 def apply_on(self, pt: list[float], as_object: bool = False) -> list[float]: 

355 ... 

356 

357 @overload 

358 def apply_on( 

359 self, pt: tuple[float, float], as_object: bool = False 

360 ) -> tuple[float, float]: 

361 ... 

362 

363 def apply_on( 

364 self, 

365 pt: Union[tuple[float, float], list[float]], 

366 as_object: bool = False, 

367 ) -> Union[tuple[float, float], list[float]]: 

368 """ 

369 Apply the transformation matrix on the given point. 

370 

371 Args: 

372 pt: A tuple or list representing the point in the form (x, y). 

373 as_object: If True, return items as FloatObject, otherwise as plain floats. 

374 

375 Returns: 

376 A tuple or list representing the transformed point in the form (x', y') 

377 

378 """ 

379 typ = FloatObject if as_object else float 

380 pt1 = ( 

381 typ(float(pt[0]) * self.ctm[0] + float(pt[1]) * self.ctm[2] + self.ctm[4]), 

382 typ(float(pt[0]) * self.ctm[1] + float(pt[1]) * self.ctm[3] + self.ctm[5]), 

383 ) 

384 return list(pt1) if isinstance(pt, list) else pt1 

385 

386 

387@dataclass 

388class ImageFile: 

389 """ 

390 Image within the PDF file. *This object is not designed to be built.* 

391 

392 This object should not be modified except using :func:`ImageFile.replace` to replace the image with a new one. 

393 """ 

394 

395 name: str = "" 

396 """ 

397 Filename as identified within the PDF file. 

398 

399 .. warning:: 

400 

401 This value can contain arbitrary characters. Please make sure to sanitize it before 

402 using it to write the file content to the disk for example. 

403 """ 

404 

405 data: bytes = b"" 

406 """ 

407 Data as bytes. 

408 """ 

409 

410 image: Optional[Image] = None 

411 """ 

412 Data as PIL image. 

413 """ 

414 

415 indirect_reference: Optional[IndirectObject] = None 

416 """ 

417 Reference to the object storing the stream. 

418 """ 

419 

420 is_inline: bool = False 

421 """ 

422 True if this is an inline image (~0~, ~1~, etc.). 

423 """ 

424 

425 is_displayed: bool = False 

426 """ 

427 True if this image is displayed in the page content stream. 

428 """ 

429 

430 def replace(self, new_image: Image, **kwargs: Any) -> None: 

431 """ 

432 Replace the image with a new PIL image. 

433 

434 Args: 

435 new_image (PIL.Image.Image): The new PIL image to replace the existing image. 

436 **kwargs: Additional keyword arguments to pass to `Image.save()`. 

437 

438 Raises: 

439 TypeError: If the image is inline or in a PdfReader. 

440 TypeError: If the image does not belong to a PdfWriter. 

441 TypeError: If `new_image` is not a PIL Image. 

442 

443 Note: 

444 This method replaces the existing image with a new image. 

445 It is not allowed for inline images or images within a PdfReader. 

446 The `kwargs` parameter allows passing additional parameters 

447 to `Image.save()`, such as quality. 

448 

449 """ 

450 if pil_not_imported: 

451 raise ImportError( 

452 "pillow is required to do image extraction. " 

453 "It can be installed via 'pip install pypdf[image]'" 

454 ) 

455 

456 from ._reader import PdfReader # noqa: PLC0415 

457 from .generic import DictionaryObject, PdfObject # noqa: PLC0415 

458 from .generic._image_xobject import _xobj_to_image # noqa: PLC0415 

459 

460 if self.indirect_reference is None: 

461 raise TypeError("Cannot update an inline image.") 

462 if not hasattr(self.indirect_reference.pdf, "_id_translated"): 

463 raise TypeError("Cannot update an image not belonging to a PdfWriter.") 

464 if not isinstance(new_image, Image): 

465 raise TypeError("new_image shall be a PIL Image") 

466 b = BytesIO() 

467 new_image.save(b, "PDF", **kwargs) 

468 reader = PdfReader(b) 

469 page_image = reader.pages[0].images[0] 

470 assert page_image.indirect_reference is not None 

471 self.indirect_reference.pdf._objects[self.indirect_reference.idnum - 1] = ( 

472 page_image.indirect_reference.get_object() 

473 ) 

474 cast( 

475 PdfObject, self.indirect_reference.get_object() 

476 ).indirect_reference = self.indirect_reference 

477 # change the object attributes 

478 extension, byte_stream, img = _xobj_to_image( 

479 cast(DictionaryObject, self.indirect_reference.get_object()), 

480 pillow_parameters=kwargs, 

481 ) 

482 assert extension is not None 

483 self.name = self.name[: self.name.rfind(".")] + extension 

484 self.data = byte_stream 

485 self.image = img 

486 

487 def __str__(self) -> str: 

488 return f"{self.__class__.__name__}(name={self.name}, data: {_human_readable_bytes(len(self.data))})" 

489 

490 def __repr__(self) -> str: 

491 return self.__str__()[:-1] + f", hash: {hash(self.data)})" 

492 

493 

494class VirtualListImages(Sequence[ImageFile]): 

495 """ 

496 Provides access to images referenced within a page. 

497 Only one copy will be returned if the usage is used on the same page multiple times. 

498 See :func:`PageObject.images` for more details. 

499 """ 

500 

501 def __init__( 

502 self, 

503 ids_function: Callable[[], list[Union[str, list[str]]]], 

504 get_function: Callable[[Union[str, list[str], tuple[str]]], ImageFile], 

505 ) -> None: 

506 self.ids_function = ids_function 

507 self.get_function = get_function 

508 self.current = -1 

509 

510 def __len__(self) -> int: 

511 return len(self.ids_function()) 

512 

513 def keys(self) -> list[Union[str, list[str]]]: 

514 return self.ids_function() 

515 

516 def items(self) -> list[tuple[Union[str, list[str]], ImageFile]]: 

517 return [(x, self[x]) for x in self.ids_function()] 

518 

519 @overload 

520 def __getitem__(self, index: Union[int, str, list[str]]) -> ImageFile: 

521 ... 

522 

523 @overload 

524 def __getitem__(self, index: slice) -> Sequence[ImageFile]: 

525 ... 

526 

527 def __getitem__( 

528 self, index: Union[int, slice, str, list[str], tuple[str]] 

529 ) -> Union[ImageFile, Sequence[ImageFile]]: 

530 lst = self.ids_function() 

531 if isinstance(index, slice): 

532 indices = range(*index.indices(len(self))) 

533 lst = [lst[x] for x in indices] 

534 cls = type(self) 

535 return cls((lambda: lst), self.get_function) 

536 if isinstance(index, (str, list, tuple)): 

537 return self.get_function(index) 

538 if not isinstance(index, int): 

539 raise TypeError("Invalid sequence indices type") 

540 len_self = len(lst) 

541 if index < 0: 

542 # support negative indexes 

543 index += len_self 

544 if not (0 <= index < len_self): 

545 raise IndexError("Sequence index out of range") 

546 return self.get_function(lst[index]) 

547 

548 def __iter__(self) -> Iterator[ImageFile]: 

549 for i in range(len(self)): 

550 yield self[i] 

551 

552 def __str__(self) -> str: 

553 p = [f"Image_{i}={n}" for i, n in enumerate(self.ids_function())] 

554 return f"[{', '.join(p)}]" 

555 

556 

557class PageObject(DictionaryObject): 

558 """ 

559 PageObject represents a single page within a PDF file. 

560 

561 Typically these objects will be created by accessing the 

562 :attr:`pages<pypdf.PdfReader.pages>` property of the 

563 :class:`PdfReader<pypdf.PdfReader>` class, but it is 

564 also possible to create an empty page with the 

565 :meth:`create_blank_page()<pypdf._page.PageObject.create_blank_page>` static method. 

566 

567 Args: 

568 pdf: PDF file the page belongs to. 

569 indirect_reference: Stores the original indirect reference to 

570 this object in its source PDF 

571 

572 """ 

573 

574 original_page: "PageObject" # very local use in writer when appending 

575 

576 def __init__( 

577 self, 

578 pdf: Optional[PdfCommonDocProtocol] = None, 

579 indirect_reference: Optional[IndirectObject] = None, 

580 ) -> None: 

581 DictionaryObject.__init__(self) 

582 self.pdf = pdf 

583 self._content_stream_images: Optional[dict[str, Optional[ImageFile]]] = None 

584 self.indirect_reference = indirect_reference 

585 if not is_null_or_none(indirect_reference): 

586 assert indirect_reference is not None, "mypy" 

587 self.update(cast(DictionaryObject, indirect_reference.get_object())) 

588 

589 def hash_bin(self) -> int: 

590 """ 

591 Used to detect modified object. 

592 

593 Note: this function is overloaded to return the same results 

594 as a DictionaryObject. 

595 

596 Returns: 

597 Hash considering type and value. 

598 

599 """ 

600 return hash( 

601 (DictionaryObject, tuple(((k, v.hash_bin()) for k, v in self.items()))) 

602 ) 

603 

604 def hash_value_data(self) -> bytes: 

605 data = super().hash_value_data() 

606 data += f"{id(self)}".encode() 

607 return data 

608 

609 @property 

610 def user_unit(self) -> float: 

611 """ 

612 A read-only positive number giving the size of user space units. 

613 

614 It is in multiples of 1/72 inch. Hence a value of 1 means a user 

615 space unit is 1/72 inch, and a value of 3 means that a user 

616 space unit is 3/72 inch. 

617 """ 

618 return cast(float, self.get(PG.USER_UNIT, 1)) 

619 

620 @staticmethod 

621 def create_blank_page( 

622 pdf: Optional[PdfCommonDocProtocol] = None, 

623 width: Union[float, Decimal, None] = None, 

624 height: Union[float, Decimal, None] = None, 

625 ) -> "PageObject": 

626 """ 

627 Return a new blank page. 

628 

629 If ``width`` or ``height`` is ``None``, try to get the page size 

630 from the last page of *pdf*. 

631 

632 Args: 

633 pdf: PDF file the page is within. 

634 width: The width of the new page expressed in default user 

635 space units. 

636 height: The height of the new page expressed in default user 

637 space units. 

638 

639 Returns: 

640 The new blank page 

641 

642 Raises: 

643 PageSizeNotDefinedError: if ``pdf`` is ``None`` or contains 

644 no page 

645 

646 """ 

647 page = PageObject(pdf) 

648 

649 # Creates a new page (cf PDF Reference §7.7.3.3) 

650 page.__setitem__(NameObject(PG.TYPE), NameObject("/Page")) 

651 page.__setitem__(NameObject(PG.PARENT), NullObject()) 

652 page.__setitem__(NameObject(PG.RESOURCES), DictionaryObject()) 

653 if width is None or height is None: 

654 if pdf is not None and len(pdf.pages) > 0: 

655 lastpage = pdf.pages[len(pdf.pages) - 1] 

656 width = lastpage.mediabox.width 

657 height = lastpage.mediabox.height 

658 else: 

659 raise PageSizeNotDefinedError 

660 page.__setitem__( 

661 NameObject(PG.MEDIABOX), RectangleObject((0, 0, width, height)) 

662 ) 

663 

664 return page 

665 

666 def _get_ids_image( 

667 self, 

668 obj: Optional[DictionaryObject] = None, 

669 ancest: Optional[list[str]] = None, 

670 call_stack: Optional[list[Any]] = None, 

671 ) -> list[Union[str, list[str]]]: 

672 if call_stack is None: 

673 call_stack = [] 

674 _i = getattr(obj, "indirect_reference", None) 

675 if _i in call_stack: 

676 return [] 

677 call_stack.append(_i) 

678 if self._content_stream_images is None: 

679 self._content_stream_images = self._parse_images_from_content_stream() 

680 if obj is None: 

681 obj = self 

682 if ancest is None: 

683 ancest = [] 

684 lst: list[Union[str, list[str]]] = [] 

685 if ( 

686 PG.RESOURCES not in obj or 

687 is_null_or_none(resources := cast(DictionaryObject, obj[PG.RESOURCES])) or 

688 RES.XOBJECT not in resources 

689 ): 

690 # Forms without XObject resources have no images inside them 

691 if len(ancest) > 0: 

692 return [] 

693 # for inline images, cache dict entries are not None 

694 return [image_name for image_name, image_value in self._content_stream_images.items() if image_value] 

695 

696 x_object = resources[RES.XOBJECT].get_object() 

697 if not isinstance(x_object, DictionaryObject): 

698 logger_warning( 

699 "XObject resources are not a dictionary: %(x_object)s", 

700 source=__name__, 

701 x_object=x_object, 

702 ) 

703 return [] 

704 

705 # Iterate through all XObject resources 

706 for o in x_object: 

707 entry = x_object[o] 

708 # Skip non-stream objects (only process StreamObject) 

709 if not isinstance(entry, StreamObject): 

710 continue 

711 if entry.get(ImageAttributes.SUBTYPE, "") == "/Image": 

712 # If it's an image, add it to lst for further processing 

713 lst.append(o if len(ancest) == 0 else [*ancest, o]) 

714 else: 

715 # If it's a form, recursively search for images inside it 

716 # Forms may contain images that are Do-referenced in their content stream 

717 lst.extend(self._get_ids_image(entry, [*ancest, o], call_stack)) 

718 

719 # Removes duplicates and preserves order 

720 deduplicated = lst.copy() 

721 

722 # Add inline images from _content_stream_images 

723 for object_name, object_value in self._content_stream_images.items(): 

724 # inline images have cache populated 

725 if object_name not in deduplicated and object_value: 

726 deduplicated.append(object_name) 

727 

728 return deduplicated 

729 

730 def _get_image( 

731 self, 

732 image_id: Union[str, list[str], tuple[str]], 

733 obj: Optional[DictionaryObject] = None, 

734 ) -> ImageFile: 

735 if obj is None: 

736 obj = cast(DictionaryObject, self) 

737 if isinstance(image_id, tuple): 

738 image_id = list(image_id) 

739 if isinstance(image_id, list) and len(image_id) == 1: 

740 image_id = image_id[0] 

741 xobjs: Optional[DictionaryObject] = None 

742 try: 

743 xobjs = cast( 

744 DictionaryObject, cast(DictionaryObject, obj[PG.RESOURCES])[RES.XOBJECT] 

745 ) 

746 except KeyError as exc: 

747 if not (image_id[0] == "~" and image_id[-1] == "~"): 

748 raise KeyError( 

749 f"Cannot access image object {image_id} without XObject resources" 

750 ) from exc 

751 if isinstance(image_id, str): 

752 if image_id[0] == "~" and image_id[-1] == "~": 

753 if self._content_stream_images is None: 

754 self._content_stream_images = self._parse_images_from_content_stream() 

755 if image_id not in self._content_stream_images: 

756 raise KeyError(f"Image {image_id} not found") 

757 image_file = self._content_stream_images[image_id] 

758 assert image_file is not None 

759 return image_file 

760 

761 # Do-referenced image name (non-inline string keys like /Im0) 

762 assert xobjs is not None 

763 if image_id not in xobjs: 

764 raise KeyError(f"Image {image_id} not found") 

765 xobj = cast(DictionaryObject, xobjs[image_id]) 

766 if xobj.get(ImageAttributes.SUBTYPE, "") != "/Image": 

767 raise KeyError(f"XObject {image_id} is not an image") 

768 

769 # Check if displayed (in content stream) 

770 is_displayed = self._content_stream_images is not None and image_id in self._content_stream_images 

771 

772 from .generic._image_xobject import _xobj_to_image # noqa: PLC0415 

773 extension, byte_stream, img = _xobj_to_image(xobj) 

774 return ImageFile( 

775 name=f"{image_id[1:]}{extension}", 

776 data=byte_stream, 

777 image=img, 

778 indirect_reference=xobj.indirect_reference, 

779 is_inline=False, 

780 is_displayed=is_displayed, 

781 ) 

782 # in a subobject 

783 assert xobjs is not None 

784 ids = image_id[1:] 

785 return self._get_image(ids, cast(DictionaryObject, xobjs[image_id[0]])) 

786 

787 @property 

788 def images(self) -> VirtualListImages: 

789 """ 

790 Read-only property emulating a list of images on a page. 

791 

792 Get a list of all images on the page. The key can be: 

793 - A string (for the top object) 

794 - A tuple (for images within XObject forms) 

795 - An integer 

796 

797 Examples: 

798 * `reader.pages[0].images[0]` # return first image 

799 * `reader.pages[0].images['/I0']` # return image '/I0' 

800 * `reader.pages[0].images['/TP1','/Image1']` # return image '/Image1' within '/TP1' XObject form 

801 * `for img in reader.pages[0].images:` # loops through all objects 

802 

803 Example usage: 

804 

805 >>> from pypdf import PdfWriter 

806 >>> writer = PdfWriter() 

807 >>> page = writer.add_blank_page(800, 600) 

808 >>> images = page.images 

809 

810 """ 

811 return VirtualListImages(self._get_ids_image, self._get_image) 

812 

813 @property 

814 def inline_images(self) -> Optional[dict[str, ImageFile]]: 

815 """ 

816 Return only inline images from the page. 

817 

818 .. deprecated:: 

819 Use :attr:`images` and filter by :attr:`ImageFile.is_inline` instead. 

820 This property will be removed in pypdf 7.0. 

821 

822 Examples: 

823 >>> from pypdf import PdfWriter 

824 >>> writer = PdfWriter() 

825 >>> page = writer.add_blank_page(800,600) 

826 >>> for image_name, image_file in page.images.items(): 

827 ... if image_file.is_inline: 

828 ... print(f"{image_name} is inline") 

829 """ 

830 deprecate_with_replacement( 

831 "PageObject.inline_images", 

832 "PageObject.images", 

833 "7.0.0", 

834 ) 

835 if self._content_stream_images is None: 

836 return None 

837 return { 

838 image_name: image_file 

839 for image_name, image_file in self._content_stream_images.items() 

840 if image_file and image_file.is_inline # for inline images, image_file is populated 

841 } 

842 

843 @inline_images.setter 

844 def inline_images(self, value: Optional[dict[str, ImageFile]]) -> None: 

845 deprecate_no_replacement( 

846 "PageObject.inline_images", 

847 "7.0.0", 

848 ) 

849 if value is None: 

850 self._content_stream_images = None 

851 else: 

852 assert self._content_stream_images is not None, "Can't edit inline_images before accessing images" 

853 self._content_stream_images.update(value) 

854 

855 def _translate_value_inline_image(self, k: str, v: PdfObject) -> PdfObject: 

856 """Translate values used in inline image""" 

857 try: 

858 v = NameObject(_INLINE_IMAGE_VALUE_MAPPING[cast(str, v)]) 

859 except (TypeError, KeyError) as exception: 

860 if isinstance(v, NameObject): 

861 # It is a custom name, thus we have to look in resources. 

862 # The only applicable case is for ColorSpace. 

863 if "/Resources" in self: 

864 resources = cast(DictionaryObject, self["/Resources"]) 

865 if "/ColorSpace" in resources and v in cast(DictionaryObject, resources["/ColorSpace"]): 

866 color_space = cast(DictionaryObject, resources["/ColorSpace"]) 

867 return color_space[v] 

868 raise PdfReadError(f"Cannot find resource entry {v} for {k}") from exception 

869 return v 

870 

871 def _parse_images_from_content_stream(self) -> dict[str, Optional[ImageFile]]: 

872 """Load images from content stream. Includes both inline images and Do-referenced images. 

873 

874 This method scans the page content stream and extracts: 

875 

876 1. **Inline images** (~0~, ~1~...): Embedded directly in content stream via BI/EI operators 

877 - is_inline=True, is_displayed=True, indirect_reference=None 

878 

879 2. **Do-referenced objects** (/Im0, /Im1..., /Form1...): Referenced via "Do" operator 

880 - is_inline=False, is_displayed=True, indirect_reference=<image object> 

881 

882 3. **Pure XObject images** (/I0, /Image1...): Defined in Resources only (not in content stream) 

883 - is_inline=False, is_displayed=False, indirect_reference=<image object> 

884 

885 Returns: 

886 Dictionary mapping names to ImageFile instances (inline) or None (Do-referenced). 

887 """ 

888 content = self.get_contents() 

889 if is_null_or_none(content): 

890 return {} 

891 imgs_data = [] 

892 do_image_names: list[bytes] = [] 

893 assert content is not None, "mypy" 

894 for param, ope in content.operations: 

895 if ope == b"INLINE IMAGE": 

896 imgs_data.append( 

897 {"settings": param["settings"], "__streamdata__": param["data"]} 

898 ) 

899 elif ope == b"Do" and param: 

900 do_image_names.append(param[0]) # First operand is the XObject name 

901 elif ope in (b"BI", b"EI", b"ID"): # pragma: no cover 

902 raise PdfReadError( 

903 f"{ope!r} operator met whereas not expected, " 

904 "please share use case with pypdf dev team" 

905 ) 

906 

907 files: dict[str, Optional[ImageFile]] = {} 

908 # Process Do-referenced objects first (images + forms, no subtype check) 

909 files = dict.fromkeys(list(map(str, do_image_names)), None) 

910 

911 # Then process inline images 

912 for num, ii in enumerate(imgs_data): 

913 init = { 

914 "__streamdata__": ii["__streamdata__"], 

915 "/Length": len(ii["__streamdata__"]), 

916 } 

917 for k, v in ii["settings"].items(): 

918 if k in {"/Length", "/L"}: # no length is expected 

919 continue 

920 if isinstance(v, list): 

921 v = ArrayObject( 

922 [self._translate_value_inline_image(k, x) for x in v] 

923 ) 

924 else: 

925 v = self._translate_value_inline_image(k, v) 

926 if k in _INLINE_IMAGE_KEY_MAPPING: 

927 k = NameObject(_INLINE_IMAGE_KEY_MAPPING[k]) 

928 else: 

929 logger_warning( 

930 "Unknown inline image key %(key)s, keeping it as-is.", 

931 source=__name__, 

932 key=k, 

933 ) 

934 k = NameObject(k) 

935 if k not in init: 

936 init[k] = v 

937 ii["object"] = EncodedStreamObject.initialize_from_dictionary(init) 

938 from .generic._image_xobject import _xobj_to_image # noqa: PLC0415 

939 extension, byte_stream, img = _xobj_to_image(ii["object"]) 

940 files[f"~{num}~"] = ImageFile( 

941 name=f"~{num}~{extension}", 

942 data=byte_stream, 

943 image=img, 

944 indirect_reference=None, 

945 is_inline=True, 

946 is_displayed=True, 

947 ) 

948 

949 return files 

950 

951 @property 

952 def rotation(self) -> int: 

953 """ 

954 The visual rotation of the page. 

955 

956 This number has to be a multiple of 90 degrees: 0, 90, 180, or 270 are 

957 valid values. This property does not affect ``/Contents``. 

958 """ 

959 rotate_obj = self.get(PG.ROTATE, 0) 

960 return rotate_obj if isinstance(rotate_obj, int) else rotate_obj.get_object() 

961 

962 @rotation.setter 

963 def rotation(self, r: float) -> None: 

964 self[NameObject(PG.ROTATE)] = NumberObject((((int(r) + 45) // 90) * 90) % 360) 

965 

966 def transfer_rotation_to_content(self) -> None: 

967 """ 

968 Apply the rotation of the page to the content and the media/crop/... 

969 boxes. 

970 

971 It is recommended to apply this function before page merging. 

972 """ 

973 r = -self.rotation # rotation to apply is in the otherway 

974 self.rotation = 0 

975 mb = RectangleObject(self.mediabox) 

976 trsf = ( 

977 Transformation() 

978 .translate( 

979 -float(mb.left + mb.width / 2), -float(mb.bottom + mb.height / 2) 

980 ) 

981 .rotate(r) 

982 ) 

983 pt1 = trsf.apply_on(mb.lower_left) 

984 pt2 = trsf.apply_on(mb.upper_right) 

985 trsf = trsf.translate(-min(pt1[0], pt2[0]), -min(pt1[1], pt2[1])) 

986 self.add_transformation(trsf, False) 

987 for b in ["/MediaBox", "/CropBox", "/BleedBox", "/TrimBox", "/ArtBox"]: 

988 if b in self: 

989 rr = RectangleObject(self[b]) # type: ignore[arg-type] 

990 pt1 = trsf.apply_on(rr.lower_left) 

991 pt2 = trsf.apply_on(rr.upper_right) 

992 self[NameObject(b)] = RectangleObject( 

993 ( 

994 min(pt1[0], pt2[0]), 

995 min(pt1[1], pt2[1]), 

996 max(pt1[0], pt2[0]), 

997 max(pt1[1], pt2[1]), 

998 ) 

999 ) 

1000 

1001 def rotate(self, angle: int) -> "PageObject": 

1002 """ 

1003 Rotate a page clockwise by increments of 90 degrees. 

1004 

1005 Args: 

1006 angle: Angle to rotate the page. Must be an increment of 90 deg. 

1007 

1008 Returns: 

1009 The rotated PageObject 

1010 

1011 """ 

1012 if angle % 90 != 0: 

1013 raise ValueError("Rotation angle must be a multiple of 90") 

1014 self[NameObject(PG.ROTATE)] = NumberObject(self.rotation + angle) 

1015 return self 

1016 

1017 def _merge_resources( 

1018 self, 

1019 res1: DictionaryObject, 

1020 res2: DictionaryObject, 

1021 resource: Any, 

1022 new_res1: bool = True, 

1023 ) -> tuple[dict[str, Any], dict[str, Any]]: 

1024 try: 

1025 assert isinstance(self.indirect_reference, IndirectObject) 

1026 pdf = self.indirect_reference.pdf 

1027 is_pdf_writer = hasattr( 

1028 pdf, "_add_object" 

1029 ) # expect isinstance(pdf, PdfWriter) 

1030 except (AssertionError, AttributeError): 

1031 pdf = None 

1032 is_pdf_writer = False 

1033 

1034 def compute_unique_key(base_key: str) -> tuple[str, bool]: 

1035 """ 

1036 Find a key that either doesn't already exist or has the same value 

1037 (indicated by the bool) 

1038 

1039 Args: 

1040 base_key: An index is added to this to get the computed key 

1041 

1042 Returns: 

1043 A tuple (computed key, bool) where the boolean indicates 

1044 if there is a resource of the given computed_key with the same 

1045 value. 

1046 

1047 """ 

1048 value = page2res.raw_get(base_key) 

1049 # TODO: a possible improvement for writer, the indirect_reference 

1050 # cannot be found because translated 

1051 

1052 # try the current key first (e.g. "foo"), but otherwise iterate 

1053 # through "foo-0", "foo-1", etc. new_res can contain only finitely 

1054 # many keys, thus this'll eventually end, even if it's been crafted 

1055 # to be maximally annoying. 

1056 computed_key = base_key 

1057 idx = 0 

1058 while computed_key in new_res: 

1059 if new_res.raw_get(computed_key) == value: 

1060 # there's already a resource of this name, with the exact 

1061 # same value 

1062 return computed_key, True 

1063 computed_key = f"{base_key}-{idx}" 

1064 idx += 1 

1065 return computed_key, False 

1066 

1067 if new_res1: 

1068 new_res = DictionaryObject() 

1069 new_res.update(res1.get(resource, DictionaryObject()).get_object()) 

1070 else: 

1071 new_res = cast(DictionaryObject, res1[resource]) 

1072 page2res = cast( 

1073 DictionaryObject, res2.get(resource, DictionaryObject()).get_object() 

1074 ) 

1075 rename_res = {} 

1076 for key in page2res: 

1077 unique_key, same_value = compute_unique_key(key) 

1078 newname = NameObject(unique_key) 

1079 if key != unique_key: 

1080 # we have to use a different name for this 

1081 rename_res[key] = newname 

1082 

1083 if not same_value: 

1084 if is_pdf_writer: 

1085 new_res[newname] = page2res.raw_get(key).clone(pdf) 

1086 try: 

1087 new_res[newname] = new_res[newname].indirect_reference 

1088 except AttributeError: 

1089 pass 

1090 else: 

1091 new_res[newname] = page2res.raw_get(key) 

1092 lst = sorted(new_res.items()) 

1093 new_res.clear() 

1094 for el in lst: 

1095 new_res[el[0]] = el[1] 

1096 return new_res, rename_res 

1097 

1098 @staticmethod 

1099 def _content_stream_rename( 

1100 stream: ContentStream, 

1101 rename: dict[Any, Any], 

1102 pdf: Optional[PdfCommonDocProtocol], 

1103 ) -> ContentStream: 

1104 if not rename: 

1105 return stream 

1106 stream = ContentStream(stream, pdf) 

1107 for operands, _operator in stream.operations: 

1108 if isinstance(operands, list): 

1109 for i, op in enumerate(operands): 

1110 if isinstance(op, NameObject): 

1111 operands[i] = rename.get(op, op) 

1112 elif isinstance(operands, dict): 

1113 for i, op in operands.items(): 

1114 if isinstance(op, NameObject): 

1115 operands[i] = rename.get(op, op) 

1116 else: 

1117 raise KeyError(f"Type of operands is {type(operands)}") 

1118 return stream 

1119 

1120 @staticmethod 

1121 def _add_transformation_matrix( 

1122 contents: Any, 

1123 pdf: Optional[PdfCommonDocProtocol], 

1124 ctm: CompressedTransformationMatrix, 

1125 ) -> ContentStream: 

1126 """Add transformation matrix at the beginning of the given contents stream.""" 

1127 content_stream = ContentStream(contents, pdf) 

1128 content_stream.operations.insert( 

1129 0, 

1130 ( 

1131 [FloatObject(x) for x in ctm], 

1132 b"cm", 

1133 ), 

1134 ) 

1135 return content_stream 

1136 

1137 def _get_contents_as_bytes(self) -> Optional[bytes]: 

1138 """ 

1139 Return the page contents as bytes. 

1140 

1141 Returns: 

1142 The ``/Contents`` object as bytes, or ``None`` if it doesn't exist. 

1143 

1144 """ 

1145 if PG.CONTENTS in self: 

1146 obj = self[PG.CONTENTS].get_object() 

1147 if isinstance(obj, list): 

1148 return b"".join(x.get_object().get_data() for x in obj) 

1149 return cast(EncodedStreamObject, obj).get_data() 

1150 return None 

1151 

1152 def get_contents(self) -> Optional[ContentStream]: 

1153 """ 

1154 Access the page contents. 

1155 

1156 Returns: 

1157 The ``/Contents`` object, or ``None`` if it does not exist. 

1158 ``/Contents`` is optional, as described in §7.7.3.3 of the PDF Reference. 

1159 

1160 """ 

1161 if PG.CONTENTS in self: 

1162 try: 

1163 pdf = cast(IndirectObject, self.indirect_reference).pdf 

1164 except AttributeError: 

1165 pdf = None 

1166 obj = self[PG.CONTENTS] 

1167 if is_null_or_none(obj): 

1168 return None 

1169 resolved_object = obj.get_object() 

1170 return ContentStream(resolved_object, pdf) 

1171 return None 

1172 

1173 def replace_contents( 

1174 self, content: Union[ContentStream, EncodedStreamObject, ArrayObject, None] 

1175 ) -> None: 

1176 """ 

1177 Replace the page contents with the new content and nullify old objects 

1178 Args: 

1179 content: new content; if None delete the content field. 

1180 """ 

1181 if not hasattr(self, "indirect_reference") or self.indirect_reference is None: 

1182 # the page is not attached : the content is directly attached. 

1183 self[NameObject(PG.CONTENTS)] = content 

1184 return 

1185 

1186 from pypdf._writer import PdfWriter # noqa: PLC0415 

1187 if not isinstance(self.indirect_reference.pdf, PdfWriter): 

1188 deprecate( 

1189 "Calling `PageObject.replace_contents()` for pages not assigned to a writer is deprecated " 

1190 "and will be removed in pypdf 7.0.0. Attach the page to the writer first or use " 

1191 "`PdfWriter(clone_from=...)` directly. The existing approach has proved being unreliable." 

1192 ) 

1193 

1194 writer = self.indirect_reference.pdf 

1195 # Resolve /Contents because it may be an indirect reference to an 

1196 # ArrayObject. Without resolving it, an indirect contents array is not 

1197 # recognized and its stream objects are left in the writer's object list. 

1198 old_contents = self.get(PG.CONTENTS, None) 

1199 if old_contents is not None: 

1200 old_contents = old_contents.get_object() 

1201 if isinstance(old_contents, ArrayObject): 

1202 for reference in old_contents: 

1203 if not isinstance(reference, IndirectObject): 

1204 # Direct objects are not part of the writer's object list. 

1205 continue 

1206 try: 

1207 writer._replace_object(indirect_reference=reference, obj=NullObject()) 

1208 except ValueError: 

1209 # Occurs when called on PdfReader. 

1210 pass 

1211 

1212 if isinstance(content, ArrayObject): 

1213 content = ArrayObject(writer._add_object(obj) for obj in content) 

1214 

1215 if is_null_or_none(content): 

1216 if PG.CONTENTS not in self: 

1217 return 

1218 assert self[PG.CONTENTS].indirect_reference is not None 

1219 writer._replace_object(indirect_reference=self[PG.CONTENTS].indirect_reference, obj=NullObject()) 

1220 del self[PG.CONTENTS] 

1221 elif not hasattr(self.get(PG.CONTENTS, None), "indirect_reference"): 

1222 try: 

1223 self[NameObject(PG.CONTENTS)] = writer._add_object(content) 

1224 except AttributeError: 

1225 # applies at least for page not in writer 

1226 # as a backup solution, we put content as an object although not in accordance with pdf ref 

1227 # this will be fixed with the _add_object 

1228 self[NameObject(PG.CONTENTS)] = content 

1229 else: 

1230 assert content is not None, "mypy" 

1231 content.indirect_reference = self[ 

1232 PG.CONTENTS 

1233 ].indirect_reference # TODO: in the future may require generation management 

1234 try: 

1235 writer._replace_object(indirect_reference=content.indirect_reference, obj=content) 

1236 except AttributeError: 

1237 # applies at least for page not in writer 

1238 # as a backup solution, we put content as an object although not in accordance with pdf ref 

1239 # this will be fixed with the _add_object 

1240 self[NameObject(PG.CONTENTS)] = content 

1241 # forces recalculation of images 

1242 self._content_stream_images = None 

1243 

1244 def merge_page( 

1245 self, page2: "PageObject", expand: bool = False, over: bool = True 

1246 ) -> None: 

1247 """ 

1248 Merge the content streams of two pages into one. 

1249 

1250 Resource references (e.g. fonts) are maintained from both pages. 

1251 The mediabox, cropbox, etc of this page are not altered. 

1252 The parameter page's content stream will 

1253 be added to the end of this page's content stream, 

1254 meaning that it will be drawn after, or "on top" of this page. 

1255 

1256 Args: 

1257 page2: The page to be merged into this one. Should be 

1258 an instance of :class:`PageObject<PageObject>`. 

1259 over: set the page2 content over page1 if True (default) else under 

1260 expand: If True, the current page dimensions will be 

1261 expanded to accommodate the dimensions of the page to be merged. 

1262 

1263 """ 

1264 self._merge_page(page2, over=over, expand=expand) 

1265 

1266 def _merge_page( 

1267 self, 

1268 page2: "PageObject", 

1269 page2_transformation: Optional[Callable[[Any], ContentStream]] = None, 

1270 ctm: Optional[CompressedTransformationMatrix] = None, 

1271 over: bool = True, 

1272 expand: bool = False, 

1273 ) -> None: 

1274 # First we work on merging the resource dictionaries. This allows us 

1275 # to find out what symbols in the content streams we might need to 

1276 # rename. 

1277 try: 

1278 assert isinstance(self.indirect_reference, IndirectObject) 

1279 if hasattr(self.indirect_reference.pdf, "_add_object"): # to detect PdfWriter 

1280 return self._merge_page_writer( 

1281 page2, page2_transformation, ctm, over, expand 

1282 ) 

1283 except (AssertionError, AttributeError): 

1284 pass 

1285 

1286 new_resources = DictionaryObject() 

1287 rename: dict[str, Any] = {} 

1288 original_resources = cast(DictionaryObject, self.get(PG.RESOURCES, DictionaryObject()).get_object()) 

1289 page2_resources = cast(DictionaryObject, page2.get(PG.RESOURCES, DictionaryObject()).get_object()) 

1290 new_annots = ArrayObject() 

1291 

1292 for page in (self, page2): 

1293 if PG.ANNOTS in page: 

1294 annots = page[PG.ANNOTS] 

1295 if isinstance(annots, ArrayObject): 

1296 new_annots.extend(annots) 

1297 self[NameObject(PG.ANNOTS)] = new_annots 

1298 

1299 for res in ( 

1300 RES.EXT_G_STATE, 

1301 RES.COLOR_SPACE, 

1302 RES.PATTERN, 

1303 RES.SHADING, 

1304 RES.XOBJECT, 

1305 RES.FONT, 

1306 RES.PROPERTIES, 

1307 ): 

1308 new, new_resource_name = self._merge_resources( 

1309 original_resources, page2_resources, res 

1310 ) 

1311 if new: 

1312 new_resources[NameObject(res)] = new 

1313 rename.update(new_resource_name) 

1314 

1315 # Combine /ProcSet sets, making sure there is a consistent order 

1316 new_resources[NameObject(RES.PROC_SET)] = ArrayObject( 

1317 sorted( 

1318 set( 

1319 original_resources.get(RES.PROC_SET, ArrayObject()).get_object() 

1320 ).union( 

1321 set(page2_resources.get(RES.PROC_SET, ArrayObject()).get_object()) 

1322 ) 

1323 ) 

1324 ) 

1325 

1326 new_content_array = ArrayObject() 

1327 original_content = self.get_contents() 

1328 if original_content is not None: 

1329 original_content.isolate_graphics_state() 

1330 new_content_array.append(original_content) 

1331 

1332 page2_content = page2.get_contents() 

1333 if page2_content is not None: 

1334 configuration = get_configuration() 

1335 rect = getattr(page2, configuration.page_merge_box) 

1336 page2_content.operations.insert( 

1337 0, 

1338 ( 

1339 map( 

1340 FloatObject, 

1341 [ 

1342 rect.left, 

1343 rect.bottom, 

1344 rect.width, 

1345 rect.height, 

1346 ], 

1347 ), 

1348 b"re", 

1349 ), 

1350 ) 

1351 page2_content.operations.insert(1, ([], b"W")) 

1352 page2_content.operations.insert(2, ([], b"n")) 

1353 if page2_transformation is not None: 

1354 page2_content = page2_transformation(page2_content) 

1355 page2_content = PageObject._content_stream_rename( 

1356 page2_content, rename, self.pdf 

1357 ) 

1358 page2_content.isolate_graphics_state() 

1359 if over: 

1360 new_content_array.append(page2_content) 

1361 else: 

1362 new_content_array.insert(0, page2_content) 

1363 

1364 # if expanding the page to fit a new page, calculate the new media box size 

1365 if expand: 

1366 self._expand_mediabox(page2, ctm) 

1367 

1368 self.replace_contents(ContentStream(new_content_array, self.pdf)) 

1369 self[NameObject(PG.RESOURCES)] = new_resources 

1370 

1371 return None 

1372 

1373 def _merge_page_writer( 

1374 self, 

1375 page2: "PageObject", 

1376 page2transformation: Optional[Callable[[Any], ContentStream]] = None, 

1377 ctm: Optional[CompressedTransformationMatrix] = None, 

1378 over: bool = True, 

1379 expand: bool = False, 

1380 ) -> None: 

1381 # First we work on merging the resource dictionaries. This allows us 

1382 # to find which symbols in the content streams we might need to 

1383 # rename. 

1384 assert isinstance(self.indirect_reference, IndirectObject) 

1385 pdf = self.indirect_reference.pdf 

1386 

1387 if PG.RESOURCES not in self: 

1388 self[NameObject(PG.RESOURCES)] = DictionaryObject() 

1389 original_resources = cast(DictionaryObject, self[PG.RESOURCES].get_object()) 

1390 if PG.RESOURCES not in page2: 

1391 page2resources = DictionaryObject() 

1392 else: 

1393 page2resources = cast(DictionaryObject, page2[PG.RESOURCES].get_object()) 

1394 

1395 rename = {} 

1396 for res in ( 

1397 RES.EXT_G_STATE, 

1398 RES.COLOR_SPACE, 

1399 RES.PATTERN, 

1400 RES.SHADING, 

1401 RES.XOBJECT, 

1402 RES.FONT, 

1403 RES.PROPERTIES, 

1404 ): 

1405 if res in page2resources: 

1406 if res not in original_resources: 

1407 original_resources[NameObject(res)] = DictionaryObject() 

1408 _, newrename = self._merge_resources( 

1409 original_resources, page2resources, res, False 

1410 ) 

1411 rename.update(newrename) 

1412 # Combine /ProcSet sets 

1413 if RES.PROC_SET in page2resources: 

1414 if RES.PROC_SET not in original_resources: 

1415 original_resources[NameObject(RES.PROC_SET)] = ArrayObject() 

1416 arr = cast(ArrayObject, original_resources[RES.PROC_SET]) 

1417 for x in cast(ArrayObject, page2resources[RES.PROC_SET]): 

1418 if x not in arr: 

1419 arr.append(x) 

1420 arr.sort() 

1421 

1422 if not is_null_or_none(page2.get(PG.ANNOTS, None)): 

1423 if PG.ANNOTS not in self: 

1424 self[NameObject(PG.ANNOTS)] = ArrayObject() 

1425 annots = cast(ArrayObject, self[PG.ANNOTS].get_object()) 

1426 if ctm is None: 

1427 trsf = Transformation() 

1428 else: 

1429 trsf = Transformation(ctm) 

1430 # Ensure we are working on a copy of the list. Otherwise, if both pages 

1431 # are the same object, we might run into an infinite loop. 

1432 for a in cast(ArrayObject, deepcopy(page2[PG.ANNOTS])): 

1433 a = a.get_object() 

1434 aa = a.clone( 

1435 pdf, 

1436 ignore_fields=("/P", "/StructParent", "/Parent"), 

1437 force_duplicate=True, 

1438 ) 

1439 r = cast(ArrayObject, a["/Rect"]) 

1440 pt1 = trsf.apply_on((r[0], r[1]), True) 

1441 pt2 = trsf.apply_on((r[2], r[3]), True) 

1442 aa[NameObject("/Rect")] = ArrayObject( 

1443 ( 

1444 min(pt1[0], pt2[0]), 

1445 min(pt1[1], pt2[1]), 

1446 max(pt1[0], pt2[0]), 

1447 max(pt1[1], pt2[1]), 

1448 ) 

1449 ) 

1450 if "/QuadPoints" in a: 

1451 q = cast(ArrayObject, a["/QuadPoints"]) 

1452 aa[NameObject("/QuadPoints")] = ArrayObject( 

1453 trsf.apply_on((q[0], q[1]), True) 

1454 + trsf.apply_on((q[2], q[3]), True) 

1455 + trsf.apply_on((q[4], q[5]), True) 

1456 + trsf.apply_on((q[6], q[7]), True) 

1457 ) 

1458 # The /Rect update above only repositions and resizes the 

1459 # annotation's bounding box; it does not touch the 

1460 # appearance stream's own coordinate system. See 

1461 # transform_annotation_appearance for why that matters. 

1462 from pypdf.generic._appearance_stream import ( # noqa: PLC0415 

1463 transform_annotation_appearance, 

1464 ) 

1465 transform_annotation_appearance(aa, trsf) 

1466 try: 

1467 aa["/Popup"][NameObject("/Parent")] = aa.indirect_reference 

1468 except KeyError: 

1469 pass 

1470 try: 

1471 aa[NameObject("/P")] = self.indirect_reference 

1472 annots.append(aa.indirect_reference) 

1473 except AttributeError: 

1474 pass 

1475 

1476 new_content_array = ArrayObject() 

1477 original_content = self.get_contents() 

1478 if original_content is not None: 

1479 original_content.isolate_graphics_state() 

1480 new_content_array.append(original_content) 

1481 

1482 page2content = page2.get_contents() 

1483 if page2content is not None: 

1484 configuration = get_configuration() 

1485 rect = getattr(page2, configuration.page_merge_box) 

1486 page2content.operations.insert( 

1487 0, 

1488 ( 

1489 map( 

1490 FloatObject, 

1491 [ 

1492 rect.left, 

1493 rect.bottom, 

1494 rect.width, 

1495 rect.height, 

1496 ], 

1497 ), 

1498 b"re", 

1499 ), 

1500 ) 

1501 page2content.operations.insert(1, ([], b"W")) 

1502 page2content.operations.insert(2, ([], b"n")) 

1503 if page2transformation is not None: 

1504 page2content = page2transformation(page2content) 

1505 page2content = PageObject._content_stream_rename( 

1506 page2content, rename, self.pdf 

1507 ) 

1508 page2content.isolate_graphics_state() 

1509 if over: 

1510 new_content_array.append(page2content) 

1511 else: 

1512 new_content_array.insert(0, page2content) 

1513 

1514 # if expanding the page to fit a new page, calculate the new media box size 

1515 if expand: 

1516 self._expand_mediabox(page2, ctm) 

1517 

1518 self.replace_contents(new_content_array) 

1519 

1520 def _expand_mediabox( 

1521 self, page2: "PageObject", ctm: Optional[CompressedTransformationMatrix] 

1522 ) -> None: 

1523 corners1 = ( 

1524 self.mediabox.left.as_numeric(), 

1525 self.mediabox.bottom.as_numeric(), 

1526 self.mediabox.right.as_numeric(), 

1527 self.mediabox.top.as_numeric(), 

1528 ) 

1529 corners2 = ( 

1530 page2.mediabox.left.as_numeric(), 

1531 page2.mediabox.bottom.as_numeric(), 

1532 page2.mediabox.left.as_numeric(), 

1533 page2.mediabox.top.as_numeric(), 

1534 page2.mediabox.right.as_numeric(), 

1535 page2.mediabox.top.as_numeric(), 

1536 page2.mediabox.right.as_numeric(), 

1537 page2.mediabox.bottom.as_numeric(), 

1538 ) 

1539 if ctm is not None: 

1540 ctm = tuple(float(x) for x in ctm) # type: ignore[assignment] 

1541 new_x = tuple( 

1542 ctm[0] * corners2[i] + ctm[2] * corners2[i + 1] + ctm[4] 

1543 for i in range(0, 8, 2) 

1544 ) 

1545 new_y = tuple( 

1546 ctm[1] * corners2[i] + ctm[3] * corners2[i + 1] + ctm[5] 

1547 for i in range(0, 8, 2) 

1548 ) 

1549 else: 

1550 new_x = corners2[0:8:2] 

1551 new_y = corners2[1:8:2] 

1552 lowerleft = (min(new_x), min(new_y)) 

1553 upperright = (max(new_x), max(new_y)) 

1554 lowerleft = (min(corners1[0], lowerleft[0]), min(corners1[1], lowerleft[1])) 

1555 upperright = ( 

1556 max(corners1[2], upperright[0]), 

1557 max(corners1[3], upperright[1]), 

1558 ) 

1559 

1560 self.mediabox.lower_left = lowerleft 

1561 self.mediabox.upper_right = upperright 

1562 

1563 def merge_transformed_page( 

1564 self, 

1565 page2: "PageObject", 

1566 ctm: Union[CompressedTransformationMatrix, Transformation], 

1567 over: bool = True, 

1568 expand: bool = False, 

1569 ) -> None: 

1570 """ 

1571 Similar to :meth:`~pypdf._page.PageObject.merge_page`, but a transformation 

1572 matrix is applied to the merged stream. 

1573 

1574 Args: 

1575 page2: The page to be merged into this one. 

1576 ctm: a 6-element tuple containing the operands of the 

1577 transformation matrix 

1578 over: set the page2 content over page1 if True (default) else under 

1579 expand: Whether the page should be expanded to fit the dimensions 

1580 of the page to be merged. 

1581 

1582 """ 

1583 if isinstance(ctm, Transformation): 

1584 ctm = ctm.ctm 

1585 self._merge_page( 

1586 page2, 

1587 lambda page2_content: PageObject._add_transformation_matrix( 

1588 page2_content, page2.pdf, ctm 

1589 ), 

1590 ctm, 

1591 over, 

1592 expand, 

1593 ) 

1594 

1595 def merge_scaled_page( 

1596 self, page2: "PageObject", scale: float, over: bool = True, expand: bool = False 

1597 ) -> None: 

1598 """ 

1599 Similar to :meth:`~pypdf._page.PageObject.merge_page`, but the stream to be merged 

1600 is scaled by applying a transformation matrix. 

1601 

1602 Args: 

1603 page2: The page to be merged into this one. 

1604 scale: The scaling factor 

1605 over: set the page2 content over page1 if True (default) else under 

1606 expand: Whether the page should be expanded to fit the 

1607 dimensions of the page to be merged. 

1608 

1609 """ 

1610 op = Transformation().scale(scale, scale) 

1611 self.merge_transformed_page(page2, op, over, expand) 

1612 

1613 def merge_rotated_page( 

1614 self, 

1615 page2: "PageObject", 

1616 rotation: float, 

1617 over: bool = True, 

1618 expand: bool = False, 

1619 ) -> None: 

1620 """ 

1621 Similar to :meth:`~pypdf._page.PageObject.merge_page`, but the stream to be merged 

1622 is rotated by applying a transformation matrix. 

1623 

1624 Args: 

1625 page2: The page to be merged into this one. 

1626 rotation: The angle of the rotation, in degrees 

1627 over: set the page2 content over page1 if True (default) else under 

1628 expand: Whether the page should be expanded to fit the 

1629 dimensions of the page to be merged. 

1630 

1631 """ 

1632 op = Transformation().rotate(rotation) 

1633 self.merge_transformed_page(page2, op, over, expand) 

1634 

1635 def merge_translated_page( 

1636 self, 

1637 page2: "PageObject", 

1638 tx: float, 

1639 ty: float, 

1640 over: bool = True, 

1641 expand: bool = False, 

1642 ) -> None: 

1643 """ 

1644 Similar to :meth:`~pypdf._page.PageObject.merge_page`, but the stream to be 

1645 merged is translated by applying a transformation matrix. 

1646 

1647 Args: 

1648 page2: the page to be merged into this one. 

1649 tx: The translation on X axis 

1650 ty: The translation on Y axis 

1651 over: set the page2 content over page1 if True (default) else under 

1652 expand: Whether the page should be expanded to fit the 

1653 dimensions of the page to be merged. 

1654 

1655 """ 

1656 op = Transformation().translate(tx, ty) 

1657 self.merge_transformed_page(page2, op, over, expand) 

1658 

1659 def add_transformation( 

1660 self, 

1661 ctm: Union[Transformation, CompressedTransformationMatrix], 

1662 expand: bool = False, 

1663 ) -> None: 

1664 """ 

1665 Apply a transformation matrix to the page. 

1666 

1667 Args: 

1668 ctm: A 6-element tuple containing the operands of the 

1669 transformation matrix. Alternatively, a 

1670 :py:class:`Transformation<pypdf.Transformation>` 

1671 object can be passed. 

1672 

1673 See :doc:`/user/cropping-and-transforming`. 

1674 

1675 """ 

1676 if isinstance(ctm, Transformation): 

1677 ctm = ctm.ctm 

1678 content = self.get_contents() 

1679 if content is not None: 

1680 content = PageObject._add_transformation_matrix(content, self.pdf, ctm) 

1681 content.isolate_graphics_state() 

1682 self.replace_contents(content) 

1683 # if expanding the page to fit a new page, calculate the new media box size 

1684 if expand: 

1685 corners = [ 

1686 self.mediabox.left.as_numeric(), 

1687 self.mediabox.bottom.as_numeric(), 

1688 self.mediabox.left.as_numeric(), 

1689 self.mediabox.top.as_numeric(), 

1690 self.mediabox.right.as_numeric(), 

1691 self.mediabox.top.as_numeric(), 

1692 self.mediabox.right.as_numeric(), 

1693 self.mediabox.bottom.as_numeric(), 

1694 ] 

1695 

1696 ctm = tuple(float(x) for x in ctm) # type: ignore[assignment] 

1697 new_x = [ 

1698 ctm[0] * corners[i] + ctm[2] * corners[i + 1] + ctm[4] 

1699 for i in range(0, 8, 2) 

1700 ] 

1701 new_y = [ 

1702 ctm[1] * corners[i] + ctm[3] * corners[i + 1] + ctm[5] 

1703 for i in range(0, 8, 2) 

1704 ] 

1705 

1706 self.mediabox.lower_left = (min(new_x), min(new_y)) 

1707 self.mediabox.upper_right = (max(new_x), max(new_y)) 

1708 

1709 def scale(self, sx: float, sy: float) -> None: 

1710 """ 

1711 Scale a page by the given factors by applying a transformation matrix 

1712 to its content and updating the page size. 

1713 

1714 This updates the various page boundaries (bleedbox, trimbox, etc.) 

1715 and the contents of the page. 

1716 

1717 Args: 

1718 sx: The scaling factor on horizontal axis. 

1719 sy: The scaling factor on vertical axis. 

1720 

1721 """ 

1722 self.add_transformation((sx, 0, 0, sy, 0, 0)) 

1723 self.bleedbox = self.bleedbox.scale(sx, sy) 

1724 self.trimbox = self.trimbox.scale(sx, sy) 

1725 self.artbox = self.artbox.scale(sx, sy) 

1726 self.cropbox = self.cropbox.scale(sx, sy) 

1727 self.mediabox = self.mediabox.scale(sx, sy) 

1728 

1729 if PG.ANNOTS in self: 

1730 annotations = self[PG.ANNOTS] 

1731 if isinstance(annotations, ArrayObject): 

1732 for annotation in annotations: 

1733 annotation_obj = annotation.get_object() 

1734 if AnnotationDictionaryAttributes.Rect in annotation_obj: 

1735 rectangle = annotation_obj[AnnotationDictionaryAttributes.Rect] 

1736 if isinstance(rectangle, ArrayObject): 

1737 rectangle[0] = FloatObject(float(rectangle[0]) * sx) 

1738 rectangle[1] = FloatObject(float(rectangle[1]) * sy) 

1739 rectangle[2] = FloatObject(float(rectangle[2]) * sx) 

1740 rectangle[3] = FloatObject(float(rectangle[3]) * sy) 

1741 

1742 if PG.VP in self: 

1743 viewport = self[PG.VP] 

1744 if isinstance(viewport, ArrayObject): 

1745 bbox = viewport[0]["/BBox"] 

1746 else: 

1747 bbox = viewport["/BBox"] # type: ignore[index] 

1748 scaled_bbox = RectangleObject( 

1749 ( 

1750 float(bbox[0]) * sx, 

1751 float(bbox[1]) * sy, 

1752 float(bbox[2]) * sx, 

1753 float(bbox[3]) * sy, 

1754 ) 

1755 ) 

1756 if isinstance(viewport, ArrayObject): 

1757 self[NameObject(PG.VP)][NumberObject(0)][ # type: ignore[index] 

1758 NameObject("/BBox") 

1759 ] = scaled_bbox 

1760 else: 

1761 self[NameObject(PG.VP)][NameObject("/BBox")] = scaled_bbox # type: ignore[index] 

1762 

1763 def scale_by(self, factor: float) -> None: 

1764 """ 

1765 Scale a page by the given factor by applying a transformation matrix to 

1766 its content and updating the page size. 

1767 

1768 Args: 

1769 factor: The scaling factor (for both X and Y axis). 

1770 

1771 """ 

1772 self.scale(factor, factor) 

1773 

1774 def scale_to(self, width: float, height: float) -> None: 

1775 """ 

1776 Scale a page to the specified dimensions by applying a transformation 

1777 matrix to its content and updating the page size. 

1778 

1779 Args: 

1780 width: The new width. 

1781 height: The new height. 

1782 

1783 """ 

1784 sx = width / float(self.mediabox.width) 

1785 sy = height / float(self.mediabox.height) 

1786 self.scale(sx, sy) 

1787 

1788 def compress_content_streams(self, level: int = -1) -> None: 

1789 """ 

1790 Compress the size of this page by joining all content streams and 

1791 applying a FlateDecode filter. 

1792 

1793 However, it is possible that this function will perform no action if 

1794 content stream compression becomes "automatic". 

1795 """ 

1796 content = self.get_contents() 

1797 if content is not None: 

1798 content_obj = content.flate_encode(level) 

1799 try: 

1800 content.indirect_reference.pdf._objects[ # type: ignore[union-attr] 

1801 content.indirect_reference.idnum - 1 # type: ignore[union-attr] 

1802 ] = content_obj 

1803 except AttributeError as exception: 

1804 if self.indirect_reference is not None and hasattr( 

1805 self.indirect_reference.pdf, "_add_object" 

1806 ): 

1807 self.replace_contents(content_obj) 

1808 else: 

1809 raise ValueError("Page must be part of a PdfWriter") from exception 

1810 

1811 @property 

1812 def page_number(self) -> Optional[int]: 

1813 """ 

1814 Read-only property which returns the page number within the PDF file. 

1815 

1816 Returns: 

1817 Page number; None if the page is not attached to a PDF. 

1818 

1819 """ 

1820 if self.indirect_reference is None: 

1821 return None 

1822 # Compare the indirect references, not the pages themselves: two pages 

1823 # with identical contents compare equal, so `list.index` would return 

1824 # the position of the first match for all of them. 

1825 for number, page in enumerate(self.indirect_reference.pdf.pages): 

1826 if page.indirect_reference == self.indirect_reference: 

1827 return number 

1828 return None 

1829 

1830 def _debug_for_extract(self) -> str: # pragma: no cover 

1831 out = "" 

1832 for ope, op in ContentStream( 

1833 self["/Contents"].get_object(), self.pdf, "bytes" 

1834 ).operations: 

1835 if op == b"TJ": 

1836 s = [x for x in ope[0] if isinstance(x, str)] 

1837 else: 

1838 s = [] 

1839 out += op.decode("utf-8") + " " + "".join(s) + ope.__repr__() + "\n" 

1840 out += "\n=============================\n" 

1841 try: 

1842 for fo in self[PG.RESOURCES]["/Font"]: # type:ignore 

1843 out += fo + "\n" 

1844 out += self[PG.RESOURCES]["/Font"][fo].__repr__() + "\n" # type:ignore 

1845 try: 

1846 enc_repr = self[PG.RESOURCES]["/Font"][fo][ # type:ignore 

1847 "/Encoding" 

1848 ].__repr__() 

1849 out += enc_repr + "\n" 

1850 except Exception: 

1851 pass 

1852 try: 

1853 out += ( 

1854 self[PG.RESOURCES]["/Font"][fo][ # type:ignore 

1855 "/ToUnicode" 

1856 ] 

1857 .get_data() 

1858 .decode() 

1859 + "\n" 

1860 ) 

1861 except Exception: 

1862 pass 

1863 

1864 except KeyError: 

1865 out += "No Font\n" 

1866 return out 

1867 

1868 def _extract_text( 

1869 self, 

1870 obj: DictionaryObject, 

1871 pdf: Any, 

1872 orientations: tuple[int, ...] = (0, 90, 180, 270), 

1873 space_width: float = 200.0, 

1874 content_key: Optional[str] = PG.CONTENTS, 

1875 visitor_operand_before: Optional[Callable[[Any, Any, Any, Any], None]] = None, 

1876 visitor_operand_after: Optional[Callable[[Any, Any, Any, Any], None]] = None, 

1877 visitor_text: Optional[Callable[[Any, Any, Any, Any, Any], None]] = None, 

1878 *, 

1879 known_ids: Optional[set[int]] = None, 

1880 traversal_state: Optional[_TraversalState] = None 

1881 ) -> str: 

1882 """ 

1883 See extract_text for most arguments. 

1884 

1885 Args: 

1886 content_key: indicate the default key where to extract data 

1887 None = the object; this allows reusing the function on an XObject 

1888 default = "/Content" 

1889 

1890 """ 

1891 if known_ids is None: 

1892 known_ids = set() 

1893 if traversal_state is None: 

1894 traversal_state = _TraversalState() 

1895 

1896 extractor = TextExtraction() 

1897 font_resources: dict[str, DictionaryObject] = {} 

1898 fonts: dict[str, Font] = {} 

1899 

1900 resources_dict = _get_page_resources(obj) 

1901 if not resources_dict: 

1902 # No resources means no text is possible (no font); we consider the 

1903 # file as not damaged, no need to check for TJ or Tj 

1904 return "" 

1905 

1906 if font_resources_dict := _get_font_resources(resources_dict): 

1907 for font_resource in font_resources_dict: 

1908 try: 

1909 font_resource_object = cast(DictionaryObject, font_resources_dict[font_resource].get_object()) 

1910 font_resources[font_resource] = font_resource_object 

1911 fonts[font_resource] = Font.from_font_resource(font_resource_object) 

1912 # Override space width, if applicable 

1913 if fonts[font_resource].character_widths.get(fonts[font_resource].space_char, 0) == 0: 

1914 fonts[font_resource].space_width = space_width 

1915 except (AttributeError, TypeError): 

1916 pass 

1917 

1918 try: 

1919 content = ( 

1920 obj[content_key].get_object() if isinstance(content_key, str) else obj 

1921 ) 

1922 if not isinstance(content, ContentStream): 

1923 content = ContentStream(content, pdf, "bytes") 

1924 except (AttributeError, KeyError): # no content can be extracted (certainly empty page) 

1925 return "" 

1926 # We check all strings are TextStringObjects. ByteStringObjects 

1927 # are strings where the byte->string encoding was unknown, so adding 

1928 # them to the text here would be gibberish. 

1929 

1930 # Initialize the extractor with the necessary parameters 

1931 extractor.initialize_extraction(orientations, visitor_text, font_resources, fonts) 

1932 

1933 for operands, operator in content.operations: 

1934 if visitor_operand_before is not None: 

1935 visitor_operand_before(operator, operands, extractor.cm_matrix, extractor.tm_matrix) 

1936 # Multiple operators are handled here 

1937 if operator == b"'": 

1938 extractor.process_operation(b"T*", []) 

1939 extractor.process_operation(b"Tj", operands) 

1940 elif operator == b'"' and len(operands) >= 3: 

1941 extractor.process_operation(b"Tw", [operands[0]]) 

1942 extractor.process_operation(b"Tc", [operands[1]]) 

1943 extractor.process_operation(b"T*", []) 

1944 extractor.process_operation(b"Tj", operands[2:]) 

1945 elif operator == b"TJ": 

1946 # The space width may be smaller than the font width, so the width should be 95%. 

1947 _confirm_space_width = extractor._space_width * 0.95 

1948 if operands and isinstance(operands[0], ArrayObject): 

1949 for op in operands[0]: 

1950 # A name is a str too, but not text to show 

1951 if isinstance(op, (str, bytes)) and not isinstance(op, NameObject): 

1952 extractor.process_operation(b"Tj", [op]) 

1953 if isinstance(op, (int, float, NumberObject, FloatObject)) and ( 

1954 abs(float(op)) >= _confirm_space_width 

1955 and extractor.text 

1956 and extractor.text[-1] != " " 

1957 ): 

1958 extractor.process_operation(b"Tj", [" "]) 

1959 elif operator == b"TD" and len(operands) >= 2: 

1960 extractor.process_operation(b"TL", [-operands[1]]) 

1961 extractor.process_operation(b"Td", operands) 

1962 elif operator == b"Do" and operands: 

1963 extractor.output += extractor.text 

1964 if visitor_text is not None: 

1965 visitor_text( 

1966 extractor.text, 

1967 extractor.memo_cm, 

1968 extractor.memo_tm, 

1969 extractor.font_resource, 

1970 extractor.font_size, 

1971 ) 

1972 try: 

1973 if extractor.output[-1] != "\n": 

1974 extractor.output += "\n" 

1975 if visitor_text is not None: 

1976 visitor_text( 

1977 "\n", 

1978 extractor.memo_cm, 

1979 extractor.memo_tm, 

1980 extractor.font_resource, 

1981 extractor.font_size, 

1982 ) 

1983 except IndexError: 

1984 pass 

1985 try: 

1986 xform_text = self._extract_text__xform( 

1987 resources_dict=resources_dict, 

1988 operands=operands, 

1989 orientations=orientations, 

1990 space_width=space_width, 

1991 visitor_operand_before=visitor_operand_before, 

1992 visitor_operand_after=visitor_operand_after, 

1993 visitor_text=visitor_text, 

1994 known_ids=known_ids, 

1995 traversal_state=traversal_state 

1996 ) 

1997 if xform_text is not None: 

1998 text = xform_text 

1999 extractor.output += text 

2000 # Nothing else to do: each text piece inside the form has 

2001 # already been reported via visitor_text while the form was 

2002 # being extracted recursively (issue #4079). 

2003 except Exception as exception: 

2004 logger_warning( 

2005 "Impossible to decode XFormObject %(operand)s: %(exception)s", 

2006 source=__name__, 

2007 operand=operands[0], 

2008 exception=exception, 

2009 ) 

2010 finally: 

2011 extractor.text = "" 

2012 extractor.memo_cm = extractor.cm_matrix.copy() 

2013 extractor.memo_tm = extractor.tm_matrix.copy() 

2014 else: 

2015 extractor.process_operation(operator, operands) 

2016 if visitor_operand_after is not None: 

2017 visitor_operand_after(operator, operands, extractor.cm_matrix, extractor.tm_matrix) 

2018 extractor.output += extractor.text # just in case 

2019 if extractor.text != "" and visitor_text is not None: 

2020 visitor_text( 

2021 extractor.text, 

2022 extractor.memo_cm, 

2023 extractor.memo_tm, 

2024 extractor.font_resource, 

2025 extractor.font_size, 

2026 ) 

2027 return extractor.output 

2028 

2029 def _extract_text__xform( 

2030 self, 

2031 *, 

2032 resources_dict: DictionaryObject, 

2033 operands: Any, 

2034 orientations: tuple[int, ...] = (0, 90, 180, 270), 

2035 space_width: float = 200.0, 

2036 visitor_operand_before: Optional[Callable[[Any, Any, Any, Any], None]] = None, 

2037 visitor_operand_after: Optional[Callable[[Any, Any, Any, Any], None]] = None, 

2038 visitor_text: Optional[Callable[[Any, Any, Any, Any, Any], None]] = None, 

2039 known_ids: set[int], 

2040 traversal_state: _TraversalState 

2041 ) -> Optional[str]: 

2042 xobj = cast(DictionaryObject, resources_dict["/XObject"]) 

2043 xform = cast(EncodedStreamObject, xobj[operands[0]]) 

2044 if xform["/Subtype"] == NameObject("/Image"): 

2045 return None 

2046 

2047 xform_id = id(xform) 

2048 if xform_id in known_ids: 

2049 logger_warning( 

2050 "Detected cyclic form XObject reference, skipping %(operand)s.", 

2051 source=__name__, 

2052 operand=operands[0] 

2053 ) 

2054 return "" 

2055 

2056 configuration = get_configuration() 

2057 if traversal_state.entry_count >= configuration.xform_maximum_invocations_per_extraction: 

2058 if not traversal_state.has_logged: 

2059 traversal_state.has_logged = True 

2060 logger_warning( 

2061 ( 

2062 "Exceeded %(limit)d form XObject invocations while extracting text; " 

2063 "further form content is skipped." 

2064 ), 

2065 source=__name__, 

2066 limit=configuration.xform_maximum_invocations_per_extraction 

2067 ) 

2068 return "" 

2069 

2070 traversal_state.entry_count += 1 

2071 known_ids.add(xform_id) 

2072 try: 

2073 text = self.extract_xform_text( 

2074 xform, 

2075 orientations, 

2076 space_width, 

2077 visitor_operand_before, 

2078 visitor_operand_after, 

2079 visitor_text, 

2080 known_ids=known_ids, 

2081 traversal_state=traversal_state, 

2082 ) 

2083 finally: 

2084 known_ids.discard(xform_id) 

2085 return text 

2086 

2087 def _layout_mode_fonts(self) -> dict[str, Font]: 

2088 """ 

2089 Get fonts formatted for "layout" mode text extraction. 

2090 

2091 Returns: 

2092 Dict[str, Font]: dictionary of Font instances keyed by font name 

2093 

2094 """ 

2095 # Font retrieval logic adapted from pypdf.PageObject._extract_text() 

2096 obj: Any = self 

2097 fonts: dict[str, Font] = {} 

2098 visited: set[int] = set() 

2099 while True: 

2100 obj_id = id(obj) 

2101 if obj_id in visited: 

2102 logger_warning("Detected cycle in /Parent hierarchy when retrieving fonts.", source=__name__) 

2103 break 

2104 visited.add(obj_id) 

2105 

2106 resources_dict: Any = obj.get(PG.RESOURCES, DictionaryObject()).get_object() 

2107 if not isinstance(resources_dict, DictionaryObject): 

2108 logger_warning( 

2109 "Page resources are not a dictionary: %(resources)s", 

2110 source=__name__, 

2111 resources=resources_dict, 

2112 ) 

2113 resources_dict = DictionaryObject() 

2114 if self.pdf is not None and (font_resources := _get_font_resources(resources_dict)): 

2115 for font_name in font_resources: 

2116 font_object = font_resources[font_name].get_object() 

2117 if isinstance(font_object, DictionaryObject): 

2118 fonts[font_name] = Font.from_font_resource(font_object) 

2119 

2120 parent = obj.get("/Parent", NullObject()).get_object() 

2121 if not isinstance(parent, DictionaryObject): 

2122 break 

2123 obj = parent 

2124 

2125 return fonts 

2126 

2127 def _layout_mode_text( 

2128 self, 

2129 space_vertically: bool = True, 

2130 scale_weight: float = 1.25, 

2131 strip_rotated: bool = True, 

2132 debug_path: Optional[Path] = None, 

2133 font_height_weight: float = 1, 

2134 ) -> str: 

2135 """ 

2136 Get text preserving fidelity to source PDF text layout. 

2137 

2138 Args: 

2139 space_vertically: include blank lines inferred from y distance + font 

2140 height. Defaults to True. 

2141 scale_weight: multiplier for string length when calculating weighted 

2142 average character width. Defaults to 1.25. 

2143 strip_rotated: Removes text that is rotated w.r.t. to the page from 

2144 layout mode output. Defaults to True. 

2145 debug_path (Path | None): if supplied, must target a directory. 

2146 creates the following files with debug information for layout mode 

2147 functions if supplied: 

2148 - fonts.json: output of self._layout_mode_fonts 

2149 - tjs.json: individual text render ops with corresponding transform matrices 

2150 - bts.json: text render ops left justified and grouped by BT/ET operators 

2151 - bt_groups.json: BT/ET operations grouped by rendered y-coord (aka lines) 

2152 Defaults to None. 

2153 font_height_weight: multiplier for font height when calculating 

2154 blank lines. Defaults to 1. 

2155 

2156 Returns: 

2157 str: multiline string containing page text in a fixed width format that 

2158 closely adheres to the rendered layout in the source pdf. 

2159 

2160 """ 

2161 fonts = self._layout_mode_fonts() 

2162 if debug_path: # pragma: no cover 

2163 import json # noqa: PLC0415 

2164 

2165 debug_path.joinpath("fonts.json").write_text( 

2166 json.dumps(fonts, indent=2, default=asdict), 

2167 "utf-8" 

2168 ) 

2169 

2170 contents = self.get("/Contents") 

2171 if contents is not None: 

2172 contents = contents.get_object() 

2173 if not isinstance(contents, (ArrayObject, StreamObject)): 

2174 # No content can be extracted (certainly an empty page). 

2175 return "" 

2176 

2177 ops = iter(ContentStream(contents, self.pdf, "bytes").operations) 

2178 bt_groups = _layout_mode.text_show_operations( 

2179 ops, fonts, strip_rotated, debug_path 

2180 ) 

2181 

2182 if not bt_groups: 

2183 return "" 

2184 

2185 ty_groups = _layout_mode.y_coordinate_groups(bt_groups, debug_path) 

2186 

2187 char_width = _layout_mode.fixed_char_width(bt_groups, scale_weight) 

2188 

2189 return _layout_mode.fixed_width_page(ty_groups, char_width, space_vertically, font_height_weight) 

2190 

2191 def extract_text( 

2192 self, 

2193 *args: Any, 

2194 orientations: Union[int, tuple[int, ...]] = (0, 90, 180, 270), 

2195 space_width: float = 200.0, 

2196 visitor_operand_before: Optional[Callable[[Any, Any, Any, Any], None]] = None, 

2197 visitor_operand_after: Optional[Callable[[Any, Any, Any, Any], None]] = None, 

2198 visitor_text: Optional[Callable[[Any, Any, Any, Any, Any], None]] = None, 

2199 extraction_mode: Literal["plain", "layout"] = "plain", 

2200 **kwargs: Any, 

2201 ) -> str: 

2202 """ 

2203 Locate all text drawing commands, in the order they are provided in the 

2204 content stream, and extract the text. 

2205 

2206 This works well for some PDF files, but poorly for others, depending on 

2207 the generator used. This will be refined in the future. 

2208 

2209 Do not rely on the order of text coming out of this function, as it 

2210 will change if this function is made more sophisticated. 

2211 

2212 Arabic and Hebrew are extracted in the correct order. 

2213 If required a custom RTL range of characters can be defined; 

2214 see function set_custom_rtl. 

2215 

2216 Additionally you can provide visitor methods to get informed on all 

2217 operations and all text objects. 

2218 For example in some PDF files this can be useful to parse tables. 

2219 

2220 Args: 

2221 orientations: list of orientations extract_text will look for 

2222 default = (0, 90, 180, 270) 

2223 note: currently only 0 (up),90 (turned left), 180 (upside down), 

2224 270 (turned right) 

2225 Silently ignored in "layout" mode. 

2226 space_width: force default space width 

2227 if not extracted from font (default: 200) 

2228 Silently ignored in "layout" mode. 

2229 visitor_operand_before: function to be called before processing an operation. 

2230 It has four arguments: operator, operand-arguments, 

2231 current transformation matrix and text matrix. 

2232 Ignored with a warning in "layout" mode. 

2233 visitor_operand_after: function to be called after processing an operation. 

2234 It has four arguments: operator, operand-arguments, 

2235 current transformation matrix and text matrix. 

2236 Ignored with a warning in "layout" mode. 

2237 visitor_text: function to be called when extracting some text at some position. 

2238 It has five arguments: text, current transformation matrix, 

2239 text matrix, font-dictionary and font-size. 

2240 The font-dictionary may be None in case of unknown fonts. 

2241 If not None it may e.g. contain key "/BaseFont" with value "/Arial,Bold". 

2242 Ignored with a warning in "layout" mode. 

2243 extraction_mode (Literal["plain", "layout"]): "plain" for legacy functionality, 

2244 "layout" for experimental layout mode functionality. 

2245 NOTE: orientations, space_width, and visitor_* parameters are NOT respected 

2246 in "layout" mode. 

2247 

2248 kwargs: 

2249 layout_mode_space_vertically (bool): include blank lines inferred from 

2250 y distance + font height. Defaults to True. 

2251 layout_mode_scale_weight (float): multiplier for string length when calculating 

2252 weighted average character width. Defaults to 1.25. 

2253 layout_mode_strip_rotated (bool): layout mode does not support rotated text. 

2254 Set to False to include rotated text anyway. If rotated text is discovered, 

2255 layout will be degraded and a warning will result. Defaults to True. 

2256 layout_mode_debug_path (Path | None): if supplied, must target a directory. 

2257 creates the following files with debug information for layout mode 

2258 functions if supplied: 

2259 

2260 - fonts.json: output of self._layout_mode_fonts 

2261 - tjs.json: individual text render ops with corresponding transform matrices 

2262 - bts.json: text render ops left justified and grouped by BT/ET operators 

2263 - bt_groups.json: BT/ET operations grouped by rendered y-coord (aka lines) 

2264 layout_mode_font_height_weight (float): multiplier for font height when calculating 

2265 blank lines. Defaults to 1. 

2266 

2267 Returns: 

2268 The extracted text 

2269 

2270 """ 

2271 if extraction_mode not in ["plain", "layout"]: 

2272 raise ValueError(f"Invalid text extraction mode '{extraction_mode}'") 

2273 if extraction_mode == "layout": 

2274 for visitor in ( 

2275 "visitor_operand_before", 

2276 "visitor_operand_after", 

2277 "visitor_text", 

2278 ): 

2279 if locals()[visitor]: 

2280 logger_warning( 

2281 "Argument %(visitor)s is ignored in layout mode", 

2282 source=__name__, 

2283 visitor=visitor, 

2284 ) 

2285 return self._layout_mode_text( 

2286 space_vertically=kwargs.get("layout_mode_space_vertically", True), 

2287 scale_weight=kwargs.get("layout_mode_scale_weight", 1.25), 

2288 strip_rotated=kwargs.get("layout_mode_strip_rotated", True), 

2289 debug_path=kwargs.get("layout_mode_debug_path"), 

2290 font_height_weight=kwargs.get("layout_mode_font_height_weight", 1) 

2291 ) 

2292 if len(args) >= 1: 

2293 if isinstance(args[0], str): 

2294 if len(args) >= 3: 

2295 if isinstance(args[2], (tuple, int)): 

2296 orientations = args[2] 

2297 else: 

2298 raise TypeError(f"Invalid positional parameter {args[2]}") 

2299 if len(args) >= 4: 

2300 if isinstance(args[3], (float, int)): 

2301 space_width = args[3] 

2302 else: 

2303 raise TypeError(f"Invalid positional parameter {args[3]}") 

2304 elif isinstance(args[0], (tuple, int)): 

2305 orientations = args[0] 

2306 if len(args) >= 2: 

2307 if isinstance(args[1], (float, int)): 

2308 space_width = args[1] 

2309 else: 

2310 raise TypeError(f"Invalid positional parameter {args[1]}") 

2311 else: 

2312 raise TypeError(f"Invalid positional parameter {args[0]}") 

2313 

2314 if isinstance(orientations, int): 

2315 orientations = (orientations,) 

2316 

2317 return self._extract_text( 

2318 self, 

2319 self.pdf, 

2320 orientations, 

2321 space_width, 

2322 PG.CONTENTS, 

2323 visitor_operand_before, 

2324 visitor_operand_after, 

2325 visitor_text, 

2326 ) 

2327 

2328 def extract_xform_text( 

2329 self, 

2330 xform: EncodedStreamObject, 

2331 orientations: tuple[int, ...] = (0, 90, 270, 360), 

2332 space_width: float = 200.0, 

2333 visitor_operand_before: Optional[Callable[[Any, Any, Any, Any], None]] = None, 

2334 visitor_operand_after: Optional[Callable[[Any, Any, Any, Any], None]] = None, 

2335 visitor_text: Optional[Callable[[Any, Any, Any, Any, Any], None]] = None, 

2336 *, 

2337 known_ids: Optional[set[int]] = None, 

2338 traversal_state: Optional[Any] = None 

2339 ) -> str: 

2340 """ 

2341 Extract text from an XObject. 

2342 

2343 Args: 

2344 xform: 

2345 orientations: 

2346 space_width: force default space width (if not extracted from font (default 200) 

2347 visitor_operand_before: 

2348 visitor_operand_after: 

2349 visitor_text: 

2350 known_ids: 

2351 traversal_state: 

2352 

2353 Returns: 

2354 The extracted text 

2355 

2356 """ 

2357 # The type hint would have to use an internal type otherwise, which is not desired. 

2358 assert traversal_state is None or isinstance(traversal_state, _TraversalState) 

2359 return self._extract_text( 

2360 xform, 

2361 self.pdf, 

2362 orientations, 

2363 space_width, 

2364 None, 

2365 visitor_operand_before, 

2366 visitor_operand_after, 

2367 visitor_text, 

2368 known_ids=known_ids, 

2369 traversal_state=traversal_state, 

2370 ) 

2371 

2372 def _get_fonts(self) -> tuple[set[str], set[str]]: 

2373 """ 

2374 Get the names of embedded fonts and unembedded fonts. 

2375 

2376 Returns: 

2377 A tuple (set of embedded fonts, set of unembedded fonts) 

2378 

2379 """ 

2380 obj = self.get_object() 

2381 assert isinstance(obj, DictionaryObject) 

2382 fonts: set[str] = set() 

2383 embedded: set[str] = set() 

2384 fonts, embedded = _get_fonts_walk(obj, fonts, embedded) 

2385 unembedded = fonts - embedded 

2386 return embedded, unembedded 

2387 

2388 mediabox = _create_rectangle_accessor(PG.MEDIABOX, ()) 

2389 """A :class:`RectangleObject<pypdf.generic.RectangleObject>`, expressed in 

2390 default user space units, defining the boundaries of the physical medium on 

2391 which the page is intended to be displayed or printed.""" 

2392 

2393 cropbox = _create_rectangle_accessor("/CropBox", (PG.MEDIABOX,)) 

2394 """ 

2395 A :class:`RectangleObject<pypdf.generic.RectangleObject>`, expressed in 

2396 default user space units, defining the visible region of default user 

2397 space. 

2398 

2399 When the page is displayed or printed, its contents are to be clipped 

2400 (cropped) to this rectangle and then imposed on the output medium in some 

2401 implementation-defined manner. Default value: same as 

2402 :attr:`mediabox<mediabox>`. 

2403 """ 

2404 

2405 bleedbox = _create_rectangle_accessor("/BleedBox", ("/CropBox", PG.MEDIABOX)) 

2406 """A :class:`RectangleObject<pypdf.generic.RectangleObject>`, expressed in 

2407 default user space units, defining the region to which the contents of the 

2408 page should be clipped when output in a production environment.""" 

2409 

2410 trimbox = _create_rectangle_accessor("/TrimBox", ("/CropBox", PG.MEDIABOX)) 

2411 """A :class:`RectangleObject<pypdf.generic.RectangleObject>`, expressed in 

2412 default user space units, defining the intended dimensions of the finished 

2413 page after trimming.""" 

2414 

2415 artbox = _create_rectangle_accessor("/ArtBox", ("/CropBox", PG.MEDIABOX)) 

2416 """A :class:`RectangleObject<pypdf.generic.RectangleObject>`, expressed in 

2417 default user space units, defining the extent of the page's meaningful 

2418 content as intended by the page's creator.""" 

2419 

2420 @property 

2421 def annotations(self) -> Optional[ArrayObject]: 

2422 if "/Annots" not in self: 

2423 return None 

2424 annotations = self["/Annots"].get_object() 

2425 if not isinstance(annotations, ArrayObject): 

2426 logger_warning( 

2427 "Annotations are not an array: %(annotations)s", 

2428 source=__name__, 

2429 annotations=annotations, 

2430 ) 

2431 return None 

2432 return annotations 

2433 

2434 @annotations.setter 

2435 def annotations(self, value: Optional[ArrayObject]) -> None: 

2436 """ 

2437 Set the annotations array of the page. 

2438 

2439 Typically you do not want to set this value, but append to it. 

2440 If you append to it, remember to add the object first to the writer 

2441 and only add the indirect object. 

2442 """ 

2443 if value is None: 

2444 if "/Annots" not in self: 

2445 return 

2446 del self[NameObject("/Annots")] 

2447 else: 

2448 self[NameObject("/Annots")] = value 

2449 

2450 def add_action(self, trigger: PageTrigger, action: Action) -> None: 

2451 """ 

2452 Add an action which will launch on the given trigger event of this page. 

2453 

2454 Args: 

2455 trigger: The action trigger to use. 

2456 action: The action to be done. 

2457 

2458 Example: 

2459 >>> from pypdf import PdfWriter 

2460 >>> from pypdf.actions import JavaScript, PageTrigger 

2461 >>> writer = PdfWriter() 

2462 >>> page = writer.add_blank_page(595, 842) 

2463 >>> # Display the page number when the page is opened 

2464 >>> page.add_action(PageTrigger("open"), JavaScript("app.alert('This is page ' + this.pageNum);")) 

2465 >>> # Display the page number when the page is closed 

2466 >>> page.add_action(PageTrigger("close"), JavaScript("app.alert('This is page ' + this.pageNum);")) 

2467 """ 

2468 return Action._create_new(self, trigger, action) 

2469 

2470 def delete_action(self, trigger: PageTrigger) -> None: 

2471 """ 

2472 Delete all actions associated with an open or close trigger event of this page. 

2473 

2474 Args: 

2475 trigger: An open or close trigger. 

2476 

2477 Example: 

2478 >>> from pypdf import PdfWriter 

2479 >>> from pypdf.actions import JavaScript, PageTrigger 

2480 >>> writer = PdfWriter() 

2481 >>> page = writer.add_blank_page(595, 842) 

2482 >>> page.add_action(PageTrigger("open"), JavaScript("app.alert('This is page ' + this.pageNum);")) 

2483 >>> page.add_action(PageTrigger("close"), JavaScript("app.alert('This is page ' + this.pageNum);")) 

2484 >>> # Delete all actions triggered by a page open 

2485 >>> page.delete_action(PageTrigger("open")) 

2486 >>> # Delete all actions triggered by a page close 

2487 >>> page.delete_action(PageTrigger("close")) 

2488 """ 

2489 return Action._delete(self, trigger) 

2490 

2491 

2492class _VirtualList(Sequence[PageObject]): 

2493 def __init__( 

2494 self, 

2495 length_function: Callable[[], int], 

2496 get_function: Callable[[int], PageObject], 

2497 ) -> None: 

2498 self.length_function = length_function 

2499 self.get_function = get_function 

2500 self.current = -1 

2501 

2502 def __len__(self) -> int: 

2503 return self.length_function() 

2504 

2505 @overload 

2506 def __getitem__(self, index: int) -> PageObject: 

2507 ... 

2508 

2509 @overload 

2510 def __getitem__(self, index: slice) -> Sequence[PageObject]: 

2511 ... 

2512 

2513 def __getitem__( 

2514 self, index: Union[int, slice] 

2515 ) -> Union[PageObject, Sequence[PageObject]]: 

2516 if isinstance(index, slice): 

2517 indices = range(*index.indices(len(self))) 

2518 cls = type(self) 

2519 return cls(indices.__len__, lambda idx: self[indices[idx]]) 

2520 if not isinstance(index, int): 

2521 raise TypeError("Sequence indices must be integers") 

2522 len_self = len(self) 

2523 if index < 0: 

2524 # support negative indexes 

2525 index += len_self 

2526 if not (0 <= index < len_self): 

2527 raise IndexError("Sequence index out of range") 

2528 return self.get_function(index) 

2529 

2530 def __delitem__(self, index: Union[int, slice]) -> None: 

2531 if isinstance(index, slice): 

2532 r = list(range(*index.indices(len(self)))) 

2533 # pages have to be deleted from last to first 

2534 r.sort() 

2535 r.reverse() 

2536 for p in r: 

2537 del self[p] # recursive call 

2538 return 

2539 if not isinstance(index, int): 

2540 raise TypeError("Index must be integers") 

2541 len_self = len(self) 

2542 if index < 0: 

2543 # support negative indexes 

2544 index += len_self 

2545 if not (0 <= index < len_self): 

2546 raise IndexError("Index out of range") 

2547 ind = self[index].indirect_reference 

2548 assert ind is not None 

2549 parent: Optional[PdfObject] = cast(DictionaryObject, ind.get_object()).get( 

2550 "/Parent", None 

2551 ) 

2552 first = True 

2553 while parent is not None: 

2554 parent = cast(DictionaryObject, parent.get_object()) 

2555 try: 

2556 i = cast(ArrayObject, parent["/Kids"]).index(ind) 

2557 del cast(ArrayObject, parent["/Kids"])[i] 

2558 first = False 

2559 try: 

2560 assert ind is not None 

2561 del ind.pdf.flattened_pages[index] # case of page in a Reader 

2562 except Exception: # pragma: no cover 

2563 pass 

2564 if "/Count" in parent: 

2565 parent[NameObject("/Count")] = NumberObject( 

2566 cast(int, parent["/Count"]) - 1 

2567 ) 

2568 if len(cast(ArrayObject, parent["/Kids"])) == 0: 

2569 # No more objects in this part of this subtree 

2570 ind = parent.indirect_reference 

2571 parent = parent.get("/Parent", None) 

2572 except ValueError as exception: # from index 

2573 if first: 

2574 raise PdfReadError(f"Page not found in page tree: {ind}") from exception 

2575 break 

2576 

2577 def __iter__(self) -> Iterator[PageObject]: 

2578 for i in range(len(self)): 

2579 yield self[i] 

2580 

2581 def __str__(self) -> str: 

2582 p = [f"PageObject({i})" for i in range(self.length_function())] 

2583 return f"[{', '.join(p)}]" 

2584 

2585 

2586def _get_fonts_walk( 

2587 obj: DictionaryObject, 

2588 fnt: set[str], 

2589 emb: set[str], 

2590) -> tuple[set[str], set[str]]: 

2591 """ 

2592 Get the set of all fonts and all embedded fonts. 

2593 

2594 Args: 

2595 obj: Page resources dictionary 

2596 fnt: font 

2597 emb: embedded fonts 

2598 

2599 Returns: 

2600 A tuple (fnt, emb) 

2601 

2602 If there is a key called 'BaseFont', that is a font that is used in the document. 

2603 If there is a key called 'FontName' and another key in the same dictionary object 

2604 that is called 'FontFilex' (where x is null, 2, or 3), then that fontname is 

2605 embedded. 

2606 

2607 We create and add to two sets, fnt = fonts used and emb = fonts embedded. 

2608 

2609 """ 

2610 fontkeys = ("/FontFile", "/FontFile2", "/FontFile3") 

2611 

2612 def process_font(f: PdfObject) -> None: 

2613 nonlocal fnt, emb 

2614 f = cast(DictionaryObject, f.get_object()) # to be sure 

2615 if "/BaseFont" in f: 

2616 fnt.add(cast(str, f["/BaseFont"])) 

2617 

2618 if ( 

2619 ("/CharProcs" in f) 

2620 or ( 

2621 "/FontDescriptor" in f 

2622 and any( 

2623 x in cast(DictionaryObject, f["/FontDescriptor"]) for x in fontkeys 

2624 ) 

2625 ) 

2626 or ( 

2627 "/DescendantFonts" in f 

2628 and "/FontDescriptor" 

2629 in cast( 

2630 DictionaryObject, 

2631 cast(ArrayObject, f["/DescendantFonts"])[0].get_object(), 

2632 ) 

2633 and any( 

2634 x 

2635 in cast( 

2636 DictionaryObject, 

2637 cast( 

2638 DictionaryObject, 

2639 cast(ArrayObject, f["/DescendantFonts"])[0].get_object(), 

2640 )["/FontDescriptor"], 

2641 ) 

2642 for x in fontkeys 

2643 ) 

2644 ) 

2645 ): 

2646 # the list comprehension ensures there is FontFile 

2647 try: 

2648 emb.add(cast(str, f["/BaseFont"])) 

2649 except KeyError: 

2650 emb.add("(" + cast(str, f["/Subtype"]) + ")") 

2651 

2652 if "/DR" in obj and "/Font" in cast(DictionaryObject, obj["/DR"]): 

2653 for f in cast( 

2654 DictionaryObject, cast(DictionaryObject, obj["/DR"])["/Font"] 

2655 ).values(): 

2656 process_font(f) 

2657 if "/Resources" in obj: 

2658 if "/Font" in cast(DictionaryObject, obj["/Resources"]): 

2659 for f in cast( 

2660 DictionaryObject, cast(DictionaryObject, obj["/Resources"])["/Font"] 

2661 ).values(): 

2662 process_font(f) 

2663 if "/XObject" in cast(DictionaryObject, obj["/Resources"]): 

2664 for x in cast( 

2665 DictionaryObject, cast(DictionaryObject, obj["/Resources"])["/XObject"] 

2666 ).values(): 

2667 _get_fonts_walk(cast(DictionaryObject, x.get_object()), fnt, emb) 

2668 if "/Annots" in obj: 

2669 for a in cast(ArrayObject, obj["/Annots"]): 

2670 _get_fonts_walk(cast(DictionaryObject, a.get_object()), fnt, emb) 

2671 if "/AP" in obj: 

2672 if ( 

2673 cast(DictionaryObject, cast(DictionaryObject, obj["/AP"])["/N"]).get( 

2674 "/Type" 

2675 ) 

2676 == "/XObject" 

2677 ): 

2678 _get_fonts_walk( 

2679 cast(DictionaryObject, cast(DictionaryObject, obj["/AP"])["/N"]), 

2680 fnt, 

2681 emb, 

2682 ) 

2683 else: 

2684 for a in cast(DictionaryObject, cast(DictionaryObject, obj["/AP"])["/N"]): 

2685 _get_fonts_walk(cast(DictionaryObject, a), fnt, emb) 

2686 return fnt, emb # return the sets for each page