From b2279eeb40fa0557e6e61e3bada0de0373e6da28 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Mon, 11 Jul 2022 07:59:54 +0200 Subject: [PATCH 001/130] DOC: Watermark and stamp (#1095) See #307 --- docs/user/add-watermark.md | 87 +++++++++++++++++++++++--------------- 1 file changed, 53 insertions(+), 34 deletions(-) diff --git a/docs/user/add-watermark.md b/docs/user/add-watermark.md index a0eabccca..4e852ae07 100644 --- a/docs/user/add-watermark.md +++ b/docs/user/add-watermark.md @@ -10,28 +10,35 @@ content stays the same. ## Stamp (Overlay) ```python -from PyPDF2 import PdfWriter, PdfReader - - -def stamp(content_page, image_page): - """Put the image over the content""" - # Note that this modifies the content_page in-place! - content_page.merge_page(image_page) - return content_page - +from pathlib import Path +from typing import Union, Literal, List -# Read the pages -reader_content = PdfReader("content.pdf") -reader_image = PdfReader("image.pdf") +from PyPDF2 import PdfWriter, PdfReader -# Modify it -modified = stamp(reader_content.pages[0], reader_image.pages[0]) -# Create the new document -writer = PdfWriter() -writer.add_page(modified) -with open("out-stamp.pdf", "wb") as fp: - writer.write(fp) +def stamp( + content_pdf: Path, + stamp_pdf: Path, + pdf_result: Path, + page_indices: Union[Literal["ALL"], List[int]] = "ALL", +): + reader = PdfReader(stamp_pdf) + image_page = reader.pages[0] + + writer = PdfWriter() + + reader = PdfReader(content_pdf) + if page_indices == "ALL": + page_indices = list(range(0, len(reader.pages))) + for index in page_indices: + content_page = reader.pages[index] + mediabox = content_page.mediabox + content_page.merge_page(image_page) + content_page.mediabox = mediabox + writer.add_page(content_page) + + with open(pdf_result, "wb") as fp: + writer.write(fp) ``` ![stamp.png](stamp.png) @@ -39,28 +46,40 @@ with open("out-stamp.pdf", "wb") as fp: ## Watermark (Underlay) ```python +from pathlib import Path +from typing import Union, Literal, List + from PyPDF2 import PdfWriter, PdfReader -def watermark(content_page, image_page): - """Put the image under the content""" - # Note that this modifies the image_page in-place! - image_page.merge_page(content_page) - return image_page +def watermark( + content_pdf: Path, + stamp_pdf: Path, + pdf_result: Path, + page_indices: Union[Literal["ALL"], List[int]] = "ALL", +): + reader = PdfReader(content_pdf) + if page_indices == "ALL": + page_indices = list(range(0, len(reader.pages))) + + reader_stamp = PdfReader(stamp_pdf) + image_page = reader_stamp.pages[0] + writer = PdfWriter() + for index in page_indices: + content_page = reader.pages[index] + mediabox = content_page.mediabox -# Read the pages -reader_content = PdfReader("content.pdf") -reader_image = PdfReader("image.pdf") + # You need to load it again, as the last time it was overwritten + reader_stamp = PdfReader(stamp_pdf) + image_page = reader_stamp.pages[0] -# Modify it -modified = stamp(reader_content.pages[0], reader_image.pages[0]) + image_page.merge_page(content_page) + image_page.mediabox = mediabox + writer.add_page(image_page) -# Create the new document -writer = PdfWriter() -writer.add_page(modified) -with open("out-watermark.pdf", "wb") as fp: - writer.write(fp) + with open(pdf_result, "wb") as fp: + writer.write(fp) ``` ![watermark.png](watermark.png) From d7b64dc817a948b275f221d326746ee07130e2de Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Tue, 12 Jul 2022 07:47:11 +0200 Subject: [PATCH 002/130] MAINT: Use add_bookmark_destination in add_bookmark_dict (#1099) Re-use code See #1098 --- PyPDF2/_writer.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/PyPDF2/_writer.py b/PyPDF2/_writer.py index bde7d15e6..463aad764 100644 --- a/PyPDF2/_writer.py +++ b/PyPDF2/_writer.py @@ -1055,7 +1055,7 @@ def getNamedDestRoot(self) -> ArrayObject: # pragma: no cover return self.get_named_dest_root() def add_bookmark_destination( - self, dest: PageObject, parent: Optional[TreeObject] = None + self, dest: Union[PageObject, TreeObject], parent: Optional[TreeObject] = None ) -> IndirectObject: dest_ref = self._add_object(dest) @@ -1096,18 +1096,7 @@ def add_bookmark_dict( action_ref = self._add_object(action) bookmark_obj[NameObject("/A")] = action_ref - bookmark_ref = self._add_object(bookmark_obj) - - outline_ref = self.get_outline_root() - - if parent is None: - parent = outline_ref - - parent = parent.get_object() # type: ignore - assert parent is not None, "hint for mypy" - parent.add_child(bookmark_ref, self) - - return bookmark_ref + return self.add_bookmark_destination(bookmark_obj, parent) def addBookmarkDict( self, bookmark: BookmarkTypes, parent: Optional[TreeObject] = None From c420beb32c89f17822fb0e25d23db3f25ebd9af9 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Tue, 12 Jul 2022 08:20:59 +0200 Subject: [PATCH 003/130] MAINT: Use add_bookmark_destination in add_bookmark (#1100) Reduce code duplication See #1098 --- PyPDF2/_writer.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/PyPDF2/_writer.py b/PyPDF2/_writer.py index 463aad764..a0b84bae0 100644 --- a/PyPDF2/_writer.py +++ b/PyPDF2/_writer.py @@ -1055,7 +1055,9 @@ def getNamedDestRoot(self) -> ArrayObject: # pragma: no cover return self.get_named_dest_root() def add_bookmark_destination( - self, dest: Union[PageObject, TreeObject], parent: Optional[TreeObject] = None + self, + dest: Union[PageObject, TreeObject], + parent: Union[None, TreeObject, IndirectObject] = None, ) -> IndirectObject: dest_ref = self._add_object(dest) @@ -1153,21 +1155,11 @@ def add_bookmark( } ) action_ref = self._add_object(action) - - outline_ref = self.get_outline_root() - - if parent is None: - parent = outline_ref - bookmark = _create_bookmark(action_ref, title, color, italic, bold) - bookmark_ref = self._add_object(bookmark) - - assert parent is not None, "hint for mypy" - parent_obj = cast(TreeObject, parent.get_object()) - parent_obj.add_child(bookmark_ref, self) - - return bookmark_ref + if parent is None: + parent = self.get_outline_root() + return self.add_bookmark_destination(bookmark, parent) def addBookmark( self, From d376d0e71939decbe21de8e93d016f09b3ce2210 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Tue, 12 Jul 2022 09:33:40 +0200 Subject: [PATCH 004/130] STY: Simplify code (#1101) --- PyPDF2/_writer.py | 47 ++++++++++++++++++----------------------------- 1 file changed, 18 insertions(+), 29 deletions(-) diff --git a/PyPDF2/_writer.py b/PyPDF2/_writer.py index a0b84bae0..7a469407d 100644 --- a/PyPDF2/_writer.py +++ b/PyPDF2/_writer.py @@ -1059,14 +1059,11 @@ def add_bookmark_destination( dest: Union[PageObject, TreeObject], parent: Union[None, TreeObject, IndirectObject] = None, ) -> IndirectObject: - dest_ref = self._add_object(dest) - - outline_ref = self.get_outline_root() - if parent is None: - parent = outline_ref + parent = self.get_outline_root() parent = cast(TreeObject, parent.get_object()) + dest_ref = self._add_object(dest) parent.add_child(dest_ref, self) return dest_ref @@ -1137,24 +1134,21 @@ def add_bookmark( :meth:`addLink()` for details. """ page_ref = NumberObject(pagenum) - action = DictionaryObject() - zoom_args: ZoomArgsType = [] - for a in args: - if a is not None: - zoom_args.append(NumberObject(a)) - else: - zoom_args.append(NullObject()) + zoom_args: ZoomArgsType = [ + NullObject() if a is None else NumberObject(a) for a in args + ] dest = Destination( NameObject("/" + title + " bookmark"), page_ref, NameObject(fit), *zoom_args ) - dest_array = dest.dest_array - action.update( - { - NameObject(GoToActionArguments.D): dest_array, - NameObject(GoToActionArguments.S): NameObject("/GoTo"), - } + + action_ref = self._add_object( + DictionaryObject( + { + NameObject(GoToActionArguments.D): dest.dest_array, + NameObject(GoToActionArguments.S): NameObject("/GoTo"), + } + ) ) - action_ref = self._add_object(action) bookmark = _create_bookmark(action_ref, title, color, italic, bold) if parent is None: @@ -1537,26 +1531,21 @@ def add_link( else: rect = RectangleObject(rect) - zoom_args: ZoomArgsType = [] - for a in args: - if a is not None: - zoom_args.append(NumberObject(a)) - else: - zoom_args.append(NullObject()) + zoom_args: ZoomArgsType = [ + NullObject() if a is None else NumberObject(a) for a in args + ] dest = Destination( NameObject("/LinkName"), page_dest, NameObject(fit), *zoom_args ) # TODO: create a better name for the link - dest_array = dest.dest_array - lnk = DictionaryObject() - lnk.update( + lnk = DictionaryObject( { NameObject("/Type"): NameObject(PG.ANNOTS), NameObject("/Subtype"): NameObject("/Link"), NameObject("/P"): page_link, NameObject("/Rect"): rect, NameObject("/Border"): ArrayObject(border_arr), - NameObject("/Dest"): dest_array, + NameObject("/Dest"): dest.dest_array, } ) lnk_ref = self._add_object(lnk) From af5a0c3394a33eae154c63f5ccaa403ca54dbf1b Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Tue, 12 Jul 2022 09:39:18 +0200 Subject: [PATCH 005/130] STY: Use IntFlag for permissions_flag / update_page_form_field_values (#1094) --- CHANGELOG.md | 2 +- PyPDF2/_writer.py | 19 +++++++++++++++---- PyPDF2/constants.py | 46 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd4e1e021..1822fba42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,7 +38,7 @@ - Apply black - Typo in Changelog -Full Changelog: https://github.com/py-pdf/PyPDF2/compare/2.4.2...2.4.3 +Full Changelog: https://github.com/py-pdf/PyPDF2/compare/2.4.2...2.5.0 ## Version 2.4.2, 2022-07-05 diff --git a/PyPDF2/_writer.py b/PyPDF2/_writer.py index 7a469407d..843549ab0 100644 --- a/PyPDF2/_writer.py +++ b/PyPDF2/_writer.py @@ -67,6 +67,7 @@ from .constants import EncryptionDictAttributes as ED from .constants import ( FieldDictionaryAttributes, + FieldFlag, FileSpecificationDictionaryEntries, GoToActionArguments, InteractiveFormDictEntries, @@ -75,7 +76,7 @@ from .constants import PagesAttributes as PA from .constants import StreamAttributes as SA from .constants import TrailerKeys as TK -from .constants import TypFitArguments +from .constants import TypFitArguments, UserAccessPermissions from .generic import ( ArrayObject, BooleanObject, @@ -110,6 +111,10 @@ logger = logging.getLogger(__name__) +OPTIONAL_READ_WRITE_FIELD = FieldFlag(0) +ALL_DOCUMENT_PERMISSIONS = UserAccessPermissions((2**31 - 1) - 3) + + class PdfWriter: """ This class supports writing PDF files out, given pages produced by another @@ -576,7 +581,10 @@ def appendPagesFromReader( self.append_pages_from_reader(reader, after_page_append) def update_page_form_field_values( - self, page: PageObject, fields: Dict[str, Any], flags: int = 0 + self, + page: PageObject, + fields: Dict[str, Any], + flags: FieldFlag = OPTIONAL_READ_WRITE_FIELD, ) -> None: """ Update the form field values for a given page from a fields dictionary. @@ -627,7 +635,10 @@ def update_page_form_field_values( ) def updatePageFormFieldValues( - self, page: PageObject, fields: Dict[str, Any], flags: int = 0 + self, + page: PageObject, + fields: Dict[str, Any], + flags: FieldFlag = OPTIONAL_READ_WRITE_FIELD, ) -> None: # pragma: no cover """ .. deprecated:: 1.28.0 @@ -699,7 +710,7 @@ def encrypt( user_pwd: str, owner_pwd: Optional[str] = None, use_128bit: bool = True, - permissions_flag: int = -1, + permissions_flag: UserAccessPermissions = ALL_DOCUMENT_PERMISSIONS, ) -> None: """ Encrypt this PDF file with the PDF Standard encryption handler. diff --git a/PyPDF2/constants.py b/PyPDF2/constants.py index ab5b55e55..a195c22fe 100644 --- a/PyPDF2/constants.py +++ b/PyPDF2/constants.py @@ -8,6 +8,7 @@ PDF Reference, sixth edition, Version 1.7, 2006. """ +from enum import IntFlag from typing import Dict, Tuple @@ -47,6 +48,43 @@ class EncryptionDictAttributes: ENCRYPT_METADATA = "/EncryptMetadata" # boolean flag, optional +class UserAccessPermissions(IntFlag): + """TABLE 3.20 User access permissions""" + + R1 = 1 + R2 = 2 + PRINT = 4 + MODIFY = 8 + EXTRACT = 16 + ADD_OR_MODIFY = 32 + R7 = 64 + R8 = 128 + FILL_FORM_FIELDS = 256 + EXTRACT_TEXT_AND_GRAPHICS = 512 + ASSEMBLE_DOC = 1024 + PRINT_TO_REPRESENTATION = 2048 + R13 = 2**12 + R14 = 2**13 + R15 = 2**14 + R16 = 2**15 + R17 = 2**16 + R18 = 2**17 + R19 = 2**18 + R20 = 2**19 + R21 = 2**20 + R22 = 2**21 + R23 = 2**22 + R24 = 2**23 + R25 = 2**24 + R26 = 2**25 + R27 = 2**26 + R28 = 2**27 + R29 = 2**28 + R30 = 2**29 + R31 = 2**30 + R32 = 2**31 + + class Ressources: """TABLE 3.30 Entries in a resource dictionary.""" @@ -294,6 +332,14 @@ def attributes_dict(cls) -> Dict[str, str]: } +class FieldFlag(IntFlag): + """TABLE 8.70 Field flags common to all field types""" + + READ_ONLY = 1 + REQUIRED = 2 + NO_EXPORT = 4 + + class DocumentInformationAttributes: """TABLE 10.2 Entries in the document information dictionary.""" From 682eff93a1250403ed08c058e65d8576772ca858 Mon Sep 17 00:00:00 2001 From: pubpub-zz <4083478+pubpub-zz@users.noreply.github.com> Date: Wed, 13 Jul 2022 07:18:05 +0200 Subject: [PATCH 006/130] ENH: Extract Text Enhancement (whitespaces) (#1084) * ENH : extract width from CIDFontType0/2 * ENH : improve cr/lf and space extraction * BUG : fix error in decoding #1075 * FIX: in ToUnicode ignore comments (starting with %) * FIX: extend utf16 for min of 4 characters Improves #234 Improves #957 Closes #1003 Closes #1019 Used https://tug.ctan.org/info/symbols/comprehensive/symbols-a4.pdf for testing --- PyPDF2/_cmap.py | 53 ++++++++---- PyPDF2/_page.py | 207 ++++++++++++++++++++++++++++++++++----------- tests/test_page.py | 1 + 3 files changed, 194 insertions(+), 67 deletions(-) diff --git a/PyPDF2/_cmap.py b/PyPDF2/_cmap.py index a8b06663c..31616de7e 100644 --- a/PyPDF2/_cmap.py +++ b/PyPDF2/_cmap.py @@ -35,10 +35,17 @@ def build_char_map( for x in int_entry: if x <= 255: encoding[x] = chr(x) - if font_name in _default_fonts_space_width: + try: # override space_width with new params - space_width = _default_fonts_space_width[font_name] - sp_width = compute_space_width(ft, space_code, space_width) + space_width = _default_fonts_space_width[cast(str, ft["/BaseFont"])] + except Exception: + pass + # I conside the space_code is available on one byte + if isinstance(space_code, str): + sp = space_code.encode("charmap")[0] + else: + sp = space_code + sp_width = compute_space_width(ft, sp, space_width) return ( font_type, @@ -193,7 +200,7 @@ def parse_to_unicode( ) for l in cm.split(b"\n"): - if l in (b"", b" "): + if l in (b"", b" ") or l[0] == 37: # 37 = % continue if b"beginbfrange" in l: process_rg = True @@ -224,7 +231,7 @@ def parse_to_unicode( a += 1 else: c = int(lst[2], 16) - fmt2 = b"%%0%dX" % len(lst[2]) + fmt2 = b"%%0%dX" % max(4, len(lst[2])) while a <= b: map_dict[ unhexlify(fmt % a).decode( @@ -259,30 +266,40 @@ def compute_space_width( ) -> float: sp_width: float = space_width * 2 # default value w = [] + w1 = {} st: int = 0 - if "/W" in ft: - if "/DW" in ft: - sp_width = cast(float, ft["/DW"]) - w = list(ft["/W"]) # type: ignore + if "/DescendantFonts" in ft: # ft["/Subtype"].startswith("/CIDFontType"): + ft1 = ft["/DescendantFonts"][0].get_object() # type: ignore + try: + w1[-1] = cast(float, ft1["/DW"]) + except Exception: + w1[-1] = 1000.0 + w = list(ft1["/W"]) # type: ignore while len(w) > 0: st = w[0] second = w[1] - if isinstance(int, second): - if st <= space_code and space_code <= second: - sp_width = w[2] - break + if isinstance(second, int): + for x in range(st, second): + w1[x] = w[2] w = w[3:] - if isinstance(list, second): - if st <= space_code and space_code <= st + len(second) - 1: - sp_width = second[space_code - st] + elif isinstance(second, list): + for y in second: + w1[st] = y + st += 1 w = w[2:] else: warnings.warn( - "unknown widths : \n" + (ft["/W"]).__repr__(), + "unknown widths : \n" + (ft1["/W"]).__repr__(), PdfReadWarning, ) break - if "/Widths" in ft: + try: + sp_width = w1[space_code] + except Exception: + sp_width = ( + w1[-1] / 2.0 + ) # if using default we consider space will be only half size + elif "/Widths" in ft: w = list(ft["/Widths"]) # type: ignore try: st = cast(int, ft["/FirstChar"]) diff --git a/PyPDF2/_page.py b/PyPDF2/_page.py index 63dd7d913..54ca9982d 100644 --- a/PyPDF2/_page.py +++ b/PyPDF2/_page.py @@ -1143,22 +1143,53 @@ def _extract_text( # are strings where the byte->string encoding was unknown, so adding # them to the text here would be gibberish. + cm_matrix: List[float] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0] + cm_stack = [] tm_matrix: List[float] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0] - tm_prev: List[float] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0] + tm_prev: List[float] = [ + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + ] # will store cm_matrix * tm_matrix char_scale = 1.0 space_scale = 1.0 _space_width: float = 500.0 # will be set correctly at first Tf TL = 0.0 font_size = 12.0 # init just in case of - # tm_matrix: Tuple = tm_matrix, output: str = output, text: str = text, - # char_scale: float = char_scale,space_scale : float = space_scale, _space_width: float = _space_width, - # TL: float = TL, font_size: float = font_size, cmap = cmap + def sign(x: float) -> float: + return 1 if x >= 0 else -1 + + def mult(m: List[float], n: List[float]) -> List[float]: + return [ + m[0] * n[0] + m[1] * n[2], + m[0] * n[1] + m[1] * n[3], + m[2] * n[0] + m[3] * n[2], + m[2] * n[1] + m[3] * n[3], + m[4] * n[0] + m[5] * n[2] + n[4], + m[4] * n[1] + m[5] * n[3] + n[5], + ] + + def orient(m: List[float]) -> int: + if m[3] > 1e-6: + return 0 + elif m[3] < -1e-6: + return 180 + elif m[1] > 0: + return 90 + else: + return 270 + + def current_spacewidth() -> float: + # return space_scale * _space_width * char_scale + return _space_width / 1000.0 def process_operation(operator: bytes, operands: List) -> None: - nonlocal tm_matrix, tm_prev, output, text, char_scale, space_scale, _space_width, TL, font_size, cmap - if tm_matrix[4] != 0 and tm_matrix[5] != 0: # o reuse of the - tm_prev = list(tm_matrix) + nonlocal cm_matrix, cm_stack, tm_matrix, tm_prev, output, text, char_scale, space_scale, _space_width, TL, font_size, cmap + check_crlf_space: bool = False # Table 5.4 page 405 if operator == b"BT": tm_matrix = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0] @@ -1172,6 +1203,29 @@ def process_operation(operator: bytes, operands: List) -> None: elif operator == b"ET": output += text text = "" + # table 4.7, page 219 + # cm_matrix calculation is a reserved for the moment + elif operator == b"q": + cm_stack.append(cm_matrix) + elif operator == b"Q": + try: + cm_matrix = cm_stack.pop() + except Exception: + cm_matrix = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0] + elif operator == b"cm": + output += text + text = "" + cm_matrix = mult( + [ + float(operands[0]), + float(operands[1]), + float(operands[2]), + float(operands[3]), + float(operands[4]), + float(operands[5]), + ], + cm_matrix, + ) # Table 5.2 page 398 elif operator == b"Tz": char_scale = float(operands[0]) / 100.0 @@ -1203,9 +1257,11 @@ def process_operation(operator: bytes, operands: List) -> None: pass # keep previous size # Table 5.5 page 406 elif operator == b"Td": - tm_matrix[5] += float(operands[1]) + check_crlf_space = True tm_matrix[4] += float(operands[0]) + tm_matrix[5] += float(operands[1]) elif operator == b"Tm": + check_crlf_space = True tm_matrix = [ float(operands[0]), float(operands[1]), @@ -1215,47 +1271,90 @@ def process_operation(operator: bytes, operands: List) -> None: float(operands[5]), ] elif operator == b"T*": + check_crlf_space = True tm_matrix[5] -= TL + elif operator == b"Tj": - t: str = "" - tt: bytes = ( - encode_pdfdocencoding(operands[0]) - if isinstance(operands[0], str) - else operands[0] - ) - if isinstance(cmap[0], str): - try: - t = tt.decode(cmap[0], "surrogatepass") # apply str encoding - except Exception: # the data does not match the expectation, we use the alternative ; text extraction may not be good - t = tt.decode( - "utf-16-be" if cmap[0] == "charmap" else "charmap", - "surrogatepass", - ) # apply str encoding - else: # apply dict encoding - t = "".join( - [ - cmap[0][x] if x in cmap[0] else bytes((x,)).decode() - for x in tt - ] + check_crlf_space = True + if isinstance(operands[0], str): + text += operands[0] + else: + t: str = "" + tt: bytes = ( + encode_pdfdocencoding(operands[0]) + if isinstance(operands[0], str) + else operands[0] ) - - text += "".join([cmap[1][x] if x in cmap[1] else x for x in t]) + if isinstance(cmap[0], str): + try: + t = tt.decode(cmap[0], "surrogatepass") # apply str encoding + except Exception: # the data does not match the expectation, we use the alternative ; text extraction may not be good + t = tt.decode( + "utf-16-be" if cmap[0] == "charmap" else "charmap", + "surrogatepass", + ) # apply str encoding + else: # apply dict encoding + t = "".join( + [ + cmap[0][x] if x in cmap[0] else bytes((x,)).decode() + for x in tt + ] + ) + + text += "".join([cmap[1][x] if x in cmap[1] else x for x in t]) else: return None - # process text changes due to positionchange: " " - if tm_matrix[5] <= ( - tm_prev[5] - - font_size # remove scaling * sqrt(tm_matrix[2] ** 2 + tm_matrix[3] ** 2) - ): # it means that we are moving down by one line - output += text + "\n" # .translate(cmap) + "\n" - text = "" - elif tm_matrix[4] >= ( - tm_prev[4] + space_scale * _space_width * char_scale - ): # it means that we are moving down by one line - text += " " - return None - # for clarity Operator in (b"g",b"G") : nothing to do - # end of process_operation ###### + if check_crlf_space: + m = mult(tm_matrix, cm_matrix) + o = orient(m) + deltaX = m[4] - tm_prev[4] + deltaY = m[5] - tm_prev[5] + k = math.sqrt(abs(m[0] * m[3]) + abs(m[1] * m[2])) + f = font_size * k + tm_prev = m + try: + if o == 0: + if deltaY < -0.8 * f: + if (output + text)[-1] != "\n": + text += "\n" + elif ( + abs(deltaY) < f * 0.3 + and abs(deltaX) > current_spacewidth() * f * 10 + ): + if (output + text)[-1] != " ": + text += " " + elif o == 180: + if deltaY > 0.8 * f: + if (output + text)[-1] != "\n": + text += "\n" + elif ( + abs(deltaY) < f * 0.3 + and abs(deltaX) > current_spacewidth() * f * 10 + ): + if (output + text)[-1] != " ": + text += " " + elif o == 90: + if deltaX > 0.8 * f: + if (output + text)[-1] != "\n": + text += "\n" + elif ( + abs(deltaX) < f * 0.3 + and abs(deltaY) > current_spacewidth() * f * 10 + ): + if (output + text)[-1] != " ": + text += " " + elif o == 270: + if deltaX < -0.8 * f: + if (output + text)[-1] != "\n": + text += "\n" + elif ( + abs(deltaX) < f * 0.3 + and abs(deltaY) > current_spacewidth() * f * 10 + ): + if (output + text)[-1] != " ": + text += " " + except Exception: + pass for operands, operator in content.operations: # multiple operators are defined in here #### @@ -1263,8 +1362,10 @@ def process_operation(operator: bytes, operands: List) -> None: process_operation(b"T*", []) process_operation(b"Tj", operands) elif operator == b'"': + process_operation(b"Tw", [operands[0]]) + process_operation(b"Tc", [operands[1]]) process_operation(b"T*", []) - process_operation(b"TJ", operands) + process_operation(b"Tj", operands[2:]) elif operator == b"TD": process_operation(b"TL", [-operands[1]]) process_operation(b"Td", operands) @@ -1273,15 +1374,23 @@ def process_operation(operator: bytes, operands: List) -> None: if isinstance(op, (str, bytes)): process_operation(b"Tj", [op]) if isinstance(op, (int, float, NumberObject, FloatObject)): - process_operation(b"Td", [-op, 0.0]) + if ( + (abs(float(op)) >= _space_width) + and (abs(float(op)) <= 8 * _space_width) + and (text[-1] != " ") + ): + process_operation(b"Tj", [" "]) elif operator == b"Do": output += text - if output != "": - output += "\n" + try: + if output[-1] != "\n": + output += "\n" + except IndexError: + pass try: xobj = resources_dict["/XObject"] # type: ignore if xobj[operands[0]]["/Subtype"] != "/Image": # type: ignore - output += text + # output += text text = self.extract_xform_text(xobj[operands[0]], space_width) # type: ignore output += text except Exception: diff --git a/tests/test_page.py b/tests/test_page.py index 65366459e..fc7c2a71a 100644 --- a/tests/test_page.py +++ b/tests/test_page.py @@ -35,6 +35,7 @@ def get_all_sample_files(): [m for m in all_files_meta["data"] if not m["encrypted"]], ids=[m["path"] for m in all_files_meta["data"] if not m["encrypted"]], ) +@pytest.mark.filterwarnings("ignore::PyPDF2.errors.PdfReadWarning") def test_read(meta): pdf_path = os.path.join(EXTERNAL_ROOT, meta["path"]) reader = PdfReader(pdf_path) From 5e1cc57677c2ee9b80d9ed27a41321d3cae2d7c3 Mon Sep 17 00:00:00 2001 From: mtd91429 Date: Thu, 14 Jul 2022 13:50:10 -0500 Subject: [PATCH 007/130] ENH: Add color and font_format to PdfReader.outlines[i] (#1104) --- PyPDF2/_reader.py | 12 ++++++++++++ PyPDF2/generic.py | 21 ++++++++++++++++++++- tests/test_reader.py | 14 ++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/PyPDF2/_reader.py b/PyPDF2/_reader.py index 74b7f36d5..7bff21ae8 100644 --- a/PyPDF2/_reader.py +++ b/PyPDF2/_reader.py @@ -78,6 +78,7 @@ DictionaryObject, EncodedStreamObject, Field, + FloatObject, IndirectObject, NameObject, NullObject, @@ -837,6 +838,17 @@ def _build_outline(self, node: DictionaryObject) -> Optional[Destination]: outline[NameObject("/Title")] = title # type: ignore else: raise PdfReadError(f"Unexpected destination {dest!r}") + + # if outline created, add color and format if present + if outline: + if "/C" in node: + # Color of outline in (R, G, B) with values ranging 0.0-1.0 + outline[NameObject("/C")] = ArrayObject(FloatObject(c) for c in node["/C"]) # type: ignore + if "/F" in node: + # specifies style characteristics bold and/or italic + # 1=italic, 2=bold, 3=both + outline[NameObject("/F")] = node["/F"] + return outline @property diff --git a/PyPDF2/generic.py b/PyPDF2/generic.py index ac6a98edf..e8a9d8d46 100644 --- a/PyPDF2/generic.py +++ b/PyPDF2/generic.py @@ -48,7 +48,7 @@ Union, cast, ) - +from enum import IntFlag from ._codecs import ( # noqa: rev_encoding _pdfdoc_encoding, _pdfdoc_encoding_rev, @@ -1732,6 +1732,15 @@ def additionalActions(self) -> Optional[DictionaryObject]: # pragma: no cover return self.additional_actions +class OutlineFontFlag(IntFlag): + """ + A class used as an enumerable flag for formatting an outline font + """ + + italic = 1 + bold = 2 + + class Destination(TreeObject): """ A class representing a destination within a PDF file. @@ -1879,6 +1888,16 @@ def bottom(self) -> Optional[FloatObject]: """Read-only property accessing the bottom vertical coordinate.""" return self.get("/Bottom", None) + @property + def color(self) -> Optional[tuple]: + """Read-only property accessing the color in (R, G, B) with values 0.0-1.0""" + return self.get("/C", [FloatObject(0), FloatObject(0), FloatObject(0)]) + + @property + def font_format(self) -> Optional[OutlineFontFlag]: + """Read-only property accessing the font type. 1=italic, 2=bold, 3=both""" + return self.get("/F", 0) + class Bookmark(Destination): def write_to_stream( diff --git a/tests/test_reader.py b/tests/test_reader.py index a605fc708..56bc2a70b 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -858,3 +858,17 @@ def test_header(src, pdf_header): reader = PdfReader(src) assert reader.pdf_header == pdf_header + + +def test_outline_color(): + url = "https://corpora.tika.apache.org/base/docs/govdocs1/924/924546.pdf" + name = "tika-924546.pdf" + reader = PdfReader(BytesIO(get_pdf_from_url(url, name=name))) + assert reader.outlines[0].color == [0, 0, 1] + + +def test_outline_font_format(): + url = "https://corpora.tika.apache.org/base/docs/govdocs1/924/924546.pdf" + name = "tika-924546.pdf" + reader = PdfReader(BytesIO(get_pdf_from_url(url, name=name))) + assert reader.outlines[0].font_format == 2 From bb2d1dbf20dbe6a77d60be46cbd8646fde6b418c Mon Sep 17 00:00:00 2001 From: dkg Date: Thu, 14 Jul 2022 14:57:11 -0400 Subject: [PATCH 008/130] BUG: Avoid IndexError in _cmap.parse_to_unicode (#1110) The code within the if block assumes that `lst` has index 0 and index 1. Fixes #1091 Related to #1111 --- PyPDF2/_cmap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PyPDF2/_cmap.py b/PyPDF2/_cmap.py index 31616de7e..2c5ec2ee5 100644 --- a/PyPDF2/_cmap.py +++ b/PyPDF2/_cmap.py @@ -245,7 +245,7 @@ def parse_to_unicode( elif process_char: lst = [x for x in l.split(b" ") if x] map_dict[-1] = len(lst[0]) // 2 - while len(lst) > 0: + while len(lst) > 1: map_dict[ unhexlify(lst[0]).decode( "charmap" if map_dict[-1] == 1 else "utf-16-be", "surrogatepass" From 9bbe827ab534cdbbb6e2687e0a41dda4b269d387 Mon Sep 17 00:00:00 2001 From: Joanne Shin Date: Thu, 14 Jul 2022 23:41:59 -0600 Subject: [PATCH 009/130] BUG: None-check in DictionaryObject.read_from_stream (#1113) Guard pdf.strict with check if pdf is None in DictionaryObject.read_from_stream Closes #1107 --- PyPDF2/generic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PyPDF2/generic.py b/PyPDF2/generic.py index e8a9d8d46..2c95a4a44 100644 --- a/PyPDF2/generic.py +++ b/PyPDF2/generic.py @@ -805,7 +805,7 @@ def read_unsized_from_steam(stream: StreamType, pdf: Any) -> bytes: # PdfReader f"Multiple definitions in dictionary at byte " f"{hex_str(stream.tell())} for key {key}" ) - if pdf.strict: + if pdf is not None and pdf.strict: raise PdfReadError(msg) else: warnings.warn(msg, PdfReadWarning) From dd2d69a8d89a1370753f1418b3e0df9a7908d928 Mon Sep 17 00:00:00 2001 From: Harry Karvonen Date: Sat, 16 Jul 2022 07:53:39 +0300 Subject: [PATCH 010/130] BUG: Prevent deduplication of PageObject (#1105) Make sure that PageObject is not deduplicated if it is not exactly same page object. Adobe Reader/Acrobat doesn't like it if same page is referred more than one time. Closes #1102 Co-authored-by: Harry Karvonen --- PyPDF2/_page.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/PyPDF2/_page.py b/PyPDF2/_page.py index 54ca9982d..340ee8d15 100644 --- a/PyPDF2/_page.py +++ b/PyPDF2/_page.py @@ -244,6 +244,11 @@ def __init__( self.pdf: Optional[PdfReader] = pdf self.indirect_ref = indirect_ref + def hash_value_data(self) -> bytes: + data = super().hash_value_data() + data += b"%d" % id(self) + return data + @staticmethod def create_blank_page( pdf: Optional[Any] = None, # PdfReader @@ -1287,7 +1292,9 @@ def process_operation(operator: bytes, operands: List) -> None: ) if isinstance(cmap[0], str): try: - t = tt.decode(cmap[0], "surrogatepass") # apply str encoding + t = tt.decode( + cmap[0], "surrogatepass" + ) # apply str encoding except Exception: # the data does not match the expectation, we use the alternative ; text extraction may not be good t = tt.decode( "utf-16-be" if cmap[0] == "charmap" else "charmap", From ed5ecd9d55cd669045fe47eadef4d049c7959b7d Mon Sep 17 00:00:00 2001 From: mtd91429 Date: Sat, 16 Jul 2022 13:14:19 -0500 Subject: [PATCH 011/130] MAINT: Destination.color returns ArrayObject instead of tuple as fallback (#1119) --- PyPDF2/generic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PyPDF2/generic.py b/PyPDF2/generic.py index 2c95a4a44..1354a910b 100644 --- a/PyPDF2/generic.py +++ b/PyPDF2/generic.py @@ -1889,9 +1889,9 @@ def bottom(self) -> Optional[FloatObject]: return self.get("/Bottom", None) @property - def color(self) -> Optional[tuple]: + def color(self) -> Optional[ArrayObject]: """Read-only property accessing the color in (R, G, B) with values 0.0-1.0""" - return self.get("/C", [FloatObject(0), FloatObject(0), FloatObject(0)]) + return self.get("/C", ArrayObject([FloatObject(0), FloatObject(0), FloatObject(0)])) @property def font_format(self) -> Optional[OutlineFontFlag]: From 5ddf4cb32505cb034496ac4be13747a61fb6ce46 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 17 Jul 2022 09:36:49 +0200 Subject: [PATCH 012/130] TST: Add MCVE showing outline title issue (#1123) See #1121 --- sample-files | 2 +- tests/test_reader.py | 62 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/sample-files b/sample-files index 6da0fbb53..31763905b 160000 --- a/sample-files +++ b/sample-files @@ -1 +1 @@ -Subproject commit 6da0fbb53f11bd5b8a4acf06e4d26e5e2bf5bf57 +Subproject commit 31763905b4a06014cbd23d2e03b7b5616661fed5 diff --git a/tests/test_reader.py b/tests/test_reader.py index 56bc2a70b..06d1cfdfb 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -17,6 +17,7 @@ PdfReadWarning, ) from PyPDF2.filters import _xobj_to_image +from PyPDF2.generic import Destination from . import get_pdf_from_url @@ -30,6 +31,7 @@ TESTS_ROOT = os.path.abspath(os.path.dirname(__file__)) PROJECT_ROOT = os.path.dirname(TESTS_ROOT) RESOURCE_ROOT = os.path.join(PROJECT_ROOT, "resources") +EXTERNAL_ROOT = Path(PROJECT_ROOT) / "sample-files" @pytest.mark.parametrize( @@ -872,3 +874,63 @@ def test_outline_font_format(): name = "tika-924546.pdf" reader = PdfReader(BytesIO(get_pdf_from_url(url, name=name))) assert reader.outlines[0].font_format == 2 + + +@pytest.mark.xfail(reason="#1121") +def test_outline_title_issue_1121(): + reader = PdfReader(EXTERNAL_ROOT / "014-outlines/mistitled_outlines_example.pdf") + + def get_titles_only(outlines, results=None): + if results is None: + results = [] + if isinstance(outlines, list): + for outline in outlines: + if isinstance(outline, Destination): + results.append(outline.title) + else: + results.append(get_titles_only(outline)) + else: + raise ValueError(f"got {type(outlines)}") + return results + + assert get_titles_only(reader.outlines) == [ + "First", + [ + "Second", + "Third", + "Fourth", + [ + "Fifth", + "Sixth", + ], + "Seventh", + [ + "Eighth", + "Ninth", + ], + ], + "Tenth", + [ + "Eleventh", + "Twelfth", + "Thirteenth", + "Fourteenth", + ], + "Fifteenth", + [ + "Sixteenth", + "Seventeenth", + ], + "Eighteenth", + "Nineteenth", + [ + "Twentieth", + "Twenty-first", + "Twenty-second", + "Twenty-third", + "Twenty-fourth", + "Twenty-fifth", + "Twenty-sixth", + "Twenty-seventh", + ], + ] From b1d4ea1fb4364336f84f1f3add19163aab2084a6 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 17 Jul 2022 09:58:31 +0200 Subject: [PATCH 013/130] TST: Add xfail test for IndexError when extracting text (#1124) See #1091 --- tests/test_page.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/test_page.py b/tests/test_page.py index fc7c2a71a..656c5111f 100644 --- a/tests/test_page.py +++ b/tests/test_page.py @@ -322,3 +322,14 @@ def test_get_fonts(pdf_path, password, embedded, unembedded): a = a.union(a_tmp) b = b.union(b_tmp) assert (a, b) == (embedded, unembedded) + + +@pytest.mark.xfail(reason="#1091") +def test_text_extraction_issue_1091(): + url = "https://corpora.tika.apache.org/base/docs/govdocs1/966/966635.pdf" + name = "tika-966635.pdf" + stream = BytesIO(get_pdf_from_url(url, name=name)) + with pytest.warns(PdfReadWarning): + reader = PdfReader(stream) + for page in reader.pages: + page.extract_text() From 8a010a5c899be2361ecd7dba29d2438425819ed4 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 17 Jul 2022 10:00:36 +0200 Subject: [PATCH 014/130] DOC: Explanation for git submodule --- docs/dev/intro.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/dev/intro.md b/docs/dev/intro.md index ced5725fb..702f4b503 100644 --- a/docs/dev/intro.md +++ b/docs/dev/intro.md @@ -31,6 +31,12 @@ most cases we typically want to test for. The `sample-files` might cover a lot more edge cases, the behavior we get when file sizes get bigger, different PDF producers. +In order to get the sample-files folder, you need to execute: + +``` +git submodule update --init +``` + ## Tools: git and pre-commit Git is a command line application for version control. If you don't know it, @@ -67,6 +73,8 @@ The `PREFIX` can be: * `ENH`: A new feature! Describe in the body what it can be used for. * `DEP`: A deprecation - either marking something as "this is going to be removed" or actually removing it. +* `PI`: A performance improvement. This could also be a reduction in the + file size of PDF files generated by PyPDF2. * `ROB`: A robustness change. Dealing better with broken PDF files. * `DOC`: A documentation change. * `TST`: Adding / adjusting tests. From cd87bbb4083347dc64aafa2571f5ebbe61f445f0 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 17 Jul 2022 10:13:15 +0200 Subject: [PATCH 015/130] TST: Add xfail for decryption fail (#1125) See #1088 --- tests/test_page.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_page.py b/tests/test_page.py index 656c5111f..aa6539d94 100644 --- a/tests/test_page.py +++ b/tests/test_page.py @@ -333,3 +333,12 @@ def test_text_extraction_issue_1091(): reader = PdfReader(stream) for page in reader.pages: page.extract_text() + + +@pytest.mark.xfail(reason="#1088") +def test_empyt_password_1088(): + url = "https://corpora.tika.apache.org/base/docs/govdocs1/941/941536.pdf" + name = "tika-941536.pdf" + stream = BytesIO(get_pdf_from_url(url, name=name)) + reader = PdfReader(stream) + len(reader.pages) From baeb7d23278de0f8d00ca9f2b656bf0674f08937 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 17 Jul 2022 10:16:36 +0200 Subject: [PATCH 016/130] STY: Apply black and isort --- PyPDF2/_cmap.py | 2 +- PyPDF2/generic.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/PyPDF2/_cmap.py b/PyPDF2/_cmap.py index 2c5ec2ee5..b07609f1e 100644 --- a/PyPDF2/_cmap.py +++ b/PyPDF2/_cmap.py @@ -269,7 +269,7 @@ def compute_space_width( w1 = {} st: int = 0 if "/DescendantFonts" in ft: # ft["/Subtype"].startswith("/CIDFontType"): - ft1 = ft["/DescendantFonts"][0].get_object() # type: ignore + ft1 = ft["/DescendantFonts"][0].get_object() # type: ignore try: w1[-1] = cast(float, ft1["/DW"]) except Exception: diff --git a/PyPDF2/generic.py b/PyPDF2/generic.py index 1354a910b..aa5444620 100644 --- a/PyPDF2/generic.py +++ b/PyPDF2/generic.py @@ -36,6 +36,7 @@ import logging import re import warnings +from enum import IntFlag from io import BytesIO from typing import ( Any, @@ -48,7 +49,7 @@ Union, cast, ) -from enum import IntFlag + from ._codecs import ( # noqa: rev_encoding _pdfdoc_encoding, _pdfdoc_encoding_rev, @@ -1891,7 +1892,9 @@ def bottom(self) -> Optional[FloatObject]: @property def color(self) -> Optional[ArrayObject]: """Read-only property accessing the color in (R, G, B) with values 0.0-1.0""" - return self.get("/C", ArrayObject([FloatObject(0), FloatObject(0), FloatObject(0)])) + return self.get( + "/C", ArrayObject([FloatObject(0), FloatObject(0), FloatObject(0)]) + ) @property def font_format(self) -> Optional[OutlineFontFlag]: From ae0ff49058e6c57a8edcfcd3d956665ddaa8a787 Mon Sep 17 00:00:00 2001 From: dkg Date: Sun, 17 Jul 2022 08:14:10 -0400 Subject: [PATCH 017/130] BUG: Avoid a crash when a ToUnicode CMap has an empty dstString in beginbfchar (#1118) This is not a principled fix, but it is a hack to avoid a crash when encountering an empty dstString in a `beginbfchar` table in a ToUnicode CMap. We take narrow aim at the issue of zero-length (empty) hex string representations. We take advantage of the fact that no angle-bracket-delimited hex string contains a . character. when we encounter an empty hex string, rather than replacing it with the empty string, we replace it with a literal ".". Then, when we encounter a ".", we remember that it was supposed to be an empty string. One consequence of this fix is that the exported cmap can now return an empty string, so we also have to clean up `PageObject::process_operation` so that it doesn't try to read the final character from an empty string. Closes #1111 --- PyPDF2/_cmap.py | 18 ++++++++++++++---- PyPDF2/_page.py | 1 + 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/PyPDF2/_cmap.py b/PyPDF2/_cmap.py index b07609f1e..75dc75ed1 100644 --- a/PyPDF2/_cmap.py +++ b/PyPDF2/_cmap.py @@ -191,7 +191,13 @@ def parse_to_unicode( for i in range(len(ll)): j = ll[i].find(b">") if j >= 0: - ll[i] = ll[i][:j].replace(b" ", b"") + b" " + ll[i][j + 1 :] + if j == 0: + # string is empty: stash a placeholder here (see below) + # see https://github.com/py-pdf/PyPDF2/issues/1111 + content = b"." + else: + content = ll[i][:j].replace(b" ", b"") + ll[i] = content + b" " + ll[i][j + 1 :] cm = ( (b" ".join(ll)) .replace(b"[", b" [ ") @@ -246,13 +252,17 @@ def parse_to_unicode( lst = [x for x in l.split(b" ") if x] map_dict[-1] = len(lst[0]) // 2 while len(lst) > 1: + map_to = "" + # placeholder (see above) means empty string + if lst[1] != b".": + map_to = unhexlify(lst[1]).decode( + "utf-16-be", "surrogatepass" + ) # join is here as some cases where the code was split map_dict[ unhexlify(lst[0]).decode( "charmap" if map_dict[-1] == 1 else "utf-16-be", "surrogatepass" ) - ] = unhexlify(lst[1]).decode( - "utf-16-be", "surrogatepass" - ) # join is here as some cases where the code was split + ] = map_to int_entry.append(int(lst[0], 16)) lst = lst[2:] for a, value in map_dict.items(): diff --git a/PyPDF2/_page.py b/PyPDF2/_page.py index 340ee8d15..efa3bd403 100644 --- a/PyPDF2/_page.py +++ b/PyPDF2/_page.py @@ -1384,6 +1384,7 @@ def process_operation(operator: bytes, operands: List) -> None: if ( (abs(float(op)) >= _space_width) and (abs(float(op)) <= 8 * _space_width) + and (len(text) > 0) and (text[-1] != " ") ): process_operation(b"Tj", [" "]) From 0b693e1122d568f29f266340121915b3813eb8c2 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 17 Jul 2022 20:41:45 +0200 Subject: [PATCH 018/130] TST: Add test for arab text (#1127) --- sample-files | 2 +- tests/test_page.py | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/sample-files b/sample-files index 31763905b..200644f72 160000 --- a/sample-files +++ b/sample-files @@ -1 +1 @@ -Subproject commit 31763905b4a06014cbd23d2e03b7b5616661fed5 +Subproject commit 200644f7219811c3930ad1732ef70c570ece2d16 diff --git a/tests/test_page.py b/tests/test_page.py index aa6539d94..d6e35e184 100644 --- a/tests/test_page.py +++ b/tests/test_page.py @@ -2,6 +2,7 @@ import os from copy import deepcopy from io import BytesIO +from pathlib import Path import pytest @@ -16,11 +17,11 @@ TESTS_ROOT = os.path.abspath(os.path.dirname(__file__)) PROJECT_ROOT = os.path.dirname(TESTS_ROOT) RESOURCE_ROOT = os.path.join(PROJECT_ROOT, "resources") -EXTERNAL_ROOT = os.path.join(PROJECT_ROOT, "sample-files") +EXTERNAL_ROOT = Path(PROJECT_ROOT) / "sample-files" def get_all_sample_files(): - with open(os.path.join(EXTERNAL_ROOT, "files.json")) as fp: + with open(EXTERNAL_ROOT / "files.json") as fp: data = fp.read() meta = json.loads(data) return meta @@ -37,7 +38,7 @@ def get_all_sample_files(): ) @pytest.mark.filterwarnings("ignore::PyPDF2.errors.PdfReadWarning") def test_read(meta): - pdf_path = os.path.join(EXTERNAL_ROOT, meta["path"]) + pdf_path = EXTERNAL_ROOT / meta["path"] reader = PdfReader(pdf_path) reader.pages[0] assert len(reader.pages) == meta["pages"] @@ -342,3 +343,9 @@ def test_empyt_password_1088(): stream = BytesIO(get_pdf_from_url(url, name=name)) reader = PdfReader(stream) len(reader.pages) + + +@pytest.mark.xfail(reason="#1088 / #1126") +def test_arab_text_extraction(): + reader = PdfReader(EXTERNAL_ROOT / "015-arabic/habibi.pdf") + assert reader.pages[0].extract_text() == "habibi حَبيبي" From e24b0a046635995c08c91ccf9d6900560d7fb390 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 17 Jul 2022 20:53:18 +0200 Subject: [PATCH 019/130] MAINT: Text extraction improvements (#1126) Credits to pubpub-zz, see https://github.com/py-pdf/PyPDF2/pull/1118#issuecomment-1186148575 Co-authored-by: pubpub-zz <4083478+pubpub-zz@users.noreply.github.com> --- PyPDF2/_page.py | 1 - 1 file changed, 1 deletion(-) diff --git a/PyPDF2/_page.py b/PyPDF2/_page.py index efa3bd403..756926d5e 100644 --- a/PyPDF2/_page.py +++ b/PyPDF2/_page.py @@ -1383,7 +1383,6 @@ def process_operation(operator: bytes, operands: List) -> None: if isinstance(op, (int, float, NumberObject, FloatObject)): if ( (abs(float(op)) >= _space_width) - and (abs(float(op)) <= 8 * _space_width) and (len(text) > 0) and (text[-1] != " ") ): From 7fba86b65e25809367ff169e779dbccb517e1b25 Mon Sep 17 00:00:00 2001 From: mtd91429 Date: Sun, 17 Jul 2022 14:04:58 -0500 Subject: [PATCH 020/130] BUG: Use `build_destination` for named destination outlines (#1128) Closes #1121 --- PyPDF2/_reader.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/PyPDF2/_reader.py b/PyPDF2/_reader.py index 7bff21ae8..43c79292e 100644 --- a/PyPDF2/_reader.py +++ b/PyPDF2/_reader.py @@ -834,8 +834,7 @@ def _build_outline(self, node: DictionaryObject) -> Optional[Destination]: if isinstance(dest, ArrayObject): outline = self._build_destination(title, dest) # type: ignore elif isinstance(dest, str) and dest in self._namedDests: - outline = self._namedDests[dest] - outline[NameObject("/Title")] = title # type: ignore + outline = self._build_destination(title, self._namedDests[dest].dest_array) # type: ignore else: raise PdfReadError(f"Unexpected destination {dest!r}") From 1800514a7e066c3a042b7d5ed93960b34c7fac2f Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 17 Jul 2022 21:05:42 +0200 Subject: [PATCH 021/130] TST: Remove xfail from test_outline_title_issue_1121 --- tests/test_reader.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_reader.py b/tests/test_reader.py index 06d1cfdfb..013bb0ed4 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -876,7 +876,6 @@ def test_outline_font_format(): assert reader.outlines[0].font_format == 2 -@pytest.mark.xfail(reason="#1121") def test_outline_title_issue_1121(): reader = PdfReader(EXTERNAL_ROOT / "014-outlines/mistitled_outlines_example.pdf") From 33634d40ffce9351f96fb35f491c2b3fe98b2406 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 17 Jul 2022 21:15:59 +0200 Subject: [PATCH 022/130] REL: 2.6.0 New Features (ENH): - Add color and font_format to PdfReader.outlines[i] (#1104) - Extract Text Enhancement (whitespaces) (#1084) Bug Fixes (BUG): - Use `build_destination` for named destination outlines (#1128) - Avoid a crash when a ToUnicode CMap has an empty dstString in beginbfchar (#1118) - Prevent deduplication of PageObject (#1105) - None-check in DictionaryObject.read_from_stream (#1113) - Avoid IndexError in _cmap.parse_to_unicode (#1110) Documentation (DOC): - Explanation for git submodule - Watermark and stamp (#1095) Maintenance (MAINT): - Text extraction improvements (#1126) - Destination.color returns ArrayObject instead of tuple as fallback (#1119) - Use add_bookmark_destination in add_bookmark (#1100) - Use add_bookmark_destination in add_bookmark_dict (#1099) Testing (TST): - Remove xfail from test_outline_title_issue_1121 - Add test for arab text (#1127) - Add xfail for decryption fail (#1125) - Add xfail test for IndexError when extracting text (#1124) - Add MCVE showing outline title issue (#1123) Code Style (STY): - Apply black and isort - Use IntFlag for permissions_flag / update_page_form_field_values (#1094) - Simplify code (#1101) Full Changelog: https://github.com/py-pdf/PyPDF2/compare/2.5.0...2.6.0 --- CHANGELOG.md | 35 +++++++++++++++++++++++++++++++++++ PyPDF2/_version.py | 2 +- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1822fba42..d06de5447 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,40 @@ # CHANGELOG +## Version 2.6.0, 2022-07-17 + +### New Features (ENH) +- Add color and font_format to PdfReader.outlines[i] (#1104) +- Extract Text Enhancement (whitespaces) (#1084) + +### Bug Fixes (BUG) +- Use `build_destination` for named destination outlines (#1128) +- Avoid a crash when a ToUnicode CMap has an empty dstString in beginbfchar (#1118) +- Prevent deduplication of PageObject (#1105) +- None-check in DictionaryObject.read_from_stream (#1113) +- Avoid IndexError in _cmap.parse_to_unicode (#1110) + +### Documentation (DOC) +- Explanation for git submodule +- Watermark and stamp (#1095) + +### Maintenance (MAINT) +- Text extraction improvements (#1126) +- Destination.color returns ArrayObject instead of tuple as fallback (#1119) +- Use add_bookmark_destination in add_bookmark (#1100) +- Use add_bookmark_destination in add_bookmark_dict (#1099) + +### Testing (TST) +- Add test for arab text (#1127) +- Add xfail for decryption fail (#1125) +- Add xfail test for IndexError when extracting text (#1124) +- Add MCVE showing outline title issue (#1123) + +### Code Style (STY) +- Use IntFlag for permissions_flag / update_page_form_field_values (#1094) +- Simplify code (#1101) + +Full Changelog: https://github.com/py-pdf/PyPDF2/compare/2.5.0...2.6.0 + ## Version 2.5.0, 2022-07-10 ### New Features (ENH) diff --git a/PyPDF2/_version.py b/PyPDF2/_version.py index 50062f87c..e5e59e38d 100644 --- a/PyPDF2/_version.py +++ b/PyPDF2/_version.py @@ -1 +1 @@ -__version__ = "2.5.0" +__version__ = "2.6.0" From 25cba33f88c6708ebc50169808f02b80e96fb0ab Mon Sep 17 00:00:00 2001 From: mtd91429 Date: Mon, 18 Jul 2022 00:45:36 -0500 Subject: [PATCH 023/130] ENH: Add `outline_count` property (#1129) Enables retrieval of "/Count" attribute of outline item in PdfReader.outlines by implementing property outline_count. Closes #1122 --- PyPDF2/_reader.py | 6 ++++- PyPDF2/generic.py | 10 ++++++++ tests/test_reader.py | 58 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 1 deletion(-) diff --git a/PyPDF2/_reader.py b/PyPDF2/_reader.py index 43c79292e..ac92e4e16 100644 --- a/PyPDF2/_reader.py +++ b/PyPDF2/_reader.py @@ -838,7 +838,7 @@ def _build_outline(self, node: DictionaryObject) -> Optional[Destination]: else: raise PdfReadError(f"Unexpected destination {dest!r}") - # if outline created, add color and format if present + # if outline created, add color, format, and child count if present if outline: if "/C" in node: # Color of outline in (R, G, B) with values ranging 0.0-1.0 @@ -847,6 +847,10 @@ def _build_outline(self, node: DictionaryObject) -> Optional[Destination]: # specifies style characteristics bold and/or italic # 1=italic, 2=bold, 3=both outline[NameObject("/F")] = node["/F"] + if "/Count" in node: + # absolute value = num. visible children + # positive = open/unfolded, negative = closed/folded + outline[NameObject("/Count")] = node["/Count"] return outline diff --git a/PyPDF2/generic.py b/PyPDF2/generic.py index aa5444620..ff416d145 100644 --- a/PyPDF2/generic.py +++ b/PyPDF2/generic.py @@ -1901,6 +1901,16 @@ def font_format(self) -> Optional[OutlineFontFlag]: """Read-only property accessing the font type. 1=italic, 2=bold, 3=both""" return self.get("/F", 0) + @property + def outline_count(self) -> Optional[int]: + """ + Read-only property accessing the outline count. + positive = expanded + negative = collapsed + absolute value = number of visible descendents at all levels + """ + return self.get("/Count", None) + class Bookmark(Destination): def write_to_stream( diff --git a/tests/test_reader.py b/tests/test_reader.py index 013bb0ed4..f163e8de7 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -933,3 +933,61 @@ def get_titles_only(outlines, results=None): "Twenty-seventh", ], ] + + +def test_outline_count(): + reader = PdfReader(EXTERNAL_ROOT / "014-outlines/mistitled_outlines_example.pdf") + + def get_counts_only(outlines, results=None): + if results is None: + results = [] + if isinstance(outlines, list): + for outline in outlines: + if isinstance(outline, Destination): + results.append(outline.outline_count) + else: + results.append(get_counts_only(outline)) + else: + raise ValueError(f"got {type(outlines)}") + return results + assert get_counts_only(reader.outlines) == [ + 5, + [ + None, + None, + 2, + [ + None, + None, + ], + -2, + [ + None, + None, + ], + ], + 4, + [ + None, + None, + None, + None, + ], + -2, + [ + None, + None, + ], + None, + 8, + [ + None, + None, + None, + None, + None, + None, + None, + None, + ], + ] From df95aae5215c7dcf7bfb14504b153427bbf8f44a Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Mon, 18 Jul 2022 07:56:53 +0200 Subject: [PATCH 024/130] STY: Re-use code via get_outlines_property in tests (#1130) --- tests/test_reader.py | 42 +++++++++++++++--------------------------- 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/tests/test_reader.py b/tests/test_reader.py index f163e8de7..7171953c7 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -876,23 +876,23 @@ def test_outline_font_format(): assert reader.outlines[0].font_format == 2 +def get_outlines_property(outlines, attribute_name: str): + results = [] + if isinstance(outlines, list): + for outline in outlines: + if isinstance(outline, Destination): + results.append(getattr(outline, attribute_name)) + else: + results.append(get_outlines_property(outline, attribute_name)) + else: + raise ValueError(f"got {type(outlines)}") + return results + + def test_outline_title_issue_1121(): reader = PdfReader(EXTERNAL_ROOT / "014-outlines/mistitled_outlines_example.pdf") - def get_titles_only(outlines, results=None): - if results is None: - results = [] - if isinstance(outlines, list): - for outline in outlines: - if isinstance(outline, Destination): - results.append(outline.title) - else: - results.append(get_titles_only(outline)) - else: - raise ValueError(f"got {type(outlines)}") - return results - - assert get_titles_only(reader.outlines) == [ + assert get_outlines_property(reader.outlines, "title") == [ "First", [ "Second", @@ -938,19 +938,7 @@ def get_titles_only(outlines, results=None): def test_outline_count(): reader = PdfReader(EXTERNAL_ROOT / "014-outlines/mistitled_outlines_example.pdf") - def get_counts_only(outlines, results=None): - if results is None: - results = [] - if isinstance(outlines, list): - for outline in outlines: - if isinstance(outline, Destination): - results.append(outline.outline_count) - else: - results.append(get_counts_only(outline)) - else: - raise ValueError(f"got {type(outlines)}") - return results - assert get_counts_only(reader.outlines) == [ + assert get_outlines_property(reader.outlines, "outline_count") == [ 5, [ None, From f2983e142f504d1fde7874af78975431e287043b Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Mon, 18 Jul 2022 08:53:13 +0200 Subject: [PATCH 025/130] DOC: Fix type in signature of PdfWriter.add_uri (#1131) --- PyPDF2/_writer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/PyPDF2/_writer.py b/PyPDF2/_writer.py index 843549ab0..7fff8a2a3 100644 --- a/PyPDF2/_writer.py +++ b/PyPDF2/_writer.py @@ -1399,7 +1399,7 @@ def removeText( def add_uri( self, pagenum: int, - uri: int, + uri: str, rect: RectangleObject, border: Optional[ArrayObject] = None, ) -> None: @@ -1408,7 +1408,7 @@ def add_uri( This uses the basic structure of :meth:`add_link` :param int pagenum: index of the page on which to place the URI action. - :param int uri: string -- uri of resource to link to. + :param str uri: URI of resource to link to. :param rect: :class:`RectangleObject` or array of four integers specifying the clickable rectangular area ``[xLL, yLL, xUR, yUR]``, or string in the form ``"[ xLL yLL xUR yUR ]"``. @@ -1466,7 +1466,7 @@ def add_uri( def addURI( self, pagenum: int, - uri: int, + uri: str, rect: RectangleObject, border: Optional[ArrayObject] = None, ) -> None: # pragma: no cover From c63a0ff24965bdbe9339ca5d837b5460f93c3c13 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Wed, 20 Jul 2022 22:54:06 +0200 Subject: [PATCH 026/130] DOC: Contributors file (#1132) We value the work of our contributors - of all of them. The CONTRIBUTORS file might give them more visibility and be more robust when the project is vendored into other projects. It is by far not complete - I hope that people add themselves in PRs :-) See #798 --- CONTRIBUTORS.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ docs/conf.py | 1 + docs/index.rst | 1 + 3 files changed, 50 insertions(+) create mode 100644 CONTRIBUTORS.md diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 000000000..180f52ae9 --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,48 @@ +# Contributors + +PyPDF2 had a lot of contributors since it started with pyPdf in 2005. We are +a free software project without any company affiliation. We cannot pay +contributors, but we do value their contributions. A lot of time, effort, and +expertise went into this project. With this list, we recognize those awesome +people 🤗 + +The list is definitely not complete. You can find more contributors via the git +history and [GitHubs 'Contributors' feature](https://github.com/py-pdf/PyPDF2/graphs/contributors). + +## Contributors to the pyPdf / PyPDF2 project + +* [Karvonen, Harry](https://github.com/Hatell/) +* [Pinheiro, Arthur](https://github.com/xilopaint) +* [pubpub-zz](https://github.com/pubpub-zz): involved in community development +* [Thoma, Martin](https://github.com/MartinThoma): Maintainer of PyPDF2 since April 2022. I hope to build a great community with many awesome contributors. [LinkedIn](https://www.linkedin.com/in/martin-thoma/) | [StackOverflow](https://stackoverflow.com/users/562769/martin-thoma) | [Blog](https://martin-thoma.com/) +* ztravis + +## Adding a new contributor + +Contributors are: + +* Anybody who has an commit in main - no matter how big/small or how many. Also if it's via co-authored-by. +* People who opened helpful issues: + (1) Bugs: with complete MCVE + (2) Well-described feature requests + (3) Potentially some more. + The maintainers of PyPDF2 have the last call on that one. +* Community work: This is exceptional. If the maintainers of PyPDF2 see people + being super helpful in answering issues / discussions or being very active on + Stackoverflow, we also consider them being contributors to PyPDF2. + +Contributors can add themselves or ask via an Github Issue to be added. + +Please use the following format: + +``` +* Last name, First name: 140-characters of text; links to linkedin / github / other profiles and personal pages are ok + +OR + +* GitHub Username: 140-characters of text; links to linkedin / github / other profiles and personal pages are ok +``` + +and add the entry in the alphabetical order. People who . The 140 characters are everything visible after the `Name:`. + +Please don't use images. diff --git a/docs/conf.py b/docs/conf.py index ec0538ce4..41e5fba22 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -17,6 +17,7 @@ sys.path.insert(0, os.path.abspath("../")) shutil.copyfile("../CHANGELOG.md", "meta/CHANGELOG.md") +shutil.copyfile("../CONTRIBUTORS.md", "meta/CONTRIBUTORS.md") # -- Project information ----------------------------------------------------- diff --git a/docs/index.rst b/docs/index.rst index 8f8ec7760..20eb49402 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -67,6 +67,7 @@ You can contribute to `PyPDF2 on Github `_. meta/CHANGELOG meta/project-governance meta/history + meta/CONTRIBUTORS meta/comparisons meta/faq From d41201b9f76fd93484f259e359877d9b87e1d201 Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Thu, 21 Jul 2022 06:54:58 +1000 Subject: [PATCH 027/130] STY: Fixing typos (#1137) There were typos in: - docs/meta/project-governance.md - tests/test_reader.py - tests/test_writer.py Fixes: - Should read `inducing` rather than `indiducing`. - Should read `decisions` rather than `decisons`. Signed-off-by: Tim Gates --- docs/meta/project-governance.md | 2 +- tests/test_reader.py | 2 +- tests/test_writer.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/meta/project-governance.md b/docs/meta/project-governance.md index dfb17aa9d..2b421fced 100644 --- a/docs/meta/project-governance.md +++ b/docs/meta/project-governance.md @@ -71,7 +71,7 @@ as their mother tongue. We try our best to understand others - The community can expect the following: * The **benevolent dictator** tries their best to make decisions from which the overall - community profits. The benevolent dictator is aware that his/her decisons can shape the + community profits. The benevolent dictator is aware that his/her decisions can shape the overall community. Once the benevolent dictator notices that she/he doesn't have the time to advance PyPDF2, he/she looks for a new benevolent dictator. As it is expected that the benevolent dictator will step down at some point of their choice diff --git a/tests/test_reader.py b/tests/test_reader.py index 7171953c7..a13d4c2c3 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -238,7 +238,7 @@ def test_get_images_raw(strict, with_prev_0, startx_correction, should_fail): pdf_data.find(b"4 0 obj"), pdf_data.find(b"5 0 obj"), b"/Prev 0 " if with_prev_0 else b"", - # startx_correction should be -1 due to double % at the beginning indiducing an error on startxref computation + # startx_correction should be -1 due to double % at the beginning inducing an error on startxref computation pdf_data.find(b"xref") + startx_correction, ) pdf_stream = io.BytesIO(pdf_data) diff --git a/tests/test_writer.py b/tests/test_writer.py index f2d3a56c6..b45485b26 100644 --- a/tests/test_writer.py +++ b/tests/test_writer.py @@ -203,7 +203,7 @@ def test_remove_text_all_operators(ignore_byte_string_object): pdf_data.find(b"4 0 obj") + startx_correction, pdf_data.find(b"5 0 obj") + startx_correction, pdf_data.find(b"6 0 obj") + startx_correction, - # startx_correction should be -1 due to double % at the beginning indiducing an error on startxref computation + # startx_correction should be -1 due to double % at the beginning inducing an error on startxref computation pdf_data.find(b"xref"), ) print(pdf_data.decode()) From 2abae354f4ce8e1cf44f90eba8a89da5f275dd03 Mon Sep 17 00:00:00 2001 From: pubpub-zz <4083478+pubpub-zz@users.noreply.github.com> Date: Wed, 20 Jul 2022 23:05:03 +0200 Subject: [PATCH 028/130] ROB: Cope with invalid parent xref (#1133) Rebuild the xref table if the parent chained xref is invalid Closes #1089 --- PyPDF2/_reader.py | 9 +++++++++ tests/test_reader.py | 11 +++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/PyPDF2/_reader.py b/PyPDF2/_reader.py index ac92e4e16..15bb2e7c3 100644 --- a/PyPDF2/_reader.py +++ b/PyPDF2/_reader.py @@ -1317,6 +1317,15 @@ def read(self, stream: StreamType) -> None: if found: continue # no xref table found at specified location + if "/Root" in self.trailer and not self.strict: + # if Root has been already found, just raise warning + warnings.warn("Invalid parent xref., rebuild xref", PdfReadWarning) + try: + self._rebuild_xref_table(stream) + break + except Exception: + raise PdfReadError("can not rebuild xref") + break raise PdfReadError("Could not find xref table at specified location") # if not zero-indexed, verify that the table is correct; change it if necessary if self.xref_index and not self.strict: diff --git a/tests/test_reader.py b/tests/test_reader.py index a13d4c2c3..7ea359741 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -770,12 +770,12 @@ def test_get_fields(): assert dict(fields["c1-1"]) == ({"/FT": "/Btn", "/T": "c1-1"}) +# covers also issue 1089 +@pytest.mark.filterwarnings("ignore::PyPDF2.errors.PdfReadWarning") def test_get_fields_read_else_block(): url = "https://corpora.tika.apache.org/base/docs/govdocs1/934/934771.pdf" name = "tika-934771.pdf" - with pytest.raises(PdfReadError) as exc: - PdfReader(BytesIO(get_pdf_from_url(url, name=name))) - assert exc.value.args[0] == "Could not find xref table at specified location" + PdfReader(BytesIO(get_pdf_from_url(url, name=name))) def test_get_fields_read_else_block2(): @@ -786,12 +786,11 @@ def test_get_fields_read_else_block2(): assert fields is None +@pytest.mark.filterwarnings("ignore::PyPDF2.errors.PdfReadWarning") def test_get_fields_read_else_block3(): url = "https://corpora.tika.apache.org/base/docs/govdocs1/957/957721.pdf" name = "tika-957721.pdf" - with pytest.raises(PdfReadError) as exc: - PdfReader(BytesIO(get_pdf_from_url(url, name=name))) - assert exc.value.args[0] == "Could not find xref table at specified location" + PdfReader(BytesIO(get_pdf_from_url(url, name=name))) def test_metadata_is_none(): From fd00f205f0ba34290cc61e2594e55c21f4c99c23 Mon Sep 17 00:00:00 2001 From: pubpub-zz <4083478+pubpub-zz@users.noreply.github.com> Date: Wed, 20 Jul 2022 23:08:31 +0200 Subject: [PATCH 029/130] ROB: Cope with missing /W entry (#1136) Closes #1134 --- PyPDF2/_cmap.py | 5 ++++- tests/test_page.py | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/PyPDF2/_cmap.py b/PyPDF2/_cmap.py index 75dc75ed1..36d12f476 100644 --- a/PyPDF2/_cmap.py +++ b/PyPDF2/_cmap.py @@ -284,7 +284,10 @@ def compute_space_width( w1[-1] = cast(float, ft1["/DW"]) except Exception: w1[-1] = 1000.0 - w = list(ft1["/W"]) # type: ignore + if "/W" in ft1: + w = list(ft1["/W"]) # type: ignore + else: + w = [] while len(w) > 0: st = w[0] second = w[1] diff --git a/tests/test_page.py b/tests/test_page.py index d6e35e184..f444d235f 100644 --- a/tests/test_page.py +++ b/tests/test_page.py @@ -243,6 +243,11 @@ def test_extract_text_single_quote_op(): "https://corpora.tika.apache.org/base/docs/govdocs1/932/932446.pdf", "tika-932446.pdf", ), + # iss 1134: + ( + "https://github.com/py-pdf/PyPDF2/files/9150656/ST.2019.PDF", + "iss_1134.pdf", + ), ], ) def test_extract_text_page_pdf(url, name): From c667ae4355507cccde493f000238bb1724159f15 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Thu, 21 Jul 2022 08:17:31 +0200 Subject: [PATCH 030/130] DOC: Recognize Lightup1 as a contributor --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 180f52ae9..47992f0f3 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -12,6 +12,7 @@ history and [GitHubs 'Contributors' feature](https://github.com/py-pdf/PyPDF2/gr ## Contributors to the pyPdf / PyPDF2 project * [Karvonen, Harry](https://github.com/Hatell/) +* [Lightup1](https://github.com/Lightup1) * [Pinheiro, Arthur](https://github.com/xilopaint) * [pubpub-zz](https://github.com/pubpub-zz): involved in community development * [Thoma, Martin](https://github.com/MartinThoma): Maintainer of PyPDF2 since April 2022. I hope to build a great community with many awesome contributors. [LinkedIn](https://www.linkedin.com/in/martin-thoma/) | [StackOverflow](https://stackoverflow.com/users/562769/martin-thoma) | [Blog](https://martin-thoma.com/) From fa96d66f6a82321ed13f2410754309f4c4c1db1c Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Thu, 21 Jul 2022 08:19:28 +0200 Subject: [PATCH 031/130] DEV: Add .git-blame-ignore-revs (#1141) See https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view --- .git-blame-ignore-revs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000..e4f010aae --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,15 @@ +# This file helps us to ignore style / formatting / doc changes +# in git blame. That is useful when we're trying to find the root cause of an +# error. + +# Docstring formatting +a89ff74d8c0203278a039d9496a3d8df4d134f84 + +# STY: Apply pre-commit (black, isort) + use snake_case variables (#832) +eef03d935dfeacaa75848b39082cf94d833d3174 + +# STY: Apply black and isort +baeb7d23278de0f8d00ca9f2b656bf0674f08937 + +# STY: Documentation, Variable names (#839) +444fca22836df061d9d23e71ffb7d68edcdfa766 From e1f9772693b788deae6b0fcdcb5ff49577706549 Mon Sep 17 00:00:00 2001 From: Matthew Peveler Date: Thu, 21 Jul 2022 09:12:15 -0700 Subject: [PATCH 032/130] BUG: Add deprecated EncodedStreamObject functions back until PyPDF2==3.0.0 (#1139) Accidentally, PyPDF2 did not follow the deprecation process: https://pypdf2.readthedocs.io/en/latest/dev/deprecations.html ISSUE: The EncodedStreamObject.getData / setData were removed AFFECTS: PyPDF2>=1.28.3,<=2.6.0 FIX: Add the getData / setData methods back with deprecation warnings Closes #1138 --- PyPDF2/generic.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/PyPDF2/generic.py b/PyPDF2/generic.py index ff416d145..5c8676fde 100644 --- a/PyPDF2/generic.py +++ b/PyPDF2/generic.py @@ -1162,9 +1162,17 @@ def get_data(self) -> Union[None, str, bytes]: self.decoded_self = decoded return decoded._data + def getData(self) -> Union[None, str, bytes]: # pragma: no cover + deprecate_with_replacement("getData", "get_data") + return self.get_data() + def set_data(self, data: Any) -> None: raise PdfReadError("Creating EncodedStreamObject is not currently supported") + def setData(self, data: Any) -> None: # pragma: no cover + deprecate_with_replacement("setData", "set_data") + return self.set_data(data) + class ContentStream(DecodedStreamObject): def __init__( From 7cba98a57789c4058898b47875d2dda0a48d6bb5 Mon Sep 17 00:00:00 2001 From: KourFrost Date: Thu, 21 Jul 2022 10:31:44 -0600 Subject: [PATCH 033/130] BUG: Make reader.get_fields also return dropdowns with options (#1114) Added /Opt to the checked field_attributes within reader.get_fields Closes #391 --- PyPDF2/_reader.py | 15 ++++++++++++--- PyPDF2/constants.py | 17 +++++++++++++++++ PyPDF2/generic.py | 7 ++++--- resources/libreoffice-form.pdf | Bin 0 -> 34186 bytes tests/test_generic.py | 5 +++++ tests/test_workflows.py | 7 +++++++ 6 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 resources/libreoffice-form.pdf diff --git a/PyPDF2/_reader.py b/PyPDF2/_reader.py index 15bb2e7c3..1ccdb9c77 100644 --- a/PyPDF2/_reader.py +++ b/PyPDF2/_reader.py @@ -65,7 +65,11 @@ from .constants import CatalogDictionary as CD from .constants import Core as CO from .constants import DocumentInformationAttributes as DI -from .constants import FieldDictionaryAttributes, GoToActionArguments +from .constants import ( + FieldDictionaryAttributes, + GoToActionArguments, + CheckboxRadioButtonAttributes, +) from .constants import PageAttributes as PG from .constants import PagesAttributes as PA from .constants import TrailerKeys as TK @@ -478,6 +482,7 @@ def get_fields( ``None`` if form data could not be located. """ field_attributes = FieldDictionaryAttributes.attributes_dict() + field_attributes.update(CheckboxRadioButtonAttributes.attributes_dict()) if retval is None: retval = {} catalog = cast(DictionaryObject, self.trailer[TK.ROOT]) @@ -488,7 +493,6 @@ def get_fields( return None if tree is None: return retval - self._check_kids(tree, retval, fileobj) for attr in field_attributes: if attr in tree: @@ -548,7 +552,12 @@ def _check_kids( self.get_fields(kid.get_object(), retval, fileobj) def _write_field(self, fileobj: Any, field: Any, field_attributes: Any) -> None: - for attr in FieldDictionaryAttributes.attributes(): + field_attributes_tuple = FieldDictionaryAttributes.attributes() + field_attributes_tuple = ( + field_attributes_tuple + CheckboxRadioButtonAttributes.attributes() + ) + + for attr in field_attributes_tuple: if attr in ( FieldDictionaryAttributes.Kids, FieldDictionaryAttributes.AA, diff --git a/PyPDF2/constants.py b/PyPDF2/constants.py index a195c22fe..ceac39865 100644 --- a/PyPDF2/constants.py +++ b/PyPDF2/constants.py @@ -332,6 +332,22 @@ def attributes_dict(cls) -> Dict[str, str]: } +class CheckboxRadioButtonAttributes: + """TABLE 8.76 Field flags common to all field types""" + + Opt = "/Opt" # Options, Optional + + @classmethod + def attributes(cls) -> Tuple[str, ...]: + return (cls.Opt,) + + @classmethod + def attributes_dict(cls) -> Dict[str, str]: + return { + cls.Opt: "Options", + } + + class FieldFlag(IntFlag): """TABLE 8.70 Field flags common to all field types""" @@ -411,6 +427,7 @@ class CatalogDictionary: CatalogAttributes, CatalogDictionary, CcittFaxDecodeParameters, + CheckboxRadioButtonAttributes, ColorSpaces, Core, DocumentInformationAttributes, diff --git a/PyPDF2/generic.py b/PyPDF2/generic.py index 5c8676fde..42cb1815a 100644 --- a/PyPDF2/generic.py +++ b/PyPDF2/generic.py @@ -68,7 +68,7 @@ skip_over_comment, str_, ) -from .constants import FieldDictionaryAttributes +from .constants import CheckboxRadioButtonAttributes, FieldDictionaryAttributes from .constants import FilterTypes as FT from .constants import StreamAttributes as SA from .constants import TypArguments as TA @@ -1618,8 +1618,9 @@ class Field(TreeObject): def __init__(self, data: Dict[str, Any]) -> None: DictionaryObject.__init__(self) - - for attr in FieldDictionaryAttributes.attributes(): + Field_attributes = FieldDictionaryAttributes.attributes() + Field_attributes = Field_attributes + CheckboxRadioButtonAttributes.attributes() + for attr in Field_attributes: try: self[NameObject(attr)] = data[attr] except KeyError: diff --git a/resources/libreoffice-form.pdf b/resources/libreoffice-form.pdf new file mode 100644 index 0000000000000000000000000000000000000000..7641b4d54a5cbb58de44b9b0f9f669ccddb4f868 GIT binary patch literal 34186 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 literal 0 HcmV?d00001 diff --git a/tests/test_generic.py b/tests/test_generic.py index c7b79b308..f2170f86e 100644 --- a/tests/test_generic.py +++ b/tests/test_generic.py @@ -11,6 +11,7 @@ Bookmark, BooleanObject, ByteStringObject, + CheckboxRadioButtonAttributes, Destination, DictionaryObject, FloatObject, @@ -491,3 +492,7 @@ def test_issue_997(): # cleanup os.remove(merged_filename) + + +def test_CheckboxRadioButtonAttributes_opt(): + assert "/Opt" in CheckboxRadioButtonAttributes.attributes_dict() diff --git a/tests/test_workflows.py b/tests/test_workflows.py index aaf99ac65..7ef216f82 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -22,6 +22,13 @@ sys.path.append(PROJECT_ROOT) +def test_dropdown_items(): + with open(os.path.join(RESOURCE_ROOT, "libreoffice-form.pdf"), "rb") as inputfile: + reader = PdfReader(inputfile) + fields = reader.get_fields() + assert "/Opt" in fields["Nationality"].keys() + + def test_PdfReaderFileLoad(): """ Test loading and parsing of a file. Extract text of the file and compare to expected From 0f520528b881688e7324ee5aab3c379dac678e1f Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Thu, 21 Jul 2022 18:53:15 +0200 Subject: [PATCH 034/130] STY: Variable naming / opening PDF with PdfReader (#1144) --- PyPDF2/generic.py | 8 +++++--- tests/test_workflows.py | 8 ++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/PyPDF2/generic.py b/PyPDF2/generic.py index 42cb1815a..91a47307e 100644 --- a/PyPDF2/generic.py +++ b/PyPDF2/generic.py @@ -1618,9 +1618,11 @@ class Field(TreeObject): def __init__(self, data: Dict[str, Any]) -> None: DictionaryObject.__init__(self) - Field_attributes = FieldDictionaryAttributes.attributes() - Field_attributes = Field_attributes + CheckboxRadioButtonAttributes.attributes() - for attr in Field_attributes: + field_attributes = ( + FieldDictionaryAttributes.attributes() + + CheckboxRadioButtonAttributes.attributes() + ) + for attr in field_attributes: try: self[NameObject(attr)] = data[attr] except KeyError: diff --git a/tests/test_workflows.py b/tests/test_workflows.py index 7ef216f82..47b2f3e56 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -23,10 +23,10 @@ def test_dropdown_items(): - with open(os.path.join(RESOURCE_ROOT, "libreoffice-form.pdf"), "rb") as inputfile: - reader = PdfReader(inputfile) - fields = reader.get_fields() - assert "/Opt" in fields["Nationality"].keys() + inputfile = os.path.join(RESOURCE_ROOT, "libreoffice-form.pdf") + reader = PdfReader(inputfile) + fields = reader.get_fields() + assert "/Opt" in fields["Nationality"].keys() def test_PdfReaderFileLoad(): From 6899c7448ee6d3546b4e3afa60754bd595556ead Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Thu, 21 Jul 2022 19:03:11 +0200 Subject: [PATCH 035/130] REL: 2.7.0 New Features (ENH): - Add `outline_count` property (#1129) Bug Fixes (BUG): - Make reader.get_fields also return dropdowns with options (#1114) - Add deprecated EncodedStreamObject functions back until PyPDF2==3.0.0 (#1139) Robustness (ROB): - Cope with missing /W entry (#1136) - Cope with invalid parent xref (#1133) Documentation (DOC): - Contributors file (#1132) - Fix type in signature of PdfWriter.add_uri (#1131) Developer Experience (DEV): - Add .git-blame-ignore-revs (#1141) Code Style (STY): - Fixing typos (#1137) - Re-use code via get_outlines_property in tests (#1130) Full Changelog: https://github.com/py-pdf/PyPDF2/compare/2.6.0...2.7.0 --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ PyPDF2/_version.py | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d06de5447..44f21d2cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ # CHANGELOG +## Version 2.7.0, 2022-07-21 + +### New Features (ENH) +- Add `outline_count` property (#1129) + +### Bug Fixes (BUG) +- Make reader.get_fields also return dropdowns with options (#1114) +- Add deprecated EncodedStreamObject functions back until PyPDF2==3.0.0 (#1139) + +### Robustness (ROB) +- Cope with missing /W entry (#1136) +- Cope with invalid parent xref (#1133) + +### Documentation (DOC) +- Contributors file (#1132) +- Fix type in signature of PdfWriter.add_uri (#1131) + +### Developer Experience (DEV) +- Add .git-blame-ignore-revs (#1141) + +### Code Style (STY) +- Fixing typos (#1137) +- Re-use code via get_outlines_property in tests (#1130) + +Full Changelog: https://github.com/py-pdf/PyPDF2/compare/2.6.0...2.7.0 + ## Version 2.6.0, 2022-07-17 ### New Features (ENH) diff --git a/PyPDF2/_version.py b/PyPDF2/_version.py index e5e59e38d..2614ce9d9 100644 --- a/PyPDF2/_version.py +++ b/PyPDF2/_version.py @@ -1 +1 @@ -__version__ = "2.6.0" +__version__ = "2.7.0" From 91357f047b697345cba9eb736b7e22862d9bcdaa Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Fri, 22 Jul 2022 07:41:11 +0200 Subject: [PATCH 036/130] DOC: Recognize KourFrost as a contributor --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 47992f0f3..28d3d9b8d 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -12,6 +12,7 @@ history and [GitHubs 'Contributors' feature](https://github.com/py-pdf/PyPDF2/gr ## Contributors to the pyPdf / PyPDF2 project * [Karvonen, Harry](https://github.com/Hatell/) +* [KourFrost](https://github.com/KourFrost) * [Lightup1](https://github.com/Lightup1) * [Pinheiro, Arthur](https://github.com/xilopaint) * [pubpub-zz](https://github.com/pubpub-zz): involved in community development From 1a65a4663cdd05e09d005a425ba674b0238fe0a0 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Fri, 22 Jul 2022 18:34:35 +0200 Subject: [PATCH 037/130] ENH: Add writer.add_annotation, page.annotations, and generic.AnnotationBuilder (#1120) * Add `page.annotations` (getter and setter) * Add `writer.add_annotation(page_number, annotation_dictionary)` * Add AnnotationBuilder to generate the `annotation_dictionary` for the different subtypes of annotations. Similarly, we could have an AnnotationsParser. See #107 Closes #981 --- PyPDF2/_page.py | 21 ++++++ PyPDF2/_writer.py | 40 ++++++++++ PyPDF2/generic.py | 101 +++++++++++++++++++++++++ docs/index.rst | 1 + docs/modules/AnnotationBuilder.rst | 7 ++ docs/user/adding-pdf-annotations.md | 68 +++++++++++++++++ docs/user/annotation-line.png | Bin 0 -> 87228 bytes docs/user/free-text-annotation.png | Bin 0 -> 80904 bytes tests/test_generic.py | 56 ++++++++++++++ tests/test_page.py | 112 +++++++++++++++++++++++++++- tests/test_writer.py | 34 ++++++++- 11 files changed, 437 insertions(+), 3 deletions(-) create mode 100644 docs/modules/AnnotationBuilder.rst create mode 100644 docs/user/annotation-line.png create mode 100644 docs/user/free-text-annotation.png diff --git a/PyPDF2/_page.py b/PyPDF2/_page.py index 756926d5e..935afbd81 100644 --- a/PyPDF2/_page.py +++ b/PyPDF2/_page.py @@ -1583,6 +1583,27 @@ def artBox(self, value: RectangleObject) -> None: # pragma: no cover deprecate_with_replacement("artBox", "artbox") self.artbox = value + @property + def annotations(self) -> Optional[ArrayObject]: + if "/Annots" not in self: + return None + else: + return cast(ArrayObject, self["/Annots"]) + + @annotations.setter + def annotations(self, value: Optional[ArrayObject]) -> None: + """ + Set the annotations array of the page. + + Typically you don't want to set this value, but append to it. + If you append to it, don't forget to add the object first to the writer + and only add the indirect object. + """ + if value is None: + del self[NameObject("/Annots")] + else: + self[NameObject("/Annots")] = value + class _VirtualList: def __init__( diff --git a/PyPDF2/_writer.py b/PyPDF2/_writer.py index 7fff8a2a3..61026f318 100644 --- a/PyPDF2/_writer.py +++ b/PyPDF2/_writer.py @@ -1794,6 +1794,46 @@ def pageMode(self, mode: PagemodeType) -> None: # pragma: no cover deprecate_with_replacement("pageMode", "page_mode") self.page_mode = mode + def add_annotation(self, page_number: int, annotation: Dict[str, Any]) -> None: + to_add = cast(DictionaryObject, _pdf_objectify(annotation)) + to_add[NameObject("/P")] = self.get_object(self._pages)["/Kids"][page_number] # type: ignore + page = self.pages[page_number] + if page.annotations is None: + page[NameObject("/Annots")] = ArrayObject() + assert page.annotations is not None + + ind_obj = self._add_object(to_add) + + page.annotations.append(ind_obj) + + +def _pdf_objectify(obj: Union[Dict[str, Any], str, int, List[Any]]) -> PdfObject: + if isinstance(obj, PdfObject): + return obj + if isinstance(obj, dict): + to_add = DictionaryObject() + for key, value in obj.items(): + name_key = NameObject(key) + casted_value = _pdf_objectify(value) + to_add[name_key] = casted_value + return to_add + elif isinstance(obj, list): + arr = ArrayObject() + for el in obj: + arr.append(_pdf_objectify(el)) + return arr + elif isinstance(obj, str): + if obj.startswith("/"): + return NameObject(obj) + else: + return TextStringObject(obj) + elif isinstance(obj, (int, float)): + return FloatObject(obj) + else: + raise NotImplementedError( + f"type(obj)={type(obj)} could not be casted to PdfObject" + ) + class PdfFileWriter(PdfWriter): # pragma: no cover def __init__(self, *args: Any, **kwargs: Any) -> None: diff --git a/PyPDF2/generic.py b/PyPDF2/generic.py index 91a47307e..3b82483c7 100644 --- a/PyPDF2/generic.py +++ b/PyPDF2/generic.py @@ -2058,3 +2058,104 @@ def decode_pdfdocencoding(byte_array: bytes) -> str: ) retval += c return retval + + +def hex_to_rgb(value: str) -> Tuple[float, float, float]: + return tuple(int(value[i : i + 2], 16) / 255.0 for i in (0, 2, 4)) # type: ignore + + +class AnnotationBuilder: + @staticmethod + def free_text( + text: str, + rect: Tuple[float, float, float, float], + font: str = "Helvetica", + bold: bool = False, + italic: bool = False, + font_size: str = "14pt", + font_color: str = "000000", + border_color: str = "000000", + background_color: str = "ffffff", + ) -> DictionaryObject: + """Add text in a rectangle to a page.""" + font_str = "font: " + if bold is True: + font_str = font_str + "bold " + if italic is True: + font_str = font_str + "italic " + font_str = font_str + font + " " + font_size + font_str = font_str + ";text-align:left;color:#" + font_color + + bg_color_str = "" + for st in hex_to_rgb(border_color): + bg_color_str = bg_color_str + str(st) + " " + bg_color_str = bg_color_str + "rg" + + free_text = DictionaryObject() + free_text.update( + { + NameObject("/Type"): NameObject("/Annot"), + NameObject("/Subtype"): NameObject("/FreeText"), + NameObject("/Rect"): RectangleObject(rect), + NameObject("/Contents"): TextStringObject(text), + # font size color + NameObject("/DS"): TextStringObject(font_str), + # border color + NameObject("/DA"): TextStringObject(bg_color_str), + # background color + NameObject("/C"): ArrayObject( + [FloatObject(n) for n in hex_to_rgb(background_color)] + ), + } + ) + return free_text + + @staticmethod + def line( + p1: Tuple[float, float], + p2: Tuple[float, float], + rect: Tuple[float, float, float, float], + text: str = "", + title_bar: str = "", + ) -> DictionaryObject: + """ + Draw a line on the PDF. + + :param p1: First point + :param p2: Second point + :param rect: Rectangle + :param text: Text to be displayed as the line annotation + :param title_bar: Text to be displayed in the title bar of the + annotation; by convention this is the name of the author + """ + line_obj = DictionaryObject( + { + NameObject("/Type"): NameObject("/Annot"), + NameObject("/Subtype"): NameObject("/Line"), + NameObject("/Rect"): RectangleObject(rect), + NameObject("/T"): TextStringObject(title_bar), + NameObject("/L"): ArrayObject( + [ + FloatObject(p1[0]), + FloatObject(p1[1]), + FloatObject(p2[0]), + FloatObject(p2[1]), + ] + ), + NameObject("/LE"): ArrayObject( + [ + NameObject(None), + NameObject(None), + ] + ), + NameObject("/IC"): ArrayObject( + [ + FloatObject(0.5), + FloatObject(0.5), + FloatObject(0.5), + ] + ), + NameObject("/Contents"): TextStringObject(text), + } + ) + return line_obj diff --git a/docs/index.rst b/docs/index.rst index 20eb49402..1d63c9102 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -50,6 +50,7 @@ You can contribute to `PyPDF2 on Github `_. modules/RectangleObject modules/Field modules/PageRange + modules/AnnotationBuilder .. toctree:: :caption: Developer Guide diff --git a/docs/modules/AnnotationBuilder.rst b/docs/modules/AnnotationBuilder.rst new file mode 100644 index 000000000..198f06501 --- /dev/null +++ b/docs/modules/AnnotationBuilder.rst @@ -0,0 +1,7 @@ +The AnnotationBuilder Class +--------------------------- + +.. autoclass:: PyPDF2.generic.AnnotationBuilder + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/user/adding-pdf-annotations.md b/docs/user/adding-pdf-annotations.md index d174e1114..78d130f21 100644 --- a/docs/user/adding-pdf-annotations.md +++ b/docs/user/adding-pdf-annotations.md @@ -14,3 +14,71 @@ writer.add_attachment("smile.png", data) with open("output.pdf", "wb") as output_stream: writer.write(output_stream) ``` + + +## Free Text + +If you want to add text in a box like this + +![](free-text-annotation.png) + +you can use the {py:class}`AnnotationBuilder `: + +```python +from PyPDF2 import PdfReader, PdfWriter +from PyPDF2.generic import AnnotationBuilder + +# Fill the writer with the pages you want +pdf_path = os.path.join(RESOURCE_ROOT, "crazyones.pdf") +reader = PdfReader(pdf_path) +page = reader.pages[0] +writer = PdfWriter() +writer.add_page(page) + +# Create the annotation and add it +annotation = AnnotationBuilder.free_text( + "Hello World\nThis is the second line!", + rect=(50, 550, 200, 650), + font="Arial", + bold=True, + italic=True, + font_size="20pt", + font_color="00ff00", + border_color="0000ff", + bg_color="cdcdcd", +) +writer.add_annotation(page_number=0, annotation=annotation) + +# Write the annotated file to disk +with open("annotated-pdf.pdf", "wb") as fp: + writer.write(fp) +``` + +## Line + +If you want to add a line like this: + +![](annotation-line.png) + +you can use the {py:class}`AnnotationBuilder `: + +```python +pdf_path = os.path.join(RESOURCE_ROOT, "crazyones.pdf") +reader = PdfReader(pdf_path) +page = reader.pages[0] +writer = PdfWriter() +writer.add_page(page) + +# Add the line +annotation = AnnotationBuilder.line( + text="Hello World\nLine2", + rect=(50, 550, 200, 650), + p1=(50, 550), + p2=(200, 650), +) +writer.add_annotation(page_number=0, annotation=annotation) + +# Write the annotated file to disk +with open("annotated-pdf.pdf", "wb") as fp: + writer.write(fp) +``` diff --git a/docs/user/annotation-line.png b/docs/user/annotation-line.png new file mode 100644 index 0000000000000000000000000000000000000000..6717e1147d29dd4cacbf47285eaa9f583e389454 GIT binary patch literal 87228 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≦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 Date: Fri, 22 Jul 2022 20:24:33 +0200 Subject: [PATCH 038/130] TST: Test CryptRC4 encryption class; test image extraction filters (#1147) --- PyPDF2/filters.py | 2 ++ tests/test_encryption.py | 14 ++++++++++++++ tests/test_workflows.py | 39 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/PyPDF2/filters.py b/PyPDF2/filters.py index 575528c5e..f23602db2 100644 --- a/PyPDF2/filters.py +++ b/PyPDF2/filters.py @@ -45,6 +45,8 @@ try: from typing import Literal # type: ignore[attr-defined] except ImportError: + # PEP 586 introduced typing.Literal with Python 3.8 + # For older Python versions, the backport typing_extensions is necessary: from typing_extensions import Literal # type: ignore[misc] from ._utils import b_, deprecate_with_replacement, ord_, paeth_predictor diff --git a/tests/test_encryption.py b/tests/test_encryption.py index 43ebdf20e..ab9d6089e 100644 --- a/tests/test_encryption.py +++ b/tests/test_encryption.py @@ -3,6 +3,7 @@ import pytest import PyPDF2 +from PyPDF2._encryption import CryptRC4 from PyPDF2.errors import DependencyError try: @@ -139,3 +140,16 @@ def test_encryption_merge(names): pdf_merger.append(pdf) # no need to write to file pdf_merger.close() + + +@pytest.mark.parametrize( + "cryptcls", + [ + CryptRC4, + ], +) +def test_encrypt_decrypt_class(cryptcls): + message = b"Hello World" + key = bytes(0 for _ in range(128)) # b"secret key" + crypt = cryptcls(key) + assert crypt.decrypt(crypt.encrypt(message)) == message diff --git a/tests/test_workflows.py b/tests/test_workflows.py index 47b2f3e56..6dfb0a680 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -439,3 +439,42 @@ def test_image_extraction(url, name): for filepath in images_extracted: if os.path.exists(filepath): os.remove(filepath) + + +@pytest.mark.parametrize( + ("url", "name"), + [ + ( + "https://corpora.tika.apache.org/base/docs/govdocs1/977/977609.pdf", + "tika-977609.pdf", + ), + ], +) +def test_image_extraction2(url, name): + data = BytesIO(get_pdf_from_url(url, name=name)) + reader = PdfReader(data) + + images_extracted = [] + root = Path("extracted-images") + if not root.exists(): + os.mkdir(root) + + for page in reader.pages: + if RES.XOBJECT in page[PG.RESOURCES]: + x_object = page[PG.RESOURCES][RES.XOBJECT].get_object() + + for obj in x_object: + if x_object[obj][IA.SUBTYPE] == "/Image": + extension, byte_stream = _xobj_to_image(x_object[obj]) + if extension is not None: + filename = root / (obj[1:] + extension) + with open(filename, "wb") as img: + img.write(byte_stream) + images_extracted.append(filename) + + # Cleanup + do_cleanup = True # set this to False for manual inspection + if do_cleanup: + for filepath in images_extracted: + if os.path.exists(filepath): + os.remove(filepath) From f233c1ad5adebe405e1184afa73442c92491aa8f Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Fri, 22 Jul 2022 23:37:53 +0200 Subject: [PATCH 039/130] TST: Decrypt file which is not encrypted (#1149) --- tests/test_encryption.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/test_encryption.py b/tests/test_encryption.py index ab9d6089e..234613d60 100644 --- a/tests/test_encryption.py +++ b/tests/test_encryption.py @@ -3,8 +3,9 @@ import pytest import PyPDF2 +from PyPDF2 import PdfReader from PyPDF2._encryption import CryptRC4 -from PyPDF2.errors import DependencyError +from PyPDF2.errors import DependencyError, PdfReadError try: from Crypto.Cipher import AES # noqa: F401 @@ -153,3 +154,10 @@ def test_encrypt_decrypt_class(cryptcls): key = bytes(0 for _ in range(128)) # b"secret key" crypt = cryptcls(key) assert crypt.decrypt(crypt.encrypt(message)) == message + + +def test_decrypt_not_decrypted_pdf(): + path = os.path.join(RESOURCE_ROOT, "crazyones.pdf") + with pytest.raises(PdfReadError) as exc: + PdfReader(path, password="nonexistant") + assert exc.value.args[0] == "Not encrypted file" From 89c0ff2e95f76960ffa7958e956270d41d3fea79 Mon Sep 17 00:00:00 2001 From: mtd91429 Date: Sat, 23 Jul 2022 01:21:12 -0500 Subject: [PATCH 040/130] ROB: Handle outlines without valid destination (#1076) Adjust `PdfReader._build_outline(...)` and `PdfReader._build_destination(...)` to handle outline items with and without valid destinations Closes #193 : PdfReadError: Unexpected destination '/__WKANCHOR_2' Closes #956 : ValueError: Unresolved bookmark #1059 no longer throws an exception, but the outlines are not extracted either. Closes #1068 : Skip NameObject when building outline --- PyPDF2/_merger.py | 2 - PyPDF2/_reader.py | 77 +++++++++++++----- resources/outline-without-title.pdf | Bin 0 -> 79232 bytes .../outlines-with-invalid-destinations.pdf | Bin 0 -> 71605 bytes tests/test_reader.py | 42 +++++++--- 5 files changed, 87 insertions(+), 34 deletions(-) create mode 100644 resources/outline-without-title.pdf create mode 100644 resources/outlines-with-invalid-destinations.pdf diff --git a/PyPDF2/_merger.py b/PyPDF2/_merger.py index 319a47ccd..69fbce90d 100644 --- a/PyPDF2/_merger.py +++ b/PyPDF2/_merger.py @@ -540,8 +540,6 @@ def _associate_bookmarks_to_pages( if pageno is not None: b[NameObject("/Page")] = NumberObject(pageno) - else: - raise ValueError(f"Unresolved bookmark '{b['/Title']}'") def find_bookmark( self, diff --git a/PyPDF2/_reader.py b/PyPDF2/_reader.py index 1ccdb9c77..844ef9fec 100644 --- a/PyPDF2/_reader.py +++ b/PyPDF2/_reader.py @@ -807,15 +807,31 @@ def _build_destination( title: str, array: List[Union[NumberObject, IndirectObject, NullObject, DictionaryObject]], ) -> Destination: - page, typ = array[0:2] - array = array[2:] - try: - return Destination(title, page, typ, *array) # type: ignore - except PdfReadError: - warnings.warn(f"Unknown destination: {title} {array}", PdfReadWarning) - if self.strict: - raise - else: + page, typ = None, None + # handle outlines with missing or invalid destination + if ( + isinstance(array, (type(None), NullObject)) + or ( + isinstance(array, ArrayObject) + and len(array) == 0 + ) + or ( + isinstance(array, str) + ) + ): + + page = NullObject() + typ = TextStringObject("/Fit") + return Destination(title, page, typ) + else: + page, typ = array[0:2] # type: ignore + array = array[2:] + try: + return Destination(title, page, typ, *array) # type: ignore + except PdfReadError: + warnings.warn(f"Unknown destination: {title} {array}", PdfReadWarning) + if self.strict: + raise # create a link to first Page tmp = self.pages[0].indirect_ref indirect_ref = NullObject() if tmp is None else tmp @@ -826,26 +842,45 @@ def _build_destination( def _build_outline(self, node: DictionaryObject) -> Optional[Destination]: dest, title, outline = None, None, None - if "/A" in node and "/Title" in node: - # Action, section 8.5 (only type GoTo supported) + # title required for valid outline + # PDF Reference 1.7: TABLE 8.4 Entries in an outline item dictionary + try: title = node["/Title"] + except KeyError: + if self.strict: + raise PdfReadError(f"Outline Entry Missing /Title attribute: {node!r}") + title = '' # type: ignore + + if "/A" in node: + # Action, PDFv1.7 Section 12.6 (only type GoTo supported) action = cast(DictionaryObject, node["/A"]) action_type = cast(NameObject, action[GoToActionArguments.S]) if action_type == "/GoTo": dest = action[GoToActionArguments.D] - elif "/Dest" in node and "/Title" in node: - # Destination, section 8.2.1 - title = node["/Title"] + elif "/Dest" in node: + # Destination, PDFv1.7 Section 12.3.2 dest = node["/Dest"] - - # if destination found, then create outline - if dest: - if isinstance(dest, ArrayObject): - outline = self._build_destination(title, dest) # type: ignore - elif isinstance(dest, str) and dest in self._namedDests: + # if array was referenced in another object, will be a dict w/ key "/D" + if isinstance(dest, DictionaryObject) and "/D" in dest: + dest = dest["/D"] + + if isinstance(dest, ArrayObject): + outline = self._build_destination(title, dest) # type: ignore + elif isinstance(dest, str): + # named destination, addresses NameObject Issue #193 + try: outline = self._build_destination(title, self._namedDests[dest].dest_array) # type: ignore - else: + except KeyError: + # named destination not found in Name Dict + outline = self._build_destination(title, None) + elif isinstance(dest, type(None)): + # outline not required to have destination or action + # PDFv1.7 Table 153 + outline = self._build_destination(title, dest) # type: ignore + else: + if self.strict: raise PdfReadError(f"Unexpected destination {dest!r}") + outline = self._build_destination(title, None) # type: ignore # if outline created, add color, format, and child count if present if outline: diff --git a/resources/outline-without-title.pdf b/resources/outline-without-title.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2734f5f820e7cdb240800386d7bda2ac7996d4da GIT binary patch literal 79232 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 A00000 literal 0 HcmV?d00001 diff --git a/tests/test_reader.py b/tests/test_reader.py index 7ea359741..042f0e25e 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -6,7 +6,7 @@ import pytest -from PyPDF2 import PdfMerger, PdfReader +from PyPDF2 import PdfReader from PyPDF2._reader import convert_to_int, convertToInt from PyPDF2.constants import ImageAttributes as IA from PyPDF2.constants import PageAttributes as PG @@ -812,16 +812,6 @@ def test_get_fields_read_write_report(): os.remove("tmp-fields-report.txt") -def test_unexpected_destination(): - url = "https://corpora.tika.apache.org/base/docs/govdocs1/913/913678.pdf" - name = "tika-913678.pdf" - reader = PdfReader(BytesIO(get_pdf_from_url(url, name=name))) - merger = PdfMerger() - with pytest.raises(PdfReadError) as exc: - merger.append(reader) - assert exc.value.args[0] == "Unexpected destination '/1'" - - @pytest.mark.parametrize( "src", [ @@ -978,3 +968,33 @@ def test_outline_count(): None, ], ] + + +def test_outline_missing_title(): + reader = PdfReader(os.path.join(RESOURCE_ROOT, "outline-without-title.pdf"), strict=True) + with pytest.raises(PdfReadError) as exc: + reader.outlines + assert exc.value.args[0].startswith("Outline Entry Missing /Title attribute:") + + +def test_outline_with_missing_named_destination(): + url = "https://corpora.tika.apache.org/base/docs/govdocs1/913/913678.pdf" + name = "tika-913678.pdf" + reader = PdfReader(BytesIO(get_pdf_from_url(url, name=name))) + # outline items in document reference a named destination that is not defined + assert reader.outlines[1][0].title.startswith('Report for 2002AZ3B: Microbial') + + +def test_outline_with_empty_action(): + url = "https://corpora.tika.apache.org/base/docs/govdocs1/924/924546.pdf" + name = "tika-924546.pdf" + reader = PdfReader(BytesIO(get_pdf_from_url(url, name=name))) + # outline (entitled Tables and Figures) utilize an empty action (/A) + # that has no type or destination + assert reader.outlines[-4].title == 'Tables' + + +def test_outlines_with_invalid_destinations(): + reader = PdfReader(os.path.join(RESOURCE_ROOT, "outlines-with-invalid-destinations.pdf")) + # contains 9 outlines, 6 with invalid destinations caused by different malformations + assert len(reader.outlines) == 9 From a6d27d754776fe1501ccffed26f0e5becf3d2faa Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sat, 23 Jul 2022 10:00:07 +0200 Subject: [PATCH 041/130] MAINT: Reduce PdfReader.read complexity (#1151) --- PyPDF2/_reader.py | 238 +++++++++++++++++++++++-------------------- tests/test_reader.py | 12 ++- 2 files changed, 133 insertions(+), 117 deletions(-) diff --git a/PyPDF2/_reader.py b/PyPDF2/_reader.py index 844ef9fec..e9817dfaf 100644 --- a/PyPDF2/_reader.py +++ b/PyPDF2/_reader.py @@ -63,13 +63,10 @@ from .constants import CatalogAttributes as CA from .constants import CatalogDictionary from .constants import CatalogDictionary as CD +from .constants import CheckboxRadioButtonAttributes from .constants import Core as CO from .constants import DocumentInformationAttributes as DI -from .constants import ( - FieldDictionaryAttributes, - GoToActionArguments, - CheckboxRadioButtonAttributes, -) +from .constants import FieldDictionaryAttributes, GoToActionArguments from .constants import PageAttributes as PG from .constants import PagesAttributes as PA from .constants import TrailerKeys as TK @@ -811,13 +808,8 @@ def _build_destination( # handle outlines with missing or invalid destination if ( isinstance(array, (type(None), NullObject)) - or ( - isinstance(array, ArrayObject) - and len(array) == 0 - ) - or ( - isinstance(array, str) - ) + or (isinstance(array, ArrayObject) and len(array) == 0) + or (isinstance(array, str)) ): page = NullObject() @@ -849,7 +841,7 @@ def _build_outline(self, node: DictionaryObject) -> Optional[Destination]: except KeyError: if self.strict: raise PdfReadError(f"Outline Entry Missing /Title attribute: {node!r}") - title = '' # type: ignore + title = "" # type: ignore if "/A" in node: # Action, PDFv1.7 Section 12.6 (only type GoTo supported) @@ -1257,26 +1249,8 @@ def cacheIndirectObject( return self.cache_indirect_object(generation, idnum, obj) def read(self, stream: StreamType) -> None: - # start at the end: - stream.seek(0, os.SEEK_END) - if not stream.tell(): - raise PdfReadError("Cannot read an empty file") - if self.strict: - stream.seek(0, os.SEEK_SET) - header_byte = stream.read(5) - if header_byte != b"%PDF-": - raise PdfReadError( - f"PDF starts with '{header_byte.decode('utf8')}', " - "but '%PDF-' expected" - ) - stream.seek(0, os.SEEK_END) - last_mb = stream.tell() - 1024 * 1024 + 1 # offset of last MB of stream - line = b"" - while line[:5] != b"%%EOF": - if stream.tell() < last_mb: - raise PdfReadError("EOF marker not found") - line = read_previous_line(stream) - + self._basic_validation(stream) + self._find_eof_marker(stream) startxref = self._find_startxref_pos(stream) # check and eventually correct the startxref only in not strict @@ -1290,87 +1264,8 @@ def read(self, stream: StreamType) -> None: ) # read all cross reference tables and their trailers - self.xref: Dict[int, Dict[Any, Any]] = {} - self.xref_free_entry: Dict[int, Dict[Any, Any]] = {} - self.xref_objStm: Dict[int, Tuple[Any, Any]] = {} - self.trailer = DictionaryObject() - while True: - # load the xref table - stream.seek(startxref, 0) - x = stream.read(1) - if x == b"x": - self._read_standard_xref_table(stream) - read_non_whitespace(stream) - stream.seek(-1, 1) - new_trailer = cast(Dict[str, Any], read_object(stream, self)) - for key, value in new_trailer.items(): - if key not in self.trailer: - self.trailer[key] = value - if "/Prev" in new_trailer: - startxref = new_trailer["/Prev"] - else: - break - elif xref_issue_nr: - try: - self._rebuild_xref_table(stream) - break - except Exception: - xref_issue_nr = 0 - elif x.isdigit(): - xrefstream = self._read_pdf15_xref_stream(stream) + self._read_xref_tables_and_trailers(stream, startxref, xref_issue_nr) - trailer_keys = TK.ROOT, TK.ENCRYPT, TK.INFO, TK.ID - for key in trailer_keys: - if key in xrefstream and key not in self.trailer: - self.trailer[NameObject(key)] = xrefstream.raw_get(key) - if "/Prev" in xrefstream: - startxref = cast(int, xrefstream["/Prev"]) - else: - break - else: - # some PDFs have /Prev=0 in the trailer, instead of no /Prev - if startxref == 0: - if self.strict: - raise PdfReadError( - "/Prev=0 in the trailer (try opening with strict=False)" - ) - else: - warnings.warn( - "/Prev=0 in the trailer - assuming there" - " is no previous xref table" - ) - break - # bad xref character at startxref. Let's see if we can find - # the xref table nearby, as we've observed this error with an - # off-by-one before. - stream.seek(-11, 1) - tmp = stream.read(20) - xref_loc = tmp.find(b"xref") - if xref_loc != -1: - startxref -= 10 - xref_loc - continue - # No explicit xref table, try finding a cross-reference stream. - stream.seek(startxref, 0) - found = False - for look in range(5): - if stream.read(1).isdigit(): - # This is not a standard PDF, consider adding a warning - startxref += look - found = True - break - if found: - continue - # no xref table found at specified location - if "/Root" in self.trailer and not self.strict: - # if Root has been already found, just raise warning - warnings.warn("Invalid parent xref., rebuild xref", PdfReadWarning) - try: - self._rebuild_xref_table(stream) - break - except Exception: - raise PdfReadError("can not rebuild xref") - break - raise PdfReadError("Could not find xref table at specified location") # if not zero-indexed, verify that the table is correct; change it if necessary if self.xref_index and not self.strict: loc = stream.tell() @@ -1390,6 +1285,29 @@ def read(self, stream: StreamType) -> None: # non-zero-index is actually correct stream.seek(loc, 0) # return to where it was + def _basic_validation(self, stream: StreamType) -> None: + # start at the end: + stream.seek(0, os.SEEK_END) + if not stream.tell(): + raise PdfReadError("Cannot read an empty file") + if self.strict: + stream.seek(0, os.SEEK_SET) + header_byte = stream.read(5) + if header_byte != b"%PDF-": + raise PdfReadError( + f"PDF starts with '{header_byte.decode('utf8')}', " + "but '%PDF-' expected" + ) + stream.seek(0, os.SEEK_END) + + def _find_eof_marker(self, stream: StreamType) -> None: + last_mb = stream.tell() - 1024 * 1024 + 1 # offset of last MB of stream + line = b"" + while line[:5] != b"%%EOF": + if stream.tell() < last_mb: + raise PdfReadError("EOF marker not found") + line = read_previous_line(stream) + def _find_startxref_pos(self, stream: StreamType) -> int: """Find startxref entry - the location of the xref table""" line = read_previous_line(stream) @@ -1482,6 +1400,100 @@ def _read_standard_xref_table(self, stream: StreamType) -> None: else: break + def _read_xref_tables_and_trailers( + self, stream: StreamType, startxref: Optional[int], xref_issue_nr: int + ) -> None: + self.xref: Dict[int, Dict[Any, Any]] = {} + self.xref_free_entry: Dict[int, Dict[Any, Any]] = {} + self.xref_objStm: Dict[int, Tuple[Any, Any]] = {} + self.trailer = DictionaryObject() + while startxref is not None: + # load the xref table + stream.seek(startxref, 0) + x = stream.read(1) + if x == b"x": + startxref = self._read_xref(stream) + elif xref_issue_nr: + try: + self._rebuild_xref_table(stream) + break + except Exception: + xref_issue_nr = 0 + elif x.isdigit(): + xrefstream = self._read_pdf15_xref_stream(stream) + + trailer_keys = TK.ROOT, TK.ENCRYPT, TK.INFO, TK.ID + for key in trailer_keys: + if key in xrefstream and key not in self.trailer: + self.trailer[NameObject(key)] = xrefstream.raw_get(key) + if "/Prev" in xrefstream: + startxref = cast(int, xrefstream["/Prev"]) + else: + break + else: + startxref = self._read_xref_other_error(stream, startxref) + + def _read_xref(self, stream: StreamType) -> Optional[int]: + self._read_standard_xref_table(stream) + read_non_whitespace(stream) + stream.seek(-1, 1) + new_trailer = cast(Dict[str, Any], read_object(stream, self)) + for key, value in new_trailer.items(): + if key not in self.trailer: + self.trailer[key] = value + if "/Prev" in new_trailer: + startxref = new_trailer["/Prev"] + return startxref + else: + return None + + def _read_xref_other_error( + self, stream: StreamType, startxref: int + ) -> Optional[int]: + # some PDFs have /Prev=0 in the trailer, instead of no /Prev + if startxref == 0: + if self.strict: + raise PdfReadError( + "/Prev=0 in the trailer (try opening with strict=False)" + ) + else: + warnings.warn( + "/Prev=0 in the trailer - assuming there" + " is no previous xref table" + ) + return None + # bad xref character at startxref. Let's see if we can find + # the xref table nearby, as we've observed this error with an + # off-by-one before. + stream.seek(-11, 1) + tmp = stream.read(20) + xref_loc = tmp.find(b"xref") + if xref_loc != -1: + startxref -= 10 - xref_loc + return startxref + # No explicit xref table, try finding a cross-reference stream. + stream.seek(startxref, 0) + found = False + for look in range(5): + if stream.read(1).isdigit(): + # This is not a standard PDF, consider adding a warning + startxref += look + found = True + break + if found: + return startxref + # no xref table found at specified location + if "/Root" in self.trailer and not self.strict: + # if Root has been already found, just raise warning + warnings.warn("Invalid parent xref., rebuild xref", PdfReadWarning) + try: + self._rebuild_xref_table(stream) + return None + except Exception: + raise PdfReadError("can not rebuild xref") + return None + raise PdfReadError("Could not find xref table at specified location") + def _read_pdf15_xref_stream( self, stream: StreamType ) -> Union[ContentStream, EncodedStreamObject, DecodedStreamObject]: diff --git a/tests/test_reader.py b/tests/test_reader.py index 042f0e25e..97365da49 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -971,7 +971,9 @@ def test_outline_count(): def test_outline_missing_title(): - reader = PdfReader(os.path.join(RESOURCE_ROOT, "outline-without-title.pdf"), strict=True) + reader = PdfReader( + os.path.join(RESOURCE_ROOT, "outline-without-title.pdf"), strict=True + ) with pytest.raises(PdfReadError) as exc: reader.outlines assert exc.value.args[0].startswith("Outline Entry Missing /Title attribute:") @@ -982,7 +984,7 @@ def test_outline_with_missing_named_destination(): name = "tika-913678.pdf" reader = PdfReader(BytesIO(get_pdf_from_url(url, name=name))) # outline items in document reference a named destination that is not defined - assert reader.outlines[1][0].title.startswith('Report for 2002AZ3B: Microbial') + assert reader.outlines[1][0].title.startswith("Report for 2002AZ3B: Microbial") def test_outline_with_empty_action(): @@ -991,10 +993,12 @@ def test_outline_with_empty_action(): reader = PdfReader(BytesIO(get_pdf_from_url(url, name=name))) # outline (entitled Tables and Figures) utilize an empty action (/A) # that has no type or destination - assert reader.outlines[-4].title == 'Tables' + assert reader.outlines[-4].title == "Tables" def test_outlines_with_invalid_destinations(): - reader = PdfReader(os.path.join(RESOURCE_ROOT, "outlines-with-invalid-destinations.pdf")) + reader = PdfReader( + os.path.join(RESOURCE_ROOT, "outlines-with-invalid-destinations.pdf") + ) # contains 9 outlines, 6 with invalid destinations caused by different malformations assert len(reader.outlines) == 9 From 27702c2e098fcf62b37d34ee52cfeeb6c3cc4f12 Mon Sep 17 00:00:00 2001 From: pubpub-zz <4083478+pubpub-zz@users.noreply.github.com> Date: Sat, 23 Jul 2022 16:34:28 +0200 Subject: [PATCH 042/130] ROB: Cope with null params for FitH /FitV destination (#1152) iaw PDF specifications, page 583 Closes #1145 --- PyPDF2/generic.py | 13 +++++++++++-- tests/test_merger.py | 8 ++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/PyPDF2/generic.py b/PyPDF2/generic.py index 3b82483c7..5b84ec02d 100644 --- a/PyPDF2/generic.py +++ b/PyPDF2/generic.py @@ -135,6 +135,9 @@ def writeToStream( deprecate_with_replacement("writeToStream", "write_to_stream") self.write_to_stream(stream, encryption_key) + def __repr__(self) -> str: + return "NullObject" + @staticmethod def readFromStream(stream: StreamType) -> "NullObject": # pragma: no cover deprecate_with_replacement("readFromStream", "read_from_stream") @@ -1813,9 +1816,15 @@ def __init__( self[NameObject(TA.TOP)], ) = args elif typ in [TF.FIT_H, TF.FIT_BH]: - (self[NameObject(TA.TOP)],) = args + try: # Prefered to be more robust not only to null parameters + (self[NameObject(TA.TOP)],) = args + except Exception: + (self[NameObject(TA.TOP)],) = (NullObject(),) elif typ in [TF.FIT_V, TF.FIT_BV]: - (self[NameObject(TA.LEFT)],) = args + try: # Prefered to be more robust not only to null parameters + (self[NameObject(TA.LEFT)],) = args + except Exception: + (self[NameObject(TA.LEFT)],) = (NullObject(),) elif typ in [TF.FIT, TF.FIT_B]: pass else: diff --git a/tests/test_merger.py b/tests/test_merger.py index 2a9d9e4e6..2af7a0251 100644 --- a/tests/test_merger.py +++ b/tests/test_merger.py @@ -288,3 +288,11 @@ def test_sweep_indirect_list_newobj_is_None(): # cleanup os.remove("tmp-merger-do-not-commit.pdf") + + +def test_iss1145(): + # issue with FitH destination with null param + url = "https://github.com/py-pdf/PyPDF2/files/9164743/file-0.pdf" + name = "iss1145.pdf" + merger = PdfMerger() + merger.append(PdfReader(BytesIO(get_pdf_from_url(url, name=name)))) From b429b395316021ff97ab41b9626287adb221f6fe Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 24 Jul 2022 07:21:45 +0200 Subject: [PATCH 043/130] DEV: Introduce _utils.logger_warning (#1148) - Exceptions: User code should handle the issue - warnings.warn: User should re-write something, e.g. deprecations - _utils.logger_warning: User might want to know in case of errors / post mortem analysis (or for developing PyPDF2 itself) --- PyPDF2/_utils.py | 20 ++++++++++++++++++++ PyPDF2/generic.py | 7 ++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/PyPDF2/_utils.py b/PyPDF2/_utils.py index e2521dc7e..aea283ed1 100644 --- a/PyPDF2/_utils.py +++ b/PyPDF2/_utils.py @@ -29,6 +29,7 @@ __author__ = "Mathieu Fenniak" __author_email__ = "biziqe@mathieu.fenniak.net" +import logging import warnings from codecs import getencoder from io import ( @@ -342,3 +343,22 @@ def deprecate_with_replacement( def deprecate_no_replacement(name: str, removed_in: str = "3.0.0") -> None: deprecate(DEPR_MSG_NO_REPLACEMENT.format(name, removed_in), 4) + + +def logger_warning(msg: str, src: str) -> None: + """ + Use this instead of logger.warning directly. + + That allows people to overwrite it more easily. + + ## Exception, warnings.warn, logger_warning + - Exceptions should be used if the user should write code that deals with + an error case, e.g. the PDF being completely broken. + - warnings.warn should be used if the user needs to fix their code, e.g. + DeprecationWarnings + - logger_warning should be used if the user needs to know that an issue was + handled by PyPDF2, e.g. a non-compliant PDF being read in a way that + PyPDF2 could apply a robustness fix to still read it. This applies mainly + to strict=False mode. + """ + logging.getLogger(src).warning(msg) diff --git a/PyPDF2/generic.py b/PyPDF2/generic.py index 5b84ec02d..652b6314a 100644 --- a/PyPDF2/generic.py +++ b/PyPDF2/generic.py @@ -63,6 +63,7 @@ deprecate_with_replacement, hex_str, hexencode, + logger_warning, read_non_whitespace, read_until_regex, skip_over_comment, @@ -331,7 +332,7 @@ def __new__( except decimal.InvalidOperation: # If this isn't a valid decimal (happens in malformed PDFs) # fallback to 0 - logger.warning(f"Invalid FloatObject {value}") + logger_warning(f"Invalid FloatObject {value}", __name__) return decimal.Decimal.__new__(cls, "0") def __repr__(self) -> str: @@ -511,7 +512,7 @@ def read_string_from_stream( tok = b"" else: msg = rf"Unexpected escaped string: {tok.decode('utf8')}" - logger.warning(msg) + logger_warning(msg, __name__) txt += tok return create_string_object(txt, forced_encoding) @@ -649,7 +650,7 @@ def read_from_stream(stream: StreamType, pdf: Any) -> "NameObject": # PdfReader # Name objects should represent irregular characters # with a '#' followed by the symbol's hex number if not pdf.strict: - warnings.warn("Illegal character in Name Object", PdfReadWarning) + logger_warning("Illegal character in Name Object", __name__) return NameObject(name) else: raise PdfReadError("Illegal character in Name Object") from e From c52988489fa5d1b83e327bbeba02a7eca2e211bb Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 24 Jul 2022 07:26:48 +0200 Subject: [PATCH 044/130] TST: Add workflow tests found by arc testing (#1154) Done with https://github.com/py-pdf/pdf-crawler/blob/main/get_coverage_by_pdf.py --- tests/test_workflows.py | 110 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 106 insertions(+), 4 deletions(-) diff --git a/tests/test_workflows.py b/tests/test_workflows.py index 6dfb0a680..3191928ea 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -297,7 +297,11 @@ def test_get_metadata(url, name): ( "https://corpora.tika.apache.org/base/docs/govdocs1/938/938702.pdf", "tika-938702.pdf", - ) + ), + ( + "https://corpora.tika.apache.org/base/docs/govdocs1/942/942358.pdf", + "tika-942358.pdf", + ), ], ) def test_extract_text(url, name): @@ -326,9 +330,16 @@ def test_compress(url, name): assert exc.value.args[0] == "Unexpected end of stream" -def test_get_fields(): - url = "https://corpora.tika.apache.org/base/docs/govdocs1/961/961883.pdf" - name = "tika-961883.pdf" +@pytest.mark.parametrize( + ("url", "name"), + [ + ( + "https://corpora.tika.apache.org/base/docs/govdocs1/961/961883.pdf", + "tika-961883.pdf", + ), + ], +) +def test_get_fields_warns(url, name): data = BytesIO(get_pdf_from_url(url, name=name)) reader = PdfReader(data) with open("tmp.txt", "w") as fp: @@ -341,6 +352,27 @@ def test_get_fields(): os.remove("tmp.txt") +@pytest.mark.parametrize( + ("url", "name"), + [ + ( + "https://corpora.tika.apache.org/base/docs/govdocs1/942/942050.pdf", + "tika-942050.pdf", + ), + ], +) +def test_get_fields_no_warning(url, name): + data = BytesIO(get_pdf_from_url(url, name=name)) + reader = PdfReader(data) + with open("tmp.txt", "w") as fp: + retrieved_fields = reader.get_fields(fileobj=fp) + + assert len(retrieved_fields) == 10 + + # Cleanup + os.remove("tmp.txt") + + def test_scale_rectangle_indirect_object(): url = "https://corpora.tika.apache.org/base/docs/govdocs1/999/999944.pdf" name = "tika-999944.pdf" @@ -409,6 +441,22 @@ def test_merge_output(): "https://corpora.tika.apache.org/base/docs/govdocs1/959/959184.pdf", "tika-959184.pdf", ), + ( + "https://corpora.tika.apache.org/base/docs/govdocs1/958/958496.pdf", + "tika-958496.pdf", + ), + ( + "https://corpora.tika.apache.org/base/docs/govdocs1/972/972174.pdf", + "tika-972174.pdf", + ), + ( + "https://corpora.tika.apache.org/base/docs/govdocs1/972/972243.pdf", + "tika-972243.pdf", + ), + ( + "https://corpora.tika.apache.org/base/docs/govdocs1/969/969502.pdf", + "tika-969502.pdf", + ), ], ) def test_image_extraction(url, name): @@ -478,3 +526,57 @@ def test_image_extraction2(url, name): for filepath in images_extracted: if os.path.exists(filepath): os.remove(filepath) + + +@pytest.mark.parametrize( + ("url", "name"), + [ + ( + "https://corpora.tika.apache.org/base/docs/govdocs1/918/918137.pdf", + "tika-918137.pdf", + ), + ], +) +def test_get_outline(url, name): + data = BytesIO(get_pdf_from_url(url, name=name)) + reader = PdfReader(data) + reader.outlines + + +@pytest.mark.parametrize( + ("url", "name"), + [ + ( + "https://corpora.tika.apache.org/base/docs/govdocs1/935/935981.pdf", + "tika-935981.pdf", + ), + ( + "https://corpora.tika.apache.org/base/docs/govdocs1/937/937334.pdf", + "tika-937334.pdf", + ), + ], +) +def test_get_xfa(url, name): + data = BytesIO(get_pdf_from_url(url, name=name)) + reader = PdfReader(data) + reader.xfa + + +@pytest.mark.parametrize( + ("url", "name"), + [ + ( + "https://corpora.tika.apache.org/base/docs/govdocs1/988/988698.pdf", + "tika-988698.pdf", + ), + ( + "https://corpora.tika.apache.org/base/docs/govdocs1/914/914133.pdf", + "tika-988698.pdf", + ), + ], +) +def test_get_fonts(url, name): + data = BytesIO(get_pdf_from_url(url, name=name)) + reader = PdfReader(data) + for page in reader.pages: + page._get_fonts() From 102260d8d5d21559371f7154ec647db5ce659dc2 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 24 Jul 2022 08:21:49 +0200 Subject: [PATCH 045/130] MAINT: Add diagnostic output to exception in read_from_stream (#1159) Co-authored-by: speedplane --- PyPDF2/generic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PyPDF2/generic.py b/PyPDF2/generic.py index 652b6314a..3aa28432a 100644 --- a/PyPDF2/generic.py +++ b/PyPDF2/generic.py @@ -862,7 +862,7 @@ def read_unsized_from_steam(stream: StreamType, pdf: Any) -> bytes: # PdfReader stream.seek(pos, 0) raise PdfReadError( "Unable to find 'endstream' marker after stream at byte " - f"{hex_str(stream.tell())}." + f"{hex_str(stream.tell())} (nd='{ndstream!r}', end='{end!r}')." ) else: stream.seek(pos, 0) From 2bf40f4a70a35434086eab2054d11425380b919c Mon Sep 17 00:00:00 2001 From: exiledkingcc Date: Sun, 24 Jul 2022 14:26:50 +0800 Subject: [PATCH 046/130] BUG: Ignore if '/Perms' verify failed (#1157) It seems to be save to ignore the /Perms entry: Qpdf ignores it: https://github.com/qpdf/qpdf/blob/main/libqpdf/QPDF_encryption.cc#L1064 pdfbox ignores it: https://github.com/apache/pdfbox/blob/dc1a75027d5bebf95a3330f6298a533e78e0b99e/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/encryption/StandardSecurityHandler.java#L311 Closes #378 --- PyPDF2/_encryption.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/PyPDF2/_encryption.py b/PyPDF2/_encryption.py index 4327baf63..a9fee04d8 100644 --- a/PyPDF2/_encryption.py +++ b/PyPDF2/_encryption.py @@ -32,6 +32,7 @@ from typing import Optional, Tuple, Union, cast from PyPDF2.errors import DependencyError +from PyPDF2._utils import logger_warning from PyPDF2.generic import ( ArrayObject, ByteStringObject, @@ -826,7 +827,7 @@ def verify_v5(self, password: bytes) -> Tuple[bytes, PasswordType]: P = (P + 0x100000000) % 0x100000000 # maybe < 0 metadata_encrypted = self.entry.get("/EncryptMetadata", True) if not AlgV5.verify_perms(key, perms, P, metadata_encrypted): - return b"", PasswordType.NOT_DECRYPTED + logger_warning("ignore '/Perms' verify failed", __name__) return key, rc @staticmethod From 35bec4034e503cac97c23de9f923154785d48767 Mon Sep 17 00:00:00 2001 From: pubpub-zz <4083478+pubpub-zz@users.noreply.github.com> Date: Sun, 24 Jul 2022 08:28:50 +0200 Subject: [PATCH 047/130] ROB: Cope with utf16 character for space calculation (#1155) See #1143 Co-authored-by: Martin Thoma --- PyPDF2/_cmap.py | 6 +++++- tests/test_workflows.py | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/PyPDF2/_cmap.py b/PyPDF2/_cmap.py index 36d12f476..595abce7f 100644 --- a/PyPDF2/_cmap.py +++ b/PyPDF2/_cmap.py @@ -42,7 +42,11 @@ def build_char_map( pass # I conside the space_code is available on one byte if isinstance(space_code, str): - sp = space_code.encode("charmap")[0] + try: # one byte + sp = space_code.encode("charmap")[0] + except Exception: + sp = space_code.encode("utf-16-be") + sp = sp[0] + 256 * sp[1] else: sp = space_code sp_width = compute_space_width(ft, sp, space_width) diff --git a/tests/test_workflows.py b/tests/test_workflows.py index 3191928ea..71e6a0240 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -149,6 +149,8 @@ def test_rotate_45(): (True, "https://arxiv.org/pdf/2201.00200.pdf", [0, 1, 5, 6]), (True, "https://arxiv.org/pdf/2201.00022.pdf", [0, 1, 5, 10]), (True, "https://arxiv.org/pdf/2201.00029.pdf", [0, 1, 6, 10]), + # #1145 + (True, "https://github.com/py-pdf/PyPDF2/files/9174594/2017.pdf", [0]), # 6 instead of 5: as there is an issue in page 5 (missing objects) # and too complex to handle the warning without hiding real regressions (True, "https://arxiv.org/pdf/1601.03642.pdf", [0, 1, 5, 7]), From fa5e3f76da2048b50c9d1dd94d7a938a11ac53e8 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 24 Jul 2022 10:01:55 +0200 Subject: [PATCH 048/130] BUG: Set /AS for /Btn form fields in writer (#1161) Closes #434 Co-authored-by: liuzhuoling --- PyPDF2/_writer.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/PyPDF2/_writer.py b/PyPDF2/_writer.py index 61026f318..937ea0062 100644 --- a/PyPDF2/_writer.py +++ b/PyPDF2/_writer.py @@ -610,6 +610,14 @@ def update_page_form_field_values( writer_parent_annot = writer_annot[PG.PARENT] for field in fields: if writer_annot.get(FieldDictionaryAttributes.T) == field: + if writer_annot.get(FieldDictionaryAttributes.FT) == "/Btn": + writer_annot.update( + { + NameObject( + AnnotationDictionaryAttributes.AS + ): NameObject(fields[field]) + } + ) writer_annot.update( { NameObject(FieldDictionaryAttributes.V): TextStringObject( From 2de09730c13e3e380f0038d241e5ee81b0509a40 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 24 Jul 2022 11:19:38 +0200 Subject: [PATCH 049/130] MAINT: Break up parse_to_unicode (#1162) Just move parts in separate functions for easier readability --- PyPDF2/_cmap.py | 171 +++++++++++++++++++++++++++--------------------- 1 file changed, 98 insertions(+), 73 deletions(-) diff --git a/PyPDF2/_cmap.py b/PyPDF2/_cmap.py index 595abce7f..5fa1d3815 100644 --- a/PyPDF2/_cmap.py +++ b/PyPDF2/_cmap.py @@ -167,19 +167,31 @@ def parse_encoding( def parse_to_unicode( ft: DictionaryObject, space_code: int -) -> Tuple[Dict, int, List[int]]: - map_dict: Dict[ - Any, Any - ] = ( - {} - ) # will store all translation code and map_dict[-1] we will have the number of bytes to convert - int_entry: List[ - int - ] = [] # will provide the list of cmap keys as int to correct encoding +) -> Tuple[Dict[Any, Any], int, List[int]]: + # will store all translation code + # and map_dict[-1] we will have the number of bytes to convert + map_dict: Dict[Any, Any] = {} + + # will provide the list of cmap keys as int to correct encoding + int_entry: List[int] = [] + if "/ToUnicode" not in ft: return {}, space_code, [] process_rg: bool = False process_char: bool = False + cm = prepare_cm(ft) + for l in cm.split(b"\n"): + process_rg, process_char = process_cm_line( + l, process_rg, process_char, map_dict, int_entry + ) + + for a, value in map_dict.items(): + if value == " ": + space_code = a + return map_dict, space_code, int_entry + + +def prepare_cm(ft: DictionaryObject) -> bytes: cm: bytes = cast(DecodedStreamObject, ft["/ToUnicode"]).get_data() # we need to prepare cm before due to missing return line in pdf printed to pdf from word cm = ( @@ -208,71 +220,84 @@ def parse_to_unicode( .replace(b"]", b" ]\n ") .replace(b"\r", b"\n") ) + return cm - for l in cm.split(b"\n"): - if l in (b"", b" ") or l[0] == 37: # 37 = % - continue - if b"beginbfrange" in l: - process_rg = True - elif b"endbfrange" in l: - process_rg = False - elif b"beginbfchar" in l: - process_char = True - elif b"endbfchar" in l: - process_char = False - elif process_rg: - lst = [x for x in l.split(b" ") if x] - a = int(lst[0], 16) - b = int(lst[1], 16) - nbi = len(lst[0]) - map_dict[-1] = nbi // 2 - fmt = b"%%0%dX" % nbi - if lst[2] == b"[": - for sq in lst[3:]: - if sq == b"]": - break - map_dict[ - unhexlify(fmt % a).decode( - "charmap" if map_dict[-1] == 1 else "utf-16-be", - "surrogatepass", - ) - ] = unhexlify(sq).decode("utf-16-be", "surrogatepass") - int_entry.append(a) - a += 1 - else: - c = int(lst[2], 16) - fmt2 = b"%%0%dX" % max(4, len(lst[2])) - while a <= b: - map_dict[ - unhexlify(fmt % a).decode( - "charmap" if map_dict[-1] == 1 else "utf-16-be", - "surrogatepass", - ) - ] = unhexlify(fmt2 % c).decode("utf-16-be", "surrogatepass") - int_entry.append(a) - a += 1 - c += 1 - elif process_char: - lst = [x for x in l.split(b" ") if x] - map_dict[-1] = len(lst[0]) // 2 - while len(lst) > 1: - map_to = "" - # placeholder (see above) means empty string - if lst[1] != b".": - map_to = unhexlify(lst[1]).decode( - "utf-16-be", "surrogatepass" - ) # join is here as some cases where the code was split - map_dict[ - unhexlify(lst[0]).decode( - "charmap" if map_dict[-1] == 1 else "utf-16-be", "surrogatepass" - ) - ] = map_to - int_entry.append(int(lst[0], 16)) - lst = lst[2:] - for a, value in map_dict.items(): - if value == " ": - space_code = a - return map_dict, space_code, int_entry + +def process_cm_line( + l: bytes, + process_rg: bool, + process_char: bool, + map_dict: Dict[Any, Any], + int_entry: List[int], +) -> Tuple[bool, bool]: + if l in (b"", b" ") or l[0] == 37: # 37 = % + return process_rg, process_char + if b"beginbfrange" in l: + process_rg = True + elif b"endbfrange" in l: + process_rg = False + elif b"beginbfchar" in l: + process_char = True + elif b"endbfchar" in l: + process_char = False + elif process_rg: + parse_bfrange(l, map_dict, int_entry) + elif process_char: + parse_bfchar(l, map_dict, int_entry) + return process_rg, process_char + + +def parse_bfrange(l: bytes, map_dict: Dict[Any, Any], int_entry: List[int]) -> None: + lst = [x for x in l.split(b" ") if x] + a = int(lst[0], 16) + b = int(lst[1], 16) + nbi = len(lst[0]) + map_dict[-1] = nbi // 2 + fmt = b"%%0%dX" % nbi + if lst[2] == b"[": + for sq in lst[3:]: + if sq == b"]": + break + map_dict[ + unhexlify(fmt % a).decode( + "charmap" if map_dict[-1] == 1 else "utf-16-be", + "surrogatepass", + ) + ] = unhexlify(sq).decode("utf-16-be", "surrogatepass") + int_entry.append(a) + a += 1 + else: + c = int(lst[2], 16) + fmt2 = b"%%0%dX" % max(4, len(lst[2])) + while a <= b: + map_dict[ + unhexlify(fmt % a).decode( + "charmap" if map_dict[-1] == 1 else "utf-16-be", + "surrogatepass", + ) + ] = unhexlify(fmt2 % c).decode("utf-16-be", "surrogatepass") + int_entry.append(a) + a += 1 + c += 1 + + +def parse_bfchar(l: bytes, map_dict: Dict[Any, Any], int_entry: List[int]) -> None: + lst = [x for x in l.split(b" ") if x] + map_dict[-1] = len(lst[0]) // 2 + while len(lst) > 1: + map_to = "" + # placeholder (see above) means empty string + if lst[1] != b".": + map_to = unhexlify(lst[1]).decode( + "utf-16-be", "surrogatepass" + ) # join is here as some cases where the code was split + map_dict[ + unhexlify(lst[0]).decode( + "charmap" if map_dict[-1] == 1 else "utf-16-be", "surrogatepass" + ) + ] = map_to + int_entry.append(int(lst[0], 16)) + lst = lst[2:] def compute_space_width( From ec30171a9da60755763ed8b2c24c96298f9ee902 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 24 Jul 2022 11:23:39 +0200 Subject: [PATCH 050/130] REL: 2.8.0 New Features (ENH): - Add writer.add_annotation, page.annotations, and generic.AnnotationBuilder (#1120) Bug Fixes (BUG): - Set /AS for /Btn form fields in writer (#1161) - Ignore if /Perms verify failed (#1157) Robustness (ROB): - Cope with utf16 character for space calculation (#1155) - Cope with null params for FitH / FitV destination (#1152) - Handle outlines without valid destination (#1076) Developer Experience (DEV): - Introduce _utils.logger_warning (#1148) Maintenance (MAINT): - Break up parse_to_unicode (#1162) - Add diagnostic output to exception in read_from_stream (#1159) - Reduce PdfReader.read complexity (#1151) Testing (TST): - Add workflow tests found by arc testing (#1154) - Decrypt file which is not encrypted (#1149) - Test CryptRC4 encryption class; test image extraction filters (#1147) Full Changelog: https://github.com/py-pdf/PyPDF2/compare/2.7.0...2.8.0 --- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ PyPDF2/_version.py | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44f21d2cc..8ceea709c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,34 @@ # CHANGELOG +## Version 2.8.0, 2022-07-24 + +### New Features (ENH) +- Add writer.add_annotation, page.annotations, and generic.AnnotationBuilder (#1120) + +### Bug Fixes (BUG) +- Set /AS for /Btn form fields in writer (#1161) +- Ignore if /Perms verify failed (#1157) + +### Robustness (ROB) +- Cope with utf16 character for space calculation (#1155) +- Cope with null params for FitH / FitV destination (#1152) +- Handle outlines without valid destination (#1076) + +### Developer Experience (DEV) +- Introduce _utils.logger_warning (#1148) + +### Maintenance (MAINT) +- Break up parse_to_unicode (#1162) +- Add diagnostic output to exception in read_from_stream (#1159) +- Reduce PdfReader.read complexity (#1151) + +### Testing (TST) +- Add workflow tests found by arc testing (#1154) +- Decrypt file which is not encrypted (#1149) +- Test CryptRC4 encryption class; test image extraction filters (#1147) + +Full Changelog: https://github.com/py-pdf/PyPDF2/compare/2.7.0...2.8.0 + ## Version 2.7.0, 2022-07-21 ### New Features (ENH) diff --git a/PyPDF2/_version.py b/PyPDF2/_version.py index 2614ce9d9..892994aa6 100644 --- a/PyPDF2/_version.py +++ b/PyPDF2/_version.py @@ -1 +1 @@ -__version__ = "2.7.0" +__version__ = "2.8.0" From db3439b3a603bd370c97994df24d8ecf8711faf6 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 24 Jul 2022 12:53:27 +0200 Subject: [PATCH 051/130] MAINT: Package updates; solve mypy strict remarks (#1163) --- .pre-commit-config.yaml | 4 ++-- Makefile | 3 +++ PyPDF2/_cmap.py | 12 ++++++------ PyPDF2/_encryption.py | 6 +++--- PyPDF2/_merger.py | 2 +- PyPDF2/_page.py | 8 ++++---- PyPDF2/_reader.py | 8 +++++--- PyPDF2/_utils.py | 4 ++-- PyPDF2/_writer.py | 4 ++-- PyPDF2/generic.py | 4 ++-- PyPDF2/types.py | 2 +- PyPDF2/xmp.py | 3 ++- requirements/ci.txt | 8 ++++---- requirements/dev.txt | 12 ++++++------ requirements/docs.txt | 8 ++++---- 15 files changed, 47 insertions(+), 41 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 891d4bcd2..e9ee5a062 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: hooks: - id: isort - repo: https://github.com/psf/black - rev: 22.3.0 + rev: 22.6.0 hooks: - id: black args: [--target-version, py36] @@ -40,7 +40,7 @@ repos: - id: blacken-docs additional_dependencies: [black==22.1.0] - repo: https://github.com/asottile/pyupgrade - rev: v2.34.0 + rev: v2.37.2 hooks: - id: pyupgrade args: [--py36-plus] diff --git a/Makefile b/Makefile index caa6c463d..9bbcf6161 100644 --- a/Makefile +++ b/Makefile @@ -31,3 +31,6 @@ mutation-results: benchmark: pytest tests/bench.py + +mypy: + mypy PyPDF2 --ignore-missing-imports --check-untyped --strict diff --git a/PyPDF2/_cmap.py b/PyPDF2/_cmap.py index 5fa1d3815..6b668d2e8 100644 --- a/PyPDF2/_cmap.py +++ b/PyPDF2/_cmap.py @@ -56,12 +56,12 @@ def build_char_map( float(sp_width / 2), encoding, # https://github.com/python/mypy/issues/4374 - map_dict, # type: ignore - ) # type: ignore + map_dict, + ) # used when missing data, e.g. font def missing -unknown_char_map: Tuple[str, float, Union[str, Dict[int, str]], Dict] = ( +unknown_char_map: Tuple[str, float, Union[str, Dict[int, str]], Dict[Any, Any]] = ( "Unknown", 9999, dict(zip(range(256), ["�"] * 256)), @@ -108,7 +108,7 @@ def parse_encoding( encoding: Union[str, List[str], Dict[int, str]] = [] if "/Encoding" not in ft: try: - if "/BaseFont" in ft and ft["/BaseFont"] in charset_encoding: + if "/BaseFont" in ft and cast(str, ft["/BaseFont"]) in charset_encoding: encoding = dict( zip(range(256), charset_encoding[cast(str, ft["/BaseFont"])]) ) @@ -116,7 +116,7 @@ def parse_encoding( encoding = "charmap" return encoding, _default_fonts_space_width[cast(str, ft["/BaseFont"])] except Exception: - if ft["/Subtype"] == "/Type1": + if cast(str, ft["/Subtype"]) == "/Type1": return "charmap", space_code else: return "", space_code @@ -314,7 +314,7 @@ def compute_space_width( except Exception: w1[-1] = 1000.0 if "/W" in ft1: - w = list(ft1["/W"]) # type: ignore + w = list(ft1["/W"]) else: w = [] while len(w) > 0: diff --git a/PyPDF2/_encryption.py b/PyPDF2/_encryption.py index a9fee04d8..15e8ee5b2 100644 --- a/PyPDF2/_encryption.py +++ b/PyPDF2/_encryption.py @@ -29,10 +29,10 @@ import random import struct from enum import IntEnum -from typing import Optional, Tuple, Union, cast +from typing import Any, Dict, Optional, Tuple, Union, cast -from PyPDF2.errors import DependencyError from PyPDF2._utils import logger_warning +from PyPDF2.errors import DependencyError from PyPDF2.generic import ( ArrayObject, ByteStringObject, @@ -566,7 +566,7 @@ def verify_perms( @staticmethod def generate_values( user_pwd: bytes, owner_pwd: bytes, key: bytes, p: int, metadata_encrypted: bool - ) -> dict: + ) -> Dict[Any, Any]: u_value, ue_value = AlgV5.compute_U_value(user_pwd, key) o_value, oe_value = AlgV5.compute_O_value(owner_pwd, key, u_value) perms = AlgV5.compute_Perms_value(key, p, metadata_encrypted) diff --git a/PyPDF2/_merger.py b/PyPDF2/_merger.py index 69fbce90d..61e7b450b 100644 --- a/PyPDF2/_merger.py +++ b/PyPDF2/_merger.py @@ -556,7 +556,7 @@ def find_bookmark( res = self.find_bookmark(bookmark, b) # type: ignore if res: return [i] + res - elif b == bookmark or b["/Title"] == bookmark: + elif b == bookmark or cast(Dict[Any, Any], b["/Title"]) == bookmark: # we found a leaf node return [i] diff --git a/PyPDF2/_page.py b/PyPDF2/_page.py index 935afbd81..799ba2949 100644 --- a/PyPDF2/_page.py +++ b/PyPDF2/_page.py @@ -506,9 +506,9 @@ def _merge_page( # Combine /ProcSet sets. new_resources[NameObject(RES.PROC_SET)] = ArrayObject( frozenset( - original_resources.get(RES.PROC_SET, ArrayObject()).get_object() # type: ignore + original_resources.get(RES.PROC_SET, ArrayObject()).get_object() ).union( - frozenset(page2resources.get(RES.PROC_SET, ArrayObject()).get_object()) # type: ignore + frozenset(page2resources.get(RES.PROC_SET, ArrayObject()).get_object()) ) ) @@ -1248,7 +1248,7 @@ def process_operation(operator: bytes, operands: List) -> None: cmaps[operands[0]][2], cmaps[operands[0]][3], operands[0], - ) # type:ignore + ) except KeyError: # font not found _space_width = unknown_char_map[1] cmap = ( @@ -1395,7 +1395,7 @@ def process_operation(operator: bytes, operands: List) -> None: except IndexError: pass try: - xobj = resources_dict["/XObject"] # type: ignore + xobj = resources_dict["/XObject"] if xobj[operands[0]]["/Subtype"] != "/Image": # type: ignore # output += text text = self.extract_xform_text(xobj[operands[0]], space_width) # type: ignore diff --git a/PyPDF2/_reader.py b/PyPDF2/_reader.py index e9817dfaf..53b9e4392 100644 --- a/PyPDF2/_reader.py +++ b/PyPDF2/_reader.py @@ -861,7 +861,9 @@ def _build_outline(self, node: DictionaryObject) -> Optional[Destination]: elif isinstance(dest, str): # named destination, addresses NameObject Issue #193 try: - outline = self._build_destination(title, self._namedDests[dest].dest_array) # type: ignore + outline = self._build_destination( + title, self._namedDests[dest].dest_array + ) except KeyError: # named destination not found in Name Dict outline = self._build_destination(title, None) @@ -1045,7 +1047,7 @@ def _get_object_from_stream( stmnum, idx = self.xref_objStm[indirect_reference.idnum] obj_stm: EncodedStreamObject = IndirectObject(stmnum, 0, self).get_object() # type: ignore # This is an xref to a stream, so its type better be a stream - assert obj_stm["/Type"] == "/ObjStm" + assert cast(str, obj_stm["/Type"]) == "/ObjStm" # /N is the number of indirect objects in the stream assert idx < obj_stm["/N"] stream_data = BytesIO(b_(obj_stm.get_data())) # type: ignore @@ -1501,7 +1503,7 @@ def _read_pdf15_xref_stream( stream.seek(-1, 1) idnum, generation = self.read_object_header(stream) xrefstream = cast(ContentStream, read_object(stream, self)) - assert xrefstream["/Type"] == "/XRef" + assert cast(str, xrefstream["/Type"]) == "/XRef" self.cache_indirect_object(generation, idnum, xrefstream) stream_data = BytesIO(b_(xrefstream.get_data())) # Index pairs specify the subsections in the dictionary. If diff --git a/PyPDF2/_utils.py b/PyPDF2/_utils.py index aea283ed1..78dc0f9f0 100644 --- a/PyPDF2/_utils.py +++ b/PyPDF2/_utils.py @@ -46,7 +46,7 @@ # Python 3.10+: https://www.python.org/dev/peps/pep-0484/ from typing import TypeAlias # type: ignore[attr-defined] except ImportError: - from typing_extensions import TypeAlias # type: ignore[misc] + from typing_extensions import TypeAlias from .errors import STREAM_TRUNCATED_PREMATURELY, PdfStreamError @@ -130,7 +130,7 @@ def skip_over_comment(stream: StreamType) -> None: def read_until_regex( - stream: StreamType, regex: Pattern, ignore_eof: bool = False + stream: StreamType, regex: Pattern[bytes], ignore_eof: bool = False ) -> bytes: """ Read until the regular expression pattern matched (ignore the match). diff --git a/PyPDF2/_writer.py b/PyPDF2/_writer.py index 937ea0062..a3b8ed645 100644 --- a/PyPDF2/_writer.py +++ b/PyPDF2/_writer.py @@ -195,7 +195,7 @@ def getObject(self, ido: IndirectObject) -> PdfObject: # pragma: no cover def _add_page( self, page: PageObject, action: Callable[[Any, IndirectObject], None] ) -> None: - assert page[PA.TYPE] == CO.PAGE + assert cast(str, page[PA.TYPE]) == CO.PAGE if page.pdf is not None: other = page.pdf.pdf_header if isinstance(other, str): @@ -292,7 +292,7 @@ def get_page( raise ValueError("Please specify the page_number") pages = cast(Dict[str, Any], self.get_object(self._pages)) # TODO: crude hack - return pages[PA.KIDS][page_number].get_object() + return cast(PageObject, pages[PA.KIDS][page_number].get_object()) def getPage(self, pageNumber: int) -> PageObject: # pragma: no cover """ diff --git a/PyPDF2/generic.py b/PyPDF2/generic.py index 3aa28432a..350bf8c16 100644 --- a/PyPDF2/generic.py +++ b/PyPDF2/generic.py @@ -192,7 +192,7 @@ def readFromStream(stream: StreamType) -> "BooleanObject": # pragma: no cover class ArrayObject(list, PdfObject): - def items(self) -> Iterable: + def items(self) -> Iterable[Any]: """ Emulate DictionaryObject.items for a list (index, object) @@ -517,7 +517,7 @@ def read_string_from_stream( return create_string_object(txt, forced_encoding) -class ByteStringObject(bytes, PdfObject): # type: ignore +class ByteStringObject(bytes, PdfObject): """ Represents a string object where the text encoding could not be determined. This occurs quite often, as the PDF spec doesn't provide an alternate way to diff --git a/PyPDF2/types.py b/PyPDF2/types.py index a55465745..cb5358647 100644 --- a/PyPDF2/types.py +++ b/PyPDF2/types.py @@ -12,7 +12,7 @@ # Python 3.10+: https://www.python.org/dev/peps/pep-0484/ from typing import TypeAlias # type: ignore[attr-defined] except ImportError: - from typing_extensions import TypeAlias # type: ignore[misc] + from typing_extensions import TypeAlias from .generic import ( ArrayObject, diff --git a/PyPDF2/xmp.py b/PyPDF2/xmp.py index 5cb79c750..ff9679fab 100644 --- a/PyPDF2/xmp.py +++ b/PyPDF2/xmp.py @@ -16,6 +16,7 @@ Optional, TypeVar, Union, + cast, ) from xml.dom.minidom import Document from xml.dom.minidom import Element as XmlElement @@ -482,7 +483,7 @@ def xmpmm_documentId(self, value: str) -> None: # pragma: no cover @property def xmpmm_instanceId(self) -> str: # pragma: no cover deprecate_with_replacement("xmpmm_instanceId", "xmpmm_instance_id") - return self.xmpmm_instance_id + return cast(str, self.xmpmm_instance_id) @xmpmm_instanceId.setter def xmpmm_instanceId(self, value: str) -> None: # pragma: no cover diff --git a/requirements/ci.txt b/requirements/ci.txt index 7e58996a6..86a7f5154 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -15,7 +15,7 @@ flake8==4.0.1 # via # -r requirements/ci.in # flake8-bugbear -flake8-bugbear==22.4.25 +flake8-bugbear==22.7.1 # via -r requirements/ci.in flake8-implicit-str-concat==0.2.0 # via -r requirements/ci.in @@ -30,7 +30,7 @@ mccabe==0.6.1 # via flake8 more-itertools==8.13.0 # via flake8-implicit-str-concat -mypy==0.961 +mypy==0.971 # via -r requirements/ci.in mypy-extensions==0.4.3 # via mypy @@ -46,7 +46,7 @@ py-cpuinfo==8.0.0 # via pytest-benchmark pycodestyle==2.8.0 # via flake8 -pycryptodome==3.14.1 +pycryptodome==3.15.0 # via -r requirements/ci.in pyflakes==2.4.0 # via flake8 @@ -66,7 +66,7 @@ typed-ast==1.5.4 # via mypy typeguard==2.13.3 # via -r requirements/ci.in -types-pillow==9.0.20 +types-pillow==9.2.0 # via -r requirements/ci.in typing-extensions==4.1.1 # via diff --git a/requirements/dev.txt b/requirements/dev.txt index 619879c41..1c20dd40f 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -6,13 +6,13 @@ # attrs==21.4.0 # via pytest -black==22.3.0 +black==22.6.0 # via -r requirements/dev.in bleach==4.1.0 # via readme-renderer certifi==2022.6.15 # via requests -cffi==1.15.0 +cffi==1.15.1 # via cryptography cfgv==3.3.1 # via pre-commit @@ -26,11 +26,11 @@ colorama==0.4.5 # via twine coverage[toml]==6.2 # via pytest-cov -cryptography==37.0.2 +cryptography==37.0.4 # via secretstorage dataclasses==0.8 # via black -distlib==0.3.4 +distlib==0.3.5 # via virtualenv docutils==0.18.1 # via readme-renderer @@ -135,11 +135,11 @@ typing-extensions==4.1.1 # via # black # importlib-metadata -urllib3==1.26.9 +urllib3==1.26.10 # via # requests # twine -virtualenv==20.14.1 +virtualenv==20.15.1 # via pre-commit webencodings==0.5.1 # via bleach diff --git a/requirements/docs.txt b/requirements/docs.txt index afe10d6a3..6516276b0 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -8,9 +8,9 @@ alabaster==0.7.12 # via sphinx attrs==21.4.0 # via markdown-it-py -babel==2.10.1 +babel==2.10.3 # via sphinx -certifi==2022.5.18.1 +certifi==2022.6.15 # via requests charset-normalizer==2.0.12 # via requests @@ -21,7 +21,7 @@ docutils==0.17.1 # sphinx-rtd-theme idna==3.3 # via requests -imagesize==1.3.0 +imagesize==1.4.1 # via sphinx importlib-metadata==4.8.3 # via sphinx @@ -78,7 +78,7 @@ typing-extensions==4.1.1 # via # importlib-metadata # markdown-it-py -urllib3==1.26.9 +urllib3==1.26.10 # via requests zipp==3.6.0 # via importlib-metadata From ab7a9ada067d88f03b226072c6fce6b55f89a6b5 Mon Sep 17 00:00:00 2001 From: pubpub-zz <4083478+pubpub-zz@users.noreply.github.com> Date: Sun, 24 Jul 2022 18:19:49 +0200 Subject: [PATCH 052/130] DOC: Typo in warning message (#1166) --- PyPDF2/_merger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PyPDF2/_merger.py b/PyPDF2/_merger.py index 61e7b450b..6db5df4d7 100644 --- a/PyPDF2/_merger.py +++ b/PyPDF2/_merger.py @@ -640,7 +640,7 @@ def add_named_destination(self, title: str, pagenum: int) -> None: class PdfFileMerger(PdfMerger): # pragma: no cover def __init__(self, *args: Any, **kwargs: Any) -> None: - deprecate_with_replacement("PdfFileMerger", "PdfMerge") + deprecate_with_replacement("PdfFileMerger", "PdfMerger") if "strict" not in kwargs and len(args) < 1: kwargs["strict"] = True # maintain the default From 0b2728737809fa6fe253b2a4505e86cd093d7006 Mon Sep 17 00:00:00 2001 From: pubpub-zz <4083478+pubpub-zz@users.noreply.github.com> Date: Sun, 24 Jul 2022 19:40:00 +0200 Subject: [PATCH 053/130] ROB: Cope with empty DecodeParams (#1165) See #1143, 2nd part --- PyPDF2/filters.py | 2 +- tests/test_workflows.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/PyPDF2/filters.py b/PyPDF2/filters.py index f23602db2..d27d09ac9 100644 --- a/PyPDF2/filters.py +++ b/PyPDF2/filters.py @@ -104,7 +104,7 @@ def decode( predictor = decode_parm["/Predictor"] else: predictor = decode_parms.get("/Predictor", 1) - except AttributeError: + except (AttributeError, TypeError): # Type Error is NullObject pass # Usually an array with a null object was read # predictor 1 == no predictor if predictor != 1: diff --git a/tests/test_workflows.py b/tests/test_workflows.py index 71e6a0240..9a9571dd7 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -151,6 +151,12 @@ def test_rotate_45(): (True, "https://arxiv.org/pdf/2201.00029.pdf", [0, 1, 6, 10]), # #1145 (True, "https://github.com/py-pdf/PyPDF2/files/9174594/2017.pdf", [0]), + # #1145, remaining issue (empty arguments for FlateEncoding) + ( + True, + "https://github.com/py-pdf/PyPDF2/files/9175966/2015._pb_decode_pg0.pdf", + [0], + ), # 6 instead of 5: as there is an issue in page 5 (missing objects) # and too complex to handle the warning without hiding real regressions (True, "https://arxiv.org/pdf/1601.03642.pdf", [0, 1, 5, 7]), From ebcf88940e876a4661e7cedcde247d908a4dcd5e Mon Sep 17 00:00:00 2001 From: pubpub-zz <4083478+pubpub-zz@users.noreply.github.com> Date: Mon, 25 Jul 2022 20:31:28 +0200 Subject: [PATCH 054/130] TST: Add test from #325 (#1169) Closes #325 --- tests/test_reader.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_reader.py b/tests/test_reader.py index 97365da49..072ab706a 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -1002,3 +1002,13 @@ def test_outlines_with_invalid_destinations(): ) # contains 9 outlines, 6 with invalid destinations caused by different malformations assert len(reader.outlines) == 9 + + +def test_PdfReaderMultipleDefinitions(): + # iss325 + url = "https://github.com/py-pdf/PyPDF2/files/9176644/multipledefs.pdf" + name = "multipledefs.pdf" + reader = PdfReader(BytesIO(get_pdf_from_url(url, name=name))) + with pytest.warns(PdfReadWarning) as w: + reader.pages[0].extract_text() + assert len(w) == 1 From 844f2380d68ef047c2f9403699a933875633af11 Mon Sep 17 00:00:00 2001 From: pubpub-zz <4083478+pubpub-zz@users.noreply.github.com> Date: Mon, 25 Jul 2022 20:35:59 +0200 Subject: [PATCH 055/130] ROB: Fix loading of file from #134 (#1167) See #134 a) cmap : strip lines when processing cmap from fonts b) look for %EOF up to beginning of file --- PyPDF2/_cmap.py | 2 +- PyPDF2/_reader.py | 2 +- tests/test_reader.py | 10 ++++------ 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/PyPDF2/_cmap.py b/PyPDF2/_cmap.py index 6b668d2e8..c3acb6564 100644 --- a/PyPDF2/_cmap.py +++ b/PyPDF2/_cmap.py @@ -182,7 +182,7 @@ def parse_to_unicode( cm = prepare_cm(ft) for l in cm.split(b"\n"): process_rg, process_char = process_cm_line( - l, process_rg, process_char, map_dict, int_entry + l.strip(b" "), process_rg, process_char, map_dict, int_entry ) for a, value in map_dict.items(): diff --git a/PyPDF2/_reader.py b/PyPDF2/_reader.py index 53b9e4392..1664bfe9d 100644 --- a/PyPDF2/_reader.py +++ b/PyPDF2/_reader.py @@ -1303,7 +1303,7 @@ def _basic_validation(self, stream: StreamType) -> None: stream.seek(0, os.SEEK_END) def _find_eof_marker(self, stream: StreamType) -> None: - last_mb = stream.tell() - 1024 * 1024 + 1 # offset of last MB of stream + last_mb = 8 # to parse whole file line = b"" while line[:5] != b"%%EOF": if stream.tell() < last_mb: diff --git a/tests/test_reader.py b/tests/test_reader.py index 072ab706a..a4503b2a2 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -11,11 +11,7 @@ from PyPDF2.constants import ImageAttributes as IA from PyPDF2.constants import PageAttributes as PG from PyPDF2.constants import Ressources as RES -from PyPDF2.errors import ( - STREAM_TRUNCATED_PREMATURELY, - PdfReadError, - PdfReadWarning, -) +from PyPDF2.errors import PdfReadError, PdfReadWarning from PyPDF2.filters import _xobj_to_image from PyPDF2.generic import Destination @@ -396,7 +392,9 @@ def test_read_malformed_header(): def test_read_malformed_body(): with pytest.raises(PdfReadError) as exc: PdfReader(io.BytesIO(b"%PDF-"), strict=True) - assert exc.value.args[0] == STREAM_TRUNCATED_PREMATURELY + assert ( + exc.value.args[0] == "EOF marker not found" + ) # used to be:STREAM_TRUNCATED_PREMATURELY def test_read_prev_0_trailer(): From 3b73b34b1014a1ccf2c8ab21153b41071fa52ef0 Mon Sep 17 00:00:00 2001 From: exiledkingcc Date: Tue, 26 Jul 2022 04:38:14 +0800 Subject: [PATCH 056/130] BUG: u_hash in AlgV4.compute_key (#1170) Closes #1088 --- PyPDF2/_encryption.py | 6 ++++-- tests/test_page.py | 1 - 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/PyPDF2/_encryption.py b/PyPDF2/_encryption.py index 15e8ee5b2..81d77f7ab 100644 --- a/PyPDF2/_encryption.py +++ b/PyPDF2/_encryption.py @@ -288,7 +288,7 @@ def compute_key( u_hash.update(o_entry) u_hash.update(struct.pack("= 3 and not metadata_encrypted: + if rev >= 4 and metadata_encrypted is False: u_hash.update(b"\xff\xff\xff\xff") u_hash_digest = u_hash.digest() length = key_size // 8 @@ -772,7 +772,9 @@ def verify_v4(self, password: bytes) -> Tuple[bytes, PasswordType]: R = cast(int, self.entry["/R"]) P = cast(int, self.entry["/P"]) P = (P + 0x100000000) % 0x100000000 # maybe < 0 - metadata_encrypted = self.entry.get("/EncryptMetadata", True) + # make type(metadata_encrypted) == bool + em = self.entry.get("/EncryptMetadata") + metadata_encrypted = em.value if em else True o_entry = cast(ByteStringObject, self.entry["/O"].get_object()).original_bytes u_entry = cast(ByteStringObject, self.entry["/U"].get_object()).original_bytes diff --git a/tests/test_page.py b/tests/test_page.py index 9959a144e..41d4a1152 100644 --- a/tests/test_page.py +++ b/tests/test_page.py @@ -449,7 +449,6 @@ def test_text_extraction_issue_1091(): page.extract_text() -@pytest.mark.xfail(reason="#1088") def test_empyt_password_1088(): url = "https://corpora.tika.apache.org/base/docs/govdocs1/941/941536.pdf" name = "tika-941536.pdf" From 5b75160144a45eb75441158046edc3c5805b0749 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Mon, 25 Jul 2022 22:41:21 +0200 Subject: [PATCH 057/130] REL: 2.8.1 Bug Fixes (BUG): - u_hash in AlgV4.compute_key (#1170) Robustness (ROB): - Fix loading of file from #134 (#1167) - Cope with empty DecodeParams (#1165) Documentation (DOC): - Typo in warning message (#1166) Maintenance (MAINT): - Package updates; solve mypy strict remarks (#1163) Testing (TST): - Add test from #325 (#1169) Full Changelog: https://github.com/py-pdf/PyPDF2/compare/2.8.0...2.8.1 --- CHANGELOG.md | 21 +++++++++++++++++++++ PyPDF2/_version.py | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ceea709c..8d0609eb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # CHANGELOG +## Version 2.8.1, 2022-07-25 + +### Bug Fixes (BUG) +- u_hash in AlgV4.compute_key (#1170) + +### Robustness (ROB) +- Fix loading of file from #134 (#1167) +- Cope with empty DecodeParams (#1165) + +### Documentation (DOC) +- Typo in merger deprecation warning message (#1166) + +### Maintenance (MAINT) +- Package updates; solve mypy strict remarks (#1163) + +### Testing (TST) +- Add test from #325 (#1169) + +Full Changelog: https://github.com/py-pdf/PyPDF2/compare/2.8.0...2.8.1 + + ## Version 2.8.0, 2022-07-24 ### New Features (ENH) diff --git a/PyPDF2/_version.py b/PyPDF2/_version.py index 892994aa6..b4066b65a 100644 --- a/PyPDF2/_version.py +++ b/PyPDF2/_version.py @@ -1 +1 @@ -__version__ = "2.8.0" +__version__ = "2.8.1" From d8bd12f3e1d6b5a5b0a488413dbe8ec598b84355 Mon Sep 17 00:00:00 2001 From: pubpub-zz <4083478+pubpub-zz@users.noreply.github.com> Date: Wed, 27 Jul 2022 19:18:30 +0200 Subject: [PATCH 058/130] BUG: Incomplete Graphic State save/restore (#1172) Graphic state shall store also the font, font size, ... See #1142 --- PyPDF2/_page.py | 30 +++++++++++++++++++++++++----- tests/test_page.py | 9 +++++++++ 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/PyPDF2/_page.py b/PyPDF2/_page.py index 799ba2949..ff8ab4b1c 100644 --- a/PyPDF2/_page.py +++ b/PyPDF2/_page.py @@ -1133,9 +1133,11 @@ def _extract_text( if "/Font" in resources_dict: for f in cast(DictionaryObject, resources_dict["/Font"]): cmaps[f] = build_char_map(f, space_width, obj) - cmap: Tuple[ - Union[str, Dict[int, str]], Dict[str, str], str - ] # (encoding,CMAP,font_name) + cmap: Tuple[Union[str, Dict[int, str]], Dict[str, str], str] = ( + "charmap", + {}, + "NotInitialized", + ) # (encoding,CMAP,font_name) try: content = ( obj[content_key].get_object() if isinstance(content_key, str) else obj @@ -1211,10 +1213,28 @@ def process_operation(operator: bytes, operands: List) -> None: # table 4.7, page 219 # cm_matrix calculation is a reserved for the moment elif operator == b"q": - cm_stack.append(cm_matrix) + cm_stack.append( + ( + cm_matrix, + cmap, + font_size, + char_scale, + space_scale, + _space_width, + TL, + ) + ) elif operator == b"Q": try: - cm_matrix = cm_stack.pop() + ( + cm_matrix, + cmap, + font_size, + char_scale, + space_scale, + _space_width, + TL, + ) = cm_stack.pop() except Exception: cm_matrix = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0] elif operator == b"cm": diff --git a/tests/test_page.py b/tests/test_page.py index 41d4a1152..1c55e4ade 100644 --- a/tests/test_page.py +++ b/tests/test_page.py @@ -238,6 +238,15 @@ def test_extract_text_single_quote_op(): page.extract_text() +def test_iss_1142(): + # check fix for problem of context save/restore (q/Q) + url = "https://github.com/py-pdf/PyPDF2/files/9150656/ST.2019.PDF" + name = "st2019.pdf" + reader = PdfReader(BytesIO(get_pdf_from_url(url, name=name))) + txt = reader.pages[3].extract_text() + assert txt.find("有限公司郑州分公司") > 0 + + @pytest.mark.parametrize( ("url", "name"), [ From 9c8252d5bc876c0048b6dfe3b531bc8fa6cfd81e Mon Sep 17 00:00:00 2001 From: pubpub-zz <4083478+pubpub-zz@users.noreply.github.com> Date: Wed, 27 Jul 2022 21:31:58 +0200 Subject: [PATCH 059/130] BUG: Named Dest in PDF1.1 (#1174) Named destinations are stored in a dictionary in PDF 1.1 Closes #1173 --- PyPDF2/_reader.py | 10 +++++++--- tests/test_reader.py | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/PyPDF2/_reader.py b/PyPDF2/_reader.py index 1664bfe9d..98f1699d9 100644 --- a/PyPDF2/_reader.py +++ b/PyPDF2/_reader.py @@ -644,9 +644,8 @@ def _get_named_destinations( # recurse down the tree for kid in cast(ArrayObject, tree[PA.KIDS]): self._get_named_destinations(kid.get_object(), retval) - # TABLE 3.33 Entries in a name tree node dictionary (PDF 1.7 specs) - if CA.NAMES in tree: + elif CA.NAMES in tree: # KIDS and NAMES are exclusives (PDF 1.7 specs p 162) names = cast(DictionaryObject, tree[CA.NAMES]) for i in range(0, len(names), 2): key = cast(str, names[i].get_object()) @@ -656,7 +655,12 @@ def _get_named_destinations( dest = self._build_destination(key, value) # type: ignore if dest is not None: retval[key] = dest - + else: # case where Dests is in root catalog (PDF 1.7 specs, §2 about PDF1.1 + for k__, v__ in tree.items(): + val = v__.get_object() + dest = self._build_destination(k__, val) + if dest is not None: + retval[k__] = dest return retval def getNamedDestinations( diff --git a/tests/test_reader.py b/tests/test_reader.py index a4503b2a2..419b1558c 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -977,6 +977,21 @@ def test_outline_missing_title(): assert exc.value.args[0].startswith("Outline Entry Missing /Title attribute:") +def test_named_destination(): + # 1st case : the named_dest are stored directly as a dictionnary, PDF1.1 style + url = "https://github.com/py-pdf/PyPDF2/files/9197028/lorem_ipsum.pdf" + name = "lorem_ipsum.pdf" + reader = PdfReader(BytesIO(get_pdf_from_url(url, name=name))) + assert len(reader.named_destinations) > 0 + # 2nd case : Dest below names and with Kids... + url = "https://opensource.adobe.com/dc-acrobat-sdk-docs/standards/pdfstandards/pdf/PDF32000_2008.pdf" + name = "PDF32000_2008.pdf" + reader = PdfReader(BytesIO(get_pdf_from_url(url, name=name))) + assert len(reader.named_destinations) > 0 + # 3nd case : Dests with Name tree + # TODO : case to be added + + def test_outline_with_missing_named_destination(): url = "https://corpora.tika.apache.org/base/docs/govdocs1/913/913678.pdf" name = "tika-913678.pdf" From 7b852acb3350033a9f76fbc61f6e0d27f561b444 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Thu, 28 Jul 2022 19:42:44 +0200 Subject: [PATCH 060/130] DOC: We now have CMAP support (#1177) --- docs/user/pdf-version-support.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/user/pdf-version-support.md b/docs/user/pdf-version-support.md index a588997ab..ad2033fb6 100644 --- a/docs/user/pdf-version-support.md +++ b/docs/user/pdf-version-support.md @@ -21,12 +21,12 @@ all features of PDF 2.0. | Feature | PDF-Version | PyPDF2 Support | | --------------------------------------- | ----------- | -------------- | | Transparent Graphics | 1.4 | ? | -| CMaps | 1.4 | ❌ [#201](https://github.com/py-pdf/PyPDF2/pull/201), [#464](https://github.com/py-pdf/PyPDF2/pull/464), [#805](https://github.com/py-pdf/PyPDF2/pull/805) | +| CMaps | 1.4 | ✅ | | Object Streams | 1.5 | ? | | Cross-reference Streams | 1.5 | ? | | Optional Content Groups (OCGs) - Layers | 1.5 | ? | | Content Stream Compression | 1.5 | ? | -| AES Encryption | 1.6 | ❌ [#749](https://github.com/py-pdf/PyPDF2/pull/749) | +| AES Encryption | 1.6 | ✅ | See [History of PDF](https://en.wikipedia.org/wiki/History_of_PDF) for more features. From 8d5037c590fbab28d9980962070d28a94dfd9be5 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Fri, 29 Jul 2022 08:47:25 +0200 Subject: [PATCH 061/130] DOC: Mention pyHanko for signing PDF documents (#1178) --- docs/user/pdf-version-support.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/user/pdf-version-support.md b/docs/user/pdf-version-support.md index ad2033fb6..e8312c752 100644 --- a/docs/user/pdf-version-support.md +++ b/docs/user/pdf-version-support.md @@ -30,3 +30,8 @@ all features of PDF 2.0. See [History of PDF](https://en.wikipedia.org/wiki/History_of_PDF) for more features. + +Some PDF features are not supported by PyPDF2, but other libraries can be used +for them: + +* [pyHanko](https://pyhanko.readthedocs.io/en/latest/index.html): Cryptographically sign a PDF From 85ca871b007c23e0b5dcb8ab5915b63b1d9ac7e7 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Fri, 29 Jul 2022 18:54:31 +0200 Subject: [PATCH 062/130] DOC: Table extraction (#1179) --- docs/user/pdf-version-support.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/user/pdf-version-support.md b/docs/user/pdf-version-support.md index e8312c752..4ac0adf13 100644 --- a/docs/user/pdf-version-support.md +++ b/docs/user/pdf-version-support.md @@ -34,4 +34,5 @@ features. Some PDF features are not supported by PyPDF2, but other libraries can be used for them: -* [pyHanko](https://pyhanko.readthedocs.io/en/latest/index.html): Cryptographically sign a PDF +* [pyHanko](https://pyhanko.readthedocs.io/en/latest/index.html): Cryptographically sign a PDF ([#302](https://github.com/py-pdf/PyPDF2/issues/302)) +* [camelot-py](https://pypi.org/project/camelot-py/): Table Extraction ([#231](https://github.com/py-pdf/PyPDF2/issues/231)) From 2d480685a72d665826dbd53f973173b34cf4c872 Mon Sep 17 00:00:00 2001 From: Mathieu Kniewallner Date: Fri, 29 Jul 2022 19:43:02 +0200 Subject: [PATCH 063/130] DOC: Update changelog url in package metadata (#1180) --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 62d77a818..b43b87b97 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,7 +16,7 @@ url = https://pypdf2.readthedocs.io/en/latest/ project_urls = Source = https://github.com/py-pdf/PyPDF2 Bug Reports = https://github.com/py-pdf/PyPDF2/issues - Changelog = https://raw.githubusercontent.com/py-pdf/PyPDF2/main/CHANGELOG + Changelog = https://pypdf2.readthedocs.io/en/latest/meta/CHANGELOG.html classifiers = Development Status :: 5 - Production/Stable Intended Audience :: Developers From 8c532a0ff13395b706d0ae1f183dd24bab577bfc Mon Sep 17 00:00:00 2001 From: mtd91429 Date: Sat, 30 Jul 2022 00:09:41 -0500 Subject: [PATCH 064/130] MAINT: Consistent terminology for outline items (#1156) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR makes sure PyPDF2 uses a consistent nomenclature for the outline: * **Outline**: A document has exactly one outline (also called "table of contents", in short toc). That outline might be empty. * **Outline Item**: An element within an outline. This is also called a "bookmark" by some PDF viewers. This means that some names will be deprecated to ensure consistency: ## PdfReader * `outlines` ➔ `outline` * `_build_outline()` ➔ `_build_outline_item()` ## PdfWriter * Keep `get_outline_root()` * `add_bookmark_dict()` ➔ `add_outline()` * `add_bookmark()` ➔ `add_outline_item()` ## PdfMerger * `find_bookmark()` ➔ `find_outline_item()` * `_write_bookmarks()` ➔ `_write_outline()` * `_write_bookmark_on_page()` ➔ `_write_outline_item_on_page()` * `_associate_bookmarks_to_pages()` ➔ `_associate_outline_items_to_pages()` * Keep `_trim_outline()` ## generic.py * `Bookmark` ➔ `OutlineItem` Closes #1048 Closes #1098 --- PyPDF2/_merger.py | 255 ++++++++++++++++++++++---------------- PyPDF2/_reader.py | 103 ++++++++------- PyPDF2/_utils.py | 44 ++++++- PyPDF2/_writer.py | 137 ++++++++++++++------ PyPDF2/generic.py | 20 +-- PyPDF2/types.py | 11 +- docs/user/extract-text.md | 4 +- tests/bench.py | 12 +- tests/test_generic.py | 10 +- tests/test_merger.py | 87 ++++++++----- tests/test_reader.py | 66 +++++----- tests/test_workflows.py | 2 +- tests/test_writer.py | 65 +++++++--- 13 files changed, 519 insertions(+), 297 deletions(-) diff --git a/PyPDF2/_merger.py b/PyPDF2/_merger.py index 6db5df4d7..78ee8c6bf 100644 --- a/PyPDF2/_merger.py +++ b/PyPDF2/_merger.py @@ -31,14 +31,13 @@ from ._encryption import Encryption from ._page import PageObject from ._reader import PdfReader -from ._utils import StrByteType, deprecate_with_replacement, str_ +from ._utils import StrByteType, deprecate_with_replacement, deprecate_bookmark, str_ from ._writer import PdfWriter from .constants import GoToActionArguments from .constants import PagesAttributes as PA from .constants import TypArguments, TypFitArguments from .generic import ( ArrayObject, - Bookmark, Destination, DictionaryObject, FloatObject, @@ -46,11 +45,12 @@ NameObject, NullObject, NumberObject, + OutlineItem, TextStringObject, TreeObject, ) from .pagerange import PageRange, PageRangeSpec -from .types import FitType, LayoutType, OutlinesType, PagemodeType, ZoomArgType +from .types import FitType, LayoutType, OutlineType, PagemodeType, ZoomArgType ERR_CLOSED_WRITER = "close() was called and thus the writer cannot be used anymore" @@ -80,22 +80,24 @@ class PdfMerger: Defaults to ``False``. """ + @deprecate_bookmark(bookmarks="outline") def __init__(self, strict: bool = False) -> None: self.inputs: List[Tuple[Any, PdfReader, bool]] = [] self.pages: List[Any] = [] self.output: Optional[PdfWriter] = PdfWriter() - self.bookmarks: OutlinesType = [] + self.outline: OutlineType = [] self.named_dests: List[Any] = [] self.id_count = 0 self.strict = strict + @deprecate_bookmark(bookmark="outline_item", import_bookmarks="import_outline") def merge( self, position: int, fileobj: Union[StrByteType, PdfReader], - bookmark: Optional[str] = None, + outline_item: Optional[str] = None, pages: Optional[PageRangeSpec] = None, - import_bookmarks: bool = True, + import_outline: bool = True, ) -> None: """ Merge the pages from the given file into the output file at the @@ -108,17 +110,18 @@ def merge( read and seek methods similar to a File Object. Could also be a string representing a path to a PDF file. - :param str bookmark: Optionally, you may specify a bookmark to be - applied at the beginning of the included file by supplying the text - of the bookmark. + :param str outline_item: Optionally, you may specify an outline item + (previously referred to as a 'bookmark') to be applied at the + beginning of the included file by supplying the text of the outline item. :param pages: can be a :class:`PageRange` or a ``(start, stop[, step])`` tuple to merge only the specified range of pages from the source document into the output document. - :param bool import_bookmarks: You may prevent the source document's - bookmarks from being imported by specifying this as ``False``. + :param bool import_outline: You may prevent the source document's + outline (collection of outline items, previously referred to as + 'bookmarks') from being imported by specifying this as ``False``. """ stream, my_file, encryption_obj = self._create_stream(fileobj) @@ -140,19 +143,19 @@ def merge( srcpages = [] outline = [] - if import_bookmarks: - outline = reader.outlines + if import_outline: + outline = reader.outline outline = self._trim_outline(reader, outline, pages) - if bookmark: - bookmark_typ = Bookmark( - TextStringObject(bookmark), + if outline_item: + outline_item_typ = OutlineItem( + TextStringObject(outline_item), NumberObject(self.id_count), NameObject(TypFitArguments.FIT), ) - self.bookmarks += [bookmark_typ, outline] # type: ignore + self.outline += [outline_item_typ, outline] # type: ignore else: - self.bookmarks += outline + self.outline += outline dests = reader.named_destinations trimmed_dests = self._trim_dests(reader, dests, pages) @@ -170,7 +173,7 @@ def merge( srcpages.append(mp) self._associate_dests_to_pages(srcpages) - self._associate_bookmarks_to_pages(srcpages) + self._associate_outline_items_to_pages(srcpages) # Slice to insert the pages at the specified position self.pages[position:position] = srcpages @@ -213,12 +216,13 @@ def _create_stream( stream = fileobj return stream, my_file, encryption_obj + @deprecate_bookmark(bookmark="outline_item", import_bookmarks="import_outline") def append( self, fileobj: Union[StrByteType, PdfReader], - bookmark: Optional[str] = None, + outline_item: Optional[str] = None, pages: Union[None, PageRange, Tuple[int, int], Tuple[int, int, int]] = None, - import_bookmarks: bool = True, + import_outline: bool = True, ) -> None: """ Identical to the :meth:`merge()` method, but assumes you want to @@ -229,19 +233,20 @@ def append( read and seek methods similar to a File Object. Could also be a string representing a path to a PDF file. - :param str bookmark: Optionally, you may specify a bookmark to be - applied at the beginning of the included file by supplying the text - of the bookmark. + :param str outline_item: Optionally, you may specify an outline item + (previously referred to as a 'bookmark') to be applied at the + beginning of the included file by supplying the text of the outline item. :param pages: can be a :class:`PageRange` or a ``(start, stop[, step])`` tuple to merge only the specified range of pages from the source document into the output document. - :param bool import_bookmarks: You may prevent the source document's - bookmarks from being imported by specifying this as ``False``. + :param bool import_outline: You may prevent the source document's + outline (collection of outline items, previously referred to as + 'bookmarks') from being imported by specifying this as ``False``. """ - self.merge(len(self.pages), fileobj, bookmark, pages, import_bookmarks) + self.merge(len(self.pages), fileobj, outline_item, pages, import_outline) def write(self, fileobj: StrByteType) -> None: """ @@ -269,9 +274,9 @@ def write(self, fileobj: StrByteType) -> None: # idnum = self.output._objects.index(self.output._pages.get_object()[PA.KIDS][-1].get_object()) + 1 # page.out_pagedata = IndirectObject(idnum, 0, self.output) - # Once all pages are added, create bookmarks to point at those pages + # Once all pages are added, create outline items to point at those pages self._write_dests() - self._write_bookmarks() + self._write_outline() # Write the output to the file self.output.write(fileobj) @@ -366,9 +371,9 @@ def set_page_mode(self, mode: PagemodeType) -> None: :widths: 50 200 * - /UseNone - - Do not show outlines or thumbnails panels + - Do not show outline or thumbnails panels * - /UseOutlines - - Show outlines (aka bookmarks) panel + - Show outline (aka bookmarks) panel * - /UseThumbs - Show page thumbnails panel * - /FullScreen @@ -402,15 +407,15 @@ def _trim_dests( def _trim_outline( self, pdf: PdfReader, - outline: OutlinesType, + outline: OutlineType, pages: Union[Tuple[int, int], Tuple[int, int, int]], - ) -> OutlinesType: - """Remove outline/bookmark entries that are not a part of the specified page set.""" + ) -> OutlineType: + """Remove outline item entries that are not a part of the specified page set.""" new_outline = [] prev_header_added = True - for i, o in enumerate(outline): - if isinstance(o, list): - sub = self._trim_outline(pdf, o, pages) # type: ignore + for i, outline_item in enumerate(outline): + if isinstance(outline_item, list): + sub = self._trim_outline(pdf, outline_item, pages) # type: ignore if sub: if not prev_header_added: new_outline.append(outline[i - 1]) @@ -418,11 +423,11 @@ def _trim_outline( else: prev_header_added = False for j in range(*pages): - if o["/Page"] is None: + if outline_item["/Page"] is None: continue - if pdf.pages[j].get_object() == o["/Page"].get_object(): - o[NameObject("/Page")] = o["/Page"].get_object() - new_outline.append(o) + if pdf.pages[j].get_object() == outline_item["/Page"].get_object(): + outline_item[NameObject("/Page")] = outline_item["/Page"].get_object() + new_outline.append(outline_item) prev_header_added = True break return new_outline @@ -441,38 +446,40 @@ def _write_dests(self) -> None: if pageno is not None: self.output.add_named_destination_object(named_dest) - def _write_bookmarks( + @deprecate_bookmark(bookmarks="outline") + def _write_outline( self, - bookmarks: Optional[Iterable[Bookmark]] = None, + outline: Optional[Iterable[OutlineItem]] = None, parent: Optional[TreeObject] = None, ) -> None: if self.output is None: raise RuntimeError(ERR_CLOSED_WRITER) - if bookmarks is None: - bookmarks = self.bookmarks # type: ignore - assert bookmarks is not None, "hint for mypy" # TODO: is that true? + if outline is None: + outline = self.outline # type: ignore + assert outline is not None, "hint for mypy" # TODO: is that true? last_added = None - for bookmark in bookmarks: - if isinstance(bookmark, list): - self._write_bookmarks(bookmark, last_added) + for outline_item in outline: + if isinstance(outline_item, list): + self._write_outline(outline_item, last_added) continue page_no = None - if "/Page" in bookmark: + if "/Page" in outline_item: for page_no, page in enumerate(self.pages): # noqa: B007 - if page.id == bookmark["/Page"]: - self._write_bookmark_on_page(bookmark, page) + if page.id == outline_item["/Page"]: + self._write_outline_item_on_page(outline_item, page) break if page_no is not None: - del bookmark["/Page"], bookmark["/Type"] - last_added = self.output.add_bookmark_dict(bookmark, parent) + del outline_item["/Page"], outline_item["/Type"] + last_added = self.output.add_outline_item_dict(outline_item, parent) - def _write_bookmark_on_page( - self, bookmark: Union[Bookmark, Destination], page: _MergedPage + @deprecate_bookmark(bookmark="outline_item") + def _write_outline_item_on_page( + self, outline_item: Union[OutlineItem, Destination], page: _MergedPage ) -> None: - bm_type = cast(str, bookmark["/Type"]) - args = [NumberObject(page.id), NameObject(bm_type)] + oi_type = cast(str, outline_item["/Type"]) + args = [NumberObject(page.id), NameObject(oi_type)] fit2arg_keys: Dict[str, Tuple[str, ...]] = { TypFitArguments.FIT_H: (TypArguments.TOP,), TypFitArguments.FIT_BH: (TypArguments.TOP,), @@ -486,14 +493,16 @@ def _write_bookmark_on_page( TypArguments.TOP, ), } - for arg_key in fit2arg_keys.get(bm_type, tuple()): - if arg_key in bookmark and not isinstance(bookmark[arg_key], NullObject): - args.append(FloatObject(bookmark[arg_key])) + for arg_key in fit2arg_keys.get(oi_type, tuple()): + if arg_key in outline_item and not isinstance( + outline_item[arg_key], NullObject + ): + args.append(FloatObject(outline_item[arg_key])) else: args.append(FloatObject(0)) - del bookmark[arg_key] + del outline_item[arg_key] - bookmark[NameObject("/A")] = DictionaryObject( + outline_item[NameObject("/A")] = DictionaryObject( { NameObject(GoToActionArguments.S): NameObject("/GoTo"), NameObject(GoToActionArguments.D): ArrayObject(args), @@ -517,51 +526,101 @@ def _associate_dests_to_pages(self, pages: List[_MergedPage]) -> None: else: raise ValueError(f"Unresolved named destination '{nd['/Title']}'") - def _associate_bookmarks_to_pages( - self, pages: List[_MergedPage], bookmarks: Optional[Iterable[Bookmark]] = None + @deprecate_bookmark(bookmarks="outline") + def _associate_outline_items_to_pages( + self, pages: List[_MergedPage], outline: Optional[Iterable[OutlineItem]] = None ) -> None: - if bookmarks is None: - bookmarks = self.bookmarks # type: ignore # TODO: self.bookmarks can be None! - assert bookmarks is not None, "hint for mypy" - for b in bookmarks: - if isinstance(b, list): - self._associate_bookmarks_to_pages(pages, b) + if outline is None: + outline = self.outline # type: ignore # TODO: self.bookmarks can be None! + assert outline is not None, "hint for mypy" + for outline_item in outline: + if isinstance(outline_item, list): + self._associate_outline_items_to_pages(pages, outline_item) continue pageno = None - bp = b["/Page"] + outline_item_page = outline_item["/Page"] - if isinstance(bp, NumberObject): + if isinstance(outline_item_page, NumberObject): continue for p in pages: - if bp.get_object() == p.pagedata.get_object(): + if outline_item_page.get_object() == p.pagedata.get_object(): pageno = p.id if pageno is not None: - b[NameObject("/Page")] = NumberObject(pageno) + outline_item[NameObject("/Page")] = NumberObject(pageno) - def find_bookmark( + @deprecate_bookmark(bookmark="outline_item") + def find_outline_item( self, - bookmark: Dict[str, Any], - root: Optional[OutlinesType] = None, + outline_item: Dict[str, Any], + root: Optional[OutlineType] = None, ) -> Optional[List[int]]: if root is None: - root = self.bookmarks + root = self.outline - for i, b in enumerate(root): - if isinstance(b, list): - # b is still an inner node - # (OutlinesType, if recursive types were supported by mypy) - res = self.find_bookmark(bookmark, b) # type: ignore + for i, oi_enum in enumerate(root): + if isinstance(oi_enum, list): + # oi_enum is still an inner node + # (OutlineType, if recursive types were supported by mypy) + res = self.find_outline_item(outline_item, oi_enum) # type: ignore if res: return [i] + res - elif b == bookmark or cast(Dict[Any, Any], b["/Title"]) == bookmark: + elif ( + oi_enum == outline_item + or cast(Dict[Any, Any], oi_enum["/Title"]) == outline_item + ): # we found a leaf node return [i] return None + @deprecate_bookmark(bookmark="outline_item") + def find_bookmark( + self, + outline_item: Dict[str, Any], + root: Optional[OutlineType] = None, + ) -> Optional[List[int]]: + """ + .. deprecated:: 2.9.0 + Use :meth:`find_outline_item` instead. + """ + + return self.find_outline_item(outline_item, root) + + def add_outline_item( + self, + title: str, + pagenum: int, + parent: Union[None, TreeObject, IndirectObject] = None, + color: Optional[Tuple[float, float, float]] = None, + bold: bool = False, + italic: bool = False, + fit: FitType = "/Fit", + *args: ZoomArgType, + ) -> IndirectObject: + """ + Add an outline item (commonly referred to as a "Bookmark") to this PDF file. + + :param str title: Title to use for this outline item. + :param int pagenum: Page number this outline item will point to. + :param parent: A reference to a parent outline item to create nested + outline items. + :param tuple color: Color of the outline item's font as a red, green, blue tuple + from 0.0 to 1.0 + :param bool bold: Outline item font is bold + :param bool italic: Outline item font is italic + :param str fit: The fit of the destination page. See + :meth:`add_link()` for details. + """ + writer = self.output + if writer is None: + raise RuntimeError(ERR_CLOSED_WRITER) + return writer.add_outline_item( + title, pagenum, parent, color, bold, italic, fit, *args + ) + def addBookmark( self, title: str, @@ -575,10 +634,10 @@ def addBookmark( ) -> IndirectObject: # pragma: no cover """ .. deprecated:: 1.28.0 - Use :meth:`add_bookmark` instead. + Use :meth:`add_outline_item` instead. """ - deprecate_with_replacement("addBookmark", "add_bookmark") - return self.add_bookmark( + deprecate_with_replacement("addBookmark", "add_outline_item") + return self.add_outline_item( title, pagenum, parent, color, bold, italic, fit, *args ) @@ -594,23 +653,11 @@ def add_bookmark( *args: ZoomArgType, ) -> IndirectObject: """ - Add a bookmark to this PDF file. - - :param str title: Title to use for this bookmark. - :param int pagenum: Page number this bookmark will point to. - :param parent: A reference to a parent bookmark to create nested - bookmarks. - :param tuple color: Color of the bookmark as a red, green, blue tuple - from 0.0 to 1.0 - :param bool bold: Bookmark is bold - :param bool italic: Bookmark is italic - :param str fit: The fit of the destination page. See - :meth:`addLink()` for details. + .. deprecated:: 2.9.0 + Use :meth:`add_outline_item` instead. """ - writer = self.output - if writer is None: - raise RuntimeError(ERR_CLOSED_WRITER) - return writer.add_bookmark( + deprecate_with_replacement("addBookmark", "add_outline_item") + return self.add_outline_item( title, pagenum, parent, color, bold, italic, fit, *args ) diff --git a/PyPDF2/_reader.py b/PyPDF2/_reader.py index 98f1699d9..80125d9d0 100644 --- a/PyPDF2/_reader.py +++ b/PyPDF2/_reader.py @@ -89,7 +89,7 @@ TreeObject, read_object, ) -from .types import OutlinesType, PagemodeType +from .types import OutlineType, PagemodeType from .xmp import XmpInformation @@ -677,19 +677,30 @@ def getNamedDestinations( return self._get_named_destinations(tree, retval) @property - def outlines(self) -> OutlinesType: + def outline(self) -> OutlineType: """ - Read-only property for outlines present in the document. + Read-only property for the outline (i.e., a collection of 'outline items' + which are also known as 'bookmarks') present in the document. :return: a nested list of :class:`Destinations`. """ - return self._get_outlines() + return self._get_outline() - def _get_outlines( - self, node: Optional[DictionaryObject] = None, outlines: Optional[Any] = None - ) -> OutlinesType: - if outlines is None: - outlines = [] + @property + def outlines(self) -> OutlineType: + """ + .. deprecated:: 2.9.0 + + Use :py:attr:`outline` instead. + """ + deprecate_with_replacement("outlines", "outline") + return self.outline + + def _get_outline( + self, node: Optional[DictionaryObject] = None, outline: Optional[Any] = None + ) -> OutlineType: + if outline is None: + outline = [] catalog = cast(DictionaryObject, self.trailer[TK.ROOT]) # get the outline dictionary and named destinations @@ -699,11 +710,11 @@ def _get_outlines( except PdfReadError: # this occurs if the /Outlines object reference is incorrect # for an example of such a file, see https://unglueit-files.s3.amazonaws.com/ebf/7552c42e9280b4476e59e77acc0bc812.pdf - # so continue to load the file without the Bookmarks - return outlines + # so continue to load the file without the Outlines + return outline if isinstance(lines, NullObject): - return outlines + return outline # TABLE 8.3 Entries in the outline dictionary if lines is not None and "/First" in lines: @@ -711,37 +722,37 @@ def _get_outlines( self._namedDests = self._get_named_destinations() if node is None: - return outlines + return outline - # see if there are any more outlines + # see if there are any more outline items while True: - outline = self._build_outline(node) - if outline: - outlines.append(outline) + outline_obj = self._build_outline_item(node) + if outline_obj: + outline.append(outline_obj) - # check for sub-outlines + # check for sub-outline if "/First" in node: - sub_outlines: List[Any] = [] - self._get_outlines(cast(DictionaryObject, node["/First"]), sub_outlines) - if sub_outlines: - outlines.append(sub_outlines) + sub_outline: List[Any] = [] + self._get_outline(cast(DictionaryObject, node["/First"]), sub_outline) + if sub_outline: + outline.append(sub_outline) if "/Next" not in node: break node = cast(DictionaryObject, node["/Next"]) - return outlines + return outline def getOutlines( - self, node: Optional[DictionaryObject] = None, outlines: Optional[Any] = None - ) -> OutlinesType: # pragma: no cover + self, node: Optional[DictionaryObject] = None, outline: Optional[Any] = None + ) -> OutlineType: # pragma: no cover """ .. deprecated:: 1.28.0 - Use :py:attr:`outlines` instead. + Use :py:attr:`outline` instead. """ - deprecate_with_replacement("getOutlines", "outlines") - return self._get_outlines(node, outlines) + deprecate_with_replacement("getOutlines", "outline") + return self._get_outline(node, outline) def _get_page_number_by_indirect( self, indirect_ref: Union[None, int, NullObject, IndirectObject] @@ -809,7 +820,7 @@ def _build_destination( array: List[Union[NumberObject, IndirectObject, NullObject, DictionaryObject]], ) -> Destination: page, typ = None, None - # handle outlines with missing or invalid destination + # handle outline items with missing or invalid destination if ( isinstance(array, (type(None), NullObject)) or (isinstance(array, ArrayObject) and len(array) == 0) @@ -835,8 +846,8 @@ def _build_destination( title, indirect_ref, TextStringObject("/Fit") # type: ignore ) - def _build_outline(self, node: DictionaryObject) -> Optional[Destination]: - dest, title, outline = None, None, None + def _build_outline_item(self, node: DictionaryObject) -> Optional[Destination]: + dest, title, outline_item = None, None, None # title required for valid outline # PDF Reference 1.7: TABLE 8.4 Entries in an outline item dictionary @@ -861,40 +872,40 @@ def _build_outline(self, node: DictionaryObject) -> Optional[Destination]: dest = dest["/D"] if isinstance(dest, ArrayObject): - outline = self._build_destination(title, dest) # type: ignore + outline_item = self._build_destination(title, dest) # type: ignore elif isinstance(dest, str): # named destination, addresses NameObject Issue #193 try: - outline = self._build_destination( + outline_item = self._build_destination( title, self._namedDests[dest].dest_array ) except KeyError: # named destination not found in Name Dict - outline = self._build_destination(title, None) + outline_item = self._build_destination(title, None) elif isinstance(dest, type(None)): - # outline not required to have destination or action + # outline item not required to have destination or action # PDFv1.7 Table 153 - outline = self._build_destination(title, dest) # type: ignore + outline_item = self._build_destination(title, dest) # type: ignore else: if self.strict: raise PdfReadError(f"Unexpected destination {dest!r}") - outline = self._build_destination(title, None) # type: ignore + outline_item = self._build_destination(title, None) # type: ignore - # if outline created, add color, format, and child count if present - if outline: + # if outline item created, add color, format, and child count if present + if outline_item: if "/C" in node: - # Color of outline in (R, G, B) with values ranging 0.0-1.0 - outline[NameObject("/C")] = ArrayObject(FloatObject(c) for c in node["/C"]) # type: ignore + # Color of outline item font in (R, G, B) with values ranging 0.0-1.0 + outline_item[NameObject("/C")] = ArrayObject(FloatObject(c) for c in node["/C"]) # type: ignore if "/F" in node: # specifies style characteristics bold and/or italic # 1=italic, 2=bold, 3=both - outline[NameObject("/F")] = node["/F"] + outline_item[NameObject("/F")] = node["/F"] if "/Count" in node: # absolute value = num. visible children # positive = open/unfolded, negative = closed/folded - outline[NameObject("/Count")] = node["/Count"] + outline_item[NameObject("/Count")] = node["/Count"] - return outline + return outline_item @property def pages(self) -> _VirtualList: @@ -961,9 +972,9 @@ def page_mode(self) -> Optional[PagemodeType]: :widths: 50 200 * - /UseNone - - Do not show outlines or thumbnails panels + - Do not show outline or thumbnails panels * - /UseOutlines - - Show outlines (aka bookmarks) panel + - Show outline (aka bookmarks) panel * - /UseThumbs - Show page thumbnails panel * - /FullScreen diff --git a/PyPDF2/_utils.py b/PyPDF2/_utils.py index 78dc0f9f0..6d80832e0 100644 --- a/PyPDF2/_utils.py +++ b/PyPDF2/_utils.py @@ -29,6 +29,7 @@ __author__ = "Mathieu Fenniak" __author_email__ = "biziqe@mathieu.fenniak.net" +import functools import logging import warnings from codecs import getencoder @@ -40,7 +41,7 @@ FileIO, ) from os import SEEK_CUR -from typing import Dict, Optional, Pattern, Tuple, Union, overload +from typing import Any, Callable, Dict, Optional, Pattern, Tuple, Union, overload try: # Python 3.10+: https://www.python.org/dev/peps/pep-0484/ @@ -362,3 +363,44 @@ def logger_warning(msg: str, src: str) -> None: to strict=False mode. """ logging.getLogger(src).warning(msg) + + +def deprecate_bookmark(**aliases: str) -> Callable: + """ + Decorator for deprecated term "bookmark" + To be used for methods and function arguments + outline_item = a bookmark + outline = a collection of outline items + """ + + def decoration(func: Callable): # type: ignore + @functools.wraps(func) + def wrapper(*args, **kwargs): # type: ignore + rename_kwargs(func.__name__, kwargs, aliases) + return func(*args, **kwargs) + + return wrapper + + return decoration + + +def rename_kwargs( # type: ignore + func_name: str, kwargs: Dict[str, Any], aliases: Dict[str, str] +): + """ + Helper function to deprecate arguments. + """ + + for old_term, new_term in aliases.items(): + if old_term in kwargs: + if new_term in kwargs: + raise TypeError( + f"{func_name} received both {old_term} and {new_term} as an argument." + f"{old_term} is deprecated. Use {new_term} instead." + ) + kwargs[new_term] = kwargs.pop(old_term) + warnings.warn( + message=( + f"{old_term} is deprecated as an argument. Use {new_term} instead" + ) + ) diff --git a/PyPDF2/_writer.py b/PyPDF2/_writer.py index a3b8ed645..579812554 100644 --- a/PyPDF2/_writer.py +++ b/PyPDF2/_writer.py @@ -59,6 +59,7 @@ _get_max_pdf_version_header, b_, deprecate_with_replacement, + deprecate_bookmark, ) from .constants import AnnotationDictionaryAttributes from .constants import CatalogAttributes as CA @@ -82,6 +83,7 @@ BooleanObject, ByteStringObject, ContentStream, + _create_outline_item, DecodedStreamObject, Destination, DictionaryObject, @@ -95,15 +97,14 @@ StreamObject, TextStringObject, TreeObject, - _create_bookmark, create_string_object, ) from .types import ( - BookmarkTypes, BorderArrayType, FitType, LayoutType, PagemodeType, + OutlineItemType, ZoomArgsType, ZoomArgType, ) @@ -1073,7 +1074,7 @@ def getNamedDestRoot(self) -> ArrayObject: # pragma: no cover deprecate_with_replacement("getNamedDestRoot", "get_named_dest_root") return self.get_named_dest_root() - def add_bookmark_destination( + def add_outline_item_destination( self, dest: Union[PageObject, TreeObject], parent: Union[None, TreeObject, IndirectObject] = None, @@ -1087,47 +1088,78 @@ def add_bookmark_destination( return dest_ref + def add_bookmark_destination( + self, + dest: Union[PageObject, TreeObject], + parent: Union[None, TreeObject, IndirectObject] = None, + ) -> IndirectObject: + """ + .. deprecated:: 2.9.0 + + Use :meth:`add_outline_item_destination` instead. + """ + deprecate_with_replacement( + "add_bookmark_destination", "add_outline_item_destination" + ) + return self.add_outline_item_destination(dest, parent) + def addBookmarkDestination( self, dest: PageObject, parent: Optional[TreeObject] = None ) -> IndirectObject: # pragma: no cover """ .. deprecated:: 1.28.0 - Use :meth:`add_bookmark_destination` instead. + Use :meth:`add_outline_item_destination` instead. """ - deprecate_with_replacement("addBookmarkDestination", "add_bookmark_destination") - return self.add_bookmark_destination(dest, parent) + deprecate_with_replacement( + "addBookmarkDestination", "add_outline_item_destination" + ) + return self.add_outline_item_destination(dest, parent) - def add_bookmark_dict( - self, bookmark: BookmarkTypes, parent: Optional[TreeObject] = None + @deprecate_bookmark(bookmark="outline_item") + def add_outline_item_dict( + self, outline_item: OutlineItemType, parent: Optional[TreeObject] = None ) -> IndirectObject: - bookmark_obj = TreeObject() - for k, v in list(bookmark.items()): - bookmark_obj[NameObject(str(k))] = v - bookmark_obj.update(bookmark) + outline_item_object = TreeObject() + for k, v in list(outline_item.items()): + outline_item_object[NameObject(str(k))] = v + outline_item_object.update(outline_item) - if "/A" in bookmark: + if "/A" in outline_item: action = DictionaryObject() - a_dict = cast(DictionaryObject, bookmark["/A"]) + a_dict = cast(DictionaryObject, outline_item["/A"]) for k, v in list(a_dict.items()): action[NameObject(str(k))] = v action_ref = self._add_object(action) - bookmark_obj[NameObject("/A")] = action_ref + outline_item_object[NameObject("/A")] = action_ref + + return self.add_outline_item_destination(outline_item_object, parent) - return self.add_bookmark_destination(bookmark_obj, parent) + @deprecate_bookmark(bookmark="outline_item") + def add_bookmark_dict( + self, outline_item: OutlineItemType, parent: Optional[TreeObject] = None + ) -> IndirectObject: + """ + .. deprecated:: 2.9.0 + + Use :meth:`add_outline_item_dict` instead. + """ + deprecate_with_replacement("add_bookmark_dict", "add_outline_item_dict") + return self.add_outline_item_dict(outline_item, parent) + @deprecate_bookmark(bookmark="outline_item") def addBookmarkDict( - self, bookmark: BookmarkTypes, parent: Optional[TreeObject] = None + self, outline_item: OutlineItemType, parent: Optional[TreeObject] = None ) -> IndirectObject: # pragma: no cover """ .. deprecated:: 1.28.0 - Use :meth:`add_bookmark_dict` instead. + Use :meth:`add_outline_item_dict` instead. """ - deprecate_with_replacement("addBookmarkDict", "add_bookmark_dict") - return self.add_bookmark_dict(bookmark, parent) + deprecate_with_replacement("addBookmarkDict", "add_outline_item_dict") + return self.add_outline_item_dict(outline_item, parent) - def add_bookmark( + def add_outline_item( self, title: str, pagenum: int, @@ -1139,25 +1171,28 @@ def add_bookmark( *args: ZoomArgType, ) -> IndirectObject: """ - Add a bookmark to this PDF file. + Add an outline item (commonly referred to as a "Bookmark") to this PDF file. - :param str title: Title to use for this bookmark. - :param int pagenum: Page number this bookmark will point to. - :param parent: A reference to a parent bookmark to create nested - bookmarks. - :param tuple color: Color of the bookmark as a red, green, blue tuple + :param str title: Title to use for this outline item. + :param int pagenum: Page number this outline item will point to. + :param parent: A reference to a parent outline item to create nested + outline items. + :param tuple color: Color of the outline item's font as a red, green, blue tuple from 0.0 to 1.0 - :param bool bold: Bookmark is bold - :param bool italic: Bookmark is italic + :param bool bold: Outline item font is bold + :param bool italic: Outline item font is italic :param str fit: The fit of the destination page. See - :meth:`addLink()` for details. + :meth:`add_link()` for details. """ page_ref = NumberObject(pagenum) zoom_args: ZoomArgsType = [ NullObject() if a is None else NumberObject(a) for a in args ] dest = Destination( - NameObject("/" + title + " bookmark"), page_ref, NameObject(fit), *zoom_args + NameObject("/" + title + " outline item"), + page_ref, + NameObject(fit), + *zoom_args, ) action_ref = self._add_object( @@ -1168,11 +1203,32 @@ def add_bookmark( } ) ) - bookmark = _create_bookmark(action_ref, title, color, italic, bold) + outline_item = _create_outline_item(action_ref, title, color, italic, bold) if parent is None: parent = self.get_outline_root() - return self.add_bookmark_destination(bookmark, parent) + return self.add_outline_item_destination(outline_item, parent) + + def add_bookmark( + self, + title: str, + pagenum: int, + parent: Union[None, TreeObject, IndirectObject] = None, + color: Optional[Tuple[float, float, float]] = None, + bold: bool = False, + italic: bool = False, + fit: FitType = "/Fit", + *args: ZoomArgType, + ) -> IndirectObject: + """ + .. deprecated:: 2.9.0 + + Use :meth:`add_outline_item` instead. + """ + deprecate_with_replacement("add_bookmark", "add_outline_item") + return self.add_outline_item( + title, pagenum, parent, color, bold, italic, fit, *args + ) def addBookmark( self, @@ -1188,13 +1244,18 @@ def addBookmark( """ .. deprecated:: 1.28.0 - Use :meth:`add_bookmark` instead. + Use :meth:`add_outline_item` instead. """ - deprecate_with_replacement("addBookmark", "add_bookmark") - return self.add_bookmark( + deprecate_with_replacement("addBookmark", "add_outline_item") + return self.add_outline_item( title, pagenum, parent, color, bold, italic, fit, *args ) + def add_outline(self) -> None: + raise NotImplementedError( + "This method is not yet implemented. Use :meth:`add_outline_item` instead." + ) + def add_named_destination_object(self, dest: PdfObject) -> IndirectObject: dest_ref = self._add_object(dest) @@ -1764,9 +1825,9 @@ def page_mode(self) -> Optional[PagemodeType]: :widths: 50 200 * - /UseNone - - Do not show outlines or thumbnails panels + - Do not show outline or thumbnails panels * - /UseOutlines - - Show outlines (aka bookmarks) panel + - Show outline (aka bookmarks) panel * - /UseThumbs - Show page thumbnails panel * - /FullScreen diff --git a/PyPDF2/generic.py b/PyPDF2/generic.py index 350bf8c16..bfdeff6ef 100644 --- a/PyPDF2/generic.py +++ b/PyPDF2/generic.py @@ -1933,7 +1933,7 @@ def outline_count(self) -> Optional[int]: return self.get("/Count", None) -class Bookmark(Destination): +class OutlineItem(Destination): def write_to_stream( self, stream: StreamType, encryption_key: Union[None, str, bytes] ) -> None: @@ -1957,6 +1957,12 @@ def write_to_stream( stream.write(b">>") +class Bookmark(OutlineItem): # pragma: no cover + def __init__(self, *args: Any, **kwargs: Any) -> None: + deprecate_with_replacement("Bookmark", "OutlineItem") + super().__init__(*args, **kwargs) + + def createStringObject( string: Union[str, bytes], forced_encoding: Union[None, str, List[str], Dict[int, str]] = None, @@ -2011,16 +2017,16 @@ def create_string_object( raise TypeError("create_string_object should have str or unicode arg") -def _create_bookmark( +def _create_outline_item( action_ref: IndirectObject, title: str, color: Optional[Tuple[float, float, float]], italic: bool, bold: bool, ) -> TreeObject: - bookmark = TreeObject() + outline_item = TreeObject() - bookmark.update( + outline_item.update( { NameObject("/A"): action_ref, NameObject("/Title"): create_string_object(title), @@ -2028,7 +2034,7 @@ def _create_bookmark( ) if color is not None: - bookmark.update( + outline_item.update( {NameObject("/C"): ArrayObject([FloatObject(c) for c in color])} ) @@ -2038,8 +2044,8 @@ def _create_bookmark( if bold: format_flag += 2 if format_flag: - bookmark.update({NameObject("/F"): NumberObject(format_flag)}) - return bookmark + outline_item.update({NameObject("/F"): NumberObject(format_flag)}) + return outline_item def encode_pdfdocencoding(unicode_string: str) -> bytes: diff --git a/PyPDF2/types.py b/PyPDF2/types.py index cb5358647..f17e5aa21 100644 --- a/PyPDF2/types.py +++ b/PyPDF2/types.py @@ -16,15 +16,17 @@ from .generic import ( ArrayObject, - Bookmark, Destination, NameObject, NullObject, NumberObject, + OutlineItem, ) BorderArrayType: TypeAlias = List[Union[NameObject, NumberObject, ArrayObject]] -BookmarkTypes: TypeAlias = Union[Bookmark, Destination] +OutlineItemType: TypeAlias = Union[OutlineItem, Destination] +# BookmarkTypes is deprecated. Use OutlineItemType instead +BookmarkTypes: TypeAlias = OutlineItemType # TODO: remove in version 3.0.0 FitType: TypeAlias = Literal[ "/Fit", "/XYZ", "/FitH", "/FitV", "/FitR", "/FitB", "/FitBH", "/FitBV" ] @@ -36,8 +38,9 @@ # OutlinesType = List[Union[Destination, "OutlinesType"]] # See https://github.com/python/mypy/issues/731 # Hence use this for the moment: -OutlinesType = List[Union[Destination, List[Union[Destination, List[Destination]]]]] - +OutlineType = List[Union[Destination, List[Union[Destination, List[Destination]]]]] +# OutlinesType is deprecated. Use OutlineType instead +OutlinesType: TypeAlias = OutlineType # TODO: remove in version 3.0.0 LayoutType: TypeAlias = Literal[ "/NoLayout", diff --git a/docs/user/extract-text.md b/docs/user/extract-text.md index 8415ee31c..30eaaf2a8 100644 --- a/docs/user/extract-text.md +++ b/docs/user/extract-text.md @@ -85,11 +85,11 @@ PyPDF2 might make mistakes parsing that. Hence I would distinguish three types of PDF documents: * **Digitally-born PDF files**: The file was created digitally on the computer. - It can contain images, texts, links, bookmarks, JavaScript, ... + It can contain images, texts, links, outline items (a.k.a., bookmarks), JavaScript, ... If you Zoom in a lot, the text still looks sharp. * **Scanned PDF files**: Any number of pages was scanned. The images were then stored in a PDF file. Hence the file is just a container for those images. - You cannot copy the text, you don't have links, bookmarks, JavaScript. + You cannot copy the text, you don't have links, outline items, JavaScript. * **OCRed PDF files**: The scanner ran OCR software and put the recognized text in the background of the image. Hence you can copy the text, but it still looks like a scan. If you zoom in enough, you can recognize pixels. diff --git a/tests/bench.py b/tests/bench.py index d8f526ed9..b2856b909 100644 --- a/tests/bench.py +++ b/tests/bench.py @@ -71,14 +71,14 @@ def merge(): file_merger.append(reader) # PdfReader object: - file_merger.append(PyPDF2.PdfReader(pdf_path, "rb"), bookmark=True) + file_merger.append(PyPDF2.PdfReader(pdf_path, "rb"), outline_item=True) # File handle with open(pdf_path, "rb") as fh: file_merger.append(fh) - bookmark = file_merger.add_bookmark("A bookmark", 0) - file_merger.add_bookmark("deeper", 0, parent=bookmark) + outline_item = file_merger.add_outline_item("An outline item", 0) + file_merger.add_outline_item("deeper", 0, parent=outline_item) file_merger.add_metadata({"author": "Martin Thoma"}) file_merger.add_named_destination("title", 0) file_merger.set_page_layout("/SinglePage") @@ -88,12 +88,12 @@ def merge(): file_merger.write(tmp_path) file_merger.close() - # Check if bookmarks are correct + # Check if outline is correct reader = PyPDF2.PdfReader(tmp_path) assert [ - el.title for el in reader._get_outlines() if isinstance(el, Destination) + el.title for el in reader._get_outline() if isinstance(el, Destination) ] == [ - "A bookmark", + "An outline item", "Foo", "Bar", "Baz", diff --git a/tests/test_generic.py b/tests/test_generic.py index 1014c5d7c..234a4ee88 100644 --- a/tests/test_generic.py +++ b/tests/test_generic.py @@ -9,7 +9,6 @@ from PyPDF2.generic import ( AnnotationBuilder, ArrayObject, - Bookmark, BooleanObject, ByteStringObject, CheckboxRadioButtonAttributes, @@ -20,6 +19,7 @@ NameObject, NullObject, NumberObject, + OutlineItem, RectangleObject, TextStringObject, TreeObject, @@ -197,12 +197,12 @@ def test_destination_exception(): ) -def test_bookmark_write_to_stream(): +def test_outline_item_write_to_stream(): stream = BytesIO() - bm = Bookmark( + oi = OutlineItem( NameObject("title"), NullObject(), NameObject(TF.FIT_V), FloatObject(0) ) - bm.write_to_stream(stream, None) + oi.write_to_stream(stream, None) stream.seek(0, 0) assert stream.read() == b"<<\n/Title title\n/Dest [ null /FitV 0 ]\n>>" @@ -400,7 +400,7 @@ def test_remove_child_in_tree(): reader = PdfReader(pdf) writer = PdfWriter() writer.add_page(reader.pages[0]) - writer.add_bookmark("foo", pagenum=0) + writer.add_outline_item("foo", pagenum=0) obj = writer._objects[-1] tree.add_child(obj, writer) tree.remove_child(obj) diff --git a/tests/test_merger.py b/tests/test_merger.py index 2af7a0251..67d3f655e 100644 --- a/tests/test_merger.py +++ b/tests/test_merger.py @@ -30,7 +30,7 @@ def test_merge(): merger.append(outline) merger.append(pdf_path, pages=PyPDF2.pagerange.PageRange(slice(0, 0))) merger.append(pdf_forms) - merger.merge(0, pdf_path, import_bookmarks=False) + merger.merge(0, pdf_path, import_outline=False) # Merging an encrypted file reader = PyPDF2.PdfReader(pdf_pw) @@ -38,40 +38,46 @@ def test_merge(): merger.append(reader) # PdfReader object: - merger.append(PyPDF2.PdfReader(pdf_path), bookmark="foo") + merger.append(PyPDF2.PdfReader(pdf_path), outline_item="foo") # File handle with open(pdf_path, "rb") as fh: merger.append(fh) - bookmark = merger.add_bookmark("A bookmark", 0) - bm2 = merger.add_bookmark("deeper", 0, parent=bookmark, italic=True, bold=True) - merger.add_bookmark("Let's see", 2, bm2, (255, 255, 0), True, True, "/FitBV", 12) - merger.add_bookmark( - "The XYZ fit", 0, bookmark, (255, 0, 15), True, True, "/XYZ", 10, 20, 3 + outline_item = merger.add_outline_item("An outline item", 0) + oi2 = merger.add_outline_item( + "deeper", 0, parent=outline_item, italic=True, bold=True ) - merger.add_bookmark( - "The FitH fit", 0, bookmark, (255, 0, 15), True, True, "/FitH", 10 + merger.add_outline_item( + "Let's see", 2, oi2, (255, 255, 0), True, True, "/FitBV", 12 ) - merger.add_bookmark( - "The FitV fit", 0, bookmark, (255, 0, 15), True, True, "/FitV", 10 + merger.add_outline_item( + "The XYZ fit", 0, outline_item, (255, 0, 15), True, True, "/XYZ", 10, 20, 3 ) - merger.add_bookmark( - "The FitR fit", 0, bookmark, (255, 0, 15), True, True, "/FitR", 10, 20, 30, 40 + merger.add_outline_item( + "The FitH fit", 0, outline_item, (255, 0, 15), True, True, "/FitH", 10 ) - merger.add_bookmark("The FitB fit", 0, bookmark, (255, 0, 15), True, True, "/FitB") - merger.add_bookmark( - "The FitBH fit", 0, bookmark, (255, 0, 15), True, True, "/FitBH", 10 + merger.add_outline_item( + "The FitV fit", 0, outline_item, (255, 0, 15), True, True, "/FitV", 10 ) - merger.add_bookmark( - "The FitBV fit", 0, bookmark, (255, 0, 15), True, True, "/FitBV", 10 + merger.add_outline_item( + "The FitR fit", 0, outline_item, (255, 0, 15), True, True, "/FitR", 10, 20, 30, 40, + ) + merger.add_outline_item( + "The FitB fit", 0, outline_item, (255, 0, 15), True, True, "/FitB" + ) + merger.add_outline_item( + "The FitBH fit", 0, outline_item, (255, 0, 15), True, True, "/FitBH", 10 + ) + merger.add_outline_item( + "The FitBV fit", 0, outline_item, (255, 0, 15), True, True, "/FitBV", 10 ) - found_bm = merger.find_bookmark("nothing here") - assert found_bm is None + found_oi = merger.find_outline_item("nothing here") + assert found_oi is None - found_bm = merger.find_bookmark("foo") - assert found_bm == [9] + found_oi = merger.find_outline_item("foo") + assert found_oi == [9] merger.add_metadata({"author": "Martin Thoma"}) merger.add_named_destination("title", 0) @@ -82,12 +88,10 @@ def test_merge(): merger.write(tmp_path) merger.close() - # Check if bookmarks are correct + # Check if outline is correct reader = PyPDF2.PdfReader(tmp_path) - assert [ - el.title for el in reader._get_outlines() if isinstance(el, Destination) - ] == [ - "A bookmark", + assert [el.title for el in reader.outline if isinstance(el, Destination)] == [ + "An outline item", "Foo", "Bar", "Baz", @@ -147,11 +151,11 @@ def test_merge_write_closed_fh(): assert exc.value.args[0] == err_closed with pytest.raises(RuntimeError) as exc: - merger._write_bookmarks() + merger._write_outline() assert exc.value.args[0] == err_closed with pytest.raises(RuntimeError) as exc: - merger.add_bookmark("A bookmark", 0) + merger.add_outline_item("An outline item", 0) assert exc.value.args[0] == err_closed with pytest.raises(RuntimeError) as exc: @@ -195,7 +199,7 @@ def test_zoom_xyz_no_left(): os.remove("tmp-merger-do-not-commit.pdf") -def test_bookmark(): +def test_outline_item(): url = "https://corpora.tika.apache.org/base/docs/govdocs1/997/997511.pdf" name = "tika-997511.pdf" reader = PdfReader(BytesIO(get_pdf_from_url(url, name=name))) @@ -296,3 +300,26 @@ def test_iss1145(): name = "iss1145.pdf" merger = PdfMerger() merger.append(PdfReader(BytesIO(get_pdf_from_url(url, name=name)))) + + +def test_deprecate_bookmark_decorator_warning(): + reader = PdfReader( + os.path.join(RESOURCE_ROOT, "outlines-with-invalid-destinations.pdf") + ) + merger = PdfMerger() + with pytest.warns( + UserWarning, + match="import_bookmarks is deprecated as an argument. Use import_outline instead", + ): + merger.merge(0, reader, import_bookmarks=True) + + +@pytest.mark.filterwarnings("ignore::UserWarning") +def test_deprecate_bookmark_decorator_output(): + reader = PdfReader( + os.path.join(RESOURCE_ROOT, "outlines-with-invalid-destinations.pdf") + ) + merger = PdfMerger() + merger.merge(0, reader, import_bookmarks=True) + first_oi_title = 'Valid Destination: Action /GoTo Named Destination "section.1"' + assert merger.outline[0].title == first_oi_title diff --git a/tests/test_reader.py b/tests/test_reader.py index 419b1558c..84260a490 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -140,10 +140,10 @@ def test_get_attachments(src): (os.path.join(RESOURCE_ROOT, "crazyones.pdf"), 0), ], ) -def test_get_outlines(src, outline_elements): +def test_get_outline(src, outline_elements): reader = PdfReader(src) - outlines = reader._get_outlines() - assert len(outlines) == outline_elements + outline = reader.outline + assert len(outline) == outline_elements @pytest.mark.parametrize( @@ -524,10 +524,10 @@ def test_read_encrypted_without_decryption(): def test_get_destination_page_number(): src = os.path.join(RESOURCE_ROOT, "pdflatex-outline.pdf") reader = PdfReader(src) - outlines = reader._get_outlines() - for outline in outlines: - if not isinstance(outline, list): - reader.get_destination_page_number(outline) + outline = reader.outline + for outline_item in outline: + if not isinstance(outline_item, list): + reader.get_destination_page_number(outline_item) def test_do_not_get_stuck_on_large_files_without_start_xref(): @@ -560,7 +560,7 @@ def test_decrypt_when_no_id(): def test_reader_properties(): reader = PdfReader(os.path.join(RESOURCE_ROOT, "crazyones.pdf")) - assert reader.outlines == [] + assert reader.outline == [] assert len(reader.pages) == 1 assert reader.page_layout is None assert reader.page_mode is None @@ -575,18 +575,18 @@ def test_issue604(strict): """Test with invalid destinations""" # todo with open(os.path.join(RESOURCE_ROOT, "issue-604.pdf"), "rb") as f: pdf = None - bookmarks = None + outline = None if strict: pdf = PdfReader(f, strict=strict) with pytest.raises(PdfReadError) as exc, pytest.warns(PdfReadWarning): - bookmarks = pdf._get_outlines() + outline = pdf.outline if "Unknown Destination" not in exc.value.args[0]: raise Exception("Expected exception not raised") - return # bookmarks not correct + return # outline is not correct else: pdf = PdfReader(f, strict=strict) with pytest.warns(PdfReadWarning): - bookmarks = pdf._get_outlines() + outline = pdf.outline def get_dest_pages(x): if isinstance(x, list): @@ -596,10 +596,8 @@ def get_dest_pages(x): return pdf.get_destination_page_number(x) + 1 out = [] - for ( - b - ) in bookmarks: # b can be destination or a list:preferred to just print them - out.append(get_dest_pages(b)) + for oi in outline: # oi can be destination or a list:preferred to just print them + out.append(get_dest_pages(oi)) def test_decode_permissions(): @@ -853,33 +851,33 @@ def test_outline_color(): url = "https://corpora.tika.apache.org/base/docs/govdocs1/924/924546.pdf" name = "tika-924546.pdf" reader = PdfReader(BytesIO(get_pdf_from_url(url, name=name))) - assert reader.outlines[0].color == [0, 0, 1] + assert reader.outline[0].color == [0, 0, 1] def test_outline_font_format(): url = "https://corpora.tika.apache.org/base/docs/govdocs1/924/924546.pdf" name = "tika-924546.pdf" reader = PdfReader(BytesIO(get_pdf_from_url(url, name=name))) - assert reader.outlines[0].font_format == 2 + assert reader.outline[0].font_format == 2 -def get_outlines_property(outlines, attribute_name: str): +def get_outline_property(outline, attribute_name: str): results = [] - if isinstance(outlines, list): - for outline in outlines: - if isinstance(outline, Destination): - results.append(getattr(outline, attribute_name)) + if isinstance(outline, list): + for outline_item in outline: + if isinstance(outline_item, Destination): + results.append(getattr(outline_item, attribute_name)) else: - results.append(get_outlines_property(outline, attribute_name)) + results.append(get_outline_property(outline_item, attribute_name)) else: - raise ValueError(f"got {type(outlines)}") + raise ValueError(f"got {type(outline)}") return results def test_outline_title_issue_1121(): reader = PdfReader(EXTERNAL_ROOT / "014-outlines/mistitled_outlines_example.pdf") - assert get_outlines_property(reader.outlines, "title") == [ + assert get_outline_property(reader.outline, "title") == [ "First", [ "Second", @@ -925,7 +923,7 @@ def test_outline_title_issue_1121(): def test_outline_count(): reader = PdfReader(EXTERNAL_ROOT / "014-outlines/mistitled_outlines_example.pdf") - assert get_outlines_property(reader.outlines, "outline_count") == [ + assert get_outline_property(reader.outline, "outline_count") == [ 5, [ None, @@ -973,7 +971,7 @@ def test_outline_missing_title(): os.path.join(RESOURCE_ROOT, "outline-without-title.pdf"), strict=True ) with pytest.raises(PdfReadError) as exc: - reader.outlines + reader.outline assert exc.value.args[0].startswith("Outline Entry Missing /Title attribute:") @@ -997,24 +995,24 @@ def test_outline_with_missing_named_destination(): name = "tika-913678.pdf" reader = PdfReader(BytesIO(get_pdf_from_url(url, name=name))) # outline items in document reference a named destination that is not defined - assert reader.outlines[1][0].title.startswith("Report for 2002AZ3B: Microbial") + assert reader.outline[1][0].title.startswith("Report for 2002AZ3B: Microbial") def test_outline_with_empty_action(): url = "https://corpora.tika.apache.org/base/docs/govdocs1/924/924546.pdf" name = "tika-924546.pdf" reader = PdfReader(BytesIO(get_pdf_from_url(url, name=name))) - # outline (entitled Tables and Figures) utilize an empty action (/A) + # outline items (entitled Tables and Figures) utilize an empty action (/A) # that has no type or destination - assert reader.outlines[-4].title == "Tables" + assert reader.outline[-4].title == "Tables" -def test_outlines_with_invalid_destinations(): +def test_outline_with_invalid_destinations(): reader = PdfReader( os.path.join(RESOURCE_ROOT, "outlines-with-invalid-destinations.pdf") ) - # contains 9 outlines, 6 with invalid destinations caused by different malformations - assert len(reader.outlines) == 9 + # contains 9 outline items, 6 with invalid destinations caused by different malformations + assert len(reader.outline) == 9 def test_PdfReaderMultipleDefinitions(): diff --git a/tests/test_workflows.py b/tests/test_workflows.py index 9a9571dd7..868018a40 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -548,7 +548,7 @@ def test_image_extraction2(url, name): def test_get_outline(url, name): data = BytesIO(get_pdf_from_url(url, name=name)) reader = PdfReader(data) - reader.outlines + reader.outline @pytest.mark.parametrize( diff --git a/tests/test_writer.py b/tests/test_writer.py index 96a186cd1..67b4188de 100644 --- a/tests/test_writer.py +++ b/tests/test_writer.py @@ -44,23 +44,33 @@ def test_writer_operations(): assert exc.value.args == () writer.insert_page(page, 1) writer.insert_page(reader_outline.pages[0], 0) - writer.add_bookmark_destination(page) + writer.add_outline_item_destination(page) writer.remove_links() - writer.add_bookmark_destination(page) - bm = writer.add_bookmark( - "A bookmark", 0, None, (255, 0, 15), True, True, "/FitBV", 10 + writer.add_outline_item_destination(page) + oi = writer.add_outline_item( + "An outline item", 0, None, (255, 0, 15), True, True, "/FitBV", 10 ) - writer.add_bookmark( - "The XYZ fit", 0, bm, (255, 0, 15), True, True, "/XYZ", 10, 20, 3 + writer.add_outline_item( + "The XYZ fit", 0, oi, (255, 0, 15), True, True, "/XYZ", 10, 20, 3 ) - writer.add_bookmark("The FitH fit", 0, bm, (255, 0, 15), True, True, "/FitH", 10) - writer.add_bookmark("The FitV fit", 0, bm, (255, 0, 15), True, True, "/FitV", 10) - writer.add_bookmark( - "The FitR fit", 0, bm, (255, 0, 15), True, True, "/FitR", 10, 20, 30, 40 + writer.add_outline_item( + "The FitH fit", 0, oi, (255, 0, 15), True, True, "/FitH", 10 + ) + writer.add_outline_item( + "The FitV fit", 0, oi, (255, 0, 15), True, True, "/FitV", 10 + ) + writer.add_outline_item( + "The FitR fit", 0, oi, (255, 0, 15), True, True, "/FitR", 10, 20, 30, 40 + ) + writer.add_outline_item( + "The FitB fit", 0, oi, (255, 0, 15), True, True, "/FitB" + ) + writer.add_outline_item( + "The FitBH fit", 0, oi, (255, 0, 15), True, True, "/FitBH", 10 + ) + writer.add_outline_item( + "The FitBV fit", 0, oi, (255, 0, 15), True, True, "/FitBV", 10 ) - writer.add_bookmark("The FitB fit", 0, bm, (255, 0, 15), True, True, "/FitB") - writer.add_bookmark("The FitBH fit", 0, bm, (255, 0, 15), True, True, "/FitBH", 10) - writer.add_bookmark("The FitBV fit", 0, bm, (255, 0, 15), True, True, "/FitBV", 10) writer.add_blank_page() writer.add_uri(2, "https://example.com", RectangleObject([0, 0, 100, 100])) writer.add_link(2, 1, RectangleObject([0, 0, 100, 100])) @@ -305,20 +315,22 @@ def test_encrypt(use_128bit): os.remove(tmp_filename) -def test_add_bookmark(): +def test_add_outline_item(): reader = PdfReader(os.path.join(RESOURCE_ROOT, "pdflatex-outline.pdf")) writer = PdfWriter() for page in reader.pages: writer.add_page(page) - bookmark = writer.add_bookmark( - "A bookmark", 1, None, (255, 0, 15), True, True, "/Fit", 200, 0, None + outline_item = writer.add_outline_item( + "An outline item", 1, None, (255, 0, 15), True, True, "/Fit", 200, 0, None + ) + writer.add_outline_item( + "Another", 2, outline_item, None, False, False, "/Fit", 0, 0, None ) - writer.add_bookmark("Another", 2, bookmark, None, False, False, "/Fit", 0, 0, None) # write "output" to PyPDF2-output.pdf - tmp_filename = "dont_commit_bookmark.pdf" + tmp_filename = "dont_commit_outline_item.pdf" with open(tmp_filename, "wb") as output_stream: writer.write(output_stream) @@ -494,7 +506,7 @@ def test_sweep_indirect_references_nullobject_exception(): os.remove("tmp-merger-do-not-commit.pdf") -def test_write_bookmark_on_page_fitv(): +def test_write_outline_item_on_page_fitv(): url = "https://corpora.tika.apache.org/base/docs/govdocs1/922/922840.pdf" name = "tika-922840.pdf" reader = PdfReader(BytesIO(get_pdf_from_url(url, name=name))) @@ -602,3 +614,18 @@ def test_add_single_annotation(): # Cleanup os.remove(target) # remove for testing + + +def test_deprecate_bookmark_decorator(): + reader = PdfReader( + os.path.join(RESOURCE_ROOT, "outlines-with-invalid-destinations.pdf") + ) + page = reader.pages[0] + outline_item = reader.outline[0] + writer = PdfWriter() + writer.add_page(page) + with pytest.warns( + UserWarning, + match="bookmark is deprecated as an argument. Use outline_item instead", + ): + writer.add_outline_item_dict(bookmark=outline_item) From 8a27fa4eea0c072cd7c8718a4c04869223c31ef6 Mon Sep 17 00:00:00 2001 From: pubpub-zz <4083478+pubpub-zz@users.noreply.github.com> Date: Sat, 30 Jul 2022 08:37:42 +0200 Subject: [PATCH 065/130] ENH: Add capability to filter text extraction by orientation (#1175) Closes #1071 --- PyPDF2/_page.py | 130 +++++++++++++++++++++++++++++----------- tests/test_workflows.py | 79 +++++++++++++++++++++++- 2 files changed, 172 insertions(+), 37 deletions(-) diff --git a/PyPDF2/_page.py b/PyPDF2/_page.py index ff8ab4b1c..b1497a9c1 100644 --- a/PyPDF2/_page.py +++ b/PyPDF2/_page.py @@ -1106,6 +1106,7 @@ def _extract_text( self, obj: Any, pdf: Any, + orientations: Tuple[int, ...] = (0, 90, 180, 270), space_width: float = 200.0, content_key: Optional[str] = PG.CONTENTS, ) -> str: @@ -1117,6 +1118,9 @@ def _extract_text( this function, as it will change if this function is made more sophisticated. + :param Tuple[int, ...] orientations: list of orientations text_extraction will look for + default = (0, 90, 180, 270) + note: currently only 0(Up),90(turned Left), 180(upside Down), 270 (turned Right) :param float space_width: force default space width (if not extracted from font (default 200) :param Optional[str] content_key: indicate the default key where to extract data @@ -1195,7 +1199,7 @@ def current_spacewidth() -> float: return _space_width / 1000.0 def process_operation(operator: bytes, operands: List) -> None: - nonlocal cm_matrix, cm_stack, tm_matrix, tm_prev, output, text, char_scale, space_scale, _space_width, TL, font_size, cmap + nonlocal cm_matrix, cm_stack, tm_matrix, tm_prev, output, text, char_scale, space_scale, _space_width, TL, font_size, cmap, orientations check_crlf_space: bool = False # Table 5.4 page 405 if operator == b"BT": @@ -1301,34 +1305,37 @@ def process_operation(operator: bytes, operands: List) -> None: elif operator == b"Tj": check_crlf_space = True - if isinstance(operands[0], str): - text += operands[0] - else: - t: str = "" - tt: bytes = ( - encode_pdfdocencoding(operands[0]) - if isinstance(operands[0], str) - else operands[0] - ) - if isinstance(cmap[0], str): - try: - t = tt.decode( - cmap[0], "surrogatepass" - ) # apply str encoding - except Exception: # the data does not match the expectation, we use the alternative ; text extraction may not be good - t = tt.decode( - "utf-16-be" if cmap[0] == "charmap" else "charmap", - "surrogatepass", - ) # apply str encoding - else: # apply dict encoding - t = "".join( - [ - cmap[0][x] if x in cmap[0] else bytes((x,)).decode() - for x in tt - ] + m = mult(tm_matrix, cm_matrix) + o = orient(m) + if o in orientations: + if isinstance(operands[0], str): + text += operands[0] + else: + t: str = "" + tt: bytes = ( + encode_pdfdocencoding(operands[0]) + if isinstance(operands[0], str) + else operands[0] ) - - text += "".join([cmap[1][x] if x in cmap[1] else x for x in t]) + if isinstance(cmap[0], str): + try: + t = tt.decode( + cmap[0], "surrogatepass" + ) # apply str encoding + except Exception: # the data does not match the expectation, we use the alternative ; text extraction may not be good + t = tt.decode( + "utf-16-be" if cmap[0] == "charmap" else "charmap", + "surrogatepass", + ) # apply str encoding + else: # apply dict encoding + t = "".join( + [ + cmap[0][x] if x in cmap[0] else bytes((x,)).decode() + for x in tt + ] + ) + + text += "".join([cmap[1][x] if x in cmap[1] else x for x in t]) else: return None if check_crlf_space: @@ -1339,6 +1346,8 @@ def process_operation(operator: bytes, operands: List) -> None: k = math.sqrt(abs(m[0] * m[3]) + abs(m[1] * m[2])) f = font_size * k tm_prev = m + if o not in orientations: + return try: if o == 0: if deltaY < -0.8 * f: @@ -1418,7 +1427,7 @@ def process_operation(operator: bytes, operands: List) -> None: xobj = resources_dict["/XObject"] if xobj[operands[0]]["/Subtype"] != "/Image": # type: ignore # output += text - text = self.extract_xform_text(xobj[operands[0]], space_width) # type: ignore + text = self.extract_xform_text(xobj[operands[0]], orientations, space_width) # type: ignore output += text except Exception: warnings.warn( @@ -1433,7 +1442,12 @@ def process_operation(operator: bytes, operands: List) -> None: return output def extract_text( - self, Tj_sep: str = "", TJ_sep: str = "", space_width: float = 200.0 + self, + *args: Any, + Tj_sep: str = None, + TJ_sep: str = None, + orientations: Union[int, Tuple[int, ...]] = (0, 90, 180, 270), + space_width: float = 200.0, ) -> str: """ Locate all text drawing commands, in the order they are provided in the @@ -1445,15 +1459,59 @@ def extract_text( Do not rely on the order of text coming out of this function, as it will change if this function is made more sophisticated. - - :param space_width : force default space width (if not extracted from font (default 200) + :params obsolete/Depreciating Tj_sep, TJ_sep: kept for compatibility + :param orientations : (list of) orientations (of the characters) (default: (0,90,270,360)) + single int is equivalent to a singleton ( 0 == (0,) ) + note: currently only 0(Up),90(turned Left), 180(upside Down),270 (turned Right) + :param space_width : force default space width (if not extracted from font (default: 200) :return: The extracted text """ - return self._extract_text(self, self.pdf, space_width, PG.CONTENTS) + if len(args) >= 1: + if isinstance(args[0], str): + Tj_sep = args[0] + if len(args) >= 2: + if isinstance(args[1], str): + TJ_sep = args[1] + else: + raise TypeError(f"Invalid positional parameter {args[1]}") + if len(args) >= 3: + if isinstance(args[2], (tuple, int)): + orientations = args[2] + else: + raise TypeError(f"Invalid positional parameter {args[2]}") + if len(args) >= 4: + if isinstance(args[3], (float, int)): + space_width = args[3] + else: + raise TypeError(f"Invalid positional parameter {args[3]}") + elif isinstance(args[0], (tuple, int)): + orientations = args[0] + if len(args) >= 2: + if isinstance(args[1], (float, int)): + space_width = args[1] + else: + raise TypeError(f"Invalid positional parameter {args[1]}") + else: + raise TypeError(f"Invalid positional parameter {args[0]}") + if Tj_sep is not None or TJ_sep is not None: + warnings.warn( + "parameters Tj_Sep, TJ_sep depreciated, and will be removed in PyPDF2 3.0.0.", + DeprecationWarning, + ) + + if isinstance(orientations, int): + orientations = (orientations,) + + return self._extract_text( + self, self.pdf, orientations, space_width, PG.CONTENTS + ) def extract_xform_text( - self, xform: EncodedStreamObject, space_width: float = 200.0 + self, + xform: EncodedStreamObject, + orientations: Tuple[int, ...] = (0, 90, 270, 360), + space_width: float = 200.0, ) -> str: """ Extract text from an XObject. @@ -1462,7 +1520,7 @@ def extract_xform_text( :return: The extracted text """ - return self._extract_text(xform, self.pdf, space_width, None) + return self._extract_text(xform, self.pdf, orientations, space_width, None) def extractText( self, Tj_sep: str = "", TJ_sep: str = "" @@ -1473,7 +1531,7 @@ def extractText( Use :meth:`extract_text` instead. """ deprecate_with_replacement("extractText", "extract_text") - return self.extract_text(Tj_sep=Tj_sep, TJ_sep=TJ_sep) + return self.extract_text() def _get_fonts(self) -> Tuple[Set[str], Set[str]]: """ diff --git a/tests/test_workflows.py b/tests/test_workflows.py index 868018a40..64a101fdc 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -3,6 +3,7 @@ import sys from io import BytesIO from pathlib import Path +from re import findall import pytest @@ -44,7 +45,7 @@ def test_PdfReaderFileLoad(): with open(os.path.join(RESOURCE_ROOT, "crazyones.txt"), "rb") as pdftext_file: pdftext = pdftext_file.read() - text = page.extract_text(Tj_sep="", TJ_sep="").encode("utf-8") + text = page.extract_text().encode("utf-8") # Compare the text of the PDF to a known source for expected_line, actual_line in zip(text.split(b"\n"), pdftext.split(b"\n")): @@ -209,6 +210,82 @@ def test_extract_textbench(enable, url, pages, print_result=False): pass +def test_orientations(): + p = PdfReader(os.path.join(RESOURCE_ROOT, "test Orient.pdf")).pages[0] + try: + p.extract_text("", "") + except DeprecationWarning: + pass + else: + raise Exception("DeprecationWarning expected") + try: + p.extract_text("", "", 0) + except DeprecationWarning: + pass + else: + raise Exception("DeprecationWarning expected") + try: + p.extract_text("", "", 0, 200) + except DeprecationWarning: + pass + else: + raise Exception("DeprecationWarning expected") + + try: + p.extract_text(Tj_sep="", TJ_sep="") + except DeprecationWarning: + pass + else: + raise Exception("DeprecationWarning expected") + assert findall("\\((.)\\)", p.extract_text()) == ["T", "B", "L", "R"] + try: + p.extract_text(None) + except Exception: + pass + else: + raise Exception("Argument 1 check invalid") + try: + p.extract_text("", 0) + except Exception: + pass + else: + raise Exception("Argument 2 check invalid") + try: + p.extract_text("", "", None) + except Exception: + pass + else: + raise Exception("Argument 3 check invalid") + try: + p.extract_text("", "", 0, "") + except Exception: + pass + else: + raise Exception("Argument 4 check invalid") + try: + p.extract_text(0, "") + except Exception: + pass + else: + raise Exception("Argument 1 new syntax check invalid") + + p.extract_text(0, 0) + p.extract_text(orientations=0) + + for (req, rst) in ( + (0, ["T"]), + (90, ["L"]), + (180, ["B"]), + (270, ["R"]), + ((0,), ["T"]), + ((0, 180), ["T", "B"]), + ((45,), []), + ): + assert ( + findall("\\((.)\\)", p.extract_text(req)) == rst + ), f"extract_text({req}) => {rst}" + + @pytest.mark.parametrize( ("base_path", "overlay_path"), [ From 89033cb37aec3a520da01b95da0a8fdd8dcf38fb Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 31 Jul 2022 11:07:56 +0200 Subject: [PATCH 066/130] STY: Apply pre-commit (#1188) --- PyPDF2/_merger.py | 11 +++++++++-- PyPDF2/_utils.py | 11 ++++++++++- PyPDF2/_writer.py | 6 +++--- tests/test_merger.py | 12 +++++++++++- tests/test_reader.py | 3 ++- tests/test_writer.py | 4 +--- 6 files changed, 36 insertions(+), 11 deletions(-) diff --git a/PyPDF2/_merger.py b/PyPDF2/_merger.py index 78ee8c6bf..f317c260f 100644 --- a/PyPDF2/_merger.py +++ b/PyPDF2/_merger.py @@ -31,7 +31,12 @@ from ._encryption import Encryption from ._page import PageObject from ._reader import PdfReader -from ._utils import StrByteType, deprecate_with_replacement, deprecate_bookmark, str_ +from ._utils import ( + StrByteType, + deprecate_bookmark, + deprecate_with_replacement, + str_, +) from ._writer import PdfWriter from .constants import GoToActionArguments from .constants import PagesAttributes as PA @@ -426,7 +431,9 @@ def _trim_outline( if outline_item["/Page"] is None: continue if pdf.pages[j].get_object() == outline_item["/Page"].get_object(): - outline_item[NameObject("/Page")] = outline_item["/Page"].get_object() + outline_item[NameObject("/Page")] = outline_item[ + "/Page" + ].get_object() new_outline.append(outline_item) prev_header_added = True break diff --git a/PyPDF2/_utils.py b/PyPDF2/_utils.py index 6d80832e0..72e676693 100644 --- a/PyPDF2/_utils.py +++ b/PyPDF2/_utils.py @@ -41,7 +41,16 @@ FileIO, ) from os import SEEK_CUR -from typing import Any, Callable, Dict, Optional, Pattern, Tuple, Union, overload +from typing import ( + Any, + Callable, + Dict, + Optional, + Pattern, + Tuple, + Union, + overload, +) try: # Python 3.10+: https://www.python.org/dev/peps/pep-0484/ diff --git a/PyPDF2/_writer.py b/PyPDF2/_writer.py index 579812554..f7d5376bd 100644 --- a/PyPDF2/_writer.py +++ b/PyPDF2/_writer.py @@ -58,8 +58,8 @@ StreamType, _get_max_pdf_version_header, b_, - deprecate_with_replacement, deprecate_bookmark, + deprecate_with_replacement, ) from .constants import AnnotationDictionaryAttributes from .constants import CatalogAttributes as CA @@ -83,7 +83,6 @@ BooleanObject, ByteStringObject, ContentStream, - _create_outline_item, DecodedStreamObject, Destination, DictionaryObject, @@ -97,14 +96,15 @@ StreamObject, TextStringObject, TreeObject, + _create_outline_item, create_string_object, ) from .types import ( BorderArrayType, FitType, LayoutType, - PagemodeType, OutlineItemType, + PagemodeType, ZoomArgsType, ZoomArgType, ) diff --git a/tests/test_merger.py b/tests/test_merger.py index 67d3f655e..560d78a0d 100644 --- a/tests/test_merger.py +++ b/tests/test_merger.py @@ -61,7 +61,17 @@ def test_merge(): "The FitV fit", 0, outline_item, (255, 0, 15), True, True, "/FitV", 10 ) merger.add_outline_item( - "The FitR fit", 0, outline_item, (255, 0, 15), True, True, "/FitR", 10, 20, 30, 40, + "The FitR fit", + 0, + outline_item, + (255, 0, 15), + True, + True, + "/FitR", + 10, + 20, + 30, + 40, ) merger.add_outline_item( "The FitB fit", 0, outline_item, (255, 0, 15), True, True, "/FitB" diff --git a/tests/test_reader.py b/tests/test_reader.py index 84260a490..cb4063780 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -596,7 +596,8 @@ def get_dest_pages(x): return pdf.get_destination_page_number(x) + 1 out = [] - for oi in outline: # oi can be destination or a list:preferred to just print them + # oi can be destination or a list:preferred to just print them + for oi in outline: out.append(get_dest_pages(oi)) diff --git a/tests/test_writer.py b/tests/test_writer.py index 67b4188de..55358af81 100644 --- a/tests/test_writer.py +++ b/tests/test_writer.py @@ -62,9 +62,7 @@ def test_writer_operations(): writer.add_outline_item( "The FitR fit", 0, oi, (255, 0, 15), True, True, "/FitR", 10, 20, 30, 40 ) - writer.add_outline_item( - "The FitB fit", 0, oi, (255, 0, 15), True, True, "/FitB" - ) + writer.add_outline_item("The FitB fit", 0, oi, (255, 0, 15), True, True, "/FitB") writer.add_outline_item( "The FitBH fit", 0, oi, (255, 0, 15), True, True, "/FitBH", 10 ) From 2a5a199757f97b0f85a08055414bf56871f23140 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 31 Jul 2022 11:19:25 +0200 Subject: [PATCH 067/130] MAINT: Consistant usage of warnings / log messages (#1164) --- PyPDF2/_cmap.py | 5 +- PyPDF2/_page.py | 7 +- PyPDF2/_reader.py | 41 +++++----- PyPDF2/_writer.py | 22 +++--- PyPDF2/generic.py | 10 +-- docs/user/suppress-warnings.md | 20 +++-- tests/__init__.py | 20 +++++ tests/bench.py | 3 - tests/test_filters.py | 13 ++-- tests/test_generic.py | 24 +++--- tests/test_merger.py | 6 +- tests/test_page.py | 13 ++-- tests/test_reader.py | 132 ++++++++++++++++++++++----------- tests/test_workflows.py | 15 ++-- 14 files changed, 206 insertions(+), 125 deletions(-) diff --git a/PyPDF2/_cmap.py b/PyPDF2/_cmap.py index c3acb6564..afce26088 100644 --- a/PyPDF2/_cmap.py +++ b/PyPDF2/_cmap.py @@ -3,6 +3,7 @@ from typing import Any, Dict, List, Tuple, Union, cast from ._codecs import adobe_glyphs, charset_encoding +from ._utils import logger_warning from .errors import PdfReadWarning from .generic import DecodedStreamObject, DictionaryObject @@ -330,9 +331,9 @@ def compute_space_width( st += 1 w = w[2:] else: - warnings.warn( + logger_warning( "unknown widths : \n" + (ft1["/W"]).__repr__(), - PdfReadWarning, + __name__, ) break try: diff --git a/PyPDF2/_page.py b/PyPDF2/_page.py index b1497a9c1..73ca6626e 100644 --- a/PyPDF2/_page.py +++ b/PyPDF2/_page.py @@ -51,11 +51,12 @@ TransformationMatrixType, deprecate_no_replacement, deprecate_with_replacement, + logger_warning, matrix_multiply, ) from .constants import PageAttributes as PG from .constants import Ressources as RES -from .errors import PageSizeNotDefinedError, PdfReadWarning +from .errors import PageSizeNotDefinedError from .generic import ( ArrayObject, ContentStream, @@ -1430,9 +1431,9 @@ def process_operation(operator: bytes, operands: List) -> None: text = self.extract_xform_text(xobj[operands[0]], orientations, space_width) # type: ignore output += text except Exception: - warnings.warn( + logger_warning( f" impossible to decode XFormObject {operands[0]}", - PdfReadWarning, + __name__, ) finally: text = "" diff --git a/PyPDF2/_reader.py b/PyPDF2/_reader.py index 80125d9d0..03c5bac9e 100644 --- a/PyPDF2/_reader.py +++ b/PyPDF2/_reader.py @@ -30,7 +30,6 @@ import os import re import struct -import warnings import zlib from io import BytesIO from pathlib import Path @@ -54,6 +53,7 @@ b_, deprecate_no_replacement, deprecate_with_replacement, + logger_warning, read_non_whitespace, read_previous_line, read_until_whitespace, @@ -70,7 +70,7 @@ from .constants import PageAttributes as PG from .constants import PagesAttributes as PA from .constants import TrailerKeys as TK -from .errors import PdfReadError, PdfReadWarning, PdfStreamError +from .errors import PdfReadError, PdfStreamError from .generic import ( ArrayObject, ContentStream, @@ -258,10 +258,10 @@ def __init__( Dict[Any, Any] ] = None # map page indirect_ref number to Page Number if hasattr(stream, "mode") and "b" not in stream.mode: # type: ignore - warnings.warn( + logger_warning( "PdfReader stream/file object is not in binary mode. " "It may not be read correctly.", - PdfReadWarning, + __name__, ) if isinstance(stream, (str, Path)): with open(stream, "rb") as fh: @@ -836,7 +836,7 @@ def _build_destination( try: return Destination(title, page, typ, *array) # type: ignore except PdfReadError: - warnings.warn(f"Unknown destination: {title} {array}", PdfReadWarning) + logger_warning(f"Unknown destination: {title} {array}", __name__) if self.strict: raise # create a link to first Page @@ -1091,11 +1091,11 @@ def _get_object_from_stream( except PdfStreamError as exc: # Stream object cannot be read. Normally, a critical error, but # Adobe Reader doesn't complain, so continue (in strict mode?) - warnings.warn( + logger_warning( f"Invalid stream (index {i}) within object " f"{indirect_reference.idnum} {indirect_reference.generation}: " f"{exc}", - PdfReadWarning, + __name__, ) if self.strict: @@ -1162,10 +1162,10 @@ def get_object(self, indirect_reference: IndirectObject) -> Optional[PdfObject]: retval, indirect_reference.idnum, indirect_reference.generation ) else: - warnings.warn( + logger_warning( f"Object {indirect_reference.idnum} {indirect_reference.generation} " "not defined.", - PdfReadWarning, + __name__, ) if self.strict: raise PdfReadError("Could not find object.") @@ -1207,9 +1207,9 @@ def read_object_header(self, stream: StreamType) -> Tuple[int, int]: read_non_whitespace(stream) stream.seek(-1, 1) if extra and self.strict: - warnings.warn( + logger_warning( f"Superfluous whitespace found in object header {idnum} {generation}", # type: ignore - PdfReadWarning, + __name__, ) return int(idnum), int(generation) @@ -1250,7 +1250,7 @@ def cache_indirect_object( if self.strict: raise PdfReadError(msg) else: - warnings.warn(msg) + logger_warning(msg, __name__) self.resolved_objects[(generation, idnum)] = obj return obj @@ -1276,8 +1276,8 @@ def read(self, stream: StreamType) -> None: if self.strict and xref_issue_nr: raise PdfReadError("Broken xref table") else: - warnings.warn( - f"incorrect startxref pointer({xref_issue_nr})", PdfReadWarning + logger_warning( + f"incorrect startxref pointer({xref_issue_nr})", __name__ ) # read all cross reference tables and their trailers @@ -1335,7 +1335,7 @@ def _find_startxref_pos(self, stream: StreamType) -> int: if not line.startswith(b"startxref"): raise PdfReadError("startxref not found") startxref = int(line[9:].strip()) - warnings.warn("startxref on same line as offset", PdfReadWarning) + logger_warning("startxref on same line as offset", __name__) else: line = read_previous_line(stream) if line[:9] != b"startxref": @@ -1355,9 +1355,9 @@ def _read_standard_xref_table(self, stream: StreamType) -> None: if firsttime and num != 0: self.xref_index = num if self.strict: - warnings.warn( + logger_warning( "Xref table not zero-indexed. ID numbers for objects will be corrected.", - PdfReadWarning, + __name__, ) # if table not zero indexed, could be due to error from when PDF was created # which will lead to mismatched indices later on, only warned and corrected if self.strict==True @@ -1474,9 +1474,10 @@ def _read_xref_other_error( "/Prev=0 in the trailer (try opening with strict=False)" ) else: - warnings.warn( + logger_warning( "/Prev=0 in the trailer - assuming there" - " is no previous xref table" + " is no previous xref table", + __name__, ) return None # bad xref character at startxref. Let's see if we can find @@ -1502,7 +1503,7 @@ def _read_xref_other_error( # no xref table found at specified location if "/Root" in self.trailer and not self.strict: # if Root has been already found, just raise warning - warnings.warn("Invalid parent xref., rebuild xref", PdfReadWarning) + logger_warning("Invalid parent xref., rebuild xref", __name__) try: self._rebuild_xref_table(stream) return None diff --git a/PyPDF2/_writer.py b/PyPDF2/_writer.py index f7d5376bd..6f4c8db9c 100644 --- a/PyPDF2/_writer.py +++ b/PyPDF2/_writer.py @@ -35,7 +35,6 @@ import struct import time import uuid -import warnings from hashlib import md5 from typing import ( Any, @@ -49,8 +48,6 @@ cast, ) -from PyPDF2.errors import PdfReadWarning - from ._page import PageObject, _VirtualList from ._reader import PdfReader from ._security import _alg33, _alg34, _alg35 @@ -60,6 +57,7 @@ b_, deprecate_bookmark, deprecate_with_replacement, + logger_warning, ) from .constants import AnnotationDictionaryAttributes from .constants import CatalogAttributes as CA @@ -780,9 +778,10 @@ def write(self, stream: StreamType) -> None: the write method and the tell method, similar to a file object. """ if hasattr(stream, "mode") and "b" not in stream.mode: - warnings.warn( + logger_warning( f"File <{stream.name}> to write to is not in binary mode. " # type: ignore - "It may not be written to correctly." + "It may not be written to correctly.", + __name__, ) if not self._root: @@ -966,10 +965,10 @@ def _resolve_indirect_object(self, data: IndirectObject) -> IndirectObject: real_obj = data.pdf.get_object(data) if real_obj is None: - warnings.warn( + logger_warning( f"Unable to resolve [{data.__class__.__name__}: {data}], " "returning NullObject instead", - PdfReadWarning, + __name__, ) real_obj = NullObject() @@ -1703,8 +1702,9 @@ def _set_page_layout(self, layout: Union[NameObject, LayoutType]) -> None: """ if not isinstance(layout, NameObject): if layout not in self._valid_layouts: - warnings.warn( - f"Layout should be one of: {'', ''.join(self._valid_layouts)}" + logger_warning( + f"Layout should be one of: {'', ''.join(self._valid_layouts)}", + __name__, ) layout = NameObject(layout) self._root_object.update({NameObject("/PageLayout"): layout}) @@ -1803,7 +1803,9 @@ def set_page_mode(self, mode: PagemodeType) -> None: mode_name: NameObject = mode else: if mode not in self._valid_modes: - warnings.warn(f"Mode should be one of: {', '.join(self._valid_modes)}") + logger_warning( + f"Mode should be one of: {', '.join(self._valid_modes)}", __name__ + ) mode_name = NameObject(mode) self._root_object.update({NameObject("/PageMode"): mode_name}) diff --git a/PyPDF2/generic.py b/PyPDF2/generic.py index bfdeff6ef..0b79cdb50 100644 --- a/PyPDF2/generic.py +++ b/PyPDF2/generic.py @@ -35,7 +35,6 @@ import hashlib import logging import re -import warnings from enum import IntFlag from io import BytesIO from typing import ( @@ -74,12 +73,7 @@ from .constants import StreamAttributes as SA from .constants import TypArguments as TA from .constants import TypFitArguments as TF -from .errors import ( - STREAM_TRUNCATED_PREMATURELY, - PdfReadError, - PdfReadWarning, - PdfStreamError, -) +from .errors import STREAM_TRUNCATED_PREMATURELY, PdfReadError, PdfStreamError logger = logging.getLogger(__name__) ObjectPrefix = b"/<[tf(n%" @@ -813,7 +807,7 @@ def read_unsized_from_steam(stream: StreamType, pdf: Any) -> bytes: # PdfReader if pdf is not None and pdf.strict: raise PdfReadError(msg) else: - warnings.warn(msg, PdfReadWarning) + logger_warning(msg, __name__) pos = stream.tell() s = read_non_whitespace(stream) diff --git a/docs/user/suppress-warnings.md b/docs/user/suppress-warnings.md index 59662c3cf..70b66de6f 100644 --- a/docs/user/suppress-warnings.md +++ b/docs/user/suppress-warnings.md @@ -1,12 +1,20 @@ -# Suppress Warnings and Log messages +# Exceptions, Warnings, and Log messages PyPDF2 makes use of 3 mechanisms to show that something went wrong: -* **Exceptions**: Error-cases the client should explicitly handle. In the - `strict=True` mode, most log messages will become exceptions. This can be - useful in applications where you can force to user to fix the broken PDF. -* **Warnings**: Avoidable issues, such as using deprecated classes / functions / parameters -* **Log messages**: Nothing the client can do, but they should know it happened. +* **Log messages** are informative messages that can be used for post-mortem + analysis. Most of the time, users can ignore them. They come in different + *levels*, such as info / warning / error indicating the severity. + Examples are non-standard compliant PDF files which PyPDF2 can deal with. +* **Warnings** are avoidable issues, such as using deprecated classes / + functions / parameters. Another example is missing capabilities of PyPDF2. + In those cases, PyPDF2 users should adjust their code. Warnings + are issued by the `warnings` module - those are different from the log-level + "warning". +* **Exceptions** are error-cases that PyPDF2 users should explicitly handle. + In the `strict=True` mode, most log messages with the warning level will + become exceptions. This can be useful in applications where you can force to + user to fix the broken PDF. ## Exceptions diff --git a/tests/__init__.py b/tests/__init__.py index 56438877e..49b7b6c1a 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,6 +1,7 @@ import os import ssl import urllib.request +from typing import List def get_pdf_from_url(url: str, name: str) -> bytes: @@ -30,3 +31,22 @@ def get_pdf_from_url(url: str, name: str) -> bytes: with open(cache_path, "rb") as fp: data = fp.read() return data + + +def _strip_position(line: str) -> str: + """ + Remove the location information. + + The message + WARNING PyPDF2._reader:_utils.py:364 Xref table not zero-indexed. + + becomes + Xref table not zero-indexed. + """ + line = ".py".join(line.split(".py:")[1:]) + line = " ".join(line.split(" ")[1:]) + return line + + +def normalize_warnings(caplog_text: str) -> List[str]: + return [_strip_position(line) for line in caplog_text.strip().split("\n")] diff --git a/tests/bench.py b/tests/bench.py index b2856b909..dcb22f278 100644 --- a/tests/bench.py +++ b/tests/bench.py @@ -1,7 +1,5 @@ import os -import pytest - import PyPDF2 from PyPDF2 import PdfReader, Transformation from PyPDF2.generic import Destination @@ -127,7 +125,6 @@ def text_extraction(pdf_path): return text -@pytest.mark.filterwarnings("ignore::PyPDF2.errors.PdfReadWarning") def test_text_extraction(benchmark): file_path = os.path.join(SAMPLE_ROOT, "009-pdflatex-geotopo/GeoTopo.pdf") benchmark(text_extraction, file_path) diff --git a/tests/test_filters.py b/tests/test_filters.py index 3f69b642b..8733df0ac 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -1,11 +1,12 @@ import string from io import BytesIO from itertools import product as cartesian_product +from unittest.mock import patch import pytest from PyPDF2 import PdfReader -from PyPDF2.errors import PdfReadError, PdfReadWarning, PdfStreamError +from PyPDF2.errors import PdfReadError, PdfStreamError from PyPDF2.filters import ( ASCII85Decode, ASCIIHexDecode, @@ -198,14 +199,16 @@ def test_CCITTFaxDecode(): ) -def test_decompress_zlib_error(): +@patch("PyPDF2._reader.logger_warning") +def test_decompress_zlib_error(mock_logger_warning): url = "https://corpora.tika.apache.org/base/docs/govdocs1/952/952445.pdf" name = "tika-952445.pdf" - with pytest.warns(PdfReadWarning, match=r"incorrect startxref pointer\(3\)"): - reader = PdfReader(BytesIO(get_pdf_from_url(url, name=name))) + reader = PdfReader(BytesIO(get_pdf_from_url(url, name=name))) for page in reader.pages: page.extract_text() - # assert exc.value.args[0] == "Could not find xref table at specified location" + mock_logger_warning.assert_called_with( + "incorrect startxref pointer(3)", "PyPDF2._reader" + ) def test_lzw_decode_neg1(): diff --git a/tests/test_generic.py b/tests/test_generic.py index 234a4ee88..4a5f6dd91 100644 --- a/tests/test_generic.py +++ b/tests/test_generic.py @@ -1,11 +1,12 @@ import os from io import BytesIO +from unittest.mock import patch import pytest from PyPDF2 import PdfMerger, PdfReader, PdfWriter from PyPDF2.constants import TypFitArguments as TF -from PyPDF2.errors import PdfReadError, PdfReadWarning, PdfStreamError +from PyPDF2.errors import PdfReadError, PdfStreamError from PyPDF2.generic import ( AnnotationBuilder, ArrayObject, @@ -408,14 +409,17 @@ def test_remove_child_in_tree(): tree.empty_tree() -def test_dict_read_from_stream(): +def test_dict_read_from_stream(caplog): url = "https://corpora.tika.apache.org/base/docs/govdocs1/984/984877.pdf" name = "tika-984877.pdf" reader = PdfReader(BytesIO(get_pdf_from_url(url, name=name))) for page in reader.pages: - with pytest.warns(PdfReadWarning): - page.extract_text() + page.extract_text() + assert ( + "Multiple definitions in dictionary at byte 0x1084 for key /Length" + in caplog.text + ) def test_parse_content_stream_peek_percentage(): @@ -477,20 +481,22 @@ def test_bool_repr(): os.remove("tmp-fields-report.txt") -def test_issue_997(): +@patch("PyPDF2._reader.logger_warning") +def test_issue_997(mock_logger_warning): url = "https://github.com/py-pdf/PyPDF2/files/8908874/Exhibit_A-2_930_Enterprise_Zone_Tax_Credits_final.pdf" name = "gh-issue-997.pdf" merger = PdfMerger() merged_filename = "tmp-out.pdf" - with pytest.warns(PdfReadWarning, match="not defined"): - merger.append( - BytesIO(get_pdf_from_url(url, name=name)) - ) # here the error raises + merger.append(BytesIO(get_pdf_from_url(url, name=name))) # here the error raises with open(merged_filename, "wb") as f: merger.write(f) merger.close() + mock_logger_warning.assert_called_with( + "Overwriting cache for 0 4", "PyPDF2._reader" + ) + # cleanup os.remove(merged_filename) diff --git a/tests/test_merger.py b/tests/test_merger.py index 560d78a0d..5884ae6b0 100644 --- a/tests/test_merger.py +++ b/tests/test_merger.py @@ -288,14 +288,14 @@ def test_sweep_recursion2(url, name): os.remove("tmp-merger-do-not-commit.pdf") -def test_sweep_indirect_list_newobj_is_None(): +def test_sweep_indirect_list_newobj_is_None(caplog): url = "https://corpora.tika.apache.org/base/docs/govdocs1/906/906769.pdf" name = "tika-906769.pdf" reader = PdfReader(BytesIO(get_pdf_from_url(url, name=name))) merger = PdfMerger() merger.append(reader) - with pytest.warns(UserWarning, match="Object 21 0 not defined."): - merger.write("tmp-merger-do-not-commit.pdf") + merger.write("tmp-merger-do-not-commit.pdf") + assert "Object 21 0 not defined." in caplog.text reader2 = PdfReader("tmp-merger-do-not-commit.pdf") reader2.pages diff --git a/tests/test_page.py b/tests/test_page.py index 1c55e4ade..6cbc068ce 100644 --- a/tests/test_page.py +++ b/tests/test_page.py @@ -20,7 +20,7 @@ TextStringObject, ) -from . import get_pdf_from_url +from . import get_pdf_from_url, normalize_warnings TESTS_ROOT = os.path.abspath(os.path.dirname(__file__)) PROJECT_ROOT = os.path.dirname(TESTS_ROOT) @@ -273,15 +273,14 @@ def test_extract_text_page_pdf(url, name): page.extract_text() -def test_extract_text_page_pdf_impossible_decode_xform(): +def test_extract_text_page_pdf_impossible_decode_xform(caplog): url = "https://corpora.tika.apache.org/base/docs/govdocs1/972/972962.pdf" name = "tika-972962.pdf" reader = PdfReader(BytesIO(get_pdf_from_url(url, name=name))) - with pytest.warns( - PdfReadWarning, match="impossible to decode XFormObject /Meta203" - ): - for page in reader.pages: - page.extract_text() + for page in reader.pages: + page.extract_text() + warn_msgs = normalize_warnings(caplog.text) + assert warn_msgs == [" impossible to decode XFormObject /Meta203"] def test_extract_text_operator_t_star(): # L1266, L1267 diff --git a/tests/test_reader.py b/tests/test_reader.py index cb4063780..c45c168ec 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -15,7 +15,7 @@ from PyPDF2.filters import _xobj_to_image from PyPDF2.generic import Destination -from . import get_pdf_from_url +from . import get_pdf_from_url, normalize_warnings try: from Crypto.Cipher import AES # noqa: F401 @@ -189,24 +189,58 @@ def test_get_images(src, nb_images): @pytest.mark.parametrize( - ("strict", "with_prev_0", "startx_correction", "should_fail"), + ("strict", "with_prev_0", "startx_correction", "should_fail", "warning_msgs"), [ - (True, False, -1, False), # all nominal => no fail - (True, True, -1, True), # Prev=0 => fail expected - (False, False, -1, False), - (False, True, -1, False), # Prev =0 => no strict so tolerant - (True, False, 0, True), # error on startxref, in strict => fail expected - (True, True, 0, True), + ( + True, + False, + -1, + False, + [ + "startxref on same line as offset", + "Xref table not zero-indexed. " + "ID numbers for objects will be corrected.", + ], + ), # all nominal => no fail + (True, True, -1, True, ""), # Prev=0 => fail expected + ( + False, + False, + -1, + False, + ["startxref on same line as offset"], + ), + ( + False, + True, + -1, + False, + [ + "startxref on same line as offset", + "/Prev=0 in the trailer - assuming there is no previous xref table", + ], + ), # Prev =0 => no strict so tolerant + (True, False, 0, True, ""), # error on startxref, in strict => fail expected + (True, True, 0, True, ""), ( False, False, 0, False, + ["startxref on same line as offset", "incorrect startxref pointer(1)"], ), # error on startxref, but no strict => xref rebuilt,no fail - (False, True, 0, False), + ( + False, + True, + 0, + False, + ["startxref on same line as offset", "incorrect startxref pointer(1)"], + ), ], ) -def test_get_images_raw(strict, with_prev_0, startx_correction, should_fail): +def test_get_images_raw( + caplog, strict, with_prev_0, startx_correction, should_fail, warning_msgs +): pdf_data = ( b"%%PDF-1.7\n" b"1 0 obj << /Count 1 /Kids [4 0 R] /Type /Pages >> endobj\n" @@ -234,7 +268,8 @@ def test_get_images_raw(strict, with_prev_0, startx_correction, should_fail): pdf_data.find(b"4 0 obj"), pdf_data.find(b"5 0 obj"), b"/Prev 0 " if with_prev_0 else b"", - # startx_correction should be -1 due to double % at the beginning inducing an error on startxref computation + # startx_correction should be -1 due to double % at the beginning + # inducing an error on startxref computation pdf_data.find(b"xref") + startx_correction, ) pdf_stream = io.BytesIO(pdf_data) @@ -248,17 +283,18 @@ def test_get_images_raw(strict, with_prev_0, startx_correction, should_fail): == "/Prev=0 in the trailer (try opening with strict=False)" ) else: - with pytest.warns(PdfReadWarning): - PdfReader(pdf_stream, strict=strict) + PdfReader(pdf_stream, strict=strict) + assert normalize_warnings(caplog.text) == warning_msgs -def test_issue297(): +def test_issue297(caplog): path = os.path.join(RESOURCE_ROOT, "issue-297.pdf") - with pytest.raises(PdfReadError) as exc, pytest.warns(PdfReadWarning): + with pytest.raises(PdfReadError) as exc: reader = PdfReader(path, strict=True) + assert caplog.text == "" assert "Broken xref table" in exc.value.args[0] - with pytest.warns(PdfReadWarning): - reader = PdfReader(path, strict=False) + reader = PdfReader(path, strict=False) + assert normalize_warnings(caplog.text) == ["incorrect startxref pointer(1)"] reader.pages[0] @@ -469,7 +505,7 @@ def test_read_missing_startxref(): assert exc.value.args[0] == "startxref not found" -def test_read_unknown_zero_pages(): +def test_read_unknown_zero_pages(caplog): pdf_data = ( b"%%PDF-1.7\n" b"1 0 obj << /Count 1 /Kids [4 0 R] /Type /Pages >> endobj\n" @@ -500,14 +536,22 @@ def test_read_unknown_zero_pages(): pdf_data.find(b"xref") - 1, ) pdf_stream = io.BytesIO(pdf_data) - with pytest.warns(PdfReadWarning): - reader = PdfReader(pdf_stream, strict=True) + reader = PdfReader(pdf_stream, strict=True) + warnings = [ + "startxref on same line as offset", + "Xref table not zero-indexed. ID numbers for objects will be corrected.", + ] + assert normalize_warnings(caplog.text) == warnings with pytest.raises(PdfReadError) as exc, pytest.warns(PdfReadWarning): len(reader.pages) assert exc.value.args[0] == "Could not find object." - with pytest.warns(PdfReadWarning): - reader = PdfReader(pdf_stream, strict=False) + reader = PdfReader(pdf_stream, strict=False) + warnings += [ + "Object 5 1 not defined.", + "startxref on same line as offset", + ] + assert normalize_warnings(caplog.text) == warnings with pytest.raises(AttributeError) as exc, pytest.warns(PdfReadWarning): len(reader.pages) assert exc.value.args[0] == "'NoneType' object has no attribute 'get_object'" @@ -569,9 +613,9 @@ def test_reader_properties(): @pytest.mark.parametrize( "strict", - [(True), (False)], + [True, False], ) -def test_issue604(strict): +def test_issue604(caplog, strict): """Test with invalid destinations""" # todo with open(os.path.join(RESOURCE_ROOT, "issue-604.pdf"), "rb") as f: pdf = None @@ -585,8 +629,11 @@ def test_issue604(strict): return # outline is not correct else: pdf = PdfReader(f, strict=strict) - with pytest.warns(PdfReadWarning): - outline = pdf.outline + outline = pdf.outline + msg = [ + "Unknown destination: ms_Thyroid_2_2020_071520_watermarked.pdf [0, 1]" + ] + assert normalize_warnings(caplog.text) == msg def get_dest_pages(x): if isinstance(x, list): @@ -596,6 +643,7 @@ def get_dest_pages(x): return pdf.get_destination_page_number(x) + 1 out = [] + # oi can be destination or a list:preferred to just print them for oi in outline: out.append(get_dest_pages(oi)) @@ -702,13 +750,12 @@ def test_read_path(): assert len(reader.pages) == 1 -def test_read_not_binary_mode(): +def test_read_not_binary_mode(caplog): with open(os.path.join(RESOURCE_ROOT, "crazyones.pdf")) as f: msg = "PdfReader stream/file object is not in binary mode. It may not be read correctly." - with pytest.warns(PdfReadWarning, match=msg), pytest.raises( - io.UnsupportedOperation - ): + with pytest.raises(io.UnsupportedOperation): PdfReader(f) + assert normalize_warnings(caplog.text) == [msg] @pytest.mark.skipif(not HAS_PYCRYPTODOME, reason="No pycryptodome") @@ -721,24 +768,24 @@ def test_read_form_416(): assert len(fields) > 0 -def test_extract_text_xref_issue_2(): +def test_extract_text_xref_issue_2(caplog): # pdf/0264cf510015b2a4b395a15cb23c001e.pdf url = "https://corpora.tika.apache.org/base/docs/govdocs1/981/981961.pdf" - msg = r"incorrect startxref pointer\(2\)" - with pytest.warns(PdfReadWarning, match=msg): - reader = PdfReader(BytesIO(get_pdf_from_url(url, name="tika-981961.pdf"))) + msg = "incorrect startxref pointer(2)" + reader = PdfReader(BytesIO(get_pdf_from_url(url, name="tika-981961.pdf"))) for page in reader.pages: page.extract_text() + assert normalize_warnings(caplog.text) == [msg] -def test_extract_text_xref_issue_3(): +def test_extract_text_xref_issue_3(caplog): # pdf/0264cf510015b2a4b395a15cb23c001e.pdf url = "https://corpora.tika.apache.org/base/docs/govdocs1/977/977774.pdf" - msg = r"incorrect startxref pointer\(3\)" - with pytest.warns(PdfReadWarning, match=msg): - reader = PdfReader(BytesIO(get_pdf_from_url(url, name="tika-977774.pdf"))) + msg = "incorrect startxref pointer(3)" + reader = PdfReader(BytesIO(get_pdf_from_url(url, name="tika-977774.pdf"))) for page in reader.pages: page.extract_text() + assert normalize_warnings(caplog.text) == [msg] def test_extract_text_pdf15(): @@ -1016,11 +1063,12 @@ def test_outline_with_invalid_destinations(): assert len(reader.outline) == 9 -def test_PdfReaderMultipleDefinitions(): +def test_PdfReaderMultipleDefinitions(caplog): # iss325 url = "https://github.com/py-pdf/PyPDF2/files/9176644/multipledefs.pdf" name = "multipledefs.pdf" reader = PdfReader(BytesIO(get_pdf_from_url(url, name=name))) - with pytest.warns(PdfReadWarning) as w: - reader.pages[0].extract_text() - assert len(w) == 1 + reader.pages[0].extract_text() + assert normalize_warnings(caplog.text) == [ + "Multiple definitions in dictionary at byte 0xb5 for key /Group" + ] diff --git a/tests/test_workflows.py b/tests/test_workflows.py index 64a101fdc..6b6aeaa68 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -14,7 +14,7 @@ from PyPDF2.errors import PdfReadError, PdfReadWarning from PyPDF2.filters import _xobj_to_image -from . import get_pdf_from_url +from . import get_pdf_from_url, normalize_warnings TESTS_ROOT = os.path.abspath(os.path.dirname(__file__)) PROJECT_ROOT = os.path.dirname(TESTS_ROOT) @@ -424,14 +424,14 @@ def test_compress(url, name): ), ], ) -def test_get_fields_warns(url, name): +def test_get_fields_warns(caplog, url, name): data = BytesIO(get_pdf_from_url(url, name=name)) reader = PdfReader(data) with open("tmp.txt", "w") as fp: - with pytest.warns(PdfReadWarning, match="Object 2 0 not defined."): - retrieved_fields = reader.get_fields(fileobj=fp) + retrieved_fields = reader.get_fields(fileobj=fp) assert retrieved_fields == {} + assert normalize_warnings(caplog.text) == ["Object 2 0 not defined."] # Cleanup os.remove("tmp.txt") @@ -468,7 +468,7 @@ def test_scale_rectangle_indirect_object(): page.scale(sx=2, sy=3) -def test_merge_output(): +def test_merge_output(caplog): # Arrange base = os.path.join(RESOURCE_ROOT, "Seige_of_Vicksburg_Sample_OCR.pdf") crazy = os.path.join(RESOURCE_ROOT, "crazyones.pdf") @@ -478,8 +478,9 @@ def test_merge_output(): # Act merger = PdfMerger(strict=True) - with pytest.warns(PdfReadWarning): - merger.append(base) + merger.append(base) + msg = "Xref table not zero-indexed. ID numbers for objects will be corrected." + assert normalize_warnings(caplog.text) == [msg] merger.merge(1, crazy) stream = BytesIO() merger.write(stream) From ab01f14f9bdebecd4efe552093605d5bb81f42c5 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 31 Jul 2022 17:03:24 +0200 Subject: [PATCH 068/130] ENH: Add link annotation (#1189) * Add AnnotationBuilder.link(...) * Allow creating a RectangleObject from a RectangleObject. This is useful to create a copy or to ensure we have a RectangleObject with little code. * Deprecate `writer.add_link` by `writer.add_annotation(AnnotationBuilder.link(...))`. * Add test for reading an external link annotation. Closes #284 --- .gitignore | 1 + PyPDF2/_writer.py | 101 ++++++------------- PyPDF2/generic.py | 148 ++++++++++++++++++++++++++-- docs/user/adding-pdf-annotations.md | 44 +++++++++ sample-files | 2 +- tests/test_generic.py | 66 ++++++++++++- tests/test_page.py | 29 ++++++ tests/test_writer.py | 57 ++++++----- 8 files changed, 338 insertions(+), 110 deletions(-) diff --git a/.gitignore b/.gitignore index 97f93ad19..75ef18635 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ docs/_build/ # Files generated by some of the scripts dont_commit_*.pdf PyPDF2-output.pdf +annotated-pdf-link.pdf Image9.png PyPDF2_pdfLocation.txt diff --git a/PyPDF2/_writer.py b/PyPDF2/_writer.py index 6f4c8db9c..d9e716d85 100644 --- a/PyPDF2/_writer.py +++ b/PyPDF2/_writer.py @@ -77,6 +77,7 @@ from .constants import TrailerKeys as TK from .constants import TypFitArguments, UserAccessPermissions from .generic import ( + AnnotationBuilder, ArrayObject, BooleanObject, ByteStringObject, @@ -1555,84 +1556,28 @@ def add_link( fit: FitType = "/Fit", *args: ZoomArgType, ) -> None: - """ - Add an internal link from a rectangular area to the specified page. - - :param int pagenum: index of the page on which to place the link. - :param int pagedest: index of the page to which the link should go. - :param rect: :class:`RectangleObject` or array of four - integers specifying the clickable rectangular area - ``[xLL, yLL, xUR, yUR]``, or string in the form ``"[ xLL yLL xUR yUR ]"``. - :param border: if provided, an array describing border-drawing - properties. See the PDF spec for details. No border will be - drawn if this argument is omitted. - :param str fit: Page fit or 'zoom' option (see below). Additional arguments may need - to be supplied. Passing ``None`` will be read as a null value for that coordinate. - - .. list-table:: Valid ``zoom`` arguments (see Table 8.2 of the PDF 1.7 reference for details) - :widths: 50 200 - - * - /Fit - - No additional arguments - * - /XYZ - - [left] [top] [zoomFactor] - * - /FitH - - [top] - * - /FitV - - [left] - * - /FitR - - [left] [bottom] [right] [top] - * - /FitB - - No additional arguments - * - /FitBH - - [top] - * - /FitBV - - [left] - """ - pages_obj = cast(Dict[str, Any], self.get_object(self._pages)) - page_link = pages_obj[PA.KIDS][pagenum] - page_dest = pages_obj[PA.KIDS][pagedest] # TODO: switch for external link - page_ref = cast(Dict[str, Any], self.get_object(page_link)) - - border_arr: BorderArrayType - if border is not None: - border_arr = [NameObject(n) for n in border[:3]] - if len(border) == 4: - dash_pattern = ArrayObject([NameObject(n) for n in border[3]]) - border_arr.append(dash_pattern) - else: - border_arr = [NumberObject(0)] * 3 + deprecate_with_replacement( + "add_link", "add_annotation(AnnotationBuilder.link(...))" + ) if isinstance(rect, str): - rect = NameObject(rect) + rect = rect.strip()[1:-1] + rect = RectangleObject( + [float(num) for num in rect.split(" ") if len(num) > 0] + ) elif isinstance(rect, RectangleObject): pass else: rect = RectangleObject(rect) - zoom_args: ZoomArgsType = [ - NullObject() if a is None else NumberObject(a) for a in args - ] - dest = Destination( - NameObject("/LinkName"), page_dest, NameObject(fit), *zoom_args - ) # TODO: create a better name for the link - - lnk = DictionaryObject( - { - NameObject("/Type"): NameObject(PG.ANNOTS), - NameObject("/Subtype"): NameObject("/Link"), - NameObject("/P"): page_link, - NameObject("/Rect"): rect, - NameObject("/Border"): ArrayObject(border_arr), - NameObject("/Dest"): dest.dest_array, - } + annotation = AnnotationBuilder.link( + rect=rect, + border=border, + target_page_index=pagedest, + fit=fit, + fit_args=args, ) - lnk_ref = self._add_object(lnk) - - if PG.ANNOTS in page_ref: - page_ref[PG.ANNOTS].append(lnk_ref) - else: - page_ref[NameObject(PG.ANNOTS)] = ArrayObject([lnk_ref]) + return self.add_annotation(page_number=pagenum, annotation=annotation) def addLink( # pragma: no cover self, @@ -1648,7 +1593,9 @@ def addLink( # pragma: no cover Use :meth:`add_link` instead. """ - deprecate_with_replacement("addLink", "add_link") + deprecate_with_replacement( + "addLink", "add_annotation(AnnotationBuilder.link(...))", "4.0.0" + ) return self.add_link(pagenum, pagedest, rect, border, fit, *args) _valid_layouts = ( @@ -1873,6 +1820,18 @@ def add_annotation(self, page_number: int, annotation: Dict[str, Any]) -> None: page[NameObject("/Annots")] = ArrayObject() assert page.annotations is not None + # Internal link annotations need the correct object type for the + # destination + if to_add.get("/Subtype") == "/Link" and NameObject("/Dest") in to_add: + tmp = cast(dict, to_add[NameObject("/Dest")]) + dest = Destination( + NameObject("/LinkName"), + tmp["target_page_index"], + tmp["fit"], + *tmp["fit_args"], + ) + to_add[NameObject("/Dest")] = dest.dest_array + ind_obj = self._add_object(to_add) page.annotations.append(ind_obj) diff --git a/PyPDF2/generic.py b/PyPDF2/generic.py index 0b79cdb50..8fc597c0a 100644 --- a/PyPDF2/generic.py +++ b/PyPDF2/generic.py @@ -70,6 +70,7 @@ ) from .constants import CheckboxRadioButtonAttributes, FieldDictionaryAttributes from .constants import FilterTypes as FT +from .constants import PageAttributes as PG from .constants import StreamAttributes as SA from .constants import TypArguments as TA from .constants import TypFitArguments as TF @@ -1375,7 +1376,9 @@ class RectangleObject(ArrayObject): * :attr:`trimbox ` """ - def __init__(self, arr: Tuple[float, float, float, float]) -> None: + def __init__( + self, arr: Union["RectangleObject", Tuple[float, float, float, float]] + ) -> None: # must have four points assert len(arr) == 4 # automatically convert arr[x] into NumberObject(arr[x]) if necessary @@ -2075,10 +2078,12 @@ def hex_to_rgb(value: str) -> Tuple[float, float, float]: class AnnotationBuilder: + from .types import FitType, ZoomArgType + @staticmethod def free_text( text: str, - rect: Tuple[float, float, float, float], + rect: Union[RectangleObject, Tuple[float, float, float, float]], font: str = "Helvetica", bold: bool = False, italic: bool = False, @@ -2087,7 +2092,21 @@ def free_text( border_color: str = "000000", background_color: str = "ffffff", ) -> DictionaryObject: - """Add text in a rectangle to a page.""" + """ + Add text in a rectangle to a page. + + :param str text: Text to be added + :param :class:`RectangleObject` rect: or array of four + integers specifying the clickable rectangular area + ``[xLL, yLL, xUR, yUR]`` + :param str font: Name of the Font, e.g. 'Helvetica' + :param bool bold: Print the text in bold + :param bool italic: Print the text in italic + :param str font_size: How big the text will be, e.g. '14pt' + :param str font_color: Hex-string for the color + :param str border_color: Hex-string for the border color + :param str background_color: Hex-string for the background of the annotation + """ font_str = "font: " if bold is True: font_str = font_str + "bold " @@ -2124,18 +2143,20 @@ def free_text( def line( p1: Tuple[float, float], p2: Tuple[float, float], - rect: Tuple[float, float, float, float], + rect: Union[RectangleObject, Tuple[float, float, float, float]], text: str = "", title_bar: str = "", ) -> DictionaryObject: """ Draw a line on the PDF. - :param p1: First point - :param p2: Second point - :param rect: Rectangle - :param text: Text to be displayed as the line annotation - :param title_bar: Text to be displayed in the title bar of the + :param Tuple[float, float] p1: First point + :param Tuple[float, float] p2: Second point + :param :class:`RectangleObject` rect: or array of four + integers specifying the clickable rectangular area + ``[xLL, yLL, xUR, yUR]`` + :param str text: Text to be displayed as the line annotation + :param str title_bar: Text to be displayed in the title bar of the annotation; by convention this is the name of the author """ line_obj = DictionaryObject( @@ -2169,3 +2190,112 @@ def line( } ) return line_obj + + @staticmethod + def link( + rect: Union[RectangleObject, Tuple[float, float, float, float]], + border: Optional[ArrayObject] = None, + url: Optional[str] = None, + target_page_index: Optional[int] = None, + fit: FitType = "/Fit", + fit_args: Tuple[ZoomArgType, ...] = tuple(), + ) -> DictionaryObject: + """ + Add a link to the document. + + The link can either be an external link or an internal link. + + An external link requires the URL parameter. + An internal link requires the target_page_index, fit, and fit args. + + + :param :class:`RectangleObject` rect: or array of four + integers specifying the clickable rectangular area + ``[xLL, yLL, xUR, yUR]`` + :param border: if provided, an array describing border-drawing + properties. See the PDF spec for details. No border will be + drawn if this argument is omitted. + - horizontal corner radius, + - vertical corner radius, and + - border width + - Optionally: Dash + :param str url: Link to a website (if you want to make an external link) + :param int target_page_index: index of the page to which the link should go + (if you want to make an internal link) + :param str fit: Page fit or 'zoom' option (see below). Additional arguments may need + to be supplied. Passing ``None`` will be read as a null value for that coordinate. + :param Tuple[int, ...] fit_args: Parameters for the fit argument. + + + .. list-table:: Valid ``fit`` arguments (see Table 8.2 of the PDF 1.7 reference for details) + :widths: 50 200 + + * - /Fit + - No additional arguments + * - /XYZ + - [left] [top] [zoomFactor] + * - /FitH + - [top] + * - /FitV + - [left] + * - /FitR + - [left] [bottom] [right] [top] + * - /FitB + - No additional arguments + * - /FitBH + - [top] + * - /FitBV + - [left] + """ + from .types import BorderArrayType + + is_external = url is not None + is_internal = target_page_index is not None + if not is_external and not is_internal: + raise ValueError( + "Either 'url' or 'target_page_index' have to be provided. Both were None." + ) + if is_external and is_internal: + raise ValueError( + f"Either 'url' or 'target_page_index' have to be provided. url={url}, target_page_index={target_page_index}" + ) + + border_arr: BorderArrayType + if border is not None: + border_arr = [NameObject(n) for n in border[:3]] + if len(border) == 4: + dash_pattern = ArrayObject([NameObject(n) for n in border[3]]) + border_arr.append(dash_pattern) + else: + border_arr = [NumberObject(0)] * 3 + + link_obj = DictionaryObject( + { + NameObject("/Type"): NameObject(PG.ANNOTS), + NameObject("/Subtype"): NameObject("/Link"), + NameObject("/Rect"): RectangleObject(rect), + NameObject("/Border"): ArrayObject(border_arr), + } + ) + if is_external: + link_obj[NameObject("/A")] = DictionaryObject( + { + NameObject("/S"): NameObject("/URI"), + NameObject("/Type"): NameObject("/Action"), + NameObject("/URI"): TextStringObject(url), + } + ) + if is_internal: + fit_arg_ready = [ + NullObject() if a is None else NumberObject(a) for a in fit_args + ] + # This needs to be updated later! + dest_deferred = DictionaryObject( + { + "target_page_index": NumberObject(target_page_index), + "fit": NameObject(fit), + "fit_args": ArrayObject(fit_arg_ready), + } + ) + link_obj[NameObject("/Dest")] = dest_deferred + return link_obj diff --git a/docs/user/adding-pdf-annotations.md b/docs/user/adding-pdf-annotations.md index 78d130f21..890dfde1c 100644 --- a/docs/user/adding-pdf-annotations.md +++ b/docs/user/adding-pdf-annotations.md @@ -82,3 +82,47 @@ writer.add_annotation(page_number=0, annotation=annotation) with open("annotated-pdf.pdf", "wb") as fp: writer.write(fp) ``` + +## Link + +If you want to add a link, you can use +the {py:class}`AnnotationBuilder `: + +```python +pdf_path = os.path.join(RESOURCE_ROOT, "crazyones.pdf") +reader = PdfReader(pdf_path) +page = reader.pages[0] +writer = PdfWriter() +writer.add_page(page) + +# Add the line +annotation = AnnotationBuilder.link( + rect=(50, 550, 200, 650), + url="https://martin-thoma.com/", +) +writer.add_annotation(page_number=0, annotation=annotation) + +# Write the annotated file to disk +with open("annotated-pdf.pdf", "wb") as fp: + writer.write(fp) +``` + +You can also add internal links: + +```python +pdf_path = os.path.join(RESOURCE_ROOT, "crazyones.pdf") +reader = PdfReader(pdf_path) +page = reader.pages[0] +writer = PdfWriter() +writer.add_page(page) + +# Add the line +annotation = AnnotationBuilder.link( + rect=(50, 550, 200, 650), target_page_index=3, fit="/FitH", fit_args=(123,) +) +writer.add_annotation(page_number=0, annotation=annotation) + +# Write the annotated file to disk +with open("annotated-pdf.pdf", "wb") as fp: + writer.write(fp) +``` diff --git a/sample-files b/sample-files index 200644f72..b6f4ff3de 160000 --- a/sample-files +++ b/sample-files @@ -1 +1 @@ -Subproject commit 200644f7219811c3930ad1732ef70c570ece2d16 +Subproject commit b6f4ff3de00745783d79f25cb8803901d1f20d28 diff --git a/tests/test_generic.py b/tests/test_generic.py index 4a5f6dd91..11871ae03 100644 --- a/tests/test_generic.py +++ b/tests/test_generic.py @@ -510,7 +510,7 @@ def test_annotation_builder_free_text(): writer.add_page(page) # Act - annotation = AnnotationBuilder.free_text( + free_text_annotation = AnnotationBuilder.free_text( "Hello World\nThis is the second line!", rect=(50, 550, 200, 650), font="Arial", @@ -521,7 +521,7 @@ def test_annotation_builder_free_text(): border_color="0000ff", background_color="cdcdcd", ) - writer.add_annotation(0, annotation) + writer.add_annotation(0, free_text_annotation) # Assert: You need to inspect the file manually target = "annotated-pdf.pd" @@ -540,13 +540,13 @@ def test_annotation_builder_line(): writer.add_page(page) # Act - annotation = AnnotationBuilder.line( + line_annotation = AnnotationBuilder.line( text="Hello World\nLine2", rect=(50, 550, 200, 650), p1=(50, 550), p2=(200, 650), ) - writer.add_annotation(0, annotation) + writer.add_annotation(0, line_annotation) # Assert: You need to inspect the file manually target = "annotated-pdf.pd" @@ -556,5 +556,63 @@ def test_annotation_builder_line(): os.remove(target) # comment this out for manual inspection +def test_annotation_builder_link(): + # Arrange + pdf_path = os.path.join(RESOURCE_ROOT, "outline-without-title.pdf") + reader = PdfReader(pdf_path) + page = reader.pages[0] + writer = PdfWriter() + writer.add_page(page) + + # Act + # Part 1: Too many args + with pytest.raises(ValueError) as exc: + AnnotationBuilder.link( + rect=(50, 550, 200, 650), + url="https://martin-thoma.com/", + target_page_index=3, + ) + assert ( + exc.value.args[0] + == "Either 'url' or 'target_page_index' have to be provided. url=https://martin-thoma.com/, target_page_index=3" + ) + + # Part 2: Too few args + with pytest.raises(ValueError) as exc: + AnnotationBuilder.link( + rect=(50, 550, 200, 650), + ) + assert ( + exc.value.args[0] + == "Either 'url' or 'target_page_index' have to be provided. Both were None." + ) + + # Part 3: External Link + link_annotation = AnnotationBuilder.link( + rect=(50, 50, 100, 100), + url="https://martin-thoma.com/", + border=[1, 0, 6, [3, 2]], + ) + writer.add_annotation(0, link_annotation) + + # Part 4: Internal Link + link_annotation = AnnotationBuilder.link( + rect=(100, 100, 300, 200), + target_page_index=1, + border=[50, 10, 4], + ) + writer.add_annotation(0, link_annotation) + + for page in reader.pages[1:]: + writer.add_page(page) + + # Assert: You need to inspect the file manually + target = "annotated-pdf-link.pdf" + with open(target, "wb") as fp: + writer.write(fp) + + # os.remove(target) # comment this out for manual inspection + + def test_CheckboxRadioButtonAttributes_opt(): assert "/Opt" in CheckboxRadioButtonAttributes.attributes_dict() diff --git a/tests/test_page.py b/tests/test_page.py index 6cbc068ce..0ca50c0eb 100644 --- a/tests/test_page.py +++ b/tests/test_page.py @@ -469,3 +469,32 @@ def test_empyt_password_1088(): def test_arab_text_extraction(): reader = PdfReader(EXTERNAL_ROOT / "015-arabic/habibi.pdf") assert reader.pages[0].extract_text() == "habibi حَبيبي" + + +def test_read_link_annotation(): + reader = PdfReader(EXTERNAL_ROOT / "016-libre-office-link/libre-office-link.pdf") + assert len(reader.pages[0].annotations) == 1 + annot = dict(reader.pages[0].annotations[0].get_object()) + expected = { + "/Type": "/Annot", + "/Subtype": "/Link", + "/A": DictionaryObject( + { + "/S": "/URI", + "/Type": "/Action", + "/URI": "https://martin-thoma.com/", + } + ), + "/Border": ArrayObject([0, 0, 0]), + "/Rect": [ + 92.043, + 771.389, + 217.757, + 785.189, + ], + } + + assert set(expected.keys()) == set(annot.keys()) + del expected["/Rect"] + del annot["/Rect"] + assert annot == expected diff --git a/tests/test_writer.py b/tests/test_writer.py index 55358af81..900fedb68 100644 --- a/tests/test_writer.py +++ b/tests/test_writer.py @@ -71,7 +71,8 @@ def test_writer_operations(): ) writer.add_blank_page() writer.add_uri(2, "https://example.com", RectangleObject([0, 0, 100, 100])) - writer.add_link(2, 1, RectangleObject([0, 0, 100, 100])) + with pytest.warns(PendingDeprecationWarning): + writer.add_link(2, 1, RectangleObject([0, 0, 100, 100])) assert writer._get_page_layout() is None writer._set_page_layout("/SinglePage") assert writer._get_page_layout() == "/SinglePage" @@ -418,30 +419,36 @@ def test_add_link(): from PyPDF2.generic import RectangleObject - writer.add_link( - 1, - 2, - RectangleObject([0, 0, 100, 100]), - border=[1, 2, 3, [4]], - fit="/Fit", - ) - writer.add_link(2, 3, RectangleObject([20, 30, 50, 80]), [1, 2, 3], "/FitH", None) - writer.add_link( - 3, - 0, - "[ 200 300 250 350 ]", - [0, 0, 0], - "/XYZ", - 0, - 0, - 2, - ) - writer.add_link( - 3, - 0, - [100, 200, 150, 250], - border=[0, 0, 0], - ) + with pytest.warns( + PendingDeprecationWarning, + match="add_link is deprecated and will be removed in PyPDF2", + ): + writer.add_link( + 1, + 2, + RectangleObject([0, 0, 100, 100]), + border=[1, 2, 3, [4]], + fit="/Fit", + ) + writer.add_link( + 2, 3, RectangleObject([20, 30, 50, 80]), [1, 2, 3], "/FitH", None + ) + writer.add_link( + 3, + 0, + "[ 200 300 250 350 ]", + [0, 0, 0], + "/XYZ", + 0, + 0, + 2, + ) + writer.add_link( + 3, + 0, + [100, 200, 150, 250], + border=[0, 0, 0], + ) # write "output" to PyPDF2-output.pdf tmp_filename = "dont_commit_link.pdf" From 42ae3127528a5edfecce504ab685cdd942700f54 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 31 Jul 2022 20:55:52 +0200 Subject: [PATCH 069/130] ENH: Add support for pathlib.Path in PdfMerger.merge (#1190) Replace many os.path usages with pathlib --- PyPDF2/_merger.py | 9 +++-- tests/bench.py | 21 +++++----- tests/test_basic_features.py | 9 +++-- tests/test_encryption.py | 20 +++++----- tests/test_generic.py | 15 +++---- tests/test_javascript.py | 10 ++--- tests/test_merger.py | 31 +++++++-------- tests/test_page.py | 46 +++++++++++----------- tests/test_reader.py | 76 +++++++++++++++++------------------- tests/test_utils.py | 7 ++-- tests/test_workflows.py | 44 ++++++++++----------- tests/test_writer.py | 45 +++++++++++---------- tests/test_xmp.py | 16 ++++---- 13 files changed, 170 insertions(+), 179 deletions(-) diff --git a/PyPDF2/_merger.py b/PyPDF2/_merger.py index f317c260f..db6bf0480 100644 --- a/PyPDF2/_merger.py +++ b/PyPDF2/_merger.py @@ -26,6 +26,7 @@ # POSSIBILITY OF SUCH DAMAGE. from io import BytesIO, FileIO, IOBase +from pathlib import Path from typing import Any, Dict, Iterable, List, Optional, Tuple, Union, cast from ._encryption import Encryption @@ -99,7 +100,7 @@ def __init__(self, strict: bool = False) -> None: def merge( self, position: int, - fileobj: Union[StrByteType, PdfReader], + fileobj: Union[Path, StrByteType, PdfReader], outline_item: Optional[str] = None, pages: Optional[PageRangeSpec] = None, import_outline: bool = True, @@ -184,7 +185,7 @@ def merge( self.pages[position:position] = srcpages def _create_stream( - self, fileobj: Union[StrByteType, PdfReader] + self, fileobj: Union[Path, StrByteType, PdfReader] ) -> Tuple[IOBase, bool, Optional[Encryption]]: # This parameter is passed to self.inputs.append and means # that the stream used was created in this method. @@ -198,7 +199,7 @@ def _create_stream( # If fileobj is none of the above types, it is not modified encryption_obj = None stream: IOBase - if isinstance(fileobj, str): + if isinstance(fileobj, (str, Path)): stream = FileIO(fileobj, "rb") my_file = True elif isinstance(fileobj, PdfReader): @@ -224,7 +225,7 @@ def _create_stream( @deprecate_bookmark(bookmark="outline_item", import_bookmarks="import_outline") def append( self, - fileobj: Union[StrByteType, PdfReader], + fileobj: Union[StrByteType, PdfReader, Path], outline_item: Optional[str] = None, pages: Union[None, PageRange, Tuple[int, int], Tuple[int, int, int]] = None, import_outline: bool = True, diff --git a/tests/bench.py b/tests/bench.py index dcb22f278..5072510cf 100644 --- a/tests/bench.py +++ b/tests/bench.py @@ -1,17 +1,18 @@ import os +from pathlib import Path import PyPDF2 from PyPDF2 import PdfReader, Transformation from PyPDF2.generic import Destination -TESTS_ROOT = os.path.abspath(os.path.dirname(__file__)) -PROJECT_ROOT = os.path.dirname(TESTS_ROOT) -RESOURCE_ROOT = os.path.join(PROJECT_ROOT, "resources") -SAMPLE_ROOT = os.path.join(PROJECT_ROOT, "sample-files") +TESTS_ROOT = Path(__file__).parent.resolve() +PROJECT_ROOT = TESTS_ROOT.parent +RESOURCE_ROOT = PROJECT_ROOT / "resources" +SAMPLE_ROOT = PROJECT_ROOT / "sample-files" def page_ops(pdf_path, password): - pdf_path = os.path.join(RESOURCE_ROOT, pdf_path) + pdf_path = RESOURCE_ROOT / pdf_path reader = PdfReader(pdf_path) @@ -50,10 +51,10 @@ def test_page_operations(benchmark): def merge(): - pdf_path = os.path.join(RESOURCE_ROOT, "crazyones.pdf") - outline = os.path.join(RESOURCE_ROOT, "pdflatex-outline.pdf") - pdf_forms = os.path.join(RESOURCE_ROOT, "pdflatex-forms.pdf") - pdf_pw = os.path.join(RESOURCE_ROOT, "libreoffice-writer-password.pdf") + pdf_path = RESOURCE_ROOT / "crazyones.pdf" + outline = RESOURCE_ROOT / "pdflatex-outline.pdf" + pdf_forms = RESOURCE_ROOT / "pdflatex-forms.pdf" + pdf_pw = RESOURCE_ROOT / "libreoffice-writer-password.pdf" file_merger = PyPDF2.PdfMerger() @@ -126,5 +127,5 @@ def text_extraction(pdf_path): def test_text_extraction(benchmark): - file_path = os.path.join(SAMPLE_ROOT, "009-pdflatex-geotopo/GeoTopo.pdf") + file_path = SAMPLE_ROOT / "009-pdflatex-geotopo/GeoTopo.pdf" benchmark(text_extraction, file_path) diff --git a/tests/test_basic_features.py b/tests/test_basic_features.py index 5a6d23bfd..bdc65d074 100644 --- a/tests/test_basic_features.py +++ b/tests/test_basic_features.py @@ -1,14 +1,15 @@ import os +from pathlib import Path from PyPDF2 import PdfReader, PdfWriter -TESTS_ROOT = os.path.abspath(os.path.dirname(__file__)) -PROJECT_ROOT = os.path.dirname(TESTS_ROOT) -RESOURCE_ROOT = os.path.join(PROJECT_ROOT, "resources") +TESTS_ROOT = Path(__file__).parent.resolve() +PROJECT_ROOT = TESTS_ROOT.parent +RESOURCE_ROOT = PROJECT_ROOT / "resources" def test_basic_features(): - pdf_path = os.path.join(RESOURCE_ROOT, "crazyones.pdf") + pdf_path = RESOURCE_ROOT / "crazyones.pdf" reader = PdfReader(pdf_path) writer = PdfWriter() diff --git a/tests/test_encryption.py b/tests/test_encryption.py index 234613d60..90c3ff4f7 100644 --- a/tests/test_encryption.py +++ b/tests/test_encryption.py @@ -1,4 +1,4 @@ -import os +from pathlib import Path import pytest @@ -14,9 +14,9 @@ except ImportError: HAS_PYCRYPTODOME = False -TESTS_ROOT = os.path.abspath(os.path.dirname(__file__)) -PROJECT_ROOT = os.path.dirname(TESTS_ROOT) -RESOURCE_ROOT = os.path.join(PROJECT_ROOT, "resources") +TESTS_ROOT = Path(__file__).parent.resolve() +PROJECT_ROOT = TESTS_ROOT.parent +RESOURCE_ROOT = PROJECT_ROOT / "resources" @pytest.mark.parametrize( @@ -51,7 +51,7 @@ ], ) def test_encryption(name, requres_pycryptodome): - inputfile = os.path.join(RESOURCE_ROOT, "encryption", name) + inputfile = RESOURCE_ROOT / "encryption" / name if requres_pycryptodome and not HAS_PYCRYPTODOME: with pytest.raises(DependencyError) as exc: ipdf = PyPDF2.PdfReader(inputfile) @@ -61,7 +61,7 @@ def test_encryption(name, requres_pycryptodome): return else: ipdf = PyPDF2.PdfReader(inputfile) - if inputfile.endswith("unencrypted.pdf"): + if str(inputfile).endswith("unencrypted.pdf"): assert not ipdf.is_encrypted else: assert ipdf.is_encrypted @@ -91,7 +91,7 @@ def test_encryption(name, requres_pycryptodome): def test_both_password(name, user_passwd, owner_passwd): from PyPDF2 import PasswordType - inputfile = os.path.join(RESOURCE_ROOT, "encryption", name) + inputfile = RESOURCE_ROOT / "encryption" / name ipdf = PyPDF2.PdfReader(inputfile) assert ipdf.is_encrypted assert ipdf.decrypt(user_passwd) == PasswordType.USER_PASSWORD @@ -113,7 +113,7 @@ def test_get_page_of_encrypted_file_new_algorithm(pdffile, password): This is a regression test for issue 327: IndexError for get_page() of decrypted file """ - path = os.path.join(RESOURCE_ROOT, pdffile) + path = RESOURCE_ROOT / pdffile PyPDF2.PdfReader(path, password=password).pages[0] @@ -133,7 +133,7 @@ def test_get_page_of_encrypted_file_new_algorithm(pdffile, password): @pytest.mark.skipif(not HAS_PYCRYPTODOME, reason="No pycryptodome") def test_encryption_merge(names): pdf_merger = PyPDF2.PdfMerger() - files = [os.path.join(RESOURCE_ROOT, "encryption", x) for x in names] + files = [RESOURCE_ROOT / "encryption" / x for x in names] pdfs = [PyPDF2.PdfReader(x) for x in files] for pdf in pdfs: if pdf.is_encrypted: @@ -157,7 +157,7 @@ def test_encrypt_decrypt_class(cryptcls): def test_decrypt_not_decrypted_pdf(): - path = os.path.join(RESOURCE_ROOT, "crazyones.pdf") + path = RESOURCE_ROOT / "crazyones.pdf" with pytest.raises(PdfReadError) as exc: PdfReader(path, password="nonexistant") assert exc.value.args[0] == "Not encrypted file" diff --git a/tests/test_generic.py b/tests/test_generic.py index 11871ae03..0ed2ee7d1 100644 --- a/tests/test_generic.py +++ b/tests/test_generic.py @@ -1,5 +1,6 @@ import os from io import BytesIO +from pathlib import Path from unittest.mock import patch import pytest @@ -33,9 +34,9 @@ from . import get_pdf_from_url -TESTS_ROOT = os.path.abspath(os.path.dirname(__file__)) -PROJECT_ROOT = os.path.dirname(TESTS_ROOT) -RESOURCE_ROOT = os.path.join(PROJECT_ROOT, "resources") +TESTS_ROOT = Path(__file__).parent.resolve() +PROJECT_ROOT = TESTS_ROOT.parent +RESOURCE_ROOT = PROJECT_ROOT / "resources" def test_float_object_exception(): @@ -395,7 +396,7 @@ def test_remove_child_not_in_tree(): def test_remove_child_in_tree(): - pdf = os.path.join(RESOURCE_ROOT, "form.pdf") + pdf = RESOURCE_ROOT / "form.pdf" tree = TreeObject() reader = PdfReader(pdf) @@ -503,7 +504,7 @@ def test_issue_997(mock_logger_warning): def test_annotation_builder_free_text(): # Arrange - pdf_path = os.path.join(RESOURCE_ROOT, "crazyones.pdf") + pdf_path = RESOURCE_ROOT / "crazyones.pdf" reader = PdfReader(pdf_path) page = reader.pages[0] writer = PdfWriter() @@ -533,7 +534,7 @@ def test_annotation_builder_free_text(): def test_annotation_builder_line(): # Arrange - pdf_path = os.path.join(RESOURCE_ROOT, "crazyones.pdf") + pdf_path = RESOURCE_ROOT / "crazyones.pdf" reader = PdfReader(pdf_path) page = reader.pages[0] writer = PdfWriter() @@ -558,7 +559,7 @@ def test_annotation_builder_line(): def test_annotation_builder_link(): # Arrange - pdf_path = os.path.join(RESOURCE_ROOT, "outline-without-title.pdf") + pdf_path = RESOURCE_ROOT / "outline-without-title.pdf" reader = PdfReader(pdf_path) page = reader.pages[0] writer = PdfWriter() diff --git a/tests/test_javascript.py b/tests/test_javascript.py index 83e08ff21..a63f0e47d 100644 --- a/tests/test_javascript.py +++ b/tests/test_javascript.py @@ -1,18 +1,18 @@ -import os +from pathlib import Path import pytest from PyPDF2 import PdfReader, PdfWriter # Configure path environment -TESTS_ROOT = os.path.abspath(os.path.dirname(__file__)) -PROJECT_ROOT = os.path.dirname(TESTS_ROOT) -RESOURCE_ROOT = os.path.join(PROJECT_ROOT, "resources") +TESTS_ROOT = Path(__file__).parent.resolve() +PROJECT_ROOT = TESTS_ROOT.parent +RESOURCE_ROOT = PROJECT_ROOT / "resources" @pytest.fixture() def pdf_file_writer(): - reader = PdfReader(os.path.join(RESOURCE_ROOT, "crazyones.pdf")) + reader = PdfReader(RESOURCE_ROOT / "crazyones.pdf") writer = PdfWriter() writer.append_pages_from_reader(reader) return writer diff --git a/tests/test_merger.py b/tests/test_merger.py index 5884ae6b0..f4cf78179 100644 --- a/tests/test_merger.py +++ b/tests/test_merger.py @@ -1,6 +1,7 @@ import os import sys from io import BytesIO +from pathlib import Path import pytest @@ -10,18 +11,18 @@ from . import get_pdf_from_url -TESTS_ROOT = os.path.abspath(os.path.dirname(__file__)) -PROJECT_ROOT = os.path.dirname(TESTS_ROOT) -RESOURCE_ROOT = os.path.join(PROJECT_ROOT, "resources") +TESTS_ROOT = Path(__file__).parent.resolve() +PROJECT_ROOT = TESTS_ROOT.parent +RESOURCE_ROOT = PROJECT_ROOT / "resources" -sys.path.append(PROJECT_ROOT) +sys.path.append(str(PROJECT_ROOT)) def test_merge(): - pdf_path = os.path.join(RESOURCE_ROOT, "crazyones.pdf") - outline = os.path.join(RESOURCE_ROOT, "pdflatex-outline.pdf") - pdf_forms = os.path.join(RESOURCE_ROOT, "pdflatex-forms.pdf") - pdf_pw = os.path.join(RESOURCE_ROOT, "libreoffice-writer-password.pdf") + pdf_path = RESOURCE_ROOT / "crazyones.pdf" + outline = RESOURCE_ROOT / "pdflatex-outline.pdf" + pdf_forms = RESOURCE_ROOT / "pdflatex-forms.pdf" + pdf_pw = RESOURCE_ROOT / "libreoffice-writer-password.pdf" merger = PyPDF2.PdfMerger() @@ -122,7 +123,7 @@ def test_merge(): def test_merge_page_exception(): merger = PyPDF2.PdfMerger() - pdf_path = os.path.join(RESOURCE_ROOT, "crazyones.pdf") + pdf_path = RESOURCE_ROOT / "crazyones.pdf" with pytest.raises(TypeError) as exc: merger.merge(0, pdf_path, pages="a:b") assert exc.value.args[0] == '"pages" must be a tuple of (start, stop[, step])' @@ -131,14 +132,14 @@ def test_merge_page_exception(): def test_merge_page_tuple(): merger = PyPDF2.PdfMerger() - pdf_path = os.path.join(RESOURCE_ROOT, "crazyones.pdf") + pdf_path = RESOURCE_ROOT / "crazyones.pdf" merger.merge(0, pdf_path, pages=(0, 1)) merger.close() def test_merge_write_closed_fh(): merger = PyPDF2.PdfMerger() - pdf_path = os.path.join(RESOURCE_ROOT, "crazyones.pdf") + pdf_path = RESOURCE_ROOT / "crazyones.pdf" merger.append(pdf_path) err_closed = "close() was called and thus the writer cannot be used anymore" @@ -313,9 +314,7 @@ def test_iss1145(): def test_deprecate_bookmark_decorator_warning(): - reader = PdfReader( - os.path.join(RESOURCE_ROOT, "outlines-with-invalid-destinations.pdf") - ) + reader = PdfReader(RESOURCE_ROOT / "outlines-with-invalid-destinations.pdf") merger = PdfMerger() with pytest.warns( UserWarning, @@ -326,9 +325,7 @@ def test_deprecate_bookmark_decorator_warning(): @pytest.mark.filterwarnings("ignore::UserWarning") def test_deprecate_bookmark_decorator_output(): - reader = PdfReader( - os.path.join(RESOURCE_ROOT, "outlines-with-invalid-destinations.pdf") - ) + reader = PdfReader(RESOURCE_ROOT / "outlines-with-invalid-destinations.pdf") merger = PdfMerger() merger.merge(0, reader, import_bookmarks=True) first_oi_title = 'Valid Destination: Action /GoTo Named Destination "section.1"' diff --git a/tests/test_page.py b/tests/test_page.py index 0ca50c0eb..3e7caa33b 100644 --- a/tests/test_page.py +++ b/tests/test_page.py @@ -22,10 +22,10 @@ from . import get_pdf_from_url, normalize_warnings -TESTS_ROOT = os.path.abspath(os.path.dirname(__file__)) -PROJECT_ROOT = os.path.dirname(TESTS_ROOT) -RESOURCE_ROOT = os.path.join(PROJECT_ROOT, "resources") -EXTERNAL_ROOT = Path(PROJECT_ROOT) / "sample-files" +TESTS_ROOT = Path(__file__).parent.resolve() +PROJECT_ROOT = TESTS_ROOT.parent +RESOURCE_ROOT = PROJECT_ROOT / "resources" +EXTERNAL_ROOT = PROJECT_ROOT / "sample-files" def get_all_sample_files(): @@ -77,7 +77,7 @@ def test_page_operations(pdf_path, password): if pdf_path.startswith("http"): pdf_path = BytesIO(get_pdf_from_url(pdf_path, pdf_path.split("/")[-1])) else: - pdf_path = os.path.join(RESOURCE_ROOT, pdf_path) + pdf_path = RESOURCE_ROOT / pdf_path reader = PdfReader(pdf_path) if password: @@ -99,11 +99,11 @@ def test_page_operations(pdf_path, password): def test_transformation_equivalence(): - pdf_path = os.path.join(RESOURCE_ROOT, "labeled-edges-center-image.pdf") + pdf_path = RESOURCE_ROOT / "labeled-edges-center-image.pdf" reader_base = PdfReader(pdf_path) page_base = reader_base.pages[0] - pdf_path = os.path.join(RESOURCE_ROOT, "box.pdf") + pdf_path = RESOURCE_ROOT / "box.pdf" reader_add = PdfReader(pdf_path) page_box = reader_add.pages[0] @@ -141,7 +141,7 @@ def compare_dict_objects(d1, d2): def test_page_transformations(): - pdf_path = os.path.join(RESOURCE_ROOT, "crazyones.pdf") + pdf_path = RESOURCE_ROOT / "crazyones.pdf" reader = PdfReader(pdf_path) page: PageObject = reader.pages[0] @@ -167,11 +167,11 @@ def test_page_transformations(): @pytest.mark.parametrize( ("pdf_path", "password"), [ - (os.path.join(RESOURCE_ROOT, "crazyones.pdf"), None), - (os.path.join(RESOURCE_ROOT, "attachment.pdf"), None), - (os.path.join(RESOURCE_ROOT, "side-by-side-subfig.pdf"), None), + (RESOURCE_ROOT / "crazyones.pdf", None), + (RESOURCE_ROOT / "attachment.pdf", None), + (RESOURCE_ROOT / "side-by-side-subfig.pdf", None), ( - os.path.join(RESOURCE_ROOT, "libreoffice-writer-password.pdf"), + RESOURCE_ROOT / "libreoffice-writer-password.pdf", "openpassword", ), ], @@ -185,7 +185,7 @@ def test_compress_content_streams(pdf_path, password): def test_page_properties(): - reader = PdfReader(os.path.join(RESOURCE_ROOT, "crazyones.pdf")) + reader = PdfReader(RESOURCE_ROOT / "crazyones.pdf") page = reader.pages[0] assert page.mediabox == RectangleObject((0, 0, 612, 792)) assert page.cropbox == RectangleObject((0, 0, 612, 792)) @@ -198,7 +198,7 @@ def test_page_properties(): def test_page_rotation_non90(): - reader = PdfReader(os.path.join(RESOURCE_ROOT, "crazyones.pdf")) + reader = PdfReader(RESOURCE_ROOT / "crazyones.pdf") page = reader.pages[0] with pytest.raises(ValueError) as exc: page.rotate(91) @@ -221,7 +221,7 @@ def test_add_transformation_on_page_without_contents(): def test_multi_language(): - reader = PdfReader(os.path.join(RESOURCE_ROOT, "multilang.pdf")) + reader = PdfReader(RESOURCE_ROOT / "multilang.pdf") txt = reader.pages[0].extract_text() assert "Hello World" in txt, "English not correctly extracted" # Arabic is for the moment left on side @@ -295,7 +295,7 @@ def test_extract_text_operator_t_star(): # L1266, L1267 ("pdf_path", "password", "embedded", "unembedded"), [ ( - os.path.join(RESOURCE_ROOT, "crazyones.pdf"), + RESOURCE_ROOT / "crazyones.pdf", None, { "/HHXGQB+SFTI1440", @@ -305,7 +305,7 @@ def test_extract_text_operator_t_star(): # L1266, L1267 set(), ), ( - os.path.join(RESOURCE_ROOT, "attachment.pdf"), + RESOURCE_ROOT / "attachment.pdf", None, { "/HHXGQB+SFTI1440", @@ -315,20 +315,20 @@ def test_extract_text_operator_t_star(): # L1266, L1267 set(), ), ( - os.path.join(RESOURCE_ROOT, "libreoffice-writer-password.pdf"), + RESOURCE_ROOT / "libreoffice-writer-password.pdf", "openpassword", {"/BAAAAA+DejaVuSans"}, set(), ), ( - os.path.join(RESOURCE_ROOT, "imagemagick-images.pdf"), + RESOURCE_ROOT / "imagemagick-images.pdf", None, set(), {"/Helvetica"}, ), - (os.path.join(RESOURCE_ROOT, "imagemagick-lzw.pdf"), None, set(), set()), + (RESOURCE_ROOT / "imagemagick-lzw.pdf", None, set(), set()), ( - os.path.join(RESOURCE_ROOT, "reportlab-inline-image.pdf"), + RESOURCE_ROOT / "reportlab-inline-image.pdf", None, set(), {"/Helvetica"}, @@ -347,7 +347,7 @@ def test_get_fonts(pdf_path, password, embedded, unembedded): def test_annotation_getter(): - pdf_path = os.path.join(RESOURCE_ROOT, "commented.pdf") + pdf_path = RESOURCE_ROOT / "commented.pdf" reader = PdfReader(pdf_path) annotations = reader.pages[0].annotations assert annotations is not None @@ -389,7 +389,7 @@ def test_annotation_getter(): def test_annotation_setter(): # Arange - pdf_path = os.path.join(RESOURCE_ROOT, "crazyones.pdf") + pdf_path = RESOURCE_ROOT / "crazyones.pdf" reader = PdfReader(pdf_path) page = reader.pages[0] writer = PdfWriter() diff --git a/tests/test_reader.py b/tests/test_reader.py index c45c168ec..4dcf38020 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -24,10 +24,10 @@ except ImportError: HAS_PYCRYPTODOME = False -TESTS_ROOT = os.path.abspath(os.path.dirname(__file__)) -PROJECT_ROOT = os.path.dirname(TESTS_ROOT) -RESOURCE_ROOT = os.path.join(PROJECT_ROOT, "resources") -EXTERNAL_ROOT = Path(PROJECT_ROOT) / "sample-files" +TESTS_ROOT = Path(__file__).parent.resolve() +PROJECT_ROOT = TESTS_ROOT.parent +RESOURCE_ROOT = PROJECT_ROOT / "resources" +EXTERNAL_ROOT = PROJECT_ROOT / "sample-files" @pytest.mark.parametrize( @@ -35,7 +35,7 @@ [("selenium-PyPDF2-issue-177.pdf", 1), ("pdflatex-outline.pdf", 4)], ) def test_get_num_pages(src, num_pages): - src = os.path.join(RESOURCE_ROOT, src) + src = RESOURCE_ROOT / src reader = PdfReader(src) assert len(reader.pages) == num_pages @@ -44,7 +44,7 @@ def test_get_num_pages(src, num_pages): ("pdf_path", "expected"), [ ( - os.path.join(RESOURCE_ROOT, "crazyones.pdf"), + RESOURCE_ROOT / "crazyones.pdf", { "/CreationDate": "D:20150604133406-06'00'", "/Creator": " XeTeX output 2015.06.04:1334", @@ -52,7 +52,7 @@ def test_get_num_pages(src, num_pages): }, ), ( - os.path.join(RESOURCE_ROOT, "metadata.pdf"), + RESOURCE_ROOT / "metadata.pdf", { "/CreationDate": "D:20220415093243+02'00'", "/ModDate": "D:20220415093243+02'00'", @@ -97,8 +97,8 @@ def test_read_metadata(pdf_path, expected): @pytest.mark.parametrize( "src", [ - (os.path.join(RESOURCE_ROOT, "crazyones.pdf")), - (os.path.join(RESOURCE_ROOT, "commented.pdf")), + RESOURCE_ROOT / "crazyones.pdf", + RESOURCE_ROOT / "commented.pdf", ], ) def test_get_annotations(src): @@ -115,8 +115,8 @@ def test_get_annotations(src): @pytest.mark.parametrize( "src", [ - (os.path.join(RESOURCE_ROOT, "attachment.pdf")), - (os.path.join(RESOURCE_ROOT, "crazyones.pdf")), + RESOURCE_ROOT / "attachment.pdf", + RESOURCE_ROOT / "crazyones.pdf", ], ) def test_get_attachments(src): @@ -136,8 +136,8 @@ def test_get_attachments(src): @pytest.mark.parametrize( ("src", "outline_elements"), [ - (os.path.join(RESOURCE_ROOT, "pdflatex-outline.pdf"), 9), - (os.path.join(RESOURCE_ROOT, "crazyones.pdf"), 0), + (RESOURCE_ROOT / "pdflatex-outline.pdf", 9), + (RESOURCE_ROOT / "crazyones.pdf", 0), ], ) def test_get_outline(src, outline_elements): @@ -158,7 +158,7 @@ def test_get_outline(src, outline_elements): ], ) def test_get_images(src, nb_images): - src = os.path.join(RESOURCE_ROOT, src) + src = RESOURCE_ROOT / src reader = PdfReader(src) with pytest.raises(TypeError): @@ -288,7 +288,7 @@ def test_get_images_raw( def test_issue297(caplog): - path = os.path.join(RESOURCE_ROOT, "issue-297.pdf") + path = RESOURCE_ROOT / "issue-297.pdf" with pytest.raises(PdfReadError) as exc: reader = PdfReader(path, strict=True) assert caplog.text == "" @@ -313,7 +313,7 @@ def test_get_page_of_encrypted_file(pdffile, password, should_fail): This is a regression test for issue 327: IndexError for get_page() of decrypted file """ - path = os.path.join(RESOURCE_ROOT, pdffile) + path = RESOURCE_ROOT / pdffile if should_fail: with pytest.raises(PdfReadError): PdfReader(path, password=password) @@ -348,7 +348,7 @@ def test_get_page_of_encrypted_file(pdffile, password, should_fail): ) def test_get_form(src, expected, expected_get_fields): """Check if we can read out form data.""" - src = os.path.join(RESOURCE_ROOT, src) + src = RESOURCE_ROOT / src reader = PdfReader(src) fields = reader.get_form_text_fields() assert fields == expected @@ -384,7 +384,7 @@ def test_get_form(src, expected, expected_get_fields): ], ) def test_get_page_number(src, page_nb): - src = os.path.join(RESOURCE_ROOT, src) + src = RESOURCE_ROOT / src reader = PdfReader(src) page = reader.pages[page_nb] assert reader.get_page_number(page) == page_nb @@ -395,7 +395,7 @@ def test_get_page_number(src, page_nb): [("form.pdf", None), ("AutoCad_Simple.pdf", "/SinglePage")], ) def test_get_page_layout(src, expected): - src = os.path.join(RESOURCE_ROOT, src) + src = RESOURCE_ROOT / src reader = PdfReader(src) assert reader.page_layout == expected @@ -408,7 +408,7 @@ def test_get_page_layout(src, expected): ], ) def test_get_page_mode(src, expected): - src = os.path.join(RESOURCE_ROOT, src) + src = RESOURCE_ROOT / src reader = PdfReader(src) assert reader.page_mode == expected @@ -558,7 +558,7 @@ def test_read_unknown_zero_pages(caplog): def test_read_encrypted_without_decryption(): - src = os.path.join(RESOURCE_ROOT, "libreoffice-writer-password.pdf") + src = RESOURCE_ROOT / "libreoffice-writer-password.pdf" reader = PdfReader(src) with pytest.raises(PdfReadError) as exc: len(reader.pages) @@ -566,7 +566,7 @@ def test_read_encrypted_without_decryption(): def test_get_destination_page_number(): - src = os.path.join(RESOURCE_ROOT, "pdflatex-outline.pdf") + src = RESOURCE_ROOT / "pdflatex-outline.pdf" reader = PdfReader(src) outline = reader.outline for outline_item in outline: @@ -594,16 +594,14 @@ def test_decrypt_when_no_id(): https://github.com/mstamy2/PyPDF2/issues/608 """ - with open( - os.path.join(RESOURCE_ROOT, "encrypted_doc_no_id.pdf"), "rb" - ) as inputfile: + with open(RESOURCE_ROOT / "encrypted_doc_no_id.pdf", "rb") as inputfile: ipdf = PdfReader(inputfile) ipdf.decrypt("") assert ipdf.metadata == {"/Producer": "European Patent Office"} def test_reader_properties(): - reader = PdfReader(os.path.join(RESOURCE_ROOT, "crazyones.pdf")) + reader = PdfReader(RESOURCE_ROOT / "crazyones.pdf") assert reader.outline == [] assert len(reader.pages) == 1 assert reader.page_layout is None @@ -617,7 +615,7 @@ def test_reader_properties(): ) def test_issue604(caplog, strict): """Test with invalid destinations""" # todo - with open(os.path.join(RESOURCE_ROOT, "issue-604.pdf"), "rb") as f: + with open(RESOURCE_ROOT / "issue-604.pdf", "rb") as f: pdf = None outline = None if strict: @@ -650,7 +648,7 @@ def get_dest_pages(x): def test_decode_permissions(): - reader = PdfReader(os.path.join(RESOURCE_ROOT, "crazyones.pdf")) + reader = PdfReader(RESOURCE_ROOT / "crazyones.pdf") base = { "accessability": False, "annotations": False, @@ -672,7 +670,7 @@ def test_decode_permissions(): def test_pages_attribute(): - pdf_path = os.path.join(RESOURCE_ROOT, "crazyones.pdf") + pdf_path = RESOURCE_ROOT / "crazyones.pdf" reader = PdfReader(pdf_path) # Test if getting as slice throws an error @@ -726,7 +724,7 @@ def test_iss925(): @pytest.mark.xfail(reason="#591") def test_extract_text_hello_world(): - reader = PdfReader(os.path.join(RESOURCE_ROOT, "hello-world.pdf")) + reader = PdfReader(RESOURCE_ROOT / "hello-world.pdf") text = reader.pages[0].extract_text().split("\n") assert text == [ "English:", @@ -751,7 +749,7 @@ def test_read_path(): def test_read_not_binary_mode(caplog): - with open(os.path.join(RESOURCE_ROOT, "crazyones.pdf")) as f: + with open(RESOURCE_ROOT / "crazyones.pdf") as f: msg = "PdfReader stream/file object is not in binary mode. It may not be read correctly." with pytest.raises(io.UnsupportedOperation): PdfReader(f) @@ -859,8 +857,8 @@ def test_get_fields_read_write_report(): @pytest.mark.parametrize( "src", [ - (os.path.join(RESOURCE_ROOT, "crazyones.pdf")), - (os.path.join(RESOURCE_ROOT, "commented.pdf")), + RESOURCE_ROOT / "crazyones.pdf", + RESOURCE_ROOT / "commented.pdf", ], ) def test_xfa(src): @@ -885,8 +883,8 @@ def test_xfa_non_empty(): @pytest.mark.parametrize( "src,pdf_header", [ - (os.path.join(RESOURCE_ROOT, "attachment.pdf"), "%PDF-1.5"), - (os.path.join(RESOURCE_ROOT, "crazyones.pdf"), "%PDF-1.5"), + (RESOURCE_ROOT / "attachment.pdf", "%PDF-1.5"), + (RESOURCE_ROOT / "crazyones.pdf", "%PDF-1.5"), ], ) def test_header(src, pdf_header): @@ -1015,9 +1013,7 @@ def test_outline_count(): def test_outline_missing_title(): - reader = PdfReader( - os.path.join(RESOURCE_ROOT, "outline-without-title.pdf"), strict=True - ) + reader = PdfReader(RESOURCE_ROOT / "outline-without-title.pdf", strict=True) with pytest.raises(PdfReadError) as exc: reader.outline assert exc.value.args[0].startswith("Outline Entry Missing /Title attribute:") @@ -1056,9 +1052,7 @@ def test_outline_with_empty_action(): def test_outline_with_invalid_destinations(): - reader = PdfReader( - os.path.join(RESOURCE_ROOT, "outlines-with-invalid-destinations.pdf") - ) + reader = PdfReader(RESOURCE_ROOT / "outlines-with-invalid-destinations.pdf") # contains 9 outline items, 6 with invalid destinations caused by different malformations assert len(reader.outline) == 9 diff --git a/tests/test_utils.py b/tests/test_utils.py index 27ff35712..954ae9d34 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,5 +1,6 @@ import io import os +from pathlib import Path import pytest @@ -17,9 +18,9 @@ ) from PyPDF2.errors import PdfStreamError -TESTS_ROOT = os.path.abspath(os.path.dirname(__file__)) -PROJECT_ROOT = os.path.dirname(TESTS_ROOT) -RESOURCE_ROOT = os.path.join(PROJECT_ROOT, "resources") +TESTS_ROOT = Path(__file__).parent.resolve() +PROJECT_ROOT = TESTS_ROOT.parent +RESOURCE_ROOT = PROJECT_ROOT / "resources" @pytest.mark.parametrize( diff --git a/tests/test_workflows.py b/tests/test_workflows.py index 6b6aeaa68..cbbf614f3 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -16,15 +16,15 @@ from . import get_pdf_from_url, normalize_warnings -TESTS_ROOT = os.path.abspath(os.path.dirname(__file__)) -PROJECT_ROOT = os.path.dirname(TESTS_ROOT) -RESOURCE_ROOT = os.path.join(PROJECT_ROOT, "resources") +TESTS_ROOT = Path(__file__).parent.resolve() +PROJECT_ROOT = TESTS_ROOT.parent +RESOURCE_ROOT = PROJECT_ROOT / "resources" -sys.path.append(PROJECT_ROOT) +sys.path.append(str(PROJECT_ROOT)) def test_dropdown_items(): - inputfile = os.path.join(RESOURCE_ROOT, "libreoffice-form.pdf") + inputfile = RESOURCE_ROOT / "libreoffice-form.pdf" reader = PdfReader(inputfile) fields = reader.get_fields() assert "/Opt" in fields["Nationality"].keys() @@ -36,13 +36,13 @@ def test_PdfReaderFileLoad(): textual output. Expected outcome: file loads, text matches expected. """ - with open(os.path.join(RESOURCE_ROOT, "crazyones.pdf"), "rb") as inputfile: + with open(RESOURCE_ROOT / "crazyones.pdf", "rb") as inputfile: # Load PDF file from file reader = PdfReader(inputfile) page = reader.pages[0] # Retrieve the text of the PDF - with open(os.path.join(RESOURCE_ROOT, "crazyones.txt"), "rb") as pdftext_file: + with open(RESOURCE_ROOT / "crazyones.txt", "rb") as pdftext_file: pdftext = pdftext_file.read() text = page.extract_text().encode("utf-8") @@ -63,12 +63,12 @@ def test_PdfReaderJpegImage(): textual output. Expected outcome: file loads, image matches expected. """ - with open(os.path.join(RESOURCE_ROOT, "jpeg.pdf"), "rb") as inputfile: + with open(RESOURCE_ROOT / "jpeg.pdf", "rb") as inputfile: # Load PDF file from file reader = PdfReader(inputfile) # Retrieve the text of the image - with open(os.path.join(RESOURCE_ROOT, "jpeg.txt")) as pdftext_file: + with open(RESOURCE_ROOT / "jpeg.txt") as pdftext_file: imagetext = pdftext_file.read() page = reader.pages[0] @@ -83,9 +83,7 @@ def test_PdfReaderJpegImage(): def test_decrypt(): - with open( - os.path.join(RESOURCE_ROOT, "libreoffice-writer-password.pdf"), "rb" - ) as inputfile: + with open(RESOURCE_ROOT / "libreoffice-writer-password.pdf", "rb") as inputfile: reader = PdfReader(inputfile) assert reader.is_encrypted is True reader.decrypt("openpassword") @@ -100,7 +98,7 @@ def test_decrypt(): def test_text_extraction_encrypted(): - inputfile = os.path.join(RESOURCE_ROOT, "libreoffice-writer-password.pdf") + inputfile = RESOURCE_ROOT / "libreoffice-writer-password.pdf" reader = PdfReader(inputfile) assert reader.is_encrypted is True reader.decrypt("openpassword") @@ -115,14 +113,14 @@ def test_text_extraction_encrypted(): @pytest.mark.parametrize("degree", [0, 90, 180, 270, 360, -90]) def test_rotate(degree): - with open(os.path.join(RESOURCE_ROOT, "crazyones.pdf"), "rb") as inputfile: + with open(RESOURCE_ROOT / "crazyones.pdf", "rb") as inputfile: reader = PdfReader(inputfile) page = reader.pages[0] page.rotate(degree) def test_rotate_45(): - with open(os.path.join(RESOURCE_ROOT, "crazyones.pdf"), "rb") as inputfile: + with open(RESOURCE_ROOT / "crazyones.pdf", "rb") as inputfile: reader = PdfReader(inputfile) page = reader.pages[0] with pytest.raises(ValueError) as exc: @@ -174,7 +172,7 @@ def test_rotate_45(): (True, "https://github.com/py-pdf/PyPDF2/files/8884469/999092.pdf", [0, 1]), ( True, - "file://" + os.path.join(RESOURCE_ROOT, "test Orient.pdf"), + "file://" + str(RESOURCE_ROOT / "test Orient.pdf"), [0], ), # TODO: preparation of text orientation validation ( @@ -211,7 +209,7 @@ def test_extract_textbench(enable, url, pages, print_result=False): def test_orientations(): - p = PdfReader(os.path.join(RESOURCE_ROOT, "test Orient.pdf")).pages[0] + p = PdfReader(RESOURCE_ROOT / "test Orient.pdf").pages[0] try: p.extract_text("", "") except DeprecationWarning: @@ -303,11 +301,11 @@ def test_overlay(base_path, overlay_path): if base_path.startswith("http"): base_path = BytesIO(get_pdf_from_url(base_path, name="tika-935981.pdf")) else: - base_path = os.path.join(PROJECT_ROOT, base_path) + base_path = PROJECT_ROOT / base_path reader = PdfReader(base_path) writer = PdfWriter() - reader_overlay = PdfReader(os.path.join(PROJECT_ROOT, overlay_path)) + reader_overlay = PdfReader(PROJECT_ROOT / overlay_path) overlay = reader_overlay.pages[0] for page in reader.pages: @@ -470,11 +468,9 @@ def test_scale_rectangle_indirect_object(): def test_merge_output(caplog): # Arrange - base = os.path.join(RESOURCE_ROOT, "Seige_of_Vicksburg_Sample_OCR.pdf") - crazy = os.path.join(RESOURCE_ROOT, "crazyones.pdf") - expected = os.path.join( - RESOURCE_ROOT, "Seige_of_Vicksburg_Sample_OCR-crazyones-merged.pdf" - ) + base = RESOURCE_ROOT / "Seige_of_Vicksburg_Sample_OCR.pdf" + crazy = RESOURCE_ROOT / "crazyones.pdf" + expected = RESOURCE_ROOT / "Seige_of_Vicksburg_Sample_OCR-crazyones-merged.pdf" # Act merger = PdfMerger(strict=True) diff --git a/tests/test_writer.py b/tests/test_writer.py index 900fedb68..0d78010ff 100644 --- a/tests/test_writer.py +++ b/tests/test_writer.py @@ -1,5 +1,6 @@ import os from io import BytesIO +from pathlib import Path import pytest @@ -9,13 +10,13 @@ from . import get_pdf_from_url -TESTS_ROOT = os.path.abspath(os.path.dirname(__file__)) -PROJECT_ROOT = os.path.dirname(TESTS_ROOT) -RESOURCE_ROOT = os.path.join(PROJECT_ROOT, "resources") +TESTS_ROOT = Path(__file__).parent.resolve() +PROJECT_ROOT = TESTS_ROOT.parent +RESOURCE_ROOT = PROJECT_ROOT / "resources" def test_writer_clone(): - src = os.path.join(RESOURCE_ROOT, "pdflatex-outline.pdf") + src = RESOURCE_ROOT / "pdflatex-outline.pdf" reader = PdfReader(src) writer = PdfWriter() @@ -31,8 +32,8 @@ def test_writer_operations(): This should be done way more thoroughly: It should be checked if the output is as expected. """ - pdf_path = os.path.join(RESOURCE_ROOT, "crazyones.pdf") - pdf_outline_path = os.path.join(RESOURCE_ROOT, "pdflatex-outline.pdf") + pdf_path = RESOURCE_ROOT / "crazyones.pdf" + pdf_outline_path = RESOURCE_ROOT / "pdflatex-outline.pdf" reader = PdfReader(pdf_path) reader_outline = PdfReader(pdf_outline_path) @@ -112,7 +113,7 @@ def test_writer_operations(): ], ) def test_remove_images(input_path, ignore_byte_string_object): - pdf_path = os.path.join(RESOURCE_ROOT, input_path) + pdf_path = RESOURCE_ROOT / input_path reader = PdfReader(pdf_path) writer = PdfWriter() @@ -146,7 +147,7 @@ def test_remove_images(input_path, ignore_byte_string_object): ], ) def test_remove_text(input_path, ignore_byte_string_object): - pdf_path = os.path.join(RESOURCE_ROOT, input_path) + pdf_path = RESOURCE_ROOT / input_path reader = PdfReader(pdf_path) writer = PdfWriter() @@ -235,7 +236,7 @@ def test_remove_text_all_operators(ignore_byte_string_object): def test_write_metadata(): - pdf_path = os.path.join(RESOURCE_ROOT, "crazyones.pdf") + pdf_path = RESOURCE_ROOT / "crazyones.pdf" reader = PdfReader(pdf_path) writer = PdfWriter() @@ -263,7 +264,7 @@ def test_write_metadata(): def test_fill_form(): - reader = PdfReader(os.path.join(RESOURCE_ROOT, "form.pdf")) + reader = PdfReader(RESOURCE_ROOT / "form.pdf") writer = PdfWriter() page = reader.pages[0] @@ -285,7 +286,7 @@ def test_fill_form(): [(True), (False)], ) def test_encrypt(use_128bit): - reader = PdfReader(os.path.join(RESOURCE_ROOT, "form.pdf")) + reader = PdfReader(RESOURCE_ROOT / "form.pdf") writer = PdfWriter() page = reader.pages[0] @@ -315,7 +316,7 @@ def test_encrypt(use_128bit): def test_add_outline_item(): - reader = PdfReader(os.path.join(RESOURCE_ROOT, "pdflatex-outline.pdf")) + reader = PdfReader(RESOURCE_ROOT / "pdflatex-outline.pdf") writer = PdfWriter() for page in reader.pages: @@ -338,7 +339,7 @@ def test_add_outline_item(): def test_add_named_destination(): - reader = PdfReader(os.path.join(RESOURCE_ROOT, "pdflatex-outline.pdf")) + reader = PdfReader(RESOURCE_ROOT / "pdflatex-outline.pdf") writer = PdfWriter() for page in reader.pages: @@ -368,7 +369,7 @@ def test_add_named_destination(): def test_add_uri(): - reader = PdfReader(os.path.join(RESOURCE_ROOT, "pdflatex-outline.pdf")) + reader = PdfReader(RESOURCE_ROOT / "pdflatex-outline.pdf") writer = PdfWriter() for page in reader.pages: @@ -411,7 +412,7 @@ def test_add_uri(): def test_add_link(): - reader = PdfReader(os.path.join(RESOURCE_ROOT, "pdflatex-outline.pdf")) + reader = PdfReader(RESOURCE_ROOT / "pdflatex-outline.pdf") writer = PdfWriter() for page in reader.pages: @@ -462,7 +463,7 @@ def test_add_link(): def test_io_streams(): """This is the example from the docs ("Streaming data").""" - filepath = os.path.join(RESOURCE_ROOT, "pdflatex-outline.pdf") + filepath = RESOURCE_ROOT / "pdflatex-outline.pdf" with open(filepath, "rb") as fh: bytes_stream = BytesIO(fh.read()) @@ -477,7 +478,7 @@ def test_io_streams(): def test_regression_issue670(): - filepath = os.path.join(RESOURCE_ROOT, "crazyones.pdf") + filepath = RESOURCE_ROOT / "crazyones.pdf" reader = PdfReader(filepath, strict=False) for _ in range(2): writer = PdfWriter() @@ -490,7 +491,7 @@ def test_issue301(): """ Test with invalid stream length object """ - with open(os.path.join(RESOURCE_ROOT, "issue-301.pdf"), "rb") as f: + with open(RESOURCE_ROOT / "issue-301.pdf", "rb") as f: reader = PdfReader(f) writer = PdfWriter() writer.append_pages_from_reader(reader) @@ -527,7 +528,7 @@ def test_pdf_header(): writer = PdfWriter() assert writer.pdf_header == b"%PDF-1.3" - reader = PdfReader(os.path.join(RESOURCE_ROOT, "crazyones.pdf")) + reader = PdfReader(RESOURCE_ROOT / "crazyones.pdf") writer.add_page(reader.pages[0]) assert writer.pdf_header == b"%PDF-1.5" @@ -590,7 +591,7 @@ def test_write_dict_stream_object(): def test_add_single_annotation(): - pdf_path = os.path.join(RESOURCE_ROOT, "crazyones.pdf") + pdf_path = RESOURCE_ROOT / "crazyones.pdf" reader = PdfReader(pdf_path) page = reader.pages[0] writer = PdfWriter() @@ -622,9 +623,7 @@ def test_add_single_annotation(): def test_deprecate_bookmark_decorator(): - reader = PdfReader( - os.path.join(RESOURCE_ROOT, "outlines-with-invalid-destinations.pdf") - ) + reader = PdfReader(RESOURCE_ROOT / "outlines-with-invalid-destinations.pdf") page = reader.pages[0] outline_item = reader.outline[0] writer = PdfWriter() diff --git a/tests/test_xmp.py b/tests/test_xmp.py index d7dfc4685..a53b27b0e 100644 --- a/tests/test_xmp.py +++ b/tests/test_xmp.py @@ -1,6 +1,6 @@ -import os from datetime import datetime from io import BytesIO +from pathlib import Path import pytest @@ -11,16 +11,16 @@ from . import get_pdf_from_url -TESTS_ROOT = os.path.abspath(os.path.dirname(__file__)) -PROJECT_ROOT = os.path.dirname(TESTS_ROOT) -RESOURCE_ROOT = os.path.join(PROJECT_ROOT, "resources") +TESTS_ROOT = Path(__file__).parent.resolve() +PROJECT_ROOT = TESTS_ROOT.parent +RESOURCE_ROOT = PROJECT_ROOT / "resources" @pytest.mark.parametrize( ("src", "has_xmp"), [ - (os.path.join(RESOURCE_ROOT, "commented-xmp.pdf"), True), - (os.path.join(RESOURCE_ROOT, "crazyones.pdf"), False), + (RESOURCE_ROOT / "commented-xmp.pdf", True), + (RESOURCE_ROOT / "crazyones.pdf", False), ], ) def test_read_xmp(src, has_xmp): @@ -74,7 +74,7 @@ def test_regression_issue774(): def test_regression_issue914(): - path = os.path.join(RESOURCE_ROOT, "issue-914-xmp-data.pdf") + path = RESOURCE_ROOT / "issue-914-xmp-data.pdf" reader = PdfReader(path) assert reader.xmp_metadata.xmp_modify_date == datetime(2022, 4, 9, 15, 22, 43) @@ -183,7 +183,7 @@ def test_issue585(): # class Tst: # to replace pdf # strict = False -# reader = PdfReader(os.path.join(RESOURCE_ROOT, "commented-xmp.pdf")) +# reader = PdfReader(RESOURCE_ROOT / "commented-xmp.pdf") # xmp_info = reader.xmp_metadata # # # # From 7c7ef7759e031aa0639d1abd496c94d0188bed92 Mon Sep 17 00:00:00 2001 From: mtd91429 Date: Sun, 31 Jul 2022 14:06:47 -0500 Subject: [PATCH 070/130] ENH: Add ability to add hex encoded colors to outline items (#1186) --- PyPDF2/_writer.py | 4 ++-- PyPDF2/generic.py | 23 +++++++++++------------ tests/test_writer.py | 23 +++++++++++++++++++++++ 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/PyPDF2/_writer.py b/PyPDF2/_writer.py index d9e716d85..6f54df2ff 100644 --- a/PyPDF2/_writer.py +++ b/PyPDF2/_writer.py @@ -1164,7 +1164,7 @@ def add_outline_item( title: str, pagenum: int, parent: Union[None, TreeObject, IndirectObject] = None, - color: Optional[Tuple[float, float, float]] = None, + color: Optional[Union[Tuple[float, float, float], str]] = None, bold: bool = False, italic: bool = False, fit: FitType = "/Fit", @@ -1178,7 +1178,7 @@ def add_outline_item( :param parent: A reference to a parent outline item to create nested outline items. :param tuple color: Color of the outline item's font as a red, green, blue tuple - from 0.0 to 1.0 + from 0.0 to 1.0 or as a Hex String (#RRGGBB) :param bool bold: Outline item font is bold :param bool italic: Outline item font is italic :param str fit: The fit of the destination page. See diff --git a/PyPDF2/generic.py b/PyPDF2/generic.py index 8fc597c0a..16c904a1c 100644 --- a/PyPDF2/generic.py +++ b/PyPDF2/generic.py @@ -2017,30 +2017,29 @@ def create_string_object( def _create_outline_item( action_ref: IndirectObject, title: str, - color: Optional[Tuple[float, float, float]], + color: Union[Tuple[float, float, float], str, None], italic: bool, bold: bool, ) -> TreeObject: outline_item = TreeObject() - outline_item.update( { NameObject("/A"): action_ref, NameObject("/Title"): create_string_object(title), } ) - - if color is not None: + if color: + if isinstance(color, str): + color = hex_to_rgb(color) outline_item.update( {NameObject("/C"): ArrayObject([FloatObject(c) for c in color])} ) - - format_flag = 0 - if italic: - format_flag += 1 - if bold: - format_flag += 2 - if format_flag: + if italic or bold: + format_flag = 0 + if italic: + format_flag += 1 + if bold: + format_flag += 2 outline_item.update({NameObject("/F"): NumberObject(format_flag)}) return outline_item @@ -2074,7 +2073,7 @@ def decode_pdfdocencoding(byte_array: bytes) -> str: def hex_to_rgb(value: str) -> Tuple[float, float, float]: - return tuple(int(value[i : i + 2], 16) / 255.0 for i in (0, 2, 4)) # type: ignore + return tuple(int(value.lstrip("#")[i : i + 2], 16) / 255.0 for i in (0, 2, 4)) # type: ignore class AnnotationBuilder: diff --git a/tests/test_writer.py b/tests/test_writer.py index 0d78010ff..048bd4ed7 100644 --- a/tests/test_writer.py +++ b/tests/test_writer.py @@ -13,6 +13,7 @@ TESTS_ROOT = Path(__file__).parent.resolve() PROJECT_ROOT = TESTS_ROOT.parent RESOURCE_ROOT = PROJECT_ROOT / "resources" +EXTERNAL_ROOT = Path(PROJECT_ROOT) / "sample-files" def test_writer_clone(): @@ -633,3 +634,25 @@ def test_deprecate_bookmark_decorator(): match="bookmark is deprecated as an argument. Use outline_item instead", ): writer.add_outline_item_dict(bookmark=outline_item) + + +def test_colors_in_outline_item(): + reader = PdfReader(EXTERNAL_ROOT / "004-pdflatex-4-pages/pdflatex-4-pages.pdf") + writer = PdfWriter() + writer.clone_document_from_reader(reader) + purple_rgb = (0.50196, 0, 0.50196) + writer.add_outline_item("First Outline Item", pagenum=2, color="800080") + writer.add_outline_item("Second Outline Item", pagenum=3, color="#800080") + writer.add_outline_item("Third Outline Item", pagenum=4, color=purple_rgb) + + target = "tmp-named-color-outline.pdf" + with open(target, "wb") as f: + writer.write(f) + + reader2 = PdfReader(target) + for outline_item in reader2.outline: + # convert float to string because of mutability + assert [str(c) for c in outline_item.color] == [str(p) for p in purple_rgb] + + # Cleanup + os.remove(target) # remove for testing From 0a6676fe064837222d391a7c73c7b0f3df782ac1 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 31 Jul 2022 21:16:03 +0200 Subject: [PATCH 071/130] REL: 2.9.0 New Features (ENH): - Add ability to add hex encoded colors to outline items (#1186) - Add support for pathlib.Path in PdfMerger.merge (#1190) - Add link annotation (#1189) - Add capability to filter text extraction by orientation (#1175) Bug Fixes (BUG): - Named Dest in PDF1.1 (#1174) - Incomplete Graphic State save/restore (#1172) Documentation (DOC): - Update changelog url in package metadata (#1180) - Table extraction (#1179) - Mention pyHanko for signing PDF documents (#1178) - We now have CMAP support (#1177) Maintenance (MAINT): - Consistant usage of warnings / log messages (#1164) - Consistent terminology for outline items (#1156) Code Style (STY): - Apply pre-commit (#1188) Full Changelog: https://github.com/py-pdf/PyPDF2/compare/2.8.1...2.9.0 --- CHANGELOG.md | 25 +++++++++++++++++++++++++ PyPDF2/_version.py | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d0609eb0..3ef7e0d28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # CHANGELOG +## Version 2.9.0, 2022-07-31 + +### New Features (ENH) +- Add ability to add hex encoded colors to outline items (#1186) +- Add support for pathlib.Path in PdfMerger.merge (#1190) +- Add link annotation (#1189) +- Add capability to filter text extraction by orientation (#1175) + +### Bug Fixes (BUG) +- Named Dest in PDF1.1 (#1174) +- Incomplete Graphic State save/restore (#1172) + +### Documentation (DOC) +- Update changelog url in package metadata (#1180) +- Mantion camelot for table extraction (#1179) +- Mention pyHanko for signing PDF documents (#1178) +- Weow have CMAP support since a while (#1177) + +### Maintenance (MAINT) +- Consistant usage of warnings / log messages (#1164) +- Consistent terminology for outline items (#1156) + + +Full Changelog: https://github.com/py-pdf/PyPDF2/compare/2.8.1...2.9.0 + ## Version 2.8.1, 2022-07-25 ### Bug Fixes (BUG) diff --git a/PyPDF2/_version.py b/PyPDF2/_version.py index b4066b65a..43ce13db0 100644 --- a/PyPDF2/_version.py +++ b/PyPDF2/_version.py @@ -1 +1 @@ -__version__ = "2.8.1" +__version__ = "2.9.0" From 4aa9ec9637c8a154d58bf3b49185df79dfbf8e12 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Wed, 3 Aug 2022 13:47:28 +0200 Subject: [PATCH 072/130] ENH: "with" support for PdfMerger and PdfWriter (#1193) Closes #1108 Closes #1117 Full credit for this PR goes to JianzhengLuo Co-authored-by: JianzhengLuo --- CONTRIBUTORS.md | 1 + PyPDF2/_merger.py | 45 +++++++++++++---- PyPDF2/_writer.py | 59 +++++++++++++++++++--- tests/test_generic.py | 1 - tests/test_merger.py | 48 +++++++++++++++--- tests/test_writer.py | 114 +++++++++++++++++++++++++++++++++++++----- 6 files changed, 229 insertions(+), 39 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 28d3d9b8d..23f1ee1a7 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -11,6 +11,7 @@ history and [GitHubs 'Contributors' feature](https://github.com/py-pdf/PyPDF2/gr ## Contributors to the pyPdf / PyPDF2 project +* [JianzhengLuo](https://github.com/JianzhengLuo) * [Karvonen, Harry](https://github.com/Hatell/) * [KourFrost](https://github.com/KourFrost) * [Lightup1](https://github.com/Lightup1) diff --git a/PyPDF2/_merger.py b/PyPDF2/_merger.py index db6bf0480..a63fe76cc 100644 --- a/PyPDF2/_merger.py +++ b/PyPDF2/_merger.py @@ -27,7 +27,18 @@ from io import BytesIO, FileIO, IOBase from pathlib import Path -from typing import Any, Dict, Iterable, List, Optional, Tuple, Union, cast +from types import TracebackType +from typing import ( + Any, + Dict, + Iterable, + List, + Optional, + Tuple, + Type, + Union, + cast, +) from ._encryption import Encryption from ._page import PageObject @@ -84,18 +95,38 @@ class PdfMerger: :param bool strict: Determines whether user should be warned of all problems and also causes some correctable problems to be fatal. Defaults to ``False``. + :param fileobj: Output file. Can be a filename or any kind of + file-like object. """ @deprecate_bookmark(bookmarks="outline") - def __init__(self, strict: bool = False) -> None: + def __init__( + self, strict: bool = False, fileobj: Union[Path, StrByteType] = "" + ) -> None: self.inputs: List[Tuple[Any, PdfReader, bool]] = [] self.pages: List[Any] = [] self.output: Optional[PdfWriter] = PdfWriter() self.outline: OutlineType = [] self.named_dests: List[Any] = [] self.id_count = 0 + self.fileobj = fileobj self.strict = strict + def __enter__(self) -> "PdfMerger": + # There is nothing to do. + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> None: + """Write to the fileobj and close the merger.""" + if self.fileobj: + self.write(self.fileobj) + self.close() + @deprecate_bookmark(bookmark="outline_item", import_bookmarks="import_outline") def merge( self, @@ -254,7 +285,7 @@ def append( """ self.merge(len(self.pages), fileobj, outline_item, pages, import_outline) - def write(self, fileobj: StrByteType) -> None: + def write(self, fileobj: Union[Path, StrByteType]) -> None: """ Write all data that has been merged to the given output file. @@ -263,10 +294,6 @@ def write(self, fileobj: StrByteType) -> None: """ if self.output is None: raise RuntimeError(ERR_CLOSED_WRITER) - my_file = False - if isinstance(fileobj, str): - fileobj = FileIO(fileobj, "wb") - my_file = True # Add pages to the PdfWriter # The commented out line below was replaced with the two lines below it @@ -285,10 +312,10 @@ def write(self, fileobj: StrByteType) -> None: self._write_outline() # Write the output to the file - self.output.write(fileobj) + my_file, ret_fileobj = self.output.write(fileobj) if my_file: - fileobj.close() + ret_fileobj.close() def close(self) -> None: """Shut all file descriptors (input and output) and clear all memory usage.""" diff --git a/PyPDF2/_writer.py b/PyPDF2/_writer.py index 6f54df2ff..d9448d1bf 100644 --- a/PyPDF2/_writer.py +++ b/PyPDF2/_writer.py @@ -36,6 +36,9 @@ import time import uuid from hashlib import md5 +from io import BufferedReader, BufferedWriter, BytesIO, FileIO +from pathlib import Path +from types import TracebackType from typing import ( Any, Callable, @@ -44,6 +47,7 @@ List, Optional, Tuple, + Type, Union, cast, ) @@ -52,6 +56,7 @@ from ._reader import PdfReader from ._security import _alg33, _alg34, _alg35 from ._utils import ( + StrByteType, StreamType, _get_max_pdf_version_header, b_, @@ -121,7 +126,7 @@ class PdfWriter: class (typically :class:`PdfReader`). """ - def __init__(self) -> None: + def __init__(self, fileobj: StrByteType = "") -> None: self._header = b"%PDF-1.3" self._objects: List[Optional[PdfObject]] = [] # array of indirect objects self._idnum_hash: Dict[bytes, IndirectObject] = {} @@ -158,6 +163,23 @@ def __init__(self) -> None: ) self._root: Optional[IndirectObject] = None self._root_object = root + self.fileobj = fileobj + self.with_as_usage = False + + def __enter__(self) -> "PdfWriter": + """Store that writer is initialized by 'with'.""" + self.with_as_usage = True + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> None: + """Write data to the fileobj.""" + if self.fileobj: + self.write(self.fileobj) @property def pdf_header(self) -> bytes: @@ -771,13 +793,7 @@ def encrypt( self._encrypt = self._add_object(encrypt) self._encrypt_key = key - def write(self, stream: StreamType) -> None: - """ - Write the collection of pages added to this object out as a PDF file. - - :param stream: An object to write the file to. The object must support - the write method and the tell method, similar to a file object. - """ + def write_stream(self, stream: StreamType) -> None: if hasattr(stream, "mode") and "b" not in stream.mode: logger_warning( f"File <{stream.name}> to write to is not in binary mode. " # type: ignore @@ -803,6 +819,33 @@ def write(self, stream: StreamType) -> None: self._write_trailer(stream) stream.write(b_(f"\nstartxref\n{xref_location}\n%%EOF\n")) # eof + def write( + self, stream: Union[Path, StrByteType] + ) -> Tuple[bool, Union[FileIO, BytesIO, BufferedReader, BufferedWriter]]: + """ + Write the collection of pages added to this object out as a PDF file. + + :param stream: An object to write the file to. The object can support + the write method and the tell method, similar to a file object, or + be a file path, just like the fileobj, just named it stream to keep + existing workflow. + """ + my_file = False + + if stream == "": + raise ValueError(f"Output(stream={stream}) is empty.") + + if isinstance(stream, (str, Path)): + stream = FileIO(stream, "wb") + my_file = True + + self.write_stream(stream) + + if self.with_as_usage: + stream.close() + + return my_file, stream + def _write_header(self, stream: StreamType) -> List[int]: object_positions = [] stream.write(self.pdf_header + b"\n") diff --git a/tests/test_generic.py b/tests/test_generic.py index 0ed2ee7d1..350ab8aef 100644 --- a/tests/test_generic.py +++ b/tests/test_generic.py @@ -351,7 +351,6 @@ class Tst: # to replace pdf if length in (6, 10): assert b"BT /F1" in do._StreamObject__data raise PdfReadError("__ALLGOOD__") - print(exc.value) assert should_fail ^ (exc.value.args[0] == "__ALLGOOD__") diff --git a/tests/test_merger.py b/tests/test_merger.py index f4cf78179..7411a7a9d 100644 --- a/tests/test_merger.py +++ b/tests/test_merger.py @@ -18,14 +18,12 @@ sys.path.append(str(PROJECT_ROOT)) -def test_merge(): +def merger_operate(merger): pdf_path = RESOURCE_ROOT / "crazyones.pdf" outline = RESOURCE_ROOT / "pdflatex-outline.pdf" pdf_forms = RESOURCE_ROOT / "pdflatex-forms.pdf" pdf_pw = RESOURCE_ROOT / "libreoffice-writer-password.pdf" - merger = PyPDF2.PdfMerger() - # string path: merger.append(pdf_path) merger.append(outline) @@ -95,10 +93,8 @@ def test_merge(): merger.set_page_layout("/SinglePage") merger.set_page_mode("/UseThumbs") - tmp_path = "dont_commit_merged.pdf" - merger.write(tmp_path) - merger.close() +def check_outline(tmp_path): # Check if outline is correct reader = PyPDF2.PdfReader(tmp_path) assert [el.title for el in reader.outline if isinstance(el, Destination)] == [ @@ -117,8 +113,44 @@ def test_merge(): # TODO: There seem to be no destinations for those links? - # Clean up - os.remove(tmp_path) + +tmp_filename = "dont_commit_merged.pdf" + + +def test_merger_operations_by_traditional_usage(tmp_path): + # Arrange + merger = PdfMerger() + merger_operate(merger) + path = tmp_path / tmp_filename + + # Act + merger.write(path) + merger.close() + + # Assert + check_outline(path) + + +def test_merger_operations_by_semi_traditional_usage(tmp_path): + path = tmp_path / tmp_filename + + with PdfMerger() as merger: + merger_operate(merger) + merger.write(path) # Act + + # Assert + assert os.path.isfile(path) + check_outline(path) + + +def test_merger_operation_by_new_usage(tmp_path): + path = tmp_path / tmp_filename + with PdfMerger(fileobj=path) as merger: + merger_operate(merger) + + # Assert + assert os.path.isfile(path) + check_outline(path) def test_merge_page_exception(): diff --git a/tests/test_writer.py b/tests/test_writer.py index 048bd4ed7..667d6590b 100644 --- a/tests/test_writer.py +++ b/tests/test_writer.py @@ -26,12 +26,9 @@ def test_writer_clone(): assert len(writer.pages) == 4 -def test_writer_operations(): +def writer_operate(writer): """ - This test just checks if the operation throws an exception. - - This should be done way more thoroughly: It should be checked if the - output is as expected. + To test the writer that initialized by each of the four usages. """ pdf_path = RESOURCE_ROOT / "crazyones.pdf" pdf_outline_path = RESOURCE_ROOT / "pdflatex-outline.pdf" @@ -39,7 +36,6 @@ def test_writer_operations(): reader = PdfReader(pdf_path) reader_outline = PdfReader(pdf_outline_path) - writer = PdfWriter() page = reader.pages[0] with pytest.raises(PageSizeNotDefinedError) as exc: writer.add_blank_page() @@ -91,19 +87,101 @@ def test_writer_operations(): writer.add_attachment("foobar.gif", b"foobarcontent") - # finally, write "output" to PyPDF2-output.pdf - tmp_path = "dont_commit_writer.pdf" - with open(tmp_path, "wb") as output_stream: - writer.write(output_stream) - # Check that every key in _idnum_hash is correct objects_hash = [o.hash_value() for o in writer._objects] for k, v in writer._idnum_hash.items(): assert v.pdf == writer assert k in objects_hash, "Missing %s" % v - # cleanup - os.remove(tmp_path) + +tmp_path = "dont_commit_writer.pdf" + + +@pytest.mark.parametrize( + ("write_data_here", "needs_cleanup"), + [ + ("dont_commit_writer.pdf", True), + (Path("dont_commit_writer.pdf"), True), + (BytesIO(), False), + ], +) +def test_writer_operations_by_traditional_usage(write_data_here, needs_cleanup): + writer = PdfWriter() + + writer_operate(writer) + + # finally, write "output" to PyPDF2-output.pdf + if needs_cleanup: + with open(write_data_here, "wb") as output_stream: + writer.write(output_stream) + else: + output_stream = write_data_here + writer.write(output_stream) + + if needs_cleanup: + os.remove(write_data_here) + + +@pytest.mark.parametrize( + ("write_data_here", "needs_cleanup"), + [ + ("dont_commit_writer.pdf", True), + (Path("dont_commit_writer.pdf"), True), + (BytesIO(), False), + ], +) +def test_writer_operations_by_semi_traditional_usage(write_data_here, needs_cleanup): + with PdfWriter() as writer: + writer_operate(writer) + + # finally, write "output" to PyPDF2-output.pdf + if needs_cleanup: + with open(write_data_here, "wb") as output_stream: + writer.write(output_stream) + else: + output_stream = write_data_here + writer.write(output_stream) + + if needs_cleanup: + os.remove(write_data_here) + + +@pytest.mark.parametrize( + ("write_data_here", "needs_cleanup"), + [ + ("dont_commit_writer.pdf", True), + (Path("dont_commit_writer.pdf"), True), + (BytesIO(), False), + ], +) +def test_writer_operations_by_semi_new_traditional_usage( + write_data_here, needs_cleanup +): + with PdfWriter() as writer: + writer_operate(writer) + + # finally, write "output" to PyPDF2-output.pdf + writer.write(write_data_here) + + if needs_cleanup: + os.remove(write_data_here) + + +@pytest.mark.parametrize( + ("write_data_here", "needs_cleanup"), + [ + ("dont_commit_writer.pdf", True), + (Path("dont_commit_writer.pdf"), True), + (BytesIO(), False), + ], +) +def test_writer_operation_by_new_usage(write_data_here, needs_cleanup): + # This includes write "output" to PyPDF2-output.pdf + with PdfWriter(write_data_here) as writer: + writer_operate(writer) + + if needs_cleanup: + os.remove(write_data_here) @pytest.mark.parametrize( @@ -656,3 +734,13 @@ def test_colors_in_outline_item(): # Cleanup os.remove(target) # remove for testing + + +def test_write_empty_stream(): + reader = PdfReader(EXTERNAL_ROOT / "004-pdflatex-4-pages/pdflatex-4-pages.pdf") + writer = PdfWriter() + writer.clone_document_from_reader(reader) + + with pytest.raises(ValueError) as exc: + writer.write("") + assert exc.value.args[0] == "Output(stream=) is empty." From d0a058ad24847f657b0922a186630535cab7a811 Mon Sep 17 00:00:00 2001 From: Ern Chow Date: Thu, 4 Aug 2022 19:31:29 +0100 Subject: [PATCH 073/130] MAINT: Introduce WrongPasswordError / FileNotDecryptedError / EmptyFileError (#1201) Some cases of PdfReadError were replaced by more specific exceptions: * FileNotDecryptedError * WrongPasswordError * EmptyFileError This enables PyPDF2 users to handle those specific issues more conveniently. --- PyPDF2/_reader.py | 9 +++++---- PyPDF2/errors.py | 12 ++++++++++++ tests/test_reader.py | 15 ++++++++++++--- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/PyPDF2/_reader.py b/PyPDF2/_reader.py index 03c5bac9e..d2a080611 100644 --- a/PyPDF2/_reader.py +++ b/PyPDF2/_reader.py @@ -70,7 +70,8 @@ from .constants import PageAttributes as PG from .constants import PagesAttributes as PA from .constants import TrailerKeys as TK -from .errors import PdfReadError, PdfStreamError +from .errors import PdfReadError, PdfStreamError, WrongPasswordError, \ + FileNotDecryptedError, EmptyFileError from .generic import ( ArrayObject, ContentStream, @@ -290,7 +291,7 @@ def __init__( and password is not None ): # raise if password provided - raise PdfReadError("Wrong password") + raise WrongPasswordError("Wrong password") self._override_encryption = False else: if password is not None: @@ -1155,7 +1156,7 @@ def get_object(self, indirect_reference: IndirectObject) -> Optional[PdfObject]: if not self._override_encryption and self._encryption is not None: # if we don't have the encryption key: if not self._encryption.is_decrypted(): - raise PdfReadError("File has not been decrypted") + raise FileNotDecryptedError("File has not been decrypted") # otherwise, decrypt here... retval = cast(PdfObject, retval) retval = self._encryption.decrypt_object( @@ -1306,7 +1307,7 @@ def _basic_validation(self, stream: StreamType) -> None: # start at the end: stream.seek(0, os.SEEK_END) if not stream.tell(): - raise PdfReadError("Cannot read an empty file") + raise EmptyFileError("Cannot read an empty file") if self.strict: stream.seek(0, os.SEEK_SET) header_byte = stream.read(5) diff --git a/PyPDF2/errors.py b/PyPDF2/errors.py index 9ed33150b..d00bc7c12 100644 --- a/PyPDF2/errors.py +++ b/PyPDF2/errors.py @@ -33,4 +33,16 @@ class ParseError(Exception): pass +class FileNotDecryptedError(PdfReadError): + pass + + +class WrongPasswordError(FileNotDecryptedError): + pass + + +class EmptyFileError(PdfReadError): + pass + + STREAM_TRUNCATED_PREMATURELY = "Stream has ended unexpectedly" diff --git a/tests/test_reader.py b/tests/test_reader.py index 4dcf38020..875b3f2de 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -11,7 +11,7 @@ from PyPDF2.constants import ImageAttributes as IA from PyPDF2.constants import PageAttributes as PG from PyPDF2.constants import Ressources as RES -from PyPDF2.errors import PdfReadError, PdfReadWarning +from PyPDF2.errors import PdfReadError, PdfReadWarning, EmptyFileError, FileNotDecryptedError, WrongPasswordError from PyPDF2.filters import _xobj_to_image from PyPDF2.generic import Destination @@ -414,7 +414,7 @@ def test_get_page_mode(src, expected): def test_read_empty(): - with pytest.raises(PdfReadError) as exc: + with pytest.raises(EmptyFileError) as exc: PdfReader(io.BytesIO()) assert exc.value.args[0] == "Cannot read an empty file" @@ -560,7 +560,7 @@ def test_read_unknown_zero_pages(caplog): def test_read_encrypted_without_decryption(): src = RESOURCE_ROOT / "libreoffice-writer-password.pdf" reader = PdfReader(src) - with pytest.raises(PdfReadError) as exc: + with pytest.raises(FileNotDecryptedError) as exc: len(reader.pages) assert exc.value.args[0] == "File has not been decrypted" @@ -1066,3 +1066,12 @@ def test_PdfReaderMultipleDefinitions(caplog): assert normalize_warnings(caplog.text) == [ "Multiple definitions in dictionary at byte 0xb5 for key /Group" ] + + +def test_wrong_password_error(): + encrypted_pdf_path = RESOURCE_ROOT / "encrypted-file.pdf" + with pytest.raises(WrongPasswordError): + PdfReader( + encrypted_pdf_path, + password="definitely_the_wrong_password!", + ) From 43197dc4ccbf1f2efefa1f41edf376c30c4de963 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Thu, 4 Aug 2022 22:25:54 +0200 Subject: [PATCH 074/130] ENH: Add AnnotationBuilder.text(...) to build text annotations (#1202) --- PyPDF2/generic.py | 30 ++++++++++++++++++++++++++-- docs/user/adding-pdf-annotations.md | 6 ++++++ docs/user/text-annotation.png | Bin 0 -> 23368 bytes tests/test_generic.py | 26 +++++++++++++++++++++++- 4 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 docs/user/text-annotation.png diff --git a/PyPDF2/generic.py b/PyPDF2/generic.py index 16c904a1c..c2198203e 100644 --- a/PyPDF2/generic.py +++ b/PyPDF2/generic.py @@ -70,7 +70,6 @@ ) from .constants import CheckboxRadioButtonAttributes, FieldDictionaryAttributes from .constants import FilterTypes as FT -from .constants import PageAttributes as PG from .constants import StreamAttributes as SA from .constants import TypArguments as TA from .constants import TypFitArguments as TF @@ -2079,6 +2078,33 @@ def hex_to_rgb(value: str) -> Tuple[float, float, float]: class AnnotationBuilder: from .types import FitType, ZoomArgType + @staticmethod + def text( + rect: Union[RectangleObject, Tuple[float, float, float, float]], + text: str, + open: bool = False, + flags: int = 0, + ) -> DictionaryObject: + """ + Add text annotation. + + :param :class:`RectangleObject` rect: + or array of four integers specifying the clickable rectangular area + ``[xLL, yLL, xUR, yUR]`` + """ + # TABLE 8.23 Additional entries specific to a text annotation + text_obj = DictionaryObject( + { + NameObject("/Type"): NameObject("/Annot"), + NameObject("/Subtype"): NameObject("/Text"), + NameObject("/Rect"): RectangleObject(rect), + NameObject("/Contents"): TextStringObject(text), + NameObject("/Open"): BooleanObject(open), + NameObject("/Flags"): NumberObject(flags), + } + ) + return text_obj + @staticmethod def free_text( text: str, @@ -2270,7 +2296,7 @@ def link( link_obj = DictionaryObject( { - NameObject("/Type"): NameObject(PG.ANNOTS), + NameObject("/Type"): NameObject("/Annot"), NameObject("/Subtype"): NameObject("/Link"), NameObject("/Rect"): RectangleObject(rect), NameObject("/Border"): ArrayObject(border_arr), diff --git a/docs/user/adding-pdf-annotations.md b/docs/user/adding-pdf-annotations.md index 890dfde1c..4cce69545 100644 --- a/docs/user/adding-pdf-annotations.md +++ b/docs/user/adding-pdf-annotations.md @@ -54,6 +54,12 @@ with open("annotated-pdf.pdf", "wb") as fp: writer.write(fp) ``` +## Text + +A text annotation looks like this: + +![](text-annotation.png) + ## Line If you want to add a line like this: diff --git a/docs/user/text-annotation.png b/docs/user/text-annotation.png new file mode 100644 index 0000000000000000000000000000000000000000..51997a11526ecae75a30b0e138b4386bf023e601 GIT binary patch literal 23368 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 Ld<|A*<9GjG^yTnj literal 0 HcmV?d00001 diff --git a/tests/test_generic.py b/tests/test_generic.py index 350ab8aef..9044eb525 100644 --- a/tests/test_generic.py +++ b/tests/test_generic.py @@ -611,7 +611,31 @@ def test_annotation_builder_link(): with open(target, "wb") as fp: writer.write(fp) - # os.remove(target) # comment this out for manual inspection + os.remove(target) # comment this out for manual inspection + + +def test_annotation_builder_text(): + # Arrange + pdf_path = RESOURCE_ROOT / "outline-without-title.pdf" + reader = PdfReader(pdf_path) + page = reader.pages[0] + writer = PdfWriter() + writer.add_page(page) + + # Act + text_annotation = AnnotationBuilder.text( + text="Hello World\nThis is the second line!", + rect=(50, 550, 500, 650), + open=True, + ) + writer.add_annotation(0, text_annotation) + + # Assert: You need to inspect the file manually + target = "annotated-pdf-popup.pdf" + with open(target, "wb") as fp: + writer.write(fp) + + os.remove(target) # comment this out for manual inspection def test_CheckboxRadioButtonAttributes_opt(): From 223da14bb249503574f1699b17d46cd4b7fc7885 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Thu, 4 Aug 2022 22:46:59 +0200 Subject: [PATCH 075/130] DEV: Add flake8-print (#1203) --- .flake8 | 3 ++- .pre-commit-config.yaml | 2 +- requirements/ci.in | 1 + requirements/ci.txt | 19 +++++++++++++------ 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/.flake8 b/.flake8 index 44091fbb1..122507c9a 100644 --- a/.flake8 +++ b/.flake8 @@ -4,4 +4,5 @@ ignore = E203,E501,E741,W503,W604,N817,N814,VNE001,VNE002,VNE003,N802,SIM105,P101 exclude = build,sample-files per-file-ignores = - tests/*: ASS001,PT011,B011 + tests/*: ASS001,PT011,B011,T001 + make_changelog.py:T001 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e9ee5a062..ffb52bab4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -40,7 +40,7 @@ repos: - id: blacken-docs additional_dependencies: [black==22.1.0] - repo: https://github.com/asottile/pyupgrade - rev: v2.37.2 + rev: v2.37.3 hooks: - id: pyupgrade args: [--py36-plus] diff --git a/requirements/ci.in b/requirements/ci.in index f94a627d7..0527a1f05 100644 --- a/requirements/ci.in +++ b/requirements/ci.in @@ -2,6 +2,7 @@ coverage flake8 flake8_implicit_str_concat flake8-bugbear +flake8-print mypy pillow pytest diff --git a/requirements/ci.txt b/requirements/ci.txt index 86a7f5154..bf8372cb2 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -11,14 +11,17 @@ attrs==20.3.0 # pytest coverage==6.2 # via -r requirements/ci.in -flake8==4.0.1 +flake8==5.0.4 # via # -r requirements/ci.in # flake8-bugbear + # flake8-print flake8-bugbear==22.7.1 # via -r requirements/ci.in flake8-implicit-str-concat==0.2.0 # via -r requirements/ci.in +flake8-print==4.0.1 + # via -r requirements/ci.in importlib-metadata==4.2.0 # via # flake8 @@ -26,7 +29,7 @@ importlib-metadata==4.2.0 # pytest iniconfig==1.1.1 # via pytest -mccabe==0.6.1 +mccabe==0.7.0 # via flake8 more-itertools==8.13.0 # via flake8-implicit-str-concat @@ -44,11 +47,13 @@ py==1.11.0 # via pytest py-cpuinfo==8.0.0 # via pytest-benchmark -pycodestyle==2.8.0 - # via flake8 +pycodestyle==2.9.1 + # via + # flake8 + # flake8-print pycryptodome==3.15.0 # via -r requirements/ci.in -pyflakes==2.4.0 +pyflakes==2.5.0 # via flake8 pyparsing==3.0.9 # via packaging @@ -58,6 +63,8 @@ pytest==7.0.1 # pytest-benchmark pytest-benchmark==3.4.1 # via -r requirements/ci.in +six==1.16.0 + # via flake8-print tomli==1.2.3 # via # mypy @@ -66,7 +73,7 @@ typed-ast==1.5.4 # via mypy typeguard==2.13.3 # via -r requirements/ci.in -types-pillow==9.2.0 +types-pillow==9.2.1 # via -r requirements/ci.in typing-extensions==4.1.1 # via From 759cbc344fb8f484dc55ba4a9f394d19f9189591 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Fri, 5 Aug 2022 13:33:22 +0200 Subject: [PATCH 076/130] DOC: Fix AnnotationBuilder parameter formatting (#1204) --- .gitignore | 1 + PyPDF2/generic.py | 31 ++++++++++++++++++++---------- docs/modules/AnnotationBuilder.rst | 2 +- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 75ef18635..6449fe86b 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,5 @@ PyPDF2_pdfLocation.txt .python-version tests/pdf_cache/ docs/meta/CHANGELOG.md +docs/meta/CONTRIBUTORS.md extracted-images/ diff --git a/PyPDF2/generic.py b/PyPDF2/generic.py index c2198203e..5f6065d5c 100644 --- a/PyPDF2/generic.py +++ b/PyPDF2/generic.py @@ -2076,6 +2076,16 @@ def hex_to_rgb(value: str) -> Tuple[float, float, float]: class AnnotationBuilder: + """ + The AnnotationBuilder creates dictionaries representing PDF annotations. + + Those dictionaries can be modified before they are added to a PdfWriter + instance via `writer.add_annotation`. + + See `adding PDF annotations <../user/adding-pdf-annotations.html>`_ for + it's usage combined with PdfWriter. + """ + from .types import FitType, ZoomArgType @staticmethod @@ -2088,9 +2098,11 @@ def text( """ Add text annotation. - :param :class:`RectangleObject` rect: + :param RectangleObject rect: or array of four integers specifying the clickable rectangular area ``[xLL, yLL, xUR, yUR]`` + :param bool open: + :param int flags: """ # TABLE 8.23 Additional entries specific to a text annotation text_obj = DictionaryObject( @@ -2121,9 +2133,8 @@ def free_text( Add text in a rectangle to a page. :param str text: Text to be added - :param :class:`RectangleObject` rect: or array of four - integers specifying the clickable rectangular area - ``[xLL, yLL, xUR, yUR]`` + :param RectangleObject rect: or array of four integers + specifying the clickable rectangular area ``[xLL, yLL, xUR, yUR]`` :param str font: Name of the Font, e.g. 'Helvetica' :param bool bold: Print the text in bold :param bool italic: Print the text in italic @@ -2177,9 +2188,9 @@ def line( :param Tuple[float, float] p1: First point :param Tuple[float, float] p2: Second point - :param :class:`RectangleObject` rect: or array of four - integers specifying the clickable rectangular area - ``[xLL, yLL, xUR, yUR]`` + :param RectangleObject rect: or array of four + integers specifying the clickable rectangular area + ``[xLL, yLL, xUR, yUR]`` :param str text: Text to be displayed as the line annotation :param str title_bar: Text to be displayed in the title bar of the annotation; by convention this is the name of the author @@ -2234,9 +2245,9 @@ def link( An internal link requires the target_page_index, fit, and fit args. - :param :class:`RectangleObject` rect: or array of four - integers specifying the clickable rectangular area - ``[xLL, yLL, xUR, yUR]`` + :param RectangleObject rect: or array of four + integers specifying the clickable rectangular area + ``[xLL, yLL, xUR, yUR]`` :param border: if provided, an array describing border-drawing properties. See the PDF spec for details. No border will be drawn if this argument is omitted. diff --git a/docs/modules/AnnotationBuilder.rst b/docs/modules/AnnotationBuilder.rst index 198f06501..8ad7e54a5 100644 --- a/docs/modules/AnnotationBuilder.rst +++ b/docs/modules/AnnotationBuilder.rst @@ -3,5 +3,5 @@ The AnnotationBuilder Class .. autoclass:: PyPDF2.generic.AnnotationBuilder :members: - :undoc-members: + :no-undoc-members: :show-inheritance: From a6b8fa6e4cd654d22760ccf62760b89de287c7d6 Mon Sep 17 00:00:00 2001 From: pubpub-zz <4083478+pubpub-zz@users.noreply.github.com> Date: Fri, 5 Aug 2022 20:18:27 +0200 Subject: [PATCH 077/130] DOC: Example for orientation parameter of extract_text (#1206) Introduced by 8a27fa4eea0c072cd7c8718a4c04869223c31ef6 (#1175) --- docs/user/extract-text.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/user/extract-text.md b/docs/user/extract-text.md index 30eaaf2a8..715a62163 100644 --- a/docs/user/extract-text.md +++ b/docs/user/extract-text.md @@ -10,6 +10,19 @@ page = reader.pages[0] print(page.extract_text()) ``` +you can also select limit the text orientation you want to extract.
+eg:
+to extract only text oriented up +```python +print(page.extract_text(0)) +``` +to extract text oriented up and turned left +```python +print(page.extract_text((0, 90))) +``` + +refer to [extract\_text](../modules/PageObject.html#PyPDF2._page.PageObject.extract_text) for more details. + ## Why Text Extraction is hard Extracting text from a PDF can be pretty tricky. In several cases there is no From cb3f66e2617eb3154646a3300dfc77ac0eb7984c Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sat, 6 Aug 2022 09:35:16 +0200 Subject: [PATCH 078/130] DOC: Page vs Content scaling (#1208) Closes #1035 --- PyPDF2/_page.py | 6 +++ docs/user/cropping-and-transforming.md | 63 +++++++++++++++++++++++++ docs/user/scaling.png | Bin 0 -> 457262 bytes 3 files changed, 69 insertions(+) create mode 100644 docs/user/scaling.png diff --git a/PyPDF2/_page.py b/PyPDF2/_page.py index 73ca6626e..dfe158d68 100644 --- a/PyPDF2/_page.py +++ b/PyPDF2/_page.py @@ -193,6 +193,12 @@ def translate(self, tx: float = 0, ty: float = 0) -> "Transformation": def scale( self, sx: Optional[float] = None, sy: Optional[float] = None ) -> "Transformation": + """ + Scale the contents of a page towards the origin of the coordinate system. + + Typically, that is the lower-left corner of the page. That can be + changed by translating the contents / the page boxes. + """ if sx is None and sy is None: raise ValueError("Either sx or sy must be specified") if sx is None: diff --git a/docs/user/cropping-and-transforming.md b/docs/user/cropping-and-transforming.md index 872907547..c83038cf0 100644 --- a/docs/user/cropping-and-transforming.md +++ b/docs/user/cropping-and-transforming.md @@ -128,3 +128,66 @@ op = Transformation().rotate(45).translate(tx=50) ``` ![](merge-translated.png) + + +## Scaling + +PyPDF2 offers two ways to scale: The page itself and the contents on a page. +Typically, you want to combine both. + +![](scaling.png) + +### Scaling a Page (the Canvas) + +```python +from PyPDF2 import PdfReader, PdfWriter + +# Read the input +reader = PdfReader("resources/side-by-side-subfig.pdf") +page = reader.pages[0] + +# Scale +page.scale(0.5) + +# Write the result to a file +writer = PdfWriter() +writer.add_page(page) +writer.write("out.pdf") +``` + +If you wish to have more control, you can adjust the various page boxes +directly: + +```python +from PyPDF2.generic import RectangleObject + +mb = page.mediabox + +page.mediabox = RectangleObject((mb.left, mb.bottom, mb.right, mb.top)) +page.cropbox = RectangleObject((mb.left, mb.bottom, mb.right, mb.top)) +page.trimbox = RectangleObject((mb.left, mb.bottom, mb.right, mb.top)) +page.bleedbox = RectangleObject((mb.left, mb.bottom, mb.right, mb.top)) +page.artbox = RectangleObject((mb.left, mb.bottom, mb.right, mb.top)) +``` + +### Scaling the content + +The content is scaled towords the origin of the coordinate system. Typically, +that is the lower-left corner. + +```python +from PyPDF2 import PdfReader, PdfWriter, Transformation + +# Read the input +reader = PdfReader("resources/side-by-side-subfig.pdf") +page = reader.pages[0] + +# Scale +op = Transformation().scale(sx=0.7, sy=0.7) +page.add_transformation(op) + +# Write the result to a file +writer = PdfWriter() +writer.add_page(page) +writer.write("out-pg-transform.pdf") +``` diff --git a/docs/user/scaling.png b/docs/user/scaling.png new file mode 100644 index 0000000000000000000000000000000000000000..8270c1e448966fc81c6b5116baabe1fb57f786b7 GIT binary patch literal 457262 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%0AZbLMo<^JX;uu}{ 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

NfX?;0{YwWM$Pk}H(Tq@}0n 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 z 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%0A0Keo1OA1Ade<~*z8x81ANU*_d%mdg zE0ci03WWSU@PwB2buqd&{_~%rqTsMbS3#@sz#@#WX#Pe zEi5gQlaqs+n|U0+v|+`RP_VH*b8~a!AdUx?O~TQU1%`r;4?m`41>*Gh2-p5T~EG(#0^M%5}G8%c#$&8MVOIlgc0(GXYsR$0Dz+-0bwetnA6^>M8>hQ$OG)(@C2F6nu8hZKxiv|jTYe|yCwl;yu zd|?6@aQ&w+w!eB7k6YKB@9qGs1OaC4(t0urwrMpEtj_0tfzRa7<;F@T{&x-J;>pFI zgTvbf3V`J>#GmVt4t^>+-5rOAg}wF<`bYZK_5P8*=XlSbCzO@Z$Ugb|88<9^LzMp` z$5fhqF`obZ7Zu)z|69ucKYg?0?JFK)bFOU1AVU`XLZOG47!-z>`7$ri;Zw|jf-4g| zXX|+Cog<~MPZTFxs!JMM!oWd@R&B0Km&&TzCFm?lR0m{B*$gTVqbK!L`QR(uJSgWy z0#5=9hQsBXNLqTj?>*<q)oy;!1$uMckK$=xBUYeY{6k8`Pshr9NZ36bie_H{$zfB z9@-@wN5Vf1nFjKr@$zMOf4}75;2;IiRO^e6vH!sW0On@7p1^x`4UJ$RrtmPJK}ct> zDV1KW-;BcyC<{V9M?#=|L0&f$c%Wea;9#JXd)vFaB}$oW?>>BxmXw5>E)aFN69$JL z^wy66V$``hQ*qCOPssZE`W(4a&?Ab52%nOgdKd=#HZBfrt?|bXTNnvYO5(JhBxEt| zCIzmSTrQuJ<3>nG2$<3>2yBA+{(L#)^^dhym(b&tAhkP723A%XXXn~lCjcm+;N`^w z`8a3HL=ry7-Y6{Uq}0?<@S(W)_*CFFh=_Di&b*@^g28lH#aw-2L=}x_xsUm zlUFVKVY`}=l0*Ghufruup!>*#y~D;{NqTxVACt<-$n=?UP;hZ!gFW?v6!V)zq7j9F zPO|D@CnFN+*srXu!H#Z$LcW!S6!ZuM2LDyR3}z~YVr0O@Cj+Xs z!Fr0gtfInVXFQ!FTNw!%`GuXGUH&hS2b8k6X9@`ob$j^Z3z|ET=~9uQyWZlE$!c^z z^I=WQ_ml<&6*U>G2*4hEW}1VA#*m|}nwpM>gCirB zU@P9edsk1UcF%D+aRg-o7XY6El=e&dFTe?pfo|GW{cvdM->ukLIw`Rs0ljhH#-438 z05`kssBuEw#3TW9m#r`z9k<$X5OSFH!i0r~^G30=CM1kJaNk+p?&iA8f?X5Zo1D7^ zu{309WvN)}!$_OQo+@yzc-p!@{JsD^rB8wP|E|gSumivYq`|Y6l$1E`tvuwkB%Tcq zGQ&)@88=wSly2g|fDk(v)a;t9{+fsz^uH=gqnC-15v&#Cq%}7fY!PyQcU1}qcYl0o z=LW5UA(NBJVCKY1zt7<9Mr` zVvwnS{I!z+%pA!Q^q>P*)}%LbbL0F+r@y2J(1DFe#5Ff>G_aOFB$aA~-h_$a;o(r@ z2rL5-Jz&ma00dfSO~h>zOe|7_DSF|bo|Z-d3>Yv;(4Zzx)-r#FbSVDb=5pi{5L8l8 zKU`pOASmu~hgXNo7(fyQypISCTAiKlo&%c#d2I*wIsb6t_U4^NDYU-xX|>kN_^?G+ zL&IEn=uVCwTq;4-yu!HSWEuPP?lV$7IPnMLYFJnr29r-Mm zN;)}jcH4nq1I6{2`*!6?s>105@Hhxn=m;GgplZF)(MZ2HZbd_S-_ix?%c5lr%OKJVX6pQxs-MZhN%Kow zBnHDZY@J3{IeDx}rsMu=X}$8vyGY3rn=C<$TB{XZi@&5n+j0MUQ9Mu-93HP?@0vDl zXOd1Qosz$f%DN~!7YexOXyM}0lB5PMLkwK$+=|($f+!OcQ%wvME<``Lk>KO+@~%T4 zDQa}>A4~Y>GeGP8I%=9Jfk5!d1FtEWn3)e*W~QdR494t@FTp4sUOly$TfcWshVk#g zG#%jNLw(X`^bvnR1MKfbUDFMasu;LC>2bO92W;$FBEDQ8VG~KKl<}$RwU@Ue$ZfEZ zJAmGMK=)dC=kXqB7)o~bnf-=E)Ac!DjaU!4^{GXVSE3ce&Y-vctAN)5?)<_+u4e%6 zMGg(r$2hei{TvSN_>aN=+sL?{ZYvO?Axg{2T0H!@$r5m*GH9_6A0Jl)5hB!qg>jsK z`gk8cuw+eHqdgZP1l9yN2lA?_knsKGI&zrrz-N;%Fd&9HZ4>N0Um%45p+L#XiVkJ# zZgbmm9I@#+2DOwGNN3=^-=AIno<3kzUH8i~g+w9m-Z89+0pHhg4UFA^nizF;^|v5^ zL8j<&YhukU?co1+#DD-k#ikAXg4+@8k7#6T@HNTQGcjqRmeS z?(RuBIWddxb=NN2ffJ6n!SDPf3RqfTdo6n3!ZI*0NOXE1LoKJaw)RT@OC6oh31c0{ z08oXvoL_1Q#g9SnJTH$FxCm9#SM>#vAf#BkGHtfi$Upl8LO`MAPOq=WYpq)yhqVr_ z_dJ(5f8(K3nSY|3z!MO24UVPKyB&ODgz3%Ub%5SbIW(&X%H zJHwd{G?>k;Eoi+PhU>6T#58p*Y;;t9?aFl<1Q%WjrC|FZR?uJ(T6G&q0XG9c zUd3R!r};jNES1V(K;060`lle7(yY=57L5xSb}A;O@eQA*^`3jqAb@H4vG^Y9-XDNU z_t~wRvoSw#F#7uX(5H3pFAm8Q?;q`#6`h<|mFcwd1B6gqf=qx_Oe!cyaQ@n1ezrU1 z)xS!f>a~_oD=arM50>7@d)118oxL@{8vSaFzL0{Nx^()gE~o%{38RxGFAOrF?O(Tc zEv4%Aaoj>%;p9&*sx$EGTDp)lWU+Su$CajalKzzI17xP9t%DV z9o^@p7B*(E+Zq~Jz&NNI8^;6lPeDhA=otkm~GzJB_o4bZso;CHJ>ta~ut029c-$XM014#Y)hec%8jR7R(#hru4# zyR3H$tz*DI6$M~q2Hw_60xd9x?d|Q?01KZ8rgW8l`0$~x!Psl@rM#M&oTn!bkOB~R zyi#jXhq4Op|1jt*idLpycY=)|Of2yEiF%i;kmSwKdgiVVDzch%LhFJIE4r33Ry zXvU%aeJfF{AJ`^~ABzJ9f8Jbs5cNVsK!LXD^~*v*6q^}GzO@L+28>h?#upV)oP!|& zeO#$yPk;gb^~<$##HzXHC@XA;k#NOd_JVHO=o3$d7XY!I5+Sz8fJXCoDOqev;5DD zK**jowfm?t=@>Fb;~NBIIgSn|uB8PU^B6YSY&8&Rxn0z6I16V8u8R!r`G6otR-{vn zyh%b=4@wx_K9z=XvPX`9jSJRpIN1SGbrr6(C(hO-C1j^+I~v3g~V^aeKMXqi}iN>Nd=^`J*lfW!?yIIss^vKg><*PuWj4jfr%UJZsL z=)oTpr&b`f7mpxp+Z;`%27=zoY6#2=h|6LDa$p0TBH(V4!@z)m42m(H&J(mmZM9pV z1H`XujiCBo1Y$WPbo3qMzfVhb3JZesQwY!vGr#f(96n;-le69*?0 z*oxA3?~s6@hMGo@ZVJ5Jr(f$>dhGmf006Q2R|h%b?J6iJfTJS`;$CpT9r1Sl(K@g& zYmML-fbjA=fbfDujfleEeUvsyxQ_{7np+gbTC11a8WBu|s?XniQ4qObApnI&jt*bdk-H?A3b2ah6P$pe=>Y1u=?j#w)3xPFMC|gv67R(tq(eE5aQ*+x#6B`q&uZ`N?j+a-$ zxi;-eH4i3%3_gMUDGJJ4Zkx9zU1eXEySu(jbos#=cfuKWz|-Jw{`VLw1KQX#Jyt4R$ z?Mu)^w9<;9*M#``-m6O4XZMH3_ek=Cz>FjqkqcbcvnkDXRp}{acD3;Uk3G1%D`5$CK-_rTP3(eZaN2ma9rg!w*15FAOK z5%LrZ>n5}7O>ocy&h_a;g$uPfc^0SCL*S)8V;UU|hqGaST@Rd6CS1_9RMW{V@D}u% zZLuoU3a4K?vsqd7_J_ZH?`C3zrkqO`Bz}SWYdiIHR!w?aDJFJjMBl1nF^RI*GEsq6 z{}s(vJaiE$6OvUwV|dPpJ_X)#t!~Nk)n=@OJ=3p6-M3?dZFE$pPj_~D#UEhNh_?-B z{Q@4ZpUzfd)cf%kF~sfdT5!Rx2eI$G4M#QH#srCz;_ExXtB}_a!9dcsGu37CpNDVm zz06iP%+9R8=}K=GdjEVvr~1VCqlVu$7H1*tsW8xW7f&7%gx~5niL@r3x___jd}~tx zJDqQthl6#~_{V>r@BWKN!34K#BH}mSNOrSaDII(cD-_oSmEMhkxI~GlJY6^^r^%LVm#d+)cO592Y%a&?o9Cq%aNx? z^8m$iwY?3qH3C18`6kdbQIu{A!+<5uM@=B;`Rfx=A_tGY5Pb2xloae&Ha0-bMg2Zf zu(FyLHFAp6@gd?v6L_6;MT~XRnoJ!`CQOJi=$|o~2&@sYke8xCG{4XLcSEtkRa%Sm zrJxvcqNV*rIfsU|K4+rY&T)Uo*}CbY1i7H*sU2lZm)WRU3#3l z@4J4%scQ|~B}DyMD^qY}k)5=O{)5CkUsXGLli2-BDgx%Dhdp&14-Xa(Z~kj{ z37kLx@k$rT>nr1;prC-YyHlm_>AEXcg;Arr+PV|lvWAc!sE?-;^Iy-;?Z%iVt;gP| z79v8N(~G;$L93U3;umURlC}**qOEi|<^r|+4i5e7oM;@Yf{-IeI}KAq<||h(UUf~l zvPyW5y@R#A)16+Q_1}IJRFB^048P{&;vywRz#6!KH8?P+2PQ_^bS3DKWTg%3^)?m} zJe$~a+XX}<+tvu|VI);i%i zm-M#}YB@CN6n@gOiW4A%YsI3@iXl4KkBF7rD(s8h_855V>hh7&!~Fb5u}f#y8>K^5 z(E$^YOG0(MQI8kfC^fRUN*SZ-JjKPgkmYhz4wvUiNkz|puDMtRB2ZDe;NjAs4;i$; zeTJkDTL#?1Tz#QVFI5vQcM&#hv=YrMagPUOwFVi5v-uJ@T}99dj>&6D_wTT`A8+>dCt0F~!bY-)Gq6}U6+)-LPKKbx7!NS7M)m*8Bdt;oSxGM$6>v}XdOzcha1IXD^T-|1^&RmY_ zj}NEbUyX5P=zPXTVN~;y8ZuE)wworE_n#3E(6dK%_+)O$t7Z_0&CMy3MCIMw*c@{N z-Js5Pv^d-maY+xq{ji>5bD(Y4wb0r6rM0#!)tcM&Pg@y=T;lk|&PS1(&Nn^^7=Bk7 z-?Y2@#{N%P*8$elwsaLmuOOg;3P@3^N(sG4QvvB9qVyutq!~IS5F=P9Qj}1n2ug=Y zuTlbvf)qglB-8|?NeO{K=;hz&yZ5{A{rUI^kdSlsKC@@m%&fJKlgMk)YBlz`D=-+? zevN`yBcW=P{qxw)@>iw?YN%*_zkM=_eth6wv-iz zSZd3pGa-5~-QmP^d8boS_ZC#y!f!7%gSs-8p>URB=im@=hPzmXGh~N_rN+Ne zBl;nEB`1aDtD7GtYRH^QZQS^Ib0G4h`p0SZ8Czc7vr{O8xkb>Kp^}Qt`!~q$GD7VJ zDp%SIt6h-D>xL~+Msb`9EMFgdEQtyv`*Q8g=2pgh;v~A_glFc7fzVMnn{-xa&6q!-|Q~SZWoayW_{T9i_Z{@SzC>1mU!|t<8r13 z+Gbw$#bs^Z%0M3PwlnImW%Z9p2?>_$GOW_D64Kf*0b&w2-h3fZIg&m5vMcU##HSyt zh(J|#bHkpB)zZE0GKa0kbqpdx!q;V@YQJ45!fQuv;-mQT2y1rs&^CswtS;~0Rd;5v z;HvuOqQdUfm3f1kAsL--(U)@#jytmPo@#$0a+gqLDjd=C@UHdVDr5F9pZ8u@RSe{l z0&mJc7Pe^X{C1m>Q}Nw`4iUXDQff6=>-(~7wl%7~mW&^w1gWZj9Dcm9vk1cd5I(qP zV3pLkmb{&oYjHJdxn;%}dRa&857zL<9zUNpN}VnRMY&0mp=T-lO>UakehNB{<^G5q zRm>INK77lBQ{fn3ew8@9ToId^q6jDqJgxrv(YvX zwqc*kHWUBqQ?&hUxZ})$;Y*}5gasv> zoy~$$!o}?rJ*c!tPy)`O9i2YzDl=V&L5Lb%s~mx&XMTRHF%`&wkt%Zv_ctb*R=SVG z$76imt&k0C!c$#4Yt?rK`&s$ufn!R8Re#(N*v)ljKEW^L^!ZK$d2dLgcW>n-m^gzOaSjL^nSlHCtOE$Yr~ ze1}dFHYqx@4@L(VmLs+-3yR*NuQwX$zi#(`Ar`V0dwS-?t?(`txwd?QRuF zq0>l@S6(*uwn3aY0p76-!_~tm6E3n=$Z#ir5Ol_pUD+E6)kN)i%#Ei2Y>b5qW?9aL z8~X&V!*fFUE)s2L^b^E`KAvPfxJF!oYll)~(V??M8w5oas%2MCQl_kx9X$xHolZwY z(;d{|?}UVU7hlNueVB2?M}@yGE$;hZHN2|(Gn9KZl-gG!TIYX6((z$vij)@oc&d_6 zA^XW=&eY8w4UE6l?uOl{Y}<(wP3LViU)WfQ5R0?aKA&XEfPBKuF&bMJ?PBfQS?;$o z!sg8_godS9?cGZ6p(|phph(n&Af3Eg&yujr?OoQC7 zn;%Y6x=^$0=dA9(P4XhXl^fsk>*&Z9DoJSNPV~Zm^!5@hKs)Tit=~25l&x6v|I3^k z_vq>=5q>oa$BZ<9L2wY>mW7MB`{(Q%qs)Ka@~R~p~P0NvdjC3C2p(o4y~G&rTxo)d{BwS2MEL~XCp?kzKsL`Oin z$lQ9&OR=uE#CGfI(^d2GV>L5eT1;GzO}Pq^uLTdy^$($FAXZnIA2P7>6)LOVoKzGL zXb(UiXDT2zqK)ODhV2qB{1XqG5poh@69w)3T&YXP>=#_2$mr#&?Qi(@|8k1K`rVQJ za7S=!ytKvr#W(%cNF>_|x@O~Z3TQk7rzrlRV|gO>O&)BV7FJh)gG=&f&q^9s zXW2H}8&5}bz73tls=u)Kk#SGF(kmOjqn9GUR%ARy)PdxO%KJyp9up`2ICFN&sukQa zXRgEOPS#Q(^}#x&BY1z$M4@nNL6WGWI_kdSNcFtX8igA7NR2vwfkSx0tU%Ac!am%O z{1-CD$)E1T>jX!*d6qOTgqCtq)|RFOYBPb8W}P%Q1iP1e>CvJUhgL~K<9%{<{$?#p zi`!R!0MB9;!E?5-zuzlndP?hSe;~OnNR0m6LujS}H#Ols;Z;k(1Y%O>Z-8`6SGgl% z^?L41RCcZq7J@NVh(uduGoRt}XIw*lME}Lo>F9XJ0Me0n@^dvKY@|~QG9(q7h`DvVdO#P z2r515Zx4)?l{GQv%TJQjKniW?K>CX=BPm+%^1fALZ~NQuk}+tm=|QJ{L923*y-}wY zS+!eDCI*>#c(Qb}`lB%U36MmtC+R^Mjcy=agsW?(D>r=!->Q-hs;P+ZavZ;@C288S zRQP8)lx=M#b|Q8SJW4DT&Rg-V1II2wp!Vf&RV%nAC4iXZ2C{0(sG)#{{DZLxxma zafN5POu%@7-)<}I<0{))$5$5)n9j4%R4PsXAaS4p%t!-gm>p*($5Q zzwQcG(Crp-(2gf5RJ1T~!vIOc!%HPq>s^>NPfa||$KST}e0dowBrb5Pwm@O%Tn8%w zVM7atrPL2|?;RTPL9MH_1`H!6UH``f&VR@>3dt3;zYM`MPR!ZoHiyb0M~sbK4Dx;q zOt=uO9p@ge=#d+%xL|4xQ|bt%z$(ieCn@dcZz@xAj7>AHgDZ=Z+|ltaF|DX$Qr;=S z;Fzi0XZCiSW--4yWr;P$uNQTkA0k{JtcPYfac|@rcf6>_NjpJ+J+$<55ET-V)v<)! z7h)O!nLoZlL1|a&M?SL$xuL81&G#WYFGkLxn%!$?ORqSvi<&K!l4t~xcQ2dBDN z1{7X;Y)Oj0y`L<4`PE>iKRLmyh>06lT_jss!B4>jHCwcVS9O^d--5d4Y#n1Vw!wJ3 zkL1s*rj3DQzc;UFVZS#|nGeHd%?RpC?T+IUCqBws`keNdh{C$^N-_^rXtk*L;seT1 z0$Lw){GqKKIy%9b8vWItZTy6@PQrFlWW?EDAbMI?6X-fqj{E!?+jWkVC1_yo{%w!f zr^-KQGWo!3A+yfni!SP+=S*HC_7p5Eu{^z@plc)`N5XxVEh=pBAswzD=3R|&#hV%@ z*q2<8b?GJ)30w~Q?TXlOe7wyH(f15K73VV@htox>tLqhOuIETqlrnPMwjt!`O;qKc zRaPOYP_9DHo#C;|TASr^dcp<4WuI z-X&JIDKu;qRIo-xuSNZaGB#KL2g)efJ$3a(dp#dK=|(K#5|AjHJ$w4e{DhnIJ%{Wq ziZUV@hb8o)8f$gz=w*u-iAF?#(Gx_Pu-QfDl^1BeqaEoya7B48u;wav30zv*iFbk%0jP;UNLtSMyzoSXO9#Oi;e? zr{6V_!6)rbX0N$^t=`SDaME4-GVti@*CDI$TTx!57Jm($Lk4_uT3UL;k96$q^?C-{ z%x{jyaojWMaGR2Jf;$GB0No~_JN5&np?h9-@S;R zNm}K7EG$Q@6$4QNrT}|Qf_(c5-hJq3XL5Gqb7GoOH1|^F^ekvf!nh-VyT|eR2#&B5Db6J&>p<;hfK z1|QPg-fsG}Y)&9VwH($kvba#&?q+O5=sVFC#J>!6dodqPrp^~jAXPO&-Q{bM_5k>0V2}#06 zKwF8)hgaTZ4`{hRgjM`8#12Bgej(#)LAt8Z6|+@69JI46(gb z59GOQ0{7Zr|G!R2-~%$+LlesY?3BxMPdD6`I+r+ybJG%;#%b%{PYv*Fn$#%|F= zewh^HZHlpMiR_Jib!cZf=FTO2d9KgbisA)q`(JXmuD$kQ(9<&HPM)8L30B#8zih=c zUv8QuUEAN2*_6J=hs@1B%%sEzc~Mb@*cH$+o+e!d4^Y<=K&Pz6Gw^HOsf||5T&4eOg<1ZI^EY%kX77f(_|o zZe}Wr9KQgPOXb#@w3dT_Veogr*s=nc!3S$%xI5@10n_#>+g?jaD;iBEC%am5-^t0Y zotaGvE2_AAX1_(wGX(F3aB!d;vzRxBPo2I0?G%;fsWF}(%up=Nd}d7h7xk_MBGbai z6=qcJ&#@b0^TnKy?eXKAvpuSx`ys!azh7*0co~^Vh@*olF^5;Vo_~XHZ3>=aY7AV{ zWOadgPaiT+91jA%fF?%qr+G*2kK1Eg^ZndQg-mmLuqKF5F9r|Rij6RoIo;0E;CGxz z{M)8>MboF|Sz|9uJU?I2+^~=F`%jT!ncOMMUN`n8brR)-4S6N)`ua;vT`L#V?CEu<^bst=9{R z_Eh?v6xjR?T&Z{aDz7A8I0G}VoiSjAE(>pNu zjh>#$5X$_Jk=xj255O~|s=4v>TRijYYAvn90tUuRK+_^!SL}w&t`YdVK zwK@zx$2Qg3^>(f!*8s0_rJ!*ZZ-9XL$&k6BuixZx0;Z)UF$R|^m#LxfXyerR<0rX` zFpumkq&Vdcjlcw#usThRd$c?2jb18FPDh-&q*4Qe!J=-V$SljZP=5yonNApeL-7+6AeI1NeuVCDbssQzN0xy$tWaHwW}5RHmeI(C5%bU(b@3kNorws*s)C^ucV-mgXZR zG!L`4TS;cSE?@{>Geb1L2YZ>>JkEdn*jz8YNckDQ-vnK}PpWQu!4S1g=t%7Na;@u2 z>k-w^t^4`6Gfe}gBQd1aIoj3PiJ;A7X$gY}>lhVL=FYA(5qt}i$~T?edPQegUmq6} zhO2uKly|oV3XOYnq}^GwBIw4*NIYXM?l}^yQeR?_5i;>Xx!Lrtq4(Fb!geL~-{rEK z0)OXI|7Dg3or5NDyPaZ}P$5X-1?M%lF}w}cy)H{lAQbdm5aW$|=8pDjzTx3R(~`H{q)$&BV#TG&jm5=IZIDLIsh5&hu3RS*4(3FUC7@fJ z;K#jbWES$LmGU^gyNu_p=wEcPY?)L)4dl-guyXmps*zLb>XfP1ucf+^eDojPyNHfN8fV(HE63(FYnoj?!^}A=Yx88WhiWEgrJSJ+!k zOnHRGqptF)y+}0lUd=ms*n2VzUSn1F#Nv@5Q*WaRxFre(CTdZMWz1Zs-TL-=uA_1H zT)>tymGo9gNQtdaH%*wyA(*{;@l|XFWPiBctQ~Cc#v`)n5 zE#eamiPV*xtqe0&dse~MH}nG6Y@r|Rm$Zn6=gr(r2UP}*RNYu{_}+tfsUR?ET+I8I!47; zHt){c-1*4DiMYhLy?Pi(-SF87VC^(#Wgkut;(5-5Iw8iMy*-jFP5F5Xs+g|Su^~_Z zxpKm^p>`8~M*S8I8AERwREk+LvC7^(Rq39R^^x2lI3Z+wS983$oF zcyEFtb5NGuTvCPImrltx`GhN^TsN_Kkn!xEBs8u5lv(*PIfOY^my2t5XNe=j(lF7* zph?=dfUL(c)fM0u`tD-5MxnSRsd)LqqgTfRTt7mj?s7!}oK}{O+jX?j%Ak6+v+JYZ zw^leeH8bg0?rzOLuixvt!iJ^}x%La5U+hsW(nt^o;h3&(<^a|0Wd8@%nKd2&Q~hYk z5Aj0%t$ZubdNLh;p0HV9ECzD5`wOP)ZaN0^o!5KOiI_ANCgsb|V@kweX1)0+UB``P zrmc0~1=fj_Z?8KP7nxYYvq zG#klRW%us>z#;*VaqsU0P{q2H=__0XJ(;;7R>5z5Rnx={vnpF60u}tVuLZU7WAD`6 z6MCn5eA5k-JA9qGkBK(K0nfX<%pkb$!U$~?QMyy1cB_38M;UZFV?r9T`Zmve#y&u> zF!4V9IXTg%Ws#pKGKLqGerK(ae?W-d0SNI*dtYoVBJw4rd9sZ%U0|oNAK48ICq6UH z^I5;Jjp*|e1?YQ1V-Tcgch%OaVfv}{mF`MGwM%q&^YgJ1_Cr$LZpab2!N*3{t1VwN z(foy>r8)3*t1&_#VxNN$eN1O#1%%v)NpW|ZAm^T2qyz<0)yaH*09&jVv-NYxIX zZ7Uz`{&x5j8CvY0<@$Fz`k{LUELTBaLqo9KiI|+x%Pwhf?OL4T#yd=S_0El;`id~i zQW{n%P2x2WG$%6V+|}7Gdz#tYe#H{D>Z9d787}x_e@CMQ&ZTIowD5ufbQ>$^l%1U= z`Z``P=s`)6o*AprLzOyomW!Iiq3Yw)UGX7Rgwm^T*%XXhLh3BAxu%lsN{p{K{BF;s_(=NFor zgF#g}_=tn!a;0k}o=wTzdd|}O`}2sBN&EbxJ~D(M@9x7Wfz8M2jIVRm)xI^vB?vsv zU2seKwlW}|)S#qdgKfYdvMU3E>V1Ut^bQb?}>R^8A5ZY%Bg zLGc^Ih3RzXMWyvarKJ)@JD{15yCQ^IA$9U6Y7W|B`I3aST`6K3&&gUSOXTJ_d3iyU zG^~?KM5>fHqf+aX0+Xu>Z#>Ovy=9)f-DR`C*Hq@vv_-{Y3&kfg*T@iu=fjUsTvGqB z3xMfl0rdGh)5kfj48AK{nPDlsPA&wqFYx#){d3X0qGMj(*%G5$sCa%!87Buoqtd3; zBoidmbnnFS&4q_c$RCv+1(6Rm_&sH2c%r3N$6dr@?8(l;$cf3xZ^X8?*jG#(msJ|z z&W)D@T|&%W3Xb&*bDUoSdJ-VeEI^}y2n}%1wOOwU+2)Bg-Op=kO;v6@$ZyRGx6gv2DRj@mOfNpVB2FR>_xG81HKSiStu!M9)= zRimahlvfOg3S+AL5tg%U@<%5R=i6z>J6Y9gZWagnZw{jzR8U&hSWabOl~BgE%6pc= z>#m8SUftkCJcrPU^AKLsmiw^b8!!t)79bMnH20AeAS#{*R6oaZHt_vcCQxNf?j9(l zl6A#!xC5O8^m|&=!C?EFU(}J7W`b5ur4u(fCic4v_x@h5ZZC1J-vP_Fw^jJZ*#r{~ z=_qOMx&D`)JtL8~s8d1ZS3fsTk!Hp|QE~v6CTVPpo{87zhEGLg@nD>|OeG)Rw-_|) ztaw@q#GsPEOYlp5|DF>29z=Evui&n3$`Nn>^C}Uh3udd!^U~fNB`(SD^4?-Szz5H8vEDSBL z*ZZ;!IC07~r-iWLidTeBn%0{t>Du$%zI_O&VH+B3ko{?GwWTPzQwQqM()#Zp*--w8 zP*c<8zUv9Jtzdum_gXOd5?{1mO~KW%!fN}Ur=4>2<4cWyE2@9H>ag|Gr^OK-7l6_W zb_Y-;jA{}T77u~C28Hru&gHt{p>iPAcrs!+woMKWTs&<3*bJfIPiT|-tsh9JHX{i9 zIp9d7YTYT=9nj!3G5||DAAn(hrBYVOW31QWJpbQ-*et-|hRBZv!-I1hD0HW|x;6w3 zChNaG5D+obaJAA)gute6uhWKGYkjX1F_EqZ?|T4CnR4eLXsCTz{d5FSH2XLJAy0YV zQb5lmH{}rxwm0}RFKg=oY4Zz3rej#GJ|-+|_Gu^}Fy(y&=>oKo!EN}M;$DsGFuN=x z=uO|>^cpMcV6Fa5^90UtS(%3?hl%S;+pX|qBh4_~R572+OFJt~%M(rE%oJ1#eX1f@ zqK9$~bT+=IC0+Wc!F2J8*5mbs6MTG!J_RxTR$7B2pCFHGH zf$S#?n9iSRt3PMMaOU9Z{dwfy|0B-+^V@5>Ka%19_u}j0KsqhG;l7&!J}c-5u#VrK z8aDC`6I=fn0Xr4K{_H^9N^bo}1I$!jb<@f9fFVu%^3NY-(MHbJjg-{*1Inv%D46g+ zk4L>FvGscczi!;P@fYLtgd9*WLu|YB{=7KJlYh~F)ofSWM_)!8aI#)VAnjkW16u%m`#u+w#LmI7 z;L9BI=il@US%8!Rd;upCii*M^VT<{S$gMZDikHAKxtcKc;7KIu^gj8|0UDd;<0u*yR2YTr1Lh^Jd7wZV9i(PTc})4QxTN o(iQ4e_k%tP6$P%|X6*j)jXQJPFF9(X4uOZ3y1rV`&HEw$2ap1P%>V!Z literal 0 HcmV?d00001 From 5b6bffb6c65cb982f0ad118405fc28458011f3d0 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sat, 6 Aug 2022 15:48:39 +0200 Subject: [PATCH 079/130] BUG: Allow IndirectObjects as stream filters (#1211) See 'TABLE 3.4 Entries common to all stream dictionaries' and > Any object in a PDF file may be labeled as an indirect object. Closes #399 --- PyPDF2/filters.py | 7 ++++--- tests/test_filters.py | 7 +++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/PyPDF2/filters.py b/PyPDF2/filters.py index d27d09ac9..cee4ce001 100644 --- a/PyPDF2/filters.py +++ b/PyPDF2/filters.py @@ -38,9 +38,9 @@ import struct import zlib from io import BytesIO -from typing import Any, Dict, Optional, Tuple, Union +from typing import Any, Dict, Optional, Tuple, Union, cast -from .generic import ArrayObject, DictionaryObject, NameObject +from .generic import ArrayObject, DictionaryObject, IndirectObject, NameObject try: from typing import Literal # type: ignore[attr-defined] @@ -506,7 +506,8 @@ def decode( def decode_stream_data(stream: Any) -> Union[str, bytes]: # utils.StreamObject filters = stream.get(SA.FILTER, ()) - + if isinstance(filters, IndirectObject): + filters = cast(ArrayObject, filters.get_object()) if len(filters) and not isinstance(filters[0], NameObject): # we have a single filter instance filters = (filters,) diff --git a/tests/test_filters.py b/tests/test_filters.py index 8733df0ac..36203d5e6 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -219,3 +219,10 @@ def test_lzw_decode_neg1(): for page in reader.pages: page.extract_text() assert exc.value.args[0] == "Missed the stop code in LZWDecode!" + + +def test_issue_399(): + url = "https://corpora.tika.apache.org/base/docs/govdocs1/976/976970.pdf" + name = "tika-976970.pdf" + reader = PdfReader(BytesIO(get_pdf_from_url(url, name=name))) + reader.pages[1].extract_text() From 4963760000a14c3b0539ec053ba805065472833e Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sat, 6 Aug 2022 15:54:12 +0200 Subject: [PATCH 080/130] STY: Minor documentation formatting change --- docs/user/extract-text.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/user/extract-text.md b/docs/user/extract-text.md index 715a62163..211319281 100644 --- a/docs/user/extract-text.md +++ b/docs/user/extract-text.md @@ -10,18 +10,17 @@ page = reader.pages[0] print(page.extract_text()) ``` -you can also select limit the text orientation you want to extract.
-eg:
-to extract only text oriented up +you can also choose to limit the text orientation you want to extract, e.g: + ```python +# extract only text oriented up print(page.extract_text(0)) -``` -to extract text oriented up and turned left -```python + +# extract text oriented up and turned left print(page.extract_text((0, 90))) ``` -refer to [extract\_text](../modules/PageObject.html#PyPDF2._page.PageObject.extract_text) for more details. +Refer to [extract\_text](../modules/PageObject.html#PyPDF2._page.PageObject.extract_text) for more details. ## Why Text Extraction is hard From 406d5f103c2543913ae4b39d1d33dddb29bfb081 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sat, 6 Aug 2022 15:58:22 +0200 Subject: [PATCH 081/130] DOC: Font scrambling --- docs/user/extract-text.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/user/extract-text.md b/docs/user/extract-text.md index 211319281..7f3596d7e 100644 --- a/docs/user/extract-text.md +++ b/docs/user/extract-text.md @@ -124,3 +124,17 @@ comes to characters which are easy to confuse such as `oO0ö`. PyPDF2 also has an edge when it comes to characters which are rare, e.g. 🤰. OCR software will not be able to recognize smileys correctly. + + + +## Attempts to prevent text extraction + +If people who share PDF documents want to prevent text extraction, there are +multiple ways to do so: + +1. Store the contents of the PDF as an image +2. [Use a scrambled font](https://stackoverflow.com/a/43466923/562769) + +However, text extraction cannot be completely prevented if people should still +be able to read the document. In the worst case people can make a screenshot, +print it, scan it, and run OCR over it. From 93514bee4092cc101280d68a42799278bb9088d9 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 7 Aug 2022 09:43:33 +0200 Subject: [PATCH 082/130] TST: Killing Security Mutants (#1212) * Killed 2340 * Killed 2341 * Killed 2342 * Killed 2383 See #1025 --- PyPDF2/_reader.py | 9 +++++++-- PyPDF2/_security.py | 21 ++++++++++++++------- PyPDF2/_writer.py | 4 ++-- tests/test_reader.py | 8 +++++++- tests/test_writer.py | 10 ++++++++-- 5 files changed, 38 insertions(+), 14 deletions(-) diff --git a/PyPDF2/_reader.py b/PyPDF2/_reader.py index d2a080611..d87e60e97 100644 --- a/PyPDF2/_reader.py +++ b/PyPDF2/_reader.py @@ -70,8 +70,13 @@ from .constants import PageAttributes as PG from .constants import PagesAttributes as PA from .constants import TrailerKeys as TK -from .errors import PdfReadError, PdfStreamError, WrongPasswordError, \ - FileNotDecryptedError, EmptyFileError +from .errors import ( + EmptyFileError, + FileNotDecryptedError, + PdfReadError, + PdfStreamError, + WrongPasswordError, +) from .generic import ( ArrayObject, ContentStream, diff --git a/PyPDF2/_security.py b/PyPDF2/_security.py index 495c2e345..28ddad8c6 100644 --- a/PyPDF2/_security.py +++ b/PyPDF2/_security.py @@ -31,11 +31,18 @@ import struct from hashlib import md5 -from typing import Any, Tuple, Union +from typing import Tuple, Union from ._utils import b_, ord_, str_ from .generic import ByteStringObject +try: + from typing import Literal # type: ignore[attr-defined] +except ImportError: + # PEP 586 introduced typing.Literal with Python 3.8 + # For older Python versions, the backport typing_extensions is necessary: + from typing_extensions import Literal # type: ignore[misc] + # ref: pdf1.8 spec section 3.5.2 algorithm 3.2 _encryption_padding = ( b"\x28\xbf\x4e\x5e\x4e\x75\x8a\x41\x64\x00\x4e\x56" @@ -46,8 +53,8 @@ def _alg32( password: Union[str, bytes], - rev: Any, - keylen: Any, + rev: Literal[2, 3, 4], + keylen: int, owner_entry: ByteStringObject, p_entry: int, id1_entry: ByteStringObject, @@ -98,7 +105,7 @@ def _alg32( return md5_hash[:keylen] -def _alg33(owner_pwd: str, user_pwd: str, rev: int, keylen: int) -> bytes: +def _alg33(owner_pwd: str, user_pwd: str, rev: Literal[2, 3, 4], keylen: int) -> bytes: """ Implementation of algorithm 3.3 of the PDF standard security handler, section 3.5.2 of the PDF 1.6 reference. @@ -128,7 +135,7 @@ def _alg33(owner_pwd: str, user_pwd: str, rev: int, keylen: int) -> bytes: return val -def _alg33_1(password: Union[bytes, str], rev: int, keylen: int) -> bytes: +def _alg33_1(password: Union[bytes, str], rev: Literal[2, 3, 4], keylen: int) -> bytes: """Steps 1-4 of algorithm 3.3""" # 1. Pad or truncate the owner password string as described in step 1 of # algorithm 3.2. If there is no owner password, use the user password @@ -166,7 +173,7 @@ def _alg34( """ # 1. Create an encryption key based on the user password string, as # described in algorithm 3.2. - rev = 2 + rev: Literal[2] = 2 keylen = 5 key = _alg32(password, rev, keylen, owner_entry, p_entry, id1_entry) # 2. Encrypt the 32-byte padding string shown in step 1 of algorithm 3.2, @@ -180,7 +187,7 @@ def _alg34( def _alg35( password: Union[str, bytes], - rev: int, + rev: Literal[2, 3, 4], keylen: int, owner_entry: ByteStringObject, p_entry: int, diff --git a/PyPDF2/_writer.py b/PyPDF2/_writer.py index d9448d1bf..40ba7440e 100644 --- a/PyPDF2/_writer.py +++ b/PyPDF2/_writer.py @@ -772,7 +772,7 @@ def encrypt( rev = 2 keylen = int(40 / 8) P = permissions_flag - O = ByteStringObject(_alg33(owner_pwd, user_pwd, rev, keylen)) + O = ByteStringObject(_alg33(owner_pwd, user_pwd, rev, keylen)) # type: ignore[arg-type] ID_1 = ByteStringObject(md5((repr(time.time())).encode("utf8")).digest()) ID_2 = ByteStringObject(md5((repr(random.random())).encode("utf8")).digest()) self._ID = ArrayObject((ID_1, ID_2)) @@ -780,7 +780,7 @@ def encrypt( U, key = _alg34(user_pwd, O, P, ID_1) else: assert rev == 3 - U, key = _alg35(user_pwd, rev, keylen, O, P, ID_1, False) + U, key = _alg35(user_pwd, rev, keylen, O, P, ID_1, False) # type: ignore[arg-type] encrypt = DictionaryObject() encrypt[NameObject(SA.FILTER)] = NameObject("/Standard") encrypt[NameObject("/V")] = NumberObject(V) diff --git a/tests/test_reader.py b/tests/test_reader.py index 875b3f2de..e84228fcd 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -11,7 +11,13 @@ from PyPDF2.constants import ImageAttributes as IA from PyPDF2.constants import PageAttributes as PG from PyPDF2.constants import Ressources as RES -from PyPDF2.errors import PdfReadError, PdfReadWarning, EmptyFileError, FileNotDecryptedError, WrongPasswordError +from PyPDF2.errors import ( + EmptyFileError, + FileNotDecryptedError, + PdfReadError, + PdfReadWarning, + WrongPasswordError, +) from PyPDF2.filters import _xobj_to_image from PyPDF2.generic import Destination diff --git a/tests/test_writer.py b/tests/test_writer.py index 667d6590b..f4eed5b8a 100644 --- a/tests/test_writer.py +++ b/tests/test_writer.py @@ -362,7 +362,7 @@ def test_fill_form(): @pytest.mark.parametrize( "use_128bit", - [(True), (False)], + [True, False], ) def test_encrypt(use_128bit): reader = PdfReader(RESOURCE_ROOT / "form.pdf") @@ -379,15 +379,21 @@ def test_encrypt(use_128bit): with open(tmp_filename, "wb") as output_stream: writer.write(output_stream) + # Test that the data is not there in clear text with open(tmp_filename, "rb") as input_stream: data = input_stream.read() - assert b"foo" not in data + # Test the user password: reader = PdfReader(tmp_filename, password="userpwd") new_text = reader.pages[0].extract_text() assert reader.metadata.get("/Producer") == "PyPDF2" + assert new_text == orig_text + # Test the owner password: + reader = PdfReader(tmp_filename, password="ownerpwd") + new_text = reader.pages[0].extract_text() + assert reader.metadata.get("/Producer") == "PyPDF2" assert new_text == orig_text # Cleanup From dbdc9016da2728a0f3a5ec671a611695acae2247 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 7 Aug 2022 11:37:31 +0200 Subject: [PATCH 083/130] STY: Apply pylint (#1213) --- .pylintrc | 601 +++++++++++++++++++++++++++++++++++++++ Makefile | 3 + PyPDF2/_encryption.py | 2 +- PyPDF2/_page.py | 2 +- PyPDF2/_reader.py | 23 +- PyPDF2/_writer.py | 9 +- PyPDF2/filters.py | 8 +- PyPDF2/generic.py | 3 +- PyPDF2/types.py | 4 +- tests/test_encryption.py | 4 +- tests/test_writer.py | 18 +- 11 files changed, 633 insertions(+), 44 deletions(-) create mode 100644 .pylintrc diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 000000000..9956cc2fc --- /dev/null +++ b/.pylintrc @@ -0,0 +1,601 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-allow-list= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. (This is an alternative name to extension-pkg-allow-list +# for backward compatibility.) +extension-pkg-whitelist= + +# Return non-zero exit code if any of these messages/categories are detected, +# even if score is above --fail-under value. Syntax same as enable. Messages +# specified are enabled, while categories only check already-enabled messages. +fail-on= + +# Specify a score threshold to be exceeded before program exits with error. +fail-under=10.0 + +# Files or directories to be skipped. They should be base names, not paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the ignore-list. The +# regex matches against paths and can be in Posix or Windows format. +ignore-paths= + +# Files or directories matching the regex patterns are skipped. The regex +# matches against base names, not paths. The default value ignores emacs file +# locks +ignore-patterns=^\.# + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Minimum Python version to use for version dependent checks. Will default to +# the version used to run pylint. +py-version=3.6 + +# Discover python modules and packages in the file system subtree. +recursive=no + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, +# UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then re-enable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + R1705, + C0301, # Line too long => black takes care of it + missing-module-docstring, + # Temporarily disable as there are too many things to do: + missing-function-docstring, + missing-class-docstring, + # Temporarily disable as we cannot change it at the moment: + C0103, # non-snake-case method => we have many deprecations + broad-except, + keyword-arg-before-vararg, # TODO: change this before 3.0.0? + redefined-builtin, + # Doesn't lead to better code in many cases: + no-else-continue, + no-else-raise, + no-else-break, + too-few-public-methods, + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'fatal', 'error', 'warning', 'refactor', +# 'convention', and 'info' which contain the number of messages in each +# category, as well as 'statement' which is the total number of statements +# analyzed. This score is used by the global evaluation report (RP0004). +evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it work, +# install the 'python-enchant' package. +spelling-dict= + +# List of comma separated words that should be considered directives if they +# appear and the beginning of a comment and should not be checked. +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +#notes-rgx= + + +[SIMILARITIES] + +# Comments are removed from the similarity computation +ignore-comments=yes + +# Docstrings are removed from the similarity computation +ignore-docstrings=yes + +# Imports are removed from the similarity computation +ignore-imports=no + +# Signatures are removed from the similarity computation +ignore-signatures=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# class is considered mixin if its name matches the mixin-class-rgx option. +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# Regex pattern to define which classes are considered mixins ignore-mixin- +# members is set to 'yes' +mixin-class-rgx=.*[Mm]ixin + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. If left empty, argument names will be checked with the set +# naming style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. If left empty, attribute names will be checked with the set naming +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. If left empty, class attribute names will be checked +# with the set naming style. +#class-attribute-rgx= + +# Naming style matching correct class constant names. +class-const-naming-style=UPPER_CASE + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. If left empty, class constant names will be checked with +# the set naming style. +#class-const-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. If left empty, class names will be checked with the set naming style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. If left empty, constant names will be checked with the set naming +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. If left empty, function names will be checked with the set +# naming style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. If left empty, inline iteration names will be checked +# with the set naming style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. If left empty, method names will be checked with the set naming style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. If left empty, module names will be checked with the set naming style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Regular expression matching correct type variable names. If left empty, type +# variable names will be checked with the set naming style. +#typevar-rgx= + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. If left empty, variable names will be checked with the set +# naming style. +#variable-rgx= + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of names allowed to shadow builtins +allowed-redefined-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules= + +# Output a graph (.gv or any supported image format) of external dependencies +# to the given file (report RP0402 must not be disabled). +ext-import-graph= + +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be +# disabled). +import-graph= + +# Output a graph (.gv or any supported image format) of internal dependencies +# to the given file (report RP0402 must not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[DESIGN] + +# List of regular expressions of class ancestor names to ignore when counting +# public methods (see R0903) +exclude-too-few-public-methods= + +# List of qualified class names to ignore when counting class parents (see +# R0901) +ignored-parents= + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[CLASSES] + +# Warn about protected attribute access inside special methods +check-protected-access-in-special-methods=no + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions=BaseException, + Exception diff --git a/Makefile b/Makefile index 9bbcf6161..b08cb9d51 100644 --- a/Makefile +++ b/Makefile @@ -34,3 +34,6 @@ benchmark: mypy: mypy PyPDF2 --ignore-missing-imports --check-untyped --strict + +pylint: + pylint PyPDF2 diff --git a/PyPDF2/_encryption.py b/PyPDF2/_encryption.py index 81d77f7ab..8a3e95439 100644 --- a/PyPDF2/_encryption.py +++ b/PyPDF2/_encryption.py @@ -848,7 +848,7 @@ def read(encryption_entry: DictionaryObject, first_id_entry: bytes) -> "Encrypti V = encryption_entry.get("/V", 0) if V not in (1, 2, 3, 4, 5): - raise NotImplementedError("Encryption V=%d NOT supported" % V) + raise NotImplementedError(f"Encryption V={V} NOT supported") if V >= 4: filters = encryption_entry["/CF"] diff --git a/PyPDF2/_page.py b/PyPDF2/_page.py index dfe158d68..d4363d774 100644 --- a/PyPDF2/_page.py +++ b/PyPDF2/_page.py @@ -1354,7 +1354,7 @@ def process_operation(operator: bytes, operands: List) -> None: f = font_size * k tm_prev = m if o not in orientations: - return + return None try: if o == 0: if deltaY < -0.8 * f: diff --git a/PyPDF2/_reader.py b/PyPDF2/_reader.py index d87e60e97..945e96386 100644 --- a/PyPDF2/_reader.py +++ b/PyPDF2/_reader.py @@ -1144,8 +1144,7 @@ def get_object(self, indirect_reference: IndirectObject) -> Optional[PdfObject]: f"does not match actual ({idnum} {generation}); " "xref table not zero-indexed." ) - else: - pass # xref table is corrected in non-strict mode + # xref table is corrected in non-strict mode elif idnum != indirect_reference.idnum and self.strict: # some other problem raise PdfReadError( @@ -1255,8 +1254,7 @@ def cache_indirect_object( msg = f"Overwriting cache for {generation} {idnum}" if self.strict: raise PdfReadError(msg) - else: - logger_warning(msg, __name__) + logger_warning(msg, __name__) self.resolved_objects[(generation, idnum)] = obj return obj @@ -1281,10 +1279,7 @@ def read(self, stream: StreamType) -> None: if xref_issue_nr != 0: if self.strict and xref_issue_nr: raise PdfReadError("Broken xref table") - else: - logger_warning( - f"incorrect startxref pointer({xref_issue_nr})", __name__ - ) + logger_warning(f"incorrect startxref pointer({xref_issue_nr})", __name__) # read all cross reference tables and their trailers self._read_xref_tables_and_trailers(stream, startxref, xref_issue_nr) @@ -1479,13 +1474,11 @@ def _read_xref_other_error( raise PdfReadError( "/Prev=0 in the trailer (try opening with strict=False)" ) - else: - logger_warning( - "/Prev=0 in the trailer - assuming there" - " is no previous xref table", - __name__, - ) - return None + logger_warning( + "/Prev=0 in the trailer - assuming there is no previous xref table", + __name__, + ) + return None # bad xref character at startxref. Let's see if we can find # the xref table nearby, as we've observed this error with an # off-by-one before. diff --git a/PyPDF2/_writer.py b/PyPDF2/_writer.py index 40ba7440e..8ecee6439 100644 --- a/PyPDF2/_writer.py +++ b/PyPDF2/_writer.py @@ -305,11 +305,10 @@ def get_page( if pageNumber is not None: # pragma: no cover if page_number is not None: raise ValueError("Please only use the page_number parameter") - else: - deprecate_with_replacement( - "get_page(pageNumber)", "get_page(page_number)", "4.0.0" - ) - page_number = pageNumber + deprecate_with_replacement( + "get_page(pageNumber)", "get_page(page_number)", "4.0.0" + ) + page_number = pageNumber if page_number is None and pageNumber is None: # pragma: no cover raise ValueError("Please specify the page_number") pages = cast(Dict[str, Any], self.get_object(self._pages)) diff --git a/PyPDF2/filters.py b/PyPDF2/filters.py index cee4ce001..9b09b86bb 100644 --- a/PyPDF2/filters.py +++ b/PyPDF2/filters.py @@ -515,13 +515,13 @@ def decode_stream_data(stream: Any) -> Union[str, bytes]: # utils.StreamObject # If there is not data to decode we should not try to decode the data. if data: for filter_type in filters: - if filter_type == FT.FLATE_DECODE or filter_type == FTA.FL: + if filter_type in (FT.FLATE_DECODE, FTA.FL): data = FlateDecode.decode(data, stream.get(SA.DECODE_PARMS)) - elif filter_type == FT.ASCII_HEX_DECODE or filter_type == FTA.AHx: + elif filter_type in (FT.ASCII_HEX_DECODE, FTA.AHx): data = ASCIIHexDecode.decode(data) # type: ignore - elif filter_type == FT.LZW_DECODE or filter_type == FTA.LZW: + elif filter_type in (FT.LZW_DECODE, FTA.LZW): data = LZWDecode.decode(data, stream.get(SA.DECODE_PARMS)) # type: ignore - elif filter_type == FT.ASCII_85_DECODE or filter_type == FTA.A85: + elif filter_type in (FT.ASCII_85_DECODE, FTA.A85): data = ASCII85Decode.decode(data) elif filter_type == FT.DCT_DECODE: data = DCTDecode.decode(data) diff --git a/PyPDF2/generic.py b/PyPDF2/generic.py index 5f6065d5c..23b01fcee 100644 --- a/PyPDF2/generic.py +++ b/PyPDF2/generic.py @@ -806,8 +806,7 @@ def read_unsized_from_steam(stream: StreamType, pdf: Any) -> bytes: # PdfReader ) if pdf is not None and pdf.strict: raise PdfReadError(msg) - else: - logger_warning(msg, __name__) + logger_warning(msg, __name__) pos = stream.tell() s = read_non_whitespace(stream) diff --git a/PyPDF2/types.py b/PyPDF2/types.py index f17e5aa21..0aad5fd6a 100644 --- a/PyPDF2/types.py +++ b/PyPDF2/types.py @@ -26,7 +26,7 @@ BorderArrayType: TypeAlias = List[Union[NameObject, NumberObject, ArrayObject]] OutlineItemType: TypeAlias = Union[OutlineItem, Destination] # BookmarkTypes is deprecated. Use OutlineItemType instead -BookmarkTypes: TypeAlias = OutlineItemType # TODO: remove in version 3.0.0 +BookmarkTypes: TypeAlias = OutlineItemType # Remove with PyPDF2==3.0.0 FitType: TypeAlias = Literal[ "/Fit", "/XYZ", "/FitH", "/FitV", "/FitR", "/FitB", "/FitBH", "/FitBV" ] @@ -40,7 +40,7 @@ # Hence use this for the moment: OutlineType = List[Union[Destination, List[Union[Destination, List[Destination]]]]] # OutlinesType is deprecated. Use OutlineType instead -OutlinesType: TypeAlias = OutlineType # TODO: remove in version 3.0.0 +OutlinesType: TypeAlias = OutlineType # Remove with PyPDF2==3.0.0 LayoutType: TypeAlias = Literal[ "/NoLayout", diff --git a/tests/test_encryption.py b/tests/test_encryption.py index 90c3ff4f7..d6393fcb5 100644 --- a/tests/test_encryption.py +++ b/tests/test_encryption.py @@ -3,7 +3,7 @@ import pytest import PyPDF2 -from PyPDF2 import PdfReader +from PyPDF2 import PasswordType, PdfReader from PyPDF2._encryption import CryptRC4 from PyPDF2.errors import DependencyError, PdfReadError @@ -89,8 +89,6 @@ def test_encryption(name, requres_pycryptodome): ) @pytest.mark.skipif(not HAS_PYCRYPTODOME, reason="No pycryptodome") def test_both_password(name, user_passwd, owner_passwd): - from PyPDF2 import PasswordType - inputfile = RESOURCE_ROOT / "encryption" / name ipdf = PyPDF2.PdfReader(inputfile) assert ipdf.is_encrypted diff --git a/tests/test_writer.py b/tests/test_writer.py index f4eed5b8a..328381e84 100644 --- a/tests/test_writer.py +++ b/tests/test_writer.py @@ -6,7 +6,12 @@ from PyPDF2 import PageObject, PdfMerger, PdfReader, PdfWriter from PyPDF2.errors import PageSizeNotDefinedError -from PyPDF2.generic import RectangleObject, StreamObject +from PyPDF2.generic import ( + IndirectObject, + NameObject, + RectangleObject, + StreamObject, +) from . import get_pdf_from_url @@ -91,7 +96,7 @@ def writer_operate(writer): objects_hash = [o.hash_value() for o in writer._objects] for k, v in writer._idnum_hash.items(): assert v.pdf == writer - assert k in objects_hash, "Missing %s" % v + assert k in objects_hash, f"Missing {v}" tmp_path = "dont_commit_writer.pdf" @@ -430,13 +435,9 @@ def test_add_named_destination(): for page in reader.pages: writer.add_page(page) - from PyPDF2.generic import NameObject - writer.add_named_destination(NameObject("A named dest"), 2) writer.add_named_destination(NameObject("A named dest2"), 2) - from PyPDF2.generic import IndirectObject - assert writer.get_named_dest_root() == [ "A named dest", IndirectObject(7, 0, writer), @@ -460,8 +461,6 @@ def test_add_uri(): for page in reader.pages: writer.add_page(page) - from PyPDF2.generic import RectangleObject - writer.add_uri( 1, "http://www.example.com", @@ -503,8 +502,6 @@ def test_add_link(): for page in reader.pages: writer.add_page(page) - from PyPDF2.generic import RectangleObject - with pytest.warns( PendingDeprecationWarning, match="add_link is deprecated and will be removed in PyPDF2", @@ -632,7 +629,6 @@ def test_write_dict_stream_object(): b"(The single quote operator) ' " b"ET" ) - from PyPDF2.generic import IndirectObject, NameObject stream_object = StreamObject() stream_object[NameObject("/Type")] = NameObject("/Text") From 20e99d9cca0c893a35d0bc959160597eae4c2153 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 7 Aug 2022 12:23:35 +0200 Subject: [PATCH 084/130] TST: Add workflow tests (#1214) --- tests/test_workflows.py | 54 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/tests/test_workflows.py b/tests/test_workflows.py index cbbf614f3..6d3e200bf 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -385,6 +385,26 @@ def test_get_metadata(url, name): "https://corpora.tika.apache.org/base/docs/govdocs1/942/942358.pdf", "tika-942358.pdf", ), + ( + "https://corpora.tika.apache.org/base/docs/govdocs1/911/911260.pdf", + "tika-911260.pdf", + ), + ( + "https://corpora.tika.apache.org/base/docs/govdocs1/992/992472.pdf", + "tika-992472.pdf", + ), + ( + "https://corpora.tika.apache.org/base/docs/govdocs1/978/978477.pdf", + "tika-978477.pdf", + ), + ( + "https://corpora.tika.apache.org/base/docs/govdocs1/960/960317.pdf", + "tika-960317.pdf", + ), + ( + "https://corpora.tika.apache.org/base/docs/govdocs1/930/930513.pdf", + "tika-930513.pdf", + ), ], ) def test_extract_text(url, name): @@ -399,10 +419,14 @@ def test_extract_text(url, name): ( "https://corpora.tika.apache.org/base/docs/govdocs1/938/938702.pdf", "tika-938702.pdf", - ) + ), + ( + "https://corpora.tika.apache.org/base/docs/govdocs1/957/957304.pdf", + "tika-938702.pdf", + ), ], ) -def test_compress(url, name): +def test_compress_raised(url, name): data = BytesIO(get_pdf_from_url(url, name=name)) reader = PdfReader(data) # TODO: which page exactly? @@ -413,6 +437,28 @@ def test_compress(url, name): assert exc.value.args[0] == "Unexpected end of stream" +@pytest.mark.parametrize( + ("url", "name"), + [ + ( + "https://corpora.tika.apache.org/base/docs/govdocs1/915/915194.pdf", + "tika-915194.pdf", + ), + ( + "https://corpora.tika.apache.org/base/docs/govdocs1/950/950337.pdf", + "tika-950337.pdf", + ), + ], +) +def test_compress(url, name): + data = BytesIO(get_pdf_from_url(url, name=name)) + reader = PdfReader(data) + # TODO: which page exactly? + # TODO: Is it reasonable to have an exception here? + for page in reader.pages: + page.compress_content_streams() + + @pytest.mark.parametrize( ("url", "name"), [ @@ -655,6 +701,10 @@ def test_get_xfa(url, name): "https://corpora.tika.apache.org/base/docs/govdocs1/914/914133.pdf", "tika-988698.pdf", ), + ( + "https://corpora.tika.apache.org/base/docs/govdocs1/912/912552.pdf", + "tika-912552.pdf", + ), ], ) def test_get_fonts(url, name): From 6cc253e838b8adcce0ff80a6e804c4536f3f6c98 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 7 Aug 2022 12:27:41 +0200 Subject: [PATCH 085/130] REL: 2.10.0 New Features (ENH): - "with" support for PdfMerger and PdfWriter (#1193) - Add AnnotationBuilder.text(...) to build text annotations (#1202) Bug Fixes (BUG): - Allow IndirectObjects as stream filters (#1211) Documentation (DOC): - Font scrambling - Page vs Content scaling (#1208) - Example for orientation parameter of extract_text (#1206) - Fix AnnotationBuilder parameter formatting (#1204) Developer Experience (DEV): - Add flake8-print (#1203) Maintenance (MAINT): - Introduce WrongPasswordError / FileNotDecryptedError / EmptyFileError (#1201) Full Changelog: https://github.com/py-pdf/PyPDF2/compare/2.9.0...2.10.0 --- CHANGELOG.md | 24 ++++++++++++++++++++++++ PyPDF2/_version.py | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ef7e0d28..1e1f756ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # CHANGELOG + +## Version 2.10.0, 2022-08-07 + +### New Features (ENH) +- "with" support for PdfMerger and PdfWriter (#1193) +- Add AnnotationBuilder.text(...) to build text annotations (#1202) + +### Bug Fixes (BUG) +- Allow IndirectObjects as stream filters (#1211) + +### Documentation (DOC) +- Font scrambling +- Page vs Content scaling (#1208) +- Example for orientation parameter of extract_text (#1206) +- Fix AnnotationBuilder parameter formatting (#1204) + +### Developer Experience (DEV) +- Add flake8-print (#1203) + +### Maintenance (MAINT) +- Introduce WrongPasswordError / FileNotDecryptedError / EmptyFileError (#1201) + +Full Changelog: https://github.com/py-pdf/PyPDF2/compare/2.9.0...2.10.0 + ## Version 2.9.0, 2022-07-31 ### New Features (ENH) diff --git a/PyPDF2/_version.py b/PyPDF2/_version.py index 43ce13db0..1c622223b 100644 --- a/PyPDF2/_version.py +++ b/PyPDF2/_version.py @@ -1 +1 @@ -__version__ = "2.9.0" +__version__ = "2.10.0" From 83fbfb218f031d8363d0e4cf4b19587081a70897 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Mon, 8 Aug 2022 13:54:56 +0200 Subject: [PATCH 086/130] TST: Don't check coverage for deprecated code (#1216) --- PyPDF2/_merger.py | 2 +- PyPDF2/_writer.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/PyPDF2/_merger.py b/PyPDF2/_merger.py index a63fe76cc..3f0b9738a 100644 --- a/PyPDF2/_merger.py +++ b/PyPDF2/_merger.py @@ -686,7 +686,7 @@ def add_bookmark( italic: bool = False, fit: FitType = "/Fit", *args: ZoomArgType, - ) -> IndirectObject: + ) -> IndirectObject: # pragma: no cover """ .. deprecated:: 2.9.0 Use :meth:`add_outline_item` instead. diff --git a/PyPDF2/_writer.py b/PyPDF2/_writer.py index 8ecee6439..ef837324c 100644 --- a/PyPDF2/_writer.py +++ b/PyPDF2/_writer.py @@ -1134,7 +1134,7 @@ def add_bookmark_destination( self, dest: Union[PageObject, TreeObject], parent: Union[None, TreeObject, IndirectObject] = None, - ) -> IndirectObject: + ) -> IndirectObject: # pragma: no cover """ .. deprecated:: 2.9.0 @@ -1180,7 +1180,7 @@ def add_outline_item_dict( @deprecate_bookmark(bookmark="outline_item") def add_bookmark_dict( self, outline_item: OutlineItemType, parent: Optional[TreeObject] = None - ) -> IndirectObject: + ) -> IndirectObject: # pragma: no cover """ .. deprecated:: 2.9.0 From c3c807a37753949a275f9a8bc578457e58bdacbf Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Mon, 8 Aug 2022 14:23:38 +0200 Subject: [PATCH 087/130] TST: 100% coverage for utils.py (#1217) --- PyPDF2/_utils.py | 2 +- tests/test_utils.py | 25 ++++++++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/PyPDF2/_utils.py b/PyPDF2/_utils.py index 72e676693..53da3b82c 100644 --- a/PyPDF2/_utils.py +++ b/PyPDF2/_utils.py @@ -404,7 +404,7 @@ def rename_kwargs( # type: ignore if old_term in kwargs: if new_term in kwargs: raise TypeError( - f"{func_name} received both {old_term} and {new_term} as an argument." + f"{func_name} received both {old_term} and {new_term} as an argument. " f"{old_term} is deprecated. Use {new_term} instead." ) kwargs[new_term] = kwargs.pop(old_term) diff --git a/tests/test_utils.py b/tests/test_utils.py index 954ae9d34..10a6a19fc 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -7,6 +7,7 @@ import PyPDF2._utils from PyPDF2._utils import ( _get_max_pdf_version_header, + deprecate_bookmark, mark_location, matrix_multiply, read_block_backwards, @@ -16,7 +17,7 @@ skip_over_comment, skip_over_whitespace, ) -from PyPDF2.errors import PdfStreamError +from PyPDF2.errors import PdfReadError, PdfStreamError TESTS_ROOT = Path(__file__).parent.resolve() PROJECT_ROOT = TESTS_ROOT.parent @@ -220,3 +221,25 @@ def test_get_max_pdf_version_header(): with pytest.raises(ValueError) as exc: _get_max_pdf_version_header(b"", b"PDF-1.2") assert exc.value.args[0] == "neither b'' nor b'PDF-1.2' are proper headers" + + +def test_read_block_backwards_exception(): + stream = io.BytesIO(b"foobar") + stream.seek(6) + with pytest.raises(PdfReadError) as exc: + read_block_backwards(stream, 7) + assert exc.value.args[0] == "Could not read malformed PDF file" + + +def test_deprecate_bookmark(): + @deprecate_bookmark(old_param="new_param") + def foo(old_param=1, baz=2): + return old_param * baz + + with pytest.raises(TypeError) as exc: + foo(old_param=12, new_param=13) + expected_msg = ( + "foo received both old_param and new_param as an argument. " + "old_param is deprecated. Use new_param instead." + ) + assert exc.value.args[0] == expected_msg From f172e43e934863be647d16cffa722fad698a215b Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Mon, 8 Aug 2022 19:02:44 +0200 Subject: [PATCH 088/130] TST: Writer exception non-binary stream (#1218) --- tests/test_writer.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_writer.py b/tests/test_writer.py index 328381e84..d2c7b6d66 100644 --- a/tests/test_writer.py +++ b/tests/test_writer.py @@ -21,6 +21,20 @@ EXTERNAL_ROOT = Path(PROJECT_ROOT) / "sample-files" +def test_writer_exception_non_binary(tmp_path, caplog): + src = RESOURCE_ROOT / "pdflatex-outline.pdf" + + reader = PdfReader(src) + writer = PdfWriter() + writer.add_page(reader.pages[0]) + + with open(tmp_path / "out.txt", "w") as fp: + with pytest.raises(TypeError): + writer.write_stream(fp) + ending = "to write to is not in binary mode. It may not be written to correctly.\n" + assert caplog.text.endswith(ending) + + def test_writer_clone(): src = RESOURCE_ROOT / "pdflatex-outline.pdf" From 2df8a4ca1f7ff60fa6c40dc4a4337136b27697c1 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Mon, 8 Aug 2022 21:18:12 +0200 Subject: [PATCH 089/130] TST: Increase PdfReader coverage (#1219) --- PyPDF2/_reader.py | 2 +- tests/test_workflows.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/PyPDF2/_reader.py b/PyPDF2/_reader.py index 945e96386..b4e50144c 100644 --- a/PyPDF2/_reader.py +++ b/PyPDF2/_reader.py @@ -693,7 +693,7 @@ def outline(self) -> OutlineType: return self._get_outline() @property - def outlines(self) -> OutlineType: + def outlines(self) -> OutlineType: # pragma: no cover """ .. deprecated:: 2.9.0 diff --git a/tests/test_workflows.py b/tests/test_workflows.py index 6d3e200bf..89808cb0a 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -663,6 +663,10 @@ def test_image_extraction2(url, name): "https://corpora.tika.apache.org/base/docs/govdocs1/918/918137.pdf", "tika-918137.pdf", ), + ( + "https://unglueit-files.s3.amazonaws.com/ebf/7552c42e9280b4476e59e77acc0bc812.pdf", + "7552c42e9280b4476e59e77acc0bc812.pdf", + ), ], ) def test_get_outline(url, name): From 658bf285c109cb26d71807c314f0989627b6364b Mon Sep 17 00:00:00 2001 From: pubpub-zz <4083478+pubpub-zz@users.noreply.github.com> Date: Thu, 11 Aug 2022 21:51:51 +0200 Subject: [PATCH 090/130] BUG: Fix stream truncated prematurely (#1223) Observed in case of \0 - \9 in streams Closes #454 --- PyPDF2/generic.py | 1 + tests/test_generic.py | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/PyPDF2/generic.py b/PyPDF2/generic.py index 23b01fcee..0b92ccbaf 100644 --- a/PyPDF2/generic.py +++ b/PyPDF2/generic.py @@ -492,6 +492,7 @@ def read_string_from_stream( if ntok.isdigit(): tok += ntok else: + stream.seek(-1, 1) # ntok has to be analysed break tok = b_(chr(int(tok, base=8))) elif tok in b"\n\r": diff --git a/tests/test_generic.py b/tests/test_generic.py index 9044eb525..c7ff2ca43 100644 --- a/tests/test_generic.py +++ b/tests/test_generic.py @@ -157,7 +157,12 @@ def test_readStringFromStream_multichar_eol2(): def test_readStringFromStream_excape_digit(): stream = BytesIO(b"x\\1a )") - assert read_string_from_stream(stream) == "\x01 " + assert read_string_from_stream(stream) == "\x01a " + + +def test_readStringFromStream_excape_digit2(): + stream = BytesIO(b"(hello \\1\\2\\3\\4)") + assert read_string_from_stream(stream) == "hello \x01\x02\x03\x04" def test_NameObject(): From d52b8e0d526e53ba6aa41b0af7f188c0d2050a32 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Fri, 12 Aug 2022 22:48:05 +0200 Subject: [PATCH 091/130] TST: PdfReader coverage (#1225) --- PyPDF2/_reader.py | 8 +------- tests/test_reader.py | 12 ++++++++++++ tests/test_workflows.py | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/PyPDF2/_reader.py b/PyPDF2/_reader.py index b4e50144c..f9d201b12 100644 --- a/PyPDF2/_reader.py +++ b/PyPDF2/_reader.py @@ -711,13 +711,7 @@ def _get_outline( # get the outline dictionary and named destinations if CO.OUTLINES in catalog: - try: - lines = cast(DictionaryObject, catalog[CO.OUTLINES]) - except PdfReadError: - # this occurs if the /Outlines object reference is incorrect - # for an example of such a file, see https://unglueit-files.s3.amazonaws.com/ebf/7552c42e9280b4476e59e77acc0bc812.pdf - # so continue to load the file without the Outlines - return outline + lines = cast(DictionaryObject, catalog[CO.OUTLINES]) if isinstance(lines, NullObject): return outline diff --git a/tests/test_reader.py b/tests/test_reader.py index e84228fcd..588d6fc7d 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -1019,11 +1019,18 @@ def test_outline_count(): def test_outline_missing_title(): + # Strict reader = PdfReader(RESOURCE_ROOT / "outline-without-title.pdf", strict=True) with pytest.raises(PdfReadError) as exc: reader.outline assert exc.value.args[0].startswith("Outline Entry Missing /Title attribute:") + # Non-strict + with pytest.raises(ValueError) as exc: + reader = PdfReader(RESOURCE_ROOT / "outline-without-title.pdf", strict=False) + reader.outline + assert exc.value.args[0] == "value must be PdfObject" + def test_named_destination(): # 1st case : the named_dest are stored directly as a dictionnary, PDF1.1 style @@ -1081,3 +1088,8 @@ def test_wrong_password_error(): encrypted_pdf_path, password="definitely_the_wrong_password!", ) + + +def test_get_page_number_by_indirect(): + reader = PdfReader(RESOURCE_ROOT / "crazyones.pdf") + reader._get_page_number_by_indirect(1) diff --git a/tests/test_workflows.py b/tests/test_workflows.py index 89808cb0a..48f54b03d 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -617,6 +617,39 @@ def test_image_extraction(url, name): os.remove(filepath) +def test_image_extraction_strict(): + # Emits log messages + url = "https://corpora.tika.apache.org/base/docs/govdocs1/914/914102.pdf" + name = "tika-914102.pdf" + data = BytesIO(get_pdf_from_url(url, name=name)) + reader = PdfReader(data, strict=True) + + images_extracted = [] + root = Path("extracted-images") + if not root.exists(): + os.mkdir(root) + + for page in reader.pages: + if RES.XOBJECT in page[PG.RESOURCES]: + x_object = page[PG.RESOURCES][RES.XOBJECT].get_object() + + for obj in x_object: + if x_object[obj][IA.SUBTYPE] == "/Image": + extension, byte_stream = _xobj_to_image(x_object[obj]) + if extension is not None: + filename = root / (obj[1:] + extension) + with open(filename, "wb") as img: + img.write(byte_stream) + images_extracted.append(filename) + + # Cleanup + do_cleanup = True # set this to False for manual inspection + if do_cleanup: + for filepath in images_extracted: + if os.path.exists(filepath): + os.remove(filepath) + + @pytest.mark.parametrize( ("url", "name"), [ From 8948878c96a133e777ff66dcca0ab5f69b0ef696 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sat, 13 Aug 2022 06:04:12 +0200 Subject: [PATCH 092/130] TST: Strict get fonts (#1226) --- tests/test_workflows.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/test_workflows.py b/tests/test_workflows.py index 48f54b03d..19cfd1563 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -728,24 +728,32 @@ def test_get_xfa(url, name): @pytest.mark.parametrize( - ("url", "name"), + ("url", "name", "strict"), [ ( "https://corpora.tika.apache.org/base/docs/govdocs1/988/988698.pdf", "tika-988698.pdf", + False, ), ( "https://corpora.tika.apache.org/base/docs/govdocs1/914/914133.pdf", "tika-988698.pdf", + False, ), ( "https://corpora.tika.apache.org/base/docs/govdocs1/912/912552.pdf", "tika-912552.pdf", + False, + ), + ( + "https://corpora.tika.apache.org/base/docs/govdocs1/914/914102.pdf", + "tika-914102.pdf", + True, ), ], ) -def test_get_fonts(url, name): +def test_get_fonts(url, name, strict): data = BytesIO(get_pdf_from_url(url, name=name)) - reader = PdfReader(data) + reader = PdfReader(data, strict=strict) for page in reader.pages: page._get_fonts() From 41e05f80fcea057f253d05d09b809b9abe7c3110 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sat, 13 Aug 2022 07:35:15 +0200 Subject: [PATCH 093/130] DOC: Fix docstring formatting (#1228) --- PyPDF2/_page.py | 12 ++++++------ PyPDF2/_writer.py | 38 ++++++++++++++++++-------------------- PyPDF2/filters.py | 6 ++++++ PyPDF2/generic.py | 4 ++-- PyPDF2/xmp.py | 2 +- 5 files changed, 33 insertions(+), 29 deletions(-) diff --git a/PyPDF2/_page.py b/PyPDF2/_page.py index d4363d774..67081cc7e 100644 --- a/PyPDF2/_page.py +++ b/PyPDF2/_page.py @@ -1466,12 +1466,12 @@ def extract_text( Do not rely on the order of text coming out of this function, as it will change if this function is made more sophisticated. - :params obsolete/Depreciating Tj_sep, TJ_sep: kept for compatibility - :param orientations : (list of) orientations (of the characters) (default: (0,90,270,360)) - single int is equivalent to a singleton ( 0 == (0,) ) + :param Tj_sep: Deprecated. Kept for compatibility until PyPDF2==4.0.0 + :param TJ_sep: Deprecated. Kept for compatibility until PyPDF2==4.0.0 + :param orientations: (list of) orientations (of the characters) (default: (0,90,270,360)) + single int is equivalent to a singleton ( 0 == (0,) ) note: currently only 0(Up),90(turned Left), 180(upside Down),270 (turned Right) - :param space_width : force default space width (if not extracted from font (default: 200) - + :param float space_width: force default space width (if not extracted from font (default: 200) :return: The extracted text """ if len(args) >= 1: @@ -1523,7 +1523,7 @@ def extract_xform_text( """ Extract text from an XObject. - space_width : float = force default space width (if not extracted from font (default 200) + :param float space_width: force default space width (if not extracted from font (default 200) :return: The extracted text """ diff --git a/PyPDF2/_writer.py b/PyPDF2/_writer.py index ef837324c..dd10b2981 100644 --- a/PyPDF2/_writer.py +++ b/PyPDF2/_writer.py @@ -277,8 +277,7 @@ def insert_page(self, page: PageObject, index: int = 0) -> None: Insert a page in this PDF file. The page is usually acquired from a :class:`PdfReader` instance. - :param PageObject page: The page to add to the document. This - argument should be an instance of :class:`PageObject`. + :param PageObject page: The page to add to the document. :param int index: Position at which the page will be inserted. """ self._add_page(page, lambda l, p: l.insert(index, p)) @@ -567,13 +566,14 @@ def append_pages_from_reader( Copy pages from reader to writer. Includes an optional callback parameter which is invoked after pages are appended to the writer. - :param reader: a PdfReader object from which to copy page + :param PdfReader reader: a PdfReader object from which to copy page annotations to this writer object. The writer's annots will then be updated - :callback after_page_append (function): Callback function that is invoked after - each page is appended to the writer. Callback signature: - :param writer_pageref (PDF page reference): Reference to the page - appended to the writer. + :param Callable[[PageObject], None] after_page_append: + Callback function that is invoked after each page is appended to + the writer. Signature includes a reference to the appended page + (delegates to append_pages_from_reader). The single parameter of the + callback is a reference to the page just appended to the document. """ # Get page count from writer and reader reader_num_pages = len(reader.pages) @@ -613,11 +613,11 @@ def update_page_form_field_values( Copy field texts and values from fields to page. If the field links to a parent object, add the information to the parent. - :param page: Page reference from PDF writer where the annotations - and field data will be updated. - :param fields: a Python dictionary of field names (/T) and text + :param PageObject page: Page reference from PDF writer where the + annotations and field data will be updated. + :param dict fields: a Python dictionary of field names (/T) and text values (/V) - :param flags: An integer (0 to 7). The first bit sets ReadOnly, the + :param int flags: An integer (0 to 7). The first bit sets ReadOnly, the second bit sets Required, the third bit sets NoExport. See PDF Reference Table 8.70 for details. """ @@ -684,7 +684,6 @@ def clone_reader_document_root(self, reader: PdfReader) -> None: Copy the reader document root to the writer. :param reader: PdfReader from the document root should be copied. - :callback after_page_append: """ self._root_object = cast(DictionaryObject, reader.trailer[TK.ROOT]) @@ -709,12 +708,11 @@ def clone_document_from_reader( :param reader: PDF file reader instance from which the clone should be created. - :callback after_page_append (function): Callback function that is invoked after - each page is appended to the writer. Signature includes a reference to the - appended page (delegates to appendPagesFromReader). Callback signature: - - :param writer_pageref (PDF page reference): Reference to the page just - appended to the document. + :param Callable[[PageObject], None] after_page_append: + Callback function that is invoked after each page is appended to + the writer. Signature includes a reference to the appended page + (delegates to append_pages_from_reader). The single parameter of the + callback is a reference to the page just appended to the document. """ self.clone_reader_document_root(reader) self.append_pages_from_reader(reader, after_page_append) @@ -1520,10 +1518,10 @@ def add_uri( :param int pagenum: index of the page on which to place the URI action. :param str uri: URI of resource to link to. - :param rect: :class:`RectangleObject` or array of four + :param Tuple[int, int, int, int] rect: :class:`RectangleObject` or array of four integers specifying the clickable rectangular area ``[xLL, yLL, xUR, yUR]``, or string in the form ``"[ xLL yLL xUR yUR ]"``. - :param border: if provided, an array describing border-drawing + :param ArrayObject border: if provided, an array describing border-drawing properties. See the PDF spec for details. No border will be drawn if this argument is omitted. """ diff --git a/PyPDF2/filters.py b/PyPDF2/filters.py index 9b09b86bb..4ac651b39 100644 --- a/PyPDF2/filters.py +++ b/PyPDF2/filters.py @@ -89,6 +89,8 @@ def decode( :param decode_parms: a dictionary of values, understanding the "/Predictor": key only :return: the flate-decoded data. + + :raises PdfReadError: """ if "decodeParms" in kwargs: # pragma: no cover deprecate_with_replacement("decodeParms", "parameters", "4.0.0") @@ -205,6 +207,8 @@ def decode( :param decode_parms: :return: a string conversion in base-7 ASCII, where each of its values v is such that 0 <= ord(v) <= 127. + + :raises PdfStreamError: """ if "decodeParms" in kwargs: # pragma: no cover deprecate_with_replacement("decodeParms", "parameters", "4.0.0") @@ -279,6 +283,8 @@ def decode(self) -> str: algorithm derived from: http://www.rasip.fer.hr/research/compress/algorithms/fund/lz/lzw.html and the PDFReference + + :raises PdfReadError: If the stop code is missing """ cW = self.CLEARDICT baos = "" diff --git a/PyPDF2/generic.py b/PyPDF2/generic.py index 0b92ccbaf..5b3d7e855 100644 --- a/PyPDF2/generic.py +++ b/PyPDF2/generic.py @@ -1974,7 +1974,7 @@ def create_string_object( """ Create a ByteStringObject or a TextStringObject from a string to represent the string. - :param string: A string + :param Union[str, bytes] string: A string :raises TypeError: If string is not of type str or bytes. """ @@ -2098,7 +2098,7 @@ def text( """ Add text annotation. - :param RectangleObject rect: + :param Tuple[int, int, int, int] rect: or array of four integers specifying the clickable rectangular area ``[xLL, yLL, xUR, yUR]`` :param bool open: diff --git a/PyPDF2/xmp.py b/PyPDF2/xmp.py index ff9679fab..06c92132d 100644 --- a/PyPDF2/xmp.py +++ b/PyPDF2/xmp.py @@ -210,7 +210,7 @@ class XmpInformation(PdfObject): An object that represents Adobe XMP metadata. Usually accessed by :py:attr:`xmp_metadata()` - :raises: PdfReadError if XML is invalid + :raises PdfReadError: if XML is invalid """ def __init__(self, stream: ContentStream) -> None: From a85148ae83033de50e59dae7ca621305bb53ef6a Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sat, 13 Aug 2022 22:03:13 +0200 Subject: [PATCH 094/130] MAINT: Split generic.py (#1229) The aim of this refactoring PR is to explicitly define the interface of `PyPDF2.generic` via `__all__` and to structure this big submodule more. I hope this makes it easier to test / expand in future if necessary. Smaller modules should have less merge conflicts. This PR should not change anything for users of PyPDF2. --- PyPDF2/_writer.py | 32 +- PyPDF2/constants.py | 9 + PyPDF2/generic.py | 2337 ---------------------------- PyPDF2/generic/__init__.py | 137 ++ PyPDF2/generic/_annotations.py | 275 ++++ PyPDF2/generic/_base.py | 464 ++++++ PyPDF2/generic/_data_structures.py | 1147 ++++++++++++++ PyPDF2/generic/_outline.py | 35 + PyPDF2/generic/_rectangle.py | 249 +++ PyPDF2/generic/_utils.py | 172 ++ PyPDF2/types.py | 11 +- docs/user/error-hierarchy.png | Bin 0 -> 50377 bytes tests/test_generic.py | 2 +- 13 files changed, 2523 insertions(+), 2347 deletions(-) delete mode 100644 PyPDF2/generic.py create mode 100644 PyPDF2/generic/__init__.py create mode 100644 PyPDF2/generic/_annotations.py create mode 100644 PyPDF2/generic/_base.py create mode 100644 PyPDF2/generic/_data_structures.py create mode 100644 PyPDF2/generic/_outline.py create mode 100644 PyPDF2/generic/_rectangle.py create mode 100644 PyPDF2/generic/_utils.py create mode 100644 docs/user/error-hierarchy.png diff --git a/PyPDF2/_writer.py b/PyPDF2/_writer.py index dd10b2981..bfebf95df 100644 --- a/PyPDF2/_writer.py +++ b/PyPDF2/_writer.py @@ -100,8 +100,8 @@ StreamObject, TextStringObject, TreeObject, - _create_outline_item, create_string_object, + hex_to_rgb, ) from .types import ( BorderArrayType, @@ -1905,6 +1905,36 @@ def _pdf_objectify(obj: Union[Dict[str, Any], str, int, List[Any]]) -> PdfObject ) +def _create_outline_item( + action_ref: IndirectObject, + title: str, + color: Union[Tuple[float, float, float], str, None], + italic: bool, + bold: bool, +) -> TreeObject: + outline_item = TreeObject() + outline_item.update( + { + NameObject("/A"): action_ref, + NameObject("/Title"): create_string_object(title), + } + ) + if color: + if isinstance(color, str): + color = hex_to_rgb(color) + outline_item.update( + {NameObject("/C"): ArrayObject([FloatObject(c) for c in color])} + ) + if italic or bold: + format_flag = 0 + if italic: + format_flag += 1 + if bold: + format_flag += 2 + outline_item.update({NameObject("/F"): NumberObject(format_flag)}) + return outline_item + + class PdfFileWriter(PdfWriter): # pragma: no cover def __init__(self, *args: Any, **kwargs: Any) -> None: deprecate_with_replacement("PdfFileWriter", "PdfWriter") diff --git a/PyPDF2/constants.py b/PyPDF2/constants.py index ceac39865..f8d3faf8f 100644 --- a/PyPDF2/constants.py +++ b/PyPDF2/constants.py @@ -422,6 +422,15 @@ class CatalogDictionary: NEEDS_RENDERING = "/NeedsRendering" # boolean, optional +class OutlineFontFlag(IntFlag): + """ + A class used as an enumerable flag for formatting an outline font + """ + + italic = 1 + bold = 2 + + PDF_KEYS = ( AnnotationDictionaryAttributes, CatalogAttributes, diff --git a/PyPDF2/generic.py b/PyPDF2/generic.py deleted file mode 100644 index 5b3d7e855..000000000 --- a/PyPDF2/generic.py +++ /dev/null @@ -1,2337 +0,0 @@ -# Copyright (c) 2006, Mathieu Fenniak -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are -# met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# * The name of the author may not be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - - -"""Implementation of generic PDF objects (dictionary, number, string, ...).""" -__author__ = "Mathieu Fenniak" -__author_email__ = "biziqe@mathieu.fenniak.net" - -import codecs -import decimal -import hashlib -import logging -import re -from enum import IntFlag -from io import BytesIO -from typing import ( - Any, - Callable, - Dict, - Iterable, - List, - Optional, - Tuple, - Union, - cast, -) - -from ._codecs import ( # noqa: rev_encoding - _pdfdoc_encoding, - _pdfdoc_encoding_rev, - rev_encoding, -) -from ._utils import ( - WHITESPACES, - StreamType, - b_, - deprecate_no_replacement, - deprecate_with_replacement, - hex_str, - hexencode, - logger_warning, - read_non_whitespace, - read_until_regex, - skip_over_comment, - str_, -) -from .constants import CheckboxRadioButtonAttributes, FieldDictionaryAttributes -from .constants import FilterTypes as FT -from .constants import StreamAttributes as SA -from .constants import TypArguments as TA -from .constants import TypFitArguments as TF -from .errors import STREAM_TRUNCATED_PREMATURELY, PdfReadError, PdfStreamError - -logger = logging.getLogger(__name__) -ObjectPrefix = b"/<[tf(n%" -NumberSigns = b"+-" -IndirectPattern = re.compile(rb"[+-]?(\d+)\s+(\d+)\s+R[^a-zA-Z]") - - -class PdfObject: - # function for calculating a hash value - hash_func: Callable[..., "hashlib._Hash"] = hashlib.sha1 - - def hash_value_data(self) -> bytes: - return ("%s" % self).encode() - - def hash_value(self) -> bytes: - return ( - "%s:%s" - % ( - self.__class__.__name__, - self.hash_func(self.hash_value_data()).hexdigest(), - ) - ).encode() - - def get_object(self) -> Optional["PdfObject"]: - """Resolve indirect references.""" - return self - - def getObject(self) -> Optional["PdfObject"]: # pragma: no cover - deprecate_with_replacement("getObject", "get_object") - return self.get_object() - - def write_to_stream( - self, stream: StreamType, encryption_key: Union[None, str, bytes] - ) -> None: - raise NotImplementedError - - -class NullObject(PdfObject): - def write_to_stream( - self, stream: StreamType, encryption_key: Union[None, str, bytes] - ) -> None: - stream.write(b"null") - - @staticmethod - def read_from_stream(stream: StreamType) -> "NullObject": - nulltxt = stream.read(4) - if nulltxt != b"null": - raise PdfReadError("Could not read Null object") - return NullObject() - - def writeToStream( - self, stream: StreamType, encryption_key: Union[None, str, bytes] - ) -> None: # pragma: no cover - deprecate_with_replacement("writeToStream", "write_to_stream") - self.write_to_stream(stream, encryption_key) - - def __repr__(self) -> str: - return "NullObject" - - @staticmethod - def readFromStream(stream: StreamType) -> "NullObject": # pragma: no cover - deprecate_with_replacement("readFromStream", "read_from_stream") - return NullObject.read_from_stream(stream) - - -class BooleanObject(PdfObject): - def __init__(self, value: Any) -> None: - self.value = value - - def __eq__(self, __o: object) -> bool: - if isinstance(__o, BooleanObject): - return self.value == __o.value - elif isinstance(__o, bool): - return self.value == __o - else: - return False - - def __repr__(self) -> str: - return "True" if self.value else "False" - - def write_to_stream( - self, stream: StreamType, encryption_key: Union[None, str, bytes] - ) -> None: - if self.value: - stream.write(b"true") - else: - stream.write(b"false") - - def writeToStream( - self, stream: StreamType, encryption_key: Union[None, str, bytes] - ) -> None: # pragma: no cover - deprecate_with_replacement("writeToStream", "write_to_stream") - self.write_to_stream(stream, encryption_key) - - @staticmethod - def read_from_stream(stream: StreamType) -> "BooleanObject": - word = stream.read(4) - if word == b"true": - return BooleanObject(True) - elif word == b"fals": - stream.read(1) - return BooleanObject(False) - else: - raise PdfReadError("Could not read Boolean object") - - @staticmethod - def readFromStream(stream: StreamType) -> "BooleanObject": # pragma: no cover - deprecate_with_replacement("readFromStream", "read_from_stream") - return BooleanObject.read_from_stream(stream) - - -class ArrayObject(list, PdfObject): - def items(self) -> Iterable[Any]: - """ - Emulate DictionaryObject.items for a list - (index, object) - """ - return enumerate(self) - - def write_to_stream( - self, stream: StreamType, encryption_key: Union[None, str, bytes] - ) -> None: - stream.write(b"[") - for data in self: - stream.write(b" ") - data.write_to_stream(stream, encryption_key) - stream.write(b" ]") - - def writeToStream( - self, stream: StreamType, encryption_key: Union[None, str, bytes] - ) -> None: # pragma: no cover - deprecate_with_replacement("writeToStream", "write_to_stream") - self.write_to_stream(stream, encryption_key) - - @staticmethod - def read_from_stream( - stream: StreamType, - pdf: Any, - forced_encoding: Union[None, str, List[str], Dict[int, str]] = None, - ) -> "ArrayObject": # PdfReader - arr = ArrayObject() - tmp = stream.read(1) - if tmp != b"[": - raise PdfReadError("Could not read array") - while True: - # skip leading whitespace - tok = stream.read(1) - while tok.isspace(): - tok = stream.read(1) - stream.seek(-1, 1) - # check for array ending - peekahead = stream.read(1) - if peekahead == b"]": - break - stream.seek(-1, 1) - # read and append obj - arr.append(read_object(stream, pdf, forced_encoding)) - return arr - - @staticmethod - def readFromStream( - stream: StreamType, pdf: Any # PdfReader - ) -> "ArrayObject": # pragma: no cover - deprecate_with_replacement("readFromStream", "read_from_stream") - return ArrayObject.read_from_stream(stream, pdf) - - -class IndirectObject(PdfObject): - def __init__(self, idnum: int, generation: int, pdf: Any) -> None: # PdfReader - self.idnum = idnum - self.generation = generation - self.pdf = pdf - - def get_object(self) -> Optional[PdfObject]: - obj = self.pdf.get_object(self) - if obj is None: - return None - return obj.get_object() - - def __repr__(self) -> str: - return f"IndirectObject({self.idnum!r}, {self.generation!r}, {id(self.pdf)})" - - def __eq__(self, other: Any) -> bool: - return ( - other is not None - and isinstance(other, IndirectObject) - and self.idnum == other.idnum - and self.generation == other.generation - and self.pdf is other.pdf - ) - - def __ne__(self, other: Any) -> bool: - return not self.__eq__(other) - - def write_to_stream( - self, stream: StreamType, encryption_key: Union[None, str, bytes] - ) -> None: - stream.write(b_(f"{self.idnum} {self.generation} R")) - - def writeToStream( - self, stream: StreamType, encryption_key: Union[None, str, bytes] - ) -> None: # pragma: no cover - deprecate_with_replacement("writeToStream", "write_to_stream") - self.write_to_stream(stream, encryption_key) - - @staticmethod - def read_from_stream(stream: StreamType, pdf: Any) -> "IndirectObject": # PdfReader - idnum = b"" - while True: - tok = stream.read(1) - if not tok: - raise PdfStreamError(STREAM_TRUNCATED_PREMATURELY) - if tok.isspace(): - break - idnum += tok - generation = b"" - while True: - tok = stream.read(1) - if not tok: - raise PdfStreamError(STREAM_TRUNCATED_PREMATURELY) - if tok.isspace(): - if not generation: - continue - break - generation += tok - r = read_non_whitespace(stream) - if r != b"R": - raise PdfReadError( - f"Error reading indirect object reference at byte {hex_str(stream.tell())}" - ) - return IndirectObject(int(idnum), int(generation), pdf) - - @staticmethod - def readFromStream( - stream: StreamType, pdf: Any # PdfReader - ) -> "IndirectObject": # pragma: no cover - deprecate_with_replacement("readFromStream", "read_from_stream") - return IndirectObject.read_from_stream(stream, pdf) - - -class FloatObject(decimal.Decimal, PdfObject): - def __new__( - cls, value: Union[str, Any] = "0", context: Optional[Any] = None - ) -> "FloatObject": - try: - return decimal.Decimal.__new__(cls, str_(value), context) - except Exception: - try: - return decimal.Decimal.__new__(cls, str(value)) - except decimal.InvalidOperation: - # If this isn't a valid decimal (happens in malformed PDFs) - # fallback to 0 - logger_warning(f"Invalid FloatObject {value}", __name__) - return decimal.Decimal.__new__(cls, "0") - - def __repr__(self) -> str: - if self == self.to_integral(): - return str(self.quantize(decimal.Decimal(1))) - else: - # Standard formatting adds useless extraneous zeros. - o = f"{self:.5f}" - # Remove the zeros. - while o and o[-1] == "0": - o = o[:-1] - return o - - def as_numeric(self) -> float: - return float(repr(self).encode("utf8")) - - def write_to_stream( - self, stream: StreamType, encryption_key: Union[None, str, bytes] - ) -> None: - stream.write(repr(self).encode("utf8")) - - def writeToStream( - self, stream: StreamType, encryption_key: Union[None, str, bytes] - ) -> None: # pragma: no cover - deprecate_with_replacement("writeToStream", "write_to_stream") - self.write_to_stream(stream, encryption_key) - - -class NumberObject(int, PdfObject): - NumberPattern = re.compile(b"[^+-.0-9]") - - def __new__(cls, value: Any) -> "NumberObject": - val = int(value) - try: - return int.__new__(cls, val) - except OverflowError: - return int.__new__(cls, 0) - - def as_numeric(self) -> int: - return int(repr(self).encode("utf8")) - - def write_to_stream( - self, stream: StreamType, encryption_key: Union[None, str, bytes] - ) -> None: - stream.write(repr(self).encode("utf8")) - - def writeToStream( - self, stream: StreamType, encryption_key: Union[None, str, bytes] - ) -> None: # pragma: no cover - deprecate_with_replacement("writeToStream", "write_to_stream") - self.write_to_stream(stream, encryption_key) - - @staticmethod - def read_from_stream(stream: StreamType) -> Union["NumberObject", FloatObject]: - num = read_until_regex(stream, NumberObject.NumberPattern) - if num.find(b".") != -1: - return FloatObject(num) - return NumberObject(num) - - @staticmethod - def readFromStream( - stream: StreamType, - ) -> Union["NumberObject", FloatObject]: # pragma: no cover - deprecate_with_replacement("readFromStream", "read_from_stream") - return NumberObject.read_from_stream(stream) - - -def readHexStringFromStream( - stream: StreamType, -) -> Union["TextStringObject", "ByteStringObject"]: # pragma: no cover - deprecate_with_replacement( - "readHexStringFromStream", "read_hex_string_from_stream", "4.0.0" - ) - return read_hex_string_from_stream(stream) - - -def read_hex_string_from_stream( - stream: StreamType, - forced_encoding: Union[None, str, List[str], Dict[int, str]] = None, -) -> Union["TextStringObject", "ByteStringObject"]: - stream.read(1) - txt = "" - x = b"" - while True: - tok = read_non_whitespace(stream) - if not tok: - raise PdfStreamError(STREAM_TRUNCATED_PREMATURELY) - if tok == b">": - break - x += tok - if len(x) == 2: - txt += chr(int(x, base=16)) - x = b"" - if len(x) == 1: - x += b"0" - if len(x) == 2: - txt += chr(int(x, base=16)) - return create_string_object(b_(txt), forced_encoding) - - -def readStringFromStream( - stream: StreamType, - forced_encoding: Union[None, str, List[str], Dict[int, str]] = None, -) -> Union["TextStringObject", "ByteStringObject"]: # pragma: no cover - deprecate_with_replacement( - "readStringFromStream", "read_string_from_stream", "4.0.0" - ) - return read_string_from_stream(stream, forced_encoding) - - -def read_string_from_stream( - stream: StreamType, - forced_encoding: Union[None, str, List[str], Dict[int, str]] = None, -) -> Union["TextStringObject", "ByteStringObject"]: - tok = stream.read(1) - parens = 1 - txt = b"" - while True: - tok = stream.read(1) - if not tok: - raise PdfStreamError(STREAM_TRUNCATED_PREMATURELY) - if tok == b"(": - parens += 1 - elif tok == b")": - parens -= 1 - if parens == 0: - break - elif tok == b"\\": - tok = stream.read(1) - escape_dict = { - b"n": b"\n", - b"r": b"\r", - b"t": b"\t", - b"b": b"\b", - b"f": b"\f", - b"c": rb"\c", - b"(": b"(", - b")": b")", - b"/": b"/", - b"\\": b"\\", - b" ": b" ", - b"%": b"%", - b"<": b"<", - b">": b">", - b"[": b"[", - b"]": b"]", - b"#": b"#", - b"_": b"_", - b"&": b"&", - b"$": b"$", - } - try: - tok = escape_dict[tok] - except KeyError: - if tok.isdigit(): - # "The number ddd may consist of one, two, or three - # octal digits; high-order overflow shall be ignored. - # Three octal digits shall be used, with leading zeros - # as needed, if the next character of the string is also - # a digit." (PDF reference 7.3.4.2, p 16) - for _ in range(2): - ntok = stream.read(1) - if ntok.isdigit(): - tok += ntok - else: - stream.seek(-1, 1) # ntok has to be analysed - break - tok = b_(chr(int(tok, base=8))) - elif tok in b"\n\r": - # This case is hit when a backslash followed by a line - # break occurs. If it's a multi-char EOL, consume the - # second character: - tok = stream.read(1) - if tok not in b"\n\r": - stream.seek(-1, 1) - # Then don't add anything to the actual string, since this - # line break was escaped: - tok = b"" - else: - msg = rf"Unexpected escaped string: {tok.decode('utf8')}" - logger_warning(msg, __name__) - txt += tok - return create_string_object(txt, forced_encoding) - - -class ByteStringObject(bytes, PdfObject): - """ - Represents a string object where the text encoding could not be determined. - This occurs quite often, as the PDF spec doesn't provide an alternate way to - represent strings -- for example, the encryption data stored in files (like - /O) is clearly not text, but is still stored in a "String" object. - """ - - @property - def original_bytes(self) -> bytes: - """For compatibility with TextStringObject.original_bytes.""" - return self - - def write_to_stream( - self, stream: StreamType, encryption_key: Union[None, str, bytes] - ) -> None: - bytearr = self - if encryption_key: - from ._security import RC4_encrypt - - bytearr = RC4_encrypt(encryption_key, bytearr) # type: ignore - stream.write(b"<") - stream.write(hexencode(bytearr)) - stream.write(b">") - - def writeToStream( - self, stream: StreamType, encryption_key: Union[None, str, bytes] - ) -> None: # pragma: no cover - deprecate_with_replacement("writeToStream", "write_to_stream") - self.write_to_stream(stream, encryption_key) - - -class TextStringObject(str, PdfObject): - """ - Represents a string object that has been decoded into a real unicode string. - If read from a PDF document, this string appeared to match the - PDFDocEncoding, or contained a UTF-16BE BOM mark to cause UTF-16 decoding to - occur. - """ - - autodetect_pdfdocencoding = False - autodetect_utf16 = False - - @property - def original_bytes(self) -> bytes: - """ - It is occasionally possible that a text string object gets created where - a byte string object was expected due to the autodetection mechanism -- - if that occurs, this "original_bytes" property can be used to - back-calculate what the original encoded bytes were. - """ - return self.get_original_bytes() - - def get_original_bytes(self) -> bytes: - # We're a text string object, but the library is trying to get our raw - # bytes. This can happen if we auto-detected this string as text, but - # we were wrong. It's pretty common. Return the original bytes that - # would have been used to create this object, based upon the autodetect - # method. - if self.autodetect_utf16: - return codecs.BOM_UTF16_BE + self.encode("utf-16be") - elif self.autodetect_pdfdocencoding: - return encode_pdfdocencoding(self) - else: - raise Exception("no information about original bytes") - - def write_to_stream( - self, stream: StreamType, encryption_key: Union[None, str, bytes] - ) -> None: - # Try to write the string out as a PDFDocEncoding encoded string. It's - # nicer to look at in the PDF file. Sadly, we take a performance hit - # here for trying... - try: - bytearr = encode_pdfdocencoding(self) - except UnicodeEncodeError: - bytearr = codecs.BOM_UTF16_BE + self.encode("utf-16be") - if encryption_key: - from ._security import RC4_encrypt - - bytearr = RC4_encrypt(encryption_key, bytearr) - obj = ByteStringObject(bytearr) - obj.write_to_stream(stream, None) - else: - stream.write(b"(") - for c in bytearr: - if not chr(c).isalnum() and c != b" ": - # This: - # stream.write(b_(rf"\{c:0>3o}")) - # gives - # https://github.com/davidhalter/parso/issues/207 - stream.write(b_("\\%03o" % c)) - else: - stream.write(b_(chr(c))) - stream.write(b")") - - def writeToStream( - self, stream: StreamType, encryption_key: Union[None, str, bytes] - ) -> None: # pragma: no cover - deprecate_with_replacement("writeToStream", "write_to_stream") - self.write_to_stream(stream, encryption_key) - - -class NameObject(str, PdfObject): - delimiter_pattern = re.compile(rb"\s+|[\(\)<>\[\]{}/%]") - surfix = b"/" - - def write_to_stream( - self, stream: StreamType, encryption_key: Union[None, str, bytes] - ) -> None: - stream.write(b_(self)) - - def writeToStream( - self, stream: StreamType, encryption_key: Union[None, str, bytes] - ) -> None: # pragma: no cover - deprecate_with_replacement("writeToStream", "write_to_stream") - self.write_to_stream(stream, encryption_key) - - @staticmethod - def read_from_stream(stream: StreamType, pdf: Any) -> "NameObject": # PdfReader - name = stream.read(1) - if name != NameObject.surfix: - raise PdfReadError("name read error") - name += read_until_regex(stream, NameObject.delimiter_pattern, ignore_eof=True) - try: - try: - ret = name.decode("utf-8") - except (UnicodeEncodeError, UnicodeDecodeError): - ret = name.decode("gbk") - return NameObject(ret) - except (UnicodeEncodeError, UnicodeDecodeError) as e: - # Name objects should represent irregular characters - # with a '#' followed by the symbol's hex number - if not pdf.strict: - logger_warning("Illegal character in Name Object", __name__) - return NameObject(name) - else: - raise PdfReadError("Illegal character in Name Object") from e - - @staticmethod - def readFromStream( - stream: StreamType, pdf: Any # PdfReader - ) -> "NameObject": # pragma: no cover - deprecate_with_replacement("readFromStream", "read_from_stream") - return NameObject.read_from_stream(stream, pdf) - - -class DictionaryObject(dict, PdfObject): - def raw_get(self, key: Any) -> Any: - return dict.__getitem__(self, key) - - def __setitem__(self, key: Any, value: Any) -> Any: - if not isinstance(key, PdfObject): - raise ValueError("key must be PdfObject") - if not isinstance(value, PdfObject): - raise ValueError("value must be PdfObject") - return dict.__setitem__(self, key, value) - - def setdefault(self, key: Any, value: Optional[Any] = None) -> Any: - if not isinstance(key, PdfObject): - raise ValueError("key must be PdfObject") - if not isinstance(value, PdfObject): - raise ValueError("value must be PdfObject") - return dict.setdefault(self, key, value) # type: ignore - - def __getitem__(self, key: Any) -> PdfObject: - return dict.__getitem__(self, key).get_object() - - @property - def xmp_metadata(self) -> Optional[PdfObject]: - """ - Retrieve XMP (Extensible Metadata Platform) data relevant to the - this object, if available. - - Stability: Added in v1.12, will exist for all future v1.x releases. - @return Returns a {@link #xmp.XmpInformation XmlInformation} instance - that can be used to access XMP metadata from the document. Can also - return None if no metadata was found on the document root. - """ - from .xmp import XmpInformation - - metadata = self.get("/Metadata", None) - if metadata is None: - return None - metadata = metadata.get_object() - - if not isinstance(metadata, XmpInformation): - metadata = XmpInformation(metadata) - self[NameObject("/Metadata")] = metadata - return metadata - - def getXmpMetadata( - self, - ) -> Optional[PdfObject]: # pragma: no cover - """ - .. deprecated:: 1.28.3 - - Use :meth:`xmp_metadata` instead. - """ - deprecate_with_replacement("getXmpMetadata", "xmp_metadata") - return self.xmp_metadata - - @property - def xmpMetadata(self) -> Optional[PdfObject]: # pragma: no cover - """ - .. deprecated:: 1.28.3 - - Use :meth:`xmp_metadata` instead. - """ - deprecate_with_replacement("xmpMetadata", "xmp_metadata") - return self.xmp_metadata - - def write_to_stream( - self, stream: StreamType, encryption_key: Union[None, str, bytes] - ) -> None: - stream.write(b"<<\n") - for key, value in list(self.items()): - key.write_to_stream(stream, encryption_key) - stream.write(b" ") - value.write_to_stream(stream, encryption_key) - stream.write(b"\n") - stream.write(b">>") - - def writeToStream( - self, stream: StreamType, encryption_key: Union[None, str, bytes] - ) -> None: # pragma: no cover - deprecate_with_replacement("writeToStream", "write_to_stream") - self.write_to_stream(stream, encryption_key) - - @staticmethod - def read_from_stream( - stream: StreamType, - pdf: Any, # PdfReader - forced_encoding: Union[None, str, List[str], Dict[int, str]] = None, - ) -> "DictionaryObject": - def get_next_obj_pos( - p: int, p1: int, rem_gens: List[int], pdf: Any - ) -> int: # PdfReader - l = pdf.xref[rem_gens[0]] - for o in l: - if p1 > l[o] and p < l[o]: - p1 = l[o] - if len(rem_gens) == 1: - return p1 - else: - return get_next_obj_pos(p, p1, rem_gens[1:], pdf) - - def read_unsized_from_steam(stream: StreamType, pdf: Any) -> bytes: # PdfReader - # we are just pointing at beginning of the stream - eon = get_next_obj_pos(stream.tell(), 2**32, list(pdf.xref), pdf) - 1 - curr = stream.tell() - rw = stream.read(eon - stream.tell()) - p = rw.find(b"endstream") - if p < 0: - raise PdfReadError( - f"Unable to find 'endstream' marker for obj starting at {curr}." - ) - stream.seek(curr + p + 9) - return rw[: p - 1] - - tmp = stream.read(2) - if tmp != b"<<": - raise PdfReadError( - f"Dictionary read error at byte {hex_str(stream.tell())}: " - "stream must begin with '<<'" - ) - data: Dict[Any, Any] = {} - while True: - tok = read_non_whitespace(stream) - if tok == b"\x00": - continue - elif tok == b"%": - stream.seek(-1, 1) - skip_over_comment(stream) - continue - if not tok: - raise PdfStreamError(STREAM_TRUNCATED_PREMATURELY) - - if tok == b">": - stream.read(1) - break - stream.seek(-1, 1) - key = read_object(stream, pdf) - tok = read_non_whitespace(stream) - stream.seek(-1, 1) - value = read_object(stream, pdf, forced_encoding) - if not data.get(key): - data[key] = value - else: - # multiple definitions of key not permitted - msg = ( - f"Multiple definitions in dictionary at byte " - f"{hex_str(stream.tell())} for key {key}" - ) - if pdf is not None and pdf.strict: - raise PdfReadError(msg) - logger_warning(msg, __name__) - - pos = stream.tell() - s = read_non_whitespace(stream) - if s == b"s" and stream.read(5) == b"tream": - eol = stream.read(1) - # odd PDF file output has spaces after 'stream' keyword but before EOL. - # patch provided by Danial Sandler - while eol == b" ": - eol = stream.read(1) - if eol not in (b"\n", b"\r"): - raise PdfStreamError("Stream data must be followed by a newline") - if eol == b"\r": - # read \n after - if stream.read(1) != b"\n": - stream.seek(-1, 1) - # this is a stream object, not a dictionary - if SA.LENGTH not in data: - raise PdfStreamError("Stream length not defined") - length = data[SA.LENGTH] - if isinstance(length, IndirectObject): - t = stream.tell() - length = pdf.get_object(length) - stream.seek(t, 0) - pstart = stream.tell() - data["__streamdata__"] = stream.read(length) - e = read_non_whitespace(stream) - ndstream = stream.read(8) - if (e + ndstream) != b"endstream": - # (sigh) - the odd PDF file has a length that is too long, so - # we need to read backwards to find the "endstream" ending. - # ReportLab (unknown version) generates files with this bug, - # and Python users into PDF files tend to be our audience. - # we need to do this to correct the streamdata and chop off - # an extra character. - pos = stream.tell() - stream.seek(-10, 1) - end = stream.read(9) - if end == b"endstream": - # we found it by looking back one character further. - data["__streamdata__"] = data["__streamdata__"][:-1] - elif not pdf.strict: - stream.seek(pstart, 0) - data["__streamdata__"] = read_unsized_from_steam(stream, pdf) - pos = stream.tell() - else: - stream.seek(pos, 0) - raise PdfReadError( - "Unable to find 'endstream' marker after stream at byte " - f"{hex_str(stream.tell())} (nd='{ndstream!r}', end='{end!r}')." - ) - else: - stream.seek(pos, 0) - if "__streamdata__" in data: - return StreamObject.initialize_from_dictionary(data) - else: - retval = DictionaryObject() - retval.update(data) - return retval - - @staticmethod - def readFromStream( - stream: StreamType, pdf: Any # PdfReader - ) -> "DictionaryObject": # pragma: no cover - deprecate_with_replacement("readFromStream", "read_from_stream") - return DictionaryObject.read_from_stream(stream, pdf) - - -class TreeObject(DictionaryObject): - def __init__(self) -> None: - DictionaryObject.__init__(self) - - def hasChildren(self) -> bool: # pragma: no cover - deprecate_with_replacement("hasChildren", "has_children", "4.0.0") - return self.has_children() - - def has_children(self) -> bool: - return "/First" in self - - def __iter__(self) -> Any: - return self.children() - - def children(self) -> Optional[Any]: - if not self.has_children(): - return - - child = self["/First"] - while True: - yield child - if child == self["/Last"]: - return - child = child["/Next"] # type: ignore - - def addChild(self, child: Any, pdf: Any) -> None: # pragma: no cover - deprecate_with_replacement("addChild", "add_child") - self.add_child(child, pdf) - - def add_child(self, child: Any, pdf: Any) -> None: # PdfReader - child_obj = child.get_object() - child = pdf.get_reference(child_obj) - assert isinstance(child, IndirectObject) - - prev: Optional[DictionaryObject] - if "/First" not in self: - self[NameObject("/First")] = child - self[NameObject("/Count")] = NumberObject(0) - prev = None - else: - prev = cast( - DictionaryObject, self["/Last"] - ) # TABLE 8.3 Entries in the outline dictionary - - self[NameObject("/Last")] = child - self[NameObject("/Count")] = NumberObject(self[NameObject("/Count")] + 1) # type: ignore - - if prev: - prev_ref = pdf.get_reference(prev) - assert isinstance(prev_ref, IndirectObject) - child_obj[NameObject("/Prev")] = prev_ref - prev[NameObject("/Next")] = child - - parent_ref = pdf.get_reference(self) - assert isinstance(parent_ref, IndirectObject) - child_obj[NameObject("/Parent")] = parent_ref - - def removeChild(self, child: Any) -> None: # pragma: no cover - deprecate_with_replacement("removeChild", "remove_child") - self.remove_child(child) - - def remove_child(self, child: Any) -> None: - child_obj = child.get_object() - - if NameObject("/Parent") not in child_obj: - raise ValueError("Removed child does not appear to be a tree item") - elif child_obj[NameObject("/Parent")] != self: - raise ValueError("Removed child is not a member of this tree") - - found = False - prev_ref = None - prev = None - cur_ref: Optional[Any] = self[NameObject("/First")] - cur: Optional[Dict[str, Any]] = cur_ref.get_object() # type: ignore - last_ref = self[NameObject("/Last")] - last = last_ref.get_object() - while cur is not None: - if cur == child_obj: - if prev is None: - if NameObject("/Next") in cur: - # Removing first tree node - next_ref = cur[NameObject("/Next")] - next_obj = next_ref.get_object() - del next_obj[NameObject("/Prev")] - self[NameObject("/First")] = next_ref - self[NameObject("/Count")] -= 1 # type: ignore - - else: - # Removing only tree node - assert self[NameObject("/Count")] == 1 - del self[NameObject("/Count")] - del self[NameObject("/First")] - if NameObject("/Last") in self: - del self[NameObject("/Last")] - else: - if NameObject("/Next") in cur: - # Removing middle tree node - next_ref = cur[NameObject("/Next")] - next_obj = next_ref.get_object() - next_obj[NameObject("/Prev")] = prev_ref - prev[NameObject("/Next")] = next_ref - self[NameObject("/Count")] -= 1 - else: - # Removing last tree node - assert cur == last - del prev[NameObject("/Next")] - self[NameObject("/Last")] = prev_ref - self[NameObject("/Count")] -= 1 - found = True - break - - prev_ref = cur_ref - prev = cur - if NameObject("/Next") in cur: - cur_ref = cur[NameObject("/Next")] - cur = cur_ref.get_object() - else: - cur_ref = None - cur = None - - if not found: - raise ValueError("Removal couldn't find item in tree") - - del child_obj[NameObject("/Parent")] - if NameObject("/Next") in child_obj: - del child_obj[NameObject("/Next")] - if NameObject("/Prev") in child_obj: - del child_obj[NameObject("/Prev")] - - def emptyTree(self) -> None: # pragma: no cover - deprecate_with_replacement("emptyTree", "empty_tree", "4.0.0") - self.empty_tree() - - def empty_tree(self) -> None: - for child in self: - child_obj = child.get_object() - del child_obj[NameObject("/Parent")] - if NameObject("/Next") in child_obj: - del child_obj[NameObject("/Next")] - if NameObject("/Prev") in child_obj: - del child_obj[NameObject("/Prev")] - - if NameObject("/Count") in self: - del self[NameObject("/Count")] - if NameObject("/First") in self: - del self[NameObject("/First")] - if NameObject("/Last") in self: - del self[NameObject("/Last")] - - -class StreamObject(DictionaryObject): - def __init__(self) -> None: - self.__data: Optional[str] = None - self.decoded_self: Optional[DecodedStreamObject] = None - - def hash_value_data(self) -> bytes: - data = super().hash_value_data() - data += b_(self._data) - return data - - @property - def decodedSelf(self) -> Optional["DecodedStreamObject"]: # pragma: no cover - deprecate_with_replacement("decodedSelf", "decoded_self") - return self.decoded_self - - @decodedSelf.setter - def decodedSelf(self, value: "DecodedStreamObject") -> None: # pragma: no cover - deprecate_with_replacement("decodedSelf", "decoded_self") - self.decoded_self = value - - @property - def _data(self) -> Any: - return self.__data - - @_data.setter - def _data(self, value: Any) -> None: - self.__data = value - - def write_to_stream( - self, stream: StreamType, encryption_key: Union[None, str, bytes] - ) -> None: - self[NameObject(SA.LENGTH)] = NumberObject(len(self._data)) - DictionaryObject.write_to_stream(self, stream, encryption_key) - del self[SA.LENGTH] - stream.write(b"\nstream\n") - data = self._data - if encryption_key: - from ._security import RC4_encrypt - - data = RC4_encrypt(encryption_key, data) - stream.write(data) - stream.write(b"\nendstream") - - @staticmethod - def initializeFromDictionary( - data: Dict[str, Any] - ) -> Union["EncodedStreamObject", "DecodedStreamObject"]: # pragma: no cover - return StreamObject.initialize_from_dictionary(data) - - @staticmethod - def initialize_from_dictionary( - data: Dict[str, Any] - ) -> Union["EncodedStreamObject", "DecodedStreamObject"]: - retval: Union["EncodedStreamObject", "DecodedStreamObject"] - if SA.FILTER in data: - retval = EncodedStreamObject() - else: - retval = DecodedStreamObject() - retval._data = data["__streamdata__"] - del data["__streamdata__"] - del data[SA.LENGTH] - retval.update(data) - return retval - - def flateEncode(self) -> "EncodedStreamObject": # pragma: no cover - deprecate_with_replacement("flateEncode", "flate_encode") - return self.flate_encode() - - def flate_encode(self) -> "EncodedStreamObject": - from .filters import FlateDecode - - if SA.FILTER in self: - f = self[SA.FILTER] - if isinstance(f, ArrayObject): - f.insert(0, NameObject(FT.FLATE_DECODE)) - else: - newf = ArrayObject() - newf.append(NameObject("/FlateDecode")) - newf.append(f) - f = newf - else: - f = NameObject("/FlateDecode") - retval = EncodedStreamObject() - retval[NameObject(SA.FILTER)] = f - retval._data = FlateDecode.encode(self._data) - return retval - - -class DecodedStreamObject(StreamObject): - def get_data(self) -> Any: - return self._data - - def set_data(self, data: Any) -> Any: - self._data = data - - def getData(self) -> Any: # pragma: no cover - deprecate_with_replacement("getData", "get_data") - return self._data - - def setData(self, data: Any) -> None: # pragma: no cover - deprecate_with_replacement("setData", "set_data") - self.set_data(data) - - -class EncodedStreamObject(StreamObject): - def __init__(self) -> None: - self.decoded_self: Optional[DecodedStreamObject] = None - - @property - def decodedSelf(self) -> Optional["DecodedStreamObject"]: # pragma: no cover - deprecate_with_replacement("decodedSelf", "decoded_self") - return self.decoded_self - - @decodedSelf.setter - def decodedSelf(self, value: DecodedStreamObject) -> None: # pragma: no cover - deprecate_with_replacement("decodedSelf", "decoded_self") - self.decoded_self = value - - def get_data(self) -> Union[None, str, bytes]: - from .filters import decode_stream_data - - if self.decoded_self is not None: - # cached version of decoded object - return self.decoded_self.get_data() - else: - # create decoded object - decoded = DecodedStreamObject() - - decoded._data = decode_stream_data(self) - for key, value in list(self.items()): - if key not in (SA.LENGTH, SA.FILTER, SA.DECODE_PARMS): - decoded[key] = value - self.decoded_self = decoded - return decoded._data - - def getData(self) -> Union[None, str, bytes]: # pragma: no cover - deprecate_with_replacement("getData", "get_data") - return self.get_data() - - def set_data(self, data: Any) -> None: - raise PdfReadError("Creating EncodedStreamObject is not currently supported") - - def setData(self, data: Any) -> None: # pragma: no cover - deprecate_with_replacement("setData", "set_data") - return self.set_data(data) - - -class ContentStream(DecodedStreamObject): - def __init__( - self, - stream: Any, - pdf: Any, - forced_encoding: Union[None, str, List[str], Dict[int, str]] = None, - ) -> None: - self.pdf = pdf - - # The inner list has two elements: - # [0] : List - # [1] : str - self.operations: List[Tuple[Any, Any]] = [] - - # stream may be a StreamObject or an ArrayObject containing - # multiple StreamObjects to be cat'd together. - stream = stream.get_object() - if isinstance(stream, ArrayObject): - data = b"" - for s in stream: - data += b_(s.get_object().get_data()) - stream_bytes = BytesIO(data) - else: - stream_data = stream.get_data() - assert stream_data is not None - stream_data_bytes = b_(stream_data) - stream_bytes = BytesIO(stream_data_bytes) - self.forced_encoding = forced_encoding - self.__parse_content_stream(stream_bytes) - - def __parse_content_stream(self, stream: StreamType) -> None: - stream.seek(0, 0) - operands: List[Union[int, str, PdfObject]] = [] - while True: - peek = read_non_whitespace(stream) - if peek == b"" or peek == 0: - break - stream.seek(-1, 1) - if peek.isalpha() or peek in (b"'", b'"'): - operator = read_until_regex(stream, NameObject.delimiter_pattern, True) - if operator == b"BI": - # begin inline image - a completely different parsing - # mechanism is required, of course... thanks buddy... - assert operands == [] - ii = self._read_inline_image(stream) - self.operations.append((ii, b"INLINE IMAGE")) - else: - self.operations.append((operands, operator)) - operands = [] - elif peek == b"%": - # If we encounter a comment in the content stream, we have to - # handle it here. Typically, read_object will handle - # encountering a comment -- but read_object assumes that - # following the comment must be the object we're trying to - # read. In this case, it could be an operator instead. - while peek not in (b"\r", b"\n"): - peek = stream.read(1) - else: - operands.append(read_object(stream, None, self.forced_encoding)) - - def _read_inline_image(self, stream: StreamType) -> Dict[str, Any]: - # begin reading just after the "BI" - begin image - # first read the dictionary of settings. - settings = DictionaryObject() - while True: - tok = read_non_whitespace(stream) - stream.seek(-1, 1) - if tok == b"I": - # "ID" - begin of image data - break - key = read_object(stream, self.pdf) - tok = read_non_whitespace(stream) - stream.seek(-1, 1) - value = read_object(stream, self.pdf) - settings[key] = value - # left at beginning of ID - tmp = stream.read(3) - assert tmp[:2] == b"ID" - data = BytesIO() - # Read the inline image, while checking for EI (End Image) operator. - while True: - # Read 8 kB at a time and check if the chunk contains the E operator. - buf = stream.read(8192) - # We have reached the end of the stream, but haven't found the EI operator. - if not buf: - raise PdfReadError("Unexpected end of stream") - loc = buf.find(b"E") - - if loc == -1: - data.write(buf) - else: - # Write out everything before the E. - data.write(buf[0:loc]) - - # Seek back in the stream to read the E next. - stream.seek(loc - len(buf), 1) - tok = stream.read(1) - # Check for End Image - tok2 = stream.read(1) - if tok2 == b"I": - # Data can contain EI, so check for the Q operator. - tok3 = stream.read(1) - info = tok + tok2 - # We need to find whitespace between EI and Q. - has_q_whitespace = False - while tok3 in WHITESPACES: - has_q_whitespace = True - info += tok3 - tok3 = stream.read(1) - if tok3 == b"Q" and has_q_whitespace: - stream.seek(-1, 1) - break - else: - stream.seek(-1, 1) - data.write(info) - else: - stream.seek(-1, 1) - data.write(tok) - return {"settings": settings, "data": data.getvalue()} - - @property - def _data(self) -> bytes: - newdata = BytesIO() - for operands, operator in self.operations: - if operator == b"INLINE IMAGE": - newdata.write(b"BI") - dicttext = BytesIO() - operands["settings"].write_to_stream(dicttext, None) - newdata.write(dicttext.getvalue()[2:-2]) - newdata.write(b"ID ") - newdata.write(operands["data"]) - newdata.write(b"EI") - else: - for op in operands: - op.write_to_stream(newdata, None) - newdata.write(b" ") - newdata.write(b_(operator)) - newdata.write(b"\n") - return newdata.getvalue() - - @_data.setter - def _data(self, value: Union[str, bytes]) -> None: - self.__parse_content_stream(BytesIO(b_(value))) - - -def read_object( - stream: StreamType, - pdf: Any, # PdfReader - forced_encoding: Union[None, str, List[str], Dict[int, str]] = None, -) -> Union[PdfObject, int, str, ContentStream]: - tok = stream.read(1) - stream.seek(-1, 1) # reset to start - idx = ObjectPrefix.find(tok) - if idx == 0: - return NameObject.read_from_stream(stream, pdf) - elif idx == 1: - # hexadecimal string OR dictionary - peek = stream.read(2) - stream.seek(-2, 1) # reset to start - - if peek == b"<<": - return DictionaryObject.read_from_stream(stream, pdf, forced_encoding) - else: - return read_hex_string_from_stream(stream, forced_encoding) - elif idx == 2: - return ArrayObject.read_from_stream(stream, pdf, forced_encoding) - elif idx == 3 or idx == 4: - return BooleanObject.read_from_stream(stream) - elif idx == 5: - return read_string_from_stream(stream, forced_encoding) - elif idx == 6: - return NullObject.read_from_stream(stream) - elif idx == 7: - # comment - while tok not in (b"\r", b"\n"): - tok = stream.read(1) - # Prevents an infinite loop by raising an error if the stream is at - # the EOF - if len(tok) <= 0: - raise PdfStreamError("File ended unexpectedly.") - tok = read_non_whitespace(stream) - stream.seek(-1, 1) - return read_object(stream, pdf, forced_encoding) - else: - # number object OR indirect reference - peek = stream.read(20) - stream.seek(-len(peek), 1) # reset to start - if IndirectPattern.match(peek) is not None: - return IndirectObject.read_from_stream(stream, pdf) - else: - return NumberObject.read_from_stream(stream) - - -class RectangleObject(ArrayObject): - """ - This class is used to represent *page boxes* in PyPDF2. These boxes include: - * :attr:`artbox ` - * :attr:`bleedbox ` - * :attr:`cropbox ` - * :attr:`mediabox ` - * :attr:`trimbox ` - """ - - def __init__( - self, arr: Union["RectangleObject", Tuple[float, float, float, float]] - ) -> None: - # must have four points - assert len(arr) == 4 - # automatically convert arr[x] into NumberObject(arr[x]) if necessary - ArrayObject.__init__(self, [self._ensure_is_number(x) for x in arr]) # type: ignore - - def _ensure_is_number(self, value: Any) -> Union[FloatObject, NumberObject]: - if not isinstance(value, (NumberObject, FloatObject)): - value = FloatObject(value) - return value - - def scale(self, sx: float, sy: float) -> "RectangleObject": - return RectangleObject( - ( - float(self.left) * sx, - float(self.bottom) * sy, - float(self.right) * sx, - float(self.top) * sy, - ) - ) - - def ensureIsNumber( - self, value: Any - ) -> Union[FloatObject, NumberObject]: # pragma: no cover - deprecate_no_replacement("ensureIsNumber") - return self._ensure_is_number(value) - - def __repr__(self) -> str: - return f"RectangleObject({repr(list(self))})" - - @property - def left(self) -> FloatObject: - return self[0] - - @property - def bottom(self) -> FloatObject: - return self[1] - - @property - def right(self) -> FloatObject: - return self[2] - - @property - def top(self) -> FloatObject: - return self[3] - - def getLowerLeft_x(self) -> FloatObject: # pragma: no cover - deprecate_with_replacement("getLowerLeft_x", "left") - return self.left - - def getLowerLeft_y(self) -> FloatObject: # pragma: no cover - deprecate_with_replacement("getLowerLeft_y", "bottom") - return self.bottom - - def getUpperRight_x(self) -> FloatObject: # pragma: no cover - deprecate_with_replacement("getUpperRight_x", "right") - return self.right - - def getUpperRight_y(self) -> FloatObject: # pragma: no cover - deprecate_with_replacement("getUpperRight_y", "top") - return self.top - - def getUpperLeft_x(self) -> FloatObject: # pragma: no cover - deprecate_with_replacement("getUpperLeft_x", "left") - return self.left - - def getUpperLeft_y(self) -> FloatObject: # pragma: no cover - deprecate_with_replacement("getUpperLeft_y", "top") - return self.top - - def getLowerRight_x(self) -> FloatObject: # pragma: no cover - deprecate_with_replacement("getLowerRight_x", "right") - return self.right - - def getLowerRight_y(self) -> FloatObject: # pragma: no cover - deprecate_with_replacement("getLowerRight_y", "bottom") - return self.bottom - - @property - def lower_left(self) -> Tuple[decimal.Decimal, decimal.Decimal]: - """ - Property to read and modify the lower left coordinate of this box - in (x,y) form. - """ - return self.left, self.bottom - - @lower_left.setter - def lower_left(self, value: List[Any]) -> None: - self[0], self[1] = (self._ensure_is_number(x) for x in value) - - @property - def lower_right(self) -> Tuple[decimal.Decimal, decimal.Decimal]: - """ - Property to read and modify the lower right coordinate of this box - in (x,y) form. - """ - return self.right, self.bottom - - @lower_right.setter - def lower_right(self, value: List[Any]) -> None: - self[2], self[1] = (self._ensure_is_number(x) for x in value) - - @property - def upper_left(self) -> Tuple[decimal.Decimal, decimal.Decimal]: - """ - Property to read and modify the upper left coordinate of this box - in (x,y) form. - """ - return self.left, self.top - - @upper_left.setter - def upper_left(self, value: List[Any]) -> None: - self[0], self[3] = (self._ensure_is_number(x) for x in value) - - @property - def upper_right(self) -> Tuple[decimal.Decimal, decimal.Decimal]: - """ - Property to read and modify the upper right coordinate of this box - in (x,y) form. - """ - return self.right, self.top - - @upper_right.setter - def upper_right(self, value: List[Any]) -> None: - self[2], self[3] = (self._ensure_is_number(x) for x in value) - - def getLowerLeft( - self, - ) -> Tuple[decimal.Decimal, decimal.Decimal]: # pragma: no cover - deprecate_with_replacement("getLowerLeft", "lower_left") - return self.lower_left - - def getLowerRight( - self, - ) -> Tuple[decimal.Decimal, decimal.Decimal]: # pragma: no cover - deprecate_with_replacement("getLowerRight", "lower_right") - return self.lower_right - - def getUpperLeft( - self, - ) -> Tuple[decimal.Decimal, decimal.Decimal]: # pragma: no cover - deprecate_with_replacement("getUpperLeft", "upper_left") - return self.upper_left - - def getUpperRight( - self, - ) -> Tuple[decimal.Decimal, decimal.Decimal]: # pragma: no cover - deprecate_with_replacement("getUpperRight", "upper_right") - return self.upper_right - - def setLowerLeft(self, value: Tuple[float, float]) -> None: # pragma: no cover - deprecate_with_replacement("setLowerLeft", "lower_left") - self.lower_left = value # type: ignore - - def setLowerRight(self, value: Tuple[float, float]) -> None: # pragma: no cover - deprecate_with_replacement("setLowerRight", "lower_right") - self[2], self[1] = (self._ensure_is_number(x) for x in value) - - def setUpperLeft(self, value: Tuple[float, float]) -> None: # pragma: no cover - deprecate_with_replacement("setUpperLeft", "upper_left") - self[0], self[3] = (self._ensure_is_number(x) for x in value) - - def setUpperRight(self, value: Tuple[float, float]) -> None: # pragma: no cover - deprecate_with_replacement("setUpperRight", "upper_right") - self[2], self[3] = (self._ensure_is_number(x) for x in value) - - @property - def width(self) -> decimal.Decimal: - return self.right - self.left - - def getWidth(self) -> decimal.Decimal: # pragma: no cover - deprecate_with_replacement("getWidth", "width") - return self.width - - @property - def height(self) -> decimal.Decimal: - return self.top - self.bottom - - def getHeight(self) -> decimal.Decimal: # pragma: no cover - deprecate_with_replacement("getHeight", "height") - return self.height - - @property - def lowerLeft(self) -> Tuple[decimal.Decimal, decimal.Decimal]: # pragma: no cover - deprecate_with_replacement("lowerLeft", "lower_left") - return self.lower_left - - @lowerLeft.setter - def lowerLeft( - self, value: Tuple[decimal.Decimal, decimal.Decimal] - ) -> None: # pragma: no cover - deprecate_with_replacement("lowerLeft", "lower_left") - self.lower_left = value - - @property - def lowerRight(self) -> Tuple[decimal.Decimal, decimal.Decimal]: # pragma: no cover - deprecate_with_replacement("lowerRight", "lower_right") - return self.lower_right - - @lowerRight.setter - def lowerRight( - self, value: Tuple[decimal.Decimal, decimal.Decimal] - ) -> None: # pragma: no cover - deprecate_with_replacement("lowerRight", "lower_right") - self.lower_right = value - - @property - def upperLeft(self) -> Tuple[decimal.Decimal, decimal.Decimal]: # pragma: no cover - deprecate_with_replacement("upperLeft", "upper_left") - return self.upper_left - - @upperLeft.setter - def upperLeft( - self, value: Tuple[decimal.Decimal, decimal.Decimal] - ) -> None: # pragma: no cover - deprecate_with_replacement("upperLeft", "upper_left") - self.upper_left = value - - @property - def upperRight(self) -> Tuple[decimal.Decimal, decimal.Decimal]: # pragma: no cover - deprecate_with_replacement("upperRight", "upper_right") - return self.upper_right - - @upperRight.setter - def upperRight( - self, value: Tuple[decimal.Decimal, decimal.Decimal] - ) -> None: # pragma: no cover - deprecate_with_replacement("upperRight", "upper_right") - self.upper_right = value - - -class Field(TreeObject): - """ - A class representing a field dictionary. - - This class is accessed through - :meth:`get_fields()` - """ - - def __init__(self, data: Dict[str, Any]) -> None: - DictionaryObject.__init__(self) - field_attributes = ( - FieldDictionaryAttributes.attributes() - + CheckboxRadioButtonAttributes.attributes() - ) - for attr in field_attributes: - try: - self[NameObject(attr)] = data[attr] - except KeyError: - pass - - # TABLE 8.69 Entries common to all field dictionaries - @property - def field_type(self) -> Optional[NameObject]: - """Read-only property accessing the type of this field.""" - return self.get(FieldDictionaryAttributes.FT) - - @property - def fieldType(self) -> Optional[NameObject]: # pragma: no cover - """ - .. deprecated:: 1.28.3 - - Use :py:attr:`field_type` instead. - """ - deprecate_with_replacement("fieldType", "field_type") - return self.field_type - - @property - def parent(self) -> Optional[DictionaryObject]: - """Read-only property accessing the parent of this field.""" - return self.get(FieldDictionaryAttributes.Parent) - - @property - def kids(self) -> Optional[ArrayObject]: - """Read-only property accessing the kids of this field.""" - return self.get(FieldDictionaryAttributes.Kids) - - @property - def name(self) -> Optional[str]: - """Read-only property accessing the name of this field.""" - return self.get(FieldDictionaryAttributes.T) - - @property - def alternate_name(self) -> Optional[str]: - """Read-only property accessing the alternate name of this field.""" - return self.get(FieldDictionaryAttributes.TU) - - @property - def altName(self) -> Optional[str]: # pragma: no cover - """ - .. deprecated:: 1.28.3 - - Use :py:attr:`alternate_name` instead. - """ - deprecate_with_replacement("altName", "alternate_name") - return self.alternate_name - - @property - def mapping_name(self) -> Optional[str]: - """ - Read-only property accessing the mapping name of this field. This - name is used by PyPDF2 as a key in the dictionary returned by - :meth:`get_fields()` - """ - return self.get(FieldDictionaryAttributes.TM) - - @property - def mappingName(self) -> Optional[str]: # pragma: no cover - """ - .. deprecated:: 1.28.3 - - Use :py:attr:`mapping_name` instead. - """ - deprecate_with_replacement("mappingName", "mapping_name") - return self.mapping_name - - @property - def flags(self) -> Optional[int]: - """ - Read-only property accessing the field flags, specifying various - characteristics of the field (see Table 8.70 of the PDF 1.7 reference). - """ - return self.get(FieldDictionaryAttributes.Ff) - - @property - def value(self) -> Optional[Any]: - """ - Read-only property accessing the value of this field. Format - varies based on field type. - """ - return self.get(FieldDictionaryAttributes.V) - - @property - def default_value(self) -> Optional[Any]: - """Read-only property accessing the default value of this field.""" - return self.get(FieldDictionaryAttributes.DV) - - @property - def defaultValue(self) -> Optional[Any]: # pragma: no cover - """ - .. deprecated:: 1.28.3 - - Use :py:attr:`default_value` instead. - """ - deprecate_with_replacement("defaultValue", "default_value") - return self.default_value - - @property - def additional_actions(self) -> Optional[DictionaryObject]: - """ - Read-only property accessing the additional actions dictionary. - This dictionary defines the field's behavior in response to trigger events. - See Section 8.5.2 of the PDF 1.7 reference. - """ - return self.get(FieldDictionaryAttributes.AA) - - @property - def additionalActions(self) -> Optional[DictionaryObject]: # pragma: no cover - """ - .. deprecated:: 1.28.3 - - Use :py:attr:`additional_actions` instead. - """ - deprecate_with_replacement("additionalActions", "additional_actions") - return self.additional_actions - - -class OutlineFontFlag(IntFlag): - """ - A class used as an enumerable flag for formatting an outline font - """ - - italic = 1 - bold = 2 - - -class Destination(TreeObject): - """ - A class representing a destination within a PDF file. - See section 8.2.1 of the PDF 1.6 reference. - - :param str title: Title of this destination. - :param IndirectObject page: Reference to the page of this destination. Should - be an instance of :class:`IndirectObject`. - :param str typ: How the destination is displayed. - :param args: Additional arguments may be necessary depending on the type. - :raises PdfReadError: If destination type is invalid. - - .. list-table:: Valid ``typ`` arguments (see PDF spec for details) - :widths: 50 50 - - * - /Fit - - No additional arguments - * - /XYZ - - [left] [top] [zoomFactor] - * - /FitH - - [top] - * - /FitV - - [left] - * - /FitR - - [left] [bottom] [right] [top] - * - /FitB - - No additional arguments - * - /FitBH - - [top] - * - /FitBV - - [left] - """ - - def __init__( - self, - title: str, - page: Union[NumberObject, IndirectObject, NullObject, DictionaryObject], - typ: Union[str, NumberObject], - *args: Any, # ZoomArgType - ) -> None: - DictionaryObject.__init__(self) - self[NameObject("/Title")] = title - self[NameObject("/Page")] = page - self[NameObject("/Type")] = typ - - # from table 8.2 of the PDF 1.7 reference. - if typ == "/XYZ": - ( - self[NameObject(TA.LEFT)], - self[NameObject(TA.TOP)], - self[NameObject("/Zoom")], - ) = args - elif typ == TF.FIT_R: - ( - self[NameObject(TA.LEFT)], - self[NameObject(TA.BOTTOM)], - self[NameObject(TA.RIGHT)], - self[NameObject(TA.TOP)], - ) = args - elif typ in [TF.FIT_H, TF.FIT_BH]: - try: # Prefered to be more robust not only to null parameters - (self[NameObject(TA.TOP)],) = args - except Exception: - (self[NameObject(TA.TOP)],) = (NullObject(),) - elif typ in [TF.FIT_V, TF.FIT_BV]: - try: # Prefered to be more robust not only to null parameters - (self[NameObject(TA.LEFT)],) = args - except Exception: - (self[NameObject(TA.LEFT)],) = (NullObject(),) - elif typ in [TF.FIT, TF.FIT_B]: - pass - else: - raise PdfReadError(f"Unknown Destination Type: {typ!r}") - - @property - def dest_array(self) -> ArrayObject: - return ArrayObject( - [self.raw_get("/Page"), self["/Type"]] - + [ - self[x] - for x in ["/Left", "/Bottom", "/Right", "/Top", "/Zoom"] - if x in self - ] - ) - - def getDestArray(self) -> ArrayObject: # pragma: no cover - """ - .. deprecated:: 1.28.3 - - Use :py:attr:`dest_array` instead. - """ - deprecate_with_replacement("getDestArray", "dest_array") - return self.dest_array - - def write_to_stream( - self, stream: StreamType, encryption_key: Union[None, str, bytes] - ) -> None: - stream.write(b"<<\n") - key = NameObject("/D") - key.write_to_stream(stream, encryption_key) - stream.write(b" ") - value = self.dest_array - value.write_to_stream(stream, encryption_key) - - key = NameObject("/S") - key.write_to_stream(stream, encryption_key) - stream.write(b" ") - value_s = NameObject("/GoTo") - value_s.write_to_stream(stream, encryption_key) - - stream.write(b"\n") - stream.write(b">>") - - @property - def title(self) -> Optional[str]: - """Read-only property accessing the destination title.""" - return self.get("/Title") - - @property - def page(self) -> Optional[int]: - """Read-only property accessing the destination page number.""" - return self.get("/Page") - - @property - def typ(self) -> Optional[str]: - """Read-only property accessing the destination type.""" - return self.get("/Type") - - @property - def zoom(self) -> Optional[int]: - """Read-only property accessing the zoom factor.""" - return self.get("/Zoom", None) - - @property - def left(self) -> Optional[FloatObject]: - """Read-only property accessing the left horizontal coordinate.""" - return self.get("/Left", None) - - @property - def right(self) -> Optional[FloatObject]: - """Read-only property accessing the right horizontal coordinate.""" - return self.get("/Right", None) - - @property - def top(self) -> Optional[FloatObject]: - """Read-only property accessing the top vertical coordinate.""" - return self.get("/Top", None) - - @property - def bottom(self) -> Optional[FloatObject]: - """Read-only property accessing the bottom vertical coordinate.""" - return self.get("/Bottom", None) - - @property - def color(self) -> Optional[ArrayObject]: - """Read-only property accessing the color in (R, G, B) with values 0.0-1.0""" - return self.get( - "/C", ArrayObject([FloatObject(0), FloatObject(0), FloatObject(0)]) - ) - - @property - def font_format(self) -> Optional[OutlineFontFlag]: - """Read-only property accessing the font type. 1=italic, 2=bold, 3=both""" - return self.get("/F", 0) - - @property - def outline_count(self) -> Optional[int]: - """ - Read-only property accessing the outline count. - positive = expanded - negative = collapsed - absolute value = number of visible descendents at all levels - """ - return self.get("/Count", None) - - -class OutlineItem(Destination): - def write_to_stream( - self, stream: StreamType, encryption_key: Union[None, str, bytes] - ) -> None: - stream.write(b"<<\n") - for key in [ - NameObject(x) - for x in ["/Title", "/Parent", "/First", "/Last", "/Next", "/Prev"] - if x in self - ]: - key.write_to_stream(stream, encryption_key) - stream.write(b" ") - value = self.raw_get(key) - value.write_to_stream(stream, encryption_key) - stream.write(b"\n") - key = NameObject("/Dest") - key.write_to_stream(stream, encryption_key) - stream.write(b" ") - value = self.dest_array - value.write_to_stream(stream, encryption_key) - stream.write(b"\n") - stream.write(b">>") - - -class Bookmark(OutlineItem): # pragma: no cover - def __init__(self, *args: Any, **kwargs: Any) -> None: - deprecate_with_replacement("Bookmark", "OutlineItem") - super().__init__(*args, **kwargs) - - -def createStringObject( - string: Union[str, bytes], - forced_encoding: Union[None, str, List[str], Dict[int, str]] = None, -) -> Union[TextStringObject, ByteStringObject]: # pragma: no cover - deprecate_with_replacement("createStringObject", "create_string_object", "4.0.0") - return create_string_object(string, forced_encoding) - - -def create_string_object( - string: Union[str, bytes], - forced_encoding: Union[None, str, List[str], Dict[int, str]] = None, -) -> Union[TextStringObject, ByteStringObject]: - """ - Create a ByteStringObject or a TextStringObject from a string to represent the string. - - :param Union[str, bytes] string: A string - - :raises TypeError: If string is not of type str or bytes. - """ - if isinstance(string, str): - return TextStringObject(string) - elif isinstance(string, bytes): - if isinstance(forced_encoding, (list, dict)): - out = "" - for x in string: - try: - out += forced_encoding[x] - except Exception: - out += bytes((x,)).decode("charmap") - return TextStringObject(out) - elif isinstance(forced_encoding, str): - if forced_encoding == "bytes": - return ByteStringObject(string) - return TextStringObject(string.decode(forced_encoding)) - else: - try: - if string.startswith(codecs.BOM_UTF16_BE): - retval = TextStringObject(string.decode("utf-16")) - retval.autodetect_utf16 = True - return retval - else: - # This is probably a big performance hit here, but we need to - # convert string objects into the text/unicode-aware version if - # possible... and the only way to check if that's possible is - # to try. Some strings are strings, some are just byte arrays. - retval = TextStringObject(decode_pdfdocencoding(string)) - retval.autodetect_pdfdocencoding = True - return retval - except UnicodeDecodeError: - return ByteStringObject(string) - else: - raise TypeError("create_string_object should have str or unicode arg") - - -def _create_outline_item( - action_ref: IndirectObject, - title: str, - color: Union[Tuple[float, float, float], str, None], - italic: bool, - bold: bool, -) -> TreeObject: - outline_item = TreeObject() - outline_item.update( - { - NameObject("/A"): action_ref, - NameObject("/Title"): create_string_object(title), - } - ) - if color: - if isinstance(color, str): - color = hex_to_rgb(color) - outline_item.update( - {NameObject("/C"): ArrayObject([FloatObject(c) for c in color])} - ) - if italic or bold: - format_flag = 0 - if italic: - format_flag += 1 - if bold: - format_flag += 2 - outline_item.update({NameObject("/F"): NumberObject(format_flag)}) - return outline_item - - -def encode_pdfdocencoding(unicode_string: str) -> bytes: - retval = b"" - for c in unicode_string: - try: - retval += b_(chr(_pdfdoc_encoding_rev[c])) - except KeyError: - raise UnicodeEncodeError( - "pdfdocencoding", c, -1, -1, "does not exist in translation table" - ) - return retval - - -def decode_pdfdocencoding(byte_array: bytes) -> str: - retval = "" - for b in byte_array: - c = _pdfdoc_encoding[b] - if c == "\u0000": - raise UnicodeDecodeError( - "pdfdocencoding", - bytearray(b), - -1, - -1, - "does not exist in translation table", - ) - retval += c - return retval - - -def hex_to_rgb(value: str) -> Tuple[float, float, float]: - return tuple(int(value.lstrip("#")[i : i + 2], 16) / 255.0 for i in (0, 2, 4)) # type: ignore - - -class AnnotationBuilder: - """ - The AnnotationBuilder creates dictionaries representing PDF annotations. - - Those dictionaries can be modified before they are added to a PdfWriter - instance via `writer.add_annotation`. - - See `adding PDF annotations <../user/adding-pdf-annotations.html>`_ for - it's usage combined with PdfWriter. - """ - - from .types import FitType, ZoomArgType - - @staticmethod - def text( - rect: Union[RectangleObject, Tuple[float, float, float, float]], - text: str, - open: bool = False, - flags: int = 0, - ) -> DictionaryObject: - """ - Add text annotation. - - :param Tuple[int, int, int, int] rect: - or array of four integers specifying the clickable rectangular area - ``[xLL, yLL, xUR, yUR]`` - :param bool open: - :param int flags: - """ - # TABLE 8.23 Additional entries specific to a text annotation - text_obj = DictionaryObject( - { - NameObject("/Type"): NameObject("/Annot"), - NameObject("/Subtype"): NameObject("/Text"), - NameObject("/Rect"): RectangleObject(rect), - NameObject("/Contents"): TextStringObject(text), - NameObject("/Open"): BooleanObject(open), - NameObject("/Flags"): NumberObject(flags), - } - ) - return text_obj - - @staticmethod - def free_text( - text: str, - rect: Union[RectangleObject, Tuple[float, float, float, float]], - font: str = "Helvetica", - bold: bool = False, - italic: bool = False, - font_size: str = "14pt", - font_color: str = "000000", - border_color: str = "000000", - background_color: str = "ffffff", - ) -> DictionaryObject: - """ - Add text in a rectangle to a page. - - :param str text: Text to be added - :param RectangleObject rect: or array of four integers - specifying the clickable rectangular area ``[xLL, yLL, xUR, yUR]`` - :param str font: Name of the Font, e.g. 'Helvetica' - :param bool bold: Print the text in bold - :param bool italic: Print the text in italic - :param str font_size: How big the text will be, e.g. '14pt' - :param str font_color: Hex-string for the color - :param str border_color: Hex-string for the border color - :param str background_color: Hex-string for the background of the annotation - """ - font_str = "font: " - if bold is True: - font_str = font_str + "bold " - if italic is True: - font_str = font_str + "italic " - font_str = font_str + font + " " + font_size - font_str = font_str + ";text-align:left;color:#" + font_color - - bg_color_str = "" - for st in hex_to_rgb(border_color): - bg_color_str = bg_color_str + str(st) + " " - bg_color_str = bg_color_str + "rg" - - free_text = DictionaryObject() - free_text.update( - { - NameObject("/Type"): NameObject("/Annot"), - NameObject("/Subtype"): NameObject("/FreeText"), - NameObject("/Rect"): RectangleObject(rect), - NameObject("/Contents"): TextStringObject(text), - # font size color - NameObject("/DS"): TextStringObject(font_str), - # border color - NameObject("/DA"): TextStringObject(bg_color_str), - # background color - NameObject("/C"): ArrayObject( - [FloatObject(n) for n in hex_to_rgb(background_color)] - ), - } - ) - return free_text - - @staticmethod - def line( - p1: Tuple[float, float], - p2: Tuple[float, float], - rect: Union[RectangleObject, Tuple[float, float, float, float]], - text: str = "", - title_bar: str = "", - ) -> DictionaryObject: - """ - Draw a line on the PDF. - - :param Tuple[float, float] p1: First point - :param Tuple[float, float] p2: Second point - :param RectangleObject rect: or array of four - integers specifying the clickable rectangular area - ``[xLL, yLL, xUR, yUR]`` - :param str text: Text to be displayed as the line annotation - :param str title_bar: Text to be displayed in the title bar of the - annotation; by convention this is the name of the author - """ - line_obj = DictionaryObject( - { - NameObject("/Type"): NameObject("/Annot"), - NameObject("/Subtype"): NameObject("/Line"), - NameObject("/Rect"): RectangleObject(rect), - NameObject("/T"): TextStringObject(title_bar), - NameObject("/L"): ArrayObject( - [ - FloatObject(p1[0]), - FloatObject(p1[1]), - FloatObject(p2[0]), - FloatObject(p2[1]), - ] - ), - NameObject("/LE"): ArrayObject( - [ - NameObject(None), - NameObject(None), - ] - ), - NameObject("/IC"): ArrayObject( - [ - FloatObject(0.5), - FloatObject(0.5), - FloatObject(0.5), - ] - ), - NameObject("/Contents"): TextStringObject(text), - } - ) - return line_obj - - @staticmethod - def link( - rect: Union[RectangleObject, Tuple[float, float, float, float]], - border: Optional[ArrayObject] = None, - url: Optional[str] = None, - target_page_index: Optional[int] = None, - fit: FitType = "/Fit", - fit_args: Tuple[ZoomArgType, ...] = tuple(), - ) -> DictionaryObject: - """ - Add a link to the document. - - The link can either be an external link or an internal link. - - An external link requires the URL parameter. - An internal link requires the target_page_index, fit, and fit args. - - - :param RectangleObject rect: or array of four - integers specifying the clickable rectangular area - ``[xLL, yLL, xUR, yUR]`` - :param border: if provided, an array describing border-drawing - properties. See the PDF spec for details. No border will be - drawn if this argument is omitted. - - horizontal corner radius, - - vertical corner radius, and - - border width - - Optionally: Dash - :param str url: Link to a website (if you want to make an external link) - :param int target_page_index: index of the page to which the link should go - (if you want to make an internal link) - :param str fit: Page fit or 'zoom' option (see below). Additional arguments may need - to be supplied. Passing ``None`` will be read as a null value for that coordinate. - :param Tuple[int, ...] fit_args: Parameters for the fit argument. - - - .. list-table:: Valid ``fit`` arguments (see Table 8.2 of the PDF 1.7 reference for details) - :widths: 50 200 - - * - /Fit - - No additional arguments - * - /XYZ - - [left] [top] [zoomFactor] - * - /FitH - - [top] - * - /FitV - - [left] - * - /FitR - - [left] [bottom] [right] [top] - * - /FitB - - No additional arguments - * - /FitBH - - [top] - * - /FitBV - - [left] - """ - from .types import BorderArrayType - - is_external = url is not None - is_internal = target_page_index is not None - if not is_external and not is_internal: - raise ValueError( - "Either 'url' or 'target_page_index' have to be provided. Both were None." - ) - if is_external and is_internal: - raise ValueError( - f"Either 'url' or 'target_page_index' have to be provided. url={url}, target_page_index={target_page_index}" - ) - - border_arr: BorderArrayType - if border is not None: - border_arr = [NameObject(n) for n in border[:3]] - if len(border) == 4: - dash_pattern = ArrayObject([NameObject(n) for n in border[3]]) - border_arr.append(dash_pattern) - else: - border_arr = [NumberObject(0)] * 3 - - link_obj = DictionaryObject( - { - NameObject("/Type"): NameObject("/Annot"), - NameObject("/Subtype"): NameObject("/Link"), - NameObject("/Rect"): RectangleObject(rect), - NameObject("/Border"): ArrayObject(border_arr), - } - ) - if is_external: - link_obj[NameObject("/A")] = DictionaryObject( - { - NameObject("/S"): NameObject("/URI"), - NameObject("/Type"): NameObject("/Action"), - NameObject("/URI"): TextStringObject(url), - } - ) - if is_internal: - fit_arg_ready = [ - NullObject() if a is None else NumberObject(a) for a in fit_args - ] - # This needs to be updated later! - dest_deferred = DictionaryObject( - { - "target_page_index": NumberObject(target_page_index), - "fit": NameObject(fit), - "fit_args": ArrayObject(fit_arg_ready), - } - ) - link_obj[NameObject("/Dest")] = dest_deferred - return link_obj diff --git a/PyPDF2/generic/__init__.py b/PyPDF2/generic/__init__.py new file mode 100644 index 000000000..b0c60da88 --- /dev/null +++ b/PyPDF2/generic/__init__.py @@ -0,0 +1,137 @@ +# Copyright (c) 2006, Mathieu Fenniak +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +"""Implementation of generic PDF objects (dictionary, number, string, ...).""" +__author__ = "Mathieu Fenniak" +__author_email__ = "biziqe@mathieu.fenniak.net" + +from typing import Dict, List, Union + +from .._utils import StreamType, deprecate_with_replacement +from ..constants import OutlineFontFlag +from ._annotations import AnnotationBuilder +from ._base import ( + BooleanObject, + ByteStringObject, + FloatObject, + IndirectObject, + NameObject, + NullObject, + NumberObject, + PdfObject, + TextStringObject, + encode_pdfdocencoding, +) +from ._data_structures import ( + ArrayObject, + ContentStream, + DecodedStreamObject, + Destination, + DictionaryObject, + EncodedStreamObject, + Field, + StreamObject, + TreeObject, + read_object, +) +from ._outline import Bookmark, OutlineItem +from ._rectangle import RectangleObject +from ._utils import ( + create_string_object, + decode_pdfdocencoding, + hex_to_rgb, + read_hex_string_from_stream, + read_string_from_stream, +) + + +def readHexStringFromStream( + stream: StreamType, +) -> Union["TextStringObject", "ByteStringObject"]: # pragma: no cover + deprecate_with_replacement( + "readHexStringFromStream", "read_hex_string_from_stream", "4.0.0" + ) + return read_hex_string_from_stream(stream) + + +def readStringFromStream( + stream: StreamType, + forced_encoding: Union[None, str, List[str], Dict[int, str]] = None, +) -> Union["TextStringObject", "ByteStringObject"]: # pragma: no cover + deprecate_with_replacement( + "readStringFromStream", "read_string_from_stream", "4.0.0" + ) + return read_string_from_stream(stream, forced_encoding) + + +def createStringObject( + string: Union[str, bytes], + forced_encoding: Union[None, str, List[str], Dict[int, str]] = None, +) -> Union[TextStringObject, ByteStringObject]: # pragma: no cover + deprecate_with_replacement("createStringObject", "create_string_object", "4.0.0") + return create_string_object(string, forced_encoding) + + +__all__ = [ + # Base types + "BooleanObject", + "FloatObject", + "NumberObject", + "NameObject", + "IndirectObject", + "NullObject", + "PdfObject", + "TextStringObject", + "ByteStringObject", + # Annotations + "AnnotationBuilder", + # Data structures + "ArrayObject", + "DictionaryObject", + "TreeObject", + "StreamObject", + "DecodedStreamObject", + "EncodedStreamObject", + "ContentStream", + "RectangleObject", + "Field", + "Destination", + # --- More specific stuff + # Outline + "OutlineItem", + "OutlineFontFlag", + "Bookmark", + # Data structures core functions + "read_object", + # Utility functions + "create_string_object", + "encode_pdfdocencoding", + "decode_pdfdocencoding", + "hex_to_rgb", + "read_hex_string_from_stream", + "read_string_from_stream", +] diff --git a/PyPDF2/generic/_annotations.py b/PyPDF2/generic/_annotations.py new file mode 100644 index 000000000..ffb4f8f17 --- /dev/null +++ b/PyPDF2/generic/_annotations.py @@ -0,0 +1,275 @@ +from typing import Optional, Tuple, Union + +from ._base import ( + BooleanObject, + FloatObject, + NameObject, + NullObject, + NumberObject, + TextStringObject, +) +from ._data_structures import ArrayObject, DictionaryObject +from ._rectangle import RectangleObject +from ._utils import hex_to_rgb + + +class AnnotationBuilder: + """ + The AnnotationBuilder creates dictionaries representing PDF annotations. + + Those dictionaries can be modified before they are added to a PdfWriter + instance via `writer.add_annotation`. + + See `adding PDF annotations <../user/adding-pdf-annotations.html>`_ for + it's usage combined with PdfWriter. + """ + + from ..types import FitType, ZoomArgType + + @staticmethod + def text( + rect: Union[RectangleObject, Tuple[float, float, float, float]], + text: str, + open: bool = False, + flags: int = 0, + ) -> DictionaryObject: + """ + Add text annotation. + + :param Tuple[int, int, int, int] rect: + or array of four integers specifying the clickable rectangular area + ``[xLL, yLL, xUR, yUR]`` + :param bool open: + :param int flags: + """ + # TABLE 8.23 Additional entries specific to a text annotation + text_obj = DictionaryObject( + { + NameObject("/Type"): NameObject("/Annot"), + NameObject("/Subtype"): NameObject("/Text"), + NameObject("/Rect"): RectangleObject(rect), + NameObject("/Contents"): TextStringObject(text), + NameObject("/Open"): BooleanObject(open), + NameObject("/Flags"): NumberObject(flags), + } + ) + return text_obj + + @staticmethod + def free_text( + text: str, + rect: Union[RectangleObject, Tuple[float, float, float, float]], + font: str = "Helvetica", + bold: bool = False, + italic: bool = False, + font_size: str = "14pt", + font_color: str = "000000", + border_color: str = "000000", + background_color: str = "ffffff", + ) -> DictionaryObject: + """ + Add text in a rectangle to a page. + + :param str text: Text to be added + :param RectangleObject rect: or array of four integers + specifying the clickable rectangular area ``[xLL, yLL, xUR, yUR]`` + :param str font: Name of the Font, e.g. 'Helvetica' + :param bool bold: Print the text in bold + :param bool italic: Print the text in italic + :param str font_size: How big the text will be, e.g. '14pt' + :param str font_color: Hex-string for the color + :param str border_color: Hex-string for the border color + :param str background_color: Hex-string for the background of the annotation + """ + font_str = "font: " + if bold is True: + font_str = font_str + "bold " + if italic is True: + font_str = font_str + "italic " + font_str = font_str + font + " " + font_size + font_str = font_str + ";text-align:left;color:#" + font_color + + bg_color_str = "" + for st in hex_to_rgb(border_color): + bg_color_str = bg_color_str + str(st) + " " + bg_color_str = bg_color_str + "rg" + + free_text = DictionaryObject() + free_text.update( + { + NameObject("/Type"): NameObject("/Annot"), + NameObject("/Subtype"): NameObject("/FreeText"), + NameObject("/Rect"): RectangleObject(rect), + NameObject("/Contents"): TextStringObject(text), + # font size color + NameObject("/DS"): TextStringObject(font_str), + # border color + NameObject("/DA"): TextStringObject(bg_color_str), + # background color + NameObject("/C"): ArrayObject( + [FloatObject(n) for n in hex_to_rgb(background_color)] + ), + } + ) + return free_text + + @staticmethod + def line( + p1: Tuple[float, float], + p2: Tuple[float, float], + rect: Union[RectangleObject, Tuple[float, float, float, float]], + text: str = "", + title_bar: str = "", + ) -> DictionaryObject: + """ + Draw a line on the PDF. + + :param Tuple[float, float] p1: First point + :param Tuple[float, float] p2: Second point + :param RectangleObject rect: or array of four + integers specifying the clickable rectangular area + ``[xLL, yLL, xUR, yUR]`` + :param str text: Text to be displayed as the line annotation + :param str title_bar: Text to be displayed in the title bar of the + annotation; by convention this is the name of the author + """ + line_obj = DictionaryObject( + { + NameObject("/Type"): NameObject("/Annot"), + NameObject("/Subtype"): NameObject("/Line"), + NameObject("/Rect"): RectangleObject(rect), + NameObject("/T"): TextStringObject(title_bar), + NameObject("/L"): ArrayObject( + [ + FloatObject(p1[0]), + FloatObject(p1[1]), + FloatObject(p2[0]), + FloatObject(p2[1]), + ] + ), + NameObject("/LE"): ArrayObject( + [ + NameObject(None), + NameObject(None), + ] + ), + NameObject("/IC"): ArrayObject( + [ + FloatObject(0.5), + FloatObject(0.5), + FloatObject(0.5), + ] + ), + NameObject("/Contents"): TextStringObject(text), + } + ) + return line_obj + + @staticmethod + def link( + rect: Union[RectangleObject, Tuple[float, float, float, float]], + border: Optional[ArrayObject] = None, + url: Optional[str] = None, + target_page_index: Optional[int] = None, + fit: FitType = "/Fit", + fit_args: Tuple[ZoomArgType, ...] = tuple(), + ) -> DictionaryObject: + """ + Add a link to the document. + + The link can either be an external link or an internal link. + + An external link requires the URL parameter. + An internal link requires the target_page_index, fit, and fit args. + + + :param RectangleObject rect: or array of four + integers specifying the clickable rectangular area + ``[xLL, yLL, xUR, yUR]`` + :param border: if provided, an array describing border-drawing + properties. See the PDF spec for details. No border will be + drawn if this argument is omitted. + - horizontal corner radius, + - vertical corner radius, and + - border width + - Optionally: Dash + :param str url: Link to a website (if you want to make an external link) + :param int target_page_index: index of the page to which the link should go + (if you want to make an internal link) + :param str fit: Page fit or 'zoom' option (see below). Additional arguments may need + to be supplied. Passing ``None`` will be read as a null value for that coordinate. + :param Tuple[int, ...] fit_args: Parameters for the fit argument. + + + .. list-table:: Valid ``fit`` arguments (see Table 8.2 of the PDF 1.7 reference for details) + :widths: 50 200 + + * - /Fit + - No additional arguments + * - /XYZ + - [left] [top] [zoomFactor] + * - /FitH + - [top] + * - /FitV + - [left] + * - /FitR + - [left] [bottom] [right] [top] + * - /FitB + - No additional arguments + * - /FitBH + - [top] + * - /FitBV + - [left] + """ + from ..types import BorderArrayType + + is_external = url is not None + is_internal = target_page_index is not None + if not is_external and not is_internal: + raise ValueError( + "Either 'url' or 'target_page_index' have to be provided. Both were None." + ) + if is_external and is_internal: + raise ValueError( + f"Either 'url' or 'target_page_index' have to be provided. url={url}, target_page_index={target_page_index}" + ) + + border_arr: BorderArrayType + if border is not None: + border_arr = [NameObject(n) for n in border[:3]] + if len(border) == 4: + dash_pattern = ArrayObject([NameObject(n) for n in border[3]]) + border_arr.append(dash_pattern) + else: + border_arr = [NumberObject(0)] * 3 + + link_obj = DictionaryObject( + { + NameObject("/Type"): NameObject("/Annot"), + NameObject("/Subtype"): NameObject("/Link"), + NameObject("/Rect"): RectangleObject(rect), + NameObject("/Border"): ArrayObject(border_arr), + } + ) + if is_external: + link_obj[NameObject("/A")] = DictionaryObject( + { + NameObject("/S"): NameObject("/URI"), + NameObject("/Type"): NameObject("/Action"), + NameObject("/URI"): TextStringObject(url), + } + ) + if is_internal: + fit_arg_ready = [ + NullObject() if a is None else NumberObject(a) for a in fit_args + ] + # This needs to be updated later! + dest_deferred = DictionaryObject( + { + "target_page_index": NumberObject(target_page_index), + "fit": NameObject(fit), + "fit_args": ArrayObject(fit_arg_ready), + } + ) + link_obj[NameObject("/Dest")] = dest_deferred + return link_obj diff --git a/PyPDF2/generic/_base.py b/PyPDF2/generic/_base.py new file mode 100644 index 000000000..5734c3304 --- /dev/null +++ b/PyPDF2/generic/_base.py @@ -0,0 +1,464 @@ +# Copyright (c) 2006, Mathieu Fenniak +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import codecs +import decimal +import hashlib +import re +from typing import Any, Callable, Optional, Union + +from .._codecs import _pdfdoc_encoding_rev +from .._utils import ( + StreamType, + b_, + deprecate_with_replacement, + hex_str, + hexencode, + logger_warning, + read_non_whitespace, + read_until_regex, + str_, +) +from ..errors import STREAM_TRUNCATED_PREMATURELY, PdfReadError, PdfStreamError + +__author__ = "Mathieu Fenniak" +__author_email__ = "biziqe@mathieu.fenniak.net" + + +class PdfObject: + # function for calculating a hash value + hash_func: Callable[..., "hashlib._Hash"] = hashlib.sha1 + + def hash_value_data(self) -> bytes: + return ("%s" % self).encode() + + def hash_value(self) -> bytes: + return ( + "%s:%s" + % ( + self.__class__.__name__, + self.hash_func(self.hash_value_data()).hexdigest(), + ) + ).encode() + + def get_object(self) -> Optional["PdfObject"]: + """Resolve indirect references.""" + return self + + def getObject(self) -> Optional["PdfObject"]: # pragma: no cover + deprecate_with_replacement("getObject", "get_object") + return self.get_object() + + def write_to_stream( + self, stream: StreamType, encryption_key: Union[None, str, bytes] + ) -> None: + raise NotImplementedError + + +class NullObject(PdfObject): + def write_to_stream( + self, stream: StreamType, encryption_key: Union[None, str, bytes] + ) -> None: + stream.write(b"null") + + @staticmethod + def read_from_stream(stream: StreamType) -> "NullObject": + nulltxt = stream.read(4) + if nulltxt != b"null": + raise PdfReadError("Could not read Null object") + return NullObject() + + def writeToStream( + self, stream: StreamType, encryption_key: Union[None, str, bytes] + ) -> None: # pragma: no cover + deprecate_with_replacement("writeToStream", "write_to_stream") + self.write_to_stream(stream, encryption_key) + + def __repr__(self) -> str: + return "NullObject" + + @staticmethod + def readFromStream(stream: StreamType) -> "NullObject": # pragma: no cover + deprecate_with_replacement("readFromStream", "read_from_stream") + return NullObject.read_from_stream(stream) + + +class BooleanObject(PdfObject): + def __init__(self, value: Any) -> None: + self.value = value + + def __eq__(self, __o: object) -> bool: + if isinstance(__o, BooleanObject): + return self.value == __o.value + elif isinstance(__o, bool): + return self.value == __o + else: + return False + + def __repr__(self) -> str: + return "True" if self.value else "False" + + def write_to_stream( + self, stream: StreamType, encryption_key: Union[None, str, bytes] + ) -> None: + if self.value: + stream.write(b"true") + else: + stream.write(b"false") + + def writeToStream( + self, stream: StreamType, encryption_key: Union[None, str, bytes] + ) -> None: # pragma: no cover + deprecate_with_replacement("writeToStream", "write_to_stream") + self.write_to_stream(stream, encryption_key) + + @staticmethod + def read_from_stream(stream: StreamType) -> "BooleanObject": + word = stream.read(4) + if word == b"true": + return BooleanObject(True) + elif word == b"fals": + stream.read(1) + return BooleanObject(False) + else: + raise PdfReadError("Could not read Boolean object") + + @staticmethod + def readFromStream(stream: StreamType) -> "BooleanObject": # pragma: no cover + deprecate_with_replacement("readFromStream", "read_from_stream") + return BooleanObject.read_from_stream(stream) + + +class IndirectObject(PdfObject): + def __init__(self, idnum: int, generation: int, pdf: Any) -> None: # PdfReader + self.idnum = idnum + self.generation = generation + self.pdf = pdf + + def get_object(self) -> Optional[PdfObject]: + obj = self.pdf.get_object(self) + if obj is None: + return None + return obj.get_object() + + def __repr__(self) -> str: + return f"IndirectObject({self.idnum!r}, {self.generation!r}, {id(self.pdf)})" + + def __eq__(self, other: Any) -> bool: + return ( + other is not None + and isinstance(other, IndirectObject) + and self.idnum == other.idnum + and self.generation == other.generation + and self.pdf is other.pdf + ) + + def __ne__(self, other: Any) -> bool: + return not self.__eq__(other) + + def write_to_stream( + self, stream: StreamType, encryption_key: Union[None, str, bytes] + ) -> None: + stream.write(b_(f"{self.idnum} {self.generation} R")) + + def writeToStream( + self, stream: StreamType, encryption_key: Union[None, str, bytes] + ) -> None: # pragma: no cover + deprecate_with_replacement("writeToStream", "write_to_stream") + self.write_to_stream(stream, encryption_key) + + @staticmethod + def read_from_stream(stream: StreamType, pdf: Any) -> "IndirectObject": # PdfReader + idnum = b"" + while True: + tok = stream.read(1) + if not tok: + raise PdfStreamError(STREAM_TRUNCATED_PREMATURELY) + if tok.isspace(): + break + idnum += tok + generation = b"" + while True: + tok = stream.read(1) + if not tok: + raise PdfStreamError(STREAM_TRUNCATED_PREMATURELY) + if tok.isspace(): + if not generation: + continue + break + generation += tok + r = read_non_whitespace(stream) + if r != b"R": + raise PdfReadError( + f"Error reading indirect object reference at byte {hex_str(stream.tell())}" + ) + return IndirectObject(int(idnum), int(generation), pdf) + + @staticmethod + def readFromStream( + stream: StreamType, pdf: Any # PdfReader + ) -> "IndirectObject": # pragma: no cover + deprecate_with_replacement("readFromStream", "read_from_stream") + return IndirectObject.read_from_stream(stream, pdf) + + +class FloatObject(decimal.Decimal, PdfObject): + def __new__( + cls, value: Union[str, Any] = "0", context: Optional[Any] = None + ) -> "FloatObject": + try: + return decimal.Decimal.__new__(cls, str_(value), context) + except Exception: + try: + return decimal.Decimal.__new__(cls, str(value)) + except decimal.InvalidOperation: + # If this isn't a valid decimal (happens in malformed PDFs) + # fallback to 0 + logger_warning(f"Invalid FloatObject {value}", __name__) + return decimal.Decimal.__new__(cls, "0") + + def __repr__(self) -> str: + if self == self.to_integral(): + return str(self.quantize(decimal.Decimal(1))) + else: + # Standard formatting adds useless extraneous zeros. + o = f"{self:.5f}" + # Remove the zeros. + while o and o[-1] == "0": + o = o[:-1] + return o + + def as_numeric(self) -> float: + return float(repr(self).encode("utf8")) + + def write_to_stream( + self, stream: StreamType, encryption_key: Union[None, str, bytes] + ) -> None: + stream.write(repr(self).encode("utf8")) + + def writeToStream( + self, stream: StreamType, encryption_key: Union[None, str, bytes] + ) -> None: # pragma: no cover + deprecate_with_replacement("writeToStream", "write_to_stream") + self.write_to_stream(stream, encryption_key) + + +class NumberObject(int, PdfObject): + NumberPattern = re.compile(b"[^+-.0-9]") + + def __new__(cls, value: Any) -> "NumberObject": + val = int(value) + try: + return int.__new__(cls, val) + except OverflowError: + return int.__new__(cls, 0) + + def as_numeric(self) -> int: + return int(repr(self).encode("utf8")) + + def write_to_stream( + self, stream: StreamType, encryption_key: Union[None, str, bytes] + ) -> None: + stream.write(repr(self).encode("utf8")) + + def writeToStream( + self, stream: StreamType, encryption_key: Union[None, str, bytes] + ) -> None: # pragma: no cover + deprecate_with_replacement("writeToStream", "write_to_stream") + self.write_to_stream(stream, encryption_key) + + @staticmethod + def read_from_stream(stream: StreamType) -> Union["NumberObject", FloatObject]: + num = read_until_regex(stream, NumberObject.NumberPattern) + if num.find(b".") != -1: + return FloatObject(num) + return NumberObject(num) + + @staticmethod + def readFromStream( + stream: StreamType, + ) -> Union["NumberObject", FloatObject]: # pragma: no cover + deprecate_with_replacement("readFromStream", "read_from_stream") + return NumberObject.read_from_stream(stream) + + +class ByteStringObject(bytes, PdfObject): + """ + Represents a string object where the text encoding could not be determined. + This occurs quite often, as the PDF spec doesn't provide an alternate way to + represent strings -- for example, the encryption data stored in files (like + /O) is clearly not text, but is still stored in a "String" object. + """ + + @property + def original_bytes(self) -> bytes: + """For compatibility with TextStringObject.original_bytes.""" + return self + + def write_to_stream( + self, stream: StreamType, encryption_key: Union[None, str, bytes] + ) -> None: + bytearr = self + if encryption_key: + from .._security import RC4_encrypt + + bytearr = RC4_encrypt(encryption_key, bytearr) # type: ignore + stream.write(b"<") + stream.write(hexencode(bytearr)) + stream.write(b">") + + def writeToStream( + self, stream: StreamType, encryption_key: Union[None, str, bytes] + ) -> None: # pragma: no cover + deprecate_with_replacement("writeToStream", "write_to_stream") + self.write_to_stream(stream, encryption_key) + + +class TextStringObject(str, PdfObject): + """ + Represents a string object that has been decoded into a real unicode string. + If read from a PDF document, this string appeared to match the + PDFDocEncoding, or contained a UTF-16BE BOM mark to cause UTF-16 decoding to + occur. + """ + + autodetect_pdfdocencoding = False + autodetect_utf16 = False + + @property + def original_bytes(self) -> bytes: + """ + It is occasionally possible that a text string object gets created where + a byte string object was expected due to the autodetection mechanism -- + if that occurs, this "original_bytes" property can be used to + back-calculate what the original encoded bytes were. + """ + return self.get_original_bytes() + + def get_original_bytes(self) -> bytes: + # We're a text string object, but the library is trying to get our raw + # bytes. This can happen if we auto-detected this string as text, but + # we were wrong. It's pretty common. Return the original bytes that + # would have been used to create this object, based upon the autodetect + # method. + if self.autodetect_utf16: + return codecs.BOM_UTF16_BE + self.encode("utf-16be") + elif self.autodetect_pdfdocencoding: + return encode_pdfdocencoding(self) + else: + raise Exception("no information about original bytes") + + def write_to_stream( + self, stream: StreamType, encryption_key: Union[None, str, bytes] + ) -> None: + # Try to write the string out as a PDFDocEncoding encoded string. It's + # nicer to look at in the PDF file. Sadly, we take a performance hit + # here for trying... + try: + bytearr = encode_pdfdocencoding(self) + except UnicodeEncodeError: + bytearr = codecs.BOM_UTF16_BE + self.encode("utf-16be") + if encryption_key: + from .._security import RC4_encrypt + + bytearr = RC4_encrypt(encryption_key, bytearr) + obj = ByteStringObject(bytearr) + obj.write_to_stream(stream, None) + else: + stream.write(b"(") + for c in bytearr: + if not chr(c).isalnum() and c != b" ": + # This: + # stream.write(b_(rf"\{c:0>3o}")) + # gives + # https://github.com/davidhalter/parso/issues/207 + stream.write(b_("\\%03o" % c)) + else: + stream.write(b_(chr(c))) + stream.write(b")") + + def writeToStream( + self, stream: StreamType, encryption_key: Union[None, str, bytes] + ) -> None: # pragma: no cover + deprecate_with_replacement("writeToStream", "write_to_stream") + self.write_to_stream(stream, encryption_key) + + +class NameObject(str, PdfObject): + delimiter_pattern = re.compile(rb"\s+|[\(\)<>\[\]{}/%]") + surfix = b"/" + + def write_to_stream( + self, stream: StreamType, encryption_key: Union[None, str, bytes] + ) -> None: + stream.write(b_(self)) + + def writeToStream( + self, stream: StreamType, encryption_key: Union[None, str, bytes] + ) -> None: # pragma: no cover + deprecate_with_replacement("writeToStream", "write_to_stream") + self.write_to_stream(stream, encryption_key) + + @staticmethod + def read_from_stream(stream: StreamType, pdf: Any) -> "NameObject": # PdfReader + name = stream.read(1) + if name != NameObject.surfix: + raise PdfReadError("name read error") + name += read_until_regex(stream, NameObject.delimiter_pattern, ignore_eof=True) + try: + try: + ret = name.decode("utf-8") + except (UnicodeEncodeError, UnicodeDecodeError): + ret = name.decode("gbk") + return NameObject(ret) + except (UnicodeEncodeError, UnicodeDecodeError) as e: + # Name objects should represent irregular characters + # with a '#' followed by the symbol's hex number + if not pdf.strict: + logger_warning("Illegal character in Name Object", __name__) + return NameObject(name) + else: + raise PdfReadError("Illegal character in Name Object") from e + + @staticmethod + def readFromStream( + stream: StreamType, pdf: Any # PdfReader + ) -> "NameObject": # pragma: no cover + deprecate_with_replacement("readFromStream", "read_from_stream") + return NameObject.read_from_stream(stream, pdf) + + +def encode_pdfdocencoding(unicode_string: str) -> bytes: + retval = b"" + for c in unicode_string: + try: + retval += b_(chr(_pdfdoc_encoding_rev[c])) + except KeyError: + raise UnicodeEncodeError( + "pdfdocencoding", c, -1, -1, "does not exist in translation table" + ) + return retval diff --git a/PyPDF2/generic/_data_structures.py b/PyPDF2/generic/_data_structures.py new file mode 100644 index 000000000..805ac2bed --- /dev/null +++ b/PyPDF2/generic/_data_structures.py @@ -0,0 +1,1147 @@ +# Copyright (c) 2006, Mathieu Fenniak +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + + +__author__ = "Mathieu Fenniak" +__author_email__ = "biziqe@mathieu.fenniak.net" + +import logging +import re +from io import BytesIO +from typing import Any, Dict, Iterable, List, Optional, Tuple, Union, cast + +from .._utils import ( + WHITESPACES, + StreamType, + b_, + deprecate_with_replacement, + hex_str, + logger_warning, + read_non_whitespace, + read_until_regex, + skip_over_comment, +) +from ..constants import ( + CheckboxRadioButtonAttributes, + FieldDictionaryAttributes, +) +from ..constants import FilterTypes as FT +from ..constants import OutlineFontFlag +from ..constants import StreamAttributes as SA +from ..constants import TypArguments as TA +from ..constants import TypFitArguments as TF +from ..errors import STREAM_TRUNCATED_PREMATURELY, PdfReadError, PdfStreamError +from ._base import ( + BooleanObject, + FloatObject, + IndirectObject, + NameObject, + NullObject, + NumberObject, + PdfObject, +) +from ._utils import read_hex_string_from_stream, read_string_from_stream + +logger = logging.getLogger(__name__) +ObjectPrefix = b"/<[tf(n%" +NumberSigns = b"+-" +IndirectPattern = re.compile(rb"[+-]?(\d+)\s+(\d+)\s+R[^a-zA-Z]") + + +class ArrayObject(list, PdfObject): + def items(self) -> Iterable[Any]: + """ + Emulate DictionaryObject.items for a list + (index, object) + """ + return enumerate(self) + + def write_to_stream( + self, stream: StreamType, encryption_key: Union[None, str, bytes] + ) -> None: + stream.write(b"[") + for data in self: + stream.write(b" ") + data.write_to_stream(stream, encryption_key) + stream.write(b" ]") + + def writeToStream( + self, stream: StreamType, encryption_key: Union[None, str, bytes] + ) -> None: # pragma: no cover + deprecate_with_replacement("writeToStream", "write_to_stream") + self.write_to_stream(stream, encryption_key) + + @staticmethod + def read_from_stream( + stream: StreamType, + pdf: Any, + forced_encoding: Union[None, str, List[str], Dict[int, str]] = None, + ) -> "ArrayObject": # PdfReader + arr = ArrayObject() + tmp = stream.read(1) + if tmp != b"[": + raise PdfReadError("Could not read array") + while True: + # skip leading whitespace + tok = stream.read(1) + while tok.isspace(): + tok = stream.read(1) + stream.seek(-1, 1) + # check for array ending + peekahead = stream.read(1) + if peekahead == b"]": + break + stream.seek(-1, 1) + # read and append obj + arr.append(read_object(stream, pdf, forced_encoding)) + return arr + + @staticmethod + def readFromStream( + stream: StreamType, pdf: Any # PdfReader + ) -> "ArrayObject": # pragma: no cover + deprecate_with_replacement("readFromStream", "read_from_stream") + return ArrayObject.read_from_stream(stream, pdf) + + +class DictionaryObject(dict, PdfObject): + def raw_get(self, key: Any) -> Any: + return dict.__getitem__(self, key) + + def __setitem__(self, key: Any, value: Any) -> Any: + if not isinstance(key, PdfObject): + raise ValueError("key must be PdfObject") + if not isinstance(value, PdfObject): + raise ValueError("value must be PdfObject") + return dict.__setitem__(self, key, value) + + def setdefault(self, key: Any, value: Optional[Any] = None) -> Any: + if not isinstance(key, PdfObject): + raise ValueError("key must be PdfObject") + if not isinstance(value, PdfObject): + raise ValueError("value must be PdfObject") + return dict.setdefault(self, key, value) # type: ignore + + def __getitem__(self, key: Any) -> PdfObject: + return dict.__getitem__(self, key).get_object() + + @property + def xmp_metadata(self) -> Optional[PdfObject]: + """ + Retrieve XMP (Extensible Metadata Platform) data relevant to the + this object, if available. + + Stability: Added in v1.12, will exist for all future v1.x releases. + @return Returns a {@link #xmp.XmpInformation XmlInformation} instance + that can be used to access XMP metadata from the document. Can also + return None if no metadata was found on the document root. + """ + from ..xmp import XmpInformation + + metadata = self.get("/Metadata", None) + if metadata is None: + return None + metadata = metadata.get_object() + + if not isinstance(metadata, XmpInformation): + metadata = XmpInformation(metadata) + self[NameObject("/Metadata")] = metadata + return metadata + + def getXmpMetadata( + self, + ) -> Optional[PdfObject]: # pragma: no cover + """ + .. deprecated:: 1.28.3 + + Use :meth:`xmp_metadata` instead. + """ + deprecate_with_replacement("getXmpMetadata", "xmp_metadata") + return self.xmp_metadata + + @property + def xmpMetadata(self) -> Optional[PdfObject]: # pragma: no cover + """ + .. deprecated:: 1.28.3 + + Use :meth:`xmp_metadata` instead. + """ + deprecate_with_replacement("xmpMetadata", "xmp_metadata") + return self.xmp_metadata + + def write_to_stream( + self, stream: StreamType, encryption_key: Union[None, str, bytes] + ) -> None: + stream.write(b"<<\n") + for key, value in list(self.items()): + key.write_to_stream(stream, encryption_key) + stream.write(b" ") + value.write_to_stream(stream, encryption_key) + stream.write(b"\n") + stream.write(b">>") + + def writeToStream( + self, stream: StreamType, encryption_key: Union[None, str, bytes] + ) -> None: # pragma: no cover + deprecate_with_replacement("writeToStream", "write_to_stream") + self.write_to_stream(stream, encryption_key) + + @staticmethod + def read_from_stream( + stream: StreamType, + pdf: Any, # PdfReader + forced_encoding: Union[None, str, List[str], Dict[int, str]] = None, + ) -> "DictionaryObject": + def get_next_obj_pos( + p: int, p1: int, rem_gens: List[int], pdf: Any + ) -> int: # PdfReader + l = pdf.xref[rem_gens[0]] + for o in l: + if p1 > l[o] and p < l[o]: + p1 = l[o] + if len(rem_gens) == 1: + return p1 + else: + return get_next_obj_pos(p, p1, rem_gens[1:], pdf) + + def read_unsized_from_steam(stream: StreamType, pdf: Any) -> bytes: # PdfReader + # we are just pointing at beginning of the stream + eon = get_next_obj_pos(stream.tell(), 2**32, list(pdf.xref), pdf) - 1 + curr = stream.tell() + rw = stream.read(eon - stream.tell()) + p = rw.find(b"endstream") + if p < 0: + raise PdfReadError( + f"Unable to find 'endstream' marker for obj starting at {curr}." + ) + stream.seek(curr + p + 9) + return rw[: p - 1] + + tmp = stream.read(2) + if tmp != b"<<": + raise PdfReadError( + f"Dictionary read error at byte {hex_str(stream.tell())}: " + "stream must begin with '<<'" + ) + data: Dict[Any, Any] = {} + while True: + tok = read_non_whitespace(stream) + if tok == b"\x00": + continue + elif tok == b"%": + stream.seek(-1, 1) + skip_over_comment(stream) + continue + if not tok: + raise PdfStreamError(STREAM_TRUNCATED_PREMATURELY) + + if tok == b">": + stream.read(1) + break + stream.seek(-1, 1) + key = read_object(stream, pdf) + tok = read_non_whitespace(stream) + stream.seek(-1, 1) + value = read_object(stream, pdf, forced_encoding) + if not data.get(key): + data[key] = value + else: + # multiple definitions of key not permitted + msg = ( + f"Multiple definitions in dictionary at byte " + f"{hex_str(stream.tell())} for key {key}" + ) + if pdf is not None and pdf.strict: + raise PdfReadError(msg) + logger_warning(msg, __name__) + + pos = stream.tell() + s = read_non_whitespace(stream) + if s == b"s" and stream.read(5) == b"tream": + eol = stream.read(1) + # odd PDF file output has spaces after 'stream' keyword but before EOL. + # patch provided by Danial Sandler + while eol == b" ": + eol = stream.read(1) + if eol not in (b"\n", b"\r"): + raise PdfStreamError("Stream data must be followed by a newline") + if eol == b"\r": + # read \n after + if stream.read(1) != b"\n": + stream.seek(-1, 1) + # this is a stream object, not a dictionary + if SA.LENGTH not in data: + raise PdfStreamError("Stream length not defined") + length = data[SA.LENGTH] + if isinstance(length, IndirectObject): + t = stream.tell() + length = pdf.get_object(length) + stream.seek(t, 0) + pstart = stream.tell() + data["__streamdata__"] = stream.read(length) + e = read_non_whitespace(stream) + ndstream = stream.read(8) + if (e + ndstream) != b"endstream": + # (sigh) - the odd PDF file has a length that is too long, so + # we need to read backwards to find the "endstream" ending. + # ReportLab (unknown version) generates files with this bug, + # and Python users into PDF files tend to be our audience. + # we need to do this to correct the streamdata and chop off + # an extra character. + pos = stream.tell() + stream.seek(-10, 1) + end = stream.read(9) + if end == b"endstream": + # we found it by looking back one character further. + data["__streamdata__"] = data["__streamdata__"][:-1] + elif not pdf.strict: + stream.seek(pstart, 0) + data["__streamdata__"] = read_unsized_from_steam(stream, pdf) + pos = stream.tell() + else: + stream.seek(pos, 0) + raise PdfReadError( + "Unable to find 'endstream' marker after stream at byte " + f"{hex_str(stream.tell())} (nd='{ndstream!r}', end='{end!r}')." + ) + else: + stream.seek(pos, 0) + if "__streamdata__" in data: + return StreamObject.initialize_from_dictionary(data) + else: + retval = DictionaryObject() + retval.update(data) + return retval + + @staticmethod + def readFromStream( + stream: StreamType, pdf: Any # PdfReader + ) -> "DictionaryObject": # pragma: no cover + deprecate_with_replacement("readFromStream", "read_from_stream") + return DictionaryObject.read_from_stream(stream, pdf) + + +class TreeObject(DictionaryObject): + def __init__(self) -> None: + DictionaryObject.__init__(self) + + def hasChildren(self) -> bool: # pragma: no cover + deprecate_with_replacement("hasChildren", "has_children", "4.0.0") + return self.has_children() + + def has_children(self) -> bool: + return "/First" in self + + def __iter__(self) -> Any: + return self.children() + + def children(self) -> Optional[Any]: + if not self.has_children(): + return + + child = self["/First"] + while True: + yield child + if child == self["/Last"]: + return + child = child["/Next"] # type: ignore + + def addChild(self, child: Any, pdf: Any) -> None: # pragma: no cover + deprecate_with_replacement("addChild", "add_child") + self.add_child(child, pdf) + + def add_child(self, child: Any, pdf: Any) -> None: # PdfReader + child_obj = child.get_object() + child = pdf.get_reference(child_obj) + assert isinstance(child, IndirectObject) + + prev: Optional[DictionaryObject] + if "/First" not in self: + self[NameObject("/First")] = child + self[NameObject("/Count")] = NumberObject(0) + prev = None + else: + prev = cast( + DictionaryObject, self["/Last"] + ) # TABLE 8.3 Entries in the outline dictionary + + self[NameObject("/Last")] = child + self[NameObject("/Count")] = NumberObject(self[NameObject("/Count")] + 1) # type: ignore + + if prev: + prev_ref = pdf.get_reference(prev) + assert isinstance(prev_ref, IndirectObject) + child_obj[NameObject("/Prev")] = prev_ref + prev[NameObject("/Next")] = child + + parent_ref = pdf.get_reference(self) + assert isinstance(parent_ref, IndirectObject) + child_obj[NameObject("/Parent")] = parent_ref + + def removeChild(self, child: Any) -> None: # pragma: no cover + deprecate_with_replacement("removeChild", "remove_child") + self.remove_child(child) + + def remove_child(self, child: Any) -> None: + child_obj = child.get_object() + + if NameObject("/Parent") not in child_obj: + raise ValueError("Removed child does not appear to be a tree item") + elif child_obj[NameObject("/Parent")] != self: + raise ValueError("Removed child is not a member of this tree") + + found = False + prev_ref = None + prev = None + cur_ref: Optional[Any] = self[NameObject("/First")] + cur: Optional[Dict[str, Any]] = cur_ref.get_object() # type: ignore + last_ref = self[NameObject("/Last")] + last = last_ref.get_object() + while cur is not None: + if cur == child_obj: + if prev is None: + if NameObject("/Next") in cur: + # Removing first tree node + next_ref = cur[NameObject("/Next")] + next_obj = next_ref.get_object() + del next_obj[NameObject("/Prev")] + self[NameObject("/First")] = next_ref + self[NameObject("/Count")] -= 1 # type: ignore + + else: + # Removing only tree node + assert self[NameObject("/Count")] == 1 + del self[NameObject("/Count")] + del self[NameObject("/First")] + if NameObject("/Last") in self: + del self[NameObject("/Last")] + else: + if NameObject("/Next") in cur: + # Removing middle tree node + next_ref = cur[NameObject("/Next")] + next_obj = next_ref.get_object() + next_obj[NameObject("/Prev")] = prev_ref + prev[NameObject("/Next")] = next_ref + self[NameObject("/Count")] -= 1 + else: + # Removing last tree node + assert cur == last + del prev[NameObject("/Next")] + self[NameObject("/Last")] = prev_ref + self[NameObject("/Count")] -= 1 + found = True + break + + prev_ref = cur_ref + prev = cur + if NameObject("/Next") in cur: + cur_ref = cur[NameObject("/Next")] + cur = cur_ref.get_object() + else: + cur_ref = None + cur = None + + if not found: + raise ValueError("Removal couldn't find item in tree") + + del child_obj[NameObject("/Parent")] + if NameObject("/Next") in child_obj: + del child_obj[NameObject("/Next")] + if NameObject("/Prev") in child_obj: + del child_obj[NameObject("/Prev")] + + def emptyTree(self) -> None: # pragma: no cover + deprecate_with_replacement("emptyTree", "empty_tree", "4.0.0") + self.empty_tree() + + def empty_tree(self) -> None: + for child in self: + child_obj = child.get_object() + del child_obj[NameObject("/Parent")] + if NameObject("/Next") in child_obj: + del child_obj[NameObject("/Next")] + if NameObject("/Prev") in child_obj: + del child_obj[NameObject("/Prev")] + + if NameObject("/Count") in self: + del self[NameObject("/Count")] + if NameObject("/First") in self: + del self[NameObject("/First")] + if NameObject("/Last") in self: + del self[NameObject("/Last")] + + +class StreamObject(DictionaryObject): + def __init__(self) -> None: + self.__data: Optional[str] = None + self.decoded_self: Optional[DecodedStreamObject] = None + + def hash_value_data(self) -> bytes: + data = super().hash_value_data() + data += b_(self._data) + return data + + @property + def decodedSelf(self) -> Optional["DecodedStreamObject"]: # pragma: no cover + deprecate_with_replacement("decodedSelf", "decoded_self") + return self.decoded_self + + @decodedSelf.setter + def decodedSelf(self, value: "DecodedStreamObject") -> None: # pragma: no cover + deprecate_with_replacement("decodedSelf", "decoded_self") + self.decoded_self = value + + @property + def _data(self) -> Any: + return self.__data + + @_data.setter + def _data(self, value: Any) -> None: + self.__data = value + + def write_to_stream( + self, stream: StreamType, encryption_key: Union[None, str, bytes] + ) -> None: + self[NameObject(SA.LENGTH)] = NumberObject(len(self._data)) + DictionaryObject.write_to_stream(self, stream, encryption_key) + del self[SA.LENGTH] + stream.write(b"\nstream\n") + data = self._data + if encryption_key: + from .._security import RC4_encrypt + + data = RC4_encrypt(encryption_key, data) + stream.write(data) + stream.write(b"\nendstream") + + @staticmethod + def initializeFromDictionary( + data: Dict[str, Any] + ) -> Union["EncodedStreamObject", "DecodedStreamObject"]: # pragma: no cover + return StreamObject.initialize_from_dictionary(data) + + @staticmethod + def initialize_from_dictionary( + data: Dict[str, Any] + ) -> Union["EncodedStreamObject", "DecodedStreamObject"]: + retval: Union["EncodedStreamObject", "DecodedStreamObject"] + if SA.FILTER in data: + retval = EncodedStreamObject() + else: + retval = DecodedStreamObject() + retval._data = data["__streamdata__"] + del data["__streamdata__"] + del data[SA.LENGTH] + retval.update(data) + return retval + + def flateEncode(self) -> "EncodedStreamObject": # pragma: no cover + deprecate_with_replacement("flateEncode", "flate_encode") + return self.flate_encode() + + def flate_encode(self) -> "EncodedStreamObject": + from ..filters import FlateDecode + + if SA.FILTER in self: + f = self[SA.FILTER] + if isinstance(f, ArrayObject): + f.insert(0, NameObject(FT.FLATE_DECODE)) + else: + newf = ArrayObject() + newf.append(NameObject("/FlateDecode")) + newf.append(f) + f = newf + else: + f = NameObject("/FlateDecode") + retval = EncodedStreamObject() + retval[NameObject(SA.FILTER)] = f + retval._data = FlateDecode.encode(self._data) + return retval + + +class DecodedStreamObject(StreamObject): + def get_data(self) -> Any: + return self._data + + def set_data(self, data: Any) -> Any: + self._data = data + + def getData(self) -> Any: # pragma: no cover + deprecate_with_replacement("getData", "get_data") + return self._data + + def setData(self, data: Any) -> None: # pragma: no cover + deprecate_with_replacement("setData", "set_data") + self.set_data(data) + + +class EncodedStreamObject(StreamObject): + def __init__(self) -> None: + self.decoded_self: Optional[DecodedStreamObject] = None + + @property + def decodedSelf(self) -> Optional["DecodedStreamObject"]: # pragma: no cover + deprecate_with_replacement("decodedSelf", "decoded_self") + return self.decoded_self + + @decodedSelf.setter + def decodedSelf(self, value: DecodedStreamObject) -> None: # pragma: no cover + deprecate_with_replacement("decodedSelf", "decoded_self") + self.decoded_self = value + + def get_data(self) -> Union[None, str, bytes]: + from ..filters import decode_stream_data + + if self.decoded_self is not None: + # cached version of decoded object + return self.decoded_self.get_data() + else: + # create decoded object + decoded = DecodedStreamObject() + + decoded._data = decode_stream_data(self) + for key, value in list(self.items()): + if key not in (SA.LENGTH, SA.FILTER, SA.DECODE_PARMS): + decoded[key] = value + self.decoded_self = decoded + return decoded._data + + def getData(self) -> Union[None, str, bytes]: # pragma: no cover + deprecate_with_replacement("getData", "get_data") + return self.get_data() + + def set_data(self, data: Any) -> None: + raise PdfReadError("Creating EncodedStreamObject is not currently supported") + + def setData(self, data: Any) -> None: # pragma: no cover + deprecate_with_replacement("setData", "set_data") + return self.set_data(data) + + +class ContentStream(DecodedStreamObject): + def __init__( + self, + stream: Any, + pdf: Any, + forced_encoding: Union[None, str, List[str], Dict[int, str]] = None, + ) -> None: + self.pdf = pdf + + # The inner list has two elements: + # [0] : List + # [1] : str + self.operations: List[Tuple[Any, Any]] = [] + + # stream may be a StreamObject or an ArrayObject containing + # multiple StreamObjects to be cat'd together. + stream = stream.get_object() + if isinstance(stream, ArrayObject): + data = b"" + for s in stream: + data += b_(s.get_object().get_data()) + stream_bytes = BytesIO(data) + else: + stream_data = stream.get_data() + assert stream_data is not None + stream_data_bytes = b_(stream_data) + stream_bytes = BytesIO(stream_data_bytes) + self.forced_encoding = forced_encoding + self.__parse_content_stream(stream_bytes) + + def __parse_content_stream(self, stream: StreamType) -> None: + stream.seek(0, 0) + operands: List[Union[int, str, PdfObject]] = [] + while True: + peek = read_non_whitespace(stream) + if peek == b"" or peek == 0: + break + stream.seek(-1, 1) + if peek.isalpha() or peek in (b"'", b'"'): + operator = read_until_regex(stream, NameObject.delimiter_pattern, True) + if operator == b"BI": + # begin inline image - a completely different parsing + # mechanism is required, of course... thanks buddy... + assert operands == [] + ii = self._read_inline_image(stream) + self.operations.append((ii, b"INLINE IMAGE")) + else: + self.operations.append((operands, operator)) + operands = [] + elif peek == b"%": + # If we encounter a comment in the content stream, we have to + # handle it here. Typically, read_object will handle + # encountering a comment -- but read_object assumes that + # following the comment must be the object we're trying to + # read. In this case, it could be an operator instead. + while peek not in (b"\r", b"\n"): + peek = stream.read(1) + else: + operands.append(read_object(stream, None, self.forced_encoding)) + + def _read_inline_image(self, stream: StreamType) -> Dict[str, Any]: + # begin reading just after the "BI" - begin image + # first read the dictionary of settings. + settings = DictionaryObject() + while True: + tok = read_non_whitespace(stream) + stream.seek(-1, 1) + if tok == b"I": + # "ID" - begin of image data + break + key = read_object(stream, self.pdf) + tok = read_non_whitespace(stream) + stream.seek(-1, 1) + value = read_object(stream, self.pdf) + settings[key] = value + # left at beginning of ID + tmp = stream.read(3) + assert tmp[:2] == b"ID" + data = BytesIO() + # Read the inline image, while checking for EI (End Image) operator. + while True: + # Read 8 kB at a time and check if the chunk contains the E operator. + buf = stream.read(8192) + # We have reached the end of the stream, but haven't found the EI operator. + if not buf: + raise PdfReadError("Unexpected end of stream") + loc = buf.find(b"E") + + if loc == -1: + data.write(buf) + else: + # Write out everything before the E. + data.write(buf[0:loc]) + + # Seek back in the stream to read the E next. + stream.seek(loc - len(buf), 1) + tok = stream.read(1) + # Check for End Image + tok2 = stream.read(1) + if tok2 == b"I": + # Data can contain EI, so check for the Q operator. + tok3 = stream.read(1) + info = tok + tok2 + # We need to find whitespace between EI and Q. + has_q_whitespace = False + while tok3 in WHITESPACES: + has_q_whitespace = True + info += tok3 + tok3 = stream.read(1) + if tok3 == b"Q" and has_q_whitespace: + stream.seek(-1, 1) + break + else: + stream.seek(-1, 1) + data.write(info) + else: + stream.seek(-1, 1) + data.write(tok) + return {"settings": settings, "data": data.getvalue()} + + @property + def _data(self) -> bytes: + newdata = BytesIO() + for operands, operator in self.operations: + if operator == b"INLINE IMAGE": + newdata.write(b"BI") + dicttext = BytesIO() + operands["settings"].write_to_stream(dicttext, None) + newdata.write(dicttext.getvalue()[2:-2]) + newdata.write(b"ID ") + newdata.write(operands["data"]) + newdata.write(b"EI") + else: + for op in operands: + op.write_to_stream(newdata, None) + newdata.write(b" ") + newdata.write(b_(operator)) + newdata.write(b"\n") + return newdata.getvalue() + + @_data.setter + def _data(self, value: Union[str, bytes]) -> None: + self.__parse_content_stream(BytesIO(b_(value))) + + +def read_object( + stream: StreamType, + pdf: Any, # PdfReader + forced_encoding: Union[None, str, List[str], Dict[int, str]] = None, +) -> Union[PdfObject, int, str, ContentStream]: + tok = stream.read(1) + stream.seek(-1, 1) # reset to start + idx = ObjectPrefix.find(tok) + if idx == 0: + return NameObject.read_from_stream(stream, pdf) + elif idx == 1: + # hexadecimal string OR dictionary + peek = stream.read(2) + stream.seek(-2, 1) # reset to start + + if peek == b"<<": + return DictionaryObject.read_from_stream(stream, pdf, forced_encoding) + else: + return read_hex_string_from_stream(stream, forced_encoding) + elif idx == 2: + return ArrayObject.read_from_stream(stream, pdf, forced_encoding) + elif idx == 3 or idx == 4: + return BooleanObject.read_from_stream(stream) + elif idx == 5: + return read_string_from_stream(stream, forced_encoding) + elif idx == 6: + return NullObject.read_from_stream(stream) + elif idx == 7: + # comment + while tok not in (b"\r", b"\n"): + tok = stream.read(1) + # Prevents an infinite loop by raising an error if the stream is at + # the EOF + if len(tok) <= 0: + raise PdfStreamError("File ended unexpectedly.") + tok = read_non_whitespace(stream) + stream.seek(-1, 1) + return read_object(stream, pdf, forced_encoding) + else: + # number object OR indirect reference + peek = stream.read(20) + stream.seek(-len(peek), 1) # reset to start + if IndirectPattern.match(peek) is not None: + return IndirectObject.read_from_stream(stream, pdf) + else: + return NumberObject.read_from_stream(stream) + + +class Field(TreeObject): + """ + A class representing a field dictionary. + + This class is accessed through + :meth:`get_fields()` + """ + + def __init__(self, data: Dict[str, Any]) -> None: + DictionaryObject.__init__(self) + field_attributes = ( + FieldDictionaryAttributes.attributes() + + CheckboxRadioButtonAttributes.attributes() + ) + for attr in field_attributes: + try: + self[NameObject(attr)] = data[attr] + except KeyError: + pass + + # TABLE 8.69 Entries common to all field dictionaries + @property + def field_type(self) -> Optional[NameObject]: + """Read-only property accessing the type of this field.""" + return self.get(FieldDictionaryAttributes.FT) + + @property + def fieldType(self) -> Optional[NameObject]: # pragma: no cover + """ + .. deprecated:: 1.28.3 + + Use :py:attr:`field_type` instead. + """ + deprecate_with_replacement("fieldType", "field_type") + return self.field_type + + @property + def parent(self) -> Optional[DictionaryObject]: + """Read-only property accessing the parent of this field.""" + return self.get(FieldDictionaryAttributes.Parent) + + @property + def kids(self) -> Optional[ArrayObject]: + """Read-only property accessing the kids of this field.""" + return self.get(FieldDictionaryAttributes.Kids) + + @property + def name(self) -> Optional[str]: + """Read-only property accessing the name of this field.""" + return self.get(FieldDictionaryAttributes.T) + + @property + def alternate_name(self) -> Optional[str]: + """Read-only property accessing the alternate name of this field.""" + return self.get(FieldDictionaryAttributes.TU) + + @property + def altName(self) -> Optional[str]: # pragma: no cover + """ + .. deprecated:: 1.28.3 + + Use :py:attr:`alternate_name` instead. + """ + deprecate_with_replacement("altName", "alternate_name") + return self.alternate_name + + @property + def mapping_name(self) -> Optional[str]: + """ + Read-only property accessing the mapping name of this field. This + name is used by PyPDF2 as a key in the dictionary returned by + :meth:`get_fields()` + """ + return self.get(FieldDictionaryAttributes.TM) + + @property + def mappingName(self) -> Optional[str]: # pragma: no cover + """ + .. deprecated:: 1.28.3 + + Use :py:attr:`mapping_name` instead. + """ + deprecate_with_replacement("mappingName", "mapping_name") + return self.mapping_name + + @property + def flags(self) -> Optional[int]: + """ + Read-only property accessing the field flags, specifying various + characteristics of the field (see Table 8.70 of the PDF 1.7 reference). + """ + return self.get(FieldDictionaryAttributes.Ff) + + @property + def value(self) -> Optional[Any]: + """ + Read-only property accessing the value of this field. Format + varies based on field type. + """ + return self.get(FieldDictionaryAttributes.V) + + @property + def default_value(self) -> Optional[Any]: + """Read-only property accessing the default value of this field.""" + return self.get(FieldDictionaryAttributes.DV) + + @property + def defaultValue(self) -> Optional[Any]: # pragma: no cover + """ + .. deprecated:: 1.28.3 + + Use :py:attr:`default_value` instead. + """ + deprecate_with_replacement("defaultValue", "default_value") + return self.default_value + + @property + def additional_actions(self) -> Optional[DictionaryObject]: + """ + Read-only property accessing the additional actions dictionary. + This dictionary defines the field's behavior in response to trigger events. + See Section 8.5.2 of the PDF 1.7 reference. + """ + return self.get(FieldDictionaryAttributes.AA) + + @property + def additionalActions(self) -> Optional[DictionaryObject]: # pragma: no cover + """ + .. deprecated:: 1.28.3 + + Use :py:attr:`additional_actions` instead. + """ + deprecate_with_replacement("additionalActions", "additional_actions") + return self.additional_actions + + +class Destination(TreeObject): + """ + A class representing a destination within a PDF file. + See section 8.2.1 of the PDF 1.6 reference. + + :param str title: Title of this destination. + :param IndirectObject page: Reference to the page of this destination. Should + be an instance of :class:`IndirectObject`. + :param str typ: How the destination is displayed. + :param args: Additional arguments may be necessary depending on the type. + :raises PdfReadError: If destination type is invalid. + + .. list-table:: Valid ``typ`` arguments (see PDF spec for details) + :widths: 50 50 + + * - /Fit + - No additional arguments + * - /XYZ + - [left] [top] [zoomFactor] + * - /FitH + - [top] + * - /FitV + - [left] + * - /FitR + - [left] [bottom] [right] [top] + * - /FitB + - No additional arguments + * - /FitBH + - [top] + * - /FitBV + - [left] + """ + + def __init__( + self, + title: str, + page: Union[NumberObject, IndirectObject, NullObject, DictionaryObject], + typ: Union[str, NumberObject], + *args: Any, # ZoomArgType + ) -> None: + DictionaryObject.__init__(self) + self[NameObject("/Title")] = title + self[NameObject("/Page")] = page + self[NameObject("/Type")] = typ + + # from table 8.2 of the PDF 1.7 reference. + if typ == "/XYZ": + ( + self[NameObject(TA.LEFT)], + self[NameObject(TA.TOP)], + self[NameObject("/Zoom")], + ) = args + elif typ == TF.FIT_R: + ( + self[NameObject(TA.LEFT)], + self[NameObject(TA.BOTTOM)], + self[NameObject(TA.RIGHT)], + self[NameObject(TA.TOP)], + ) = args + elif typ in [TF.FIT_H, TF.FIT_BH]: + try: # Prefered to be more robust not only to null parameters + (self[NameObject(TA.TOP)],) = args + except Exception: + (self[NameObject(TA.TOP)],) = (NullObject(),) + elif typ in [TF.FIT_V, TF.FIT_BV]: + try: # Prefered to be more robust not only to null parameters + (self[NameObject(TA.LEFT)],) = args + except Exception: + (self[NameObject(TA.LEFT)],) = (NullObject(),) + elif typ in [TF.FIT, TF.FIT_B]: + pass + else: + raise PdfReadError(f"Unknown Destination Type: {typ!r}") + + @property + def dest_array(self) -> ArrayObject: + return ArrayObject( + [self.raw_get("/Page"), self["/Type"]] + + [ + self[x] + for x in ["/Left", "/Bottom", "/Right", "/Top", "/Zoom"] + if x in self + ] + ) + + def getDestArray(self) -> ArrayObject: # pragma: no cover + """ + .. deprecated:: 1.28.3 + + Use :py:attr:`dest_array` instead. + """ + deprecate_with_replacement("getDestArray", "dest_array") + return self.dest_array + + def write_to_stream( + self, stream: StreamType, encryption_key: Union[None, str, bytes] + ) -> None: + stream.write(b"<<\n") + key = NameObject("/D") + key.write_to_stream(stream, encryption_key) + stream.write(b" ") + value = self.dest_array + value.write_to_stream(stream, encryption_key) + + key = NameObject("/S") + key.write_to_stream(stream, encryption_key) + stream.write(b" ") + value_s = NameObject("/GoTo") + value_s.write_to_stream(stream, encryption_key) + + stream.write(b"\n") + stream.write(b">>") + + @property + def title(self) -> Optional[str]: + """Read-only property accessing the destination title.""" + return self.get("/Title") + + @property + def page(self) -> Optional[int]: + """Read-only property accessing the destination page number.""" + return self.get("/Page") + + @property + def typ(self) -> Optional[str]: + """Read-only property accessing the destination type.""" + return self.get("/Type") + + @property + def zoom(self) -> Optional[int]: + """Read-only property accessing the zoom factor.""" + return self.get("/Zoom", None) + + @property + def left(self) -> Optional[FloatObject]: + """Read-only property accessing the left horizontal coordinate.""" + return self.get("/Left", None) + + @property + def right(self) -> Optional[FloatObject]: + """Read-only property accessing the right horizontal coordinate.""" + return self.get("/Right", None) + + @property + def top(self) -> Optional[FloatObject]: + """Read-only property accessing the top vertical coordinate.""" + return self.get("/Top", None) + + @property + def bottom(self) -> Optional[FloatObject]: + """Read-only property accessing the bottom vertical coordinate.""" + return self.get("/Bottom", None) + + @property + def color(self) -> Optional[ArrayObject]: + """Read-only property accessing the color in (R, G, B) with values 0.0-1.0""" + return self.get( + "/C", ArrayObject([FloatObject(0), FloatObject(0), FloatObject(0)]) + ) + + @property + def font_format(self) -> Optional[OutlineFontFlag]: + """Read-only property accessing the font type. 1=italic, 2=bold, 3=both""" + return self.get("/F", 0) + + @property + def outline_count(self) -> Optional[int]: + """ + Read-only property accessing the outline count. + positive = expanded + negative = collapsed + absolute value = number of visible descendents at all levels + """ + return self.get("/Count", None) diff --git a/PyPDF2/generic/_outline.py b/PyPDF2/generic/_outline.py new file mode 100644 index 000000000..0c8b17cdf --- /dev/null +++ b/PyPDF2/generic/_outline.py @@ -0,0 +1,35 @@ +from typing import Any, Union + +from .._utils import StreamType, deprecate_with_replacement +from ._base import NameObject +from ._data_structures import Destination + + +class OutlineItem(Destination): + def write_to_stream( + self, stream: StreamType, encryption_key: Union[None, str, bytes] + ) -> None: + stream.write(b"<<\n") + for key in [ + NameObject(x) + for x in ["/Title", "/Parent", "/First", "/Last", "/Next", "/Prev"] + if x in self + ]: + key.write_to_stream(stream, encryption_key) + stream.write(b" ") + value = self.raw_get(key) + value.write_to_stream(stream, encryption_key) + stream.write(b"\n") + key = NameObject("/Dest") + key.write_to_stream(stream, encryption_key) + stream.write(b" ") + value = self.dest_array + value.write_to_stream(stream, encryption_key) + stream.write(b"\n") + stream.write(b">>") + + +class Bookmark(OutlineItem): # pragma: no cover + def __init__(self, *args: Any, **kwargs: Any) -> None: + deprecate_with_replacement("Bookmark", "OutlineItem") + super().__init__(*args, **kwargs) diff --git a/PyPDF2/generic/_rectangle.py b/PyPDF2/generic/_rectangle.py new file mode 100644 index 000000000..e8e19d5ab --- /dev/null +++ b/PyPDF2/generic/_rectangle.py @@ -0,0 +1,249 @@ +import decimal +from typing import Any, List, Tuple, Union + +from .._utils import deprecate_no_replacement, deprecate_with_replacement +from ._base import FloatObject, NumberObject +from ._data_structures import ArrayObject + + +class RectangleObject(ArrayObject): + """ + This class is used to represent *page boxes* in PyPDF2. These boxes include: + * :attr:`artbox ` + * :attr:`bleedbox ` + * :attr:`cropbox ` + * :attr:`mediabox ` + * :attr:`trimbox ` + """ + + def __init__( + self, arr: Union["RectangleObject", Tuple[float, float, float, float]] + ) -> None: + # must have four points + assert len(arr) == 4 + # automatically convert arr[x] into NumberObject(arr[x]) if necessary + ArrayObject.__init__(self, [self._ensure_is_number(x) for x in arr]) # type: ignore + + def _ensure_is_number(self, value: Any) -> Union[FloatObject, NumberObject]: + if not isinstance(value, (NumberObject, FloatObject)): + value = FloatObject(value) + return value + + def scale(self, sx: float, sy: float) -> "RectangleObject": + return RectangleObject( + ( + float(self.left) * sx, + float(self.bottom) * sy, + float(self.right) * sx, + float(self.top) * sy, + ) + ) + + def ensureIsNumber( + self, value: Any + ) -> Union[FloatObject, NumberObject]: # pragma: no cover + deprecate_no_replacement("ensureIsNumber") + return self._ensure_is_number(value) + + def __repr__(self) -> str: + return f"RectangleObject({repr(list(self))})" + + @property + def left(self) -> FloatObject: + return self[0] + + @property + def bottom(self) -> FloatObject: + return self[1] + + @property + def right(self) -> FloatObject: + return self[2] + + @property + def top(self) -> FloatObject: + return self[3] + + def getLowerLeft_x(self) -> FloatObject: # pragma: no cover + deprecate_with_replacement("getLowerLeft_x", "left") + return self.left + + def getLowerLeft_y(self) -> FloatObject: # pragma: no cover + deprecate_with_replacement("getLowerLeft_y", "bottom") + return self.bottom + + def getUpperRight_x(self) -> FloatObject: # pragma: no cover + deprecate_with_replacement("getUpperRight_x", "right") + return self.right + + def getUpperRight_y(self) -> FloatObject: # pragma: no cover + deprecate_with_replacement("getUpperRight_y", "top") + return self.top + + def getUpperLeft_x(self) -> FloatObject: # pragma: no cover + deprecate_with_replacement("getUpperLeft_x", "left") + return self.left + + def getUpperLeft_y(self) -> FloatObject: # pragma: no cover + deprecate_with_replacement("getUpperLeft_y", "top") + return self.top + + def getLowerRight_x(self) -> FloatObject: # pragma: no cover + deprecate_with_replacement("getLowerRight_x", "right") + return self.right + + def getLowerRight_y(self) -> FloatObject: # pragma: no cover + deprecate_with_replacement("getLowerRight_y", "bottom") + return self.bottom + + @property + def lower_left(self) -> Tuple[decimal.Decimal, decimal.Decimal]: + """ + Property to read and modify the lower left coordinate of this box + in (x,y) form. + """ + return self.left, self.bottom + + @lower_left.setter + def lower_left(self, value: List[Any]) -> None: + self[0], self[1] = (self._ensure_is_number(x) for x in value) + + @property + def lower_right(self) -> Tuple[decimal.Decimal, decimal.Decimal]: + """ + Property to read and modify the lower right coordinate of this box + in (x,y) form. + """ + return self.right, self.bottom + + @lower_right.setter + def lower_right(self, value: List[Any]) -> None: + self[2], self[1] = (self._ensure_is_number(x) for x in value) + + @property + def upper_left(self) -> Tuple[decimal.Decimal, decimal.Decimal]: + """ + Property to read and modify the upper left coordinate of this box + in (x,y) form. + """ + return self.left, self.top + + @upper_left.setter + def upper_left(self, value: List[Any]) -> None: + self[0], self[3] = (self._ensure_is_number(x) for x in value) + + @property + def upper_right(self) -> Tuple[decimal.Decimal, decimal.Decimal]: + """ + Property to read and modify the upper right coordinate of this box + in (x,y) form. + """ + return self.right, self.top + + @upper_right.setter + def upper_right(self, value: List[Any]) -> None: + self[2], self[3] = (self._ensure_is_number(x) for x in value) + + def getLowerLeft( + self, + ) -> Tuple[decimal.Decimal, decimal.Decimal]: # pragma: no cover + deprecate_with_replacement("getLowerLeft", "lower_left") + return self.lower_left + + def getLowerRight( + self, + ) -> Tuple[decimal.Decimal, decimal.Decimal]: # pragma: no cover + deprecate_with_replacement("getLowerRight", "lower_right") + return self.lower_right + + def getUpperLeft( + self, + ) -> Tuple[decimal.Decimal, decimal.Decimal]: # pragma: no cover + deprecate_with_replacement("getUpperLeft", "upper_left") + return self.upper_left + + def getUpperRight( + self, + ) -> Tuple[decimal.Decimal, decimal.Decimal]: # pragma: no cover + deprecate_with_replacement("getUpperRight", "upper_right") + return self.upper_right + + def setLowerLeft(self, value: Tuple[float, float]) -> None: # pragma: no cover + deprecate_with_replacement("setLowerLeft", "lower_left") + self.lower_left = value # type: ignore + + def setLowerRight(self, value: Tuple[float, float]) -> None: # pragma: no cover + deprecate_with_replacement("setLowerRight", "lower_right") + self[2], self[1] = (self._ensure_is_number(x) for x in value) + + def setUpperLeft(self, value: Tuple[float, float]) -> None: # pragma: no cover + deprecate_with_replacement("setUpperLeft", "upper_left") + self[0], self[3] = (self._ensure_is_number(x) for x in value) + + def setUpperRight(self, value: Tuple[float, float]) -> None: # pragma: no cover + deprecate_with_replacement("setUpperRight", "upper_right") + self[2], self[3] = (self._ensure_is_number(x) for x in value) + + @property + def width(self) -> decimal.Decimal: + return self.right - self.left + + def getWidth(self) -> decimal.Decimal: # pragma: no cover + deprecate_with_replacement("getWidth", "width") + return self.width + + @property + def height(self) -> decimal.Decimal: + return self.top - self.bottom + + def getHeight(self) -> decimal.Decimal: # pragma: no cover + deprecate_with_replacement("getHeight", "height") + return self.height + + @property + def lowerLeft(self) -> Tuple[decimal.Decimal, decimal.Decimal]: # pragma: no cover + deprecate_with_replacement("lowerLeft", "lower_left") + return self.lower_left + + @lowerLeft.setter + def lowerLeft( + self, value: Tuple[decimal.Decimal, decimal.Decimal] + ) -> None: # pragma: no cover + deprecate_with_replacement("lowerLeft", "lower_left") + self.lower_left = value + + @property + def lowerRight(self) -> Tuple[decimal.Decimal, decimal.Decimal]: # pragma: no cover + deprecate_with_replacement("lowerRight", "lower_right") + return self.lower_right + + @lowerRight.setter + def lowerRight( + self, value: Tuple[decimal.Decimal, decimal.Decimal] + ) -> None: # pragma: no cover + deprecate_with_replacement("lowerRight", "lower_right") + self.lower_right = value + + @property + def upperLeft(self) -> Tuple[decimal.Decimal, decimal.Decimal]: # pragma: no cover + deprecate_with_replacement("upperLeft", "upper_left") + return self.upper_left + + @upperLeft.setter + def upperLeft( + self, value: Tuple[decimal.Decimal, decimal.Decimal] + ) -> None: # pragma: no cover + deprecate_with_replacement("upperLeft", "upper_left") + self.upper_left = value + + @property + def upperRight(self) -> Tuple[decimal.Decimal, decimal.Decimal]: # pragma: no cover + deprecate_with_replacement("upperRight", "upper_right") + return self.upper_right + + @upperRight.setter + def upperRight( + self, value: Tuple[decimal.Decimal, decimal.Decimal] + ) -> None: # pragma: no cover + deprecate_with_replacement("upperRight", "upper_right") + self.upper_right = value diff --git a/PyPDF2/generic/_utils.py b/PyPDF2/generic/_utils.py new file mode 100644 index 000000000..1d4b492ec --- /dev/null +++ b/PyPDF2/generic/_utils.py @@ -0,0 +1,172 @@ +import codecs +from typing import Dict, List, Tuple, Union + +from .._codecs import _pdfdoc_encoding +from .._utils import StreamType, b_, logger_warning, read_non_whitespace +from ..errors import STREAM_TRUNCATED_PREMATURELY, PdfStreamError +from ._base import ByteStringObject, TextStringObject + + +def hex_to_rgb(value: str) -> Tuple[float, float, float]: + return tuple(int(value.lstrip("#")[i : i + 2], 16) / 255.0 for i in (0, 2, 4)) # type: ignore + + +def read_hex_string_from_stream( + stream: StreamType, + forced_encoding: Union[None, str, List[str], Dict[int, str]] = None, +) -> Union["TextStringObject", "ByteStringObject"]: + stream.read(1) + txt = "" + x = b"" + while True: + tok = read_non_whitespace(stream) + if not tok: + raise PdfStreamError(STREAM_TRUNCATED_PREMATURELY) + if tok == b">": + break + x += tok + if len(x) == 2: + txt += chr(int(x, base=16)) + x = b"" + if len(x) == 1: + x += b"0" + if len(x) == 2: + txt += chr(int(x, base=16)) + return create_string_object(b_(txt), forced_encoding) + + +def read_string_from_stream( + stream: StreamType, + forced_encoding: Union[None, str, List[str], Dict[int, str]] = None, +) -> Union["TextStringObject", "ByteStringObject"]: + tok = stream.read(1) + parens = 1 + txt = b"" + while True: + tok = stream.read(1) + if not tok: + raise PdfStreamError(STREAM_TRUNCATED_PREMATURELY) + if tok == b"(": + parens += 1 + elif tok == b")": + parens -= 1 + if parens == 0: + break + elif tok == b"\\": + tok = stream.read(1) + escape_dict = { + b"n": b"\n", + b"r": b"\r", + b"t": b"\t", + b"b": b"\b", + b"f": b"\f", + b"c": rb"\c", + b"(": b"(", + b")": b")", + b"/": b"/", + b"\\": b"\\", + b" ": b" ", + b"%": b"%", + b"<": b"<", + b">": b">", + b"[": b"[", + b"]": b"]", + b"#": b"#", + b"_": b"_", + b"&": b"&", + b"$": b"$", + } + try: + tok = escape_dict[tok] + except KeyError: + if tok.isdigit(): + # "The number ddd may consist of one, two, or three + # octal digits; high-order overflow shall be ignored. + # Three octal digits shall be used, with leading zeros + # as needed, if the next character of the string is also + # a digit." (PDF reference 7.3.4.2, p 16) + for _ in range(2): + ntok = stream.read(1) + if ntok.isdigit(): + tok += ntok + else: + stream.seek(-1, 1) # ntok has to be analysed + break + tok = b_(chr(int(tok, base=8))) + elif tok in b"\n\r": + # This case is hit when a backslash followed by a line + # break occurs. If it's a multi-char EOL, consume the + # second character: + tok = stream.read(1) + if tok not in b"\n\r": + stream.seek(-1, 1) + # Then don't add anything to the actual string, since this + # line break was escaped: + tok = b"" + else: + msg = rf"Unexpected escaped string: {tok.decode('utf8')}" + logger_warning(msg, __name__) + txt += tok + return create_string_object(txt, forced_encoding) + + +def create_string_object( + string: Union[str, bytes], + forced_encoding: Union[None, str, List[str], Dict[int, str]] = None, +) -> Union[TextStringObject, ByteStringObject]: + """ + Create a ByteStringObject or a TextStringObject from a string to represent the string. + + :param Union[str, bytes] string: A string + + :raises TypeError: If string is not of type str or bytes. + """ + if isinstance(string, str): + return TextStringObject(string) + elif isinstance(string, bytes): + if isinstance(forced_encoding, (list, dict)): + out = "" + for x in string: + try: + out += forced_encoding[x] + except Exception: + out += bytes((x,)).decode("charmap") + return TextStringObject(out) + elif isinstance(forced_encoding, str): + if forced_encoding == "bytes": + return ByteStringObject(string) + return TextStringObject(string.decode(forced_encoding)) + else: + try: + if string.startswith(codecs.BOM_UTF16_BE): + retval = TextStringObject(string.decode("utf-16")) + retval.autodetect_utf16 = True + return retval + else: + # This is probably a big performance hit here, but we need to + # convert string objects into the text/unicode-aware version if + # possible... and the only way to check if that's possible is + # to try. Some strings are strings, some are just byte arrays. + retval = TextStringObject(decode_pdfdocencoding(string)) + retval.autodetect_pdfdocencoding = True + return retval + except UnicodeDecodeError: + return ByteStringObject(string) + else: + raise TypeError("create_string_object should have str or unicode arg") + + +def decode_pdfdocencoding(byte_array: bytes) -> str: + retval = "" + for b in byte_array: + c = _pdfdoc_encoding[b] + if c == "\u0000": + raise UnicodeDecodeError( + "pdfdocencoding", + bytearray(b), + -1, + -1, + "does not exist in translation table", + ) + retval += c + return retval diff --git a/PyPDF2/types.py b/PyPDF2/types.py index 0aad5fd6a..9683c1edd 100644 --- a/PyPDF2/types.py +++ b/PyPDF2/types.py @@ -14,14 +14,9 @@ except ImportError: from typing_extensions import TypeAlias -from .generic import ( - ArrayObject, - Destination, - NameObject, - NullObject, - NumberObject, - OutlineItem, -) +from .generic._base import NameObject, NullObject, NumberObject +from .generic._data_structures import ArrayObject, Destination +from .generic._outline import OutlineItem BorderArrayType: TypeAlias = List[Union[NameObject, NumberObject, ArrayObject]] OutlineItemType: TypeAlias = Union[OutlineItem, Destination] diff --git a/docs/user/error-hierarchy.png b/docs/user/error-hierarchy.png new file mode 100644 index 0000000000000000000000000000000000000000..2b0b250e30db71568661daa762135f6df22283df GIT binary patch literal 50377 zcmb5V1yCMA)GYep?(P}_B)Ge~OMu|+9^5r}(BLk?U4pv=cXtWy?z|FzVL2~(7pK!V4I2Z2CHQj(&|AQ1Qy@S_L|4qVCNB%Ah)IRv;*gr=EUGPig0AGD^%qEdR=HDA76scgK;08 zmd+YXig(B(AjhjOKRy^^u5_aZ!W(=l(CZxwO(9=J2$V|e`ia4k+I{mCJ3;{}<1;i;CN0`~DX=SuV8 z06fs+QTQd(}8}(oWCq#^=#~y)@^3_v2Sb$`A%u zFF3l-HM>36%R$*?ZP9(JPYs?O2ehubKUCVb4Nrf);ppwU!o2Mcr?X&@7wV6B8TB=` z(nc6ie|sGKO4*yCzBYw)zC8cwB~ij4PfqM1k&5iYVTpqP!@6nk+oCRQJTM zW@+9JSlEo!m$ao74UaCSftWm}byxH)rwvazbMl-Q`SA)|H+9qNwYNiuNtGRcQaw#Z z*JTe{FEiQJ*a}*8y5Z#G9L4*+oQzwkAu#LI$E=;1JWS-dYDcFr2U-5)uO$CXU{c2Y zeRa@yxAO95h2-yO{-cEZ)ms8r-pq@y@mNfArTY_=l?*S3U4JIMs;o`+6KCO*<$XgU z$=hlDv=uL9p4H!-Rq3IDzEy7!LRf|-s^Buk&s7P|72ml9|EczPhL7hh<(r5&vSsZH zs2^#{xvR~V$6R6&o>sKep5s(jO`s}rSU&r&5J#SH_z6Gk#^IUt6E!1*2C}HHR?8bs z4_Zv$m>s1l_$3z0Y5Itgq@`7V=Ng3N2)C7GY#G6nflcEuteC!W$drIB@72NSZI&1> zUD8=uT)B`%@tm3SCxWLh+y(J#QjU7Y5RaD!bKzPRSIiyX9cS3FcAX=6*N9AHJj*?d z1qN(^H3(SQ)E?tD5YD?*IrbNw&KP@&!qC~1uuX&=W*Qt6G{;SIA=;t-Nl45}Ib+Im za8FN%X0w_fq&W7bR;=JZcHj!~pjLV;L3Y%L%ln}dwKi8T^tdQbF!S}R=}DDGbQF(Z znnL)Klw9S<@2Q>$d9zKc$AyvNqa_~9FDHg^+#b47F9%U?w_k9iUCJiOU)X5tMc69D zdt7sMiGpDaBeBFfplG-F~@<4$mds!&s6DH+(H^M$$g zUW`(4U*$V7@p1hqfLmFq-Eu9zQ-v$kLys)!q-w2@msG}q_Lbl*-ttVoxG!04n z6N#QIy)`ZU*5Fv}e^eR&HNbuPQw#pRGpUf$u_(^TDhkdpSHi_A0bI;xYhAnynqlQp&aye(%)MCMqL{}HbBpRe)pJ=3_EiO6dsQ*vHw{BJ&JCyT>4>H{VL?+ zExW>CWDlC3JXc-}* zY(fyT9!*SWQ7IM0rC)RFtj$gr{51ST91*oj_ma(DFv}fe#QX*ouxVarIDVr0+L8tj z4wgZMqLE5U-Jtd#$PIJK=GG<5NH8r5GQDUf)YC=>Mtt82S0_tb7#2UPYW_eD5XK}z zH|5xnMK8-;N1M;SjY1`XUF& zRR;3Phr)z^0a=uZ2V)%BK0@}8xzGxS4(Fld8_>|varRaCYfJgDohX66;)YI+NWU~- z-lab{D_q@eOT}Pt>%%O7Vr#&{_*3!HG5i+~{ngR=%OP5XPNVcwS`N>R4eTeaIh(-h>KeCQO z7I4KjM;MgBW&7inXD_=svKk3OLWN#r`!kmdDd_HN(O*5c5d1)_32PTrIgf`CRaB;M zq6uw!lnd%ahpP6j$U+|6)@i{Vg(ym))esvCH@{p#vqPF!VDuGrgV-<+bc{I$7xuU8 zj+d52{TMs85ler)yyaOMTxk;L7)0DU4igL)nPBk^7mA;hm>2U!nkid0dmdui_>~P} zAM)xddh+00_$jc5hT`lC5>HCaBvr4#TwDm$IB3=X^prZAlJz1q0a3?QmGIEzcOXr} z^FfYmXh3493EcHfz~`2T_5Y zdXeVi;^#>#Y-zi~Y9rW&8abiwdo8Nh(Oez&$t@uKgMFzfcS<{X%jIIxvVS$c%@_Mr z{Z9|c4b7;Cbi?`6nZ$7n*w{JFOln2yOEKX>AbK}`Nn7J}mIlh5S49)A!nDfD-9C|S zqgFJxk&U%;x6hq{{Su|5<(gEzA`U|S3ZgKYOb>#ewKJ+<1ITkD_94IjNE0OCl7=!s zpg!+9$leQtMtU-hKFY2-7|<+=5k33ReS-KqRI)*Iu(4^=jTg-j90lfcGw(})GGA|P zAygjQ-{7UYu&iY@;izAR`*X4*xSPe2tUD7U+^(HmI{B0j5Pt%d&u55XkK)e@h~$vl zi2chb9rKq)GvX%^k(H4u8-jkG6Kq)~Mr~Rz=%bCHk&uX5!eMAeAaX}EIUwK(+fr@A zpjN_$#+nQN$$_xXZ|y?`Z6@q?K}!5hHH5*(D!x!o|ARXfNP7aUA!g0`^@Fs1s1sOR z^*$5fP|&twdf$bgm4O)FAjH~Q6j(35JxQ?f%)ybPMY$k34)BHu%e>#CzO+ zONgX4u#^liL6dCNxbB3nw8`ymFH47b4hw|AIQyoS?U%;+kOq-07eOFKEmFccT-8e% zNk0kQ380yTJ1GNlq4IF#n5$zvw-|1BDgLk@`q4wPs-Q=)Ki;7=o%XL8i&y zvb)$_p?8WLAB7BrQ8lX|T0B9|7cXu>626?7MF(k$(28gH$HCbs*+{tNeGxsXFt8|H zDIZuHwbp*yxFzw$Dc09I1n4Vj7T)1teO7#2qmM}S`w{rPRj{3&!~S!JcB&~Q?aek% z!c{#Ue6UWSMaQc>H$}!8#E3cRlsE zgH83HJE-`)oczbVU03J?Y1~`qtW5@lmdU*=OJNYR3DBd2J=paOLlMNS8A*1w9$WrX zZrxWpupZ+?RYxGL`E)d&=3$I^CBfceY24(G_NC*{r=Sv}KV}AmL`yTCE9&rtTVHzq zQer&YA(0Fa4`sE0k;ie zE5y-*IbJ+PLeUt5IJ{~Mb}agqwLvy}%A9M1aFI#HsdLSEL;h6QD#&<8TDBh5<+xs~ z(YShH>i}y`KAFXwd|q@e2wm0dI>`_uBUU-JDD`4akfQ4)MibI}FgvLJz4`>})^c>;Q45{-uBoQn02|Di%^?YQtwkf?*h zDE|jlo>Jp=Q z!{&x97$wA3p3~ZKEcd7LPU%<3Eb&x5$b>{r=lL+;>?XW@m)T4bkJ zu$|;x*c=XuIKuO-WrXIbp>uC{b5A;MQBNKObq=N}9jvNSjdrXl;d&-zx22r5i7ZIe ze+`B`J9KMcG+22OqpU8c!UZREr`gK*6?0Oe+PY6GGinX)U;4q5bFERpp+h9%F)05z z+32`8#wK=0EZBurP9jYV4xuMPNKjoN<-q$kf|CnN5x1|Yl2%to zrF5to%)~DHQT;LgD-t2S@LFzGjTAOM#O!7`xMjESxARc5ofS5T<;udvSMA8emu4=N zgRYRlwhY6yu_m*XFxVw6UiE<OABVAvT`iBR>DFflZ>77la$N{SWb#x zN(j|&D`5W;`p?vifL`rM9byo=`l)_!iClXoLZ4D^y`Otp}7{RQ^&3JYE+mMpExx_70qy@y$jwHm)B?;t>L#jkhw z(hrGOOafycW!jGmnXDCua_|v08%{}{mv*RRd8M@rth^)`MVqAE=C-!0eLpW)+9jsq zGl%dbPYFBdJkPf0h%?WI$NvjQu(2-xQS zOq6T=na5ChHnl|bxXcT-256dA@yUt!-pgnaRv!r9KI!iFX{Cr$EYxqDlENTc9r~Si z*s}W#Ex0hNqfcZYZ=@^otHm49{~27vBWWdT_7%o7E-C}f!uhPm>PKr5W~s@LAi;l{ z0z-dIleQ;$s5SjJ03jT8~bw6}&P1H7_*9jwZe2Lza3}gk!8w*%=Enr#QW% zQ7xM*4~u{y`r;=xns<_J)erte49A_p%Rk;uE(*sc;;V=ghlS;zlB}drP$-?iMJc!4 zZL#t@FrPiOyyX|ZVulHqTkMd2OQ(g$FWkm0Ik2VmtEt;}tQ}I>{vrOhP`cztjf)qD;@h zD=FrL4T5(j+iRkm^?7IZ-&;INLugOlPNjoI=G=L5FdcG}umMrGznRtg-efl}H-Aky z!iTLESUcDeLIy;T>m zwa(zYUY&3YU!!ttkPC!eG%777cT!y8U%rY# zMgpj_P;y8$?j7jv_?Wy!{*okAt2THMn$NT`T19w!QTuC+dC$X7Z;slbpR5_>riHtW ze|_D+aEodTcQl6@Ess4(7S(4}_2+9RnM}P&d?HNySd|d6 zPs5%aPFzvNPPJP!=%XC^8UME!sML7gCq)ZQIaN-La?4}Gk$z`v6^)shO{G;NApmzK2GPHiJiU;i@bd=a z;i^=F;W@a{u{(cxRzHI!u@75goD`|5(Al@T%`Vlwp429ujt0pnyz?B3T~yhk z8k{%z<}^r9J9W?7DvJV>f57L}o=2Ux*)grg1n;WMxkK7fbGhir9Q7yrZWY$P5C&PC z8a`AqR1ez+Hg0ZZ6^{-0T&-bNoR4+aEtx`otY3~^n64IYBx3I|*P{&>c9$b9DD)kd zc8v~{EcrhOLDN4~-1)Z7Ez6B`49`ZRrDlcP7qb1OkyqZWKifuJmV0290o(JSOYLcz zP+#g)j?ju&+?PEeY8w6+XJ|Wr$*QOYf6GSAfP%i&Zm1^DJa={9|AW$IoV&sEnJiDl zn2+C-X~S(~xTJO5t!$$HI28R{(=(=}4D9aqGKB%gW9@rh7jLIaJMyj#b)~;~{mZam zrosLK3%r?b;658S9AX)VB+lglpA<8aO}>MbrP?F)u{ZrzBB~zROLl!t5#E~h`NtR( z)O-`+lxk`duq#>%YXRb0Bh5e?CJ=nOhLkuVcwaRAFhfDS zK9jBBtp0GJuxoM4(s7ELm)>9)Jg<$B(Li-e8qI`mOo?F$36hdrwSS^6E81d>?h{g$ zfnrfA$9!uC_RwTB!FDqfl6+hj4`@=Hj>4Q_QPtm|A9bYYj|Th-^Ta0z+^?SI0;&{V zIL-HFf`=mCTp<{1kX!qt*tlLDHp>mL6|1$Q+)=$)@ch^vMc6s~7EyI3k;&YBfVDSmF7_rie^P zyIKtGmRL2_K6h#&IUii0bB)+z0~qR`l^7{jte;3j7AftTn3>Zx(G3Ymg*1v@VRoVnKdhWq943%Y%IQ2qtsXj!@@u(<^dD%~AjJiyx$M9vn;>$A+Y!&*))HYu(!cm4_5KC|2HFhS{@73rt!hZ;Y3+?0H$sZJ(UI*Jm$95}<9ANR9#grGU7g1K2 zwVI8~nU3jGo6yncqqOGP5@{;0Ph zN!v!X`F@FWG3togCg)M3+;7*y{$61+h`K>k<@vG?`pIAj(%>I_^IL0JE7(84lY}JV zjg+La;o-%q4fZF*&bEq`(p!{`AeFqzn8=d;N~Hy3BP{P6t4XxUNgA* zeYP#Xq?bk2*wbd1AbQT*W>MzC238=d)7;|88`Vz44M{NG^LFvWEn;^&l1OcfE9p2j zZeUa3#+Am5u8V-qTZ%ACzAM|cIg@CWT|_=7TM^&1f%w<02_yW{;ZtP00GCYj4-*z= zMImAO_4AJokLY0v{`aU5L35uF6;QQ3+^%sP+`7-b@kcy~ZNZ-Tkvn7pA3}t9Mec3T zGtrXQ#SKpLXlhipvVYQLOw|dtG+;CO>FhB74KuFNzvZv=pnqY5fO~iweJhIX)E#{$ zIZ*0EHfEYt1cAVUEJQ>Ur9?#j>lhC>yi50v<&*3YB<$1Esu$BFJSOcU6LGE~rJYhL zRSbg9Q8SZ7Hu|wbcSe@jQyJGMeciIQy9*O&;!{I~>(Z3>NL0?0bH%thX8_%Q>Om-TB20eOGPk_zM@g z^rW$$0%@i}vjJKqhJu|Y_n!9_b*Uu$1=mHg5Dul!PV@04!Cp;|PbMCl<097wHU z96`eJLa1vFbWB3VW#w~lYd*u`E)@T9iFXrF>a6ad5c12g-8;iumn%i}L_MmL1( z%I3EEJBLTmPEqLylD$1_WFn2{==N&P>Dnvb^ywLlub^XD-KnJ~aJmX-FZuNw2!w#~ z{sRU|O~(Z;!Z=CEiNWl`B0&?-Hcoe}f8p=MsYERfQXisyZLXu;{guh4lMHL`5tA0Kb-&QXT$0 zsnTKvp8q(O{tiqWntXwZhK9zpO-4>mZumY>wD*02&`>-S;5r%$F>sySloz-zI^y>K zb^ZTsqW^V07hF0E>H7U{I(!ygy_PbqnTAGXYaGT(BzPP^#k&!hI z1sVq`+Mwz)nrfP9`DgkMGr2_ti!Tn zF+ud;Yf~L-AGqnbtSqNLn^`tpWVcij7S_^hSPbfjFN*Y>$v!`-+T{2VisGA{lkC~ zx(W=Y3Pk6sBIVB9ROYA>Ik`S+QH`e@lgO&&0lnzg_8pVXqt@3x*MWCvD7{ieqkRDB zn})5%Pt+r{wr!q7okCP^=u0P}2G6eNnc+Ac1Z)mxd!`h86-r}f;>T6-$aQpYZ0hWS z3REX!A636q2>#6>UKU6kdMlw|=jLfN(Y;sxVQ|p7F_~GVo6kBmm|jj_9pU4tkvu~J zbI2E{X9sk!G{zc)p=E;=IKnK5Xl2F*;U@v*#5c(H_{m)2!=^k{B~=@Ly@rAYd6TAv z+A>Zk)b&|%@}Y6?+s#a3#hflr^JS=5ok>@SYkb7zh;jU1BgV*Qu?w2mdo4yT(R&&9 zDoJyM;vm2M>HP)x9J=aPMD0;~R|am+O;z%QgrqT@WkYj{6>a~MLZu>fXry3lMw-W5 z0!yNXr)*uVMoWFzfkQ0Lb~OHwY{&kUiu-ZGA0N5RE+BmbZ|1Iu?+I|?Ml)1o<<{RxVrWO&^`SP)h^S$J#-`t!qd)|VO ztI7h;T637iqb<%?BSvFGVHG$EU#S8RaTyVV^?WQqx_1`E2vFT1pOm?Ik8S$b0GcWpaU6)68>B7=*=%o9i!iH zWd5y`!P5&iP$D~>&k(%Y^uudJpw-8bjKC!-P&RE~WsA`=qP1?gr;Q?OLBZc{OGx%$ z^}%DhlV|JCt>2qFa=$wqst^4Z?W_K?_KghUM_Ip)jb;D0S5)#TGjU6nWZKZ;P_(RH z<1#Z6WOZ^J zLr4ggV<6eozC1$HWSURdk`eU=XJbR{b88TjEyxeDOd}%tSEu~XjDmwIN4721B4rAl zifoNe-lzH0)7l9Rxu>{Igv(qO9NP7!KxJffvjz8f9b{@e+)2*cO4zs;d) z>qEJrI`kV8hdrwQvFIi97cy0g$fc;-bfJ^Tt`i4yEbNi)7y$$1UP3=Ra_qX1wJAgc zP9G%2t|exehFnhGqYiOwL&46k`(Wkl+rlF{o7r7leBgn8j%5r9ng{QE?<%)UW+>dD3;FbwL#h9fSwnqdEa*~Nl`A^04#K`@Zj&$XcfoSIT`ig~aZVT-8So!x1 zt=WjzGiBrLpqY+~bbFWG!>p~=uKudT41fdiRp^cm*5KRU3*Pq2LxR}Cae~1`A-Cio znbNSJ!okIm%*@mv-xcDzB5Sz?MO+GNjIKMbfKwSyr^fd|JxTlnfuQ}4-HJ~`n<%Np z+lS(}(ZoL|_uKoW*Hz{VxZ?0mU<+5I-q1H7G;kX>FOAe2zjtZ)D-sq4iaB?r_@w0( z%0Fw+7**Myr|X)aY~pM2>pmZ8rZevZw}IAKR7i(17eq(uHk{4ltQug?Y5;wA0M1T z!=bJl9Y5$K5q)uxeZewe<>ba<$!!1h20OjmIfT)1E4jAw8Ak>R#g{)blsf6>C-t#E zfA&rG=5&(WJ0#b|bH6~MihVfM`8%-w*ZMf39q$7~Mz2LoZ+rh#?wI7=P_8m0Dz%}O z*4%O8qoz`1{$&WW!O!%|_zUlXWGeUX(kCXDtczHbufrxQdt0iKAh6y);wvRm^$8u) z@`gEB)LTqqPD)!WVoP5zp}@tgEr<4@92FSI(Kjf1mcmlV{fcaQJ?jWtlPcWy5+!YT zCKtlzlbB5coXRLhp$D6ac1Fi7s~2;Wi)p`<^h!S=D~c{mNr)zt4ka;=;N#b{_8#2F^ojO?0R<@819zb%>reTisa zEGV6|G&9?iNusY_+jS+34hh{Sx-tIPfYsNKYPTV+)bxQ?;H40&^))G}VH(q!Pbujs zP1o<`9pO^YWnV+3b(CT|$^{b=nlR8NdE)=3zY3)P5ILcmc#G1rHE>W^TR?xeJRZL( z3A+bKC=_8prdUizcC4&Ms{homO)C;#zssqNFFKcPvJ{3;78m`W2|6QYOGOf9>G!9` z@amNLZ%_2UJkI~kMEzgP)&H2tPDERqFi`NlcK4&)@6R{ey`L^Mi`i|KcDA>B+C-Y9 zdQgEPY7Yv6#KB2Q*J=+Hm5>ndxTdD1Epa-S`t|FVyXBRUp`q~t_Op-4fI_x5t;>(SQk zdbYOmGa+x`TS_RkUU+0=I*+Suxn_NddL=*Lh#MOlb=y3MIPEts{|+M&u&>mbMd#&h zpKtbDSw9RX(jn*r%b=p7@_xL2II8SuRG=}tvVJ$4Buu!7Pj_c}b>@?gH%IYn7q{o< z`_qLAfVHNhqXXN&dF%>=fk z--KE!*ZQ6K`D=~&WNvmgDM&0F6OU0}FpJ-NWo2b*tO1A;FtzV_7tG00{qS&Fe8R;_ zqeGrZ2=a1+jn!<)m$%oK8;=)1Fo?#=uy;cxw6nLz;dy&(v)lltSgcCoeXRsc#>>YS z6&cAqf(LkX(-GV#aTcTgNZ!})O3M8h=;+|tQt?#FzwO@k^P-`V3C$UC+wZp!PMtiq z7l4wXu8aU)MB264^^6!Dy~g=yPCkR1FYOeBE8z3o6M_PRLY$r#_|l3d_2LHPHTHU3 zJPO}ty?u3M#cLv4h{bGdNB-lxw}u)UAo1mOzcdC({2+9fpHIKq8wWDDJT?a0=HTv| zm&rft!Chd`^PR`#xT>lOm)mLo`e3?g{ojlEvv^$Z0~v#YhITn#VB+V0UBnYL-k-?v zXnO_bt!ro)i^fCo15yGYV|r@pvb3`O@n&AHY5f3c3pNPw#3I7N!T^kJ&(_!5Jnsr* zl1%r1)f)6bGcZ(Y)>{(sxX2a^k+{~*&CT`q_urka4l};oUu@H9*128pPbvt!usZHY zo_cstECA}2Ab^X9_we}0&BaBh*Uo!DHm_1$T`eVYHj>W8(vi;t8cLwSz{bWVAV@Rt z$l!K%IaxBXvEj3vDI)QGj%~?&uTZSdU0q#DN=g98j0S$_)S8Y?AE*?{Bf-HPOy)_n zdE8u1?ovAIw!2^L){+3JwwkZd2}ZynW-%Sfmx^yZ`j329TTK4~0v;S3?C#-V$(jm; z=;7%J0u~PL;bMSfbad4B@u2XzUM$7sulvyIs`u$i6R7_O5syJPq#Ed-5dXl41?F(x zpCIGs_Zds)dVfx%y@8jPSBuL@gb-lg4KoyLqag0w*IRelH;14;GClL+IEtkqVI5-F= z7z~f~9hoTgjGb+?6fe@v&VZfv~sG0W?6T;MH% zPRj)-Pe9^)9w&p~Fe-FfcDJ`7HtyDaIoR39T(y!6P`kGWqTkMYP_!DX!8UewSOMe^ z^LZF^PmsTNqPeaEH%If!br#<)w!~0SqUF-qvFJ2ePYP*&S^_msMn=ZY&JGa8P&C1P zUDV!0j)=hXc@My5V7LSX0|NsLx~+{4+x=w_*U%2Uz;vyD*4H}#F1tHhf7^*y0IFHJ zNHIs&ti|ihys=v@1qg;-U&_eSdFH# z0Z9*!j()pcw)-fbo*eysVUQjMCL|^%=5V$&nuw>t`{|bU=a4?5YO(U)t-gqXXp%~u z7WPjNZ8To$5uwy5Xt6*gyL);PAb$TKlPH_YdO#%a3UWGLaJbqXEtJnVT&OmhN-R+= zvFmuj28C?qa75SumFX_#<@Icq<=x=*;7C9~pr^M^C6|gh!nuFr0muQ6Gbl(%)l#)6 ztiUzchIh9GheFJIzY&O%$e^d2BODAQ7JI=7#!s^x04L(Yh=?8L^1Fz(e^Rfgvt8w8 zWUK<#)!yC?1Qv>yG%PG^Y-|jO^WJ!ttFyBSQqKIqJJJ8PUT^olIh;j6L_A)s1^ARF zJW3fem`Mtqgha31>jB^~;EyKZ zV0c8>Hmdj4wh=)G8UQ5>6#bkzEATc%d>*XR8Fi6SQ4&%3CXBehWK+7$CytB=fHY{K zVqi=I4+IopVR`v&{p}?yBm~CjvaGR@^W(=tfR=y>xVX8QNw2y85yA55S{s0+Y2b#& zrY3+zA`j%%WMyT=BXM))kAUL)C?2Sb@g5Gx*`{xMO91S)`=b)+v>xy8HNJjjq8dlT zz}Vi{K*htu!^PETcCvsD(#88Xe!RhE8K2eM9vBY>0d2il6MQg+kdm5;Ov2C0#B`CQ z=VQRS?_3oHaCFj0CZFe@7&3BZK(EL|zE@>k%6MGvgarnQI5}MdaF4)Y81tJbH2~gG z>BscZ4WJ-ItmcY9@&FUS>a?%;i-%#nyL)Sxp(8RnI*Z$xHczNB@*NypM}zSn*Vfj2 ze0%^*t<;#v04c(YKL!-_Gf-fLhKBa`Rc&p25WB9vhsO(j3VTk8XB^E zx;>F~T_g4Me0qI(0xXdET-n!1=>Hs=92yD>$OYg|IJvnWA0C<-8yTtW=1!J>XQZc( zjEn>X2GV{0jEm~wZvE~HS->G6j%G`Ln~i4zx&IBkZC;&u+4wu@~7WiTmVh_W^TSeRUm_hzgO~s zysio$y8u7v0v7qK2MkxM#WdBchU*%u%drLsu)td?2=7v! zz(>vuSX+QL00%Rc%4W4Yl7b)g-C!^cppl8PueE-Det;4);wNC;z6V*da2kn-+X-02 z?(Qx>KR+OPhO(E908z5rt+fK41&E9aW-u7pyM1Fg2iy?A$#(1QZ9q=|lzy>hJ*`pP zU6>?5!2l<<0_p^;8(_xI{J?&hG!{DgRJKqc5E8&pJOMY$x?Te6mrHrGo5JZpezx;& zU?#5!d0a+%dbXb*t^l931h99tVIOY1SN=Or4`;X>cm4nn!$tb{eoX)}*=&}s0sjv? zQ?tdT9Hpw+VxYuol3j9 z-T_vFgxje|C=ePEwxAEt9oh+WbaaoqQ$D~MTUi|eT?_N-;5x@e_~okWy8kF|g2jzK z05U9%&CN}K-&Z<(eF05>etxD^EBlkY;|d*^1NcWk%LIUI0Pg^le>i}4RYAZLl-sU0 z1I>cnOwlBOBj<$;8COd%La0?f)JERP9FlE+Dyp?I7_w?*jNZ`4kgUTwEMr0L)k#m0yhb@6Vp( zu>+6>_`5;CxB=l3u$uc^t~Q*EiR$3sATKX(U|^tLsq5_MXebEJ`98ko7^rgV)#jV{ zj|x6dC%*w9#!rFq8R7+n0=KtdPm;U~eD#;|DT4JgrhLB!sg5 zzwtBx8C{;A!@|OzUtjaS&rDPUv^nh78i4(pDOSOY3V2uC>(hlg3zZ~#9Y9*Y1Gd8X zP_`D@zDGyVd>bGacQ0xz>EH7(#y#GXOsZt%kDDprD|I zr6p!kDAf1Sz7dg;?_mZ;4<*uh?)*Xf#gjk-G&KzE_vC;A0>Wd)3jgl1l=xkMu6C|m zORw3f7yvy1hwW$0I%*@~uW$$m-|X!H8wac#=&gV__W*Gos{BF7wFOWgVD}aHAB=%U zP}X(-;rQePi0~C)Y{ygCA||Q*-e2%x3ZOE8X|YI1-hh&nOJ)=**m@yoTL4OxkcepX zzjZeTJa`Y5e8^06Cxpc3{Pva|7ZCvC3&0G?rxP-?x~*=2T=f9LJCf34j>-C7e$SNr z{OtgxeNro{s;+)`I_m&dK9a210BmC<0^4XqL$pA!M@AySXgJ-BfG)B>5C&z<_w5Di zlbULg;^zKj9F6S1F9@LI~y>n zOr%hd@6je%0BCBp*%<-?!t>^^{@b4XFP>}e|I}WfAq7kTGd|S2C{b5x)tM^+y}vW} zDzJG1$W{S0ypeF_0njJV)6)aMy$7TNFt~Uz0q>?P!4(hjyHcL`&B3%p-eeg-On^`V z>yEPjC%#vJssMN!GRH)Hj|nd=71b{QYy|Aq)S7kszvR+*d3kX$JOb&p8x@-zO@Nl9 zRK3!&UK4sJ zC>I?Sm3R*|YFmC^1dcjz;^sijNw3?w0CcO`8cGkBf0G#W#{4k;1_5yW@PfiZoSXFJ zx2=vTFI0Fzfl0-ea==D0Z-{brt**Z8494q^n*iscw6OoNVfQ~zN&m-2-v4&2`v2p& z^}iqBk#-F?A+Tp8;`517NqLlw5qc3>l^%wUY%?RW==;y`eYvc{Zii#Stq;!Oxq=1$ z#cV^I-DB5c?jn>fzzX*#2Wd2+9K(_V5U@4}%VfOmaFp-k zqK#mhJgl}s%2cpiQ9b_DS~z?m+ocVRqg#}xzYz|%Fk`G8p!k@6l9Eb@S>x9PRVKUMD~*L~ z;qXNInjR+d8^q#OizUG3V@gkvOufK{w9S!cukw6LWQpH#i2sF=DfpF^3UQzdMp3nj zF66L++BCQk77}V>1%gvmNJRmWF!YeUGU1d^B5ln4B&58eu^Dyr$w(T9)%tcf;5J?4 zfi}^mONivS2|l6(l+7Lx7SP7R=t6-GWs%=Tg^fBh6*a4%Yz=Hta zHu%=sB1M}T`ZKSHl-6_c+KEl6rmh9|Q?7ZRRW;?Vmmf3T`9XDVaHj@#kiZjE${9`h z?%u_V53rF1($&@F`W+6K3GjV@H*VXpTg1XkA!!eqXiSuYp4T*I!i!FfhbC8|;&s)0 z$WyzSuN<2Plai#|DJI$DNOh3aJ4g?D{xT>hZSy8739IHb+}37c@G9Gr$Y)){8>dvkv-EY- z;gvHE;N-t65U_Go653DE204Q08@1pUUq3>QyFfFFZXG)dBIL1#l!x&h%Q0&O;4OKB z*z`j?e~2{2t`+NMeVE{z>lOPu%;tXfGpw+b;Hs zQnb834snQE(2=8L58JPB7!xx&yD+#zTc<_xMQ450m?J2ljZ`X~>;VhyS^-okJE#L}%X%Xi+~+2$c*;%NA^Y{ttnQ=WK=m@vCcUHOnG?eD?zrwh#z1Rb>Yr_U;% ziB<5}Hu83K!Mu<@T3o)VPQ_Zw@L`@2f__{iD|n(U%tTi+^i`&26}`A-;I7Bu7m$?r zw-Irvf1h8dGHAtr%eOTXaCIn6o1*@iH&C;Av#wy$FOtx*!;}jN8Y#<`OTi_Hq~TL) z;$6|+*a+wTJ>aH>1;wr2av(A@}lNTumm|;Z!<9Vzkh7a=bTeV^{d2BhXzsH>*2V0n%&Mr)he{AMtOS_+j63* z2n0w+)9D#>57#;J`JWlr6@_@5Qh&kxqP3A^Lqk{Tpus^n3bU1n{HPy!2yJH0fzfCU^*AJRRI#00n{e|IOigD{+*^h- z>;^~AYvSbP4hm))O7Gg%zxhV78dLg<*t#C-a~&8j*#GH!7~K!WCWWfG=?6{e)Z3QE z$4#HfB_zV3!GG52P)e<0q8zkjWQr)T-&b(dqGVqV*Q5#3gll7$kIFv76!y_*$`lgup zObbB}4xj6g_~uO-#Vsjf##H;$MbDUdbw$3P2pz5I##eb%b7?dIwk=e01G?Cj-jXNf zn1THAyy}_zD?T=wU^>qb%xr-Oj{`lq3$Y{hsm^{=IxZ<&-GL{4gk)=^6PBRrT+RNE z&_9zGh6Rap-#+MyUe|n!!KE=g`{0^+DwFZ~MKn^@HOxU@4Nr(AA5h#@f`lRuUh9? zGsc{2jLB%ihWGueRPKd&y0#0NZBi8ZmaI7MLs@nbJF?TcC%32w{QdWi$*wQImj*H6 zM>aZ(p3&Qug@v)%F;ZzrGv>CjW(h^23=0oTzqQf6))8f2naJk7EO+eBwpDM;xS+U4 zOGw6qS{j*~WED@ZF1@t!iA!krtxl=*7K>PD_k!`)cWj)e+1~GdI!`NJ`94OL=8A3b zFlr_-O@f@a$H?G3(G$fBl>W||I{*$w~9o<)`yYxg_4)Y>4&$Y?95nQ zH4PWXAKF~gKkwXk!|1EkA&8~W@0wF5*0{5h{DLj(>~Vyy{I>!^dfM=sr|FYLXt2Fo zI|JvksmBIl0tzA9%2(HAw}kbdoSI3FW9AuMIlj?JPD;IwFp^%f=#=1k(p8aqA{Vi~ zx=WZUsv1#*M!s<(UOX>9&6hyCw#21ubf)Sz?UH&2Pwjz~XzbPZc15J>GS*)6c;o8R zPr6aI1L3@(I|W%?()}qC>kky~IxsL!;oOP2s{VB*WjhnmqbGbpBf!)AfRJO^F8uu1 z#%M7j@!l$WgeK(!C)#R1bp!h8Y3F;ZkxzRgNzF$1ELn{j+xzdtXv;dpws^;1Urn)% zoP}qYJNLw=OP+=$(niRN8D(xCCY8P0dRdT>L45S+7~@N-g4w&U9tHlwW`U(Y;Y|3C znRWdx4ZWYRjzsnPH|rFmu@sA_nPs)I7bQ|Z$oJVQt1qmS#M6${--Aw%*B@M3z+#{8unQU6JfcrKSI@wC#Hq86|K?nC5UV5z50{dAI)!`wb(R9 zFhcVah7rlhORh{bxyOY!aS6O!{u}nKnw;V;3Q!WSO44w;uc?;t^TTjNc1mvu6*Gpv!yR@=MzwJ>g1ckk*>ECif-N zn@=PbL(buf%*{>O*@@H|^C=>HMrCvKG>`j_aS3pxHMoR!VkVit>J93v{<%ABB5 zh8B@2;O!6BUQ82Iyp*372~KQSdVF6`Q!mQ%8i<(wVHo8{4*2uFdY63DT_&2> z{8fTuc*NAN9cF^t+L#q2CgzdHn4LpJ$&T&EA_8aY(>?>&vIGy_7Q| zv1@$lN75hYg&%jkb2LSfh_oFu(Lm-?%0J?$Rx=5cwHTm7z4qT7LYh(cJPUf=a|d?- zML1nJ_2R_*{82jlOKlQ$mPaU>70<^dTqhfu3pmoSs2_Fp8?I~P+M?(71|M4Mqh={f zrQ9jScaH}(9K3{BtoYaexwCeTuW`{l*tYr*Z8z- zuf@7z?#Ef)2=in((WP`vRf*}`5nm4Eny!63(%o|dHMB#!<1jJBU#LNo^j!ePoLu{l zS`5z>xYZZ>JbU+2lz{lvbC)O^jETk~E&kQrann`Xu42^!*+$kzGvq(^6*A0GHdod} z(ciYrO=-SfME{{v+fL=eQQ7YCy_<9xVti)aX3NJ!8b(-TSoPa#zpF1&hnJ#MKnm0P zkh57YkZ)ZK+ZVe~A#gieDfA%tiBthnNmK>{PZm{7?b~dP9QzPc7wkMOsj|5+4Zj;# z7^|bIrP(xte%U)H=-U}TbaDQxGo2^+5|aB&Dxp@s&@v~6Y<9eot653;Jr^6lF%hk|oFE)S9;BZ@F|ibkTZ49d8Yt|MFDbD3dX z;lBLA<m;_F{X5Dga|NX7 zukGDaCuPFpmmKE){@1lPMsqZEb>3yo=aPILr`;sJ2>PM7UF)u_`N%e_B%T7!R{anD z;~pux4Hs2Xo|ZVnWQ?mi~f(W1;eG`?(c?KDhpL1!8gb2)Q%3$~&= zxOd}wPI{xnXocjF^(;noRvkz6)e}HLT8(A{wmuqnM85mz9HUOMNv$|h#z>9idt?(m z|3j4^X+6&*)P~cF>&9m<_$q>fNF6r~r+fIB&Ix;~Vc5B_fnF5HTNA_M6+4%tbHmWn z%jzQrtg-f7W7EO`qiElchAWp@oEvPizdGJ=?7hV9X)0*a#7qyS8nWd{>YXLIMV>xF z$9cmuBZ?6@Pyk7S>t*Cf=`$HytMesh`F_T@`OpZw9*2h|xLrBJscmy!=XeGqQkKYr zPsG$Ut$&CAsf-Pt#cgMQNX*WA#{<>iZq**8BuWnI{A|wWLOp56n0q*Q==jpT&DEBA z0=+>5oT$&rJ|&CYSr`n9e0dw6@}ikOJtIc%(CHKE`LVgHoKEq)PhgX>)cYrQH;i)$ z=mL;qs4Oq~#@9ATqLFCM*qZHr3=*X@P)Ew;m1yu(2SZ!)>%I9 zwziIKA%81m?$dx{NJ$gJRY2i0(mIo<9!M=dprb{suTyAq`NHzdgz^>|(w`UBW=G`u zdnHRtCit^BU-yIGRdI$%EaCql*SDP`5$E@gKD&d9j1#lRwEQgK@%}T<&blm7M*J2& z#d|48L(7|E0u7Td7EJA>iVk$dDOtbA4609(M^K$#_ggS!pOZOF1UwgarF^ySuET}$ zv4rwL6>=yZP18#RkSZeOq}jFYc@j*5j+O2HibuZ^`IWhjTv=pK8A(yr7{|wwVPArt zQMu3JMHH6`%U6HImE}>7cI4){f!H`2O79pI%ku z@O$-Fv;XB}QcCYo@7}P!#145f2*xviSbl$vP42PFeT-axN3<=Cx<*emCJAYp?e(l; zNi1T%^bBLxqr#*mY1(Yr&~K^+hkE*Zwx6*&r90*>1y4V-;2t~eJ-{MCLAqo&@IAoL z_BiK!VCkQ-liQu61-0^+$Q>Rk8DwHX;kGGEH7HrXw{| zr@I-cM_nh`BC7uSj!72{OgSoiQi@z2WT#Xu$a(o>Y42S66?v5xauA_f1F}6gvob&S z1#RriUhnX)tl_fZdSPN36UWURD@Lzf{M>9k>;&!i&iBlC#?k)MxP!|xm&~j?_^xAe z9OpVyQXc<-LC-4PNi{ax_t66K2db_*!gm5U;$NODr=)xj^R)5kQHkeuz+um3!D%Wx z4*MY{d(fRKnD9Ho%U7M+RQm3xigS*N>Tgen@mULVi?Hx+Eg7Vhm71F0W*$N}`S$W4 z*el>h;74*Xw#&`f81YB~mp4pRj=?)Kt)ouK&HDEb+ba9($qhaX8FsF8-*LoweSIrS zHNpL5G!|iGn}FqahO`!LX|n<9%I^w2jrU3@kcEHE#kb;Mh(-nE%0%Mr$>LeUg{cq& z7XcDs<{#P+W-%L7zFljPkaEQqrEwWrED74Bk#vmvq^8 z%JW)OwyOw;js$As{y4eP*F1dWB0I=pI>2cNVhWQ&$x$k{hPZ7uuT5SY{@c54u0M^u z7^$7;X}G@3e{246pKuT5(#=3&>eBQnzAe)I1}l`X>A-uDrZ(6a_XcINW5k0~gD_`? za>O6V6l$iL;M{pNkP#{WqC(j_|Mp%SGBvp&H;!ZyX{%(;qubH&Vz$8qr_ZeBjqND;_0 zA$iw|(&BS;6rAh$!eaq8JRg{E-uS4}RkCGMlPrhy=5`hXx`O;IwYAVVIG0V0;Kvta?gNu|BezQCQizMtAXz{<6eKglt8XRa%ILO68U7M-qt-ha*up zk8ljDJQIo0b8ioxcUoL3?`6=H&vwhw5A|gm68dp?!K64#vvNXf%g^DiN;+HYEH^&v zFDFKsXO->TBbOnjJGE#P^*WpBi|^K>4`Aw1|NU@plcshsOxJDdT1VRA8(D4$lhyat zCwntA#(wwN)51t|AJFk=4^E7{S6h79DUBMa-IMjDeysXG?urKD{Wp=q1hGYKW_g5( z(QJ510{Yq`GK_;_q~)|9cLr0}NB<)$O;tE=Q}1Qd{mIS}_PBR6aDP5^&y#jMtXvVP z_}V2&eCTeS9OW2_j6q}H3-QxsrQ!)m7F%@NQ;U#`qgu+Sg31C zJZk-Py^o$TvD~2P>0W8M^*yH`fh&1)p>H{##35PTR*fvNdgeQ>=*fz)*Eel8+y215 zD5=5v*N-y)3{X~UgquBB{(e2*wEgbDJzgdjoK%6XKB|JP+nYY-QGB@dX-;6#hYeX8 z96bih54kmo3tvU=awy4a#>f3$)l+;#q@tiz&P2XFuQf>0_tOuyQ1W3PNL(@f-MHR$tYcQA5Wn7_gYG;^`EyDyhd_0d6klw z&{pyAAwgJOv;_U#&YNNlVe`X@51n_6_<|g=$#EC zT}@`G@#*m-zNUg!ZDP9Oi=0O~^v9L`)XN3`ajDS+q-e!eG7bjat#JvAI)9}24(D9E zJ|yZ)Z9GfCrNoB`HC-l9A~w#=qFqoZizzqxWn0*LuYuLa1R{94{tpk)4vMArBY0&j zQYxkICPnB4(z-`mE_5a{?P5(;Yxej)P!5iJVroJ6Y%E`Iz?hz~dI*bnFNL;|X)l%b zHwyz9Fv6pZXpRqPbswulr?G7?y6aV!KQLB)bJJKcqlDYqW}lCPMdBS!e1uhh-jA_^ zBy>#U9|oypJegb9eH_dAYT+xs`8BZ>d8;1A{rS!t7E^`&H=}c7Uv%DQQf#eY!jd*; zT-YIM3A@U<70isDgf$lV@c(y|^#-pz9y7cm?p=tc6G4+? zu1??P%Ggsw^Q^wsuZKOcQ~ATKQRZqQSDWtOnmfe`@5nfkj& zZ(h6QPtgdGB|f7ZHhvq9g(6dC+jkv2u#L`LVZ8ZYVNq9Oe!X^5#A?0;bMd_NnGd5r zho$yZruQuRvwxC`)4czYRHQ2m9Z@A1%PG=pAWK`aKI|P>AX^zAy>p#Ptc6VzoW8o1 z75|MN@0KFw&y^CCuyN&(GL_`obX}Un^0=p}qw3yoer3|?g~ew0Og>p-kIm}s$5dwK zxgDOTpEjqd5TP4}!Ua*Hkqxhrj4Dd)XTGt+ux);0@}m@5WA}uTFBTgOD=Mfy7CIta z8ff^n(Vj6$FOQ9cmer)uqz??>_ICg95Xe621&WEzR=C3H3%J)bDT%#uU(^J#wtRn_ z5X?rdPHMz*{+f%NTBaEouO($a6vR^6A`^wQ>@{Ka4j7 zs?KSi$#k|-lJiQcvxB7V9Vw(^6GxMC2 z*m5gRM$0;2omI#Gc15{rla4cW+OY2;_uZE7&Ea>iZ`3(1?waA8WBM_wG18Zq{;WE` zO{?|XAxd^t<6L^(anYKVJOLFLT`$dl;vb>O2CEey+a~*(8!~2Um>Yl3WA)S_SAKVA z6aV#gA=N4Z*zBE5qt#SmW#0*k052i5NO;^?|sYda?FwSONjghk`_ zRE-j@c{0fiv7dhESmU4Mx*va)6JqjVJy@t|$T z&)u)EO*XYuF`V;*0`s(iJuXk+fp6aau4CPks%1LoBWr{n?GLUXlq#ot{8&B=0XOkl zq!8o@O&YCp41ZjSVL|(}h+DX70~mU^pY2s!P?d$vN4OJ~*|}OYbaKMOuoyI4v!a#h zs@<@a4L-fROBC!xvm4R}@2qd5S*bWkP}Es zVoCmqmpMs(Zv;)BJ1-{;CF#j|9hg+ zc$>@-=vE2>GQhxwR~n2#sDi<3JBrYC0FBqM<%OG$&SXzCMU&eWh6(V0fh<_}{{5e$ zqxmXRsYj1+N{I;mvM}d^7*#TXiR6X|40X?Yu@aWVP2e29e*Id3XnA?r z1J;Qk0aFOTuA^}B@i~l@jDfCXwnU57#l>Zu3)qrjOm$t|-Q_yfxj>+a$i06z0OUHS zzutcXK}swTOH8d=j23{pg*5W+u{Uc0E*2JwYOSr-%Z|mhH4K{j_gBgjEP!>2lqTT# zjPo|P4>2`$8*nCp-Z(uy&9KnJtOx|)CuKSWBeD+6|H}oSrvAnvC#?c5IPefeO+bKD z$(r~z-y*`r#Z{ULj9lz&pj!!NY?mjv0~;R6?_y7mAeGmKFdz%4Gvx`=g(G9d8kp_y ztnbqpxwxEwjH%KjT96zIsTnnn0jRIL{MabT!cd^4~YAVLCe zBnk?Ofq}uiqFSkDvGdi%>DEN0h@c=EIy#UOD}efljEwA9z)AMNphzt*Bs{zycuI=N z4>HE=L2a`Mbjw&;nF^y8JfzDf5Wl6FP09dO^9927ntS-;ZJOG@)HAtS>VpXr}}MWrSI$(lMd_C&;mc%xWTy~ zIe7xYQkJ^~#&ZZdPo7K^6%~Oi*TJ?pmU;qr5r|6=5kA({sib*=rvMF8SWxf+h(|Wq z>2H;@#K4psl#~V`4U)2IvpEEU+X4H?`(V8q4x&y=6tu?>#t+=N0iI|HL@Y-)OMOQ4n{;3A#yHdE9q?%-8lS64m|Imx5fU@sJ?%XQyl1ON|p z#`nE2a~49L5V8Tsio)r-K2)dk5p+TMt&g|H%aziccNhPw)pM}2O5%O31_3TNH&^FN zSlEs&LCa&&|IZwWI$Z-|EC@TT!CPS1(cTBd3x5jwfTP^oOBFUE@aiMSpg>K z&Ro+|8=KUlJeWBFh4u7>xcBj0((c+2!)g>P`I*aY>Hw6WfW-Z)Owk5`!R=pP1%9UcdJLq_ zHh^XN+? zC9gGp0PuRpDviZ|JS$vWTm-T;GA9p@e#;BKx0D1eU0qTKT##Z9e$S8vXqRU2*=c`j zcu7Y~YqUKKwAn|Vo+pKuMb+=o96*sd)z&j_SPF!_tRtzA;NX_#-y!ecyL@^X08#fZ z0~|x5&F>P3W|J3T@}8QJ>vW)Bk_7<>aF`=V*&E$|>w&c8zDm8@R#JJngR1JAx;hU~ zBZ>cz^96|%csz-W7o2X*@%PjCg;Ptal`ZBUrU{YPz{{lYn$a;0YK;! z^S^3_t?uu+iv+?AdkCfS@_iuk68kL&2jLD60y`d;5j54^EmJtcXFs_MnHs(poKD~^ ztr~x?uc$P2gi$5+!xsm@IVX|111=IqKsWF6v9iNNwODK~<_&B%>>EJrOU$dOyF!f;HG zFhKGGa<+uW31Q*DLJkjm^p+pNGY3K=HON^34n+_%$JvCSCJvnN>6Md${rFz0WL?}g zmV)QSs2c2WNYp!EEVXZXGcti&0n-48-jdmLff)U%#^N~$CxPB;SY;9zb^wAgZ~&mhvtc7XMZW_yQi#3) zXjrvM5@KRhVyGVK#DA7lOF*c8!A_;a36+xAoZQ_(GsZnA2@WT%042!Rj*cJjT@KS9 z_KuH17s_H6w=p?21p>Gawqu)+fIuz-K*Hp4SPIZ^l{Ga-0n!M&|GG^@B?cb?y0(sv zP8C~f0t}~shX-jHAr;j;qd|9e-|^EtRwL@s0aBzsB7B_11^UY8`?svRU*olSsDIp;N z&;owVD2~fnds?t|mj;`g8P1S0xYy9Y8gY&&e)utwE1H zIWjV0!udZx(+x%slxu#_Gl0|^e*X&eR?X-h+6BYbJuwK-;tnYbgaHC} zAXx;_O}b@2gV_16_aF{{xOWU%lZuK8;0v#Pkqd|2uE~aPb^|RVFnIy)m1>u#J@o!q zUClKZR}RWd;PLMO9vQW*1^3*6Yy&Y|mlW}9Wy^4)w{G16Fu`Hm+FV)b2%D-CZ$Ai5 z3X(DqEx}p1xw?vaY*X4HEFpmelx<}|;RWRH?%^R0k_b?Iz$Yp|iUmOQ*KETd;Qy9t zl_C_A|F)XQ8$xBJQ48STcN1IJC(AKVIJ`|ut6@WH|NPk=#9DyG$F>d)!NlPZV6f6( zqf0n7Xn8T#-EBFXE0g7WGzBVKkfMC)Phh?$qt<$PG=oq|uxi_bH2|?2u$#Fq!t%J| z!QWA+6g!8C)e+SNyx~hS#{v-1Hzz9nudjTQf)U0?H{IsnR$l%aR38S0hHk=!kn44b z6q8|JyXRHMVfEwVM<9Orw?GUt&f6eT8>%ve=t&K=g)5X!=-8wdy6f!3ESlC3C&4ig zX$WTOGbpF`y^SG+@MC!cYAc)J+-?Z*&Z`5pfZRX=W5Y|H6g-~>i4&p=xZ&q7U*JpU zp&$gJcFkX3%K8pb`S-71AU?B!aI>?s078iISM!k&md?-5pOovl{XhBY?+S+*X=vdH z0T8W0oPa~}1r9yHxf-?uszeqogtQivca2_utRTIC7^L^#0(~_B0RmuM@IB7ZF1>_N z0#{%gltRu`YzOnJpHp4nWFd-am5xDVKLm9MF0SR@jTZ|6wHwF`0i#a$^sIG-6H!r7 zmEH7Jg)I2cX-PVj+fuv2Ko@=!QeQIPJ&56FRgfD&5dpHhHo&=OXJ-HeWBGDovH@v9 z-i4?WgF6e3WA-w~E2r5npt|+~JT`~graBo@ zz~(ztBwCe58GJb^eN$7qki%_9`z9tPAQwU~__x&}ptQLo=Hm%S6C{<8ia~7@%cw$? z6fD*NS`?@kTt2736>x{K&t5l2+ePlg*1w;rBe;Eg5=;nEiJrdx&eZ#-D}AxBI7>>~ zT2O%x!&Z6BH+w1i;!3P12M0lz)3h1vn4*&^-IY`21@YR60Odd16*B~x{Kn>Ck=o&%?AvHc8 z0sM6kp?IKmyuxs6aS>7R!DE7gR;yS&m@kK|6O=)qAB7!4#Q5jWb=v$M{@v~=M2dxe z$nI*gu`CRaOW`sHoe@k{V8gcLN=ixswN5&}gC1Z60HaV#;**ec|688Z);K%VV+(%g z(^~?H(YYwp$%8C zPI#z6i1kUzg-{#LL%xHs0m-uy0cli|3MRGydCYme{fGXC*5n=xU7IjVtl6Gn5Cmz6;j5`t_CIHN)~2$^rhj`4>!0xg}DL{qRYWB|ADN(zHW)Ya3Jk#bY6`J4ebTT6}!H0 z8Gps33n^2?Yd^oK=^RW9QI$gr?{BW<4g>@BfgOT8DsUSI^1Lya87M&qB@Zw_K@JVO zECI(%qskqK6FqLL10)daA$KP;sVzaoUhw&Y%c#Mih%KWA-8~nG5P&c_ z^gi@;cdtU%=-)F*zvXwRsjCzBJ#&SC1WpKoAzh+eR}lGumJiSk2b6M_j*e{*`~NNQ z&HDN}4Em^stP0vifL;)D-X|uKzye8~e|~KNJ4ZYyWLs#IL0Suc{n`p~y6YoN-p2st z-C!4uvLyl`C0rhisIZXC+#42&T4ou8zyZZ%vbg^hi2NWc?|>5a?;?E>y`%TFwcvx+ zkY`rdJyy;ehL)oj z;B81Cbh1$}&!!um`>)rZ-G(-VDd1AOi3dAykMCy*XMzwm?8u}>MpPa zm^;&YedS%QO(7y~42TU+`$lIY{F z)`?0qi{rAgi~%=X!(ao*so*2yuK=#iD?lxen8PC~nwysg^26oTRZAq zuY-aUUIJ|goFm1ZJN-5IM0mq9^?sKx5OCsKmT{R*HSB`V@tgg?=zyW3+L__s#J%O3eFI8V09USPG+4UxS(v*MSM(g9;#G( z2M4^n!kQp$&QN{=+8b80)S`8wJ=l@>OjYlcer+v z3Fsf}Cg3F)wJ?C_LAPwj@qrBuypy(oI1lI?%Bc0J_k~bwf_h1pG?y(8bj6SdAxuHr zqA@aZ5K=RwT!`W`po-ms#0)l}$ub5g62@C4{7tz`kdzUgN%Fy6Kr6D5yI$HwO#(KJ%)q}T^r&D1}BIU!N5V7 zLJftC8kmSHoEj9nP|`R;SqBY5$YS z^a`vLie7}3<~&lVoB-rp1L+VZRzQt9Uu&0KTgwMV@C6>)5lpQOS~d`N!0Lq|5Vjb7 z`@3t}c+_I{v-Nw>_CiEo$c0?yJw;=-P^TajjctPF-VGiXl&G1RnJ}IR!VAG5SY5&? zv{gqTY(gahy#(Qm3dGo-n*{X3(Buc3h42I|_FkxBAd?_MgX+6`w9suTGHg16_KM%h zk5IsS&~t%UlWPHj;m?4L0W2f>WAGNp%0dV;Rb{}3NeR&60_+H(!3CcE`h4LUdgBm1 zU!X#g1($?Y#K7dFCk)hr$_R!#L2(uy%3T57x2c&KsLhJu>Ix7(U&<>MzNJZfPU>T5XiST@1 zAc|*qccU5$g>XW8fHC9Xyvpw$N$=1e$!%c2xmY2(AUg}<7Jc|s{j|Aw`vtk` zh=CYz-cag-Z@z$33?2vp10Dzm@Nskoc`YP+D6YrG68Y}s;%%;4|8eH_Kt%a>Y3;AnP6{(({Vo%!D|K?omzx0}ou z{O3Rcasutqzz~4_si`RlzCnn?e}N}$0k{?6eU1xP;D_K$`%_P|5L zCkS-sC18$tV|D}yE0nQt!3LALB4K)iezh4oA|oU)z*uQm>^20^M~^vN7 zeDVeQ4YMHJTNNxyG@tUzMRa1dI$F7FZ^DjU_E$u!#*<2pZunKE9AJ{yqpT zgmCbD#1$fZ4-}hiV1qFE4vNi$4G45k!TP~aU?2iaW1)ok4{+0oFa$It=;2lxw_QO7 zgL~&X2oyg$LrXPhUrQ(0w1sMxKr3Z?>OCVQWC(K4AQ+gK&>%iIH5Z7A3Hh|8r3G4w z#UqG00KIAWYNYTM)C}dHOH0mhKLOVnu&Qcs>iEYn4*(D-zx5Ebb20y?;|SwBAmR*g zL;Dymb2^~W0wpeZts^vunopUy_AEqHvc$l#R>8Jm>H=bNl;r^N--N10nm1+(K&=%9 z>hbWvSiO*1$Qw*dP%toL{`&Oo+c%gX1&<9t3KtN_TqB>Iq_nOM?ARp?Qh8F*>@dSG zA|m4N?~ib$8bN|o0Y@W1K5*l`2iom2)x&j9LLdV#A8`CTAF61@0hX^9*9S@S*# z@ToGG{|J@^{sUv5Ab!ET0#4|mB0fa~X~_VR7FdxMix#*xWKTjwRvpqG3G0IbUMDPS z4I&s^@;AVJ2+#u{_1T!4$0Iv7Kp+!?J0ZLeyD+Z-Os<;V#~BVP#^Fu#hPkY2k?K9@ zlESM{1|r%7dF9Z;Mr3HN=ZozS^-oStKED-+A#<>hfGqV5jpxdxg7JkNfUI~tD_Bq& zry8Oaefm7(>&Qqv$-6pp&fE7pN189{Arvk052`{31~I+S$`-*Jkn`sF_=whB7>SmU zgak1vrss`={+dXHk|;C?pk>2eM_B-umdQ{y$$92Mm4FK~DbN|yAS1xUxdVcEYw#kd z5X?H5qhSn8ita71Kc5j(2nNmosKWo?PT~LN-+v$a|Nn1zsFAt+)sGaWB!(?}6)7Au zowUy!5iQ*Bhv*cuw$+lEE_pZnWF{Y<7^}1p*#7BkFbKJ&8B!?dN2(S#Fyfpnvnq-7 z+?RRts2iK{*Zj+pw0d>>qfcpvf)*hf=W3D}XK&{ZIuD0NGxKTuWfBHE$5UH(_ZOVd z-W|~w4&W9JaH1Sc{Lm(QYh?22*@X_@N6Y0tE6?(dHL2S^STB|G`4z?6hy9mDf`Xd) zi*#sG3+KdwOFP2(pE-FaaC*Ph!fAZPbNnX1g9S0a;8}2xjH$yX_khLgs;}5~<3BVl zXZ29~S(deWez}sVEc#jGu9d5I^r4+b=G42{9*!=z`7hRzZ+*lFR#}lf5D(}fEN~dE=VECQd%pL-T!7W?M$4i(+*9s}BkY%1 z;*uJ6X%VC(8L^>{|7g%t)e<-}336i73DD`OHs2yoP#nb!diG=So^wx(IXdP zoGtJ#@Rr(`%6tO8+NbR(Ky(j)PMt!d^j3a^Hj+&vXKI^~E{#lDzxioV(5=3YA{+iw z*c(O?NX(NOM)$8Guc|K$9#i`Jj+E|PFv#LNus4v#zdjy~CT2(LsoCAwN=!s&&!0l+ zpM(bRO%tf&skhl-HYbh=^yoLdr#^1y1{Glqyv~2CaqEgr`?a&!Dtkd2om(`TUaC9$ z-5t*_N{ZHK5yV?;0*C$LKhq0WnCL?37IJk5(;VWS7Zdm^5!6c1Z&MIG7K0 z?zIQaE5vBserB>sed8~N__TbiX{G+JHzb+Hc%a8k7Y!1E#I-Yl`pL%ZX zS&&(6`wwDSYlwY#s*c^M-%^cZ%d}zq_>cmRVXY|m$}OkLsnxJ^^qqk)(E-Kl+!BG0 zn{}m47!|cSB%U!niy8gte7VC&o36+FokUSt;ZG}Dl37-JesSacX_7!q&k8l2;h9p% za$Jav*Obo~?k#1LjW=+@{wRCbMPhe>oHE*H1V8P=6z1}0#4rXl+=u%Z6`KS^AypI8 zAMB_nZ3tJ_4412~b=NB9m1&OR$cJuJKb3`OD_O8}@gC2hdF{LP{L=zWE{zEp(4Q zW6GfDS$n=6K4Mx)dU!#e6eFjExv~}Z9Q8;gP{ZjqMNSpFtbN3c#@T`}Z%HV-$GsQ! zlp$X;_Qp8b_Fgqn2z$u>{JOgy863JyVZo#wE&D{EzQJ_Y*s<}3u0KiXt+umoVNE(B zxW7y2VpuEkg_(NWz4`SfWNHK~ zvxypp%Y~CIc1UB+2R_-;r4<8B4it0j51B-5vfrwmM9GCZr>JE1^PK4(O&!|yI!8o4 zjCY%9pxSzJ?_zANYp?bs=WfJW$0%!Y+gYnXt6g;3nCc|T>0r(To2aLQ3yamRLEyX7 zY`iWS4@%$6GqbF$-V>_l?m;cM`(*#+RaS4wUSM9}y`;$5A?>>H%)aYM#_%a?SySxC z1S6abgB&CR;j)LxuWN4z2bHkX=VnoP9WYMZdSm7^0B>U?)2LbGQ=>y*nuZjU=WT$n;`TojXo@-K9@nQA;L&eNG+3`a=G;m7C1Zug`Zv-*a)6hTAf{ zv1W6T!&aG9))A!h8KdwTlRz8Cm2GLQr$D{L#W}~y5Ly1|=DD{9m7O$)^L6uyr;>|OpmwVvotT*OMbs;qzBu>X z5D7X|JjN>@(|s+YVqVTG-fHVRYRsuT$&Qx~?p{Y3PkGmvM7Y+Gkw|Z`B@?C7l6^FA zW(xoIHu4QR`~j~z=hNV@zVhE@SxUr28+FTLr-gwo-Aq$F$$6FrB2#3*O7^E!DpXiV z%MG#~P&^J%FO42$W3}=sIDdX;;mLjp8oAAkVaa%$HXnUuswOo(0qt4S_sR%j2YSE% zXlV(a6gXu3G! zO(KGD_$zhO7GDjN*AJ|RC9~x_y`0J)jMdhElw~1J=n%QDCv9fo<*ml5p5I~+6PKr? zVebfkS;4dxN!>kL`u-ITMH}I=;{LaAkM(Ywt;fhC1py7Nh738hgzV>X3eOU0_EHCJ zC@V856K$3Ytc;NJ?e%SRw5C7A)p*gXc%448=Z?{Tl1xWH`(kQZg)MTx(Jz-Q_QurE zlh4G}6|7<%v}Xb&oU}^m4L-IB)mZfRf{Pvg4p$Q z?g1@LPq#Ja&r(7^=;S68WXhS6xX1K;d{*j}gAQpCV?`Qh10-T+-ML-b?za%qNPf-2 z-`OxLCUo%9bcYF_+<(PO<7|{%xK<>@ZCE9 zUYO2=@5VJnmA=USVae-32V*lcyEC@tcL$|h6)G1s1Lwh6{Ypt$`=$O7$)ldiz9jJi zHB+0br@3VF7g@Ip?(LyP5h!^Jkz8&HVV*}~DD)iQ0F$+G(5=Y~R#XVXgh>dTH z2v*FFdog0{n?izM@QFZ+J0nT3ikL)%U0`hSKQO#|i7NmQBbBRff#C zLXgx=Q=|(`*=be6-#1LgcpZ`uHXD2<4%C+2P`7c6)CuX-!0GWx|LE|NEmVq$*2 zk*sj~ZP1atZuw;NBmBE&@B$O_{mju`)>yu~GJ)0+efwKBFD`n_sd5&h4bg&RI~av@96^-OQ_8n|qtGv&ORWO81OR8J+&&yJx1in2LCi440l7_DvA; zx%`KA=r3ER=!8dVjUvL1qw@UsM(LBEpKl_im7FZZggx|E(n2C)%nKtw6VYiESzUJP z-p#}0#jw{;vfjBo)vDLS{Ef}%dljjqL)AtK1Hl$ezccFS=KnYdP`%)%?k3NRLRM3A zx=yG>iw;UDoy=8wv3@*RB0Y9)#~MUvtdP$i9F7u=8{(?yvspmC#t?u(CAF&?BrZ+a zy>ge+{l7spR%^{WsBfjhI|lYnRIYovty1r9a_oNFKPjo``WTSkd#I&t)5x~9v+(@g z?X*O%T0X5+HEq=Jj<4+UM=NN(SzM*Kqd};2yCd@}F;-NU)b64oO02}nL&&VhGJizF z8eZ>V&mSs2q+71nsG>7&e-lzWDSDHUcBtd7>>d<&(e9QeYB>Dkj<$}f9hY(m-xoXh z?O(hxnb`9xYr={-ati&jP=l}|v)yx!`m**Gktib9=$=E3bZfT5JuFVXP?Sl}?2)H7 zo}wt%Z=-s9tjD-JgivJHMex0d4UEIluUo*9ibo!H{@UTPB4mh`B$D%_lB(EGsluhB zM5bAz^wY)ZzB%VWlo6z{z;O4c3GczHYw?p3tWhlKm_J*?-$^s97ENs#aqo*S6->8( zC{jmn7;LXly^!k*oyEd2z42PyqFAThAkZuvIkU)~;8^JGN9hM7_Zj^%gB91D@c3=e z%7Yjuc6l3A?CM`?-PBvFDVTi0vf-KsaVwz*Dr`i^V*TE{g~IZ}q<WA;FRy zNG8vawMy&=;GzKudy-gTXgR`cqkD9QB7l-_s25$Fs%Iy1*voTP|D>Y5^PRQjrE-hl z*gP>&fEKbUNuWcHv=3#fx?i+`^ykDGy-ZpHEkaZBo?EWFW#-q@UhI0a2I|(U zyHX78JNIzDRjtXTHw4UK&%55Ss-ZL5?5rhU*!gr^%}#%N=DI6LOOOE7I!B;cuSO3> z5|zF)yXk`dEm7JrnTIS2I1u=0v+X&>VN*(Pb}3iSS%=p=)AX$Nm%Y+@QxixByW-#7 zzLLRCSDG!2$HnS*znH|NAA9c;ty)X@oI-`VZ6nIWP1k3ET&}t5Ise%;G7rrx1w~$U zmDA>y=?L-aw?ddMefpp&(EEkvMkJkh+m}`%t^Yne_X;I8YcJ**LYH*% zeFy*bD#ftvz0V8f2$$ae(qZ{fbFHtyFKbz=5iRqR;X~rzcF5B>tKyw&@9VutxB`Sl zTzN?LwIp47S3=*#Gv;7^W?ztF<}JEJeeECIO~+}*Mb*8yK!016VoUVW;wzR*W!i1zdgOU6^3jO9 zR`M2lVCdrYG&5C*Ts?luYviROf$xT*d{}FtC}@V=zP{h*?BNf_Mb}sgoxSsQ`NWNR z5IDJ&^%RnZPb?^QBo;O8s-pxEe4q9U!1seH}P*Y%W#rJre#+e`HM*=@&+`oA7zwgS! zc<@GX>iD^u8`)X+2{emWm;PU!G&dD=<>o-M1$5Fqif~CgdNBAlf$GhXkqCN(M#z*A z5gH|gvcJV#vDMx`?X)suohYb9pmOs^4r>iRoH+Gr@QnztsbWN}QE%U#T3(p9M_YL< zd{ia&%>1??MURWM(_Ei#>gltgFYUS1fHIMzmNk77_j~P}54pttMxI_b#OTxd4Qbm& zbV*JoHcJYNuhgWt>xG|&)7O!nM*^pqHraE_znzjo!!$>JnGIvS;y(nn;bbEmuVf#0 zf^B#}EE=3d>R5K8{Rw9}37#K+@n9=w2${662ZhO)Ib}bdF^!7f&W}xdJTX0Mt+J!aS8S_GWi3&uXeUcpUV7~X!}vD7vhKO(C=|KlT2h)IA9Fz4g5C74B8V&Kw9jUFdR+s z_@DSgk=-3owNEJPMab5BBlu3prEX)Mo0u$MV^sS-X$hLZ9XexwnSsZyeP2=`q>RC# z--gjGvE*`l8(dJ74spPlKp-@$*4+q?up%2ErerZVHP0-Isy=vE#xqYvX5l%Ep14OP z6tZ}?OFx`B!S|9{r3Nd>+H?w)$|W@Rn|Y}#hsFLz5JPn`79a0D)I{+!zxzD(`w9Wv zS85jv=iksh{<=XEUfU%Dg|^m$ZRF-SesRJoPx2AQMUCvdJ$o3xmdH8;X=rYkL^cEx z&jp>VBtb7SyoY<6P&!;W`}LqADV4p_`aZrdm@p*x#n$pdju~wG%*(l0GM{)?zH_#m z+db_>H3z&rD^%$|-5OEk!6^>3X2Y*Og;xww@muDH^wn3YkvjWr)C9dp9Pju+u<9DC zT`i6Rl`9paC=O}H;wOy!Yf!-T0|oX)I5yrw8|D2M7yqh2W@6`Oq}z?fk(Ous>@e|> z)#(*XXEyIuEOGc!ykICZXZm;@ZCSIoiB_W+(5iEB`}Jctb02y{wn6c|EOy zubeT+E<=QJ?v}T^7-9B2iSB);KCGwsGUS0$WHZTb5xf#Xh+)@buFHxgmD9+i8R!4DA zV%ioAGQ>B-FnoUoI36@m>5b!$8x0X!&j$x&RgqIn)XSZo8WgXIF*;k^FW`r+dy+u$ zwq*@c#>oHdB>92iNSBbIH=d$aB zYWIJvcwP!`h^hKqIgVR41S2Q|(nGwuwPzXN!Dd19f!=dCH_Y?R1B#DMDaZtEDzhAJ z6AqH5vfyAdFqeUs4vHG)hEE?{aG^Y62+&p=^6!uW-}{q*_WmFQY^@AnvY}KWaL;=W|qbhZxm^T{SDMsY;fTFPLzzsrru6@zWy4R#ZvZ}9V2yt*leRH z@B>doE76xpsg}zZ7^6%i@dx6|5VLLb)!4#2Aa#H^6Z>iR3fevGk$s zHK;gRpW|BPU)}}UT`uj%fY(=rTtcln2`uVFyFeY-398$S1#RIZ&-`#oGW{IQl&w>j z^baMEp{_?r^~&!v0=37>Vwx z34S`?N0leaamixxUrW_5J&CiR?NDTXV*7ow$45@}&~1X}GeGQ`X`vgu0&9NsV@GF1CWwr{G6-AYC{t}EFOm)-vov` z+(2^0FpmgySky7hnm0QCgrE1p^VJ7 zm?=HVmT5H6yB36IN?O**i6?l7#E<>cij$QEpsFcBJgIPGfW%)CNa9a{*~t@(`aSd= zx3LZ!;+CjG3*DU;@x-eM6S%!4eP0!wN&^#?l=Z|bHX1SzM|mNl%%b)#V#_IcB9c-8 zm_ag$Feeqy2A7RNu5Vc_^1PMxFB0R=5io~uDIHDI0h1(p()X*?^ga2iDZgt58p=n? z^aq-q^d}av)^f`K2uGBvqTNS-urcXr@0wLx#fp)_)gsU%h0HYFTR1SV)S%_Q1bKkRB5{pW${9?J(3up__mNJUHSk|o_dyv zAfugbS=?WK^vW9gn9A5f`R`<+lIcf!+__@4-c0?aB=2thZ-^a%gzlVsrf+N2& zgcdN9ToqSKrlL)*N^KZ%%=E^b{EaL+LwCPcx;MjR%HS4zta@&dai?GvOSE>Pu9>F0 z##Q{rW(DVt=6$bXEq?KWUa#B_Xd8&5uBI!eZ(u7s4s*hYATGJrF7<(Pf>q13H8O?nzj@w#YTmHMqqxP2B(_FrzKjAfDc@fuqa?dmm3rj9 zu&0b&f?3S&U4O}>Q;7TpTY!s~G-G(>*qxqh|2io1XwMDK(iKO!+gTd0T&Nc{$mN=M zd*tY#BoW()2AG$cT=CDJXE>Kq&?2fRK@w#27&_!lwnZ(J^*$o1817Oj&w@2_=<%3j z!M#43+_#B6p_C6*^x3pN_|l^(SW+qOGpr>l@$qn?C1HjXSd0ZrFv_3DR=GzD7Ak9& zP=lpqWTM3Li&csz)7bDX>ZH|+@|NV8EhF2-xEm2(`2tLkrb|L$DzZ9#pGjUCL!_hE zRk+hyo>$dEW@u&XpUfXU9B!cGbb@G^bO>^5dnA~2xMNPc8k?9$Luok_cwQfM|Na*q z@fL!O!aWaATLX&8ycL`S5Po%k~~8kQN@5RNW-Q z(buS^Z~f}X9G0Z7Rtb~A`h7co20BP1St~EUqiZXAWQhrRqe0+l^Wcldsg|dfRl~4P zPgE3q#5=lP4wfx6I~X^|J%5-J%L6>OF%Ckb-6m>jka2yc#T^g1KV*xfvPi26l?}SD z+VKO7TDI?l1+caXP-u^WR%eGF*a-XnPZpq}4agb*IlDKO<3T~y)A@s!54vdtzO3R3 zvhSfo{k!yqA|=LW2EtgNKAt~xz5HgkCa?DgaypgY(0v;ff41xim!hnC&6fzjPfgma zjJKn5*Q12m9R>iK2`_-XhDA zQiW6SH25Bg@*qb&YwPvO8_jjQkFhEC&LLW%n1$A-(=)BT7%o%rRH9XS?};%(87^aR zzy@Mye%>pEE+gGu_ODMna$|Jw;HeJm7n9UpC0z!`{gm z6224*bwkY%r1vk`5^d{)eCUnjP@NKvNc3H~$~3l^jWXWzK|}7S7hbvQuRAzQZ@tuo zoDx~85|7`TGIepE3pLpdt_{6Uezr<&g@JT!D|%bU)=j&l(G`0W*Kg0e_n_MiBzINC z;P>i;aiIGa#_c*2n=~dlvi>nE($ol&F{Cskh7o_i@p>9Aq zIRI(}2rOfcYN?_(&jgB`g);yo%Otx)O_%FtXveUo1ED$~`H!arOr2+ux>bN4G7aAqoHxJu{|D?RA@xYj{-rtE`}wb zHd0nhHM5wg1{|66EI?Hve*_sY<4#*%1Hm5vW&@Htic~#pDd8ZoHvk+L73#|K8OZHx zppyY`dLDiM&=5*H02b=M_>twwL`eJz19<=^_f|j%z+K0(z5+?-MgT7Zp34#->sE@2 zVIscNCn7rV7T<>g7)LQhz?>m!41DP~_DtzN`%1L_901}4AdjL|Z9D*ER8knUXMu1T zPz5iEG;jD#CIG?%BpQHc#u?zp03r-22jItn+$s=41CU5#hySoy)nNca2W{`<1{K?)={9RRBKiAJdaDAooXuJZA_Dp}s}_qUIs z0g^ZX2KkmBF#%EO$@gu2Ex~G^dOK6cq@ff1cZJ|aWPD=-@3EnXyFeX1w{b>R=%kMfG9X{{{Z#2769MEkeFy%SO_IaUWy@_ z1rQU`e-rrJ8^rT<04OFxB`|0Ld~vm*XrkuXFKmn+kC!J80F(w0xXFvHOdcnG@4IoJ z;FAk^o=gC6>GxE_kbq!;ob{W!rBFVbC|<+Bz(87}H7tykglL8=?`~ZXm^Q0)=Y#Kg zdAl>{F)&Q@-p=vA@ihI{bNBzjBm2L3MEL(@^RITNt(*D3X5nLAO~Hs#ByhrlKjehp z=ibt`ARwauj)*qxh}6@s_*elgTA|*fCqy&(?%hW*LBT<|07*M&QBRbBcS^3e9c9__=}4!D*+~zY@qh1K2s4e z!U9H)S@+*w@<7}Z2sr(AP&g=@N*j`NrZ;Wdd1(Hz%E7kfoeo|bTPSEeO|pP;;# zq2fi}(2qg@Z{OPGrqJuyAy z>Haf=lkcF)cx<$5QNNw~Ybki6xQr`pl?&rg(yv%rIndV2{`1i*aZx<03m;RUv@+G% z%fchxW9iwV+J@5sbx6`dG$`*`6zB{`)6OZBM`$YEvA>tNR1qp9rL;XhQc>IX>|e)} z>);uhSiQNwLAwpj4OCam)x~I*NYXg>&5Rxe6m=Z9xtn0n2&X027O%N}jNccujC2C$8z1H7J7jj%JsO$(2Z9nxaM^%Z1~6Yd=9OQGzkj@BZ8YT-r5h~o zqem?N=c|zuZ%fs&+0vzTL)bTnrITe6**e;6sl?#838ImN^0}}1yR^qrJSO=UjgeG! z?)X3mD(pf-w7pc7_49Y1fni>ef*UNziNG5`3vZ z{9U=JC(_IZxkZ@Tv}}8|tSWmax|R10dEds;9_|fvakn`F54w*+_I-y5fB7#OqP$7<;0`;y>(yM1ERLxYzxh9 zFyTFI&mDolVPVNEx}S16?M({pQG=D-LL)~%$wO_&JBgGAGgzXllYmc*6+4ekCj_1` zViQ+K&bOjtk&Zpkghf2b-rz5zt96G?#9Pfbnx~f2S8dnJTQPXpiAwdno6yn{<4Xj= z$Cq9yPKMb3U{E;mLNh6Q0OUOi<{Fj>Ys_Z#UgO2rE>f zg>2@xLLG9xsRP55nz8mbZmf}M9CN*Z2dqk-xikJUTE+1MX|$@d*eHaEy^3&i*?jm! zI`KyMC~Z+PZn=vEqpMg7*7*Jx)K_)S_4A1+0;yNY2Z2HLuTF?A7iv4FQm?0J=7p`2 zkrC6MhZ;wCm!Ps?R1P|T@(p^e2?f^~4WcnpCjiZLqr|Bo8h4&@GoHO{svx&dWry~jg?hcSp>{uHv4mGH z`$8G1T>tfyX(vTrqb5Wa!8X+iF=Z1P)y}&NDn_FfSN8c}x?FZYFA>>fE>y>vhHM{- z$ra-GsnZf2Q@W^}ogho^81`~sD}^?98j-Y?Mwfui^HWTMjCbd}`6HU)5{&f+PM-ng zpl}&o?%*mX$&m)v5tD9oGIEbCxSMS#L}T;C+{<6i1%4px7XK;akH9J z>fwy+6`h*JTy6O+g;2VtaL+{X_r+Mk3N#CRy=W3nCq7@TJ)0E|IEP+`w{9J0^J2}5 zVLzgJZjwYogRV+W;OVOvqe_zZlzRT=cm{Zbsvt^5mAHDWj%jm7+a)xtrQxaQRt%b~ zB9^L&a?cdaxgVLkLG-5$GD6X-RL2qEB+}}ydZJs?v=&4pYn&(-@SkFqLCSgwRATQ6 zX4{N=^OdS%t_VQy!4Ux;Z5cwX_Z?E%RHUa9O!~w`t@{exkXTA5z5dRLVxmb`#KRaO zCA);%o`dhyBCvn#suhj2@-f)(R{v~lzU1&Qgme3Z?6_Z$a6jAV}cToyPWl9xZmdq%7m`+9&yALw4TXFjr{+i5Et9IQdnAaR*lIvfpcLKw< z&_O_fzMDc+J6fu4O#0Z`B-8atb`5VBlcS2p;~K*hv;|{6CmxfcE6?6Wm-*xIJeRFk zs?}c~s9O`#eJ7uF&BhI}B~xQlVJR(*Mvq0!80lQJmw_KRTbH9SfnqQB0&x_0`sxQ! z35kbH`UypJ$RWYJOyOklWLO7?A&I|e?5rJIx2G|SH%Lh!p!3}z;Zcu>i5G@5_I7?i@cGOLZ}>jpn1+nZ&zlOGiY4hs zkNQ$xymQ((q%Dt6B1itAX$va6F=s4W^2_;UTys=72^{2LRae-mS3E-$<`+5 zoW1)j-YvFJ>@l_0OQE$fHF9r$`|`x_CS@$9iGsKdEMmt(&+06tC0QZa5Ni-GZL@v2 zJX-(EuU8tPK`_`_>ppryM7kRJ^(UCM6?jCIzj?USg!TV1 zQR<|a^{oC{cJ_I*C*mFIkLe`FF-!rpv}S>pxl}jE0;1XC&lzoA{DGG%9hnsyI`YYM z&Edrj8_57u%+2`u*OEe9s(p?w25w@P_>R>bQiXG;?tkcAWElEQtNUpg{D<-98UzEA zwYJrsN%U!y^6WnxyOuGpj{b_cx>OP;odj&G&QP*p-#>Glb9O7~C1;N1Y5+fS|7xGk z3`WBcjg}n!!wlCS?fYuJiX_20Q0zx#`OXNDWwnM64;9%HCOCSH_8=2I|iPabm~a$f%) zy?uX#!2VJtX{r0Ph_%7``RgunkUS}>dk)Sk-d?j=cRnk_U_elp1XM%+R=rf|i2uPp zWqnvqJTb(IyQn*vs67Xndz9HHcZdUppk{Z%5uK9lIDeBMm7nj~Zb4nN&osSM0TEUh zexkyfF`4?}<}i~<@Ya^`9}gey3!xZl%uw4BAS^>V+(kI~Q$YvFO1u%-LDxitzVdS7kQ#8m+_=F(Bg!WQ9E zOsNGdD9K!TMhB>7*_AAXIGPgrId<l9~Ui)rDhptr2;sm#i7W{`zl<#-z!_IgppJzLr5uQ4pIh(Qaxqza~X z&8YOGQTw}`bUa)>v&YgG4yH}c3Hwubj0?{J3G;3VM+cQJHpy8<4sP{&cwaIZ#VS9O z9$=Ym4wP#^rfp0b|Mc|fi$THa>(?=^Wky#JnWR02hRk+@M4N{cRK}lur|O>F4Zis4 z?bNqiNg6KmjQWagn%JHqA;G~qH9MJe2CWN0rzj2xK*5&lia-&tnI)L#i8Iu%4}*eZ zhy3cxhZ{RhQT#(g!FdK$?Kr_!|Dejtjke*uzbXuFClEy$`xSu0f1>(D$iH^np4M>1 zTVnffF%0t!b)RU;{Q1ov#zgFtLBU=lkg>FC#QDviW@=EAQGTI+16KwDQlGPF^!u9b zSp)iMGUT31YueSAw_n~uK3s#xH+Viunmk5bj?k)}_6VC*wEj%ulV0P=^Qzt6VOETa zf-`B&?b{f((%aZQ2U3NJ+m4|JhY^8GuQ?7#umzv^uS}ew5av$ul|HM zrG3p{!ODak2nga1zT+6???)kpLTFvr!AbYpCRGT*OMs@jr5R{Etg$uHb}94?w;J@c zq*~8#oRO_A7r}JGF=A7s>Y74++MiBrjGU{B%4bsO$h4`FE?zuHz8Nq!jDf~oQK1=| z$e$ZQ)d$k$y=}*b=E@|ChP;+aJ*Xc>v#Iw%)W<3Wqq8#KMeWR#GN#vQOWt!phrzZx zB#$6;1naL8s0wg>8oe0Le$Xs-v&`i0r<52q(m(6ELL@uqDlB(q)O_TiS+>vzsei3< zQulrl#&-OYj^&CzdqF=XttV|QVV4FR?{fk6$U;G)$5a0eH@RWt+R0>_# z4KGGMgCajn)Yvd0JEOP2e$^<$BM6dL9n?bq$`WcavZ`WkGdHiy00O?x7>I#NYr~>xSv45*L{>?Kax&=eYe3t`)O-U z37(6BJ=0ESr~lt0wcT~znrgCK;Wy@WrW&@I=*1|O5&Yo2Hph>@^CzT zBQT$QqdyVs`Q1>!ph76yVu>`qGz|J^5NUmiJ`Zc;*lhm;TPF*Q2q=$Pdzdp7swm5cv6jr|+2 z7~94ZAy7YkaZ7@DlzGSTPlVl_X+a1yxP?OjZCHV#iTc)-S zZ*l=(MQrD_ebD3MiI5MPf6fW-R_NTFE72p4eFNpOH^5+P(fuV%IcKEJ54-#r$9*wQ z{MahjhD`Vp??6`to8o>ekjdB`sf;-AnAH6_TcMss8^6-@J=3*J8kVE%@fs`eiM?xE3N|VU3X1NMtY-N+HU6?+}4^ z^cMf3AHJ9kS+S(>y%rqS5Gj_NQdjM`wg zJ%FI)xIzUet|`VsP+)Cz4UxfHCq8mbOH(|h(fUMoLe0N2+DKUn8p+Y}h`ArQJ{oz< z-wl*D;IQv@TvO?d>SJ!t(K#z?eYwA_3Z9_**wryMb#D!~?lZ|*hUl~S4bydu+>*b{ zGrojagH4$X5!TR%d;6u`s;ixudH5Z|Zm#Ffg*QuVuTZIbgtY1}o!C>DJ4*&5f*lVq z6J!3aB<5c>qv2>@?x`Zyieb}NQ*n-7@8~pgk5!1i%1Or&gY{4GO+%fp?fpgE_C9QU zc;f_-S>M?j9g~Z}?v5bnVJDKE8V6W5qkS`cD&U{ES7 zp5O0fX6AOCm^H7$Y~bIuJkhAlk-;!R0bf(aYV9%)uZ$-yW+1&D>6XSHbnuKcdg%55mhom1aH@Gq$J~MD zRI6}&jXziurVmMS{mi_|X0sxkt*7yRL+#86iA={d(d`JF6Q1ZZ%p*6(*~m~~;lzt- zJyH7Q)K`N~zSN*hDiq<;k(s~eSEvO?h7AHwbBYhbJag;|$3;ypn19hZq4u znEp__k;QwCC^tUlkg5Sw^pafS4%Z_l@gFiFPYpNy`PX5x$#Zq$; zp<>R-wr^R$&#)*q6ppoo3-GKXU-Q#Dbl<2w8Ur84bD&^cJQaH=h3Kb~M=1pn4+JxgfG|&(9z@6)Np$1hFkKtl z*Ox$o(3vp#1!J_qB;sd7?F@0>`8G?0`?kGL_xxl0KpoDPhL54`aXuExfrL-MC= zNIH4LbVqx?IMXF(3!21rZwpZU?n}RvkNLlsCwu_iYCZcE$D+<&0;RI##jZliSdlK4^ z3AVrw*TpQsp)az(;7Hc#)!#LCnxtS*zt9S9P@2V0zkKQh2WEN1PB^fms8ybcc=8Wq zyLR^`zhZbLC+eu&9r3&?*Kx$t{$8RE&8X}6Q_+1#wzD~ou6a3{c4TikAw+fWjAl%> z|89FxDG1{;b3AA%+%^CY_U8oxH6hfloJo3bV*f1`?i0;tPj30%?mlvRN59^C5ltp8 zjC=SN>4b*xqSf^Wnec17s@ar(t1?YE|HPj-Dr@{+m-+NNbYjAKapCqpt^1w~hVtt& zXY9|>9j+#huzD6^HlI^L7_2>m+$M>Jz=U=_{&wmsgqgDgnA~n~!C*!KBbsv^ILyYx z1;*o}`N(I?F|{u10^Ji9W|_O>-D0LG<)^ODsvz=!RIGW|<8jN>^)+hLXS7wnAtzmT zBWiyi0jC|-9UF8`xo;aGwJrMwWBe3hVRcZB(KPr6A7`PU;Af;ng;cucf4HvCI0X1m zBOORC3foEnzHfJ-byr@@_}Qou<{Xxgv=dl|Eaapi!TF6wsyX@U)#o&n#{HUbL1e#7 zDErJ!nGs zp(0t6T*elp@It~3B)3hKcx1$*Q=}^TMYbWrIGR4QWvQ2k6ucU!ncEt$+xz$NWMnke zcd|$o^zeaF2WW9zIMSWI9%jw#NSLrJ$kO$G{ZF6C$j;k_at;hxGdXCKDixJKv_#E5ImZ3QdDc|LU&m3V6MFs${g1 zWJ)SYeL1%Bk2NK@Ud(9hlHy91>fQ32S=1Ji-I5L5B^xc3Y6>qa#7$`QBvHHb1CFYs zylZ8=<%FBzx|zYplK0M8bCiqQ`@}8EKYvlHMFw9dBVldN|HNF7Eo){MQIHPtmtu~9j?zrs@CzNedl5bj^{D% zqaZ4p#khoCcs$O-U{I3D1^%w9&$ON(ue4;gBIb4aRT*Vcm&wSzjL$=&^LdKdYy$)D zeeqqW@pIxj0UWK~j!fbSsbPVwmW{{0GS4RFHo@(@1M!eV#gMWM#9)B{W)R|*kj_!y z?NEhp=-%JWH-9>wc|dZ`dW?irD;>w|;TkC8u67VQ*uOuD@MC9A?g}YbdYL~YOa`c7 z0Jz3}0?sbLwfz!9L3dAIqV9NyDONIBo;LJM?z&>$fkLpy6EtRdIaQ(K@91LjIeF&z z)4-9=$m<6qTIowym8%hTRqG?If!;26^#UWSr_HoihV3s(MeZlGnbhj@cUW!N-At*j zWr+Pdp06{Xf@8kj*iARdDwD+87%nqnA}i!ljR>DN4DsnIWo}k6SE_-v^&P} zo^G9JH6|a$mONsPH-}!0#MtWOM!V!3bhbh%pDe~3$a4dr9{r4cV6y%yE(veg*4-&k z#q8;3hUH6oq}Qzl)IP$xy#~sI9KO!GgmHuPGqC?uw|dw=I!4sFWOrirRhK!NXN_D{ zv|;{Qx}MiQK44KR-xP)Ep&c*46XL4Lc0OKk0X%hpstB;s{5#zO))VVDD-RT-ZsK^F z``TE2=vEs(TGkfz>aRQ<97|>YJpi9dTxhOmdQAL6h<5b+C(nj-06t!H>yy|IK{adBLgVQFD^+{^<5hR5lFpCPeV_@CIg!yyck*+4lpQ)pp!;8(MWqYQ?@J#AcS&u* z!V3wm*GOZQ?BM17T+LGLGD;{0M4Py-qUk|G7X2^vtsDV^y4fu=&C*jYP}0RsR%`#z zmxKuYF7Bn_uO#g58kpP!^kcrM^7`s&w&3>_f%lieiy=zV+qWoFpDC|fokO2}zYJ$> zHbE%(B_kEE`MbhjP-*gyFZf5?<+6G(hJNiF;fvj>sW5=Qp)qEYd4A4r3yLxj7JHwd zB)traH)isI5Mlj{s66oBa}|QGPjr}D6*N5Qv*w-UtQ%7CV=h}ObC#3*#XS59?f}8R z6oR`D<_8-++X`x6_4MFm%-uI*WFvY`ixoX8A?lgS24S*wsih4ey}8{FrJ62$K^ON) z!PZYrcsZf@_xVdjb3+`2Zr&@ZBv9W$IAap>UW|QS5m*T&-z+AOTJC>zKuZO)I#zRv zCY3vrhj2|YgCNr*5Vn-0x0Tlah4t3|=f&9n#!B!1b&YtLnEv0Z3t$uj9PO5((+n;( z3#pvykhgX0u_!aZ(Fu?@vthrQIspTBU|=Bo2jDC(ew-&V>i#tuO2$XIt_Q%1;oHA} o<$HSP_lXCf#KM0Eu`Pc^2)~5?9N!y`0tLLJ#N Date: Sat, 13 Aug 2022 22:43:21 +0200 Subject: [PATCH 095/130] TST: generic._base (#1230) --- tests/__init__.py | 5 +++++ tests/test_generic.py | 22 +++++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/tests/__init__.py b/tests/__init__.py index 49b7b6c1a..217f4550c 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -50,3 +50,8 @@ def _strip_position(line: str) -> str: def normalize_warnings(caplog_text: str) -> List[str]: return [_strip_position(line) for line in caplog_text.strip().split("\n")] + + +class ReaderDummy: + def __init__(self, strict=False): + self.strict = strict diff --git a/tests/test_generic.py b/tests/test_generic.py index 4e78af4d3..823b6ea1d 100644 --- a/tests/test_generic.py +++ b/tests/test_generic.py @@ -32,7 +32,7 @@ read_string_from_stream, ) -from . import get_pdf_from_url +from . import ReaderDummy, get_pdf_from_url TESTS_ROOT = Path(__file__).parent.resolve() PROJECT_ROOT = TESTS_ROOT.parent @@ -645,3 +645,23 @@ def test_annotation_builder_text(): def test_CheckboxRadioButtonAttributes_opt(): assert "/Opt" in CheckboxRadioButtonAttributes.attributes_dict() + + +def test_name_object_invalid_decode(): + stream = BytesIO(b"/\x80\x02\x03") + + # strict: + with pytest.raises(PdfReadError) as exc: + NameObject.read_from_stream(stream, ReaderDummy(strict=True)) + assert exc.value.args[0] == "Illegal character in Name Object" + + # non-strict: + stream.seek(0) + NameObject.read_from_stream(stream, ReaderDummy(strict=False)) + + +def test_indirect_object_invalid_read(): + stream = BytesIO(b"0 1 s") + with pytest.raises(PdfReadError) as exc: + IndirectObject.read_from_stream(stream, ReaderDummy()) + assert exc.value.args[0] == "Error reading indirect object reference at byte 0x5" From e11b373e45605c312ce3e94171a79068cb5de7a8 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sat, 13 Aug 2022 23:10:46 +0200 Subject: [PATCH 096/130] TST: Free-Text annotations (#1231) --- tests/test_generic.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/test_generic.py b/tests/test_generic.py index 823b6ea1d..5471989c5 100644 --- a/tests/test_generic.py +++ b/tests/test_generic.py @@ -516,7 +516,7 @@ def test_annotation_builder_free_text(): # Act free_text_annotation = AnnotationBuilder.free_text( - "Hello World\nThis is the second line!", + "Hello World - bold and italic\nThis is the second line!", rect=(50, 550, 200, 650), font="Arial", bold=True, @@ -528,8 +528,21 @@ def test_annotation_builder_free_text(): ) writer.add_annotation(0, free_text_annotation) + free_text_annotation = AnnotationBuilder.free_text( + "Another free text annotation (not bold, not italic)", + rect=(500, 550, 200, 650), + font="Arial", + bold=False, + italic=False, + font_size="20pt", + font_color="00ff00", + border_color="0000ff", + background_color="cdcdcd", + ) + writer.add_annotation(0, free_text_annotation) + # Assert: You need to inspect the file manually - target = "annotated-pdf.pd" + target = "annotated-pdf.pdf" with open(target, "wb") as fp: writer.write(fp) From b440f54ff00116cb079f2e3d459b0fa434092b48 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 14 Aug 2022 08:54:45 +0200 Subject: [PATCH 097/130] TST: create_string_object (#1232) --- tests/test_generic.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_generic.py b/tests/test_generic.py index 5471989c5..51a3e9397 100644 --- a/tests/test_generic.py +++ b/tests/test_generic.py @@ -678,3 +678,9 @@ def test_indirect_object_invalid_read(): with pytest.raises(PdfReadError) as exc: IndirectObject.read_from_stream(stream, ReaderDummy()) assert exc.value.args[0] == "Error reading indirect object reference at byte 0x5" + + +def test_create_string_object_force(): + assert create_string_object(b"Hello World", []) == "Hello World" + assert create_string_object(b"Hello World", {72: "A"}) == "Aello World" + assert create_string_object(b"Hello World", "utf8") == "Hello World" From f066d3173cfba5d46d22e27368b46a6953716cd7 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 14 Aug 2022 12:07:54 +0200 Subject: [PATCH 098/130] BUG: TreeObject.remove_child had non-PdfObject assignment (#1233) --- PyPDF2/generic/_data_structures.py | 7 ++-- tests/__init__.py | 12 ++++++ tests/test_generic.py | 63 ++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 3 deletions(-) diff --git a/PyPDF2/generic/_data_structures.py b/PyPDF2/generic/_data_structures.py index 805ac2bed..3f910f521 100644 --- a/PyPDF2/generic/_data_structures.py +++ b/PyPDF2/generic/_data_structures.py @@ -374,7 +374,7 @@ def addChild(self, child: Any, pdf: Any) -> None: # pragma: no cover deprecate_with_replacement("addChild", "add_child") self.add_child(child, pdf) - def add_child(self, child: Any, pdf: Any) -> None: # PdfReader + def add_child(self, child: Any, pdf: Any) -> None: # PdfWriter child_obj = child.get_object() child = pdf.get_reference(child_obj) assert isinstance(child, IndirectObject) @@ -446,13 +446,14 @@ def remove_child(self, child: Any) -> None: next_obj = next_ref.get_object() next_obj[NameObject("/Prev")] = prev_ref prev[NameObject("/Next")] = next_ref - self[NameObject("/Count")] -= 1 else: # Removing last tree node assert cur == last del prev[NameObject("/Next")] self[NameObject("/Last")] = prev_ref - self[NameObject("/Count")] -= 1 + self[NameObject("/Count")] = NumberObject( + self[NameObject("/Count")] - 1 + ) found = True break diff --git a/tests/__init__.py b/tests/__init__.py index 217f4550c..3b4539cc7 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -3,6 +3,8 @@ import urllib.request from typing import List +from PyPDF2.generic import DictionaryObject, IndirectObject + def get_pdf_from_url(url: str, name: str) -> bytes: """ @@ -55,3 +57,13 @@ def normalize_warnings(caplog_text: str) -> List[str]: class ReaderDummy: def __init__(self, strict=False): self.strict = strict + + def get_object(self, indirect_ref): + class DummyObj: + def get_object(self): + return self + + return DictionaryObject() + + def get_reference(self, obj): + return IndirectObject(idnum=1, generation=1, pdf=self) diff --git a/tests/test_generic.py b/tests/test_generic.py index 51a3e9397..aa0124cc5 100644 --- a/tests/test_generic.py +++ b/tests/test_generic.py @@ -399,6 +399,69 @@ def test_remove_child_not_in_tree(): assert exc.value.args[0] == "Removed child does not appear to be a tree item" +def test_remove_child_not_in_that_tree(): + class ChildDummy: + def __init__(self, parent): + self.parent = parent + + def get_object(self): + tree = DictionaryObject() + tree[NameObject("/Parent")] = self.parent + return tree + + tree = TreeObject() + child = ChildDummy(TreeObject()) + tree.add_child(child, ReaderDummy()) + with pytest.raises(ValueError) as exc: + tree.remove_child(child) + assert exc.value.args[0] == "Removed child is not a member of this tree" + + +def test_remove_child_not_found_in_tree(): + class ChildDummy: + def __init__(self, parent): + self.parent = parent + + def get_object(self): + tree = DictionaryObject() + tree[NameObject("/Parent")] = self.parent + return tree + + tree = TreeObject() + child = ChildDummy(tree) + tree.add_child(child, ReaderDummy()) + with pytest.raises(ValueError) as exc: + tree.remove_child(child) + assert exc.value.args[0] == "Removal couldn't find item in tree" + + +def test_remove_child_found_in_tree(): + writer = PdfWriter() + + class ChildDummy: + def __init__(self, parent): + self.parent = parent + + def get_object(self): + tree = DictionaryObject() + tree[NameObject("/Parent")] = self.parent + return tree + + tree = TreeObject() + child = DictionaryObject() + writer._add_object(tree) + + child_ref = writer._add_object(child) + tree.add_child(child_ref, writer) + assert tree[NameObject("/Count")] == 1 + + child2 = TreeObject() + child2_ref = writer._add_object(child2) + tree.add_child(child2_ref, writer) + assert tree[NameObject("/Count")] == 2 + tree.remove_child(child2) + + def test_remove_child_in_tree(): pdf = RESOURCE_ROOT / "form.pdf" From dc84a42b2fee59978c531d58663fbbbb63df4d86 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 14 Aug 2022 13:56:02 +0200 Subject: [PATCH 099/130] BUG: TreeObject.remove_child had an assignment issue for Count (#1234) --- PyPDF2/generic/_data_structures.py | 87 +++++++++++++++++------------- tests/test_generic.py | 36 ++++++++----- 2 files changed, 75 insertions(+), 48 deletions(-) diff --git a/PyPDF2/generic/_data_structures.py b/PyPDF2/generic/_data_structures.py index 3f910f521..fae74f997 100644 --- a/PyPDF2/generic/_data_structures.py +++ b/PyPDF2/generic/_data_structures.py @@ -406,6 +406,41 @@ def removeChild(self, child: Any) -> None: # pragma: no cover deprecate_with_replacement("removeChild", "remove_child") self.remove_child(child) + def _remove_node_from_tree( + self, prev: Any, prev_ref: Any, cur: Any, last: Any + ) -> None: + """Adjust the pointers of the linked list and tree node count.""" + next_ref = cur.get(NameObject("/Next"), None) + if prev is None: + if next_ref: + # Removing first tree node + next_obj = next_ref.get_object() + del next_obj[NameObject("/Prev")] + self[NameObject("/First")] = next_ref + self[NameObject("/Count")] = NumberObject( + self[NameObject("/Count")] - 1 # type: ignore + ) + + else: + # Removing only tree node + assert self[NameObject("/Count")] == 1 + del self[NameObject("/Count")] + del self[NameObject("/First")] + if NameObject("/Last") in self: + del self[NameObject("/Last")] + else: + if next_ref: + # Removing middle tree node + next_obj = next_ref.get_object() + next_obj[NameObject("/Prev")] = prev_ref + prev[NameObject("/Next")] = next_ref + else: + # Removing last tree node + assert cur == last + del prev[NameObject("/Next")] + self[NameObject("/Last")] = prev_ref + self[NameObject("/Count")] = NumberObject(self[NameObject("/Count")] - 1) # type: ignore + def remove_child(self, child: Any) -> None: child_obj = child.get_object() @@ -423,40 +458,11 @@ def remove_child(self, child: Any) -> None: last = last_ref.get_object() while cur is not None: if cur == child_obj: - if prev is None: - if NameObject("/Next") in cur: - # Removing first tree node - next_ref = cur[NameObject("/Next")] - next_obj = next_ref.get_object() - del next_obj[NameObject("/Prev")] - self[NameObject("/First")] = next_ref - self[NameObject("/Count")] -= 1 # type: ignore - - else: - # Removing only tree node - assert self[NameObject("/Count")] == 1 - del self[NameObject("/Count")] - del self[NameObject("/First")] - if NameObject("/Last") in self: - del self[NameObject("/Last")] - else: - if NameObject("/Next") in cur: - # Removing middle tree node - next_ref = cur[NameObject("/Next")] - next_obj = next_ref.get_object() - next_obj[NameObject("/Prev")] = prev_ref - prev[NameObject("/Next")] = next_ref - else: - # Removing last tree node - assert cur == last - del prev[NameObject("/Next")] - self[NameObject("/Last")] = prev_ref - self[NameObject("/Count")] = NumberObject( - self[NameObject("/Count")] - 1 - ) + self._remove_node_from_tree(prev, prev_ref, cur, last) found = True break + # Go to the next node prev_ref = cur_ref prev = cur if NameObject("/Next") in cur: @@ -469,11 +475,7 @@ def remove_child(self, child: Any) -> None: if not found: raise ValueError("Removal couldn't find item in tree") - del child_obj[NameObject("/Parent")] - if NameObject("/Next") in child_obj: - del child_obj[NameObject("/Next")] - if NameObject("/Prev") in child_obj: - del child_obj[NameObject("/Prev")] + _reset_node_tree_relationship(child_obj) def emptyTree(self) -> None: # pragma: no cover deprecate_with_replacement("emptyTree", "empty_tree", "4.0.0") @@ -496,6 +498,19 @@ def empty_tree(self) -> None: del self[NameObject("/Last")] +def _reset_node_tree_relationship(child_obj: Any) -> None: + """ + Call this after a node has been removed from a tree. + + This resets the nodes attributes in respect to that tree. + """ + del child_obj[NameObject("/Parent")] + if NameObject("/Next") in child_obj: + del child_obj[NameObject("/Next")] + if NameObject("/Prev") in child_obj: + del child_obj[NameObject("/Prev")] + + class StreamObject(DictionaryObject): def __init__(self) -> None: self.__data: Optional[str] = None diff --git a/tests/test_generic.py b/tests/test_generic.py index aa0124cc5..7f981fe4f 100644 --- a/tests/test_generic.py +++ b/tests/test_generic.py @@ -438,28 +438,40 @@ def get_object(self): def test_remove_child_found_in_tree(): writer = PdfWriter() - class ChildDummy: - def __init__(self, parent): - self.parent = parent - - def get_object(self): - tree = DictionaryObject() - tree[NameObject("/Parent")] = self.parent - return tree - + # Add Tree tree = TreeObject() - child = DictionaryObject() writer._add_object(tree) - child_ref = writer._add_object(child) - tree.add_child(child_ref, writer) + # Add first child + # It's important to set a value, otherwise the writer.get_reference will + # return the same object when a second child is added. + child1 = TreeObject() + child1[NameObject("/Foo")] = TextStringObject("bar") + child1_ref = writer._add_object(child1) + tree.add_child(child1_ref, writer) assert tree[NameObject("/Count")] == 1 + # Add second child child2 = TreeObject() + child2[NameObject("/Foo")] = TextStringObject("baz") child2_ref = writer._add_object(child2) tree.add_child(child2_ref, writer) assert tree[NameObject("/Count")] == 2 + + # Remove child tree.remove_child(child2) + assert tree[NameObject("/Count")] == 1 + + # Add new child + child3 = TreeObject() + child3_ref = writer._add_object(child3) + tree.add_child(child3_ref, writer) + assert tree[NameObject("/Count")] == 2 + + # Remove child + child1 = tree[NameObject("/First")] + tree.remove_child(child1) + assert tree[NameObject("/Count")] == 1 def test_remove_child_in_tree(): From fd0c802bb35996591b55fd0ed0ca4d239190a59c Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 14 Aug 2022 14:14:31 +0200 Subject: [PATCH 100/130] TST: TreeObject.remove_child for middle node (#1235) --- tests/test_generic.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/tests/test_generic.py b/tests/test_generic.py index 7f981fe4f..f94954914 100644 --- a/tests/test_generic.py +++ b/tests/test_generic.py @@ -458,21 +458,38 @@ def test_remove_child_found_in_tree(): tree.add_child(child2_ref, writer) assert tree[NameObject("/Count")] == 2 - # Remove child + # Remove last child tree.remove_child(child2) assert tree[NameObject("/Count")] == 1 # Add new child child3 = TreeObject() + child3[NameObject("/Foo")] = TextStringObject("3") child3_ref = writer._add_object(child3) tree.add_child(child3_ref, writer) assert tree[NameObject("/Count")] == 2 - # Remove child + # Remove first child child1 = tree[NameObject("/First")] tree.remove_child(child1) assert tree[NameObject("/Count")] == 1 + child4 = TreeObject() + child4[NameObject("/Foo")] = TextStringObject("4") + child4_ref = writer._add_object(child4) + tree.add_child(child4_ref, writer) + assert tree[NameObject("/Count")] == 2 + + child5 = TreeObject() + child5[NameObject("/Foo")] = TextStringObject("5") + child5_ref = writer._add_object(child5) + tree.add_child(child5_ref, writer) + assert tree[NameObject("/Count")] == 3 + + # Remove middle child + tree.remove_child(child4) + assert tree[NameObject("/Count")] == 2 + def test_remove_child_in_tree(): pdf = RESOURCE_ROOT / "form.pdf" From b4852391e3ff5b4b1803c516971b38c7bf3234d5 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 14 Aug 2022 18:06:26 +0200 Subject: [PATCH 101/130] TST: TreeObject.empty_tree() (#1236) --- PyPDF2/generic/_data_structures.py | 20 ++++++++++---------- tests/test_generic.py | 16 +++++++++++++++- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/PyPDF2/generic/_data_structures.py b/PyPDF2/generic/_data_structures.py index fae74f997..85c4e8819 100644 --- a/PyPDF2/generic/_data_structures.py +++ b/PyPDF2/generic/_data_structures.py @@ -359,16 +359,20 @@ def has_children(self) -> bool: def __iter__(self) -> Any: return self.children() - def children(self) -> Optional[Any]: + def children(self) -> Iterable[Any]: if not self.has_children(): return - child = self["/First"] + child_ref = self[NameObject("/First")] + child = child_ref.get_object() while True: yield child - if child == self["/Last"]: + if child == self[NameObject("/Last")]: return - child = child["/Next"] # type: ignore + child_ref = child.get(NameObject("/Next")) # type: ignore + if child_ref is None: + return + child = child_ref.get_object() def addChild(self, child: Any, pdf: Any) -> None: # pragma: no cover deprecate_with_replacement("addChild", "add_child") @@ -484,11 +488,7 @@ def emptyTree(self) -> None: # pragma: no cover def empty_tree(self) -> None: for child in self: child_obj = child.get_object() - del child_obj[NameObject("/Parent")] - if NameObject("/Next") in child_obj: - del child_obj[NameObject("/Next")] - if NameObject("/Prev") in child_obj: - del child_obj[NameObject("/Prev")] + _reset_node_tree_relationship(child_obj) if NameObject("/Count") in self: del self[NameObject("/Count")] @@ -650,7 +650,7 @@ def getData(self) -> Union[None, str, bytes]: # pragma: no cover deprecate_with_replacement("getData", "get_data") return self.get_data() - def set_data(self, data: Any) -> None: + def set_data(self, data: Any) -> None: # pragma: no cover raise PdfReadError("Creating EncodedStreamObject is not currently supported") def setData(self, data: Any) -> None: # pragma: no cover diff --git a/tests/test_generic.py b/tests/test_generic.py index f94954914..13a0c6b6c 100644 --- a/tests/test_generic.py +++ b/tests/test_generic.py @@ -196,12 +196,16 @@ def test_destination_fit_r(): def test_destination_fit_v(): Destination(NameObject("title"), NullObject(), NameObject(TF.FIT_V), FloatObject(0)) + # Trigger Exception + Destination(NameObject("title"), NullObject(), NameObject(TF.FIT_V), None) + def test_destination_exception(): - with pytest.raises(PdfReadError): + with pytest.raises(PdfReadError) as exc: Destination( NameObject("title"), NullObject(), NameObject("foo"), FloatObject(0) ) + assert exc.value.args[0] == "Unknown Destination Type: 'foo'" def test_outline_item_write_to_stream(): @@ -450,6 +454,7 @@ def test_remove_child_found_in_tree(): child1_ref = writer._add_object(child1) tree.add_child(child1_ref, writer) assert tree[NameObject("/Count")] == 1 + assert len([el for el in tree.children()]) == 1 # Add second child child2 = TreeObject() @@ -457,10 +462,12 @@ def test_remove_child_found_in_tree(): child2_ref = writer._add_object(child2) tree.add_child(child2_ref, writer) assert tree[NameObject("/Count")] == 2 + assert len([el for el in tree.children()]) == 2 # Remove last child tree.remove_child(child2) assert tree[NameObject("/Count")] == 1 + assert len([el for el in tree.children()]) == 1 # Add new child child3 = TreeObject() @@ -468,27 +475,34 @@ def test_remove_child_found_in_tree(): child3_ref = writer._add_object(child3) tree.add_child(child3_ref, writer) assert tree[NameObject("/Count")] == 2 + assert len([el for el in tree.children()]) == 2 # Remove first child child1 = tree[NameObject("/First")] tree.remove_child(child1) assert tree[NameObject("/Count")] == 1 + assert len([el for el in tree.children()]) == 1 child4 = TreeObject() child4[NameObject("/Foo")] = TextStringObject("4") child4_ref = writer._add_object(child4) tree.add_child(child4_ref, writer) assert tree[NameObject("/Count")] == 2 + assert len([el for el in tree.children()]) == 2 child5 = TreeObject() child5[NameObject("/Foo")] = TextStringObject("5") child5_ref = writer._add_object(child5) tree.add_child(child5_ref, writer) assert tree[NameObject("/Count")] == 3 + assert len([el for el in tree.children()]) == 3 # Remove middle child tree.remove_child(child4) assert tree[NameObject("/Count")] == 2 + assert len([el for el in tree.children()]) == 2 + + tree.empty_tree() def test_remove_child_in_tree(): From 3285673bf23247ff6b9184b2ec7189c708e3907f Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 14 Aug 2022 22:09:46 +0200 Subject: [PATCH 102/130] TST: PdfWriter (#1237) --- PyPDF2/_writer.py | 4 ++-- tests/test_writer.py | 20 +++++++++++++++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/PyPDF2/_writer.py b/PyPDF2/_writer.py index bfebf95df..a56d4a8fb 100644 --- a/PyPDF2/_writer.py +++ b/PyPDF2/_writer.py @@ -1619,7 +1619,7 @@ def add_link( ) return self.add_annotation(page_number=pagenum, annotation=annotation) - def addLink( # pragma: no cover + def addLink( self, pagenum: int, pagedest: int, @@ -1627,7 +1627,7 @@ def addLink( # pragma: no cover border: Optional[ArrayObject] = None, fit: FitType = "/Fit", *args: ZoomArgType, - ) -> None: + ) -> None: # pragma: no cover """ .. deprecated:: 1.28.0 diff --git a/tests/test_writer.py b/tests/test_writer.py index d2c7b6d66..d90c8bd45 100644 --- a/tests/test_writer.py +++ b/tests/test_writer.py @@ -91,6 +91,7 @@ def writer_operate(writer): with pytest.warns(PendingDeprecationWarning): writer.add_link(2, 1, RectangleObject([0, 0, 100, 100])) assert writer._get_page_layout() is None + writer._set_page_layout("broken") writer._set_page_layout("/SinglePage") assert writer._get_page_layout() == "/SinglePage" assert writer._get_page_mode() is None @@ -373,6 +374,10 @@ def test_fill_form(): writer.pages[0], {"foo": "some filled in text"}, flags=1 ) + writer.update_page_form_field_values( + writer.pages[0], {"foo": "some filled in text"} + ) + # write "output" to PyPDF2-output.pdf tmp_filename = "dont_commit_filled_pdf.pdf" with open(tmp_filename, "wb") as output_stream: @@ -445,16 +450,19 @@ def test_add_outline_item(): def test_add_named_destination(): reader = PdfReader(RESOURCE_ROOT / "pdflatex-outline.pdf") writer = PdfWriter() + assert writer.get_named_dest_root() == [] for page in reader.pages: writer.add_page(page) + assert writer.get_named_dest_root() == [] + writer.add_named_destination(NameObject("A named dest"), 2) writer.add_named_destination(NameObject("A named dest2"), 2) assert writer.get_named_dest_root() == [ "A named dest", - IndirectObject(7, 0, writer), + IndirectObject(9, 0, writer), "A named dest2", IndirectObject(10, 0, writer), ] @@ -595,6 +603,16 @@ def test_issue301(): writer.write(o) +def test_append_pages_from_reader_append(): + """use append_pages_from_reader with a callable""" + with open(RESOURCE_ROOT / "issue-301.pdf", "rb") as f: + reader = PdfReader(f) + writer = PdfWriter() + writer.append_pages_from_reader(reader, callable) + o = BytesIO() + writer.write(o) + + def test_sweep_indirect_references_nullobject_exception(): # TODO: Check this more closely... this looks weird url = "https://corpora.tika.apache.org/base/docs/govdocs1/924/924666.pdf" From eee49b98e365484529479e88013a91b09d6642c1 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Mon, 15 Aug 2022 12:28:46 +0200 Subject: [PATCH 103/130] TST: AlgV5.generate_values (#1238) --- tests/test_encryption.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/test_encryption.py b/tests/test_encryption.py index d6393fcb5..530870c49 100644 --- a/tests/test_encryption.py +++ b/tests/test_encryption.py @@ -4,7 +4,7 @@ import PyPDF2 from PyPDF2 import PasswordType, PdfReader -from PyPDF2._encryption import CryptRC4 +from PyPDF2._encryption import AlgV5, CryptRC4 from PyPDF2.errors import DependencyError, PdfReadError try: @@ -159,3 +159,24 @@ def test_decrypt_not_decrypted_pdf(): with pytest.raises(PdfReadError) as exc: PdfReader(path, password="nonexistant") assert exc.value.args[0] == "Not encrypted file" + + +def test_generate_values(): + """ + This test only checks if there is an exception. + + It does not verify that the content is correct. + """ + if not HAS_PYCRYPTODOME: + return + key = b"0123456789123451" + values = AlgV5.generate_values( + user_pwd=b"foo", owner_pwd=b"bar", key=key, p=0, metadata_encrypted=True + ) + assert values == { + "/U": values["/U"], + "/UE": values["/UE"], + "/O": values["/O"], + "/OE": values["/OE"], + "/Perms": values["/Perms"], + } From 5713f5080d94764fbb891e5c333d747d69f01468 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Mon, 15 Aug 2022 13:42:28 +0200 Subject: [PATCH 104/130] TST: Decrypt AlgV4 with owner password (#1239) --- resources/encryption/r2-owner-password.pdf | Bin 0 -> 15448 bytes resources/encryption/r4-owner-password.pdf | Bin 0 -> 15540 bytes tests/test_encryption.py | 28 ++++++++++++--------- 3 files changed, 16 insertions(+), 12 deletions(-) create mode 100644 resources/encryption/r2-owner-password.pdf create mode 100644 resources/encryption/r4-owner-password.pdf diff --git a/resources/encryption/r2-owner-password.pdf b/resources/encryption/r2-owner-password.pdf new file mode 100644 index 0000000000000000000000000000000000000000..121c61ed7a5c2922738afa8101e832e47f72f642 GIT binary patch literal 15448 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 Jq?DyG{y)nU>>dCB literal 0 HcmV?d00001 diff --git a/tests/test_encryption.py b/tests/test_encryption.py index 530870c49..b59b204cd 100644 --- a/tests/test_encryption.py +++ b/tests/test_encryption.py @@ -24,29 +24,33 @@ [ # unencrypted pdf ("unencrypted.pdf", False), - # created by `qpdf --encrypt "" "" 40 -- unencrypted.pdf r2-empty-password.pdf` + # created by `qpdf --encrypt "" "" 40 -- unencrypted.pdf r2-empty-password.pdf`: ("r2-empty-password.pdf", False), - # created by `qpdf --encrypt "" "" 128 -- unencrypted.pdf r3-empty-password.pdf` + # created by `qpdf --encrypt "" "" 128 -- unencrypted.pdf r3-empty-password.pdf`: ("r3-empty-password.pdf", False), - # created by `qpdf --encrypt "asdfzxcv" "" 40 -- unencrypted.pdf r2-user-password.pdf` + # created by `qpdf --encrypt "asdfzxcv" "" 40 -- unencrypted.pdf r2-user-password.pdf`: ("r2-user-password.pdf", False), - # created by `qpdf --encrypt "asdfzxcv" "" 128 -- unencrypted.pdf r3-user-password.pdf` + # created by `qpdf --encrypt "" "asdfzxcv" 40 -- unencrypted.pdf r2-user-password.pdf`: + ("r2-owner-password.pdf", False), + # created by `qpdf --encrypt "asdfzxcv" "" 128 -- unencrypted.pdf r3-user-password.pdf`: ("r3-user-password.pdf", False), - # created by `qpdf --encrypt "asdfzxcv" "" 128 --force-V4 -- unencrypted.pdf r4-user-password.pdf` + # created by `qpdf --encrypt "asdfzxcv" "" 128 --force-V4 -- unencrypted.pdf r4-user-password.pdf`: ("r4-user-password.pdf", False), - # created by `qpdf --encrypt "asdfzxcv" "" 128 --use-aes=y -- unencrypted.pdf r4-aes-user-password.pdf` + # created by `qpdf --encrypt "" "asdfzxcv" 128 --force-V4 -- unencrypted.pdf r4-owner-password.pdf`: + ("r4-owner-password.pdf", False), + # created by `qpdf --encrypt "asdfzxcv" "" 128 --use-aes=y -- unencrypted.pdf r4-aes-user-password.pdf`: ("r4-aes-user-password.pdf", True), - # # created by `qpdf --encrypt "" "" 256 --force-R5 -- unencrypted.pdf r5-empty-password.pdf` + # # created by `qpdf --encrypt "" "" 256 --force-R5 -- unencrypted.pdf r5-empty-password.pdf`: ("r5-empty-password.pdf", True), - # # created by `qpdf --encrypt "asdfzxcv" "" 256 --force-R5 -- unencrypted.pdf r5-user-password.pdf` + # # created by `qpdf --encrypt "asdfzxcv" "" 256 --force-R5 -- unencrypted.pdf r5-user-password.pdf`: ("r5-user-password.pdf", True), - # # created by `qpdf --encrypt "" "asdfzxcv" 256 --force-R5 -- unencrypted.pdf r5-owner-password.pdf` + # # created by `qpdf --encrypt "" "asdfzxcv" 256 --force-R5 -- unencrypted.pdf r5-owner-password.pdf`: ("r5-owner-password.pdf", True), - # created by `qpdf --encrypt "" "" 256 -- unencrypted.pdf r6-empty-password.pdf` + # created by `qpdf --encrypt "" "" 256 -- unencrypted.pdf r6-empty-password.pdf`: ("r6-empty-password.pdf", True), - # created by `qpdf --encrypt "asdfzxcv" "" 256 -- unencrypted.pdf r6-user-password.pdf` + # created by `qpdf --encrypt "asdfzxcv" "" 256 -- unencrypted.pdf r6-user-password.pdf`: ("r6-user-password.pdf", True), - # created by `qpdf --encrypt "" "asdfzxcv" 256 -- unencrypted.pdf r6-owner-password.pdf` + # created by `qpdf --encrypt "" "asdfzxcv" 256 -- unencrypted.pdf r6-owner-password.pdf`: ("r6-owner-password.pdf", True), ], ) From 1423c0d76d40490e0074a1836cb5a7b06934fbcb Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Mon, 15 Aug 2022 13:47:39 +0200 Subject: [PATCH 105/130] REL: 2.10.1 Bug Fixes (BUG): - TreeObject.remove_child had a non-PdfObject assignment for Count (#1233, #1234) - Fix stream truncated prematurely (#1223) Documentation (DOC): - Fix docstring formatting (#1228) Maintenance (MAINT): - Split generic.py (#1229) Testing (TST): - Decrypt AlgV4 with owner password (#1239) - AlgV5.generate_values (#1238) - TreeObject.remove_child / empty_tree (#1235, #1236) - create_string_object (#1232) - Free-Text annotations (#1231) - generic._base (#1230) - Strict get fonts (#1226) - Increase PdfReader coverage (#1219, #1225) - Increase PdfWriter coverage (#1237) - 100% coverage for utils.py (#1217) - Writer exception non-binary stream (#1218) - Don't check coverage for deprecated code (#1216) Full Changelog: https://github.com/py-pdf/PyPDF2/compare/2.10.0...2.10.1 --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ PyPDF2/_version.py | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e1f756ad..83351b915 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # CHANGELOG +## Version 2.10.1, 2022-08-15 + +### Bug Fixes (BUG) +- TreeObject.remove_child had a non-PdfObject assignment for Count (#1233, #1234) +- Fix stream truncated prematurely (#1223) + +### Documentation (DOC) +- Fix docstring formatting (#1228) + +### Maintenance (MAINT) +- Split generic.py (#1229) + +### Testing (TST) +- Decrypt AlgV4 with owner password (#1239) +- AlgV5.generate_values (#1238) +- TreeObject.remove_child / empty_tree (#1235, #1236) +- create_string_object (#1232) +- Free-Text annotations (#1231) +- generic._base (#1230) +- Strict get fonts (#1226) +- Increase PdfReader coverage (#1219, #1225) +- Increase PdfWriter coverage (#1237) +- 100% coverage for utils.py (#1217) +- PdfWriter exception non-binary stream (#1218) +- Don't check coverage for deprecated code (#1216) + +Full Changelog: https://github.com/py-pdf/PyPDF2/compare/2.10.0...2.10.1 + ## Version 2.10.0, 2022-08-07 diff --git a/PyPDF2/_version.py b/PyPDF2/_version.py index 1c622223b..565443f86 100644 --- a/PyPDF2/_version.py +++ b/PyPDF2/_version.py @@ -1 +1 @@ -__version__ = "2.10.0" +__version__ = "2.10.1" From 870198d52271fc12eeefe149f1a9ebab1dd5883a Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Mon, 15 Aug 2022 16:16:16 +0200 Subject: [PATCH 106/130] BUG: Add PyPDF2.generic to distribution Closes #1243 --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index b43b87b97..ced33099b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,6 +35,7 @@ classifiers = packages = PyPDF2 PyPDF2._codecs + PyPDF2.generic python_requires = >=3.6 install_requires = typing_extensions; python_version < '3.10' From b50c3a82f330a9b21b575131537fa9ac55a21b2b Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Mon, 15 Aug 2022 16:21:20 +0200 Subject: [PATCH 107/130] REL: 2.10.2 BUG: Add PyPDF2.generic to PyPI distribution --- CHANGELOG.md | 4 ++++ PyPDF2/_version.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83351b915..ec85f3bf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +## Version 2.10.2, 2022-08-15 + +BUG: Add PyPDF2.generic to PyPI distribution + ## Version 2.10.1, 2022-08-15 ### Bug Fixes (BUG) diff --git a/PyPDF2/_version.py b/PyPDF2/_version.py index 565443f86..6c96c9755 100644 --- a/PyPDF2/_version.py +++ b/PyPDF2/_version.py @@ -1 +1 @@ -__version__ = "2.10.1" +__version__ = "2.10.2" From 917ff2ecbf26660520d7385d55ae68ac3f1b0c4d Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Mon, 15 Aug 2022 16:53:28 +0200 Subject: [PATCH 108/130] DOC: Adding WevertonGomes as a Contributor Thank you for reporting the issue so quickly! --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 23f1ee1a7..50eb2540b 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -18,6 +18,7 @@ history and [GitHubs 'Contributors' feature](https://github.com/py-pdf/PyPDF2/gr * [Pinheiro, Arthur](https://github.com/xilopaint) * [pubpub-zz](https://github.com/pubpub-zz): involved in community development * [Thoma, Martin](https://github.com/MartinThoma): Maintainer of PyPDF2 since April 2022. I hope to build a great community with many awesome contributors. [LinkedIn](https://www.linkedin.com/in/martin-thoma/) | [StackOverflow](https://stackoverflow.com/users/562769/martin-thoma) | [Blog](https://martin-thoma.com/) +* [WevertonGomes](https://github.com/WevertonGomesCosta) * ztravis ## Adding a new contributor From 28cf36aa9546787789b2c0b59f947bc2594e50be Mon Sep 17 00:00:00 2001 From: Matthew Peveler Date: Wed, 17 Aug 2022 19:27:16 +0000 Subject: [PATCH 109/130] DEV: Modify CI to better verify built package contents (#1244) PR modifies the package CI job in two ways: 1. Pass package to check-wheel-contents. This makes it so that check-wheel-contents verifies that each file in the package are actually in the wheel following their directory structure. 2. Have CI steps that verify we can install the package, and that we can run a minimal example with it Either of these steps would have been sufficient to have caught #1242 per the example runs above. Signed-off-by: Matthew Peveler --- .github/workflows/github-ci.yaml | 7 +++++++ setup.cfg | 3 +++ 2 files changed, 10 insertions(+) diff --git a/.github/workflows/github-ci.yaml b/.github/workflows/github-ci.yaml index 67fb0e1a3..8b1583ba8 100644 --- a/.github/workflows/github-ci.yaml +++ b/.github/workflows/github-ci.yaml @@ -88,6 +88,13 @@ jobs: - run: check-wheel-contents dist/*.whl - name: Check long_description run: python -m twine check dist/* + + - name: Test installing package + run: python -m pip install . + + - name: Test running installed package + working-directory: /tmp + run: python -c "import PyPDF2;print(PyPDF2.__version__)" # - name: Release to pypi if tagged. # if: startsWith(github.ref, 'refs/tags') diff --git a/setup.cfg b/setup.cfg index ced33099b..bded39e8a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -47,3 +47,6 @@ crypto = PyCryptodome backup = False runner = ./mutmut-test.sh tests_dir = tests/ + +[tool:check-wheel-contents] +package = ./PyPDF2 From cb6c2247566cb42596de2dbbc05cae202242336f Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Wed, 17 Aug 2022 22:16:18 +0200 Subject: [PATCH 110/130] TST: Various PdfWriter (Layout, Bookmark deprecation) (#1249) --- PyPDF2/_writer.py | 2 +- tests/test_generic.py | 12 ++++++++++++ tests/test_writer.py | 5 +++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/PyPDF2/_writer.py b/PyPDF2/_writer.py index a56d4a8fb..a8ee9232d 100644 --- a/PyPDF2/_writer.py +++ b/PyPDF2/_writer.py @@ -1259,7 +1259,7 @@ def add_bookmark( italic: bool = False, fit: FitType = "/Fit", *args: ZoomArgType, - ) -> IndirectObject: + ) -> IndirectObject: # pragma: no cover """ .. deprecated:: 2.9.0 diff --git a/tests/test_generic.py b/tests/test_generic.py index 13a0c6b6c..6a82df22c 100644 --- a/tests/test_generic.py +++ b/tests/test_generic.py @@ -608,6 +608,18 @@ def test_issue_997(mock_logger_warning): "Overwriting cache for 0 4", "PyPDF2._reader" ) + # Strict + merger = PdfMerger(strict=True) + merged_filename = "tmp-out.pdf" + with pytest.raises(PdfReadError) as exc: + merger.append( + BytesIO(get_pdf_from_url(url, name=name)) + ) # here the error raises + assert exc.value.args[0] == "Could not find object." + with open(merged_filename, "wb") as f: + merger.write(f) + merger.close() + # cleanup os.remove(merged_filename) diff --git a/tests/test_writer.py b/tests/test_writer.py index d90c8bd45..0b7fa8b89 100644 --- a/tests/test_writer.py +++ b/tests/test_writer.py @@ -91,8 +91,9 @@ def writer_operate(writer): with pytest.warns(PendingDeprecationWarning): writer.add_link(2, 1, RectangleObject([0, 0, 100, 100])) assert writer._get_page_layout() is None - writer._set_page_layout("broken") - writer._set_page_layout("/SinglePage") + writer.page_layout = "broken" + assert writer.page_layout == "broken" + writer.page_layout = NameObject("/SinglePage") assert writer._get_page_layout() == "/SinglePage" assert writer._get_page_mode() is None writer.set_page_mode("/UseNone") From 52463ea626dba78dbcfab17074b5dc941f1319f5 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Fri, 19 Aug 2022 19:25:30 +0200 Subject: [PATCH 111/130] MAINT: Remove unreachable code in read_block_backwards (#1250) Co-authored-by: Matthew Peveler --- PyPDF2/_utils.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/PyPDF2/_utils.py b/PyPDF2/_utils.py index 53da3b82c..eeceda1b4 100644 --- a/PyPDF2/_utils.py +++ b/PyPDF2/_utils.py @@ -166,9 +166,11 @@ def read_until_regex( def read_block_backwards(stream: StreamType, to_read: int) -> bytes: - """Given a stream at position X, read a block of size - to_read ending at position X. - The stream's position should be unchanged. + """ + Given a stream at position X, read a block of size to_read ending at position X. + + This changes the stream's position to the beginning of where the block was + read. """ if stream.tell() < to_read: raise PdfStreamError("Could not read malformed PDF file") @@ -177,8 +179,6 @@ def read_block_backwards(stream: StreamType, to_read: int) -> bytes: read = stream.read(to_read) # Seek to the start of the block we read after reading it. stream.seek(-to_read, SEEK_CUR) - if len(read) != to_read: - raise PdfStreamError(f"EOF: read {len(read)}, expected {to_read}?") return read From c188fb023762950075efe5a220e465018af0f16d Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sat, 20 Aug 2022 13:20:04 +0200 Subject: [PATCH 112/130] TST: PdfReader.xmp_metadata workflow (#1257) --- tests/test_workflows.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/test_workflows.py b/tests/test_workflows.py index 19cfd1563..672ba90b3 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -1,3 +1,9 @@ +""" +Tests in this module behave like user code. + +They don't mock/patch anything, they cover typical user needs. +""" + import binascii import os import sys @@ -757,3 +763,19 @@ def test_get_fonts(url, name, strict): reader = PdfReader(data, strict=strict) for page in reader.pages: page._get_fonts() + + +@pytest.mark.parametrize( + ("url", "name", "strict"), + [ + ( + "https://corpora.tika.apache.org/base/docs/govdocs1/942/942303.pdf", + "tika-942303.pdf", + True, + ), + ], +) +def test_get_xmp(url, name, strict): + data = BytesIO(get_pdf_from_url(url, name=name)) + reader = PdfReader(data, strict=strict) + reader.xmp_metadata From 7f0a6b01b271b415188934003e383641aa308481 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sat, 20 Aug 2022 17:10:46 +0200 Subject: [PATCH 113/130] MAINT: password param of _security._alg32(...) is only a string, not bytes (#1259) Adjust type annotations --- PyPDF2/_security.py | 10 ++++------ tests/test_encryption.py | 1 + tests/test_reader.py | 1 + tests/test_writer.py | 24 ++++++++++++++++++------ 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/PyPDF2/_security.py b/PyPDF2/_security.py index 28ddad8c6..1fc6d1e59 100644 --- a/PyPDF2/_security.py +++ b/PyPDF2/_security.py @@ -52,7 +52,7 @@ def _alg32( - password: Union[str, bytes], + password: str, rev: Literal[2, 3, 4], keylen: int, owner_entry: ByteStringObject, @@ -135,13 +135,11 @@ def _alg33(owner_pwd: str, user_pwd: str, rev: Literal[2, 3, 4], keylen: int) -> return val -def _alg33_1(password: Union[bytes, str], rev: Literal[2, 3, 4], keylen: int) -> bytes: +def _alg33_1(password: str, rev: Literal[2, 3, 4], keylen: int) -> bytes: """Steps 1-4 of algorithm 3.3""" # 1. Pad or truncate the owner password string as described in step 1 of # algorithm 3.2. If there is no owner password, use the user password # instead. - if isinstance(password, bytes): - password = password.decode() password_bytes = b_((password + str_(_encryption_padding))[:32]) # 2. Initialize the MD5 hash function and pass the result of step 1 as # input to this function. @@ -161,7 +159,7 @@ def _alg33_1(password: Union[bytes, str], rev: Literal[2, 3, 4], keylen: int) -> def _alg34( - password: Union[str, bytes], + password: str, owner_entry: ByteStringObject, p_entry: int, id1_entry: ByteStringObject, @@ -186,7 +184,7 @@ def _alg34( def _alg35( - password: Union[str, bytes], + password: str, rev: Literal[2, 3, 4], keylen: int, owner_entry: ByteStringObject, diff --git a/tests/test_encryption.py b/tests/test_encryption.py index b59b204cd..ff24dc420 100644 --- a/tests/test_encryption.py +++ b/tests/test_encryption.py @@ -105,6 +105,7 @@ def test_both_password(name, user_passwd, owner_passwd): ("pdffile", "password"), [ ("crazyones-encrypted-256.pdf", "password"), + ("crazyones-encrypted-256.pdf", b"password"), ], ) @pytest.mark.skipif(not HAS_PYCRYPTODOME, reason="No pycryptodome") diff --git a/tests/test_reader.py b/tests/test_reader.py index 588d6fc7d..12f956d0a 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -308,6 +308,7 @@ def test_issue297(caplog): ("pdffile", "password", "should_fail"), [ ("encrypted-file.pdf", "test", False), + ("encrypted-file.pdf", b"test", False), ("encrypted-file.pdf", "qwerty", True), ("encrypted-file.pdf", b"qwerty", True), ], diff --git a/tests/test_writer.py b/tests/test_writer.py index 0b7fa8b89..d83db2002 100644 --- a/tests/test_writer.py +++ b/tests/test_writer.py @@ -386,10 +386,10 @@ def test_fill_form(): @pytest.mark.parametrize( - "use_128bit", - [True, False], + ("use_128bit", "user_pwd", "owner_pwd"), + [(True, "userpwd", "ownerpwd"), (False, "userpwd", "ownerpwd")], ) -def test_encrypt(use_128bit): +def test_encrypt(use_128bit, user_pwd, owner_pwd): reader = PdfReader(RESOURCE_ROOT / "form.pdf") writer = PdfWriter() @@ -397,7 +397,7 @@ def test_encrypt(use_128bit): orig_text = page.extract_text() writer.add_page(page) - writer.encrypt(user_pwd="userpwd", owner_pwd="ownerpwd", use_128bit=use_128bit) + writer.encrypt(user_pwd=user_pwd, owner_pwd=owner_pwd, use_128bit=use_128bit) # write "output" to PyPDF2-output.pdf tmp_filename = "dont_commit_encrypted.pdf" @@ -409,18 +409,30 @@ def test_encrypt(use_128bit): data = input_stream.read() assert b"foo" not in data - # Test the user password: + # Test the user password (str): reader = PdfReader(tmp_filename, password="userpwd") new_text = reader.pages[0].extract_text() assert reader.metadata.get("/Producer") == "PyPDF2" assert new_text == orig_text - # Test the owner password: + # Test the owner password (str): reader = PdfReader(tmp_filename, password="ownerpwd") new_text = reader.pages[0].extract_text() assert reader.metadata.get("/Producer") == "PyPDF2" assert new_text == orig_text + # Test the user password (bytes): + reader = PdfReader(tmp_filename, password=b"userpwd") + new_text = reader.pages[0].extract_text() + assert reader.metadata.get("/Producer") == "PyPDF2" + assert new_text == orig_text + + # Test the owner password (stbytesr): + reader = PdfReader(tmp_filename, password=b"ownerpwd") + new_text = reader.pages[0].extract_text() + assert reader.metadata.get("/Producer") == "PyPDF2" + assert new_text == orig_text + # Cleanup os.remove(tmp_filename) From 0983fe4b198eb121799a9f403bf3f6ff5604e9dd Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sat, 20 Aug 2022 19:16:12 +0200 Subject: [PATCH 114/130] MAINT: Let PdfMerger._create_stream raise NotImplemented (#1251) ... if arg is none of str/Path/stream/PdfReader --- PyPDF2/_merger.py | 7 ++++++- tests/test_merger.py | 8 ++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/PyPDF2/_merger.py b/PyPDF2/_merger.py index 3f0b9738a..aff717b68 100644 --- a/PyPDF2/_merger.py +++ b/PyPDF2/_merger.py @@ -250,7 +250,12 @@ def _create_stream( stream = BytesIO(filecontent) my_file = True else: - stream = fileobj + raise NotImplementedError( + "PdfMerger.merge requires an object that PdfReader can parse. " + "Typically, that is a Path or a string representing a Path, " + "a file object, or an object implementing .seek and .read. " + "Passing a PdfReader directly works as well." + ) return stream, my_file, encryption_obj @deprecate_bookmark(bookmark="outline_item", import_bookmarks="import_outline") diff --git a/tests/test_merger.py b/tests/test_merger.py index 7411a7a9d..77e646740 100644 --- a/tests/test_merger.py +++ b/tests/test_merger.py @@ -30,6 +30,14 @@ def merger_operate(merger): merger.append(pdf_path, pages=PyPDF2.pagerange.PageRange(slice(0, 0))) merger.append(pdf_forms) merger.merge(0, pdf_path, import_outline=False) + with pytest.raises(NotImplementedError) as exc: + with open(pdf_path, "rb") as fp: + data = fp.read() + merger.append(data) + assert exc.value.args[0].startswith( + "PdfMerger.merge requires an object that PdfReader can parse. " + "Typically, that is a Path" + ) # Merging an encrypted file reader = PyPDF2.PdfReader(pdf_pw) From baf0de1be0b68a1c1654d63ba60bbdca87ca6e80 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sat, 20 Aug 2022 20:15:04 +0200 Subject: [PATCH 115/130] TST: Close PdfMerger in tests (#1260) --- tests/bench.py | 32 ++++++++++++++++---------------- tests/test_encryption.py | 6 +++--- tests/test_merger.py | 10 ++++++++++ 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/tests/bench.py b/tests/bench.py index 5072510cf..28329494b 100644 --- a/tests/bench.py +++ b/tests/bench.py @@ -56,36 +56,36 @@ def merge(): pdf_forms = RESOURCE_ROOT / "pdflatex-forms.pdf" pdf_pw = RESOURCE_ROOT / "libreoffice-writer-password.pdf" - file_merger = PyPDF2.PdfMerger() + merger = PyPDF2.PdfMerger() # string path: - file_merger.append(pdf_path) - file_merger.append(outline) - file_merger.append(pdf_path, pages=PyPDF2.pagerange.PageRange(slice(0, 0))) - file_merger.append(pdf_forms) + merger.append(pdf_path) + merger.append(outline) + merger.append(pdf_path, pages=PyPDF2.pagerange.PageRange(slice(0, 0))) + merger.append(pdf_forms) # Merging an encrypted file reader = PyPDF2.PdfReader(pdf_pw) reader.decrypt("openpassword") - file_merger.append(reader) + merger.append(reader) # PdfReader object: - file_merger.append(PyPDF2.PdfReader(pdf_path, "rb"), outline_item=True) + merger.append(PyPDF2.PdfReader(pdf_path, "rb"), outline_item=True) # File handle with open(pdf_path, "rb") as fh: - file_merger.append(fh) + merger.append(fh) - outline_item = file_merger.add_outline_item("An outline item", 0) - file_merger.add_outline_item("deeper", 0, parent=outline_item) - file_merger.add_metadata({"author": "Martin Thoma"}) - file_merger.add_named_destination("title", 0) - file_merger.set_page_layout("/SinglePage") - file_merger.set_page_mode("/UseThumbs") + outline_item = merger.add_outline_item("An outline item", 0) + merger.add_outline_item("deeper", 0, parent=outline_item) + merger.add_metadata({"author": "Martin Thoma"}) + merger.add_named_destination("title", 0) + merger.set_page_layout("/SinglePage") + merger.set_page_mode("/UseThumbs") tmp_path = "dont_commit_merged.pdf" - file_merger.write(tmp_path) - file_merger.close() + merger.write(tmp_path) + merger.close() # Check if outline is correct reader = PyPDF2.PdfReader(tmp_path) diff --git a/tests/test_encryption.py b/tests/test_encryption.py index ff24dc420..4534dfd34 100644 --- a/tests/test_encryption.py +++ b/tests/test_encryption.py @@ -135,15 +135,15 @@ def test_get_page_of_encrypted_file_new_algorithm(pdffile, password): ) @pytest.mark.skipif(not HAS_PYCRYPTODOME, reason="No pycryptodome") def test_encryption_merge(names): - pdf_merger = PyPDF2.PdfMerger() + merger = PyPDF2.PdfMerger() files = [RESOURCE_ROOT / "encryption" / x for x in names] pdfs = [PyPDF2.PdfReader(x) for x in files] for pdf in pdfs: if pdf.is_encrypted: pdf.decrypt("asdfzxcv") - pdf_merger.append(pdf) + merger.append(pdf) # no need to write to file - pdf_merger.close() + merger.close() @pytest.mark.parametrize( diff --git a/tests/test_merger.py b/tests/test_merger.py index 77e646740..2cce04122 100644 --- a/tests/test_merger.py +++ b/tests/test_merger.py @@ -221,6 +221,7 @@ def test_trim_outline_list(): merger = PdfMerger() merger.append(reader) merger.write("tmp-merger-do-not-commit.pdf") + merger.close() # cleanup os.remove("tmp-merger-do-not-commit.pdf") @@ -233,6 +234,7 @@ def test_zoom(): merger = PdfMerger() merger.append(reader) merger.write("tmp-merger-do-not-commit.pdf") + merger.close() # cleanup os.remove("tmp-merger-do-not-commit.pdf") @@ -245,6 +247,7 @@ def test_zoom_xyz_no_left(): merger = PdfMerger() merger.append(reader) merger.write("tmp-merger-do-not-commit.pdf") + merger.close() # cleanup os.remove("tmp-merger-do-not-commit.pdf") @@ -257,6 +260,7 @@ def test_outline_item(): merger = PdfMerger() merger.append(reader) merger.write("tmp-merger-do-not-commit.pdf") + merger.close() # cleanup os.remove("tmp-merger-do-not-commit.pdf") @@ -269,6 +273,7 @@ def test_trim_outline(): merger = PdfMerger() merger.append(reader) merger.write("tmp-merger-do-not-commit.pdf") + merger.close() # cleanup os.remove("tmp-merger-do-not-commit.pdf") @@ -281,6 +286,7 @@ def test1(): merger = PdfMerger() merger.append(reader) merger.write("tmp-merger-do-not-commit.pdf") + merger.close() # cleanup os.remove("tmp-merger-do-not-commit.pdf") @@ -294,6 +300,7 @@ def test_sweep_recursion1(): merger = PdfMerger() merger.append(reader) merger.write("tmp-merger-do-not-commit.pdf") + merger.close() reader2 = PdfReader("tmp-merger-do-not-commit.pdf") reader2.pages @@ -321,6 +328,7 @@ def test_sweep_recursion2(url, name): merger = PdfMerger() merger.append(reader) merger.write("tmp-merger-do-not-commit.pdf") + merger.close() reader2 = PdfReader("tmp-merger-do-not-commit.pdf") reader2.pages @@ -336,6 +344,7 @@ def test_sweep_indirect_list_newobj_is_None(caplog): merger = PdfMerger() merger.append(reader) merger.write("tmp-merger-do-not-commit.pdf") + merger.close() assert "Object 21 0 not defined." in caplog.text reader2 = PdfReader("tmp-merger-do-not-commit.pdf") @@ -351,6 +360,7 @@ def test_iss1145(): name = "iss1145.pdf" merger = PdfMerger() merger.append(PdfReader(BytesIO(get_pdf_from_url(url, name=name)))) + merger.close() def test_deprecate_bookmark_decorator_warning(): From cf3aab4b784163539a1f68ebd3f8979018449204 Mon Sep 17 00:00:00 2001 From: pubpub-zz <4083478+pubpub-zz@users.noreply.github.com> Date: Sun, 21 Aug 2022 17:37:22 +0200 Subject: [PATCH 116/130] ROB: Decrypt returns empty bytestring (#1258) Closes #1245 --- PyPDF2/_encryption.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/PyPDF2/_encryption.py b/PyPDF2/_encryption.py index 8a3e95439..80cd369b6 100644 --- a/PyPDF2/_encryption.py +++ b/PyPDF2/_encryption.py @@ -85,7 +85,10 @@ def decrypt(self, data: bytes) -> bytes: data = data[16:] aes = AES.new(self.key, AES.MODE_CBC, iv) d = aes.decrypt(data) - return d[: -d[-1]] + if len(d) == 0: + return d + else: + return d[: -d[-1]] def RC4_encrypt(key: bytes, data: bytes) -> bytes: return ARC4.ARC4Cipher(key).encrypt(data) From 67144813863936e34b37a0fe382a1f811a5f3681 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 21 Aug 2022 20:02:01 +0200 Subject: [PATCH 117/130] MAINT: Remove 'mine' as PdfMerger always creates the stream (#1261) --- PyPDF2/_merger.py | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/PyPDF2/_merger.py b/PyPDF2/_merger.py index aff717b68..8fc697f02 100644 --- a/PyPDF2/_merger.py +++ b/PyPDF2/_merger.py @@ -103,7 +103,7 @@ class PdfMerger: def __init__( self, strict: bool = False, fileobj: Union[Path, StrByteType] = "" ) -> None: - self.inputs: List[Tuple[Any, PdfReader, bool]] = [] + self.inputs: List[Tuple[Any, PdfReader]] = [] self.pages: List[Any] = [] self.output: Optional[PdfWriter] = PdfWriter() self.outline: OutlineType = [] @@ -160,12 +160,12 @@ def merge( outline (collection of outline items, previously referred to as 'bookmarks') from being imported by specifying this as ``False``. """ - stream, my_file, encryption_obj = self._create_stream(fileobj) + stream, encryption_obj = self._create_stream(fileobj) # Create a new PdfReader instance using the stream # (either file or BytesIO or StringIO) created above reader = PdfReader(stream, strict=self.strict) # type: ignore[arg-type] - self.inputs.append((stream, reader, my_file)) + self.inputs.append((stream, reader)) if encryption_obj is not None: reader._encryption = encryption_obj @@ -217,11 +217,7 @@ def merge( def _create_stream( self, fileobj: Union[Path, StrByteType, PdfReader] - ) -> Tuple[IOBase, bool, Optional[Encryption]]: - # This parameter is passed to self.inputs.append and means - # that the stream used was created in this method. - my_file = False - + ) -> Tuple[IOBase, Optional[Encryption]]: # If the fileobj parameter is a string, assume it is a path # and create a file object at that location. If it is a file, # copy the file's contents into a BytesIO stream object; if @@ -232,7 +228,6 @@ def _create_stream( stream: IOBase if isinstance(fileobj, (str, Path)): stream = FileIO(fileobj, "rb") - my_file = True elif isinstance(fileobj, PdfReader): if fileobj._encryption: encryption_obj = fileobj._encryption @@ -242,13 +237,10 @@ def _create_stream( # reset the stream to its original location fileobj.stream.seek(orig_tell) - - my_file = True elif hasattr(fileobj, "seek") and hasattr(fileobj, "read"): fileobj.seek(0) filecontent = fileobj.read() stream = BytesIO(filecontent) - my_file = True else: raise NotImplementedError( "PdfMerger.merge requires an object that PdfReader can parse. " @@ -256,7 +248,7 @@ def _create_stream( "a file object, or an object implementing .seek and .read. " "Passing a PdfReader directly works as well." ) - return stream, my_file, encryption_obj + return stream, encryption_obj @deprecate_bookmark(bookmark="outline_item", import_bookmarks="import_outline") def append( @@ -325,9 +317,8 @@ def write(self, fileobj: Union[Path, StrByteType]) -> None: def close(self) -> None: """Shut all file descriptors (input and output) and clear all memory usage.""" self.pages = [] - for fo, _reader, mine in self.inputs: - if mine: - fo.close() + for fo, _reader in self.inputs: + fo.close() self.inputs = [] self.output = None From 2ff3bff55b8208e57c1d292d63ef72a82e071b06 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 21 Aug 2022 20:24:34 +0200 Subject: [PATCH 118/130] MAINT: Remove unused sign function in _extract_text (#1262) --- PyPDF2/_page.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/PyPDF2/_page.py b/PyPDF2/_page.py index 67081cc7e..77a15ab32 100644 --- a/PyPDF2/_page.py +++ b/PyPDF2/_page.py @@ -1178,9 +1178,6 @@ def _extract_text( TL = 0.0 font_size = 12.0 # init just in case of - def sign(x: float) -> float: - return 1 if x >= 0 else -1 - def mult(m: List[float], n: List[float]) -> List[float]: return [ m[0] * n[0] + m[1] * n[2], From b086e20dc4d0a24f3849652b8164088788479387 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 21 Aug 2022 21:15:10 +0200 Subject: [PATCH 119/130] TST: Delete annotations (#1263) --- tests/test_page.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_page.py b/tests/test_page.py index 3e7caa33b..2a9c97b00 100644 --- a/tests/test_page.py +++ b/tests/test_page.py @@ -433,6 +433,9 @@ def test_annotation_setter(): arr = ArrayObject() page.annotations = arr + # Delete Annotations + page.annotations = None + d = DictionaryObject(annot_dict) ind_obj = writer._add_object(d) arr.append(ind_obj) From 2ddc48a8933bce3efec757fd0222fa07d0f07b0e Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 21 Aug 2022 21:18:49 +0200 Subject: [PATCH 120/130] REL: 2.10.3 Robustness (ROB): - Decrypt returns empty bytestring (#1258) Documentation (DOC): - Adding WevertonGomes as a Contributor Developer Experience (DEV): - Modify CI to better verify built package contents (#1244) Maintenance (MAINT): - Remove unused sign function in _extract_text (#1262) - Remove \'mine\' as PdfMerger always creates the stream (#1261) - Let PdfMerger._create_stream raise NotImplemented (#1251) - password param of _security._alg32(...) is only a string, not bytes (#1259) - Remove unreachable code in read_block_backwards (#1250) Testing (TST): - Delete annotations (#1263) - Close PdfMerger in tests (#1260) - PdfReader.xmp_metadata workflow (#1257) - Various PdfWriter (Layout, Bookmark deprecation) (#1249) Full Changelog: https://github.com/py-pdf/PyPDF2/compare/2.10.2...2.10.3 --- CHANGELOG.md | 23 +++++++++++++++++++++++ PyPDF2/_version.py | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec85f3bf8..366a02d18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # CHANGELOG +## Version 2.10.3, 2022-08-21 + +### Robustness (ROB) +- Decrypt returns empty bytestring (#1258) + +### Developer Experience (DEV) +- Modify CI to better verify built package contents (#1244) + +### Maintenance (MAINT) +- Remove 'mine' as PdfMerger always creates the stream (#1261) +- Let PdfMerger._create_stream raise NotImplemented (#1251) +- password param of _security._alg32(...) is only a string, not bytes (#1259) +- Remove unreachable code in read_block_backwards (#1250) + and sign function in _extract_text (#1262) + +### Testing (TST) +- Delete annotations (#1263) +- Close PdfMerger in tests (#1260) +- PdfReader.xmp_metadata workflow (#1257) +- Various PdfWriter (Layout, Bookmark deprecation) (#1249) + +Full Changelog: https://github.com/py-pdf/PyPDF2/compare/2.10.2...2.10.3 + ## Version 2.10.2, 2022-08-15 BUG: Add PyPDF2.generic to PyPI distribution diff --git a/PyPDF2/_version.py b/PyPDF2/_version.py index 6c96c9755..21e33ba2c 100644 --- a/PyPDF2/_version.py +++ b/PyPDF2/_version.py @@ -1 +1 @@ -__version__ = "2.10.2" +__version__ = "2.10.3" From 84460f54aa4721db36452fe510f8063838e358d5 Mon Sep 17 00:00:00 2001 From: Maximilian Date: Sat, 27 Aug 2022 11:37:58 +0200 Subject: [PATCH 121/130] PKG: Add minimum version for typing_extensions requirement (#1277) PyPDF2 uses TypeAlias which was introduced via PEP 613 in Python 3.10. Older versions of Python need typing_extensions>=3.10.0.0. --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index bded39e8a..2c0eebe8f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,7 +38,7 @@ packages = PyPDF2.generic python_requires = >=3.6 install_requires = - typing_extensions; python_version < '3.10' + typing_extensions >= 3.10.0.0; python_version < '3.10' [options.extras_require] crypto = PyCryptodome From b63085e94e634a65d6b4b03fe1caa0c96886521d Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sat, 27 Aug 2022 13:16:24 +0200 Subject: [PATCH 122/130] TST: Remove files after tests ran (#1286) * Move test_basic_features.py into test_workflows.py * Use the tmp_path fixture in cases where we're not interested to manually check the resulting PDF file --- tests/bench.py | 12 ++---- tests/test_basic_features.py | 54 -------------------------- tests/test_generic.py | 16 +++++--- tests/test_workflows.py | 75 ++++++++++++++++++++++++++---------- tests/test_writer.py | 8 +++- 5 files changed, 76 insertions(+), 89 deletions(-) delete mode 100644 tests/test_basic_features.py diff --git a/tests/bench.py b/tests/bench.py index 28329494b..248e9c9d0 100644 --- a/tests/bench.py +++ b/tests/bench.py @@ -1,4 +1,3 @@ -import os from pathlib import Path import PyPDF2 @@ -50,7 +49,7 @@ def test_page_operations(benchmark): benchmark(page_ops, "libreoffice-writer-password.pdf", "openpassword") -def merge(): +def merge(tmp_path): pdf_path = RESOURCE_ROOT / "crazyones.pdf" outline = RESOURCE_ROOT / "pdflatex-outline.pdf" pdf_forms = RESOURCE_ROOT / "pdflatex-forms.pdf" @@ -83,12 +82,12 @@ def merge(): merger.set_page_layout("/SinglePage") merger.set_page_mode("/UseThumbs") - tmp_path = "dont_commit_merged.pdf" - merger.write(tmp_path) + write_path = tmp_path / "dont_commit_merged.pdf" + merger.write(write_path) merger.close() # Check if outline is correct - reader = PyPDF2.PdfReader(tmp_path) + reader = PyPDF2.PdfReader(write_path) assert [ el.title for el in reader._get_outline() if isinstance(el, Destination) ] == [ @@ -105,9 +104,6 @@ def merge(): "True", ] - # Clean up - os.remove(tmp_path) - def test_merge(benchmark): """ diff --git a/tests/test_basic_features.py b/tests/test_basic_features.py deleted file mode 100644 index bdc65d074..000000000 --- a/tests/test_basic_features.py +++ /dev/null @@ -1,54 +0,0 @@ -import os -from pathlib import Path - -from PyPDF2 import PdfReader, PdfWriter - -TESTS_ROOT = Path(__file__).parent.resolve() -PROJECT_ROOT = TESTS_ROOT.parent -RESOURCE_ROOT = PROJECT_ROOT / "resources" - - -def test_basic_features(): - pdf_path = RESOURCE_ROOT / "crazyones.pdf" - reader = PdfReader(pdf_path) - writer = PdfWriter() - - assert len(reader.pages) == 1 - - # add page 1 from input1 to output document, unchanged - writer.add_page(reader.pages[0]) - - # add page 2 from input1, but rotated clockwise 90 degrees - writer.add_page(reader.pages[0].rotate(90)) - - # add page 3 from input1, but first add a watermark from another PDF: - page3 = reader.pages[0] - watermark_pdf = pdf_path - watermark = PdfReader(watermark_pdf) - page3.merge_page(watermark.pages[0]) - writer.add_page(page3) - - # add page 4 from input1, but crop it to half size: - page4 = reader.pages[0] - page4.mediabox.upper_right = ( - page4.mediabox.right / 2, - page4.mediabox.top / 2, - ) - writer.add_page(page4) - - # add some Javascript to launch the print window on opening this PDF. - # the password dialog may prevent the print dialog from being shown, - # comment the the encription lines, if that's the case, to try this out - writer.add_js("this.print({bUI:true,bSilent:false,bShrinkToFit:true});") - - # encrypt your new PDF and add a password - password = "secret" - writer.encrypt(password) - - # finally, write "output" to PyPDF2-output.pdf - tmp_path = "PyPDF2-output.pdf" - with open(tmp_path, "wb") as output_stream: - writer.write(output_stream) - - # cleanup - os.remove(tmp_path) diff --git a/tests/test_generic.py b/tests/test_generic.py index 6a82df22c..5cb1ae5d1 100644 --- a/tests/test_generic.py +++ b/tests/test_generic.py @@ -579,17 +579,23 @@ def test_name_object_read_from_stream_unicode_error(): # L588 page.extract_text() -def test_bool_repr(): +def test_bool_repr(tmp_path): url = "https://corpora.tika.apache.org/base/docs/govdocs1/932/932449.pdf" name = "tika-932449.pdf" reader = PdfReader(BytesIO(get_pdf_from_url(url, name=name))) - with open("tmp-fields-report.txt", "w") as fp: + write_path = tmp_path / "tmp-fields-report.txt" + with open(write_path, "w") as fp: fields = reader.get_fields(fileobj=fp) assert fields - - # cleanup - os.remove("tmp-fields-report.txt") + assert list(fields.keys()) == ["USGPOSignature"] + with open(write_path) as fp: + data = fp.read() + assert data.startswith( + "Field Name: USGPOSignature\nField Type: Signature\nField Flags: 1\n" + "Value: {'/Type': '/Sig', '/Filter': '/Adobe.PPKLite', " + "'/SubFilter':" + ) @patch("PyPDF2._reader.logger_warning") diff --git a/tests/test_workflows.py b/tests/test_workflows.py index 672ba90b3..0a1c124b4 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -29,6 +29,49 @@ sys.path.append(str(PROJECT_ROOT)) +def test_basic_features(tmp_path): + pdf_path = RESOURCE_ROOT / "crazyones.pdf" + reader = PdfReader(pdf_path) + writer = PdfWriter() + + assert len(reader.pages) == 1 + + # add page 1 from input1 to output document, unchanged + writer.add_page(reader.pages[0]) + + # add page 2 from input1, but rotated clockwise 90 degrees + writer.add_page(reader.pages[0].rotate(90)) + + # add page 3 from input1, but first add a watermark from another PDF: + page3 = reader.pages[0] + watermark_pdf = pdf_path + watermark = PdfReader(watermark_pdf) + page3.merge_page(watermark.pages[0]) + writer.add_page(page3) + + # add page 4 from input1, but crop it to half size: + page4 = reader.pages[0] + page4.mediabox.upper_right = ( + page4.mediabox.right / 2, + page4.mediabox.top / 2, + ) + writer.add_page(page4) + + # add some Javascript to launch the print window on opening this PDF. + # the password dialog may prevent the print dialog from being shown, + # comment the the encription lines, if that's the case, to try this out + writer.add_js("this.print({bUI:true,bSilent:false,bShrinkToFit:true});") + + # encrypt your new PDF and add a password + password = "secret" + writer.encrypt(password) + + # finally, write "output" to PyPDF2-output.pdf + write_path = tmp_path / "PyPDF2-output.pdf" + with open(write_path, "wb") as output_stream: + writer.write(output_stream) + + def test_dropdown_items(): inputfile = RESOURCE_ROOT / "libreoffice-form.pdf" reader = PdfReader(inputfile) @@ -321,7 +364,7 @@ def test_overlay(base_path, overlay_path): writer.write(fp) # Cleanup - os.remove("dont_commit_overlay.pdf") + os.remove("dont_commit_overlay.pdf") # remove for manual inspection @pytest.mark.parametrize( @@ -333,16 +376,13 @@ def test_overlay(base_path, overlay_path): ) ], ) -def test_merge_with_warning(url, name): +def test_merge_with_warning(tmp_path, url, name): data = BytesIO(get_pdf_from_url(url, name=name)) reader = PdfReader(data) merger = PdfMerger() merger.append(reader) # This could actually be a performance bottleneck: - merger.write("tmp.merged.pdf") - - # Cleanup - os.remove("tmp.merged.pdf") + merger.write(tmp_path / "tmp.merged.pdf") @pytest.mark.parametrize( @@ -354,15 +394,12 @@ def test_merge_with_warning(url, name): ) ], ) -def test_merge(url, name): +def test_merge(tmp_path, url, name): data = BytesIO(get_pdf_from_url(url, name=name)) reader = PdfReader(data) merger = PdfMerger() merger.append(reader) - merger.write("tmp.merged.pdf") - - # Cleanup - os.remove("tmp.merged.pdf") + merger.write(tmp_path / "tmp.merged.pdf") @pytest.mark.parametrize( @@ -474,18 +511,16 @@ def test_compress(url, name): ), ], ) -def test_get_fields_warns(caplog, url, name): +def test_get_fields_warns(tmp_path, caplog, url, name): data = BytesIO(get_pdf_from_url(url, name=name)) reader = PdfReader(data) - with open("tmp.txt", "w") as fp: + write_path = tmp_path / "tmp.txt" + with open(write_path, "w") as fp: retrieved_fields = reader.get_fields(fileobj=fp) assert retrieved_fields == {} assert normalize_warnings(caplog.text) == ["Object 2 0 not defined."] - # Cleanup - os.remove("tmp.txt") - @pytest.mark.parametrize( ("url", "name"), @@ -496,17 +531,15 @@ def test_get_fields_warns(caplog, url, name): ), ], ) -def test_get_fields_no_warning(url, name): +def test_get_fields_no_warning(tmp_path, url, name): data = BytesIO(get_pdf_from_url(url, name=name)) reader = PdfReader(data) - with open("tmp.txt", "w") as fp: + write_path = tmp_path / "tmp.txt" + with open(write_path, "w") as fp: retrieved_fields = reader.get_fields(fileobj=fp) assert len(retrieved_fields) == 10 - # Cleanup - os.remove("tmp.txt") - def test_scale_rectangle_indirect_object(): url = "https://corpora.tika.apache.org/base/docs/govdocs1/999/999944.pdf" diff --git a/tests/test_writer.py b/tests/test_writer.py index d83db2002..9c8f0dae3 100644 --- a/tests/test_writer.py +++ b/tests/test_writer.py @@ -384,6 +384,8 @@ def test_fill_form(): with open(tmp_filename, "wb") as output_stream: writer.write(output_stream) + os.remove(tmp_filename) # cleanup + @pytest.mark.parametrize( ("use_128bit", "user_pwd", "owner_pwd"), @@ -595,14 +597,18 @@ def test_io_streams(): def test_regression_issue670(): + tmp_file = "dont_commit_issue670.pdf" filepath = RESOURCE_ROOT / "crazyones.pdf" reader = PdfReader(filepath, strict=False) for _ in range(2): writer = PdfWriter() writer.add_page(reader.pages[0]) - with open("dont_commit_issue670.pdf", "wb") as f_pdf: + with open(tmp_file, "wb") as f_pdf: writer.write(f_pdf) + # cleanup + os.remove(tmp_file) + def test_issue301(): """ From c819acbe267564afad1dddad31c256594f906bd6 Mon Sep 17 00:00:00 2001 From: pubpub-zz <4083478+pubpub-zz@users.noreply.github.com> Date: Sat, 27 Aug 2022 13:37:48 +0200 Subject: [PATCH 123/130] ROB: Add required line separators in ContentStream ArrayObjects (#1281) Closes #1278 --- PyPDF2/generic/_data_structures.py | 2 ++ tests/test_page.py | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/PyPDF2/generic/_data_structures.py b/PyPDF2/generic/_data_structures.py index 85c4e8819..283b33b22 100644 --- a/PyPDF2/generic/_data_structures.py +++ b/PyPDF2/generic/_data_structures.py @@ -679,6 +679,8 @@ def __init__( data = b"" for s in stream: data += b_(s.get_object().get_data()) + if data[-1] != b"\n": + data += b"\n" stream_bytes = BytesIO(data) else: stream_data = stream.get_data() diff --git a/tests/test_page.py b/tests/test_page.py index 2a9c97b00..e9f3ea721 100644 --- a/tests/test_page.py +++ b/tests/test_page.py @@ -265,6 +265,11 @@ def test_iss_1142(): "https://github.com/py-pdf/PyPDF2/files/9150656/ST.2019.PDF", "iss_1134.pdf", ), + # iss 1: + ( + "https://github.com/py-pdf/PyPDF2/files/9432350/Work.Flow.From.Check.to.QA.pdf", + "WFCA.pdf", + ), ], ) def test_extract_text_page_pdf(url, name): From e909d8cfd402f942f0ef765721d8276941da69a3 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sat, 27 Aug 2022 14:10:17 +0200 Subject: [PATCH 124/130] DOC: Add DL6ER as a contributor --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 50eb2540b..a0ec2945c 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -11,6 +11,7 @@ history and [GitHubs 'Contributors' feature](https://github.com/py-pdf/PyPDF2/gr ## Contributors to the pyPdf / PyPDF2 project +* [DL6ER](https://github.com/DL6ER) * [JianzhengLuo](https://github.com/JianzhengLuo) * [Karvonen, Harry](https://github.com/Hatell/) * [KourFrost](https://github.com/KourFrost) From ceb997d68d17481b5d3afd3e956de4a3bdb3074d Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sat, 27 Aug 2022 14:16:05 +0200 Subject: [PATCH 125/130] TST: Add workflow tests (#1287) --- tests/test_workflows.py | 67 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 7 deletions(-) diff --git a/tests/test_workflows.py b/tests/test_workflows.py index 0a1c124b4..d39eab254 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -418,42 +418,88 @@ def test_get_metadata(url, name): @pytest.mark.parametrize( - ("url", "name"), + ("url", "name", "strict", "exception"), [ ( "https://corpora.tika.apache.org/base/docs/govdocs1/938/938702.pdf", "tika-938702.pdf", + False, + (PdfReadError, "Unexpected end of stream"), ), ( "https://corpora.tika.apache.org/base/docs/govdocs1/942/942358.pdf", "tika-942358.pdf", + False, + None, ), ( "https://corpora.tika.apache.org/base/docs/govdocs1/911/911260.pdf", "tika-911260.pdf", + False, + None, ), ( "https://corpora.tika.apache.org/base/docs/govdocs1/992/992472.pdf", "tika-992472.pdf", + False, + None, ), ( "https://corpora.tika.apache.org/base/docs/govdocs1/978/978477.pdf", "tika-978477.pdf", + False, + None, ), ( "https://corpora.tika.apache.org/base/docs/govdocs1/960/960317.pdf", "tika-960317.pdf", + False, + None, ), ( "https://corpora.tika.apache.org/base/docs/govdocs1/930/930513.pdf", "tika-930513.pdf", + False, + None, + ), + ( + "https://corpora.tika.apache.org/base/docs/govdocs1/918/918113.pdf", + "tika-918113.pdf", + True, + None, + ), + ( + "https://corpora.tika.apache.org/base/docs/govdocs1/940/940704.pdf", + "tika-940704.pdf", + True, + None, + ), + ( + "https://corpora.tika.apache.org/base/docs/govdocs1/976/976488.pdf", + "tika-976488.pdf", + True, + None, + ), + ( + "https://corpora.tika.apache.org/base/docs/govdocs1/948/948176.pdf", + "tika-948176.pdf", + True, + None, ), ], ) -def test_extract_text(url, name): +def test_extract_text(url, name, strict, exception): data = BytesIO(get_pdf_from_url(url, name=name)) - reader = PdfReader(data) - reader.metadata + reader = PdfReader(data, strict=strict) + if not exception: + for page in reader.pages: + page.extract_text() + else: + exc, exc_text = exception + with pytest.raises(exc) as ex_info: + for page in reader.pages: + page.extract_text() + assert ex_info.value.args[0] == exc_text @pytest.mark.parametrize( @@ -481,21 +527,28 @@ def test_compress_raised(url, name): @pytest.mark.parametrize( - ("url", "name"), + ("url", "name", "strict"), [ ( "https://corpora.tika.apache.org/base/docs/govdocs1/915/915194.pdf", "tika-915194.pdf", + False, ), ( "https://corpora.tika.apache.org/base/docs/govdocs1/950/950337.pdf", "tika-950337.pdf", + False, + ), + ( + "https://corpora.tika.apache.org/base/docs/govdocs1/962/962292.pdf", + "tika-962292.pdf", + True, ), ], ) -def test_compress(url, name): +def test_compress(url, name, strict): data = BytesIO(get_pdf_from_url(url, name=name)) - reader = PdfReader(data) + reader = PdfReader(data, strict=strict) # TODO: which page exactly? # TODO: Is it reasonable to have an exception here? for page in reader.pages: From af9c01b94c0a736105abedfa36adc8fa04d844b1 Mon Sep 17 00:00:00 2001 From: pubpub-zz <4083478+pubpub-zz@users.noreply.github.com> Date: Sun, 28 Aug 2022 12:26:50 +0200 Subject: [PATCH 126/130] ROB: Fix errors/warnings on no /Resources within extract_text (#1276) Look for /Ressources in parents Closes #1272 (in text) Closes #1269 (in Xform) --- PyPDF2/_page.py | 10 +++++++++- tests/test_page.py | 9 ++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/PyPDF2/_page.py b/PyPDF2/_page.py index 77a15ab32..f818ff544 100644 --- a/PyPDF2/_page.py +++ b/PyPDF2/_page.py @@ -1140,7 +1140,15 @@ def _extract_text( cmaps: Dict[ str, Tuple[str, float, Union[str, Dict[int, str]], Dict[str, str]] ] = {} - resources_dict = cast(DictionaryObject, obj["/Resources"]) + try: + objr = obj + while NameObject("/Resources") not in objr: + # /Resources can be inherited sometimes so we look to parents + objr = objr["/Parent"].get_object() + # if no parents we will have no /Resources will be available => an exception wil be raised + resources_dict = cast(DictionaryObject, objr["/Resources"]) + except Exception: + return "" # no resources means no text is possible (no font) we consider the file as not damaged, no need to check for TJ or Tj if "/Font" in resources_dict: for f in cast(DictionaryObject, resources_dict["/Font"]): cmaps[f] = build_char_map(f, space_width, obj) diff --git a/tests/test_page.py b/tests/test_page.py index e9f3ea721..40906bd3e 100644 --- a/tests/test_page.py +++ b/tests/test_page.py @@ -238,6 +238,13 @@ def test_extract_text_single_quote_op(): page.extract_text() +def test_no_ressources_on_text_extract(): + url = "https://github.com/py-pdf/PyPDF2/files/9428434/TelemetryTX_EM.pdf" + reader = PdfReader(BytesIO(get_pdf_from_url(url, name="tika-964029.pdf"))) + for page in reader.pages: + page.extract_text() + + def test_iss_1142(): # check fix for problem of context save/restore (q/Q) url = "https://github.com/py-pdf/PyPDF2/files/9150656/ST.2019.PDF" @@ -285,7 +292,7 @@ def test_extract_text_page_pdf_impossible_decode_xform(caplog): for page in reader.pages: page.extract_text() warn_msgs = normalize_warnings(caplog.text) - assert warn_msgs == [" impossible to decode XFormObject /Meta203"] + assert warn_msgs == [""] # text extraction recognise no text def test_extract_text_operator_t_star(): # L1266, L1267 From 4745984bee177eec5c92adc0df13c6f293aca9c5 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 28 Aug 2022 12:42:31 +0200 Subject: [PATCH 127/130] DEV: Fix benchmark --- tests/bench.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/bench.py b/tests/bench.py index 248e9c9d0..4ae9bb2d1 100644 --- a/tests/bench.py +++ b/tests/bench.py @@ -49,7 +49,7 @@ def test_page_operations(benchmark): benchmark(page_ops, "libreoffice-writer-password.pdf", "openpassword") -def merge(tmp_path): +def merge(): pdf_path = RESOURCE_ROOT / "crazyones.pdf" outline = RESOURCE_ROOT / "pdflatex-outline.pdf" pdf_forms = RESOURCE_ROOT / "pdflatex-forms.pdf" @@ -82,7 +82,7 @@ def merge(tmp_path): merger.set_page_layout("/SinglePage") merger.set_page_mode("/UseThumbs") - write_path = tmp_path / "dont_commit_merged.pdf" + write_path = "dont_commit_merged.pdf" merger.write(write_path) merger.close() From 69a27ae9db9eda4406942c0c4f7effff38498523 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 28 Aug 2022 13:22:50 +0200 Subject: [PATCH 128/130] TST: Rectangle deletion (#1289) --- tests/test_workflows.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_workflows.py b/tests/test_workflows.py index d39eab254..0a7b1efb8 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -55,6 +55,7 @@ def test_basic_features(tmp_path): page4.mediabox.right / 2, page4.mediabox.top / 2, ) + del page4.mediabox writer.add_page(page4) # add some Javascript to launch the print window on opening this PDF. From 347cc24cb0d3b3a8db27d82031e4d8351d2db2ab Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 28 Aug 2022 13:41:00 +0200 Subject: [PATCH 129/130] MAINT: Use NameObject idempotency (#1290) --- PyPDF2/_page.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/PyPDF2/_page.py b/PyPDF2/_page.py index f818ff544..45bf36662 100644 --- a/PyPDF2/_page.py +++ b/PyPDF2/_page.py @@ -97,8 +97,7 @@ def getRectangle( def _set_rectangle(self: Any, name: str, value: Union[RectangleObject, float]) -> None: - if not isinstance(name, NameObject): - name = NameObject(name) + name = NameObject(name) self[name] = value From 3b74312924542a59dce5c3f8e067b6e1765a12e6 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Sun, 28 Aug 2022 14:41:24 +0200 Subject: [PATCH 130/130] REL: 2.10.4 Robustness (ROB): - Fix errors/warnings on no /Resources within extract_text (#1276) - Add required line separators in ContentStream ArrayObjects (#1281) Maintenance (MAINT): - Use NameObject idempotency (#1290) Testing (TST): - Rectangle deletion (#1289) - Add workflow tests (#1287) - Remove files after tests ran (#1286) Packaging (PKG): - Add minimum version for typing_extensions requirement (#1277) Full Changelog: https://github.com/py-pdf/PyPDF2/compare/2.10.3...2.10.4 --- CHANGELOG.md | 19 +++++++++++++++++++ PyPDF2/_version.py | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 366a02d18..c41f56340 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # CHANGELOG +## Version 2.10.4, 2022-08-28 + +### Robustness (ROB) +- Fix errors/warnings on no /Resources within extract_text (#1276) +- Add required line separators in ContentStream ArrayObjects (#1281) + +### Maintenance (MAINT) +- Use NameObject idempotency (#1290) + +### Testing (TST) +- Rectangle deletion (#1289) +- Add workflow tests (#1287) +- Remove files after tests ran (#1286) + +### Packaging (PKG) +- Add minimum version for typing_extensions requirement (#1277) + +Full Changelog: https://github.com/py-pdf/PyPDF2/compare/2.10.3...2.10.4 + ## Version 2.10.3, 2022-08-21 ### Robustness (ROB) diff --git a/PyPDF2/_version.py b/PyPDF2/_version.py index 21e33ba2c..e3b571e0f 100644 --- a/PyPDF2/_version.py +++ b/PyPDF2/_version.py @@ -1 +1 @@ -__version__ = "2.10.3" +__version__ = "2.10.4"