_writer.py 128 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341
  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 decimal
  30. import enum
  31. import hashlib
  32. import re
  33. import struct
  34. import sys
  35. import uuid
  36. from collections.abc import Iterable, Mapping
  37. from io import BytesIO, FileIO, IOBase
  38. from itertools import compress
  39. from pathlib import Path
  40. from re import Pattern
  41. from types import TracebackType
  42. from typing import (
  43. IO,
  44. Any,
  45. Callable,
  46. Optional,
  47. Union,
  48. cast,
  49. )
  50. if sys.version_info >= (3, 11):
  51. from typing import Self
  52. else:
  53. from typing_extensions import Self
  54. from ._doc_common import DocumentInformation, PdfDocCommon
  55. from ._encryption import EncryptAlgorithm, Encryption
  56. from ._page import PageObject, Transformation
  57. from ._page_labels import nums_clear_range, nums_insert, nums_next
  58. from ._reader import PdfReader
  59. from ._utils import (
  60. StrByteType,
  61. StreamType,
  62. _get_max_pdf_version_header,
  63. deprecation_no_replacement,
  64. logger_warning,
  65. )
  66. from .constants import AnnotationDictionaryAttributes as AA
  67. from .constants import CatalogAttributes as CA
  68. from .constants import (
  69. CatalogDictionary,
  70. GoToActionArguments,
  71. ImageType,
  72. InteractiveFormDictEntries,
  73. OutlineFontFlag,
  74. PageLabelStyle,
  75. PagesAttributes,
  76. TypFitArguments,
  77. UserAccessPermissions,
  78. )
  79. from .constants import Core as CO
  80. from .constants import FieldDictionaryAttributes as FA
  81. from .constants import PageAttributes as PG
  82. from .constants import TrailerKeys as TK
  83. from .errors import PdfReadError, PyPdfError
  84. from .generic import (
  85. PAGE_FIT,
  86. ArrayObject,
  87. BooleanObject,
  88. ByteStringObject,
  89. ContentStream,
  90. Destination,
  91. DictionaryObject,
  92. EmbeddedFile,
  93. Fit,
  94. FloatObject,
  95. IndirectObject,
  96. NameObject,
  97. NullObject,
  98. NumberObject,
  99. PdfObject,
  100. RectangleObject,
  101. ReferenceLink,
  102. StreamObject,
  103. TextStringObject,
  104. TreeObject,
  105. ViewerPreferences,
  106. create_string_object,
  107. extract_links,
  108. hex_to_rgb,
  109. is_null_or_none,
  110. )
  111. from .generic._appearance_stream import TextStreamAppearance
  112. from .pagerange import PageRange, PageRangeSpec
  113. from .types import (
  114. AnnotationSubtype,
  115. BorderArrayType,
  116. LayoutType,
  117. OutlineItemType,
  118. OutlineType,
  119. PagemodeType,
  120. )
  121. from .xmp import XmpInformation
  122. ALL_DOCUMENT_PERMISSIONS = UserAccessPermissions.all()
  123. class ObjectDeletionFlag(enum.IntFlag):
  124. NONE = 0
  125. TEXT = enum.auto()
  126. LINKS = enum.auto()
  127. ATTACHMENTS = enum.auto()
  128. OBJECTS_3D = enum.auto()
  129. ALL_ANNOTATIONS = enum.auto()
  130. XOBJECT_IMAGES = enum.auto()
  131. INLINE_IMAGES = enum.auto()
  132. DRAWING_IMAGES = enum.auto()
  133. IMAGES = XOBJECT_IMAGES | INLINE_IMAGES | DRAWING_IMAGES
  134. def _rolling_checksum(stream: BytesIO, blocksize: int = 65536) -> str:
  135. hash = hashlib.md5(usedforsecurity=False)
  136. for block in iter(lambda: stream.read(blocksize), b""):
  137. hash.update(block)
  138. return hash.hexdigest()
  139. class PdfWriter(PdfDocCommon):
  140. """
  141. Write a PDF file out, given pages produced by another class or through
  142. cloning a PDF file during initialization.
  143. Typically data is added from a :class:`PdfReader<pypdf.PdfReader>`.
  144. Args:
  145. clone_from: identical to fileobj (for compatibility)
  146. incremental: If true, loads the document and set the PdfWriter in incremental mode.
  147. When writing incrementally, the original document is written first and new/modified
  148. content is appended. To be used for signed document/forms to keep signature valid.
  149. full: If true, loads all the objects (always full if incremental = True).
  150. This parameter may allow loading large PDFs.
  151. strict: If true, pypdf will raise an exception if a PDF does not follow the specification.
  152. If false, pypdf will try to be forgiving and do something reasonable, but it will log
  153. a warning message. It is a best-effort approach.
  154. """
  155. def __init__(
  156. self,
  157. fileobj: Union[None, PdfReader, StrByteType, Path] = "",
  158. clone_from: Union[None, PdfReader, StrByteType, Path] = None,
  159. incremental: bool = False,
  160. full: bool = False,
  161. strict: bool = False,
  162. ) -> None:
  163. self.strict = strict
  164. """
  165. If true, pypdf will raise an exception if a PDF does not follow the specification.
  166. If false, pypdf will try to be forgiving and do something reasonable, but it will log
  167. a warning message. It is a best-effort approach.
  168. """
  169. self.incremental = incremental or full
  170. """
  171. Returns if the PdfWriter object has been started in incremental mode.
  172. """
  173. self._objects: list[Optional[PdfObject]] = []
  174. """
  175. The indirect objects in the PDF.
  176. For the incremental case, it will be filled with None
  177. in clone_reader_document_root.
  178. """
  179. self._original_hash: list[int] = []
  180. """
  181. List of hashes after import; used to identify changes.
  182. """
  183. self._idnum_hash: dict[bytes, tuple[IndirectObject, list[IndirectObject]]] = {}
  184. """
  185. Maps hash values of indirect objects to the list of IndirectObjects.
  186. This is used for compression.
  187. """
  188. self._id_translated: dict[int, dict[int, int]] = {}
  189. """List of already translated IDs.
  190. dict[id(pdf)][(idnum, generation)]
  191. """
  192. self._info_obj: Optional[PdfObject]
  193. """The PDF files's document information dictionary,
  194. defined by Info in the PDF file's trailer dictionary."""
  195. self._ID: Union[ArrayObject, None] = None
  196. """The PDF file identifier,
  197. defined by the ID in the PDF file's trailer dictionary."""
  198. self._unresolved_links: list[tuple[ReferenceLink, ReferenceLink]] = []
  199. "Tracks links in pages added to the writer for resolving later."
  200. self._merged_in_pages: dict[Optional[IndirectObject], Optional[IndirectObject]] = {}
  201. "Tracks pages added to the writer and what page they turned into."
  202. if self.incremental:
  203. if isinstance(fileobj, (str, Path)):
  204. with open(fileobj, "rb") as f:
  205. fileobj = BytesIO(f.read(-1))
  206. if isinstance(fileobj, BytesIO):
  207. fileobj = PdfReader(fileobj)
  208. if not isinstance(fileobj, PdfReader):
  209. raise PyPdfError("Invalid type for incremental mode")
  210. self._reader = fileobj # prev content is in _reader.stream
  211. self._header = fileobj.pdf_header.encode()
  212. self._readonly = True # TODO: to be analysed
  213. else:
  214. self._header = b"%PDF-1.3"
  215. self._info_obj = self._add_object(
  216. DictionaryObject(
  217. {NameObject("/Producer"): create_string_object("pypdf")}
  218. )
  219. )
  220. def _get_clone_from(
  221. fileobj: Union[None, PdfReader, str, Path, IO[Any], BytesIO],
  222. clone_from: Union[None, PdfReader, str, Path, IO[Any], BytesIO],
  223. ) -> Union[None, PdfReader, str, Path, IO[Any], BytesIO]:
  224. if isinstance(fileobj, (str, Path, IO, BytesIO)) and (
  225. fileobj == "" or clone_from is not None
  226. ):
  227. return clone_from
  228. cloning = True
  229. if isinstance(fileobj, (str, Path)) and (
  230. not Path(str(fileobj)).exists()
  231. or Path(str(fileobj)).stat().st_size == 0
  232. ):
  233. cloning = False
  234. if isinstance(fileobj, (IOBase, BytesIO)):
  235. t = fileobj.tell()
  236. if fileobj.seek(0, 2) == 0:
  237. cloning = False
  238. fileobj.seek(t, 0)
  239. if cloning:
  240. clone_from = fileobj
  241. return clone_from
  242. clone_from = _get_clone_from(fileobj, clone_from)
  243. # To prevent overwriting
  244. self.temp_fileobj = fileobj
  245. self.fileobj = ""
  246. self._with_as_usage = False
  247. self._cloned = False
  248. # The root of our page tree node
  249. pages = DictionaryObject(
  250. {
  251. NameObject(PagesAttributes.TYPE): NameObject("/Pages"),
  252. NameObject(PagesAttributes.COUNT): NumberObject(0),
  253. NameObject(PagesAttributes.KIDS): ArrayObject(),
  254. }
  255. )
  256. self.flattened_pages = []
  257. self._encryption: Optional[Encryption] = None
  258. self._encrypt_entry: Optional[DictionaryObject] = None
  259. if clone_from is not None:
  260. if not isinstance(clone_from, PdfReader):
  261. clone_from = PdfReader(clone_from)
  262. self.clone_document_from_reader(clone_from)
  263. self._cloned = True
  264. else:
  265. self._pages = self._add_object(pages)
  266. self._root_object = DictionaryObject(
  267. {
  268. NameObject(PagesAttributes.TYPE): NameObject(CO.CATALOG),
  269. NameObject(CO.PAGES): self._pages,
  270. }
  271. )
  272. self._add_object(self._root_object)
  273. if full and not incremental:
  274. self.incremental = False
  275. if isinstance(self._ID, list):
  276. if isinstance(self._ID[0], TextStringObject):
  277. self._ID[0] = ByteStringObject(self._ID[0].get_original_bytes())
  278. if isinstance(self._ID[1], TextStringObject):
  279. self._ID[1] = ByteStringObject(self._ID[1].get_original_bytes())
  280. # for commonality
  281. @property
  282. def is_encrypted(self) -> bool:
  283. """
  284. Read-only boolean property showing whether this PDF file is encrypted.
  285. Note that this property, if true, will remain true even after the
  286. :meth:`decrypt()<pypdf.PdfReader.decrypt>` method is called.
  287. """
  288. return False
  289. @property
  290. def root_object(self) -> DictionaryObject:
  291. """
  292. Provide direct access to PDF Structure.
  293. Note:
  294. Recommended only for read access.
  295. """
  296. return self._root_object
  297. @property
  298. def _info(self) -> Optional[DictionaryObject]:
  299. """
  300. Provide access to "/Info". Standardized with PdfReader.
  301. Returns:
  302. /Info Dictionary; None if the entry does not exist
  303. """
  304. return (
  305. None
  306. if self._info_obj is None
  307. else cast(DictionaryObject, self._info_obj.get_object())
  308. )
  309. @_info.setter
  310. def _info(self, value: Optional[Union[IndirectObject, DictionaryObject]]) -> None:
  311. if value is None:
  312. try:
  313. self._objects[self._info_obj.indirect_reference.idnum - 1] = None # type: ignore
  314. except (KeyError, AttributeError):
  315. pass
  316. self._info_obj = None
  317. else:
  318. if self._info_obj is None:
  319. self._info_obj = self._add_object(DictionaryObject())
  320. obj = cast(DictionaryObject, self._info_obj.get_object())
  321. obj.clear()
  322. obj.update(cast(DictionaryObject, value.get_object()))
  323. @property
  324. def xmp_metadata(self) -> Optional[XmpInformation]:
  325. """XMP (Extensible Metadata Platform) data."""
  326. return cast(XmpInformation, self.root_object.xmp_metadata)
  327. @xmp_metadata.setter
  328. def xmp_metadata(self, value: Union[XmpInformation, bytes, None]) -> None:
  329. """XMP (Extensible Metadata Platform) data."""
  330. if value is None:
  331. if "/Metadata" in self.root_object:
  332. del self.root_object["/Metadata"]
  333. return
  334. metadata = self.root_object.get("/Metadata", None)
  335. if not isinstance(metadata, IndirectObject):
  336. if metadata is not None:
  337. del self.root_object["/Metadata"]
  338. metadata_stream = StreamObject()
  339. stream_reference = self._add_object(metadata_stream)
  340. self.root_object[NameObject("/Metadata")] = stream_reference
  341. else:
  342. metadata_stream = cast(StreamObject, metadata.get_object())
  343. if isinstance(value, XmpInformation):
  344. bytes_data = value.stream.get_data()
  345. else:
  346. bytes_data = value
  347. metadata_stream.set_data(bytes_data)
  348. @property
  349. def with_as_usage(self) -> bool:
  350. deprecation_no_replacement("with_as_usage", "5.0")
  351. return self._with_as_usage
  352. @with_as_usage.setter
  353. def with_as_usage(self, value: bool) -> None:
  354. deprecation_no_replacement("with_as_usage", "5.0")
  355. self._with_as_usage = value
  356. def __enter__(self) -> Self:
  357. """Store how writer is initialized by 'with'."""
  358. c: bool = self._cloned
  359. t = self.temp_fileobj
  360. self.__init__() # type: ignore
  361. self._cloned = c
  362. self._with_as_usage = True
  363. self.fileobj = t # type: ignore
  364. return self
  365. def __exit__(
  366. self,
  367. exc_type: Optional[type[BaseException]],
  368. exc: Optional[BaseException],
  369. traceback: Optional[TracebackType],
  370. ) -> None:
  371. """Write data to the fileobj."""
  372. if self.fileobj and not self._cloned:
  373. self.write(self.fileobj)
  374. @property
  375. def pdf_header(self) -> str:
  376. """
  377. Read/Write property of the PDF header that is written.
  378. This should be something like ``'%PDF-1.5'``. It is recommended to set
  379. the lowest version that supports all features which are used within the
  380. PDF file.
  381. Note: `pdf_header` returns a string but accepts bytes or str for writing
  382. """
  383. return self._header.decode()
  384. @pdf_header.setter
  385. def pdf_header(self, new_header: Union[str, bytes]) -> None:
  386. if isinstance(new_header, str):
  387. new_header = new_header.encode()
  388. self._header = new_header
  389. def _add_object(self, obj: PdfObject) -> IndirectObject:
  390. if (
  391. getattr(obj, "indirect_reference", None) is not None
  392. and obj.indirect_reference.pdf == self # type: ignore
  393. ):
  394. return obj.indirect_reference # type: ignore
  395. # check for /Contents in Pages (/Contents in annotations are strings)
  396. if isinstance(obj, DictionaryObject) and isinstance(
  397. obj.get(PG.CONTENTS, None), (ArrayObject, DictionaryObject)
  398. ):
  399. obj[NameObject(PG.CONTENTS)] = self._add_object(obj[PG.CONTENTS])
  400. self._objects.append(obj)
  401. obj.indirect_reference = IndirectObject(len(self._objects), 0, self)
  402. return obj.indirect_reference
  403. def get_object(
  404. self,
  405. indirect_reference: Union[int, IndirectObject],
  406. ) -> PdfObject:
  407. if isinstance(indirect_reference, int):
  408. obj = self._objects[indirect_reference - 1]
  409. elif indirect_reference.pdf != self:
  410. raise ValueError("PDF must be self")
  411. else:
  412. obj = self._objects[indirect_reference.idnum - 1]
  413. assert obj is not None, "mypy"
  414. return obj
  415. def _replace_object(
  416. self,
  417. indirect_reference: Union[int, IndirectObject],
  418. obj: PdfObject,
  419. ) -> PdfObject:
  420. if isinstance(indirect_reference, IndirectObject):
  421. if indirect_reference.pdf != self:
  422. raise ValueError("PDF must be self")
  423. indirect_reference = indirect_reference.idnum
  424. gen = self._objects[indirect_reference - 1].indirect_reference.generation # type: ignore
  425. if (
  426. getattr(obj, "indirect_reference", None) is not None
  427. and obj.indirect_reference.pdf != self # type: ignore
  428. ):
  429. obj = obj.clone(self)
  430. self._objects[indirect_reference - 1] = obj
  431. obj.indirect_reference = IndirectObject(indirect_reference, gen, self)
  432. assert isinstance(obj, PdfObject), "mypy"
  433. return obj
  434. def _add_page(
  435. self,
  436. page: PageObject,
  437. index: int,
  438. excluded_keys: Iterable[str] = (),
  439. ) -> PageObject:
  440. if not isinstance(page, PageObject) or page.get(PagesAttributes.TYPE, None) != CO.PAGE:
  441. raise ValueError("Invalid page object")
  442. assert self.flattened_pages is not None, "for mypy"
  443. page_org = page
  444. excluded_keys = list(excluded_keys)
  445. excluded_keys += [PagesAttributes.PARENT, "/StructParents"]
  446. # Acrobat does not accept two indirect references pointing on the same
  447. # page; therefore in order to add multiple copies of the same
  448. # page, we need to create a new dictionary for the page, however the
  449. # objects below (including content) are not duplicated:
  450. try: # delete an already existing page
  451. del self._id_translated[id(page_org.indirect_reference.pdf)][ # type: ignore
  452. page_org.indirect_reference.idnum # type: ignore
  453. ]
  454. except Exception:
  455. pass
  456. page = cast(
  457. "PageObject", page_org.clone(self, False, excluded_keys).get_object()
  458. )
  459. if page_org.pdf is not None:
  460. other = page_org.pdf.pdf_header
  461. self.pdf_header = _get_max_pdf_version_header(self.pdf_header, other)
  462. node, idx = self._get_page_in_node(index)
  463. page[NameObject(PagesAttributes.PARENT)] = node.indirect_reference
  464. if idx >= 0:
  465. cast(ArrayObject, node[PagesAttributes.KIDS]).insert(idx, page.indirect_reference)
  466. self.flattened_pages.insert(index, page)
  467. else:
  468. cast(ArrayObject, node[PagesAttributes.KIDS]).append(page.indirect_reference)
  469. self.flattened_pages.append(page)
  470. recurse = 0
  471. while not is_null_or_none(node):
  472. node = cast(DictionaryObject, node.get_object())
  473. node[NameObject(PagesAttributes.COUNT)] = NumberObject(cast(int, node[PagesAttributes.COUNT]) + 1)
  474. node = node.get(PagesAttributes.PARENT, None) # type: ignore[assignment] # TODO: Fix.
  475. recurse += 1
  476. if recurse > 1000:
  477. raise PyPdfError("Too many recursive calls!")
  478. if page_org.pdf is not None:
  479. # the page may contain links to other pages, and those other
  480. # pages may or may not already be added. we store the
  481. # information we need, so that we can resolve the references
  482. # later.
  483. self._unresolved_links.extend(extract_links(page, page_org))
  484. self._merged_in_pages[page_org.indirect_reference] = page.indirect_reference
  485. return page
  486. def set_need_appearances_writer(self, state: bool = True) -> None:
  487. """
  488. Sets the "NeedAppearances" flag in the PDF writer.
  489. The "NeedAppearances" flag indicates whether the appearance dictionary
  490. for form fields should be automatically generated by the PDF viewer or
  491. if the embedded appearance should be used.
  492. Args:
  493. state: The actual value of the NeedAppearances flag.
  494. Returns:
  495. None
  496. """
  497. # See §12.7.2 and §7.7.2 for more information:
  498. # https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf
  499. try:
  500. # get the AcroForm tree
  501. if CatalogDictionary.ACRO_FORM not in self._root_object:
  502. self._root_object[
  503. NameObject(CatalogDictionary.ACRO_FORM)
  504. ] = self._add_object(DictionaryObject())
  505. need_appearances = NameObject(InteractiveFormDictEntries.NeedAppearances)
  506. cast(DictionaryObject, self._root_object[CatalogDictionary.ACRO_FORM])[
  507. need_appearances
  508. ] = BooleanObject(state)
  509. except Exception as exc: # pragma: no cover
  510. logger_warning(
  511. f"set_need_appearances_writer({state}) catch : {exc}", __name__
  512. )
  513. def create_viewer_preferences(self) -> ViewerPreferences:
  514. o = ViewerPreferences()
  515. self._root_object[
  516. NameObject(CatalogDictionary.VIEWER_PREFERENCES)
  517. ] = self._add_object(o)
  518. return o
  519. def add_page(
  520. self,
  521. page: PageObject,
  522. excluded_keys: Iterable[str] = (),
  523. ) -> PageObject:
  524. """
  525. Add a page to this PDF file.
  526. Recommended for advanced usage including the adequate excluded_keys.
  527. The page is usually acquired from a :class:`PdfReader<pypdf.PdfReader>`
  528. instance.
  529. Args:
  530. page: The page to add to the document. Should be
  531. an instance of :class:`PageObject<pypdf._page.PageObject>`
  532. excluded_keys:
  533. Returns:
  534. The added PageObject.
  535. """
  536. assert self.flattened_pages is not None, "mypy"
  537. return self._add_page(page, len(self.flattened_pages), excluded_keys)
  538. def insert_page(
  539. self,
  540. page: PageObject,
  541. index: int = 0,
  542. excluded_keys: Iterable[str] = (),
  543. ) -> PageObject:
  544. """
  545. Insert a page in this PDF file. The page is usually acquired from a
  546. :class:`PdfReader<pypdf.PdfReader>` instance.
  547. Args:
  548. page: The page to add to the document.
  549. index: Position at which the page will be inserted.
  550. excluded_keys:
  551. Returns:
  552. The added PageObject.
  553. """
  554. assert self.flattened_pages is not None, "mypy"
  555. if index < 0:
  556. index += len(self.flattened_pages)
  557. if index < 0:
  558. raise ValueError("Invalid index value")
  559. if index >= len(self.flattened_pages):
  560. return self.add_page(page, excluded_keys)
  561. return self._add_page(page, index, excluded_keys)
  562. def _get_page_number_by_indirect(
  563. self, indirect_reference: Union[None, int, NullObject, IndirectObject]
  564. ) -> Optional[int]:
  565. """
  566. Generate _page_id2num.
  567. Args:
  568. indirect_reference:
  569. Returns:
  570. The page number or None
  571. """
  572. # To provide same function as in PdfReader
  573. if is_null_or_none(indirect_reference):
  574. return None
  575. assert indirect_reference is not None, "mypy"
  576. if isinstance(indirect_reference, int):
  577. indirect_reference = IndirectObject(indirect_reference, 0, self)
  578. obj = indirect_reference.get_object()
  579. if isinstance(obj, PageObject):
  580. return obj.page_number
  581. return None
  582. def add_blank_page(
  583. self, width: Optional[float] = None, height: Optional[float] = None
  584. ) -> PageObject:
  585. """
  586. Append a blank page to this PDF file and return it.
  587. If no page size is specified, use the size of the last page.
  588. Args:
  589. width: The width of the new page expressed in default user
  590. space units.
  591. height: The height of the new page expressed in default
  592. user space units.
  593. Returns:
  594. The newly appended page.
  595. Raises:
  596. PageSizeNotDefinedError: if width and height are not defined
  597. and previous page does not exist.
  598. """
  599. page = PageObject.create_blank_page(self, width, height)
  600. return self.add_page(page)
  601. def insert_blank_page(
  602. self,
  603. width: Optional[Union[float, decimal.Decimal]] = None,
  604. height: Optional[Union[float, decimal.Decimal]] = None,
  605. index: int = 0,
  606. ) -> PageObject:
  607. """
  608. Insert a blank page to this PDF file and return it.
  609. If no page size is specified for a dimension, use the size of the last page.
  610. Args:
  611. width: The width of the new page in default user space units.
  612. height: The height of the new page in default user space units.
  613. index: Position to add the page.
  614. Returns:
  615. The newly inserted page.
  616. Raises:
  617. PageSizeNotDefinedError: if width and height are not defined
  618. and previous page does not exist.
  619. IndexError: Index is outside of [-self.get_num_pages(), self.get_num_pages()]
  620. """
  621. num_pages = self.get_num_pages()
  622. if abs(index) <= num_pages:
  623. # Use the chosen index, but do not exceed the available pages
  624. fixed_index = min(index, num_pages - 1)
  625. mediabox = self.pages[fixed_index].mediabox
  626. if width is None or width <= 0:
  627. width = mediabox.width
  628. if height is None or height <= 0:
  629. height = mediabox.height
  630. else:
  631. raise IndexError(f"Index should be in range [-{num_pages}, {num_pages}]")
  632. page = PageObject.create_blank_page(self, width, height)
  633. self.insert_page(page, index)
  634. return page
  635. @property
  636. def open_destination(
  637. self,
  638. ) -> Union[None, Destination, TextStringObject, ByteStringObject]:
  639. return super().open_destination
  640. @open_destination.setter
  641. def open_destination(self, dest: Union[None, str, Destination, PageObject]) -> None:
  642. if dest is None:
  643. try:
  644. del self._root_object["/OpenAction"]
  645. except KeyError:
  646. pass
  647. elif isinstance(dest, str):
  648. self._root_object[NameObject("/OpenAction")] = TextStringObject(dest)
  649. elif isinstance(dest, Destination):
  650. self._root_object[NameObject("/OpenAction")] = dest.dest_array
  651. elif isinstance(dest, PageObject):
  652. self._root_object[NameObject("/OpenAction")] = Destination(
  653. "Opening",
  654. dest.indirect_reference
  655. if dest.indirect_reference is not None
  656. else NullObject(),
  657. PAGE_FIT,
  658. ).dest_array
  659. def add_js(self, javascript: str) -> None:
  660. """
  661. Add JavaScript which will launch upon opening this PDF.
  662. Args:
  663. javascript: Your JavaScript.
  664. Example:
  665. This will launch the print window when the PDF is opened.
  666. >>> from pypdf import PdfWriter
  667. >>> output = PdfWriter()
  668. >>> output.add_js("this.print({bUI:true,bSilent:false,bShrinkToFit:true});")
  669. """
  670. # Names / JavaScript preferred to be able to add multiple scripts
  671. if "/Names" not in self._root_object:
  672. self._root_object[NameObject(CA.NAMES)] = DictionaryObject()
  673. names = cast(DictionaryObject, self._root_object[CA.NAMES])
  674. if "/JavaScript" not in names:
  675. names[NameObject("/JavaScript")] = DictionaryObject(
  676. {NameObject("/Names"): ArrayObject()}
  677. )
  678. js_list = cast(
  679. ArrayObject, cast(DictionaryObject, names["/JavaScript"])["/Names"]
  680. )
  681. # We need a name for parameterized JavaScript in the PDF file,
  682. # but it can be anything.
  683. js_list.append(create_string_object(str(uuid.uuid4())))
  684. js = DictionaryObject(
  685. {
  686. NameObject(PagesAttributes.TYPE): NameObject("/Action"),
  687. NameObject("/S"): NameObject("/JavaScript"),
  688. NameObject("/JS"): TextStringObject(f"{javascript}"),
  689. }
  690. )
  691. js_list.append(self._add_object(js))
  692. def add_attachment(self, filename: str, data: Union[str, bytes]) -> "EmbeddedFile":
  693. """
  694. Embed a file inside the PDF.
  695. Reference:
  696. https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf
  697. Section 7.11.3
  698. Args:
  699. filename: The filename to display.
  700. data: The data in the file.
  701. Returns:
  702. EmbeddedFile instance for the newly created embedded file.
  703. """
  704. return EmbeddedFile._create_new(self, filename, data)
  705. def append_pages_from_reader(
  706. self,
  707. reader: PdfReader,
  708. after_page_append: Optional[Callable[[PageObject], None]] = None,
  709. ) -> None:
  710. """
  711. Copy pages from reader to writer. Includes an optional callback
  712. parameter which is invoked after pages are appended to the writer.
  713. ``append`` should be preferred.
  714. Args:
  715. reader: a PdfReader object from which to copy page
  716. annotations to this writer object. The writer's annots
  717. will then be updated.
  718. after_page_append:
  719. Callback function that is invoked after each page is appended to
  720. the writer. Signature includes a reference to the appended page
  721. (delegates to append_pages_from_reader). The single parameter of
  722. the callback is a reference to the page just appended to the
  723. document.
  724. """
  725. reader_num_pages = len(reader.pages)
  726. # Copy pages from reader to writer
  727. for reader_page_number in range(reader_num_pages):
  728. reader_page = reader.pages[reader_page_number]
  729. writer_page = self.add_page(reader_page)
  730. # Trigger callback, pass writer page as parameter
  731. if callable(after_page_append):
  732. after_page_append(writer_page)
  733. def _merge_content_stream_to_page(
  734. self,
  735. page: PageObject,
  736. new_content_data: bytes,
  737. ) -> None:
  738. """
  739. Combines existing content stream(s) with new content (as bytes).
  740. Args:
  741. page: The page to which the new content data will be added.
  742. new_content_data: A binary-encoded new content stream, for
  743. instance the commands to draw an XObject.
  744. """
  745. # First resolve the existing page content. This always is an IndirectObject:
  746. # PDF Explained by John Whitington
  747. # https://www.oreilly.com/library/view/pdf-explained/9781449321581/ch04.html
  748. if NameObject("/Contents") in page:
  749. existing_content_ref = page[NameObject("/Contents")]
  750. existing_content = existing_content_ref.get_object()
  751. if isinstance(existing_content, ArrayObject):
  752. # Create a new StreamObject for the new_content_data
  753. new_stream_obj = StreamObject()
  754. new_stream_obj.set_data(new_content_data)
  755. existing_content.append(self._add_object(new_stream_obj))
  756. page[NameObject("/Contents")] = self._add_object(existing_content)
  757. if isinstance(existing_content, StreamObject):
  758. # Merge new content to existing StreamObject
  759. merged_data = existing_content.get_data() + b"\n" + new_content_data
  760. new_stream = StreamObject()
  761. new_stream.set_data(merged_data)
  762. page[NameObject("/Contents")] = self._add_object(new_stream)
  763. else:
  764. # If no existing content, then we have an empty page.
  765. # Create a new StreamObject in a new /Contents entry.
  766. new_stream = StreamObject()
  767. new_stream.set_data(new_content_data)
  768. page[NameObject("/Contents")] = self._add_object(new_stream)
  769. def _add_apstream_object(
  770. self,
  771. page: PageObject,
  772. appearance_stream_obj: StreamObject,
  773. object_name: str,
  774. x_offset: float,
  775. y_offset: float,
  776. ) -> None:
  777. """
  778. Adds an appearance stream to the page content in the form of
  779. an XObject.
  780. Args:
  781. page: The page to which to add the appearance stream.
  782. appearance_stream_obj: The appearance stream.
  783. object_name: The name of the appearance stream.
  784. x_offset: The horizontal offset for the appearance stream.
  785. y_offset: The vertical offset for the appearance stream.
  786. """
  787. # Prepare XObject resource dictionary on the page. This currently
  788. # only deals with font resources, but can easily be adapted to also
  789. # include other resources.
  790. pg_res = cast(DictionaryObject, page[PG.RESOURCES])
  791. if "/Resources" in appearance_stream_obj:
  792. ap_stream_res = cast(DictionaryObject, appearance_stream_obj["/Resources"])
  793. ap_stream_font_dict = cast(DictionaryObject, ap_stream_res.get("/Font", DictionaryObject()))
  794. if "/Font" not in pg_res:
  795. font_dict_ref = self._add_object(DictionaryObject())
  796. pg_res[NameObject("/Font")] = font_dict_ref
  797. pg_font_res = cast(DictionaryObject, pg_res["/Font"].get_object())
  798. # Merge fonts from the appearance stream into the page's font resources
  799. for font_name, font_res in ap_stream_font_dict.items():
  800. if font_name not in pg_font_res:
  801. font_res_ref = self._add_object(font_res)
  802. pg_font_res[font_name] = font_res_ref
  803. # Always add the resolved stream object to the writer to get a new IndirectObject.
  804. # This ensures we have a valid IndirectObject managed by *this* writer.
  805. xobject_ref = self._add_object(appearance_stream_obj)
  806. xobject_name = NameObject(f"/Fm_{object_name}")._sanitize()
  807. if "/XObject" not in pg_res:
  808. pg_res[NameObject("/XObject")] = DictionaryObject()
  809. pg_xo_res = cast(DictionaryObject, pg_res["/XObject"])
  810. if xobject_name not in pg_xo_res:
  811. pg_xo_res[xobject_name] = xobject_ref
  812. else:
  813. logger_warning(
  814. f"XObject {xobject_name!r} already added to page resources. This might be an issue.",
  815. __name__
  816. )
  817. xobject_cm = Transformation().translate(x_offset, y_offset)
  818. xobject_drawing_commands = f"q\n{xobject_cm._to_cm()}\n{xobject_name} Do\nQ".encode()
  819. self._merge_content_stream_to_page(page, xobject_drawing_commands)
  820. FFBITS_NUL = FA.FfBits(0)
  821. def update_page_form_field_values(
  822. self,
  823. page: Union[PageObject, list[PageObject], None],
  824. fields: Mapping[str, Union[str, list[str], tuple[str, str, float]]],
  825. flags: FA.FfBits = FFBITS_NUL,
  826. auto_regenerate: Optional[bool] = True,
  827. flatten: bool = False,
  828. ) -> None:
  829. """
  830. Update the form field values for a given page from a fields dictionary.
  831. Copy field texts and values from fields to page.
  832. If the field links to a parent object, add the information to the parent.
  833. Args:
  834. page: `PageObject` - references **PDF writer's page** where the
  835. annotations and field data will be updated.
  836. `List[Pageobject]` - provides list of pages to be processed.
  837. `None` - all pages.
  838. fields: a Python dictionary of:
  839. * field names (/T) as keys and text values (/V) as value
  840. * field names (/T) as keys and list of text values (/V) for multiple choice list
  841. * field names (/T) as keys and tuple of:
  842. * text values (/V)
  843. * font id (e.g. /F1, the font id must exist)
  844. * font size (0 for autosize)
  845. flags: A set of flags from :class:`~pypdf.constants.FieldDictionaryAttributes.FfBits`.
  846. auto_regenerate: Set/unset the need_appearances flag;
  847. the flag is unchanged if auto_regenerate is None.
  848. flatten: Whether or not to flatten the annotation. If True, this adds the annotation's
  849. appearance stream to the page contents. Note that this option does not remove the
  850. annotation itself.
  851. """
  852. if CatalogDictionary.ACRO_FORM not in self._root_object:
  853. raise PyPdfError("No /AcroForm dictionary in PDF of PdfWriter Object")
  854. acro_form = cast(DictionaryObject, self._root_object[CatalogDictionary.ACRO_FORM])
  855. if InteractiveFormDictEntries.Fields not in acro_form:
  856. raise PyPdfError("No /Fields dictionary in PDF of PdfWriter Object")
  857. if isinstance(auto_regenerate, bool):
  858. self.set_need_appearances_writer(auto_regenerate)
  859. # Iterate through pages, update field values
  860. if page is None:
  861. page = list(self.pages)
  862. if isinstance(page, list):
  863. for p in page:
  864. if PG.ANNOTS in p: # just to prevent warnings
  865. self.update_page_form_field_values(p, fields, flags, None, flatten=flatten)
  866. return
  867. if PG.ANNOTS not in page:
  868. logger_warning("No fields to update on this page", __name__)
  869. return
  870. appearance_stream_obj: Optional[StreamObject] = None
  871. for annotation in page[PG.ANNOTS]: # type: ignore
  872. annotation = cast(DictionaryObject, annotation.get_object())
  873. if annotation.get("/Subtype", "") != "/Widget":
  874. continue
  875. if "/FT" in annotation and "/T" in annotation:
  876. parent_annotation = annotation
  877. else:
  878. parent_annotation = annotation.get(
  879. PG.PARENT, DictionaryObject()
  880. ).get_object()
  881. for field, value in fields.items():
  882. rectangle = cast(RectangleObject, annotation[AA.Rect])
  883. if not (
  884. self._get_qualified_field_name(parent_annotation) == field
  885. or parent_annotation.get("/T", None) == field
  886. ):
  887. continue
  888. if (
  889. parent_annotation.get("/FT", None) == "/Ch"
  890. and "/I" in parent_annotation
  891. ):
  892. del parent_annotation["/I"]
  893. if flags:
  894. annotation[NameObject(FA.Ff)] = NumberObject(flags)
  895. # Set the field value
  896. if not (value is None and flatten): # Only change values if given by user and not flattening.
  897. if isinstance(value, list):
  898. lst = ArrayObject(TextStringObject(v) for v in value)
  899. parent_annotation[NameObject(FA.V)] = lst
  900. elif isinstance(value, tuple):
  901. annotation[NameObject(FA.V)] = TextStringObject(
  902. value[0],
  903. )
  904. else:
  905. parent_annotation[NameObject(FA.V)] = TextStringObject(value)
  906. # Get or create the field's appearance stream object
  907. if parent_annotation.get(FA.FT) == "/Btn":
  908. # Checkbox button (no /FT found in Radio widgets);
  909. # We can find the associated appearance stream object
  910. # within the annotation.
  911. v = NameObject(value)
  912. ap = cast(DictionaryObject, annotation[NameObject(AA.AP)])
  913. normal_ap = cast(DictionaryObject, ap["/N"])
  914. if v not in normal_ap:
  915. v = NameObject("/Off")
  916. appearance_stream_obj = normal_ap.get(v)
  917. # Other cases will be updated through the for loop
  918. annotation[NameObject(AA.AS)] = v
  919. annotation[NameObject(FA.V)] = v
  920. elif (
  921. parent_annotation.get(FA.FT) == "/Tx"
  922. or parent_annotation.get(FA.FT) == "/Ch"
  923. ):
  924. # Textbox; we need to generate the appearance stream object
  925. if isinstance(value, tuple):
  926. appearance_stream_obj = TextStreamAppearance.from_text_annotation(
  927. acro_form, parent_annotation, annotation, value[1], value[2]
  928. )
  929. else:
  930. appearance_stream_obj = TextStreamAppearance.from_text_annotation(
  931. acro_form, parent_annotation, annotation
  932. )
  933. # Add the appearance stream object
  934. if AA.AP not in annotation:
  935. annotation[NameObject(AA.AP)] = DictionaryObject(
  936. {NameObject("/N"): self._add_object(appearance_stream_obj)}
  937. )
  938. elif "/N" not in (ap:= cast(DictionaryObject, annotation[AA.AP])):
  939. cast(DictionaryObject, annotation[NameObject(AA.AP)])[
  940. NameObject("/N")
  941. ] = self._add_object(appearance_stream_obj)
  942. else: # [/AP][/N] exists
  943. n = annotation[AA.AP]["/N"].indirect_reference.idnum # type: ignore
  944. self._objects[n - 1] = appearance_stream_obj
  945. appearance_stream_obj.indirect_reference = IndirectObject(n, 0, self)
  946. elif (
  947. annotation.get(FA.FT) == "/Sig"
  948. ): # deprecated # not implemented yet
  949. logger_warning("Signature forms not implemented yet", __name__)
  950. if flatten and appearance_stream_obj is not None:
  951. self._add_apstream_object(page, appearance_stream_obj, field, rectangle[0], rectangle[1])
  952. def reattach_fields(
  953. self, page: Optional[PageObject] = None
  954. ) -> list[DictionaryObject]:
  955. """
  956. Parse annotations within the page looking for orphan fields and
  957. reattach then into the Fields Structure.
  958. Args:
  959. page: page to analyze.
  960. If none is provided, all pages will be analyzed.
  961. Returns:
  962. list of reattached fields.
  963. """
  964. lst = []
  965. if page is None:
  966. for p in self.pages:
  967. lst += self.reattach_fields(p)
  968. return lst
  969. try:
  970. af = cast(DictionaryObject, self._root_object[CatalogDictionary.ACRO_FORM])
  971. except KeyError:
  972. af = DictionaryObject()
  973. self._root_object[NameObject(CatalogDictionary.ACRO_FORM)] = af
  974. try:
  975. fields = cast(ArrayObject, af[InteractiveFormDictEntries.Fields])
  976. except KeyError:
  977. fields = ArrayObject()
  978. af[NameObject(InteractiveFormDictEntries.Fields)] = fields
  979. if "/Annots" not in page:
  980. return lst
  981. annotations = cast(ArrayObject, page["/Annots"])
  982. for idx, annotation in enumerate(annotations):
  983. is_indirect = isinstance(annotation, IndirectObject)
  984. annotation = cast(DictionaryObject, annotation.get_object())
  985. if annotation.get("/Subtype", "") == "/Widget" and "/FT" in annotation:
  986. if (
  987. "indirect_reference" in annotation.__dict__
  988. and annotation.indirect_reference in fields
  989. ):
  990. continue
  991. if not is_indirect:
  992. annotations[idx] = self._add_object(annotation)
  993. fields.append(annotation.indirect_reference)
  994. lst.append(annotation)
  995. return lst
  996. def clone_reader_document_root(self, reader: PdfReader) -> None:
  997. """
  998. Copy the reader document root to the writer and all sub-elements,
  999. including pages, threads, outlines,... For partial insertion, ``append``
  1000. should be considered.
  1001. Args:
  1002. reader: PdfReader from which the document root should be copied.
  1003. """
  1004. self._info_obj = None
  1005. if self.incremental:
  1006. self._objects = [None] * (cast(int, reader.trailer["/Size"]) - 1)
  1007. for i in range(len(self._objects)):
  1008. o = reader.get_object(i + 1)
  1009. if o is not None:
  1010. self._objects[i] = o.replicate(self)
  1011. else:
  1012. self._objects.clear()
  1013. self._root_object = reader.root_object.clone(self)
  1014. self._pages = self._root_object.raw_get("/Pages")
  1015. if len(self._objects) > cast(int, reader.trailer["/Size"]):
  1016. if self.strict:
  1017. raise PdfReadError(
  1018. f"Object count {len(self._objects)} exceeds defined trailer size {reader.trailer['/Size']}"
  1019. )
  1020. logger_warning(
  1021. f"Object count {len(self._objects)} exceeds defined trailer size {reader.trailer['/Size']}",
  1022. __name__
  1023. )
  1024. # must be done here before rewriting
  1025. if self.incremental:
  1026. self._original_hash = [
  1027. (obj.hash_bin() if obj is not None else 0) for obj in self._objects
  1028. ]
  1029. try:
  1030. self._flatten()
  1031. except IndexError:
  1032. raise PdfReadError("Got index error while flattening.")
  1033. assert self.flattened_pages is not None
  1034. for p in self.flattened_pages:
  1035. self._replace_object(cast(IndirectObject, p.indirect_reference).idnum, p)
  1036. if not self.incremental:
  1037. p[NameObject("/Parent")] = self._pages
  1038. if not self.incremental:
  1039. cast(DictionaryObject, self._pages.get_object())[
  1040. NameObject("/Kids")
  1041. ] = ArrayObject([p.indirect_reference for p in self.flattened_pages])
  1042. def clone_document_from_reader(
  1043. self,
  1044. reader: PdfReader,
  1045. after_page_append: Optional[Callable[[PageObject], None]] = None,
  1046. ) -> None:
  1047. """
  1048. Create a copy (clone) of a document from a PDF file reader cloning
  1049. section '/Root' and '/Info' and '/ID' of the pdf.
  1050. Args:
  1051. reader: PDF file reader instance from which the clone
  1052. should be created.
  1053. after_page_append:
  1054. Callback function that is invoked after each page is appended to
  1055. the writer. Signature includes a reference to the appended page
  1056. (delegates to append_pages_from_reader). The single parameter of
  1057. the callback is a reference to the page just appended to the
  1058. document.
  1059. """
  1060. self.clone_reader_document_root(reader)
  1061. inf = reader._info
  1062. if self.incremental:
  1063. if inf is not None:
  1064. self._info_obj = cast(
  1065. IndirectObject, inf.clone(self).indirect_reference
  1066. )
  1067. assert isinstance(self._info, DictionaryObject), "for mypy"
  1068. self._original_hash[
  1069. self._info_obj.indirect_reference.idnum - 1
  1070. ] = self._info.hash_bin()
  1071. elif inf is not None:
  1072. self._info_obj = self._add_object(
  1073. DictionaryObject(cast(DictionaryObject, inf.get_object()))
  1074. )
  1075. # else: _info_obj = None done in clone_reader_document_root()
  1076. try:
  1077. self._ID = cast(ArrayObject, reader._ID).clone(self)
  1078. except AttributeError:
  1079. pass
  1080. if callable(after_page_append):
  1081. for page in cast(
  1082. ArrayObject, cast(DictionaryObject, self._pages.get_object())["/Kids"]
  1083. ):
  1084. after_page_append(page.get_object())
  1085. def _compute_document_identifier(self) -> ByteStringObject:
  1086. stream = BytesIO()
  1087. self._write_pdf_structure(stream)
  1088. stream.seek(0)
  1089. return ByteStringObject(_rolling_checksum(stream).encode("utf8"))
  1090. def generate_file_identifiers(self) -> None:
  1091. """
  1092. Generate an identifier for the PDF that will be written.
  1093. The only point of this is ensuring uniqueness. Reproducibility is not
  1094. required.
  1095. When a file is first written, both identifiers shall be set to the same value.
  1096. If both identifiers match when a file reference is resolved, it is very
  1097. likely that the correct and unchanged file has been found. If only the first
  1098. identifier matches, a different version of the correct file has been found.
  1099. see §14.4 "File Identifiers".
  1100. """
  1101. if self._ID:
  1102. id1 = self._ID[0]
  1103. id2 = self._compute_document_identifier()
  1104. else:
  1105. id1 = self._compute_document_identifier()
  1106. id2 = id1
  1107. self._ID = ArrayObject((id1, id2))
  1108. def encrypt(
  1109. self,
  1110. user_password: str,
  1111. owner_password: Optional[str] = None,
  1112. use_128bit: bool = True,
  1113. permissions_flag: UserAccessPermissions = ALL_DOCUMENT_PERMISSIONS,
  1114. *,
  1115. algorithm: Optional[str] = None,
  1116. ) -> None:
  1117. """
  1118. Encrypt this PDF file with the PDF Standard encryption handler.
  1119. Args:
  1120. user_password: The password which allows for opening
  1121. and reading the PDF file with the restrictions provided.
  1122. owner_password: The password which allows for
  1123. opening the PDF files without any restrictions. By default,
  1124. the owner password is the same as the user password.
  1125. use_128bit: flag as to whether to use 128bit
  1126. encryption. When false, 40bit encryption will be used.
  1127. By default, this flag is on.
  1128. permissions_flag: permissions as described in
  1129. Table 3.20 of the PDF 1.7 specification. A bit value of 1 means
  1130. the permission is granted.
  1131. Hence an integer value of -1 will set all flags.
  1132. Bit position 3 is for printing, 4 is for modifying content,
  1133. 5 and 6 control annotations, 9 for form fields,
  1134. 10 for extraction of text and graphics.
  1135. algorithm: encrypt algorithm. Values may be one of "RC4-40", "RC4-128",
  1136. "AES-128", "AES-256-R5", "AES-256". If it is valid,
  1137. `use_128bit` will be ignored.
  1138. """
  1139. if owner_password is None:
  1140. owner_password = user_password
  1141. if algorithm is not None:
  1142. try:
  1143. alg = getattr(EncryptAlgorithm, algorithm.replace("-", "_"))
  1144. except AttributeError:
  1145. raise ValueError(f"Algorithm '{algorithm}' NOT supported")
  1146. else:
  1147. alg = EncryptAlgorithm.RC4_128
  1148. if not use_128bit:
  1149. alg = EncryptAlgorithm.RC4_40
  1150. self.generate_file_identifiers()
  1151. assert self._ID
  1152. self._encryption = Encryption.make(alg, permissions_flag, self._ID[0])
  1153. # in case call `encrypt` again
  1154. entry = self._encryption.write_entry(user_password, owner_password)
  1155. if self._encrypt_entry:
  1156. # replace old encrypt_entry
  1157. assert self._encrypt_entry.indirect_reference is not None
  1158. entry.indirect_reference = self._encrypt_entry.indirect_reference
  1159. self._objects[entry.indirect_reference.idnum - 1] = entry
  1160. else:
  1161. self._add_object(entry)
  1162. self._encrypt_entry = entry
  1163. def _resolve_links(self) -> None:
  1164. """Patch up links that were added to the document earlier, to
  1165. make sure they still point to the same pages.
  1166. """
  1167. for (new_link, old_link) in self._unresolved_links:
  1168. old_page = old_link.find_referenced_page()
  1169. if not old_page:
  1170. continue
  1171. new_page = self._merged_in_pages.get(old_page)
  1172. if new_page is None:
  1173. continue
  1174. new_link.patch_reference(self, new_page)
  1175. def write_stream(self, stream: StreamType) -> None:
  1176. if hasattr(stream, "mode") and "b" not in stream.mode:
  1177. logger_warning(
  1178. f"File <{stream.name}> to write to is not in binary mode. "
  1179. "It may not be written to correctly.",
  1180. __name__,
  1181. )
  1182. self._resolve_links()
  1183. if self.incremental:
  1184. self._reader.stream.seek(0)
  1185. stream.write(self._reader.stream.read(-1))
  1186. if len(self.list_objects_in_increment()) > 0:
  1187. self._write_increment(stream) # writes objs, xref stream and startxref
  1188. else:
  1189. object_positions, free_objects = self._write_pdf_structure(stream)
  1190. xref_location = self._write_xref_table(
  1191. stream, object_positions, free_objects
  1192. )
  1193. self._write_trailer(stream, xref_location)
  1194. def write(self, stream: Union[Path, StrByteType]) -> tuple[bool, IO[Any]]:
  1195. """
  1196. Write the collection of pages added to this object out as a PDF file.
  1197. Args:
  1198. stream: An object to write the file to. The object can support
  1199. the write method and the tell method, similar to a file object, or
  1200. be a file path, just like the fileobj, just named it stream to keep
  1201. existing workflow.
  1202. Returns:
  1203. A tuple (bool, IO).
  1204. """
  1205. my_file = False
  1206. if stream == "":
  1207. raise ValueError(f"Output({stream=}) is empty.")
  1208. if isinstance(stream, (str, Path)):
  1209. stream = FileIO(stream, "wb")
  1210. my_file = True
  1211. self.write_stream(stream)
  1212. if my_file:
  1213. stream.close()
  1214. else:
  1215. stream.flush()
  1216. return my_file, stream
  1217. def list_objects_in_increment(self) -> list[IndirectObject]:
  1218. """
  1219. For analysis or debugging.
  1220. Provides the list of new or modified objects that will be written
  1221. in the increment.
  1222. Deleted objects will not be freed but will become orphans.
  1223. Returns:
  1224. List of new or modified IndirectObjects
  1225. """
  1226. original_hash_count = len(self._original_hash)
  1227. return [
  1228. cast(IndirectObject, obj).indirect_reference
  1229. for i, obj in enumerate(self._objects)
  1230. if (
  1231. obj is not None
  1232. and (
  1233. i >= original_hash_count
  1234. or obj.hash_bin() != self._original_hash[i]
  1235. )
  1236. )
  1237. ]
  1238. def _write_increment(self, stream: StreamType) -> None:
  1239. object_positions = {}
  1240. object_blocks = []
  1241. current_start = -1
  1242. current_stop = -2
  1243. original_hash_count = len(self._original_hash)
  1244. for i, obj in enumerate(self._objects):
  1245. if obj is not None and (
  1246. i >= original_hash_count
  1247. or obj.hash_bin() != self._original_hash[i]
  1248. ):
  1249. idnum = i + 1
  1250. assert isinstance(obj, PdfObject), "mypy"
  1251. # first write new/modified object
  1252. object_positions[idnum] = stream.tell()
  1253. stream.write(f"{idnum} 0 obj\n".encode())
  1254. """ encryption is not operational
  1255. if self._encryption and obj != self._encrypt_entry:
  1256. obj = self._encryption.encrypt_object(obj, idnum, 0)
  1257. """
  1258. obj.write_to_stream(stream)
  1259. stream.write(b"\nendobj\n")
  1260. # prepare xref
  1261. if idnum != current_stop:
  1262. if current_start > 0:
  1263. object_blocks.append(
  1264. [current_start, current_stop - current_start]
  1265. )
  1266. current_start = idnum
  1267. current_stop = idnum + 1
  1268. assert current_start > 0, "for pytest only"
  1269. object_blocks.append([current_start, current_stop - current_start])
  1270. # write incremented xref
  1271. xref_location = stream.tell()
  1272. xr_id = len(self._objects) + 1
  1273. stream.write(f"{xr_id} 0 obj".encode())
  1274. init_data = {
  1275. NameObject("/Type"): NameObject("/XRef"),
  1276. NameObject("/Size"): NumberObject(xr_id + 1),
  1277. NameObject("/Root"): self.root_object.indirect_reference,
  1278. NameObject("/Filter"): NameObject("/FlateDecode"),
  1279. NameObject("/Index"): ArrayObject(
  1280. [NumberObject(_it) for _su in object_blocks for _it in _su]
  1281. ),
  1282. NameObject("/W"): ArrayObject(
  1283. [NumberObject(1), NumberObject(4), NumberObject(1)]
  1284. ),
  1285. "__streamdata__": b"",
  1286. }
  1287. if self._info is not None and (
  1288. self._info.indirect_reference.idnum - 1 # type: ignore
  1289. >= len(self._original_hash)
  1290. or cast(IndirectObject, self._info).hash_bin() # kept for future
  1291. != self._original_hash[
  1292. self._info.indirect_reference.idnum - 1 # type: ignore
  1293. ]
  1294. ):
  1295. init_data[NameObject(TK.INFO)] = self._info.indirect_reference
  1296. init_data[NameObject(TK.PREV)] = NumberObject(self._reader._startxref)
  1297. if self._ID:
  1298. init_data[NameObject(TK.ID)] = self._ID
  1299. xr = StreamObject.initialize_from_dictionary(init_data)
  1300. xr.set_data(
  1301. b"".join(
  1302. [struct.pack(b">BIB", 1, _pos, 0) for _pos in object_positions.values()]
  1303. )
  1304. )
  1305. xr.write_to_stream(stream)
  1306. stream.write(f"\nendobj\nstartxref\n{xref_location}\n%%EOF\n".encode()) # eof
  1307. def _write_pdf_structure(self, stream: StreamType) -> tuple[list[int], list[int]]:
  1308. object_positions = []
  1309. free_objects = []
  1310. stream.write(self.pdf_header.encode() + b"\n")
  1311. stream.write(b"%\xE2\xE3\xCF\xD3\n")
  1312. for idnum, obj in enumerate(self._objects, start=1):
  1313. if obj is not None:
  1314. object_positions.append(stream.tell())
  1315. stream.write(f"{idnum} 0 obj\n".encode())
  1316. if self._encryption and obj != self._encrypt_entry:
  1317. obj = self._encryption.encrypt_object(obj, idnum, 0)
  1318. obj.write_to_stream(stream)
  1319. stream.write(b"\nendobj\n")
  1320. else:
  1321. object_positions.append(-1)
  1322. free_objects.append(idnum)
  1323. free_objects.append(0) # add 0 to loop in accordance with specification
  1324. return object_positions, free_objects
  1325. def _write_xref_table(
  1326. self, stream: StreamType, object_positions: list[int], free_objects: list[int]
  1327. ) -> int:
  1328. xref_location = stream.tell()
  1329. stream.write(b"xref\n")
  1330. stream.write(f"0 {len(self._objects) + 1}\n".encode())
  1331. stream.write(f"{free_objects[0]:0>10} {65535:0>5} f \n".encode())
  1332. free_idx = 1
  1333. for offset in object_positions:
  1334. if offset > 0:
  1335. stream.write(f"{offset:0>10} {0:0>5} n \n".encode())
  1336. else:
  1337. stream.write(f"{free_objects[free_idx]:0>10} {1:0>5} f \n".encode())
  1338. free_idx += 1
  1339. return xref_location
  1340. def _write_trailer(self, stream: StreamType, xref_location: int) -> None:
  1341. """
  1342. Write the PDF trailer to the stream.
  1343. To quote the PDF specification:
  1344. [The] trailer [gives] the location of the cross-reference table and
  1345. of certain special objects within the body of the file.
  1346. """
  1347. stream.write(b"trailer\n")
  1348. trailer = DictionaryObject(
  1349. {
  1350. NameObject(TK.SIZE): NumberObject(len(self._objects) + 1),
  1351. NameObject(TK.ROOT): self.root_object.indirect_reference,
  1352. }
  1353. )
  1354. if self._info is not None:
  1355. trailer[NameObject(TK.INFO)] = self._info.indirect_reference
  1356. if self._ID is not None:
  1357. trailer[NameObject(TK.ID)] = self._ID
  1358. if self._encrypt_entry:
  1359. trailer[NameObject(TK.ENCRYPT)] = self._encrypt_entry.indirect_reference
  1360. trailer.write_to_stream(stream)
  1361. stream.write(f"\nstartxref\n{xref_location}\n%%EOF\n".encode()) # eof
  1362. @property
  1363. def metadata(self) -> Optional[DocumentInformation]:
  1364. """
  1365. Retrieve/set the PDF file's document information dictionary, if it exists.
  1366. Args:
  1367. value: dict with the entries to be set. if None : remove the /Info entry from the pdf.
  1368. Note that some PDF files use (XMP) metadata streams instead of document
  1369. information dictionaries, and these metadata streams will not be
  1370. accessed by this function, but by :meth:`~xmp_metadata`.
  1371. """
  1372. return super().metadata
  1373. @metadata.setter
  1374. def metadata(
  1375. self,
  1376. value: Optional[Union[DocumentInformation, DictionaryObject, dict[Any, Any]]],
  1377. ) -> None:
  1378. if value is None:
  1379. self._info = None
  1380. else:
  1381. if self._info is not None:
  1382. self._info.clear()
  1383. self.add_metadata(value)
  1384. def add_metadata(self, infos: dict[str, Any]) -> None:
  1385. """
  1386. Add custom metadata to the output.
  1387. Args:
  1388. infos: a Python dictionary where each key is a field
  1389. and each value is your new metadata.
  1390. """
  1391. args = {}
  1392. if isinstance(infos, PdfObject):
  1393. infos = cast(DictionaryObject, infos.get_object())
  1394. for key, value in list(infos.items()):
  1395. if isinstance(value, PdfObject):
  1396. value = value.get_object()
  1397. args[NameObject(key)] = create_string_object(str(value))
  1398. if self._info is None:
  1399. self._info = DictionaryObject()
  1400. self._info.update(args)
  1401. def compress_identical_objects(
  1402. self,
  1403. remove_identicals: bool = True,
  1404. remove_orphans: bool = True,
  1405. ) -> None:
  1406. """
  1407. Parse the PDF file and merge objects that have the same hash.
  1408. This will make objects common to multiple pages.
  1409. Recommended to be used just before writing output.
  1410. Args:
  1411. remove_identicals: Remove identical objects.
  1412. remove_orphans: Remove unreferenced objects.
  1413. """
  1414. def replace_in_obj(
  1415. obj: PdfObject, crossref: dict[IndirectObject, IndirectObject]
  1416. ) -> None:
  1417. if isinstance(obj, DictionaryObject):
  1418. key_val = obj.items()
  1419. elif isinstance(obj, ArrayObject):
  1420. key_val = enumerate(obj) # type: ignore
  1421. else:
  1422. return
  1423. assert isinstance(obj, (DictionaryObject, ArrayObject))
  1424. for k, v in key_val:
  1425. if isinstance(v, IndirectObject):
  1426. orphans[v.idnum - 1] = False
  1427. if v in crossref:
  1428. obj[k] = crossref[v]
  1429. else:
  1430. """the filtering on DictionaryObject and ArrayObject only
  1431. will be performed within replace_in_obj"""
  1432. replace_in_obj(v, crossref)
  1433. # _idnum_hash :dict[hash]=(1st_ind_obj,[other_indir_objs,...])
  1434. self._idnum_hash = {}
  1435. orphans = [True] * len(self._objects)
  1436. # look for similar objects
  1437. for idx, obj in enumerate(self._objects):
  1438. if is_null_or_none(obj):
  1439. continue
  1440. assert obj is not None, "mypy" # mypy: TypeGuard of `is_null_or_none` does not help here.
  1441. assert isinstance(obj.indirect_reference, IndirectObject)
  1442. h = obj.hash_value()
  1443. if remove_identicals and h in self._idnum_hash:
  1444. self._idnum_hash[h][1].append(obj.indirect_reference)
  1445. self._objects[idx] = None
  1446. else:
  1447. self._idnum_hash[h] = (obj.indirect_reference, [])
  1448. # generate the dict converting others to 1st
  1449. cnv = {v[0]: v[1] for v in self._idnum_hash.values() if len(v[1]) > 0}
  1450. cnv_rev: dict[IndirectObject, IndirectObject] = {}
  1451. for k, v in cnv.items():
  1452. cnv_rev.update(zip(v, (k,) * len(v)))
  1453. # replace reference to merged objects
  1454. for obj in self._objects:
  1455. if isinstance(obj, (DictionaryObject, ArrayObject)):
  1456. replace_in_obj(obj, cnv_rev)
  1457. # remove orphans (if applicable)
  1458. orphans[self.root_object.indirect_reference.idnum - 1] = False # type: ignore
  1459. if not is_null_or_none(self._info):
  1460. orphans[self._info.indirect_reference.idnum - 1] = False # type: ignore
  1461. try:
  1462. orphans[self._ID.indirect_reference.idnum - 1] = False # type: ignore
  1463. except AttributeError:
  1464. pass
  1465. for i in compress(range(len(self._objects)), orphans):
  1466. self._objects[i] = None
  1467. def get_reference(self, obj: PdfObject) -> IndirectObject:
  1468. idnum = self._objects.index(obj) + 1
  1469. ref = IndirectObject(idnum, 0, self)
  1470. assert ref.get_object() == obj
  1471. return ref
  1472. def get_outline_root(self) -> TreeObject:
  1473. if CO.OUTLINES in self._root_object:
  1474. # Entries in the catalog dictionary
  1475. outline = cast(TreeObject, self._root_object[CO.OUTLINES])
  1476. if not isinstance(outline, TreeObject):
  1477. t = TreeObject(outline)
  1478. self._replace_object(outline.indirect_reference.idnum, t)
  1479. outline = t
  1480. idnum = self._objects.index(outline) + 1
  1481. outline_ref = IndirectObject(idnum, 0, self)
  1482. assert outline_ref.get_object() == outline
  1483. else:
  1484. outline = TreeObject()
  1485. outline.update({})
  1486. outline_ref = self._add_object(outline)
  1487. self._root_object[NameObject(CO.OUTLINES)] = outline_ref
  1488. return outline
  1489. def get_threads_root(self) -> ArrayObject:
  1490. """
  1491. The list of threads.
  1492. See §12.4.3 of the PDF 1.7 or PDF 2.0 specification.
  1493. Returns:
  1494. An array (possibly empty) of Dictionaries with an ``/F`` key,
  1495. and optionally information about the thread in ``/I`` or ``/Metadata`` keys.
  1496. """
  1497. if CO.THREADS in self._root_object:
  1498. # Entries in the catalog dictionary
  1499. threads = cast(ArrayObject, self._root_object[CO.THREADS])
  1500. else:
  1501. threads = ArrayObject()
  1502. self._root_object[NameObject(CO.THREADS)] = threads
  1503. return threads
  1504. @property
  1505. def threads(self) -> ArrayObject:
  1506. """
  1507. Read-only property for the list of threads.
  1508. See §12.4.3 of the PDF 1.7 or PDF 2.0 specification.
  1509. Each element is a dictionary with an ``/F`` key, and optionally
  1510. information about the thread in ``/I`` or ``/Metadata`` keys.
  1511. """
  1512. return self.get_threads_root()
  1513. def add_outline_item_destination(
  1514. self,
  1515. page_destination: Union[IndirectObject, PageObject, TreeObject],
  1516. parent: Union[None, TreeObject, IndirectObject] = None,
  1517. before: Union[None, TreeObject, IndirectObject] = None,
  1518. is_open: bool = True,
  1519. ) -> IndirectObject:
  1520. page_destination = cast(PageObject, page_destination.get_object())
  1521. if isinstance(page_destination, PageObject):
  1522. return self.add_outline_item_destination(
  1523. Destination(
  1524. f"page #{page_destination.page_number}",
  1525. cast(IndirectObject, page_destination.indirect_reference),
  1526. Fit.fit(),
  1527. )
  1528. )
  1529. if parent is None:
  1530. parent = self.get_outline_root()
  1531. page_destination[NameObject("/%is_open%")] = BooleanObject(is_open)
  1532. parent = cast(TreeObject, parent.get_object())
  1533. page_destination_ref = self._add_object(page_destination)
  1534. if before is not None:
  1535. before = before.indirect_reference
  1536. parent.insert_child(
  1537. page_destination_ref,
  1538. before,
  1539. self,
  1540. page_destination.inc_parent_counter_outline
  1541. if is_open
  1542. else (lambda x, y: 0), # noqa: ARG005
  1543. )
  1544. if "/Count" not in page_destination:
  1545. page_destination[NameObject("/Count")] = NumberObject(0)
  1546. return page_destination_ref
  1547. def add_outline_item_dict(
  1548. self,
  1549. outline_item: OutlineItemType,
  1550. parent: Union[None, TreeObject, IndirectObject] = None,
  1551. before: Union[None, TreeObject, IndirectObject] = None,
  1552. is_open: bool = True,
  1553. ) -> IndirectObject:
  1554. outline_item_object = TreeObject()
  1555. outline_item_object.update(outline_item)
  1556. """code currently unreachable
  1557. if "/A" in outline_item:
  1558. action = DictionaryObject()
  1559. a_dict = cast(DictionaryObject, outline_item["/A"])
  1560. for k, v in list(a_dict.items()):
  1561. action[NameObject(str(k))] = v
  1562. action_ref = self._add_object(action)
  1563. outline_item_object[NameObject("/A")] = action_ref
  1564. """
  1565. return self.add_outline_item_destination(
  1566. outline_item_object, parent, before, is_open
  1567. )
  1568. def add_outline_item(
  1569. self,
  1570. title: str,
  1571. page_number: Union[None, PageObject, IndirectObject, int],
  1572. parent: Union[None, TreeObject, IndirectObject] = None,
  1573. before: Union[None, TreeObject, IndirectObject] = None,
  1574. color: Optional[Union[tuple[float, float, float], str]] = None,
  1575. bold: bool = False,
  1576. italic: bool = False,
  1577. fit: Fit = PAGE_FIT,
  1578. is_open: bool = True,
  1579. ) -> IndirectObject:
  1580. """
  1581. Add an outline item (commonly referred to as a "Bookmark") to the PDF file.
  1582. Args:
  1583. title: Title to use for this outline item.
  1584. page_number: Page number this outline item will point to.
  1585. parent: A reference to a parent outline item to create nested
  1586. outline items.
  1587. before:
  1588. color: Color of the outline item's font as a red, green, blue tuple
  1589. from 0.0 to 1.0 or as a Hex String (#RRGGBB)
  1590. bold: Outline item font is bold
  1591. italic: Outline item font is italic
  1592. fit: The fit of the destination page.
  1593. Returns:
  1594. The added outline item as an indirect object.
  1595. """
  1596. page_ref: Union[None, NullObject, IndirectObject, NumberObject]
  1597. if isinstance(italic, Fit): # it means that we are on the old params
  1598. if fit is not None and page_number is None:
  1599. page_number = fit
  1600. return self.add_outline_item(
  1601. title, page_number, parent, None, before, color, bold, italic, is_open=is_open
  1602. )
  1603. if page_number is None:
  1604. action_ref = None
  1605. else:
  1606. if isinstance(page_number, IndirectObject):
  1607. page_ref = page_number
  1608. elif isinstance(page_number, PageObject):
  1609. page_ref = page_number.indirect_reference
  1610. elif isinstance(page_number, int):
  1611. try:
  1612. page_ref = self.pages[page_number].indirect_reference
  1613. except IndexError:
  1614. page_ref = NumberObject(page_number)
  1615. if page_ref is None:
  1616. logger_warning(
  1617. f"can not find reference of page {page_number}",
  1618. __name__,
  1619. )
  1620. page_ref = NullObject()
  1621. dest = Destination(
  1622. NameObject("/" + title + " outline item"),
  1623. page_ref,
  1624. fit,
  1625. )
  1626. action_ref = self._add_object(
  1627. DictionaryObject(
  1628. {
  1629. NameObject(GoToActionArguments.D): dest.dest_array,
  1630. NameObject(GoToActionArguments.S): NameObject("/GoTo"),
  1631. }
  1632. )
  1633. )
  1634. outline_item = self._add_object(
  1635. _create_outline_item(action_ref, title, color, italic, bold)
  1636. )
  1637. if parent is None:
  1638. parent = self.get_outline_root()
  1639. return self.add_outline_item_destination(outline_item, parent, before, is_open)
  1640. def add_outline(self) -> None:
  1641. raise NotImplementedError(
  1642. "This method is not yet implemented. Use :meth:`add_outline_item` instead."
  1643. )
  1644. def add_named_destination_array(
  1645. self, title: TextStringObject, destination: Union[IndirectObject, ArrayObject]
  1646. ) -> None:
  1647. named_dest = self.get_named_dest_root()
  1648. i = 0
  1649. while i < len(named_dest):
  1650. if title < named_dest[i]:
  1651. named_dest.insert(i, destination)
  1652. named_dest.insert(i, TextStringObject(title))
  1653. return
  1654. i += 2
  1655. named_dest.extend([TextStringObject(title), destination])
  1656. return
  1657. def add_named_destination_object(
  1658. self,
  1659. page_destination: PdfObject,
  1660. ) -> IndirectObject:
  1661. page_destination_ref = self._add_object(page_destination.dest_array) # type: ignore
  1662. self.add_named_destination_array(
  1663. cast("TextStringObject", page_destination["/Title"]), page_destination_ref # type: ignore
  1664. )
  1665. return page_destination_ref
  1666. def add_named_destination(
  1667. self,
  1668. title: str,
  1669. page_number: int,
  1670. ) -> IndirectObject:
  1671. page_ref = self.get_object(self._pages)[PagesAttributes.KIDS][page_number] # type: ignore
  1672. dest = DictionaryObject()
  1673. dest.update(
  1674. {
  1675. NameObject(GoToActionArguments.D): ArrayObject(
  1676. [page_ref, NameObject(TypFitArguments.FIT_H), NumberObject(826)]
  1677. ),
  1678. NameObject(GoToActionArguments.S): NameObject("/GoTo"),
  1679. }
  1680. )
  1681. dest_ref = self._add_object(dest)
  1682. if not isinstance(title, TextStringObject):
  1683. title = TextStringObject(str(title))
  1684. self.add_named_destination_array(title, dest_ref)
  1685. return dest_ref
  1686. def remove_links(self) -> None:
  1687. """Remove links and annotations from this output."""
  1688. for page in self.pages:
  1689. self.remove_objects_from_page(page, ObjectDeletionFlag.ALL_ANNOTATIONS)
  1690. def remove_annotations(
  1691. self, subtypes: Optional[Union[AnnotationSubtype, Iterable[AnnotationSubtype]]]
  1692. ) -> None:
  1693. """
  1694. Remove annotations by annotation subtype.
  1695. Args:
  1696. subtypes: subtype or list of subtypes to be removed.
  1697. Examples are: "/Link", "/FileAttachment", "/Sound",
  1698. "/Movie", "/Screen", ...
  1699. If you want to remove all annotations, use subtypes=None.
  1700. """
  1701. for page in self.pages:
  1702. self._remove_annots_from_page(page, subtypes)
  1703. def _remove_annots_from_page(
  1704. self,
  1705. page: Union[IndirectObject, PageObject, DictionaryObject],
  1706. subtypes: Optional[Iterable[str]],
  1707. ) -> None:
  1708. page = cast(DictionaryObject, page.get_object())
  1709. if PG.ANNOTS in page:
  1710. i = 0
  1711. while i < len(cast(ArrayObject, page[PG.ANNOTS])):
  1712. an = cast(ArrayObject, page[PG.ANNOTS])[i]
  1713. obj = cast(DictionaryObject, an.get_object())
  1714. if subtypes is None or cast(str, obj["/Subtype"]) in subtypes:
  1715. if isinstance(an, IndirectObject):
  1716. self._objects[an.idnum - 1] = NullObject() # to reduce PDF size
  1717. del page[PG.ANNOTS][i] # type:ignore
  1718. else:
  1719. i += 1
  1720. def remove_objects_from_page(
  1721. self,
  1722. page: Union[PageObject, DictionaryObject],
  1723. to_delete: Union[ObjectDeletionFlag, Iterable[ObjectDeletionFlag]],
  1724. text_filters: Optional[dict[str, Any]] = None
  1725. ) -> None:
  1726. """
  1727. Remove objects specified by ``to_delete`` from the given page.
  1728. Args:
  1729. page: Page object to clean up.
  1730. to_delete: Objects to be deleted; can be a ``ObjectDeletionFlag``
  1731. or a list of ObjectDeletionFlag
  1732. text_filters: Properties of text to be deleted, if applicable. Optional.
  1733. This is a Python dictionary with the following properties:
  1734. * font_ids: List of font resource IDs (such as /F1 or /T1_0) to be deleted.
  1735. """
  1736. if isinstance(to_delete, (list, tuple)):
  1737. for to_d in to_delete:
  1738. self.remove_objects_from_page(page, to_d)
  1739. return None
  1740. assert isinstance(to_delete, ObjectDeletionFlag)
  1741. if to_delete & ObjectDeletionFlag.LINKS:
  1742. return self._remove_annots_from_page(page, ("/Link",))
  1743. if to_delete & ObjectDeletionFlag.ATTACHMENTS:
  1744. return self._remove_annots_from_page(
  1745. page, ("/FileAttachment", "/Sound", "/Movie", "/Screen")
  1746. )
  1747. if to_delete & ObjectDeletionFlag.OBJECTS_3D:
  1748. return self._remove_annots_from_page(page, ("/3D",))
  1749. if to_delete & ObjectDeletionFlag.ALL_ANNOTATIONS:
  1750. return self._remove_annots_from_page(page, None)
  1751. jump_operators = []
  1752. if to_delete & ObjectDeletionFlag.DRAWING_IMAGES:
  1753. jump_operators = [
  1754. b"w", b"J", b"j", b"M", b"d", b"i",
  1755. b"W", b"W*",
  1756. b"b", b"b*", b"B", b"B*", b"S", b"s", b"f", b"f*", b"F", b"n",
  1757. b"m", b"l", b"c", b"v", b"y", b"h", b"re",
  1758. b"sh"
  1759. ]
  1760. if to_delete & ObjectDeletionFlag.TEXT:
  1761. jump_operators = [b"Tj", b"TJ", b"'", b'"']
  1762. if not isinstance(page, PageObject):
  1763. page = PageObject(self, page.indirect_reference) # pragma: no cover
  1764. if "/Contents" in page:
  1765. content = cast(ContentStream, page.get_contents())
  1766. images, forms = self._remove_objects_from_page__clean_forms(
  1767. elt=page, stack=[], jump_operators=jump_operators, to_delete=to_delete, text_filters=text_filters,
  1768. )
  1769. self._remove_objects_from_page__clean(
  1770. content=content, images=images, forms=forms,
  1771. jump_operators=jump_operators, to_delete=to_delete,
  1772. text_filters=text_filters
  1773. )
  1774. page.replace_contents(content)
  1775. return [], [] # type: ignore[return-value]
  1776. def _remove_objects_from_page__clean(
  1777. self,
  1778. content: ContentStream,
  1779. images: list[str],
  1780. forms: list[str],
  1781. jump_operators: list[bytes],
  1782. to_delete: ObjectDeletionFlag,
  1783. text_filters: Optional[dict[str, Any]] = None,
  1784. ) -> None:
  1785. font_id = None
  1786. font_ids_to_delete = []
  1787. if text_filters and to_delete & ObjectDeletionFlag.TEXT:
  1788. font_ids_to_delete = text_filters.get("font_ids", [])
  1789. i = 0
  1790. while i < len(content.operations):
  1791. operands, operator = content.operations[i]
  1792. if operator == b"Tf":
  1793. font_id = operands[0]
  1794. if (
  1795. (
  1796. operator == b"INLINE IMAGE"
  1797. and (to_delete & ObjectDeletionFlag.INLINE_IMAGES)
  1798. )
  1799. or (operator in jump_operators)
  1800. or (
  1801. operator == b"Do"
  1802. and (to_delete & ObjectDeletionFlag.XOBJECT_IMAGES)
  1803. and (operands[0] in images)
  1804. )
  1805. ):
  1806. if (
  1807. not to_delete & ObjectDeletionFlag.TEXT
  1808. or (to_delete & ObjectDeletionFlag.TEXT and not text_filters)
  1809. or (to_delete & ObjectDeletionFlag.TEXT and font_id in font_ids_to_delete)
  1810. ):
  1811. del content.operations[i]
  1812. else:
  1813. i += 1
  1814. else:
  1815. i += 1
  1816. content.get_data() # this ensures ._data is rebuilt from the .operations
  1817. def _remove_objects_from_page__clean_forms(
  1818. self,
  1819. elt: DictionaryObject,
  1820. stack: list[DictionaryObject],
  1821. jump_operators: list[bytes],
  1822. to_delete: ObjectDeletionFlag,
  1823. text_filters: Optional[dict[str, Any]] = None,
  1824. ) -> tuple[list[str], list[str]]:
  1825. # elt in recursive call is a new ContentStream object, so we have to check the indirect_reference
  1826. if (elt in stack) or (
  1827. hasattr(elt, "indirect_reference") and any(
  1828. elt.indirect_reference == getattr(x, "indirect_reference", -1)
  1829. for x in stack
  1830. )
  1831. ):
  1832. # to prevent infinite looping
  1833. return [], [] # pragma: no cover
  1834. try:
  1835. d = cast(
  1836. dict[Any, Any],
  1837. cast(DictionaryObject, elt["/Resources"])["/XObject"],
  1838. )
  1839. except KeyError:
  1840. d = {}
  1841. images = []
  1842. forms = []
  1843. for k, v in d.items():
  1844. o = v.get_object()
  1845. try:
  1846. content: Any = None
  1847. if (
  1848. to_delete & ObjectDeletionFlag.XOBJECT_IMAGES
  1849. and o["/Subtype"] == "/Image"
  1850. ):
  1851. content = NullObject() # to delete the image keeping the entry
  1852. images.append(k)
  1853. if o["/Subtype"] == "/Form":
  1854. forms.append(k)
  1855. if isinstance(o, ContentStream):
  1856. content = o
  1857. else:
  1858. content = ContentStream(o, self)
  1859. content.update(
  1860. {
  1861. k1: v1
  1862. for k1, v1 in o.items()
  1863. if k1 not in ["/Length", "/Filter", "/DecodeParms"]
  1864. }
  1865. )
  1866. try:
  1867. content.indirect_reference = o.indirect_reference
  1868. except AttributeError: # pragma: no cover
  1869. pass
  1870. stack.append(elt)
  1871. # clean subforms
  1872. self._remove_objects_from_page__clean_forms(
  1873. elt=content, stack=stack, jump_operators=jump_operators, to_delete=to_delete,
  1874. text_filters=text_filters,
  1875. )
  1876. if content is not None:
  1877. if isinstance(v, IndirectObject):
  1878. self._objects[v.idnum - 1] = content
  1879. else:
  1880. # should only occur in a PDF not respecting PDF spec
  1881. # where streams must be indirected.
  1882. d[k] = self._add_object(content) # pragma: no cover
  1883. except (TypeError, KeyError):
  1884. pass
  1885. for im in images:
  1886. del d[im] # for clean-up
  1887. if isinstance(elt, StreamObject): # for /Form
  1888. if not isinstance(elt, ContentStream): # pragma: no cover
  1889. e = ContentStream(elt, self)
  1890. e.update(elt.items())
  1891. elt = e
  1892. # clean the content
  1893. self._remove_objects_from_page__clean(
  1894. content=elt, images=images, forms=forms, jump_operators=jump_operators,
  1895. to_delete=to_delete, text_filters=text_filters
  1896. )
  1897. return images, forms
  1898. def remove_images(
  1899. self,
  1900. to_delete: ImageType = ImageType.ALL,
  1901. ) -> None:
  1902. """
  1903. Remove images from this output.
  1904. Args:
  1905. to_delete: The type of images to be deleted
  1906. (default = all images types)
  1907. """
  1908. if isinstance(to_delete, bool):
  1909. to_delete = ImageType.ALL
  1910. i = ObjectDeletionFlag.NONE
  1911. for image in ("XOBJECT_IMAGES", "INLINE_IMAGES", "DRAWING_IMAGES"):
  1912. if to_delete & ImageType[image]:
  1913. i |= ObjectDeletionFlag[image]
  1914. for page in self.pages:
  1915. self.remove_objects_from_page(page, i)
  1916. def remove_text(self, font_names: Optional[list[str]] = None) -> None:
  1917. """
  1918. Remove text from the PDF.
  1919. Args:
  1920. font_names: List of font names to remove, such as "Helvetica-Bold".
  1921. Optional. If not specified, all text will be removed.
  1922. """
  1923. if not font_names:
  1924. font_names = []
  1925. for page in self.pages:
  1926. resource_ids_to_remove = []
  1927. # Content streams reference fonts and other resources with names like "/F1" or "/T1_0"
  1928. # Font names need to be converted to resource names/IDs for easier removal
  1929. if font_names:
  1930. # Recursively loop through page objects to gather font info
  1931. def get_font_info(
  1932. obj: Any,
  1933. font_info: Optional[dict[str, Any]] = None,
  1934. key: Optional[str] = None
  1935. ) -> dict[str, Any]:
  1936. if font_info is None:
  1937. font_info = {}
  1938. if isinstance(obj, IndirectObject):
  1939. obj = obj.get_object()
  1940. if isinstance(obj, dict):
  1941. if obj.get("/Type") == "/Font":
  1942. font_name = obj.get("/BaseFont", "")
  1943. # Normalize font names like "/RRXFFV+Palatino-Bold" to "Palatino-Bold"
  1944. normalized_font_name = font_name.lstrip("/").split("+")[-1]
  1945. if normalized_font_name not in font_info:
  1946. font_info[normalized_font_name] = {
  1947. "normalized_font_name": normalized_font_name,
  1948. "resource_ids": [],
  1949. }
  1950. if key not in font_info[normalized_font_name]["resource_ids"]:
  1951. font_info[normalized_font_name]["resource_ids"].append(key)
  1952. for k in obj:
  1953. font_info = get_font_info(obj[k], font_info, k)
  1954. elif isinstance(obj, (list, ArrayObject)):
  1955. for child_obj in obj:
  1956. font_info = get_font_info(child_obj, font_info)
  1957. return font_info
  1958. # Add relevant resource names for removal
  1959. font_info = get_font_info(page.get("/Resources"))
  1960. for font_name in font_names:
  1961. if font_name in font_info:
  1962. resource_ids_to_remove.extend(font_info[font_name]["resource_ids"])
  1963. text_filters = {}
  1964. if font_names:
  1965. text_filters["font_ids"] = resource_ids_to_remove
  1966. self.remove_objects_from_page(page, ObjectDeletionFlag.TEXT, text_filters=text_filters)
  1967. def add_uri(
  1968. self,
  1969. page_number: int,
  1970. uri: str,
  1971. rect: RectangleObject,
  1972. border: Optional[ArrayObject] = None,
  1973. ) -> None:
  1974. """
  1975. Add an URI from a rectangular area to the specified page.
  1976. Args:
  1977. page_number: index of the page on which to place the URI action.
  1978. uri: URI of resource to link to.
  1979. rect: :class:`RectangleObject<pypdf.generic.RectangleObject>` or
  1980. array of four integers specifying the clickable rectangular area
  1981. ``[xLL, yLL, xUR, yUR]``, or string in the form
  1982. ``"[ xLL yLL xUR yUR ]"``.
  1983. border: if provided, an array describing border-drawing
  1984. properties. See the PDF spec for details. No border will be
  1985. drawn if this argument is omitted.
  1986. """
  1987. page_link = self.get_object(self._pages)[PagesAttributes.KIDS][page_number] # type: ignore
  1988. page_ref = cast(dict[str, Any], self.get_object(page_link))
  1989. border_arr: BorderArrayType
  1990. if border is not None:
  1991. border_arr = [NumberObject(n) for n in border[:3]]
  1992. if len(border) == 4:
  1993. dash_pattern = ArrayObject([NumberObject(n) for n in border[3]])
  1994. border_arr.append(dash_pattern)
  1995. else:
  1996. border_arr = [NumberObject(2), NumberObject(2), NumberObject(2)]
  1997. if isinstance(rect, str):
  1998. rect = NumberObject(rect)
  1999. elif isinstance(rect, RectangleObject):
  2000. pass
  2001. else:
  2002. rect = RectangleObject(rect)
  2003. lnk2 = DictionaryObject()
  2004. lnk2.update(
  2005. {
  2006. NameObject("/S"): NameObject("/URI"),
  2007. NameObject("/URI"): TextStringObject(uri),
  2008. }
  2009. )
  2010. lnk = DictionaryObject()
  2011. lnk.update(
  2012. {
  2013. NameObject(AA.Type): NameObject("/Annot"),
  2014. NameObject(AA.Subtype): NameObject("/Link"),
  2015. NameObject(AA.P): page_link,
  2016. NameObject(AA.Rect): rect,
  2017. NameObject("/H"): NameObject("/I"),
  2018. NameObject(AA.Border): ArrayObject(border_arr),
  2019. NameObject("/A"): lnk2,
  2020. }
  2021. )
  2022. lnk_ref = self._add_object(lnk)
  2023. if PG.ANNOTS in page_ref:
  2024. page_ref[PG.ANNOTS].append(lnk_ref)
  2025. else:
  2026. page_ref[NameObject(PG.ANNOTS)] = ArrayObject([lnk_ref])
  2027. _valid_layouts = (
  2028. "/NoLayout",
  2029. "/SinglePage",
  2030. "/OneColumn",
  2031. "/TwoColumnLeft",
  2032. "/TwoColumnRight",
  2033. "/TwoPageLeft",
  2034. "/TwoPageRight",
  2035. )
  2036. def _get_page_layout(self) -> Optional[LayoutType]:
  2037. try:
  2038. return cast(LayoutType, self._root_object["/PageLayout"])
  2039. except KeyError:
  2040. return None
  2041. def _set_page_layout(self, layout: Union[NameObject, LayoutType]) -> None:
  2042. """
  2043. Set the page layout.
  2044. Args:
  2045. layout: The page layout to be used.
  2046. .. list-table:: Valid ``layout`` arguments
  2047. :widths: 50 200
  2048. * - /NoLayout
  2049. - Layout explicitly not specified
  2050. * - /SinglePage
  2051. - Show one page at a time
  2052. * - /OneColumn
  2053. - Show one column at a time
  2054. * - /TwoColumnLeft
  2055. - Show pages in two columns, odd-numbered pages on the left
  2056. * - /TwoColumnRight
  2057. - Show pages in two columns, odd-numbered pages on the right
  2058. * - /TwoPageLeft
  2059. - Show two pages at a time, odd-numbered pages on the left
  2060. * - /TwoPageRight
  2061. - Show two pages at a time, odd-numbered pages on the right
  2062. """
  2063. if not isinstance(layout, NameObject):
  2064. if layout not in self._valid_layouts:
  2065. logger_warning(
  2066. f"Layout should be one of: {'', ''.join(self._valid_layouts)}",
  2067. __name__,
  2068. )
  2069. layout = NameObject(layout)
  2070. self._root_object.update({NameObject("/PageLayout"): layout})
  2071. def set_page_layout(self, layout: LayoutType) -> None:
  2072. """
  2073. Set the page layout.
  2074. Args:
  2075. layout: The page layout to be used
  2076. .. list-table:: Valid ``layout`` arguments
  2077. :widths: 50 200
  2078. * - /NoLayout
  2079. - Layout explicitly not specified
  2080. * - /SinglePage
  2081. - Show one page at a time
  2082. * - /OneColumn
  2083. - Show one column at a time
  2084. * - /TwoColumnLeft
  2085. - Show pages in two columns, odd-numbered pages on the left
  2086. * - /TwoColumnRight
  2087. - Show pages in two columns, odd-numbered pages on the right
  2088. * - /TwoPageLeft
  2089. - Show two pages at a time, odd-numbered pages on the left
  2090. * - /TwoPageRight
  2091. - Show two pages at a time, odd-numbered pages on the right
  2092. """
  2093. self._set_page_layout(layout)
  2094. @property
  2095. def page_layout(self) -> Optional[LayoutType]:
  2096. """
  2097. Page layout property.
  2098. .. list-table:: Valid ``layout`` values
  2099. :widths: 50 200
  2100. * - /NoLayout
  2101. - Layout explicitly not specified
  2102. * - /SinglePage
  2103. - Show one page at a time
  2104. * - /OneColumn
  2105. - Show one column at a time
  2106. * - /TwoColumnLeft
  2107. - Show pages in two columns, odd-numbered pages on the left
  2108. * - /TwoColumnRight
  2109. - Show pages in two columns, odd-numbered pages on the right
  2110. * - /TwoPageLeft
  2111. - Show two pages at a time, odd-numbered pages on the left
  2112. * - /TwoPageRight
  2113. - Show two pages at a time, odd-numbered pages on the right
  2114. """
  2115. return self._get_page_layout()
  2116. @page_layout.setter
  2117. def page_layout(self, layout: LayoutType) -> None:
  2118. self._set_page_layout(layout)
  2119. _valid_modes = (
  2120. "/UseNone",
  2121. "/UseOutlines",
  2122. "/UseThumbs",
  2123. "/FullScreen",
  2124. "/UseOC",
  2125. "/UseAttachments",
  2126. )
  2127. def _get_page_mode(self) -> Optional[PagemodeType]:
  2128. try:
  2129. return cast(PagemodeType, self._root_object["/PageMode"])
  2130. except KeyError:
  2131. return None
  2132. @property
  2133. def page_mode(self) -> Optional[PagemodeType]:
  2134. """
  2135. Page mode property.
  2136. .. list-table:: Valid ``mode`` values
  2137. :widths: 50 200
  2138. * - /UseNone
  2139. - Do not show outline or thumbnails panels
  2140. * - /UseOutlines
  2141. - Show outline (aka bookmarks) panel
  2142. * - /UseThumbs
  2143. - Show page thumbnails panel
  2144. * - /FullScreen
  2145. - Fullscreen view
  2146. * - /UseOC
  2147. - Show Optional Content Group (OCG) panel
  2148. * - /UseAttachments
  2149. - Show attachments panel
  2150. """
  2151. return self._get_page_mode()
  2152. @page_mode.setter
  2153. def page_mode(self, mode: PagemodeType) -> None:
  2154. if isinstance(mode, NameObject):
  2155. mode_name: NameObject = mode
  2156. else:
  2157. if mode not in self._valid_modes:
  2158. logger_warning(
  2159. f"Mode should be one of: {', '.join(self._valid_modes)}", __name__
  2160. )
  2161. mode_name = NameObject(mode)
  2162. self._root_object.update({NameObject("/PageMode"): mode_name})
  2163. def add_annotation(
  2164. self,
  2165. page_number: Union[int, PageObject],
  2166. annotation: dict[str, Any],
  2167. ) -> DictionaryObject:
  2168. """
  2169. Add a single annotation to the page.
  2170. The added annotation must be a new annotation.
  2171. It cannot be recycled.
  2172. Args:
  2173. page_number: PageObject or page index.
  2174. annotation: Annotation to be added (created with annotation).
  2175. Returns:
  2176. The inserted object.
  2177. This can be used for popup creation, for example.
  2178. """
  2179. page = page_number
  2180. if isinstance(page, int):
  2181. page = self.pages[page]
  2182. elif not isinstance(page, PageObject):
  2183. raise TypeError("page: invalid type")
  2184. to_add = cast(DictionaryObject, _pdf_objectify(annotation))
  2185. to_add[NameObject("/P")] = page.indirect_reference
  2186. if page.annotations is None:
  2187. page[NameObject("/Annots")] = ArrayObject()
  2188. assert page.annotations is not None
  2189. # Internal link annotations need the correct object type for the
  2190. # destination
  2191. if to_add.get("/Subtype") == "/Link" and "/Dest" in to_add:
  2192. tmp = cast(dict[Any, Any], to_add[NameObject("/Dest")])
  2193. dest = Destination(
  2194. NameObject("/LinkName"),
  2195. tmp["target_page_index"],
  2196. Fit(
  2197. fit_type=tmp["fit"], fit_args=dict(tmp)["fit_args"]
  2198. ), # I have no clue why this dict-hack is necessary
  2199. )
  2200. to_add[NameObject("/Dest")] = dest.dest_array
  2201. page.annotations.append(self._add_object(to_add))
  2202. if to_add.get("/Subtype") == "/Popup" and NameObject("/Parent") in to_add:
  2203. cast(DictionaryObject, to_add["/Parent"].get_object())[
  2204. NameObject("/Popup")
  2205. ] = to_add.indirect_reference
  2206. return to_add
  2207. def clean_page(self, page: Union[PageObject, IndirectObject]) -> PageObject:
  2208. """
  2209. Perform some clean up in the page.
  2210. Currently: convert NameObject named destination to TextStringObject
  2211. (required for names/dests list)
  2212. Args:
  2213. page:
  2214. Returns:
  2215. The cleaned PageObject
  2216. """
  2217. page = cast("PageObject", page.get_object())
  2218. for a in page.get("/Annots", []):
  2219. a_obj = a.get_object()
  2220. d = a_obj.get("/Dest", None)
  2221. act = a_obj.get("/A", None)
  2222. if isinstance(d, NameObject):
  2223. a_obj[NameObject("/Dest")] = TextStringObject(d)
  2224. elif act is not None:
  2225. act = act.get_object()
  2226. d = act.get("/D", None)
  2227. if isinstance(d, NameObject):
  2228. act[NameObject("/D")] = TextStringObject(d)
  2229. return page
  2230. def _create_stream(
  2231. self, fileobj: Union[Path, StrByteType, PdfReader]
  2232. ) -> tuple[IOBase, Optional[Encryption]]:
  2233. # If the fileobj parameter is a string, assume it is a path
  2234. # and create a file object at that location. If it is a file,
  2235. # copy the file's contents into a BytesIO stream object; if
  2236. # it is a PdfReader, copy that reader's stream into a
  2237. # BytesIO stream.
  2238. # If fileobj is none of the above types, it is not modified
  2239. encryption_obj = None
  2240. stream: IOBase
  2241. if isinstance(fileobj, (str, Path)):
  2242. with FileIO(fileobj, "rb") as f:
  2243. stream = BytesIO(f.read())
  2244. elif isinstance(fileobj, PdfReader):
  2245. if fileobj._encryption:
  2246. encryption_obj = fileobj._encryption
  2247. orig_tell = fileobj.stream.tell()
  2248. fileobj.stream.seek(0)
  2249. stream = BytesIO(fileobj.stream.read())
  2250. # reset the stream to its original location
  2251. fileobj.stream.seek(orig_tell)
  2252. elif hasattr(fileobj, "seek") and hasattr(fileobj, "read"):
  2253. fileobj.seek(0)
  2254. filecontent = fileobj.read()
  2255. stream = BytesIO(filecontent)
  2256. else:
  2257. raise NotImplementedError(
  2258. "Merging requires an object that PdfReader can parse. "
  2259. "Typically, that is a Path or a string representing a Path, "
  2260. "a file object, or an object implementing .seek and .read. "
  2261. "Passing a PdfReader directly works as well."
  2262. )
  2263. return stream, encryption_obj
  2264. def append(
  2265. self,
  2266. fileobj: Union[StrByteType, PdfReader, Path],
  2267. outline_item: Union[
  2268. str, None, PageRange, tuple[int, int], tuple[int, int, int], list[int]
  2269. ] = None,
  2270. pages: Union[
  2271. None,
  2272. PageRange,
  2273. tuple[int, int],
  2274. tuple[int, int, int],
  2275. list[int],
  2276. list[PageObject],
  2277. ] = None,
  2278. import_outline: bool = True,
  2279. excluded_fields: Optional[Union[list[str], tuple[str, ...]]] = None,
  2280. ) -> None:
  2281. """
  2282. Identical to the :meth:`merge()<merge>` method, but assumes you want to
  2283. concatenate all pages onto the end of the file instead of specifying a
  2284. position.
  2285. Args:
  2286. fileobj: A File Object or an object that supports the standard
  2287. read and seek methods similar to a File Object. Could also be a
  2288. string representing a path to a PDF file.
  2289. outline_item: Optionally, you may specify a string to build an
  2290. outline (aka 'bookmark') to identify the beginning of the
  2291. included file.
  2292. pages: Can be a :class:`PageRange<pypdf.pagerange.PageRange>`
  2293. or a ``(start, stop[, step])`` tuple
  2294. or a list of pages to be processed
  2295. to merge only the specified range of pages from the source
  2296. document into the output document.
  2297. import_outline: You may prevent the source document's
  2298. outline (collection of outline items, previously referred to as
  2299. 'bookmarks') from being imported by specifying this as ``False``.
  2300. excluded_fields: Provide the list of fields/keys to be ignored
  2301. if ``/Annots`` is part of the list, the annotation will be ignored
  2302. if ``/B`` is part of the list, the articles will be ignored
  2303. """
  2304. if excluded_fields is None:
  2305. excluded_fields = ()
  2306. if isinstance(outline_item, (tuple, list, PageRange)):
  2307. if isinstance(pages, bool):
  2308. if not isinstance(import_outline, bool):
  2309. excluded_fields = import_outline
  2310. import_outline = pages
  2311. pages = outline_item
  2312. self.merge(
  2313. None,
  2314. fileobj,
  2315. None,
  2316. pages,
  2317. import_outline,
  2318. excluded_fields,
  2319. )
  2320. else: # if isinstance(outline_item, str):
  2321. self.merge(
  2322. None,
  2323. fileobj,
  2324. outline_item,
  2325. pages,
  2326. import_outline,
  2327. excluded_fields,
  2328. )
  2329. def merge(
  2330. self,
  2331. position: Optional[int],
  2332. fileobj: Union[Path, StrByteType, PdfReader],
  2333. outline_item: Optional[str] = None,
  2334. pages: Optional[Union[PageRangeSpec, list[PageObject]]] = None,
  2335. import_outline: bool = True,
  2336. excluded_fields: Optional[Union[list[str], tuple[str, ...]]] = (),
  2337. ) -> None:
  2338. """
  2339. Merge the pages from the given file into the output file at the
  2340. specified page number.
  2341. Args:
  2342. position: The *page number* to insert this file. File will
  2343. be inserted after the given number.
  2344. fileobj: A File Object or an object that supports the standard
  2345. read and seek methods similar to a File Object. Could also be a
  2346. string representing a path to a PDF file.
  2347. outline_item: Optionally, you may specify a string to build an outline
  2348. (aka 'bookmark') to identify the
  2349. beginning of the included file.
  2350. pages: can be a :class:`PageRange<pypdf.pagerange.PageRange>`
  2351. or a ``(start, stop[, step])`` tuple
  2352. or a list of pages to be processed
  2353. to merge only the specified range of pages from the source
  2354. document into the output document.
  2355. import_outline: You may prevent the source document's
  2356. outline (collection of outline items, previously referred to as
  2357. 'bookmarks') from being imported by specifying this as ``False``.
  2358. excluded_fields: provide the list of fields/keys to be ignored
  2359. if ``/Annots`` is part of the list, the annotation will be ignored
  2360. if ``/B`` is part of the list, the articles will be ignored
  2361. Raises:
  2362. TypeError: The pages attribute is not configured properly
  2363. """
  2364. if isinstance(fileobj, PdfDocCommon):
  2365. reader = fileobj
  2366. else:
  2367. stream, _encryption_obj = self._create_stream(fileobj)
  2368. # Create a new PdfReader instance using the stream
  2369. # (either file or BytesIO or StringIO) created above
  2370. reader = PdfReader(stream, strict=False) # type: ignore[arg-type]
  2371. if excluded_fields is None:
  2372. excluded_fields = ()
  2373. # Find the range of pages to merge.
  2374. if pages is None:
  2375. pages = list(range(len(reader.pages)))
  2376. elif isinstance(pages, PageRange):
  2377. pages = list(range(*pages.indices(len(reader.pages))))
  2378. elif isinstance(pages, list):
  2379. pass # keep unchanged
  2380. elif isinstance(pages, tuple) and len(pages) <= 3:
  2381. pages = list(range(*pages))
  2382. elif not isinstance(pages, tuple):
  2383. raise TypeError(
  2384. '"pages" must be a tuple of (start, stop[, step]) or a list'
  2385. )
  2386. srcpages = {}
  2387. for page in pages:
  2388. if isinstance(page, PageObject):
  2389. pg = page
  2390. else:
  2391. pg = reader.pages[page]
  2392. assert pg.indirect_reference is not None
  2393. if position is None:
  2394. # numbers in the exclude list identifies that the exclusion is
  2395. # only applicable to 1st level of cloning
  2396. srcpages[pg.indirect_reference.idnum] = self.add_page(
  2397. pg, [*list(excluded_fields), 1, "/B", 1, "/Annots"] # type: ignore
  2398. )
  2399. else:
  2400. srcpages[pg.indirect_reference.idnum] = self.insert_page(
  2401. pg, position, [*list(excluded_fields), 1, "/B", 1, "/Annots"] # type: ignore
  2402. )
  2403. position += 1
  2404. srcpages[pg.indirect_reference.idnum].original_page = pg
  2405. reader._named_destinations = (
  2406. reader.named_destinations
  2407. ) # need for the outline processing below
  2408. arr: Any
  2409. for dest in reader._named_destinations.values():
  2410. self._merge__process_named_dests(dest=dest, reader=reader, srcpages=srcpages)
  2411. outline_item_typ: TreeObject
  2412. if outline_item is not None:
  2413. outline_item_typ = cast(
  2414. "TreeObject",
  2415. self.add_outline_item(
  2416. TextStringObject(outline_item),
  2417. next(iter(srcpages.values())).indirect_reference,
  2418. fit=PAGE_FIT,
  2419. ).get_object(),
  2420. )
  2421. else:
  2422. outline_item_typ = self.get_outline_root()
  2423. _ro = reader.root_object
  2424. if import_outline and CO.OUTLINES in _ro:
  2425. outline = self._get_filtered_outline(
  2426. _ro.get(CO.OUTLINES, None), srcpages, reader
  2427. )
  2428. self._insert_filtered_outline(
  2429. outline, outline_item_typ, None
  2430. ) # TODO: use before parameter
  2431. if "/Annots" not in excluded_fields:
  2432. for pag in srcpages.values():
  2433. lst = self._insert_filtered_annotations(
  2434. pag.original_page.get("/Annots", []), pag, srcpages, reader
  2435. )
  2436. if len(lst) > 0:
  2437. pag[NameObject("/Annots")] = lst
  2438. self.clean_page(pag)
  2439. if "/AcroForm" in _ro and not is_null_or_none(_ro["/AcroForm"]):
  2440. if "/AcroForm" not in self._root_object:
  2441. self._root_object[NameObject("/AcroForm")] = self._add_object(
  2442. cast(
  2443. DictionaryObject,
  2444. reader.root_object["/AcroForm"],
  2445. ).clone(self, False, ("/Fields",))
  2446. )
  2447. arr = ArrayObject()
  2448. else:
  2449. arr = cast(
  2450. ArrayObject,
  2451. cast(DictionaryObject, self._root_object["/AcroForm"])["/Fields"],
  2452. )
  2453. trslat = self._id_translated[id(reader)]
  2454. try:
  2455. for f in reader.root_object["/AcroForm"]["/Fields"]: # type: ignore
  2456. try:
  2457. ind = IndirectObject(trslat[f.idnum], 0, self)
  2458. if ind not in arr:
  2459. arr.append(ind)
  2460. except KeyError:
  2461. # for trslat[] which mean the field has not be copied
  2462. # through the page
  2463. pass
  2464. except KeyError: # for /Acroform or /Fields are not existing
  2465. arr = self._add_object(ArrayObject())
  2466. cast(DictionaryObject, self._root_object["/AcroForm"])[
  2467. NameObject("/Fields")
  2468. ] = arr
  2469. if "/B" not in excluded_fields:
  2470. self.add_filtered_articles("", srcpages, reader)
  2471. def _merge__process_named_dests(self, dest: Any, reader: PdfDocCommon, srcpages: dict[int, PageObject]) -> None:
  2472. arr: Any = dest.dest_array
  2473. if "/Names" in self._root_object and dest["/Title"] in cast(
  2474. list[Any],
  2475. cast(
  2476. DictionaryObject,
  2477. cast(DictionaryObject, self._root_object["/Names"]).get("/Dests", DictionaryObject()),
  2478. ).get("/Names", DictionaryObject()),
  2479. ):
  2480. # already exists: should not duplicate it
  2481. pass
  2482. elif dest["/Page"] is None or isinstance(dest["/Page"], NullObject):
  2483. pass
  2484. elif isinstance(dest["/Page"], int):
  2485. # the page reference is a page number normally not a PDF Reference
  2486. # page numbers as int are normally accepted only in external goto
  2487. try:
  2488. p = reader.pages[dest["/Page"]]
  2489. except IndexError:
  2490. return
  2491. assert p.indirect_reference is not None
  2492. try:
  2493. arr[NumberObject(0)] = NumberObject(
  2494. srcpages[p.indirect_reference.idnum].page_number
  2495. )
  2496. self.add_named_destination_array(dest["/Title"], arr)
  2497. except KeyError:
  2498. pass
  2499. elif dest["/Page"].indirect_reference.idnum in srcpages:
  2500. arr[NumberObject(0)] = srcpages[
  2501. dest["/Page"].indirect_reference.idnum
  2502. ].indirect_reference
  2503. self.add_named_destination_array(dest["/Title"], arr)
  2504. def _add_articles_thread(
  2505. self,
  2506. thread: DictionaryObject, # thread entry from the reader's array of threads
  2507. pages: dict[int, PageObject],
  2508. reader: PdfReader,
  2509. ) -> IndirectObject:
  2510. """
  2511. Clone the thread with only the applicable articles.
  2512. Args:
  2513. thread:
  2514. pages:
  2515. reader:
  2516. Returns:
  2517. The added thread as an indirect reference
  2518. """
  2519. nthread = thread.clone(
  2520. self, force_duplicate=True, ignore_fields=("/F",)
  2521. ) # use of clone to keep link between reader and writer
  2522. self.threads.append(nthread.indirect_reference)
  2523. first_article = cast("DictionaryObject", thread["/F"])
  2524. current_article: Optional[DictionaryObject] = first_article
  2525. new_article: Optional[DictionaryObject] = None
  2526. while current_article is not None:
  2527. pag = self._get_cloned_page(
  2528. cast("PageObject", current_article["/P"]), pages, reader
  2529. )
  2530. if pag is not None:
  2531. if new_article is None:
  2532. new_article = cast(
  2533. "DictionaryObject",
  2534. self._add_object(DictionaryObject()).get_object(),
  2535. )
  2536. new_first = new_article
  2537. nthread[NameObject("/F")] = new_article.indirect_reference
  2538. else:
  2539. new_article2 = cast(
  2540. "DictionaryObject",
  2541. self._add_object(
  2542. DictionaryObject(
  2543. {NameObject("/V"): new_article.indirect_reference}
  2544. )
  2545. ).get_object(),
  2546. )
  2547. new_article[NameObject("/N")] = new_article2.indirect_reference
  2548. new_article = new_article2
  2549. new_article[NameObject("/P")] = pag
  2550. new_article[NameObject("/T")] = nthread.indirect_reference
  2551. new_article[NameObject("/R")] = current_article["/R"]
  2552. pag_obj = cast("PageObject", pag.get_object())
  2553. if "/B" not in pag_obj:
  2554. pag_obj[NameObject("/B")] = ArrayObject()
  2555. cast("ArrayObject", pag_obj["/B"]).append(
  2556. new_article.indirect_reference
  2557. )
  2558. current_article = cast("DictionaryObject", current_article["/N"])
  2559. if current_article == first_article:
  2560. new_article[NameObject("/N")] = new_first.indirect_reference # type: ignore
  2561. new_first[NameObject("/V")] = new_article.indirect_reference # type: ignore
  2562. current_article = None
  2563. assert nthread.indirect_reference is not None
  2564. return nthread.indirect_reference
  2565. def add_filtered_articles(
  2566. self,
  2567. fltr: Union[
  2568. Pattern[Any], str
  2569. ], # thread entry from the reader's array of threads
  2570. pages: dict[int, PageObject],
  2571. reader: PdfReader,
  2572. ) -> None:
  2573. """
  2574. Add articles matching the defined criteria.
  2575. Args:
  2576. fltr:
  2577. pages:
  2578. reader:
  2579. """
  2580. if isinstance(fltr, str):
  2581. fltr = re.compile(fltr)
  2582. elif not isinstance(fltr, Pattern):
  2583. fltr = re.compile("")
  2584. for p in pages.values():
  2585. pp = p.original_page
  2586. for a in pp.get("/B", ()):
  2587. a_obj = a.get_object()
  2588. if is_null_or_none(a_obj):
  2589. continue
  2590. thr = a_obj.get("/T")
  2591. if thr is None:
  2592. continue
  2593. thr = thr.get_object()
  2594. if thr.indirect_reference.idnum not in self._id_translated[
  2595. id(reader)
  2596. ] and fltr.search((thr.get("/I", {})).get("/Title", "")):
  2597. self._add_articles_thread(thr, pages, reader)
  2598. def _get_cloned_page(
  2599. self,
  2600. page: Union[None, IndirectObject, PageObject, NullObject],
  2601. pages: dict[int, PageObject],
  2602. reader: PdfReader,
  2603. ) -> Optional[IndirectObject]:
  2604. if isinstance(page, NullObject):
  2605. return None
  2606. if isinstance(page, DictionaryObject) and page.get("/Type", "") == "/Page":
  2607. _i = page.indirect_reference
  2608. elif isinstance(page, IndirectObject):
  2609. _i = page
  2610. try:
  2611. return pages[_i.idnum].indirect_reference # type: ignore
  2612. except Exception:
  2613. return None
  2614. def _insert_filtered_annotations(
  2615. self,
  2616. annots: Union[IndirectObject, list[DictionaryObject], None],
  2617. page: PageObject,
  2618. pages: dict[int, PageObject],
  2619. reader: PdfReader,
  2620. ) -> list[Destination]:
  2621. outlist = ArrayObject()
  2622. if isinstance(annots, IndirectObject):
  2623. annots = cast("list[Any]", annots.get_object())
  2624. if annots is None:
  2625. return outlist
  2626. if not isinstance(annots, list):
  2627. logger_warning(f"Expected list of annotations, got {annots} of type {annots.__class__.__name__}.", __name__)
  2628. return outlist
  2629. for an in annots:
  2630. ano = cast("DictionaryObject", an.get_object())
  2631. if (
  2632. ano["/Subtype"] != "/Link"
  2633. or "/A" not in ano
  2634. or cast("DictionaryObject", ano["/A"])["/S"] != "/GoTo"
  2635. or "/Dest" in ano
  2636. ):
  2637. if "/Dest" not in ano:
  2638. outlist.append(self._add_object(ano.clone(self)))
  2639. else:
  2640. d = ano["/Dest"]
  2641. if isinstance(d, str):
  2642. # it is a named dest
  2643. if str(d) in self.get_named_dest_root():
  2644. outlist.append(ano.clone(self).indirect_reference)
  2645. else:
  2646. d = cast("ArrayObject", d)
  2647. p = self._get_cloned_page(d[0], pages, reader)
  2648. if p is not None:
  2649. anc = ano.clone(self, ignore_fields=("/Dest",))
  2650. anc[NameObject("/Dest")] = ArrayObject([p, *d[1:]])
  2651. outlist.append(self._add_object(anc))
  2652. else:
  2653. d = cast("DictionaryObject", ano["/A"]).get("/D", NullObject())
  2654. if is_null_or_none(d):
  2655. continue
  2656. if isinstance(d, str):
  2657. # it is a named dest
  2658. if str(d) in self.get_named_dest_root():
  2659. outlist.append(ano.clone(self).indirect_reference)
  2660. else:
  2661. d = cast("ArrayObject", d)
  2662. p = self._get_cloned_page(d[0], pages, reader)
  2663. if p is not None:
  2664. anc = ano.clone(self, ignore_fields=("/D",))
  2665. cast("DictionaryObject", anc["/A"])[
  2666. NameObject("/D")
  2667. ] = ArrayObject([p, *d[1:]])
  2668. outlist.append(self._add_object(anc))
  2669. return outlist
  2670. def _get_filtered_outline(
  2671. self,
  2672. node: Any,
  2673. pages: dict[int, PageObject],
  2674. reader: PdfReader,
  2675. ) -> list[Destination]:
  2676. """
  2677. Extract outline item entries that are part of the specified page set.
  2678. Args:
  2679. node:
  2680. pages:
  2681. reader:
  2682. Returns:
  2683. A list of destination objects.
  2684. """
  2685. new_outline = []
  2686. if node is None:
  2687. node = NullObject()
  2688. node = node.get_object()
  2689. if is_null_or_none(node):
  2690. node = DictionaryObject()
  2691. if node.get("/Type", "") == "/Outlines" or "/Title" not in node:
  2692. node = node.get("/First", None)
  2693. if node is not None:
  2694. node = node.get_object()
  2695. new_outline += self._get_filtered_outline(node, pages, reader)
  2696. else:
  2697. v: Union[None, IndirectObject, NullObject]
  2698. while node is not None:
  2699. node = node.get_object()
  2700. o = cast("Destination", reader._build_outline_item(node))
  2701. v = self._get_cloned_page(cast("PageObject", o["/Page"]), pages, reader)
  2702. if v is None:
  2703. v = NullObject()
  2704. o[NameObject("/Page")] = v
  2705. if "/First" in node:
  2706. o._filtered_children = self._get_filtered_outline(
  2707. node["/First"], pages, reader
  2708. )
  2709. else:
  2710. o._filtered_children = []
  2711. if (
  2712. not isinstance(o["/Page"], NullObject)
  2713. or len(o._filtered_children) > 0
  2714. ):
  2715. new_outline.append(o)
  2716. node = node.get("/Next", None)
  2717. return new_outline
  2718. def _clone_outline(self, dest: Destination) -> TreeObject:
  2719. n_ol = TreeObject()
  2720. self._add_object(n_ol)
  2721. n_ol[NameObject("/Title")] = TextStringObject(dest["/Title"])
  2722. if not isinstance(dest["/Page"], NullObject):
  2723. if dest.node is not None and "/A" in dest.node:
  2724. n_ol[NameObject("/A")] = dest.node["/A"].clone(self)
  2725. else:
  2726. n_ol[NameObject("/Dest")] = dest.dest_array
  2727. # TODO: /SE
  2728. if dest.node is not None:
  2729. n_ol[NameObject("/F")] = NumberObject(dest.node.get("/F", 0))
  2730. n_ol[NameObject("/C")] = ArrayObject(
  2731. dest.node.get(
  2732. "/C", [FloatObject(0.0), FloatObject(0.0), FloatObject(0.0)]
  2733. )
  2734. )
  2735. return n_ol
  2736. def _insert_filtered_outline(
  2737. self,
  2738. outlines: list[Destination],
  2739. parent: Union[TreeObject, IndirectObject],
  2740. before: Union[None, TreeObject, IndirectObject] = None,
  2741. ) -> None:
  2742. for dest in outlines:
  2743. # TODO: can be improved to keep A and SE entries (ignored for the moment)
  2744. # with np=self.add_outline_item_destination(dest,parent,before)
  2745. if dest.get("/Type", "") == "/Outlines" or "/Title" not in dest:
  2746. np = parent
  2747. else:
  2748. np = self._clone_outline(dest)
  2749. cast(TreeObject, parent.get_object()).insert_child(np, before, self)
  2750. self._insert_filtered_outline(dest._filtered_children, np, None)
  2751. def close(self) -> None:
  2752. """Implemented for API harmonization."""
  2753. return
  2754. def find_outline_item(
  2755. self,
  2756. outline_item: dict[str, Any],
  2757. root: Optional[OutlineType] = None,
  2758. ) -> Optional[list[int]]:
  2759. if root is None:
  2760. o = self.get_outline_root()
  2761. else:
  2762. o = cast("TreeObject", root)
  2763. i = 0
  2764. while o is not None:
  2765. if (
  2766. o.indirect_reference == outline_item
  2767. or o.get("/Title", None) == outline_item
  2768. ):
  2769. return [i]
  2770. if "/First" in o:
  2771. res = self.find_outline_item(
  2772. outline_item, cast(OutlineType, o["/First"])
  2773. )
  2774. if res:
  2775. return ([i] if "/Title" in o else []) + res
  2776. if "/Next" in o:
  2777. i += 1
  2778. o = cast(TreeObject, o["/Next"])
  2779. else:
  2780. return None
  2781. raise PyPdfError("This line is theoretically unreachable.") # pragma: no cover
  2782. def reset_translation(
  2783. self, reader: Union[None, PdfReader, IndirectObject] = None
  2784. ) -> None:
  2785. """
  2786. Reset the translation table between reader and the writer object.
  2787. Late cloning will create new independent objects.
  2788. Args:
  2789. reader: PdfReader or IndirectObject referencing a PdfReader object.
  2790. if set to None or omitted, all tables will be reset.
  2791. """
  2792. if reader is None:
  2793. self._id_translated = {}
  2794. elif isinstance(reader, PdfReader):
  2795. try:
  2796. del self._id_translated[id(reader)]
  2797. except Exception:
  2798. pass
  2799. elif isinstance(reader, IndirectObject):
  2800. try:
  2801. del self._id_translated[id(reader.pdf)]
  2802. except Exception:
  2803. pass
  2804. else:
  2805. raise Exception("invalid parameter {reader}")
  2806. def set_page_label(
  2807. self,
  2808. page_index_from: int,
  2809. page_index_to: int,
  2810. style: Optional[PageLabelStyle] = None,
  2811. prefix: Optional[str] = None,
  2812. start: Optional[int] = 0,
  2813. ) -> None:
  2814. """
  2815. Set a page label to a range of pages.
  2816. Page indexes must be given starting from 0.
  2817. Labels must have a style, a prefix or both.
  2818. If a range is not assigned any page label, a decimal label starting from 1 is applied.
  2819. Args:
  2820. page_index_from: page index of the beginning of the range starting from 0
  2821. page_index_to: page index of the beginning of the range starting from 0
  2822. style: The numbering style to be used for the numeric portion of each page label:
  2823. * ``/D`` Decimal Arabic numerals
  2824. * ``/R`` Uppercase Roman numerals
  2825. * ``/r`` Lowercase Roman numerals
  2826. * ``/A`` Uppercase letters (A to Z for the first 26 pages,
  2827. AA to ZZ for the next 26, and so on)
  2828. * ``/a`` Lowercase letters (a to z for the first 26 pages,
  2829. aa to zz for the next 26, and so on)
  2830. prefix: The label prefix for page labels in this range.
  2831. start: The value of the numeric portion for the first page label
  2832. in the range.
  2833. Subsequent pages are numbered sequentially from this value,
  2834. which must be greater than or equal to 1.
  2835. Default value: 1.
  2836. """
  2837. if style is None and prefix is None:
  2838. raise ValueError("At least one of style and prefix must be given")
  2839. if page_index_from < 0:
  2840. raise ValueError("page_index_from must be greater or equal than 0")
  2841. if page_index_to < page_index_from:
  2842. raise ValueError(
  2843. "page_index_to must be greater or equal than page_index_from"
  2844. )
  2845. if page_index_to >= len(self.pages):
  2846. raise ValueError("page_index_to exceeds number of pages")
  2847. if start is not None and start != 0 and start < 1:
  2848. raise ValueError("If given, start must be greater or equal than one")
  2849. self._set_page_label(page_index_from, page_index_to, style, prefix, start)
  2850. def _set_page_label(
  2851. self,
  2852. page_index_from: int,
  2853. page_index_to: int,
  2854. style: Optional[PageLabelStyle] = None,
  2855. prefix: Optional[str] = None,
  2856. start: Optional[int] = 0,
  2857. ) -> None:
  2858. """
  2859. Set a page label to a range of pages.
  2860. Page indexes must be given starting from 0.
  2861. Labels must have a style, a prefix or both.
  2862. If a range is not assigned any page label a decimal label starting from 1 is applied.
  2863. Args:
  2864. page_index_from: page index of the beginning of the range starting from 0
  2865. page_index_to: page index of the beginning of the range starting from 0
  2866. style: The numbering style to be used for the numeric portion of each page label:
  2867. /D Decimal Arabic numerals
  2868. /R Uppercase Roman numerals
  2869. /r Lowercase Roman numerals
  2870. /A Uppercase letters (A to Z for the first 26 pages,
  2871. AA to ZZ for the next 26, and so on)
  2872. /a Lowercase letters (a to z for the first 26 pages,
  2873. aa to zz for the next 26, and so on)
  2874. prefix: The label prefix for page labels in this range.
  2875. start: The value of the numeric portion for the first page label
  2876. in the range.
  2877. Subsequent pages are numbered sequentially from this value,
  2878. which must be greater than or equal to 1. Default value: 1.
  2879. """
  2880. default_page_label = DictionaryObject()
  2881. default_page_label[NameObject("/S")] = NameObject("/D")
  2882. new_page_label = DictionaryObject()
  2883. if style is not None:
  2884. new_page_label[NameObject("/S")] = NameObject(style)
  2885. if prefix is not None:
  2886. new_page_label[NameObject("/P")] = TextStringObject(prefix)
  2887. if start != 0:
  2888. new_page_label[NameObject("/St")] = NumberObject(start)
  2889. if NameObject(CatalogDictionary.PAGE_LABELS) not in self._root_object:
  2890. nums = ArrayObject()
  2891. nums_insert(NumberObject(0), default_page_label, nums)
  2892. page_labels = TreeObject()
  2893. page_labels[NameObject("/Nums")] = nums
  2894. self._root_object[NameObject(CatalogDictionary.PAGE_LABELS)] = page_labels
  2895. page_labels = cast(
  2896. TreeObject, self._root_object[NameObject(CatalogDictionary.PAGE_LABELS)]
  2897. )
  2898. nums = cast(ArrayObject, page_labels[NameObject("/Nums")])
  2899. nums_insert(NumberObject(page_index_from), new_page_label, nums)
  2900. nums_clear_range(NumberObject(page_index_from), page_index_to, nums)
  2901. next_label_pos, *_ = nums_next(NumberObject(page_index_from), nums)
  2902. if next_label_pos != page_index_to + 1 and page_index_to + 1 < len(self.pages):
  2903. nums_insert(NumberObject(page_index_to + 1), default_page_label, nums)
  2904. page_labels[NameObject("/Nums")] = nums
  2905. self._root_object[NameObject(CatalogDictionary.PAGE_LABELS)] = page_labels
  2906. def _repr_mimebundle_(
  2907. self,
  2908. include: Union[None, Iterable[str]] = None,
  2909. exclude: Union[None, Iterable[str]] = None,
  2910. ) -> dict[str, Any]:
  2911. """
  2912. Integration into Jupyter Notebooks.
  2913. This method returns a dictionary that maps a mime-type to its
  2914. representation.
  2915. .. seealso::
  2916. https://ipython.readthedocs.io/en/stable/config/integrating.html
  2917. """
  2918. pdf_data = BytesIO()
  2919. self.write(pdf_data)
  2920. data = {
  2921. "application/pdf": pdf_data,
  2922. }
  2923. if include is not None:
  2924. # Filter representations based on include list
  2925. data = {k: v for k, v in data.items() if k in include}
  2926. if exclude is not None:
  2927. # Remove representations based on exclude list
  2928. data = {k: v for k, v in data.items() if k not in exclude}
  2929. return data
  2930. def _pdf_objectify(obj: Union[dict[str, Any], str, float, list[Any]]) -> PdfObject:
  2931. if isinstance(obj, PdfObject):
  2932. return obj
  2933. if isinstance(obj, dict):
  2934. to_add = DictionaryObject()
  2935. for key, value in obj.items():
  2936. to_add[NameObject(key)] = _pdf_objectify(value)
  2937. return to_add
  2938. if isinstance(obj, str):
  2939. if obj.startswith("/"):
  2940. return NameObject(obj)
  2941. return TextStringObject(obj)
  2942. if isinstance(obj, (float, int)):
  2943. return FloatObject(obj)
  2944. if isinstance(obj, list):
  2945. return ArrayObject(_pdf_objectify(i) for i in obj)
  2946. raise NotImplementedError(
  2947. f"{type(obj)=} could not be cast to a PdfObject"
  2948. )
  2949. def _create_outline_item(
  2950. action_ref: Union[None, IndirectObject],
  2951. title: str,
  2952. color: Union[tuple[float, float, float], str, None],
  2953. italic: bool,
  2954. bold: bool,
  2955. ) -> TreeObject:
  2956. outline_item = TreeObject()
  2957. if action_ref is not None:
  2958. outline_item[NameObject("/A")] = action_ref
  2959. outline_item.update(
  2960. {
  2961. NameObject("/Title"): create_string_object(title),
  2962. }
  2963. )
  2964. if color:
  2965. if isinstance(color, str):
  2966. color = hex_to_rgb(color)
  2967. outline_item.update(
  2968. {NameObject("/C"): ArrayObject([FloatObject(c) for c in color])}
  2969. )
  2970. if italic or bold:
  2971. format_flag = 0
  2972. if italic:
  2973. format_flag += OutlineFontFlag.italic
  2974. if bold:
  2975. format_flag += OutlineFontFlag.bold
  2976. outline_item.update({NameObject("/F"): NumberObject(format_flag)})
  2977. return outline_item