_page.py 88 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354
  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. import math
  30. from collections.abc import Iterable, Iterator, Sequence
  31. from copy import deepcopy
  32. from dataclasses import asdict, dataclass
  33. from decimal import Decimal
  34. from io import BytesIO
  35. from pathlib import Path
  36. from typing import (
  37. Any,
  38. Callable,
  39. Literal,
  40. Optional,
  41. Union,
  42. cast,
  43. overload,
  44. )
  45. from ._font import Font
  46. from ._protocols import PdfCommonDocProtocol
  47. from ._text_extraction import (
  48. _layout_mode,
  49. )
  50. from ._text_extraction._text_extractor import TextExtraction
  51. from ._utils import (
  52. CompressedTransformationMatrix,
  53. TransformationMatrixType,
  54. _human_readable_bytes,
  55. deprecate,
  56. logger_warning,
  57. matrix_multiply,
  58. )
  59. from .constants import _INLINE_IMAGE_KEY_MAPPING, _INLINE_IMAGE_VALUE_MAPPING
  60. from .constants import AnnotationDictionaryAttributes as ADA
  61. from .constants import ImageAttributes as IA
  62. from .constants import PageAttributes as PG
  63. from .constants import Resources as RES
  64. from .errors import PageSizeNotDefinedError, PdfReadError
  65. from .generic import (
  66. ArrayObject,
  67. ContentStream,
  68. DictionaryObject,
  69. EncodedStreamObject,
  70. FloatObject,
  71. IndirectObject,
  72. NameObject,
  73. NullObject,
  74. NumberObject,
  75. PdfObject,
  76. RectangleObject,
  77. StreamObject,
  78. is_null_or_none,
  79. )
  80. try:
  81. from PIL.Image import Image
  82. pil_not_imported = False
  83. except ImportError:
  84. Image = object # type: ignore[assignment,misc,unused-ignore] # TODO: Remove unused-ignore on Python 3.10
  85. pil_not_imported = True # error will be raised only when using images
  86. MERGE_CROP_BOX = "cropbox" # pypdf <= 3.4.0 used "trimbox"
  87. def _get_rectangle(self: Any, name: str, defaults: Iterable[str]) -> RectangleObject:
  88. retval: Union[None, RectangleObject, ArrayObject, IndirectObject] = self.get(name)
  89. if isinstance(retval, RectangleObject):
  90. return retval
  91. if is_null_or_none(retval):
  92. for d in defaults:
  93. retval = self.get(d)
  94. if retval is not None:
  95. break
  96. if isinstance(retval, IndirectObject):
  97. retval = self.pdf.get_object(retval)
  98. if isinstance(retval, ArrayObject) and (length := len(retval)) > 4:
  99. logger_warning(f"Expected four values, got {length}: {retval}", __name__)
  100. retval = RectangleObject(tuple(retval[:4]))
  101. else:
  102. retval = RectangleObject(retval) # type: ignore
  103. _set_rectangle(self, name, retval)
  104. return retval
  105. def _set_rectangle(self: Any, name: str, value: Union[RectangleObject, float]) -> None:
  106. self[NameObject(name)] = value
  107. def _delete_rectangle(self: Any, name: str) -> None:
  108. del self[name]
  109. def _create_rectangle_accessor(name: str, fallback: Iterable[str]) -> property:
  110. return property(
  111. lambda self: _get_rectangle(self, name, fallback),
  112. lambda self, value: _set_rectangle(self, name, value),
  113. lambda self: _delete_rectangle(self, name),
  114. )
  115. class Transformation:
  116. """
  117. Represent a 2D transformation.
  118. The transformation between two coordinate systems is represented by a 3-by-3
  119. transformation matrix with the following form::
  120. a b 0
  121. c d 0
  122. e f 1
  123. Because a transformation matrix has only six elements that can be changed,
  124. it is usually specified in PDF as the six-element array [ a b c d e f ].
  125. Coordinate transformations are expressed as matrix multiplications::
  126. a b 0
  127. [ x′ y′ 1 ] = [ x y 1 ] × c d 0
  128. e f 1
  129. Example:
  130. >>> from pypdf import PdfWriter, Transformation
  131. >>> page = PdfWriter().add_blank_page(800, 600)
  132. >>> op = Transformation().scale(sx=2, sy=3).translate(tx=10, ty=20)
  133. >>> page.add_transformation(op)
  134. """
  135. def __init__(self, ctm: CompressedTransformationMatrix = (1, 0, 0, 1, 0, 0)) -> None:
  136. self.ctm = ctm
  137. @property
  138. def matrix(self) -> TransformationMatrixType:
  139. """
  140. Return the transformation matrix as a tuple of tuples in the form:
  141. ((a, b, 0), (c, d, 0), (e, f, 1))
  142. """
  143. return (
  144. (self.ctm[0], self.ctm[1], 0),
  145. (self.ctm[2], self.ctm[3], 0),
  146. (self.ctm[4], self.ctm[5], 1),
  147. )
  148. @staticmethod
  149. def compress(matrix: TransformationMatrixType) -> CompressedTransformationMatrix:
  150. """
  151. Compresses the transformation matrix into a tuple of (a, b, c, d, e, f).
  152. Args:
  153. matrix: The transformation matrix as a tuple of tuples.
  154. Returns:
  155. A tuple representing the transformation matrix as (a, b, c, d, e, f)
  156. """
  157. return (
  158. matrix[0][0],
  159. matrix[0][1],
  160. matrix[1][0],
  161. matrix[1][1],
  162. matrix[2][0],
  163. matrix[2][1],
  164. )
  165. def _to_cm(self) -> str:
  166. # Returns the cm operation string for the given transformation matrix
  167. return (
  168. f"{self.ctm[0]:.4f} {self.ctm[1]:.4f} {self.ctm[2]:.4f} "
  169. f"{self.ctm[3]:.4f} {self.ctm[4]:.4f} {self.ctm[5]:.4f} cm"
  170. )
  171. def transform(self, m: "Transformation") -> "Transformation":
  172. """
  173. Apply one transformation to another.
  174. Args:
  175. m: a Transformation to apply.
  176. Returns:
  177. A new ``Transformation`` instance
  178. Example:
  179. >>> from pypdf import PdfWriter, Transformation
  180. >>> height, width = 40, 50
  181. >>> page = PdfWriter().add_blank_page(800, 600)
  182. >>> op = Transformation((1, 0, 0, -1, 0, height)) # vertical mirror
  183. >>> op = Transformation().transform(Transformation((-1, 0, 0, 1, width, 0))) # horizontal mirror
  184. >>> page.add_transformation(op)
  185. """
  186. ctm = Transformation.compress(matrix_multiply(self.matrix, m.matrix))
  187. return Transformation(ctm)
  188. def translate(self, tx: float = 0, ty: float = 0) -> "Transformation":
  189. """
  190. Translate the contents of a page.
  191. Args:
  192. tx: The translation along the x-axis.
  193. ty: The translation along the y-axis.
  194. Returns:
  195. A new ``Transformation`` instance
  196. """
  197. m = self.ctm
  198. return Transformation(ctm=(m[0], m[1], m[2], m[3], m[4] + tx, m[5] + ty))
  199. def scale(
  200. self, sx: Optional[float] = None, sy: Optional[float] = None
  201. ) -> "Transformation":
  202. """
  203. Scale the contents of a page towards the origin of the coordinate system.
  204. Typically, that is the lower-left corner of the page. That can be
  205. changed by translating the contents / the page boxes.
  206. Args:
  207. sx: The scale factor along the x-axis.
  208. sy: The scale factor along the y-axis.
  209. Returns:
  210. A new Transformation instance with the scaled matrix.
  211. """
  212. if sx is None and sy is None:
  213. raise ValueError("Either sx or sy must be specified")
  214. if sx is None:
  215. sx = sy
  216. if sy is None:
  217. sy = sx
  218. assert sx is not None
  219. assert sy is not None
  220. op: TransformationMatrixType = ((sx, 0, 0), (0, sy, 0), (0, 0, 1))
  221. ctm = Transformation.compress(matrix_multiply(self.matrix, op))
  222. return Transformation(ctm)
  223. def rotate(self, rotation: float) -> "Transformation":
  224. """
  225. Rotate the contents of a page.
  226. Args:
  227. rotation: The angle of rotation in degrees.
  228. Returns:
  229. A new ``Transformation`` instance with the rotated matrix.
  230. """
  231. rotation = math.radians(rotation)
  232. op: TransformationMatrixType = (
  233. (math.cos(rotation), math.sin(rotation), 0),
  234. (-math.sin(rotation), math.cos(rotation), 0),
  235. (0, 0, 1),
  236. )
  237. ctm = Transformation.compress(matrix_multiply(self.matrix, op))
  238. return Transformation(ctm)
  239. def __repr__(self) -> str:
  240. return f"Transformation(ctm={self.ctm})"
  241. @overload
  242. def apply_on(self, pt: list[float], as_object: bool = False) -> list[float]:
  243. ...
  244. @overload
  245. def apply_on(
  246. self, pt: tuple[float, float], as_object: bool = False
  247. ) -> tuple[float, float]:
  248. ...
  249. def apply_on(
  250. self,
  251. pt: Union[tuple[float, float], list[float]],
  252. as_object: bool = False,
  253. ) -> Union[tuple[float, float], list[float]]:
  254. """
  255. Apply the transformation matrix on the given point.
  256. Args:
  257. pt: A tuple or list representing the point in the form (x, y).
  258. as_object: If True, return items as FloatObject, otherwise as plain floats.
  259. Returns:
  260. A tuple or list representing the transformed point in the form (x', y')
  261. """
  262. typ = FloatObject if as_object else float
  263. pt1 = (
  264. typ(float(pt[0]) * self.ctm[0] + float(pt[1]) * self.ctm[2] + self.ctm[4]),
  265. typ(float(pt[0]) * self.ctm[1] + float(pt[1]) * self.ctm[3] + self.ctm[5]),
  266. )
  267. return list(pt1) if isinstance(pt, list) else pt1
  268. @dataclass
  269. class ImageFile:
  270. """
  271. Image within the PDF file. *This object is not designed to be built.*
  272. This object should not be modified except using :func:`ImageFile.replace` to replace the image with a new one.
  273. """
  274. name: str = ""
  275. """
  276. Filename as identified within the PDF file.
  277. """
  278. data: bytes = b""
  279. """
  280. Data as bytes.
  281. """
  282. image: Optional[Image] = None
  283. """
  284. Data as PIL image.
  285. """
  286. indirect_reference: Optional[IndirectObject] = None
  287. """
  288. Reference to the object storing the stream.
  289. """
  290. def replace(self, new_image: Image, **kwargs: Any) -> None:
  291. """
  292. Replace the image with a new PIL image.
  293. Args:
  294. new_image (PIL.Image.Image): The new PIL image to replace the existing image.
  295. **kwargs: Additional keyword arguments to pass to `Image.save()`.
  296. Raises:
  297. TypeError: If the image is inline or in a PdfReader.
  298. TypeError: If the image does not belong to a PdfWriter.
  299. TypeError: If `new_image` is not a PIL Image.
  300. Note:
  301. This method replaces the existing image with a new image.
  302. It is not allowed for inline images or images within a PdfReader.
  303. The `kwargs` parameter allows passing additional parameters
  304. to `Image.save()`, such as quality.
  305. """
  306. if pil_not_imported:
  307. raise ImportError(
  308. "pillow is required to do image extraction. "
  309. "It can be installed via 'pip install pypdf[image]'"
  310. )
  311. from ._reader import PdfReader # noqa: PLC0415
  312. from .generic import DictionaryObject, PdfObject # noqa: PLC0415
  313. from .generic._image_xobject import _xobj_to_image # noqa: PLC0415
  314. if self.indirect_reference is None:
  315. raise TypeError("Cannot update an inline image.")
  316. if not hasattr(self.indirect_reference.pdf, "_id_translated"):
  317. raise TypeError("Cannot update an image not belonging to a PdfWriter.")
  318. if not isinstance(new_image, Image):
  319. raise TypeError("new_image shall be a PIL Image")
  320. b = BytesIO()
  321. new_image.save(b, "PDF", **kwargs)
  322. reader = PdfReader(b)
  323. page_image = reader.pages[0].images[0]
  324. assert page_image.indirect_reference is not None
  325. self.indirect_reference.pdf._objects[self.indirect_reference.idnum - 1] = (
  326. page_image.indirect_reference.get_object()
  327. )
  328. cast(
  329. PdfObject, self.indirect_reference.get_object()
  330. ).indirect_reference = self.indirect_reference
  331. # change the object attributes
  332. extension, byte_stream, img = _xobj_to_image(
  333. cast(DictionaryObject, self.indirect_reference.get_object()),
  334. pillow_parameters=kwargs,
  335. )
  336. assert extension is not None
  337. self.name = self.name[: self.name.rfind(".")] + extension
  338. self.data = byte_stream
  339. self.image = img
  340. def __str__(self) -> str:
  341. return f"{self.__class__.__name__}(name={self.name}, data: {_human_readable_bytes(len(self.data))})"
  342. def __repr__(self) -> str:
  343. return self.__str__()[:-1] + f", hash: {hash(self.data)})"
  344. class VirtualListImages(Sequence[ImageFile]):
  345. """
  346. Provides access to images referenced within a page.
  347. Only one copy will be returned if the usage is used on the same page multiple times.
  348. See :func:`PageObject.images` for more details.
  349. """
  350. def __init__(
  351. self,
  352. ids_function: Callable[[], list[Union[str, list[str]]]],
  353. get_function: Callable[[Union[str, list[str], tuple[str]]], ImageFile],
  354. ) -> None:
  355. self.ids_function = ids_function
  356. self.get_function = get_function
  357. self.current = -1
  358. def __len__(self) -> int:
  359. return len(self.ids_function())
  360. def keys(self) -> list[Union[str, list[str]]]:
  361. return self.ids_function()
  362. def items(self) -> list[tuple[Union[str, list[str]], ImageFile]]:
  363. return [(x, self[x]) for x in self.ids_function()]
  364. @overload
  365. def __getitem__(self, index: Union[int, str, list[str]]) -> ImageFile:
  366. ...
  367. @overload
  368. def __getitem__(self, index: slice) -> Sequence[ImageFile]:
  369. ...
  370. def __getitem__(
  371. self, index: Union[int, slice, str, list[str], tuple[str]]
  372. ) -> Union[ImageFile, Sequence[ImageFile]]:
  373. lst = self.ids_function()
  374. if isinstance(index, slice):
  375. indices = range(*index.indices(len(self)))
  376. lst = [lst[x] for x in indices]
  377. cls = type(self)
  378. return cls((lambda: lst), self.get_function)
  379. if isinstance(index, (str, list, tuple)):
  380. return self.get_function(index)
  381. if not isinstance(index, int):
  382. raise TypeError("Invalid sequence indices type")
  383. len_self = len(lst)
  384. if index < 0:
  385. # support negative indexes
  386. index += len_self
  387. if not (0 <= index < len_self):
  388. raise IndexError("Sequence index out of range")
  389. return self.get_function(lst[index])
  390. def __iter__(self) -> Iterator[ImageFile]:
  391. for i in range(len(self)):
  392. yield self[i]
  393. def __str__(self) -> str:
  394. p = [f"Image_{i}={n}" for i, n in enumerate(self.ids_function())]
  395. return f"[{', '.join(p)}]"
  396. class PageObject(DictionaryObject):
  397. """
  398. PageObject represents a single page within a PDF file.
  399. Typically these objects will be created by accessing the
  400. :attr:`pages<pypdf.PdfReader.pages>` property of the
  401. :class:`PdfReader<pypdf.PdfReader>` class, but it is
  402. also possible to create an empty page with the
  403. :meth:`create_blank_page()<pypdf._page.PageObject.create_blank_page>` static method.
  404. Args:
  405. pdf: PDF file the page belongs to.
  406. indirect_reference: Stores the original indirect reference to
  407. this object in its source PDF
  408. """
  409. original_page: "PageObject" # very local use in writer when appending
  410. def __init__(
  411. self,
  412. pdf: Optional[PdfCommonDocProtocol] = None,
  413. indirect_reference: Optional[IndirectObject] = None,
  414. ) -> None:
  415. DictionaryObject.__init__(self)
  416. self.pdf = pdf
  417. self.inline_images: Optional[dict[str, ImageFile]] = None
  418. self.indirect_reference = indirect_reference
  419. if not is_null_or_none(indirect_reference):
  420. assert indirect_reference is not None, "mypy"
  421. self.update(cast(DictionaryObject, indirect_reference.get_object()))
  422. self._font_width_maps: dict[str, tuple[dict[str, float], str, float]] = {}
  423. def hash_bin(self) -> int:
  424. """
  425. Used to detect modified object.
  426. Note: this function is overloaded to return the same results
  427. as a DictionaryObject.
  428. Returns:
  429. Hash considering type and value.
  430. """
  431. return hash(
  432. (DictionaryObject, tuple(((k, v.hash_bin()) for k, v in self.items())))
  433. )
  434. def hash_value_data(self) -> bytes:
  435. data = super().hash_value_data()
  436. data += f"{id(self)}".encode()
  437. return data
  438. @property
  439. def user_unit(self) -> float:
  440. """
  441. A read-only positive number giving the size of user space units.
  442. It is in multiples of 1/72 inch. Hence a value of 1 means a user
  443. space unit is 1/72 inch, and a value of 3 means that a user
  444. space unit is 3/72 inch.
  445. """
  446. return self.get(PG.USER_UNIT, 1)
  447. @staticmethod
  448. def create_blank_page(
  449. pdf: Optional[PdfCommonDocProtocol] = None,
  450. width: Union[float, Decimal, None] = None,
  451. height: Union[float, Decimal, None] = None,
  452. ) -> "PageObject":
  453. """
  454. Return a new blank page.
  455. If ``width`` or ``height`` is ``None``, try to get the page size
  456. from the last page of *pdf*.
  457. Args:
  458. pdf: PDF file the page is within.
  459. width: The width of the new page expressed in default user
  460. space units.
  461. height: The height of the new page expressed in default user
  462. space units.
  463. Returns:
  464. The new blank page
  465. Raises:
  466. PageSizeNotDefinedError: if ``pdf`` is ``None`` or contains
  467. no page
  468. """
  469. page = PageObject(pdf)
  470. # Creates a new page (cf PDF Reference §7.7.3.3)
  471. page.__setitem__(NameObject(PG.TYPE), NameObject("/Page"))
  472. page.__setitem__(NameObject(PG.PARENT), NullObject())
  473. page.__setitem__(NameObject(PG.RESOURCES), DictionaryObject())
  474. if width is None or height is None:
  475. if pdf is not None and len(pdf.pages) > 0:
  476. lastpage = pdf.pages[len(pdf.pages) - 1]
  477. width = lastpage.mediabox.width
  478. height = lastpage.mediabox.height
  479. else:
  480. raise PageSizeNotDefinedError
  481. page.__setitem__(
  482. NameObject(PG.MEDIABOX), RectangleObject((0, 0, width, height)) # type: ignore
  483. )
  484. return page
  485. def _get_ids_image(
  486. self,
  487. obj: Optional[DictionaryObject] = None,
  488. ancest: Optional[list[str]] = None,
  489. call_stack: Optional[list[Any]] = None,
  490. ) -> list[Union[str, list[str]]]:
  491. if call_stack is None:
  492. call_stack = []
  493. _i = getattr(obj, "indirect_reference", None)
  494. if _i in call_stack:
  495. return []
  496. call_stack.append(_i)
  497. if self.inline_images is None:
  498. self.inline_images = self._get_inline_images()
  499. if obj is None:
  500. obj = self
  501. if ancest is None:
  502. ancest = []
  503. lst: list[Union[str, list[str]]] = []
  504. if (
  505. PG.RESOURCES not in obj or
  506. is_null_or_none(resources := obj[PG.RESOURCES]) or
  507. RES.XOBJECT not in cast(DictionaryObject, resources)
  508. ):
  509. return [] if self.inline_images is None else list(self.inline_images.keys())
  510. x_object = resources[RES.XOBJECT].get_object() # type: ignore
  511. for o in x_object:
  512. if not isinstance(x_object[o], StreamObject):
  513. continue
  514. if x_object[o][IA.SUBTYPE] == "/Image":
  515. lst.append(o if len(ancest) == 0 else [*ancest, o])
  516. else: # is a form with possible images inside
  517. lst.extend(self._get_ids_image(x_object[o], [*ancest, o], call_stack))
  518. assert self.inline_images is not None
  519. lst.extend(list(self.inline_images.keys()))
  520. return lst
  521. def _get_image(
  522. self,
  523. id: Union[str, list[str], tuple[str]],
  524. obj: Optional[DictionaryObject] = None,
  525. ) -> ImageFile:
  526. if obj is None:
  527. obj = cast(DictionaryObject, self)
  528. if isinstance(id, tuple):
  529. id = list(id)
  530. if isinstance(id, list) and len(id) == 1:
  531. id = id[0]
  532. xobjs: Optional[DictionaryObject] = None
  533. try:
  534. xobjs = cast(
  535. DictionaryObject, cast(DictionaryObject, obj[PG.RESOURCES])[RES.XOBJECT]
  536. )
  537. except KeyError as exc:
  538. if not (id[0] == "~" and id[-1] == "~"):
  539. raise KeyError(
  540. f"Cannot access image object {id} without XObject resources"
  541. ) from exc
  542. if isinstance(id, str):
  543. if id[0] == "~" and id[-1] == "~":
  544. if self.inline_images is None:
  545. self.inline_images = self._get_inline_images()
  546. if self.inline_images is None:
  547. raise KeyError("No inline image can be found")
  548. return self.inline_images[id]
  549. assert xobjs is not None
  550. from .generic._image_xobject import _xobj_to_image # noqa: PLC0415
  551. imgd = _xobj_to_image(cast(DictionaryObject, xobjs[id]))
  552. extension, byte_stream = imgd[:2]
  553. return ImageFile(
  554. name=f"{id[1:]}{extension}",
  555. data=byte_stream,
  556. image=imgd[2],
  557. indirect_reference=xobjs[id].indirect_reference,
  558. )
  559. # in a subobject
  560. assert xobjs is not None
  561. ids = id[1:]
  562. return self._get_image(ids, cast(DictionaryObject, xobjs[id[0]]))
  563. @property
  564. def images(self) -> VirtualListImages:
  565. """
  566. Read-only property emulating a list of images on a page.
  567. Get a list of all images on the page. The key can be:
  568. - A string (for the top object)
  569. - A tuple (for images within XObject forms)
  570. - An integer
  571. Examples:
  572. * `reader.pages[0].images[0]` # return first image
  573. * `reader.pages[0].images['/I0']` # return image '/I0'
  574. * `reader.pages[0].images['/TP1','/Image1']` # return image '/Image1' within '/TP1' XObject form
  575. * `for img in reader.pages[0].images:` # loops through all objects
  576. images.keys() and images.items() can be used.
  577. The ImageFile has the following properties:
  578. * `.name` : name of the object
  579. * `.data` : bytes of the object
  580. * `.image` : PIL Image Object
  581. * `.indirect_reference` : object reference
  582. and the following methods:
  583. `.replace(new_image: PIL.Image.Image, **kwargs)` :
  584. replace the image in the pdf with the new image
  585. applying the saving parameters indicated (such as quality)
  586. Example usage:
  587. reader.pages[0].images[0].replace(Image.open("new_image.jpg"), quality=20)
  588. Inline images are extracted and named ~0~, ~1~, ..., with the
  589. indirect_reference set to None.
  590. """
  591. return VirtualListImages(self._get_ids_image, self._get_image)
  592. def _translate_value_inline_image(self, k: str, v: PdfObject) -> PdfObject:
  593. """Translate values used in inline image"""
  594. try:
  595. v = NameObject(_INLINE_IMAGE_VALUE_MAPPING[cast(str, v)])
  596. except (TypeError, KeyError):
  597. if isinstance(v, NameObject):
  598. # It is a custom name, thus we have to look in resources.
  599. # The only applicable case is for ColorSpace.
  600. try:
  601. res = cast(DictionaryObject, self["/Resources"])["/ColorSpace"]
  602. v = cast(DictionaryObject, res)[v]
  603. except KeyError: # for res and v
  604. raise PdfReadError(f"Cannot find resource entry {v} for {k}")
  605. return v
  606. def _get_inline_images(self) -> dict[str, ImageFile]:
  607. """Load inline images. Entries will be identified as `~1~`."""
  608. content = self.get_contents()
  609. if is_null_or_none(content):
  610. return {}
  611. imgs_data = []
  612. assert content is not None, "mypy"
  613. for param, ope in content.operations:
  614. if ope == b"INLINE IMAGE":
  615. imgs_data.append(
  616. {"settings": param["settings"], "__streamdata__": param["data"]}
  617. )
  618. elif ope in (b"BI", b"EI", b"ID"): # pragma: no cover
  619. raise PdfReadError(
  620. f"{ope!r} operator met whereas not expected, "
  621. "please share use case with pypdf dev team"
  622. )
  623. files = {}
  624. for num, ii in enumerate(imgs_data):
  625. init = {
  626. "__streamdata__": ii["__streamdata__"],
  627. "/Length": len(ii["__streamdata__"]),
  628. }
  629. for k, v in ii["settings"].items():
  630. if k in {"/Length", "/L"}: # no length is expected
  631. continue
  632. if isinstance(v, list):
  633. v = ArrayObject(
  634. [self._translate_value_inline_image(k, x) for x in v]
  635. )
  636. else:
  637. v = self._translate_value_inline_image(k, v)
  638. k = NameObject(_INLINE_IMAGE_KEY_MAPPING[k])
  639. if k not in init:
  640. init[k] = v
  641. ii["object"] = EncodedStreamObject.initialize_from_dictionary(init)
  642. from .generic._image_xobject import _xobj_to_image # noqa: PLC0415
  643. extension, byte_stream, img = _xobj_to_image(ii["object"])
  644. files[f"~{num}~"] = ImageFile(
  645. name=f"~{num}~{extension}",
  646. data=byte_stream,
  647. image=img,
  648. indirect_reference=None,
  649. )
  650. return files
  651. @property
  652. def rotation(self) -> int:
  653. """
  654. The visual rotation of the page.
  655. This number has to be a multiple of 90 degrees: 0, 90, 180, or 270 are
  656. valid values. This property does not affect ``/Contents``.
  657. """
  658. rotate_obj = self.get(PG.ROTATE, 0)
  659. return rotate_obj if isinstance(rotate_obj, int) else rotate_obj.get_object()
  660. @rotation.setter
  661. def rotation(self, r: float) -> None:
  662. self[NameObject(PG.ROTATE)] = NumberObject((((int(r) + 45) // 90) * 90) % 360)
  663. def transfer_rotation_to_content(self) -> None:
  664. """
  665. Apply the rotation of the page to the content and the media/crop/...
  666. boxes.
  667. It is recommended to apply this function before page merging.
  668. """
  669. r = -self.rotation # rotation to apply is in the otherway
  670. self.rotation = 0
  671. mb = RectangleObject(self.mediabox)
  672. trsf = (
  673. Transformation()
  674. .translate(
  675. -float(mb.left + mb.width / 2), -float(mb.bottom + mb.height / 2)
  676. )
  677. .rotate(r)
  678. )
  679. pt1 = trsf.apply_on(mb.lower_left)
  680. pt2 = trsf.apply_on(mb.upper_right)
  681. trsf = trsf.translate(-min(pt1[0], pt2[0]), -min(pt1[1], pt2[1]))
  682. self.add_transformation(trsf, False)
  683. for b in ["/MediaBox", "/CropBox", "/BleedBox", "/TrimBox", "/ArtBox"]:
  684. if b in self:
  685. rr = RectangleObject(self[b]) # type: ignore
  686. pt1 = trsf.apply_on(rr.lower_left)
  687. pt2 = trsf.apply_on(rr.upper_right)
  688. self[NameObject(b)] = RectangleObject(
  689. (
  690. min(pt1[0], pt2[0]),
  691. min(pt1[1], pt2[1]),
  692. max(pt1[0], pt2[0]),
  693. max(pt1[1], pt2[1]),
  694. )
  695. )
  696. def rotate(self, angle: int) -> "PageObject":
  697. """
  698. Rotate a page clockwise by increments of 90 degrees.
  699. Args:
  700. angle: Angle to rotate the page. Must be an increment of 90 deg.
  701. Returns:
  702. The rotated PageObject
  703. """
  704. if angle % 90 != 0:
  705. raise ValueError("Rotation angle must be a multiple of 90")
  706. self[NameObject(PG.ROTATE)] = NumberObject(self.rotation + angle)
  707. return self
  708. def _merge_resources(
  709. self,
  710. res1: DictionaryObject,
  711. res2: DictionaryObject,
  712. resource: Any,
  713. new_res1: bool = True,
  714. ) -> tuple[dict[str, Any], dict[str, Any]]:
  715. try:
  716. assert isinstance(self.indirect_reference, IndirectObject)
  717. pdf = self.indirect_reference.pdf
  718. is_pdf_writer = hasattr(
  719. pdf, "_add_object"
  720. ) # expect isinstance(pdf, PdfWriter)
  721. except (AssertionError, AttributeError):
  722. pdf = None
  723. is_pdf_writer = False
  724. def compute_unique_key(base_key: str) -> tuple[str, bool]:
  725. """
  726. Find a key that either doesn't already exist or has the same value
  727. (indicated by the bool)
  728. Args:
  729. base_key: An index is added to this to get the computed key
  730. Returns:
  731. A tuple (computed key, bool) where the boolean indicates
  732. if there is a resource of the given computed_key with the same
  733. value.
  734. """
  735. value = page2res.raw_get(base_key)
  736. # TODO: a possible improvement for writer, the indirect_reference
  737. # cannot be found because translated
  738. # try the current key first (e.g. "foo"), but otherwise iterate
  739. # through "foo-0", "foo-1", etc. new_res can contain only finitely
  740. # many keys, thus this'll eventually end, even if it's been crafted
  741. # to be maximally annoying.
  742. computed_key = base_key
  743. idx = 0
  744. while computed_key in new_res:
  745. if new_res.raw_get(computed_key) == value:
  746. # there's already a resource of this name, with the exact
  747. # same value
  748. return computed_key, True
  749. computed_key = f"{base_key}-{idx}"
  750. idx += 1
  751. return computed_key, False
  752. if new_res1:
  753. new_res = DictionaryObject()
  754. new_res.update(res1.get(resource, DictionaryObject()).get_object())
  755. else:
  756. new_res = cast(DictionaryObject, res1[resource])
  757. page2res = cast(
  758. DictionaryObject, res2.get(resource, DictionaryObject()).get_object()
  759. )
  760. rename_res = {}
  761. for key in page2res:
  762. unique_key, same_value = compute_unique_key(key)
  763. newname = NameObject(unique_key)
  764. if key != unique_key:
  765. # we have to use a different name for this
  766. rename_res[key] = newname
  767. if not same_value:
  768. if is_pdf_writer:
  769. new_res[newname] = page2res.raw_get(key).clone(pdf)
  770. try:
  771. new_res[newname] = new_res[newname].indirect_reference
  772. except AttributeError:
  773. pass
  774. else:
  775. new_res[newname] = page2res.raw_get(key)
  776. lst = sorted(new_res.items())
  777. new_res.clear()
  778. for el in lst:
  779. new_res[el[0]] = el[1]
  780. return new_res, rename_res
  781. @staticmethod
  782. def _content_stream_rename(
  783. stream: ContentStream,
  784. rename: dict[Any, Any],
  785. pdf: Optional[PdfCommonDocProtocol],
  786. ) -> ContentStream:
  787. if not rename:
  788. return stream
  789. stream = ContentStream(stream, pdf)
  790. for operands, _operator in stream.operations:
  791. if isinstance(operands, list):
  792. for i, op in enumerate(operands):
  793. if isinstance(op, NameObject):
  794. operands[i] = rename.get(op, op)
  795. elif isinstance(operands, dict):
  796. for i, op in operands.items():
  797. if isinstance(op, NameObject):
  798. operands[i] = rename.get(op, op)
  799. else:
  800. raise KeyError(f"Type of operands is {type(operands)}")
  801. return stream
  802. @staticmethod
  803. def _add_transformation_matrix(
  804. contents: Any,
  805. pdf: Optional[PdfCommonDocProtocol],
  806. ctm: CompressedTransformationMatrix,
  807. ) -> ContentStream:
  808. """Add transformation matrix at the beginning of the given contents stream."""
  809. contents = ContentStream(contents, pdf)
  810. contents.operations.insert(
  811. 0,
  812. [
  813. [FloatObject(x) for x in ctm],
  814. b"cm",
  815. ],
  816. )
  817. return contents
  818. def _get_contents_as_bytes(self) -> Optional[bytes]:
  819. """
  820. Return the page contents as bytes.
  821. Returns:
  822. The ``/Contents`` object as bytes, or ``None`` if it doesn't exist.
  823. """
  824. if PG.CONTENTS in self:
  825. obj = self[PG.CONTENTS].get_object()
  826. if isinstance(obj, list):
  827. return b"".join(x.get_object().get_data() for x in obj)
  828. return cast(EncodedStreamObject, obj).get_data()
  829. return None
  830. def get_contents(self) -> Optional[ContentStream]:
  831. """
  832. Access the page contents.
  833. Returns:
  834. The ``/Contents`` object, or ``None`` if it does not exist.
  835. ``/Contents`` is optional, as described in §7.7.3.3 of the PDF Reference.
  836. """
  837. if PG.CONTENTS in self:
  838. try:
  839. pdf = cast(IndirectObject, self.indirect_reference).pdf
  840. except AttributeError:
  841. pdf = None
  842. obj = self[PG.CONTENTS]
  843. if is_null_or_none(obj):
  844. return None
  845. resolved_object = obj.get_object()
  846. return ContentStream(resolved_object, pdf)
  847. return None
  848. def replace_contents(
  849. self, content: Union[None, ContentStream, EncodedStreamObject, ArrayObject]
  850. ) -> None:
  851. """
  852. Replace the page contents with the new content and nullify old objects
  853. Args:
  854. content: new content; if None delete the content field.
  855. """
  856. if not hasattr(self, "indirect_reference") or self.indirect_reference is None:
  857. # the page is not attached : the content is directly attached.
  858. self[NameObject(PG.CONTENTS)] = content
  859. return
  860. from pypdf._writer import PdfWriter # noqa: PLC0415
  861. if not isinstance(self.indirect_reference.pdf, PdfWriter):
  862. deprecate(
  863. "Calling `PageObject.replace_contents()` for pages not assigned to a writer is deprecated "
  864. "and will be removed in pypdf 7.0.0. Attach the page to the writer first or use "
  865. "`PdfWriter(clone_from=...)` directly. The existing approach has proved being unreliable."
  866. )
  867. writer = self.indirect_reference.pdf
  868. if isinstance(self.get(PG.CONTENTS, None), ArrayObject):
  869. content_array = cast(ArrayObject, self[PG.CONTENTS])
  870. for reference in content_array:
  871. try:
  872. writer._replace_object(indirect_reference=reference.indirect_reference, obj=NullObject())
  873. except ValueError:
  874. # Occurs when called on PdfReader.
  875. pass
  876. if isinstance(content, ArrayObject):
  877. content = ArrayObject(writer._add_object(obj) for obj in content)
  878. if is_null_or_none(content):
  879. if PG.CONTENTS not in self:
  880. return
  881. assert self[PG.CONTENTS].indirect_reference is not None
  882. writer._replace_object(indirect_reference=self[PG.CONTENTS].indirect_reference, obj=NullObject())
  883. del self[PG.CONTENTS]
  884. elif not hasattr(self.get(PG.CONTENTS, None), "indirect_reference"):
  885. try:
  886. self[NameObject(PG.CONTENTS)] = writer._add_object(content)
  887. except AttributeError:
  888. # applies at least for page not in writer
  889. # as a backup solution, we put content as an object although not in accordance with pdf ref
  890. # this will be fixed with the _add_object
  891. self[NameObject(PG.CONTENTS)] = content
  892. else:
  893. assert content is not None, "mypy"
  894. content.indirect_reference = self[
  895. PG.CONTENTS
  896. ].indirect_reference # TODO: in the future may require generation management
  897. try:
  898. writer._replace_object(indirect_reference=content.indirect_reference, obj=content)
  899. except AttributeError:
  900. # applies at least for page not in writer
  901. # as a backup solution, we put content as an object although not in accordance with pdf ref
  902. # this will be fixed with the _add_object
  903. self[NameObject(PG.CONTENTS)] = content
  904. # forces recalculation of inline_images
  905. self.inline_images = None
  906. def merge_page(
  907. self, page2: "PageObject", expand: bool = False, over: bool = True
  908. ) -> None:
  909. """
  910. Merge the content streams of two pages into one.
  911. Resource references (e.g. fonts) are maintained from both pages.
  912. The mediabox, cropbox, etc of this page are not altered.
  913. The parameter page's content stream will
  914. be added to the end of this page's content stream,
  915. meaning that it will be drawn after, or "on top" of this page.
  916. Args:
  917. page2: The page to be merged into this one. Should be
  918. an instance of :class:`PageObject<PageObject>`.
  919. over: set the page2 content over page1 if True (default) else under
  920. expand: If True, the current page dimensions will be
  921. expanded to accommodate the dimensions of the page to be merged.
  922. """
  923. self._merge_page(page2, over=over, expand=expand)
  924. def _merge_page(
  925. self,
  926. page2: "PageObject",
  927. page2transformation: Optional[Callable[[Any], ContentStream]] = None,
  928. ctm: Optional[CompressedTransformationMatrix] = None,
  929. over: bool = True,
  930. expand: bool = False,
  931. ) -> None:
  932. # First we work on merging the resource dictionaries. This allows us
  933. # to find out what symbols in the content streams we might need to
  934. # rename.
  935. try:
  936. assert isinstance(self.indirect_reference, IndirectObject)
  937. if hasattr(
  938. self.indirect_reference.pdf, "_add_object"
  939. ): # to detect PdfWriter
  940. return self._merge_page_writer(
  941. page2, page2transformation, ctm, over, expand
  942. )
  943. except (AssertionError, AttributeError):
  944. pass
  945. new_resources = DictionaryObject()
  946. rename = {}
  947. original_resources = cast(DictionaryObject, self.get(PG.RESOURCES, DictionaryObject()).get_object())
  948. page2resources = cast(DictionaryObject, page2.get(PG.RESOURCES, DictionaryObject()).get_object())
  949. new_annots = ArrayObject()
  950. for page in (self, page2):
  951. if PG.ANNOTS in page:
  952. annots = page[PG.ANNOTS]
  953. if isinstance(annots, ArrayObject):
  954. new_annots.extend(annots)
  955. for res in (
  956. RES.EXT_G_STATE,
  957. RES.FONT,
  958. RES.XOBJECT,
  959. RES.COLOR_SPACE,
  960. RES.PATTERN,
  961. RES.SHADING,
  962. RES.PROPERTIES,
  963. ):
  964. new, newrename = self._merge_resources(
  965. original_resources, page2resources, res
  966. )
  967. if new:
  968. new_resources[NameObject(res)] = new
  969. rename.update(newrename)
  970. # Combine /ProcSet sets, making sure there's a consistent order
  971. new_resources[NameObject(RES.PROC_SET)] = ArrayObject(
  972. sorted(
  973. set(
  974. original_resources.get(RES.PROC_SET, ArrayObject()).get_object()
  975. ).union(
  976. set(page2resources.get(RES.PROC_SET, ArrayObject()).get_object())
  977. )
  978. )
  979. )
  980. new_content_array = ArrayObject()
  981. original_content = self.get_contents()
  982. if original_content is not None:
  983. original_content.isolate_graphics_state()
  984. new_content_array.append(original_content)
  985. page2content = page2.get_contents()
  986. if page2content is not None:
  987. rect = getattr(page2, MERGE_CROP_BOX)
  988. page2content.operations.insert(
  989. 0,
  990. (
  991. map(
  992. FloatObject,
  993. [
  994. rect.left,
  995. rect.bottom,
  996. rect.width,
  997. rect.height,
  998. ],
  999. ),
  1000. b"re",
  1001. ),
  1002. )
  1003. page2content.operations.insert(1, ([], b"W"))
  1004. page2content.operations.insert(2, ([], b"n"))
  1005. if page2transformation is not None:
  1006. page2content = page2transformation(page2content)
  1007. page2content = PageObject._content_stream_rename(
  1008. page2content, rename, self.pdf
  1009. )
  1010. page2content.isolate_graphics_state()
  1011. if over:
  1012. new_content_array.append(page2content)
  1013. else:
  1014. new_content_array.insert(0, page2content)
  1015. # if expanding the page to fit a new page, calculate the new media box size
  1016. if expand:
  1017. self._expand_mediabox(page2, ctm)
  1018. self.replace_contents(ContentStream(new_content_array, self.pdf))
  1019. self[NameObject(PG.RESOURCES)] = new_resources
  1020. self[NameObject(PG.ANNOTS)] = new_annots
  1021. return None
  1022. def _merge_page_writer(
  1023. self,
  1024. page2: "PageObject",
  1025. page2transformation: Optional[Callable[[Any], ContentStream]] = None,
  1026. ctm: Optional[CompressedTransformationMatrix] = None,
  1027. over: bool = True,
  1028. expand: bool = False,
  1029. ) -> None:
  1030. # First we work on merging the resource dictionaries. This allows us
  1031. # to find which symbols in the content streams we might need to
  1032. # rename.
  1033. assert isinstance(self.indirect_reference, IndirectObject)
  1034. pdf = self.indirect_reference.pdf
  1035. rename = {}
  1036. if PG.RESOURCES not in self:
  1037. self[NameObject(PG.RESOURCES)] = DictionaryObject()
  1038. original_resources = cast(DictionaryObject, self[PG.RESOURCES].get_object())
  1039. if PG.RESOURCES not in page2:
  1040. page2resources = DictionaryObject()
  1041. else:
  1042. page2resources = cast(DictionaryObject, page2[PG.RESOURCES].get_object())
  1043. for res in (
  1044. RES.EXT_G_STATE,
  1045. RES.FONT,
  1046. RES.XOBJECT,
  1047. RES.COLOR_SPACE,
  1048. RES.PATTERN,
  1049. RES.SHADING,
  1050. RES.PROPERTIES,
  1051. ):
  1052. if res in page2resources:
  1053. if res not in original_resources:
  1054. original_resources[NameObject(res)] = DictionaryObject()
  1055. _, newrename = self._merge_resources(
  1056. original_resources, page2resources, res, False
  1057. )
  1058. rename.update(newrename)
  1059. # Combine /ProcSet sets.
  1060. if RES.PROC_SET in page2resources:
  1061. if RES.PROC_SET not in original_resources:
  1062. original_resources[NameObject(RES.PROC_SET)] = ArrayObject()
  1063. arr = cast(ArrayObject, original_resources[RES.PROC_SET])
  1064. for x in cast(ArrayObject, page2resources[RES.PROC_SET]):
  1065. if x not in arr:
  1066. arr.append(x)
  1067. arr.sort()
  1068. if PG.ANNOTS in page2:
  1069. if PG.ANNOTS not in self:
  1070. self[NameObject(PG.ANNOTS)] = ArrayObject()
  1071. annots = cast(ArrayObject, self[PG.ANNOTS].get_object())
  1072. if ctm is None:
  1073. trsf = Transformation()
  1074. else:
  1075. trsf = Transformation(ctm)
  1076. # Ensure we are working on a copy of the list. Otherwise, if both pages
  1077. # are the same object, we might run into an infinite loop.
  1078. for a in cast(ArrayObject, deepcopy(page2[PG.ANNOTS])):
  1079. a = a.get_object()
  1080. aa = a.clone(
  1081. pdf,
  1082. ignore_fields=("/P", "/StructParent", "/Parent"),
  1083. force_duplicate=True,
  1084. )
  1085. r = cast(ArrayObject, a["/Rect"])
  1086. pt1 = trsf.apply_on((r[0], r[1]), True)
  1087. pt2 = trsf.apply_on((r[2], r[3]), True)
  1088. aa[NameObject("/Rect")] = ArrayObject(
  1089. (
  1090. min(pt1[0], pt2[0]),
  1091. min(pt1[1], pt2[1]),
  1092. max(pt1[0], pt2[0]),
  1093. max(pt1[1], pt2[1]),
  1094. )
  1095. )
  1096. if "/QuadPoints" in a:
  1097. q = cast(ArrayObject, a["/QuadPoints"])
  1098. aa[NameObject("/QuadPoints")] = ArrayObject(
  1099. trsf.apply_on((q[0], q[1]), True)
  1100. + trsf.apply_on((q[2], q[3]), True)
  1101. + trsf.apply_on((q[4], q[5]), True)
  1102. + trsf.apply_on((q[6], q[7]), True)
  1103. )
  1104. try:
  1105. aa["/Popup"][NameObject("/Parent")] = aa.indirect_reference
  1106. except KeyError:
  1107. pass
  1108. try:
  1109. aa[NameObject("/P")] = self.indirect_reference
  1110. annots.append(aa.indirect_reference)
  1111. except AttributeError:
  1112. pass
  1113. new_content_array = ArrayObject()
  1114. original_content = self.get_contents()
  1115. if original_content is not None:
  1116. original_content.isolate_graphics_state()
  1117. new_content_array.append(original_content)
  1118. page2content = page2.get_contents()
  1119. if page2content is not None:
  1120. rect = getattr(page2, MERGE_CROP_BOX)
  1121. page2content.operations.insert(
  1122. 0,
  1123. (
  1124. map(
  1125. FloatObject,
  1126. [
  1127. rect.left,
  1128. rect.bottom,
  1129. rect.width,
  1130. rect.height,
  1131. ],
  1132. ),
  1133. b"re",
  1134. ),
  1135. )
  1136. page2content.operations.insert(1, ([], b"W"))
  1137. page2content.operations.insert(2, ([], b"n"))
  1138. if page2transformation is not None:
  1139. page2content = page2transformation(page2content)
  1140. page2content = PageObject._content_stream_rename(
  1141. page2content, rename, self.pdf
  1142. )
  1143. page2content.isolate_graphics_state()
  1144. if over:
  1145. new_content_array.append(page2content)
  1146. else:
  1147. new_content_array.insert(0, page2content)
  1148. # if expanding the page to fit a new page, calculate the new media box size
  1149. if expand:
  1150. self._expand_mediabox(page2, ctm)
  1151. self.replace_contents(new_content_array)
  1152. def _expand_mediabox(
  1153. self, page2: "PageObject", ctm: Optional[CompressedTransformationMatrix]
  1154. ) -> None:
  1155. corners1 = (
  1156. self.mediabox.left.as_numeric(),
  1157. self.mediabox.bottom.as_numeric(),
  1158. self.mediabox.right.as_numeric(),
  1159. self.mediabox.top.as_numeric(),
  1160. )
  1161. corners2 = (
  1162. page2.mediabox.left.as_numeric(),
  1163. page2.mediabox.bottom.as_numeric(),
  1164. page2.mediabox.left.as_numeric(),
  1165. page2.mediabox.top.as_numeric(),
  1166. page2.mediabox.right.as_numeric(),
  1167. page2.mediabox.top.as_numeric(),
  1168. page2.mediabox.right.as_numeric(),
  1169. page2.mediabox.bottom.as_numeric(),
  1170. )
  1171. if ctm is not None:
  1172. ctm = tuple(float(x) for x in ctm) # type: ignore[assignment]
  1173. new_x = tuple(
  1174. ctm[0] * corners2[i] + ctm[2] * corners2[i + 1] + ctm[4]
  1175. for i in range(0, 8, 2)
  1176. )
  1177. new_y = tuple(
  1178. ctm[1] * corners2[i] + ctm[3] * corners2[i + 1] + ctm[5]
  1179. for i in range(0, 8, 2)
  1180. )
  1181. else:
  1182. new_x = corners2[0:8:2]
  1183. new_y = corners2[1:8:2]
  1184. lowerleft = (min(new_x), min(new_y))
  1185. upperright = (max(new_x), max(new_y))
  1186. lowerleft = (min(corners1[0], lowerleft[0]), min(corners1[1], lowerleft[1]))
  1187. upperright = (
  1188. max(corners1[2], upperright[0]),
  1189. max(corners1[3], upperright[1]),
  1190. )
  1191. self.mediabox.lower_left = lowerleft
  1192. self.mediabox.upper_right = upperright
  1193. def merge_transformed_page(
  1194. self,
  1195. page2: "PageObject",
  1196. ctm: Union[CompressedTransformationMatrix, Transformation],
  1197. over: bool = True,
  1198. expand: bool = False,
  1199. ) -> None:
  1200. """
  1201. Similar to :meth:`~pypdf._page.PageObject.merge_page`, but a transformation
  1202. matrix is applied to the merged stream.
  1203. Args:
  1204. page2: The page to be merged into this one.
  1205. ctm: a 6-element tuple containing the operands of the
  1206. transformation matrix
  1207. over: set the page2 content over page1 if True (default) else under
  1208. expand: Whether the page should be expanded to fit the dimensions
  1209. of the page to be merged.
  1210. """
  1211. if isinstance(ctm, Transformation):
  1212. ctm = ctm.ctm
  1213. self._merge_page(
  1214. page2,
  1215. lambda page2_content: PageObject._add_transformation_matrix(
  1216. page2_content, page2.pdf, ctm
  1217. ),
  1218. ctm,
  1219. over,
  1220. expand,
  1221. )
  1222. def merge_scaled_page(
  1223. self, page2: "PageObject", scale: float, over: bool = True, expand: bool = False
  1224. ) -> None:
  1225. """
  1226. Similar to :meth:`~pypdf._page.PageObject.merge_page`, but the stream to be merged
  1227. is scaled by applying a transformation matrix.
  1228. Args:
  1229. page2: The page to be merged into this one.
  1230. scale: The scaling factor
  1231. over: set the page2 content over page1 if True (default) else under
  1232. expand: Whether the page should be expanded to fit the
  1233. dimensions of the page to be merged.
  1234. """
  1235. op = Transformation().scale(scale, scale)
  1236. self.merge_transformed_page(page2, op, over, expand)
  1237. def merge_rotated_page(
  1238. self,
  1239. page2: "PageObject",
  1240. rotation: float,
  1241. over: bool = True,
  1242. expand: bool = False,
  1243. ) -> None:
  1244. """
  1245. Similar to :meth:`~pypdf._page.PageObject.merge_page`, but the stream to be merged
  1246. is rotated by applying a transformation matrix.
  1247. Args:
  1248. page2: The page to be merged into this one.
  1249. rotation: The angle of the rotation, in degrees
  1250. over: set the page2 content over page1 if True (default) else under
  1251. expand: Whether the page should be expanded to fit the
  1252. dimensions of the page to be merged.
  1253. """
  1254. op = Transformation().rotate(rotation)
  1255. self.merge_transformed_page(page2, op, over, expand)
  1256. def merge_translated_page(
  1257. self,
  1258. page2: "PageObject",
  1259. tx: float,
  1260. ty: float,
  1261. over: bool = True,
  1262. expand: bool = False,
  1263. ) -> None:
  1264. """
  1265. Similar to :meth:`~pypdf._page.PageObject.merge_page`, but the stream to be
  1266. merged is translated by applying a transformation matrix.
  1267. Args:
  1268. page2: the page to be merged into this one.
  1269. tx: The translation on X axis
  1270. ty: The translation on Y axis
  1271. over: set the page2 content over page1 if True (default) else under
  1272. expand: Whether the page should be expanded to fit the
  1273. dimensions of the page to be merged.
  1274. """
  1275. op = Transformation().translate(tx, ty)
  1276. self.merge_transformed_page(page2, op, over, expand)
  1277. def add_transformation(
  1278. self,
  1279. ctm: Union[Transformation, CompressedTransformationMatrix],
  1280. expand: bool = False,
  1281. ) -> None:
  1282. """
  1283. Apply a transformation matrix to the page.
  1284. Args:
  1285. ctm: A 6-element tuple containing the operands of the
  1286. transformation matrix. Alternatively, a
  1287. :py:class:`Transformation<pypdf.Transformation>`
  1288. object can be passed.
  1289. See :doc:`/user/cropping-and-transforming`.
  1290. """
  1291. if isinstance(ctm, Transformation):
  1292. ctm = ctm.ctm
  1293. content = self.get_contents()
  1294. if content is not None:
  1295. content = PageObject._add_transformation_matrix(content, self.pdf, ctm)
  1296. content.isolate_graphics_state()
  1297. self.replace_contents(content)
  1298. # if expanding the page to fit a new page, calculate the new media box size
  1299. if expand:
  1300. corners = [
  1301. self.mediabox.left.as_numeric(),
  1302. self.mediabox.bottom.as_numeric(),
  1303. self.mediabox.left.as_numeric(),
  1304. self.mediabox.top.as_numeric(),
  1305. self.mediabox.right.as_numeric(),
  1306. self.mediabox.top.as_numeric(),
  1307. self.mediabox.right.as_numeric(),
  1308. self.mediabox.bottom.as_numeric(),
  1309. ]
  1310. ctm = tuple(float(x) for x in ctm) # type: ignore[assignment]
  1311. new_x = [
  1312. ctm[0] * corners[i] + ctm[2] * corners[i + 1] + ctm[4]
  1313. for i in range(0, 8, 2)
  1314. ]
  1315. new_y = [
  1316. ctm[1] * corners[i] + ctm[3] * corners[i + 1] + ctm[5]
  1317. for i in range(0, 8, 2)
  1318. ]
  1319. self.mediabox.lower_left = (min(new_x), min(new_y))
  1320. self.mediabox.upper_right = (max(new_x), max(new_y))
  1321. def scale(self, sx: float, sy: float) -> None:
  1322. """
  1323. Scale a page by the given factors by applying a transformation matrix
  1324. to its content and updating the page size.
  1325. This updates the various page boundaries (bleedbox, trimbox, etc.)
  1326. and the contents of the page.
  1327. Args:
  1328. sx: The scaling factor on horizontal axis.
  1329. sy: The scaling factor on vertical axis.
  1330. """
  1331. self.add_transformation((sx, 0, 0, sy, 0, 0))
  1332. self.bleedbox = self.bleedbox.scale(sx, sy)
  1333. self.trimbox = self.trimbox.scale(sx, sy)
  1334. self.artbox = self.artbox.scale(sx, sy)
  1335. self.cropbox = self.cropbox.scale(sx, sy)
  1336. self.mediabox = self.mediabox.scale(sx, sy)
  1337. if PG.ANNOTS in self:
  1338. annotations = self[PG.ANNOTS]
  1339. if isinstance(annotations, ArrayObject):
  1340. for annotation in annotations:
  1341. annotation_obj = annotation.get_object()
  1342. if ADA.Rect in annotation_obj:
  1343. rectangle = annotation_obj[ADA.Rect]
  1344. if isinstance(rectangle, ArrayObject):
  1345. rectangle[0] = FloatObject(float(rectangle[0]) * sx)
  1346. rectangle[1] = FloatObject(float(rectangle[1]) * sy)
  1347. rectangle[2] = FloatObject(float(rectangle[2]) * sx)
  1348. rectangle[3] = FloatObject(float(rectangle[3]) * sy)
  1349. if PG.VP in self:
  1350. viewport = self[PG.VP]
  1351. if isinstance(viewport, ArrayObject):
  1352. bbox = viewport[0]["/BBox"]
  1353. else:
  1354. bbox = viewport["/BBox"] # type: ignore
  1355. scaled_bbox = RectangleObject(
  1356. (
  1357. float(bbox[0]) * sx,
  1358. float(bbox[1]) * sy,
  1359. float(bbox[2]) * sx,
  1360. float(bbox[3]) * sy,
  1361. )
  1362. )
  1363. if isinstance(viewport, ArrayObject):
  1364. self[NameObject(PG.VP)][NumberObject(0)][ # type: ignore
  1365. NameObject("/BBox")
  1366. ] = scaled_bbox
  1367. else:
  1368. self[NameObject(PG.VP)][NameObject("/BBox")] = scaled_bbox # type: ignore
  1369. def scale_by(self, factor: float) -> None:
  1370. """
  1371. Scale a page by the given factor by applying a transformation matrix to
  1372. its content and updating the page size.
  1373. Args:
  1374. factor: The scaling factor (for both X and Y axis).
  1375. """
  1376. self.scale(factor, factor)
  1377. def scale_to(self, width: float, height: float) -> None:
  1378. """
  1379. Scale a page to the specified dimensions by applying a transformation
  1380. matrix to its content and updating the page size.
  1381. Args:
  1382. width: The new width.
  1383. height: The new height.
  1384. """
  1385. sx = width / float(self.mediabox.width)
  1386. sy = height / float(self.mediabox.height)
  1387. self.scale(sx, sy)
  1388. def compress_content_streams(self, level: int = -1) -> None:
  1389. """
  1390. Compress the size of this page by joining all content streams and
  1391. applying a FlateDecode filter.
  1392. However, it is possible that this function will perform no action if
  1393. content stream compression becomes "automatic".
  1394. """
  1395. content = self.get_contents()
  1396. if content is not None:
  1397. content_obj = content.flate_encode(level)
  1398. try:
  1399. content.indirect_reference.pdf._objects[ # type: ignore
  1400. content.indirect_reference.idnum - 1 # type: ignore
  1401. ] = content_obj
  1402. except AttributeError:
  1403. if self.indirect_reference is not None and hasattr(
  1404. self.indirect_reference.pdf, "_add_object"
  1405. ):
  1406. self.replace_contents(content_obj)
  1407. else:
  1408. raise ValueError("Page must be part of a PdfWriter")
  1409. @property
  1410. def page_number(self) -> Optional[int]:
  1411. """
  1412. Read-only property which returns the page number within the PDF file.
  1413. Returns:
  1414. Page number; None if the page is not attached to a PDF.
  1415. """
  1416. if self.indirect_reference is None:
  1417. return None
  1418. try:
  1419. lst = self.indirect_reference.pdf.pages
  1420. return lst.index(self)
  1421. except ValueError:
  1422. return None
  1423. def _debug_for_extract(self) -> str: # pragma: no cover
  1424. out = ""
  1425. for ope, op in ContentStream(
  1426. self["/Contents"].get_object(), self.pdf, "bytes"
  1427. ).operations:
  1428. if op == b"TJ":
  1429. s = [x for x in ope[0] if isinstance(x, str)]
  1430. else:
  1431. s = []
  1432. out += op.decode("utf-8") + " " + "".join(s) + ope.__repr__() + "\n"
  1433. out += "\n=============================\n"
  1434. try:
  1435. for fo in self[PG.RESOURCES]["/Font"]: # type:ignore
  1436. out += fo + "\n"
  1437. out += self[PG.RESOURCES]["/Font"][fo].__repr__() + "\n" # type:ignore
  1438. try:
  1439. enc_repr = self[PG.RESOURCES]["/Font"][fo][ # type:ignore
  1440. "/Encoding"
  1441. ].__repr__()
  1442. out += enc_repr + "\n"
  1443. except Exception:
  1444. pass
  1445. try:
  1446. out += (
  1447. self[PG.RESOURCES]["/Font"][fo][ # type:ignore
  1448. "/ToUnicode"
  1449. ]
  1450. .get_data()
  1451. .decode()
  1452. + "\n"
  1453. )
  1454. except Exception:
  1455. pass
  1456. except KeyError:
  1457. out += "No Font\n"
  1458. return out
  1459. def _extract_text(
  1460. self,
  1461. obj: Any,
  1462. pdf: Any,
  1463. orientations: tuple[int, ...] = (0, 90, 180, 270),
  1464. space_width: float = 200.0,
  1465. content_key: Optional[str] = PG.CONTENTS,
  1466. visitor_operand_before: Optional[Callable[[Any, Any, Any, Any], None]] = None,
  1467. visitor_operand_after: Optional[Callable[[Any, Any, Any, Any], None]] = None,
  1468. visitor_text: Optional[Callable[[Any, Any, Any, Any, Any], None]] = None,
  1469. ) -> str:
  1470. """
  1471. See extract_text for most arguments.
  1472. Args:
  1473. content_key: indicate the default key where to extract data
  1474. None = the object; this allows reusing the function on an XObject
  1475. default = "/Content"
  1476. """
  1477. extractor = TextExtraction()
  1478. font_resources: dict[str, DictionaryObject] = {}
  1479. fonts: dict[str, Font] = {}
  1480. try:
  1481. objr = obj
  1482. while NameObject(PG.RESOURCES) not in objr:
  1483. # /Resources can be inherited so we look to parents
  1484. objr = objr["/Parent"].get_object()
  1485. # If no parents then no /Resources will be available,
  1486. # so an exception will be raised
  1487. resources_dict = cast(DictionaryObject, objr[PG.RESOURCES])
  1488. except Exception:
  1489. # No resources means no text is possible (no font); we consider the
  1490. # file as not damaged, no need to check for TJ or Tj
  1491. return ""
  1492. if (
  1493. not is_null_or_none(resources_dict)
  1494. and "/Font" in resources_dict
  1495. and (font_resources_dict := cast(DictionaryObject, resources_dict["/Font"]))
  1496. ):
  1497. for font_resource in font_resources_dict:
  1498. try:
  1499. font_resource_object = cast(DictionaryObject, font_resources_dict[font_resource].get_object())
  1500. font_resources[font_resource] = font_resource_object
  1501. fonts[font_resource] = Font.from_font_resource(font_resource_object)
  1502. # Override space width, if applicable
  1503. if fonts[font_resource].character_widths.get(" ", 0) == 0:
  1504. fonts[font_resource].space_width = space_width
  1505. except (AttributeError, TypeError):
  1506. pass
  1507. try:
  1508. content = (
  1509. obj[content_key].get_object() if isinstance(content_key, str) else obj
  1510. )
  1511. if not isinstance(content, ContentStream):
  1512. content = ContentStream(content, pdf, "bytes")
  1513. except (AttributeError, KeyError): # no content can be extracted (certainly empty page)
  1514. return ""
  1515. # We check all strings are TextStringObjects. ByteStringObjects
  1516. # are strings where the byte->string encoding was unknown, so adding
  1517. # them to the text here would be gibberish.
  1518. # Initialize the extractor with the necessary parameters
  1519. extractor.initialize_extraction(orientations, visitor_text, font_resources, fonts)
  1520. for operands, operator in content.operations:
  1521. if visitor_operand_before is not None:
  1522. visitor_operand_before(operator, operands, extractor.cm_matrix, extractor.tm_matrix)
  1523. # Multiple operators are handled here
  1524. if operator == b"'":
  1525. extractor.process_operation(b"T*", [])
  1526. extractor.process_operation(b"Tj", operands)
  1527. elif operator == b'"':
  1528. extractor.process_operation(b"Tw", [operands[0]])
  1529. extractor.process_operation(b"Tc", [operands[1]])
  1530. extractor.process_operation(b"T*", [])
  1531. extractor.process_operation(b"Tj", operands[2:])
  1532. elif operator == b"TJ":
  1533. # The space width may be smaller than the font width, so the width should be 95%.
  1534. _confirm_space_width = extractor._space_width * 0.95
  1535. if operands:
  1536. for op in operands[0]:
  1537. if isinstance(op, (str, bytes)):
  1538. extractor.process_operation(b"Tj", [op])
  1539. if isinstance(op, (int, float, NumberObject, FloatObject)) and (
  1540. abs(float(op)) >= _confirm_space_width
  1541. and extractor.text
  1542. and extractor.text[-1] != " "
  1543. ):
  1544. extractor.process_operation(b"Tj", [" "])
  1545. elif operator == b"TD":
  1546. extractor.process_operation(b"TL", [-operands[1]])
  1547. extractor.process_operation(b"Td", operands)
  1548. elif operator == b"Do":
  1549. extractor.output += extractor.text
  1550. if visitor_text is not None:
  1551. visitor_text(
  1552. extractor.text,
  1553. extractor.memo_cm,
  1554. extractor.memo_tm,
  1555. extractor.font_resource,
  1556. extractor.font_size,
  1557. )
  1558. try:
  1559. if extractor.output[-1] != "\n":
  1560. extractor.output += "\n"
  1561. if visitor_text is not None:
  1562. visitor_text(
  1563. "\n",
  1564. extractor.memo_cm,
  1565. extractor.memo_tm,
  1566. extractor.font_resource,
  1567. extractor.font_size,
  1568. )
  1569. except IndexError:
  1570. pass
  1571. try:
  1572. xobj = resources_dict["/XObject"]
  1573. if xobj[operands[0]]["/Subtype"] != "/Image": # type: ignore
  1574. text = self.extract_xform_text(
  1575. xobj[operands[0]], # type: ignore
  1576. orientations,
  1577. space_width,
  1578. visitor_operand_before,
  1579. visitor_operand_after,
  1580. visitor_text,
  1581. )
  1582. extractor.output += text
  1583. if visitor_text is not None:
  1584. visitor_text(
  1585. text,
  1586. extractor.memo_cm,
  1587. extractor.memo_tm,
  1588. extractor.font_resource,
  1589. extractor.font_size,
  1590. )
  1591. except Exception as exception:
  1592. logger_warning(
  1593. f"Impossible to decode XFormObject {operands[0]}: {exception}",
  1594. __name__,
  1595. )
  1596. finally:
  1597. extractor.text = ""
  1598. extractor.memo_cm = extractor.cm_matrix.copy()
  1599. extractor.memo_tm = extractor.tm_matrix.copy()
  1600. else:
  1601. extractor.process_operation(operator, operands)
  1602. if visitor_operand_after is not None:
  1603. visitor_operand_after(operator, operands, extractor.cm_matrix, extractor.tm_matrix)
  1604. extractor.output += extractor.text # just in case
  1605. if extractor.text != "" and visitor_text is not None:
  1606. visitor_text(
  1607. extractor.text,
  1608. extractor.memo_cm,
  1609. extractor.memo_tm,
  1610. extractor.font_resource,
  1611. extractor.font_size,
  1612. )
  1613. return extractor.output
  1614. def _layout_mode_fonts(self) -> dict[str, Font]:
  1615. """
  1616. Get fonts formatted for "layout" mode text extraction.
  1617. Returns:
  1618. Dict[str, Font]: dictionary of Font instances keyed by font name
  1619. """
  1620. # Font retrieval logic adapted from pypdf.PageObject._extract_text()
  1621. objr: Any = self
  1622. fonts: dict[str, Font] = {}
  1623. while objr is not None:
  1624. try:
  1625. resources_dict: Any = objr[PG.RESOURCES]
  1626. except KeyError:
  1627. resources_dict = {}
  1628. if "/Font" in resources_dict and self.pdf is not None:
  1629. for font_name in resources_dict["/Font"]:
  1630. fonts[font_name] = Font.from_font_resource(resources_dict["/Font"][font_name])
  1631. try:
  1632. objr = objr["/Parent"].get_object()
  1633. except KeyError:
  1634. objr = None
  1635. return fonts
  1636. def _layout_mode_text(
  1637. self,
  1638. space_vertically: bool = True,
  1639. scale_weight: float = 1.25,
  1640. strip_rotated: bool = True,
  1641. debug_path: Optional[Path] = None,
  1642. font_height_weight: float = 1,
  1643. ) -> str:
  1644. """
  1645. Get text preserving fidelity to source PDF text layout.
  1646. Args:
  1647. space_vertically: include blank lines inferred from y distance + font
  1648. height. Defaults to True.
  1649. scale_weight: multiplier for string length when calculating weighted
  1650. average character width. Defaults to 1.25.
  1651. strip_rotated: Removes text that is rotated w.r.t. to the page from
  1652. layout mode output. Defaults to True.
  1653. debug_path (Path | None): if supplied, must target a directory.
  1654. creates the following files with debug information for layout mode
  1655. functions if supplied:
  1656. - fonts.json: output of self._layout_mode_fonts
  1657. - tjs.json: individual text render ops with corresponding transform matrices
  1658. - bts.json: text render ops left justified and grouped by BT/ET operators
  1659. - bt_groups.json: BT/ET operations grouped by rendered y-coord (aka lines)
  1660. Defaults to None.
  1661. font_height_weight: multiplier for font height when calculating
  1662. blank lines. Defaults to 1.
  1663. Returns:
  1664. str: multiline string containing page text in a fixed width format that
  1665. closely adheres to the rendered layout in the source pdf.
  1666. """
  1667. fonts = self._layout_mode_fonts()
  1668. if debug_path: # pragma: no cover
  1669. import json # noqa: PLC0415
  1670. debug_path.joinpath("fonts.json").write_text(
  1671. json.dumps(fonts, indent=2, default=asdict),
  1672. "utf-8"
  1673. )
  1674. ops = iter(
  1675. ContentStream(self["/Contents"].get_object(), self.pdf, "bytes").operations
  1676. )
  1677. bt_groups = _layout_mode.text_show_operations(
  1678. ops, fonts, strip_rotated, debug_path
  1679. )
  1680. if not bt_groups:
  1681. return ""
  1682. ty_groups = _layout_mode.y_coordinate_groups(bt_groups, debug_path)
  1683. char_width = _layout_mode.fixed_char_width(bt_groups, scale_weight)
  1684. return _layout_mode.fixed_width_page(ty_groups, char_width, space_vertically, font_height_weight)
  1685. def extract_text(
  1686. self,
  1687. *args: Any,
  1688. orientations: Union[int, tuple[int, ...]] = (0, 90, 180, 270),
  1689. space_width: float = 200.0,
  1690. visitor_operand_before: Optional[Callable[[Any, Any, Any, Any], None]] = None,
  1691. visitor_operand_after: Optional[Callable[[Any, Any, Any, Any], None]] = None,
  1692. visitor_text: Optional[Callable[[Any, Any, Any, Any, Any], None]] = None,
  1693. extraction_mode: Literal["plain", "layout"] = "plain",
  1694. **kwargs: Any,
  1695. ) -> str:
  1696. """
  1697. Locate all text drawing commands, in the order they are provided in the
  1698. content stream, and extract the text.
  1699. This works well for some PDF files, but poorly for others, depending on
  1700. the generator used. This will be refined in the future.
  1701. Do not rely on the order of text coming out of this function, as it
  1702. will change if this function is made more sophisticated.
  1703. Arabic and Hebrew are extracted in the correct order.
  1704. If required a custom RTL range of characters can be defined;
  1705. see function set_custom_rtl.
  1706. Additionally you can provide visitor methods to get informed on all
  1707. operations and all text objects.
  1708. For example in some PDF files this can be useful to parse tables.
  1709. Args:
  1710. orientations: list of orientations extract_text will look for
  1711. default = (0, 90, 180, 270)
  1712. note: currently only 0 (up),90 (turned left), 180 (upside down),
  1713. 270 (turned right)
  1714. Silently ignored in "layout" mode.
  1715. space_width: force default space width
  1716. if not extracted from font (default: 200)
  1717. Silently ignored in "layout" mode.
  1718. visitor_operand_before: function to be called before processing an operation.
  1719. It has four arguments: operator, operand-arguments,
  1720. current transformation matrix and text matrix.
  1721. Ignored with a warning in "layout" mode.
  1722. visitor_operand_after: function to be called after processing an operation.
  1723. It has four arguments: operator, operand-arguments,
  1724. current transformation matrix and text matrix.
  1725. Ignored with a warning in "layout" mode.
  1726. visitor_text: function to be called when extracting some text at some position.
  1727. It has five arguments: text, current transformation matrix,
  1728. text matrix, font-dictionary and font-size.
  1729. The font-dictionary may be None in case of unknown fonts.
  1730. If not None it may e.g. contain key "/BaseFont" with value "/Arial,Bold".
  1731. Ignored with a warning in "layout" mode.
  1732. extraction_mode (Literal["plain", "layout"]): "plain" for legacy functionality,
  1733. "layout" for experimental layout mode functionality.
  1734. NOTE: orientations, space_width, and visitor_* parameters are NOT respected
  1735. in "layout" mode.
  1736. kwargs:
  1737. layout_mode_space_vertically (bool): include blank lines inferred from
  1738. y distance + font height. Defaults to True.
  1739. layout_mode_scale_weight (float): multiplier for string length when calculating
  1740. weighted average character width. Defaults to 1.25.
  1741. layout_mode_strip_rotated (bool): layout mode does not support rotated text.
  1742. Set to False to include rotated text anyway. If rotated text is discovered,
  1743. layout will be degraded and a warning will result. Defaults to True.
  1744. layout_mode_debug_path (Path | None): if supplied, must target a directory.
  1745. creates the following files with debug information for layout mode
  1746. functions if supplied:
  1747. - fonts.json: output of self._layout_mode_fonts
  1748. - tjs.json: individual text render ops with corresponding transform matrices
  1749. - bts.json: text render ops left justified and grouped by BT/ET operators
  1750. - bt_groups.json: BT/ET operations grouped by rendered y-coord (aka lines)
  1751. layout_mode_font_height_weight (float): multiplier for font height when calculating
  1752. blank lines. Defaults to 1.
  1753. Returns:
  1754. The extracted text
  1755. """
  1756. if extraction_mode not in ["plain", "layout"]:
  1757. raise ValueError(f"Invalid text extraction mode '{extraction_mode}'")
  1758. if extraction_mode == "layout":
  1759. for visitor in (
  1760. "visitor_operand_before",
  1761. "visitor_operand_after",
  1762. "visitor_text",
  1763. ):
  1764. if locals()[visitor]:
  1765. logger_warning(
  1766. f"Argument {visitor} is ignored in layout mode",
  1767. __name__,
  1768. )
  1769. return self._layout_mode_text(
  1770. space_vertically=kwargs.get("layout_mode_space_vertically", True),
  1771. scale_weight=kwargs.get("layout_mode_scale_weight", 1.25),
  1772. strip_rotated=kwargs.get("layout_mode_strip_rotated", True),
  1773. debug_path=kwargs.get("layout_mode_debug_path"),
  1774. font_height_weight=kwargs.get("layout_mode_font_height_weight", 1)
  1775. )
  1776. if len(args) >= 1:
  1777. if isinstance(args[0], str):
  1778. if len(args) >= 3:
  1779. if isinstance(args[2], (tuple, int)):
  1780. orientations = args[2]
  1781. else:
  1782. raise TypeError(f"Invalid positional parameter {args[2]}")
  1783. if len(args) >= 4:
  1784. if isinstance(args[3], (float, int)):
  1785. space_width = args[3]
  1786. else:
  1787. raise TypeError(f"Invalid positional parameter {args[3]}")
  1788. elif isinstance(args[0], (tuple, int)):
  1789. orientations = args[0]
  1790. if len(args) >= 2:
  1791. if isinstance(args[1], (float, int)):
  1792. space_width = args[1]
  1793. else:
  1794. raise TypeError(f"Invalid positional parameter {args[1]}")
  1795. else:
  1796. raise TypeError(f"Invalid positional parameter {args[0]}")
  1797. if isinstance(orientations, int):
  1798. orientations = (orientations,)
  1799. return self._extract_text(
  1800. self,
  1801. self.pdf,
  1802. orientations,
  1803. space_width,
  1804. PG.CONTENTS,
  1805. visitor_operand_before,
  1806. visitor_operand_after,
  1807. visitor_text,
  1808. )
  1809. def extract_xform_text(
  1810. self,
  1811. xform: EncodedStreamObject,
  1812. orientations: tuple[int, ...] = (0, 90, 270, 360),
  1813. space_width: float = 200.0,
  1814. visitor_operand_before: Optional[Callable[[Any, Any, Any, Any], None]] = None,
  1815. visitor_operand_after: Optional[Callable[[Any, Any, Any, Any], None]] = None,
  1816. visitor_text: Optional[Callable[[Any, Any, Any, Any, Any], None]] = None,
  1817. ) -> str:
  1818. """
  1819. Extract text from an XObject.
  1820. Args:
  1821. xform:
  1822. orientations:
  1823. space_width: force default space width (if not extracted from font (default 200)
  1824. visitor_operand_before:
  1825. visitor_operand_after:
  1826. visitor_text:
  1827. Returns:
  1828. The extracted text
  1829. """
  1830. return self._extract_text(
  1831. xform,
  1832. self.pdf,
  1833. orientations,
  1834. space_width,
  1835. None,
  1836. visitor_operand_before,
  1837. visitor_operand_after,
  1838. visitor_text,
  1839. )
  1840. def _get_fonts(self) -> tuple[set[str], set[str]]:
  1841. """
  1842. Get the names of embedded fonts and unembedded fonts.
  1843. Returns:
  1844. A tuple (set of embedded fonts, set of unembedded fonts)
  1845. """
  1846. obj = self.get_object()
  1847. assert isinstance(obj, DictionaryObject)
  1848. fonts: set[str] = set()
  1849. embedded: set[str] = set()
  1850. fonts, embedded = _get_fonts_walk(obj, fonts, embedded)
  1851. unembedded = fonts - embedded
  1852. return embedded, unembedded
  1853. mediabox = _create_rectangle_accessor(PG.MEDIABOX, ())
  1854. """A :class:`RectangleObject<pypdf.generic.RectangleObject>`, expressed in
  1855. default user space units, defining the boundaries of the physical medium on
  1856. which the page is intended to be displayed or printed."""
  1857. cropbox = _create_rectangle_accessor("/CropBox", (PG.MEDIABOX,))
  1858. """
  1859. A :class:`RectangleObject<pypdf.generic.RectangleObject>`, expressed in
  1860. default user space units, defining the visible region of default user
  1861. space.
  1862. When the page is displayed or printed, its contents are to be clipped
  1863. (cropped) to this rectangle and then imposed on the output medium in some
  1864. implementation-defined manner. Default value: same as
  1865. :attr:`mediabox<mediabox>`.
  1866. """
  1867. bleedbox = _create_rectangle_accessor("/BleedBox", ("/CropBox", PG.MEDIABOX))
  1868. """A :class:`RectangleObject<pypdf.generic.RectangleObject>`, expressed in
  1869. default user space units, defining the region to which the contents of the
  1870. page should be clipped when output in a production environment."""
  1871. trimbox = _create_rectangle_accessor("/TrimBox", ("/CropBox", PG.MEDIABOX))
  1872. """A :class:`RectangleObject<pypdf.generic.RectangleObject>`, expressed in
  1873. default user space units, defining the intended dimensions of the finished
  1874. page after trimming."""
  1875. artbox = _create_rectangle_accessor("/ArtBox", ("/CropBox", PG.MEDIABOX))
  1876. """A :class:`RectangleObject<pypdf.generic.RectangleObject>`, expressed in
  1877. default user space units, defining the extent of the page's meaningful
  1878. content as intended by the page's creator."""
  1879. @property
  1880. def annotations(self) -> Optional[ArrayObject]:
  1881. if "/Annots" not in self:
  1882. return None
  1883. return cast(ArrayObject, self["/Annots"])
  1884. @annotations.setter
  1885. def annotations(self, value: Optional[ArrayObject]) -> None:
  1886. """
  1887. Set the annotations array of the page.
  1888. Typically you do not want to set this value, but append to it.
  1889. If you append to it, remember to add the object first to the writer
  1890. and only add the indirect object.
  1891. """
  1892. if value is None:
  1893. if "/Annots" not in self:
  1894. return
  1895. del self[NameObject("/Annots")]
  1896. else:
  1897. self[NameObject("/Annots")] = value
  1898. class _VirtualList(Sequence[PageObject]):
  1899. def __init__(
  1900. self,
  1901. length_function: Callable[[], int],
  1902. get_function: Callable[[int], PageObject],
  1903. ) -> None:
  1904. self.length_function = length_function
  1905. self.get_function = get_function
  1906. self.current = -1
  1907. def __len__(self) -> int:
  1908. return self.length_function()
  1909. @overload
  1910. def __getitem__(self, index: int) -> PageObject:
  1911. ...
  1912. @overload
  1913. def __getitem__(self, index: slice) -> Sequence[PageObject]:
  1914. ...
  1915. def __getitem__(
  1916. self, index: Union[int, slice]
  1917. ) -> Union[PageObject, Sequence[PageObject]]:
  1918. if isinstance(index, slice):
  1919. indices = range(*index.indices(len(self)))
  1920. cls = type(self)
  1921. return cls(indices.__len__, lambda idx: self[indices[idx]])
  1922. if not isinstance(index, int):
  1923. raise TypeError("Sequence indices must be integers")
  1924. len_self = len(self)
  1925. if index < 0:
  1926. # support negative indexes
  1927. index += len_self
  1928. if not (0 <= index < len_self):
  1929. raise IndexError("Sequence index out of range")
  1930. return self.get_function(index)
  1931. def __delitem__(self, index: Union[int, slice]) -> None:
  1932. if isinstance(index, slice):
  1933. r = list(range(*index.indices(len(self))))
  1934. # pages have to be deleted from last to first
  1935. r.sort()
  1936. r.reverse()
  1937. for p in r:
  1938. del self[p] # recursive call
  1939. return
  1940. if not isinstance(index, int):
  1941. raise TypeError("Index must be integers")
  1942. len_self = len(self)
  1943. if index < 0:
  1944. # support negative indexes
  1945. index += len_self
  1946. if not (0 <= index < len_self):
  1947. raise IndexError("Index out of range")
  1948. ind = self[index].indirect_reference
  1949. assert ind is not None
  1950. parent: Optional[PdfObject] = cast(DictionaryObject, ind.get_object()).get(
  1951. "/Parent", None
  1952. )
  1953. first = True
  1954. while parent is not None:
  1955. parent = cast(DictionaryObject, parent.get_object())
  1956. try:
  1957. i = cast(ArrayObject, parent["/Kids"]).index(ind)
  1958. del cast(ArrayObject, parent["/Kids"])[i]
  1959. first = False
  1960. try:
  1961. assert ind is not None
  1962. del ind.pdf.flattened_pages[index] # case of page in a Reader
  1963. except Exception: # pragma: no cover
  1964. pass
  1965. if "/Count" in parent:
  1966. parent[NameObject("/Count")] = NumberObject(
  1967. cast(int, parent["/Count"]) - 1
  1968. )
  1969. if len(cast(ArrayObject, parent["/Kids"])) == 0:
  1970. # No more objects in this part of this subtree
  1971. ind = parent.indirect_reference
  1972. parent = parent.get("/Parent", None)
  1973. except ValueError: # from index
  1974. if first:
  1975. raise PdfReadError(f"Page not found in page tree: {ind}")
  1976. break
  1977. def __iter__(self) -> Iterator[PageObject]:
  1978. for i in range(len(self)):
  1979. yield self[i]
  1980. def __str__(self) -> str:
  1981. p = [f"PageObject({i})" for i in range(self.length_function())]
  1982. return f"[{', '.join(p)}]"
  1983. def _get_fonts_walk(
  1984. obj: DictionaryObject,
  1985. fnt: set[str],
  1986. emb: set[str],
  1987. ) -> tuple[set[str], set[str]]:
  1988. """
  1989. Get the set of all fonts and all embedded fonts.
  1990. Args:
  1991. obj: Page resources dictionary
  1992. fnt: font
  1993. emb: embedded fonts
  1994. Returns:
  1995. A tuple (fnt, emb)
  1996. If there is a key called 'BaseFont', that is a font that is used in the document.
  1997. If there is a key called 'FontName' and another key in the same dictionary object
  1998. that is called 'FontFilex' (where x is null, 2, or 3), then that fontname is
  1999. embedded.
  2000. We create and add to two sets, fnt = fonts used and emb = fonts embedded.
  2001. """
  2002. fontkeys = ("/FontFile", "/FontFile2", "/FontFile3")
  2003. def process_font(f: DictionaryObject) -> None:
  2004. nonlocal fnt, emb
  2005. f = cast(DictionaryObject, f.get_object()) # to be sure
  2006. if "/BaseFont" in f:
  2007. fnt.add(cast(str, f["/BaseFont"]))
  2008. if (
  2009. ("/CharProcs" in f)
  2010. or (
  2011. "/FontDescriptor" in f
  2012. and any(
  2013. x in cast(DictionaryObject, f["/FontDescriptor"]) for x in fontkeys
  2014. )
  2015. )
  2016. or (
  2017. "/DescendantFonts" in f
  2018. and "/FontDescriptor"
  2019. in cast(
  2020. DictionaryObject,
  2021. cast(ArrayObject, f["/DescendantFonts"])[0].get_object(),
  2022. )
  2023. and any(
  2024. x
  2025. in cast(
  2026. DictionaryObject,
  2027. cast(
  2028. DictionaryObject,
  2029. cast(ArrayObject, f["/DescendantFonts"])[0].get_object(),
  2030. )["/FontDescriptor"],
  2031. )
  2032. for x in fontkeys
  2033. )
  2034. )
  2035. ):
  2036. # the list comprehension ensures there is FontFile
  2037. try:
  2038. emb.add(cast(str, f["/BaseFont"]))
  2039. except KeyError:
  2040. emb.add("(" + cast(str, f["/Subtype"]) + ")")
  2041. if "/DR" in obj and "/Font" in cast(DictionaryObject, obj["/DR"]):
  2042. for f in cast(DictionaryObject, cast(DictionaryObject, obj["/DR"])["/Font"]):
  2043. process_font(f)
  2044. if "/Resources" in obj:
  2045. if "/Font" in cast(DictionaryObject, obj["/Resources"]):
  2046. for f in cast(
  2047. DictionaryObject, cast(DictionaryObject, obj["/Resources"])["/Font"]
  2048. ).values():
  2049. process_font(f)
  2050. if "/XObject" in cast(DictionaryObject, obj["/Resources"]):
  2051. for x in cast(
  2052. DictionaryObject, cast(DictionaryObject, obj["/Resources"])["/XObject"]
  2053. ).values():
  2054. _get_fonts_walk(cast(DictionaryObject, x.get_object()), fnt, emb)
  2055. if "/Annots" in obj:
  2056. for a in cast(ArrayObject, obj["/Annots"]):
  2057. _get_fonts_walk(cast(DictionaryObject, a.get_object()), fnt, emb)
  2058. if "/AP" in obj:
  2059. if (
  2060. cast(DictionaryObject, cast(DictionaryObject, obj["/AP"])["/N"]).get(
  2061. "/Type"
  2062. )
  2063. == "/XObject"
  2064. ):
  2065. _get_fonts_walk(
  2066. cast(DictionaryObject, cast(DictionaryObject, obj["/AP"])["/N"]),
  2067. fnt,
  2068. emb,
  2069. )
  2070. else:
  2071. for a in cast(DictionaryObject, cast(DictionaryObject, obj["/AP"])["/N"]):
  2072. _get_fonts_walk(cast(DictionaryObject, a), fnt, emb)
  2073. return fnt, emb # return the sets for each page