_doc_common.py 53 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490
  1. # Copyright (c) 2006, Mathieu Fenniak
  2. # Copyright (c) 2007, Ashish Kulkarni <kulkarni.ashish@gmail.com>
  3. # Copyright (c) 2024, Pubpub-ZZ
  4. #
  5. # All rights reserved.
  6. #
  7. # Redistribution and use in source and binary forms, with or without
  8. # modification, are permitted provided that the following conditions are
  9. # met:
  10. #
  11. # * Redistributions of source code must retain the above copyright notice,
  12. # this list of conditions and the following disclaimer.
  13. # * Redistributions in binary form must reproduce the above copyright notice,
  14. # this list of conditions and the following disclaimer in the documentation
  15. # and/or other materials provided with the distribution.
  16. # * The name of the author may not be used to endorse or promote products
  17. # derived from this software without specific prior written permission.
  18. #
  19. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  20. # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  21. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  22. # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
  23. # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  24. # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  25. # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  26. # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  27. # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  28. # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  29. # POSSIBILITY OF SUCH DAMAGE.
  30. import struct
  31. from abc import abstractmethod
  32. from collections.abc import Generator, Iterable, Iterator, Mapping
  33. from datetime import datetime
  34. from typing import (
  35. Any,
  36. Optional,
  37. Union,
  38. cast,
  39. )
  40. from ._encryption import Encryption
  41. from ._page import PageObject, _VirtualList
  42. from ._page_labels import index2label as page_index2page_label
  43. from ._utils import (
  44. deprecation_with_replacement,
  45. logger_warning,
  46. parse_iso8824_date,
  47. )
  48. from .constants import CatalogAttributes as CA
  49. from .constants import CatalogDictionary as CD
  50. from .constants import (
  51. CheckboxRadioButtonAttributes,
  52. GoToActionArguments,
  53. PagesAttributes,
  54. UserAccessPermissions,
  55. )
  56. from .constants import Core as CO
  57. from .constants import DocumentInformationAttributes as DI
  58. from .constants import FieldDictionaryAttributes as FA
  59. from .constants import PageAttributes as PG
  60. from .errors import PdfReadError, PyPdfError
  61. from .filters import _decompress_with_limit
  62. from .generic import (
  63. ArrayObject,
  64. BooleanObject,
  65. ByteStringObject,
  66. Destination,
  67. DictionaryObject,
  68. EncodedStreamObject,
  69. Field,
  70. Fit,
  71. FloatObject,
  72. IndirectObject,
  73. NameObject,
  74. NullObject,
  75. NumberObject,
  76. PdfObject,
  77. TextStringObject,
  78. TreeObject,
  79. ViewerPreferences,
  80. create_string_object,
  81. is_null_or_none,
  82. )
  83. from .generic._files import EmbeddedFile
  84. from .types import OutlineType, PagemodeType
  85. from .xmp import XmpInformation
  86. def convert_to_int(d: bytes, size: int) -> Union[int, tuple[Any, ...]]:
  87. if size > 8:
  88. raise PdfReadError("Invalid size in convert_to_int")
  89. d = b"\x00\x00\x00\x00\x00\x00\x00\x00" + d
  90. d = d[-8:]
  91. return struct.unpack(">q", d)[0]
  92. class DocumentInformation(DictionaryObject):
  93. """
  94. A class representing the basic document metadata provided in a PDF File.
  95. This class is accessible through
  96. :py:class:`PdfReader.metadata<pypdf.PdfReader.metadata>`.
  97. All text properties of the document metadata have
  98. *two* properties, e.g. author and author_raw. The non-raw property will
  99. always return a ``TextStringObject``, making it ideal for a case where the
  100. metadata is being displayed. The raw property can sometimes return a
  101. ``ByteStringObject``, if pypdf was unable to decode the string's text
  102. encoding; this requires additional safety in the caller and therefore is not
  103. as commonly accessed.
  104. """
  105. def __init__(self) -> None:
  106. DictionaryObject.__init__(self)
  107. def _get_text(self, key: str) -> Optional[str]:
  108. retval = self.get(key, None)
  109. if isinstance(retval, TextStringObject):
  110. return retval
  111. if isinstance(retval, ByteStringObject):
  112. return str(retval)
  113. return None
  114. @property
  115. def title(self) -> Optional[str]:
  116. """
  117. Read-only property accessing the document's title.
  118. Returns a ``TextStringObject`` or ``None`` if the title is not
  119. specified.
  120. """
  121. return (
  122. self._get_text(DI.TITLE) or self.get(DI.TITLE).get_object() # type: ignore
  123. if self.get(DI.TITLE)
  124. else None
  125. )
  126. @property
  127. def title_raw(self) -> Optional[str]:
  128. """The "raw" version of title; can return a ``ByteStringObject``."""
  129. return self.get(DI.TITLE)
  130. @property
  131. def author(self) -> Optional[str]:
  132. """
  133. Read-only property accessing the document's author.
  134. Returns a ``TextStringObject`` or ``None`` if the author is not
  135. specified.
  136. """
  137. return self._get_text(DI.AUTHOR)
  138. @property
  139. def author_raw(self) -> Optional[str]:
  140. """The "raw" version of author; can return a ``ByteStringObject``."""
  141. return self.get(DI.AUTHOR)
  142. @property
  143. def subject(self) -> Optional[str]:
  144. """
  145. Read-only property accessing the document's subject.
  146. Returns a ``TextStringObject`` or ``None`` if the subject is not
  147. specified.
  148. """
  149. return self._get_text(DI.SUBJECT)
  150. @property
  151. def subject_raw(self) -> Optional[str]:
  152. """The "raw" version of subject; can return a ``ByteStringObject``."""
  153. return self.get(DI.SUBJECT)
  154. @property
  155. def creator(self) -> Optional[str]:
  156. """
  157. Read-only property accessing the document's creator.
  158. If the document was converted to PDF from another format, this is the
  159. name of the application (e.g. OpenOffice) that created the original
  160. document from which it was converted. Returns a ``TextStringObject`` or
  161. ``None`` if the creator is not specified.
  162. """
  163. return self._get_text(DI.CREATOR)
  164. @property
  165. def creator_raw(self) -> Optional[str]:
  166. """The "raw" version of creator; can return a ``ByteStringObject``."""
  167. return self.get(DI.CREATOR)
  168. @property
  169. def producer(self) -> Optional[str]:
  170. """
  171. Read-only property accessing the document's producer.
  172. If the document was converted to PDF from another format, this is the
  173. name of the application (for example, macOS Quartz) that converted it to
  174. PDF. Returns a ``TextStringObject`` or ``None`` if the producer is not
  175. specified.
  176. """
  177. return self._get_text(DI.PRODUCER)
  178. @property
  179. def producer_raw(self) -> Optional[str]:
  180. """The "raw" version of producer; can return a ``ByteStringObject``."""
  181. return self.get(DI.PRODUCER)
  182. @property
  183. def creation_date(self) -> Optional[datetime]:
  184. """Read-only property accessing the document's creation date."""
  185. return parse_iso8824_date(self._get_text(DI.CREATION_DATE))
  186. @property
  187. def creation_date_raw(self) -> Optional[str]:
  188. """
  189. The "raw" version of creation date; can return a ``ByteStringObject``.
  190. Typically in the format ``D:YYYYMMDDhhmmss[+Z-]hh'mm`` where the suffix
  191. is the offset from UTC.
  192. """
  193. return self.get(DI.CREATION_DATE)
  194. @property
  195. def modification_date(self) -> Optional[datetime]:
  196. """
  197. Read-only property accessing the document's modification date.
  198. The date and time the document was most recently modified.
  199. """
  200. return parse_iso8824_date(self._get_text(DI.MOD_DATE))
  201. @property
  202. def modification_date_raw(self) -> Optional[str]:
  203. """
  204. The "raw" version of modification date; can return a
  205. ``ByteStringObject``.
  206. Typically in the format ``D:YYYYMMDDhhmmss[+Z-]hh'mm`` where the suffix
  207. is the offset from UTC.
  208. """
  209. return self.get(DI.MOD_DATE)
  210. @property
  211. def keywords(self) -> Optional[str]:
  212. """
  213. Read-only property accessing the document's keywords.
  214. Returns a ``TextStringObject`` or ``None`` if keywords are not
  215. specified.
  216. """
  217. return self._get_text(DI.KEYWORDS)
  218. @property
  219. def keywords_raw(self) -> Optional[str]:
  220. """The "raw" version of keywords; can return a ``ByteStringObject``."""
  221. return self.get(DI.KEYWORDS)
  222. class PdfDocCommon:
  223. """
  224. Common functions from PdfWriter and PdfReader objects.
  225. This root class is strongly abstracted.
  226. """
  227. strict: bool = False # default
  228. flattened_pages: Optional[list[PageObject]] = None
  229. _encryption: Optional[Encryption] = None
  230. _readonly: bool = False
  231. @property
  232. @abstractmethod
  233. def root_object(self) -> DictionaryObject:
  234. ... # pragma: no cover
  235. @property
  236. @abstractmethod
  237. def pdf_header(self) -> str:
  238. ... # pragma: no cover
  239. @abstractmethod
  240. def get_object(
  241. self, indirect_reference: Union[int, IndirectObject]
  242. ) -> Optional[PdfObject]:
  243. ... # pragma: no cover
  244. @abstractmethod
  245. def _replace_object(self, indirect: IndirectObject, obj: PdfObject) -> PdfObject:
  246. ... # pragma: no cover
  247. @property
  248. @abstractmethod
  249. def _info(self) -> Optional[DictionaryObject]:
  250. ... # pragma: no cover
  251. @property
  252. def metadata(self) -> Optional[DocumentInformation]:
  253. """
  254. Retrieve the PDF file's document information dictionary, if it exists.
  255. Note that some PDF files use metadata streams instead of document
  256. information dictionaries, and these metadata streams will not be
  257. accessed by this function.
  258. """
  259. retval = DocumentInformation()
  260. if self._info is None:
  261. return None
  262. retval.update(self._info)
  263. return retval
  264. @property
  265. def xmp_metadata(self) -> Optional[XmpInformation]:
  266. ... # pragma: no cover
  267. @property
  268. def viewer_preferences(self) -> Optional[ViewerPreferences]:
  269. """Returns the existing ViewerPreferences as an overloaded dictionary."""
  270. o = self.root_object.get(CD.VIEWER_PREFERENCES, None)
  271. if o is None:
  272. return None
  273. o = o.get_object()
  274. if not isinstance(o, ViewerPreferences):
  275. o = ViewerPreferences(o)
  276. if hasattr(o, "indirect_reference") and o.indirect_reference is not None:
  277. self._replace_object(o.indirect_reference, o)
  278. else:
  279. self.root_object[NameObject(CD.VIEWER_PREFERENCES)] = o
  280. return o
  281. def get_num_pages(self) -> int:
  282. """
  283. Calculate the number of pages in this PDF file.
  284. Returns:
  285. The number of pages of the parsed PDF file.
  286. Raises:
  287. PdfReadError: If restrictions prevent this action.
  288. """
  289. # Flattened pages will not work on an encrypted PDF;
  290. # the PDF file's page count is used in this case. Otherwise,
  291. # the original method (flattened page count) is used.
  292. if self.is_encrypted:
  293. return self.root_object["/Pages"]["/Count"] # type: ignore
  294. if self.flattened_pages is None:
  295. self._flatten(self._readonly)
  296. assert self.flattened_pages is not None
  297. return len(self.flattened_pages)
  298. def get_page(self, page_number: int) -> PageObject:
  299. """
  300. Retrieve a page by number from this PDF file.
  301. Most of the time ``.pages[page_number]`` is preferred.
  302. Args:
  303. page_number: The page number to retrieve
  304. (pages begin at zero)
  305. Returns:
  306. A :class:`PageObject<pypdf._page.PageObject>` instance.
  307. """
  308. if self.flattened_pages is None:
  309. self._flatten(self._readonly)
  310. assert self.flattened_pages is not None, "hint for mypy"
  311. return self.flattened_pages[page_number]
  312. def _get_page_in_node(
  313. self,
  314. page_number: int,
  315. ) -> tuple[DictionaryObject, int]:
  316. """
  317. Retrieve the node and position within the /Kids containing the page.
  318. If page_number is greater than the number of pages, it returns the top node, -1.
  319. """
  320. top = cast(DictionaryObject, self.root_object["/Pages"])
  321. def recursive_call(
  322. node: DictionaryObject, mi: int
  323. ) -> tuple[Optional[PdfObject], int]:
  324. ma = cast(int, node.get("/Count", 1)) # default 1 for /Page types
  325. if node["/Type"] == "/Page":
  326. if page_number == mi:
  327. return node, -1
  328. return None, mi + 1
  329. if (page_number - mi) >= ma: # not in nodes below
  330. if node == top:
  331. return top, -1
  332. return None, mi + ma
  333. for idx, kid in enumerate(cast(ArrayObject, node["/Kids"])):
  334. kid = cast(DictionaryObject, kid.get_object())
  335. n, i = recursive_call(kid, mi)
  336. if n is not None: # page has just been found ...
  337. if i < 0: # ... just below!
  338. return node, idx
  339. # ... at lower levels
  340. return n, i
  341. mi = i
  342. raise PyPdfError("Unexpectedly cannot find the node.")
  343. node, idx = recursive_call(top, 0)
  344. assert isinstance(node, DictionaryObject), "mypy"
  345. return node, idx
  346. @property
  347. def named_destinations(self) -> dict[str, Destination]:
  348. """A read-only dictionary which maps names to destinations."""
  349. return self._get_named_destinations()
  350. def get_named_dest_root(self) -> ArrayObject:
  351. named_dest = ArrayObject()
  352. if CA.NAMES in self.root_object and isinstance(
  353. self.root_object[CA.NAMES], DictionaryObject
  354. ):
  355. names = cast(DictionaryObject, self.root_object[CA.NAMES])
  356. if CA.DESTS in names and isinstance(names[CA.DESTS], DictionaryObject):
  357. # §3.6.3 Name Dictionary (PDF spec 1.7)
  358. dests = cast(DictionaryObject, names[CA.DESTS])
  359. dests_ref = dests.indirect_reference
  360. if CA.NAMES in dests:
  361. # §7.9.6, entries in a name tree node dictionary
  362. named_dest = cast(ArrayObject, dests[CA.NAMES])
  363. else:
  364. named_dest = ArrayObject()
  365. dests[NameObject(CA.NAMES)] = named_dest
  366. elif hasattr(self, "_add_object"):
  367. dests = DictionaryObject()
  368. dests_ref = self._add_object(dests)
  369. names[NameObject(CA.DESTS)] = dests_ref
  370. dests[NameObject(CA.NAMES)] = named_dest
  371. elif hasattr(self, "_add_object"):
  372. names = DictionaryObject()
  373. names_ref = self._add_object(names)
  374. self.root_object[NameObject(CA.NAMES)] = names_ref
  375. dests = DictionaryObject()
  376. dests_ref = self._add_object(dests)
  377. names[NameObject(CA.DESTS)] = dests_ref
  378. dests[NameObject(CA.NAMES)] = named_dest
  379. return named_dest
  380. ## common
  381. def _get_named_destinations(
  382. self,
  383. tree: Union[TreeObject, None] = None,
  384. retval: Optional[dict[str, Destination]] = None,
  385. ) -> dict[str, Destination]:
  386. """
  387. Retrieve the named destinations present in the document.
  388. Args:
  389. tree: The current tree.
  390. retval: The previously retrieved destinations for nested calls.
  391. Returns:
  392. A dictionary which maps names to destinations.
  393. """
  394. if retval is None:
  395. retval = {}
  396. catalog = self.root_object
  397. # get the name tree
  398. if CA.DESTS in catalog:
  399. tree = cast(TreeObject, catalog[CA.DESTS])
  400. elif CA.NAMES in catalog:
  401. names = cast(DictionaryObject, catalog[CA.NAMES])
  402. if CA.DESTS in names:
  403. tree = cast(TreeObject, names[CA.DESTS])
  404. if is_null_or_none(tree):
  405. return retval
  406. assert tree is not None, "mypy"
  407. if PagesAttributes.KIDS in tree:
  408. # recurse down the tree
  409. for kid in cast(ArrayObject, tree[PagesAttributes.KIDS]):
  410. self._get_named_destinations(kid.get_object(), retval)
  411. # §7.9.6, entries in a name tree node dictionary
  412. elif CA.NAMES in tree: # /Kids and /Names are exclusives (§7.9.6)
  413. names = cast(DictionaryObject, tree[CA.NAMES])
  414. i = 0
  415. while i < len(names):
  416. key = names[i].get_object()
  417. i += 1
  418. if not isinstance(key, (bytes, str)):
  419. continue
  420. try:
  421. value = names[i].get_object()
  422. except IndexError:
  423. break
  424. i += 1
  425. if isinstance(value, DictionaryObject):
  426. if "/D" in value:
  427. value = value["/D"]
  428. else:
  429. continue
  430. dest = self._build_destination(key, value)
  431. if dest is not None:
  432. retval[cast(str, dest["/Title"])] = dest
  433. # Remain backwards-compatible.
  434. retval[str(key)] = dest
  435. else: # case where Dests is in root catalog (PDF 1.7 specs, §2 about PDF 1.1)
  436. for k__, v__ in tree.items():
  437. val = v__.get_object()
  438. if isinstance(val, DictionaryObject):
  439. if "/D" in val:
  440. val = val["/D"].get_object()
  441. else:
  442. continue
  443. dest = self._build_destination(k__, val)
  444. if dest is not None:
  445. retval[k__] = dest
  446. return retval
  447. # A select group of relevant field attributes. For the complete list,
  448. # see §12.3.2 of the PDF 1.7 or PDF 2.0 specification.
  449. def get_fields(
  450. self,
  451. tree: Optional[TreeObject] = None,
  452. retval: Optional[dict[Any, Any]] = None,
  453. fileobj: Optional[Any] = None,
  454. stack: Optional[list[PdfObject]] = None,
  455. ) -> Optional[dict[str, Any]]:
  456. """
  457. Extract field data if this PDF contains interactive form fields.
  458. The *tree*, *retval*, *stack* parameters are for recursive use.
  459. Args:
  460. tree: Current object to parse.
  461. retval: In-progress list of fields.
  462. fileobj: A file object (usually a text file) to write
  463. a report to on all interactive form fields found.
  464. stack: List of already parsed objects.
  465. Returns:
  466. A dictionary where each key is a field name, and each
  467. value is a :class:`Field<pypdf.generic.Field>` object. By
  468. default, the mapping name is used for keys.
  469. ``None`` if form data could not be located.
  470. """
  471. field_attributes = FA.attributes_dict()
  472. field_attributes.update(CheckboxRadioButtonAttributes.attributes_dict())
  473. if retval is None:
  474. retval = {}
  475. catalog = self.root_object
  476. stack = []
  477. # get the AcroForm tree
  478. if CD.ACRO_FORM in catalog:
  479. tree = cast(Optional[TreeObject], catalog[CD.ACRO_FORM])
  480. else:
  481. return None
  482. if tree is None:
  483. return retval
  484. assert stack is not None
  485. if "/Fields" in tree:
  486. fields = cast(ArrayObject, tree["/Fields"])
  487. for f in fields:
  488. field = f.get_object()
  489. self._build_field(field, retval, fileobj, field_attributes, stack)
  490. elif any(attr in tree for attr in field_attributes):
  491. # Tree is a field
  492. self._build_field(tree, retval, fileobj, field_attributes, stack)
  493. return retval
  494. def _get_qualified_field_name(self, parent: DictionaryObject) -> str:
  495. if "/TM" in parent:
  496. return cast(str, parent["/TM"])
  497. if "/Parent" in parent:
  498. return (
  499. self._get_qualified_field_name(
  500. cast(DictionaryObject, parent["/Parent"])
  501. )
  502. + "."
  503. + cast(str, parent.get("/T", ""))
  504. )
  505. return cast(str, parent.get("/T", ""))
  506. def _build_field(
  507. self,
  508. field: Union[TreeObject, DictionaryObject],
  509. retval: dict[Any, Any],
  510. fileobj: Any,
  511. field_attributes: Any,
  512. stack: list[PdfObject],
  513. ) -> None:
  514. if all(attr not in field for attr in ("/T", "/TM")):
  515. return
  516. key = self._get_qualified_field_name(field)
  517. if fileobj:
  518. self._write_field(fileobj, field, field_attributes)
  519. fileobj.write("\n")
  520. retval[key] = Field(field)
  521. obj = retval[key].indirect_reference.get_object() # to get the full object
  522. if obj.get(FA.FT, "") == "/Ch" and obj.get(NameObject(FA.Opt)):
  523. retval[key][NameObject("/_States_")] = obj[NameObject(FA.Opt)]
  524. if obj.get(FA.FT, "") == "/Btn" and "/AP" in obj:
  525. # Checkbox
  526. retval[key][NameObject("/_States_")] = ArrayObject(
  527. list(obj["/AP"]["/N"].keys())
  528. )
  529. if "/Off" not in retval[key]["/_States_"]:
  530. retval[key][NameObject("/_States_")].append(NameObject("/Off"))
  531. elif obj.get(FA.FT, "") == "/Btn" and obj.get(FA.Ff, 0) & FA.FfBits.Radio != 0:
  532. states: list[str] = []
  533. retval[key][NameObject("/_States_")] = ArrayObject(states)
  534. for k in obj.get(FA.Kids, {}):
  535. k = k.get_object()
  536. for s in list(k["/AP"]["/N"].keys()):
  537. if s not in states:
  538. states.append(s)
  539. retval[key][NameObject("/_States_")] = ArrayObject(states)
  540. if (
  541. obj.get(FA.Ff, 0) & FA.FfBits.NoToggleToOff != 0
  542. and "/Off" in retval[key]["/_States_"]
  543. ):
  544. del retval[key]["/_States_"][retval[key]["/_States_"].index("/Off")]
  545. # at last for order
  546. self._check_kids(field, retval, fileobj, stack)
  547. def _check_kids(
  548. self,
  549. tree: Union[TreeObject, DictionaryObject],
  550. retval: Any,
  551. fileobj: Any,
  552. stack: list[PdfObject],
  553. ) -> None:
  554. if tree in stack:
  555. logger_warning(
  556. f"{self._get_qualified_field_name(tree)} already parsed", __name__
  557. )
  558. return
  559. stack.append(tree)
  560. if PagesAttributes.KIDS in tree:
  561. # recurse down the tree
  562. for kid in tree[PagesAttributes.KIDS]: # type: ignore
  563. kid = kid.get_object()
  564. self.get_fields(kid, retval, fileobj, stack)
  565. def _write_field(self, fileobj: Any, field: Any, field_attributes: Any) -> None:
  566. field_attributes_tuple = FA.attributes()
  567. field_attributes_tuple = (
  568. field_attributes_tuple + CheckboxRadioButtonAttributes.attributes()
  569. )
  570. for attr in field_attributes_tuple:
  571. if attr in (
  572. FA.Kids,
  573. FA.AA,
  574. ):
  575. continue
  576. attr_name = field_attributes[attr]
  577. try:
  578. if attr == FA.FT:
  579. # Make the field type value clearer
  580. types = {
  581. "/Btn": "Button",
  582. "/Tx": "Text",
  583. "/Ch": "Choice",
  584. "/Sig": "Signature",
  585. }
  586. if field[attr] in types:
  587. fileobj.write(f"{attr_name}: {types[field[attr]]}\n")
  588. elif attr == FA.Parent:
  589. # Let's just write the name of the parent
  590. try:
  591. name = field[attr][FA.TM]
  592. except KeyError:
  593. name = field[attr][FA.T]
  594. fileobj.write(f"{attr_name}: {name}\n")
  595. else:
  596. fileobj.write(f"{attr_name}: {field[attr]}\n")
  597. except KeyError:
  598. # Field attribute is N/A or unknown, so don't write anything
  599. pass
  600. def get_form_text_fields(self, full_qualified_name: bool = False) -> dict[str, Any]:
  601. """
  602. Retrieve form fields from the document with textual data.
  603. Args:
  604. full_qualified_name: to get full name
  605. Returns:
  606. A dictionary. The key is the name of the form field,
  607. the value is the content of the field.
  608. If the document contains multiple form fields with the same name, the
  609. second and following will get the suffix .2, .3, ...
  610. """
  611. def indexed_key(k: str, fields: dict[Any, Any]) -> str:
  612. if k not in fields:
  613. return k
  614. return (
  615. k
  616. + "."
  617. + str(sum(1 for kk in fields if kk.startswith(k + ".")) + 2)
  618. )
  619. # Retrieve document form fields
  620. formfields = self.get_fields()
  621. if formfields is None:
  622. return {}
  623. ff = {}
  624. for field, value in formfields.items():
  625. if value.get("/FT") == "/Tx":
  626. if full_qualified_name:
  627. ff[field] = value.get("/V")
  628. else:
  629. ff[indexed_key(cast(str, value["/T"]), ff)] = value.get("/V")
  630. return ff
  631. def get_pages_showing_field(
  632. self, field: Union[Field, PdfObject, IndirectObject]
  633. ) -> list[PageObject]:
  634. """
  635. Provides list of pages where the field is called.
  636. Args:
  637. field: Field Object, PdfObject or IndirectObject referencing a Field
  638. Returns:
  639. List of pages:
  640. - Empty list:
  641. The field has no widgets attached
  642. (either hidden field or ancestor field).
  643. - Single page list:
  644. Page where the widget is present
  645. (most common).
  646. - Multi-page list:
  647. Field with multiple kids widgets
  648. (example: radio buttons, field repeated on multiple pages).
  649. """
  650. def _get_inherited(obj: DictionaryObject, key: str) -> Any:
  651. if key in obj:
  652. return obj[key]
  653. if "/Parent" in obj:
  654. return _get_inherited(
  655. cast(DictionaryObject, obj["/Parent"].get_object()), key
  656. )
  657. return None
  658. try:
  659. # to cope with all types
  660. field = cast(DictionaryObject, field.indirect_reference.get_object()) # type: ignore
  661. except Exception as exc:
  662. raise ValueError("Field type is invalid") from exc
  663. if is_null_or_none(_get_inherited(field, "/FT")):
  664. raise ValueError("Field is not valid")
  665. ret = []
  666. if field.get("/Subtype", "") == "/Widget":
  667. if "/P" in field:
  668. ret = [field["/P"].get_object()]
  669. else:
  670. ret = [
  671. p
  672. for p in self.pages
  673. if field.indirect_reference in p.get("/Annots", "")
  674. ]
  675. else:
  676. kids = field.get("/Kids", ())
  677. for k in kids:
  678. k = k.get_object()
  679. if (k.get("/Subtype", "") == "/Widget") and ("/T" not in k):
  680. # Kid that is just a widget, not a field:
  681. if "/P" in k:
  682. ret += [k["/P"].get_object()]
  683. else:
  684. ret += [
  685. p
  686. for p in self.pages
  687. if k.indirect_reference in p.get("/Annots", "")
  688. ]
  689. return [
  690. x
  691. if isinstance(x, PageObject)
  692. else (self.pages[self._get_page_number_by_indirect(x.indirect_reference)]) # type: ignore
  693. for x in ret
  694. ]
  695. @property
  696. def open_destination(
  697. self,
  698. ) -> Union[None, Destination, TextStringObject, ByteStringObject]:
  699. """
  700. Property to access the opening destination (``/OpenAction`` entry in
  701. the PDF catalog). It returns ``None`` if the entry does not exist
  702. or is not set.
  703. Raises:
  704. Exception: If a destination is invalid.
  705. """
  706. if "/OpenAction" not in self.root_object:
  707. return None
  708. oa: Any = self.root_object["/OpenAction"]
  709. if isinstance(oa, bytes): # pragma: no cover
  710. oa = oa.decode()
  711. if isinstance(oa, str):
  712. return create_string_object(oa)
  713. if isinstance(oa, ArrayObject):
  714. try:
  715. page, typ, *array = oa
  716. fit = Fit(typ, tuple(array))
  717. return Destination("OpenAction", page, fit)
  718. except Exception as exc:
  719. raise Exception(f"Invalid Destination {oa}: {exc}")
  720. else:
  721. return None
  722. @open_destination.setter
  723. def open_destination(self, dest: Union[None, str, Destination, PageObject]) -> None:
  724. raise NotImplementedError("No setter for open_destination")
  725. @property
  726. def outline(self) -> OutlineType:
  727. """
  728. Read-only property for the outline present in the document
  729. (i.e., a collection of 'outline items' which are also known as
  730. 'bookmarks').
  731. """
  732. return self._get_outline()
  733. def _get_outline(
  734. self,
  735. node: Optional[DictionaryObject] = None,
  736. outline: Optional[Any] = None,
  737. visited: Optional[set[int]] = None,
  738. ) -> OutlineType:
  739. if outline is None:
  740. outline = []
  741. catalog = self.root_object
  742. # get the outline dictionary and named destinations
  743. if CO.OUTLINES in catalog:
  744. lines = cast(DictionaryObject, catalog[CO.OUTLINES])
  745. if isinstance(lines, NullObject):
  746. return outline
  747. # §12.3.3 Document outline, entries in the outline dictionary
  748. if not is_null_or_none(lines) and "/First" in lines:
  749. node = cast(DictionaryObject, lines["/First"])
  750. self._named_destinations = self._get_named_destinations()
  751. if node is None:
  752. return outline
  753. # see if there are any more outline items
  754. if visited is None:
  755. visited = set()
  756. while True:
  757. node_id = id(node)
  758. if node_id in visited:
  759. logger_warning(f"Detected cycle in outline structure for {node}", __name__)
  760. break
  761. visited.add(node_id)
  762. outline_obj = self._build_outline_item(node)
  763. if outline_obj:
  764. outline.append(outline_obj)
  765. # check for sub-outline
  766. if "/First" in node:
  767. sub_outline: list[Any] = []
  768. # Pass a copy to allow multiple outer entries to reference the same inner one.
  769. inner_visited = visited.copy()
  770. self._get_outline(
  771. node=cast(DictionaryObject, node["/First"]),
  772. outline=sub_outline,
  773. visited=inner_visited,
  774. )
  775. if sub_outline:
  776. outline.append(sub_outline)
  777. if "/Next" not in node:
  778. break
  779. node = cast(DictionaryObject, node["/Next"])
  780. return outline
  781. @property
  782. def threads(self) -> Optional[ArrayObject]:
  783. """
  784. Read-only property for the list of threads.
  785. See §12.4.3 from the PDF 1.7 or 2.0 specification.
  786. It is an array of dictionaries with "/F" (the first bead in the thread)
  787. and "/I" (a thread information dictionary containing information about
  788. the thread, such as its title, author, and creation date) properties or
  789. None if there are no articles.
  790. Since PDF 2.0 it can also contain an indirect reference to a metadata
  791. stream containing information about the thread, such as its title,
  792. author, and creation date.
  793. """
  794. catalog = self.root_object
  795. if CO.THREADS in catalog:
  796. return cast("ArrayObject", catalog[CO.THREADS])
  797. return None
  798. @abstractmethod
  799. def _get_page_number_by_indirect(
  800. self, indirect_reference: Union[None, int, NullObject, IndirectObject]
  801. ) -> Optional[int]:
  802. ... # pragma: no cover
  803. def get_page_number(self, page: PageObject) -> Optional[int]:
  804. """
  805. Retrieve page number of a given PageObject.
  806. Args:
  807. page: The page to get page number. Should be
  808. an instance of :class:`PageObject<pypdf._page.PageObject>`
  809. Returns:
  810. The page number or None if page is not found
  811. """
  812. return self._get_page_number_by_indirect(page.indirect_reference)
  813. def get_destination_page_number(self, destination: Destination) -> Optional[int]:
  814. """
  815. Retrieve page number of a given Destination object.
  816. Args:
  817. destination: The destination to get page number.
  818. Returns:
  819. The page number or None if page is not found
  820. """
  821. return self._get_page_number_by_indirect(destination.page)
  822. def _build_destination(
  823. self,
  824. title: Union[str, bytes],
  825. array: Optional[
  826. list[
  827. Union[NumberObject, IndirectObject, None, NullObject, DictionaryObject]
  828. ]
  829. ],
  830. ) -> Destination:
  831. page, typ = None, None
  832. # handle outline items with missing or invalid destination
  833. if (
  834. isinstance(array, (NullObject, str))
  835. or (isinstance(array, ArrayObject) and len(array) == 0)
  836. or array is None
  837. ):
  838. page = NullObject()
  839. return Destination(title, page, Fit.fit())
  840. page, typ, *array = array # type: ignore
  841. try:
  842. return Destination(title, page, Fit(fit_type=typ, fit_args=array)) # type: ignore
  843. except PdfReadError:
  844. logger_warning(f"Unknown destination: {title!r} {array}", __name__)
  845. if self.strict:
  846. raise
  847. # create a link to first Page
  848. tmp = self.pages[0].indirect_reference
  849. indirect_reference = NullObject() if tmp is None else tmp
  850. return Destination(title, indirect_reference, Fit.fit())
  851. def _build_outline_item(self, node: DictionaryObject) -> Optional[Destination]:
  852. dest, title, outline_item = None, None, None
  853. # title required for valid outline
  854. # §12.3.3, entries in an outline item dictionary
  855. try:
  856. title = cast("str", node["/Title"])
  857. except KeyError:
  858. if self.strict:
  859. raise PdfReadError(f"Outline Entry Missing /Title attribute: {node!r}")
  860. title = ""
  861. if "/A" in node:
  862. # Action, PDF 1.7 and PDF 2.0 §12.6 (only type GoTo supported)
  863. action = cast(DictionaryObject, node["/A"])
  864. action_type = cast(NameObject, action[GoToActionArguments.S])
  865. if action_type == "/GoTo":
  866. if GoToActionArguments.D in action:
  867. dest = action[GoToActionArguments.D]
  868. elif self.strict:
  869. raise PdfReadError(f"Outline Action Missing /D attribute: {node!r}")
  870. elif "/Dest" in node:
  871. # Destination, PDF 1.7 and PDF 2.0 §12.3.2
  872. dest = node["/Dest"]
  873. # if array was referenced in another object, will be a dict w/ key "/D"
  874. if isinstance(dest, DictionaryObject) and "/D" in dest:
  875. dest = dest["/D"]
  876. if isinstance(dest, ArrayObject):
  877. outline_item = self._build_destination(title, dest)
  878. elif isinstance(dest, str):
  879. # named destination, addresses NameObject Issue #193
  880. # TODO: Keep named destination instead of replacing it?
  881. try:
  882. outline_item = self._build_destination(
  883. title, self._named_destinations[dest].dest_array
  884. )
  885. except KeyError:
  886. # named destination not found in Name Dict
  887. outline_item = self._build_destination(title, None)
  888. elif dest is None:
  889. # outline item not required to have destination or action
  890. # PDFv1.7 Table 153
  891. outline_item = self._build_destination(title, dest)
  892. else:
  893. if self.strict:
  894. raise PdfReadError(f"Unexpected destination {dest!r}")
  895. logger_warning(
  896. f"Removed unexpected destination {dest!r} from destination",
  897. __name__,
  898. )
  899. outline_item = self._build_destination(title, None)
  900. # if outline item created, add color, format, and child count if present
  901. if outline_item:
  902. if "/C" in node:
  903. # Color of outline item font in (R, G, B) with values ranging 0.0-1.0
  904. outline_item[NameObject("/C")] = ArrayObject(FloatObject(c) for c in node["/C"]) # type: ignore
  905. if "/F" in node:
  906. # specifies style characteristics bold and/or italic
  907. # with 1=italic, 2=bold, 3=both
  908. outline_item[NameObject("/F")] = node["/F"]
  909. if "/Count" in node:
  910. # absolute value = num. visible children
  911. # with positive = open/unfolded, negative = closed/folded
  912. outline_item[NameObject("/Count")] = node["/Count"]
  913. # if count is 0 we will consider it as open (to have available is_open)
  914. outline_item[NameObject("/%is_open%")] = BooleanObject(
  915. node.get("/Count", 0) >= 0
  916. )
  917. outline_item.node = node
  918. try:
  919. outline_item.indirect_reference = node.indirect_reference
  920. except AttributeError:
  921. pass
  922. return outline_item
  923. @property
  924. def pages(self) -> list[PageObject]:
  925. """
  926. Property that emulates a list of :class:`PageObject<pypdf._page.PageObject>`.
  927. This property allows to get a page or a range of pages.
  928. Note:
  929. For PdfWriter only: Provides the capability to remove a page/range of
  930. page from the list (using the del operator). Remember: Only the page
  931. entry is removed, as the objects beneath can be used elsewhere. A
  932. solution to completely remove them - if they are not used anywhere - is
  933. to write to a buffer/temporary file and then load it into a new
  934. PdfWriter.
  935. """
  936. return _VirtualList(self.get_num_pages, self.get_page) # type: ignore
  937. @property
  938. def page_labels(self) -> list[str]:
  939. """
  940. A list of labels for the pages in this document.
  941. This property is read-only. The labels are in the order that the pages
  942. appear in the document.
  943. """
  944. return [page_index2page_label(self, i) for i in range(len(self.pages))]
  945. @property
  946. def page_layout(self) -> Optional[str]:
  947. """
  948. Get the page layout currently being used.
  949. .. list-table:: Valid ``layout`` values
  950. :widths: 50 200
  951. * - /NoLayout
  952. - Layout explicitly not specified
  953. * - /SinglePage
  954. - Show one page at a time
  955. * - /OneColumn
  956. - Show one column at a time
  957. * - /TwoColumnLeft
  958. - Show pages in two columns, odd-numbered pages on the left
  959. * - /TwoColumnRight
  960. - Show pages in two columns, odd-numbered pages on the right
  961. * - /TwoPageLeft
  962. - Show two pages at a time, odd-numbered pages on the left
  963. * - /TwoPageRight
  964. - Show two pages at a time, odd-numbered pages on the right
  965. """
  966. try:
  967. return cast(NameObject, self.root_object[CD.PAGE_LAYOUT])
  968. except KeyError:
  969. return None
  970. @property
  971. def page_mode(self) -> Optional[PagemodeType]:
  972. """
  973. Get the page mode currently being used.
  974. .. list-table:: Valid ``mode`` values
  975. :widths: 50 200
  976. * - /UseNone
  977. - Do not show outline or thumbnails panels
  978. * - /UseOutlines
  979. - Show outline (aka bookmarks) panel
  980. * - /UseThumbs
  981. - Show page thumbnails panel
  982. * - /FullScreen
  983. - Fullscreen view
  984. * - /UseOC
  985. - Show Optional Content Group (OCG) panel
  986. * - /UseAttachments
  987. - Show attachments panel
  988. """
  989. try:
  990. return self.root_object["/PageMode"] # type: ignore
  991. except KeyError:
  992. return None
  993. def _flatten(
  994. self,
  995. list_only: bool = False,
  996. pages: Union[None, DictionaryObject, PageObject] = None,
  997. inherit: Optional[dict[str, Any]] = None,
  998. indirect_reference: Optional[IndirectObject] = None,
  999. ) -> None:
  1000. """
  1001. Process the document pages to ease searching.
  1002. Attributes of a page may inherit from ancestor nodes
  1003. in the page tree. Flattening means moving
  1004. any inheritance data into descendant nodes,
  1005. effectively removing the inheritance dependency.
  1006. Note: It is distinct from another use of "flattening" applied to PDFs.
  1007. Flattening a PDF also means combining all the contents into one single layer
  1008. and making the file less editable.
  1009. Args:
  1010. list_only: Will only list the pages within _flatten_pages.
  1011. pages:
  1012. inherit:
  1013. indirect_reference: Used recursively to flatten the /Pages object.
  1014. """
  1015. inheritable_page_attributes = (
  1016. NameObject(PG.RESOURCES),
  1017. NameObject(PG.MEDIABOX),
  1018. NameObject(PG.CROPBOX),
  1019. NameObject(PG.ROTATE),
  1020. )
  1021. if inherit is None:
  1022. inherit = {}
  1023. if pages is None:
  1024. # Fix issue 327: set flattened_pages attribute only for
  1025. # decrypted file
  1026. catalog = self.root_object
  1027. pages = catalog.get("/Pages").get_object() # type: ignore
  1028. if not isinstance(pages, DictionaryObject):
  1029. raise PdfReadError("Invalid object in /Pages")
  1030. self.flattened_pages = []
  1031. if PagesAttributes.TYPE in pages:
  1032. t = cast(str, pages[PagesAttributes.TYPE])
  1033. # if the page tree node has no /Type, consider as a page if /Kids is also missing
  1034. elif PagesAttributes.KIDS not in pages:
  1035. t = "/Page"
  1036. else:
  1037. t = "/Pages"
  1038. if t == "/Pages":
  1039. for attr in inheritable_page_attributes:
  1040. if attr in pages:
  1041. inherit[attr] = pages[attr]
  1042. pages_reference = getattr(pages, "indirect_reference", object())
  1043. for page in cast(ArrayObject, pages[PagesAttributes.KIDS]):
  1044. if getattr(page, "indirect_reference", object()) == pages_reference:
  1045. raise PdfReadError("Detected cyclic page references.")
  1046. addt = {}
  1047. if isinstance(page, IndirectObject):
  1048. addt["indirect_reference"] = page
  1049. obj = page.get_object()
  1050. if obj:
  1051. # damaged file may have invalid child in /Pages
  1052. try:
  1053. self._flatten(list_only, obj, inherit, **addt)
  1054. except RecursionError:
  1055. raise PdfReadError(
  1056. "Maximum recursion depth reached during page flattening."
  1057. )
  1058. elif t == "/Page":
  1059. for attr_in, value in inherit.items():
  1060. # if the page has its own value, it does not inherit the
  1061. # parent's value
  1062. if attr_in not in pages:
  1063. pages[attr_in] = value
  1064. page_obj = PageObject(self, indirect_reference)
  1065. if not list_only:
  1066. page_obj.update(pages)
  1067. # TODO: Could flattened_pages be None at this point?
  1068. self.flattened_pages.append(page_obj) # type: ignore
  1069. def remove_page(
  1070. self,
  1071. page: Union[int, PageObject, IndirectObject],
  1072. clean: bool = False,
  1073. ) -> None:
  1074. """
  1075. Remove page from pages list.
  1076. Args:
  1077. page:
  1078. * :class:`int`: Page number to be removed.
  1079. * :class:`~pypdf._page.PageObject`: page to be removed. If the page appears many times
  1080. only the first one will be removed.
  1081. * :class:`~pypdf.generic.IndirectObject`: Reference to page to be removed.
  1082. clean: replace PageObject with NullObject to prevent annotations
  1083. or destinations to reference a detached page.
  1084. """
  1085. if self.flattened_pages is None:
  1086. self._flatten(self._readonly)
  1087. assert self.flattened_pages is not None
  1088. if isinstance(page, IndirectObject):
  1089. p = page.get_object()
  1090. if not isinstance(p, PageObject):
  1091. logger_warning("IndirectObject is not referencing a page", __name__)
  1092. return
  1093. page = p
  1094. if not isinstance(page, int):
  1095. try:
  1096. page = self.flattened_pages.index(page)
  1097. except ValueError:
  1098. logger_warning("Cannot find page in pages", __name__)
  1099. return
  1100. if not (0 <= page < len(self.flattened_pages)):
  1101. logger_warning("Page number is out of range", __name__)
  1102. return
  1103. ind = self.pages[page].indirect_reference
  1104. del self.pages[page]
  1105. if clean and ind is not None:
  1106. self._replace_object(ind, NullObject())
  1107. def _get_indirect_object(self, num: int, gen: int) -> Optional[PdfObject]:
  1108. """
  1109. Used to ease development.
  1110. This is equivalent to generic.IndirectObject(num,gen,self).get_object()
  1111. Args:
  1112. num: The object number of the indirect object.
  1113. gen: The generation number of the indirect object.
  1114. Returns:
  1115. A PdfObject
  1116. """
  1117. return IndirectObject(num, gen, self).get_object()
  1118. def decode_permissions(
  1119. self, permissions_code: int
  1120. ) -> dict[str, bool]: # pragma: no cover
  1121. """Take the permissions as an integer, return the allowed access."""
  1122. deprecation_with_replacement(
  1123. old_name="decode_permissions",
  1124. new_name="user_access_permissions",
  1125. removed_in="5.0.0",
  1126. )
  1127. permissions_mapping = {
  1128. "print": UserAccessPermissions.PRINT,
  1129. "modify": UserAccessPermissions.MODIFY,
  1130. "copy": UserAccessPermissions.EXTRACT,
  1131. "annotations": UserAccessPermissions.ADD_OR_MODIFY,
  1132. "forms": UserAccessPermissions.FILL_FORM_FIELDS,
  1133. # Do not fix typo, as part of official, but deprecated API.
  1134. "accessability": UserAccessPermissions.EXTRACT_TEXT_AND_GRAPHICS,
  1135. "assemble": UserAccessPermissions.ASSEMBLE_DOC,
  1136. "print_high_quality": UserAccessPermissions.PRINT_TO_REPRESENTATION,
  1137. }
  1138. return {
  1139. key: permissions_code & flag != 0
  1140. for key, flag in permissions_mapping.items()
  1141. }
  1142. @property
  1143. def user_access_permissions(self) -> Optional[UserAccessPermissions]:
  1144. """
  1145. Get the user access permissions for encrypted documents.
  1146. Returns None if not encrypted.
  1147. .. warning::
  1148. For AES-256 encrypted documents (R=5/R=6), the returned
  1149. permissions are derived from the ``/P`` field, which is
  1150. only trustworthy if the ``/Perms`` integrity check passed.
  1151. Check :attr:`are_permissions_valid` to verify.
  1152. """
  1153. if self._encryption is None:
  1154. return None
  1155. return UserAccessPermissions(self._encryption.P)
  1156. @property
  1157. def are_permissions_valid(self) -> Optional[bool]:
  1158. """
  1159. Whether the ``/Perms`` integrity check passed for this document.
  1160. For AES-256 encrypted documents (R=5/R=6), the ``/Perms`` field
  1161. is an encrypted copy of the permissions that can be verified
  1162. independently. Returns ``False`` if this check fails (the ``/P``
  1163. permissions may have been tampered with).
  1164. Returns ``None`` if the document is not encrypted or has not yet
  1165. been decrypted via :meth:`decrypt()<pypdf.PdfReader.decrypt>`.
  1166. Returns ``True`` for non-AES-256 encryption (no ``/Perms`` to check).
  1167. """
  1168. if self._encryption is None:
  1169. return None
  1170. if not self._encryption.is_decrypted():
  1171. return None
  1172. return self._encryption._are_permissions_valid
  1173. @property
  1174. @abstractmethod
  1175. def is_encrypted(self) -> bool:
  1176. """
  1177. Read-only boolean property showing whether this PDF file is encrypted.
  1178. Note that this property, if true, will remain true even after the
  1179. :meth:`decrypt()<pypdf.PdfReader.decrypt>` method is called.
  1180. """
  1181. ... # pragma: no cover
  1182. @property
  1183. def xfa(self) -> Optional[dict[str, Any]]:
  1184. retval: dict[str, Any] = {}
  1185. catalog = self.root_object
  1186. if "/AcroForm" not in catalog or not catalog["/AcroForm"]:
  1187. return None
  1188. tree = cast(TreeObject, catalog["/AcroForm"])
  1189. if "/XFA" in tree:
  1190. fields = cast(ArrayObject, tree["/XFA"])
  1191. i = iter(fields)
  1192. for f in i:
  1193. tag = f
  1194. f = next(i)
  1195. if isinstance(f, IndirectObject):
  1196. field = cast(Optional[EncodedStreamObject], f.get_object())
  1197. if field:
  1198. es = _decompress_with_limit(field._data)
  1199. retval[tag] = es
  1200. return retval
  1201. @property
  1202. def attachments(self) -> Mapping[str, list[bytes]]:
  1203. """Mapping of attachment filenames to their content."""
  1204. return LazyDict(
  1205. {
  1206. name: (self._get_attachment_list, name)
  1207. for name in self._list_attachments()
  1208. }
  1209. )
  1210. @property
  1211. def attachment_list(self) -> Generator[EmbeddedFile, None, None]:
  1212. """Iterable of attachment objects."""
  1213. yield from EmbeddedFile._load(self.root_object)
  1214. def _list_attachments(self) -> list[str]:
  1215. """
  1216. Retrieves the list of filenames of file attachments.
  1217. Returns:
  1218. list of filenames
  1219. """
  1220. names = []
  1221. for entry in self.attachment_list:
  1222. names.append(entry.name)
  1223. if (name := entry.alternative_name) != entry.name and name:
  1224. names.append(name)
  1225. return names
  1226. def _get_attachment_list(self, name: str) -> list[bytes]:
  1227. out = self._get_attachments(name)[name]
  1228. if isinstance(out, list):
  1229. return out
  1230. return [out]
  1231. def _get_attachments(
  1232. self, filename: Optional[str] = None
  1233. ) -> dict[str, Union[bytes, list[bytes]]]:
  1234. """
  1235. Retrieves all or selected file attachments of the PDF as a dictionary of file names
  1236. and the file data as a bytestring.
  1237. Args:
  1238. filename: If filename is None, then a dictionary of all attachments
  1239. will be returned, where the key is the filename and the value
  1240. is the content. Otherwise, a dictionary with just a single key
  1241. - the filename - and its content will be returned.
  1242. Returns:
  1243. dictionary of filename -> Union[bytestring or List[ByteString]]
  1244. If the filename exists multiple times a list of the different versions will be provided.
  1245. """
  1246. attachments: dict[str, Union[bytes, list[bytes]]] = {}
  1247. for entry in self.attachment_list:
  1248. names = set()
  1249. alternative_name = entry.alternative_name
  1250. if filename is not None:
  1251. if filename in {entry.name, alternative_name}:
  1252. name = entry.name if filename == entry.name else alternative_name
  1253. names.add(name)
  1254. else:
  1255. continue
  1256. else:
  1257. names = {entry.name, alternative_name}
  1258. for name in names:
  1259. if name is None:
  1260. continue
  1261. if name in attachments:
  1262. if not isinstance(attachments[name], list):
  1263. attachments[name] = [attachments[name]] # type:ignore
  1264. attachments[name].append(entry.content) # type:ignore
  1265. else:
  1266. attachments[name] = entry.content
  1267. return attachments
  1268. @abstractmethod
  1269. def _repr_mimebundle_(
  1270. self,
  1271. include: Union[None, Iterable[str]] = None,
  1272. exclude: Union[None, Iterable[str]] = None,
  1273. ) -> dict[str, Any]:
  1274. """
  1275. Integration into Jupyter Notebooks.
  1276. This method returns a dictionary that maps a mime-type to its
  1277. representation.
  1278. .. seealso::
  1279. https://ipython.readthedocs.io/en/stable/config/integrating.html
  1280. """
  1281. ... # pragma: no cover
  1282. class LazyDict(Mapping[Any, Any]):
  1283. def __init__(self, *args: Any, **kwargs: Any) -> None:
  1284. self._raw_dict = dict(*args, **kwargs)
  1285. def __getitem__(self, key: str) -> Any:
  1286. func, arg = self._raw_dict.__getitem__(key)
  1287. return func(arg)
  1288. def __iter__(self) -> Iterator[Any]:
  1289. return iter(self._raw_dict)
  1290. def __len__(self) -> int:
  1291. return len(self._raw_dict)
  1292. def __str__(self) -> str:
  1293. return f"LazyDict(keys={list(self.keys())})"