From f7c5507104c449aba1eb19e457a6c157431bf765 Mon Sep 17 00:00:00 2001 From: HSY-999 Date: Fri, 21 Nov 2025 21:06:23 +0100 Subject: [PATCH 01/17] BUG: inital fix for merge page #3467 --- pypdf/annotations/_markup_annotations.py | 37 ++++++++++++++++-------- pypdf/generic/_data_structures.py | 9 +++++- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/pypdf/annotations/_markup_annotations.py b/pypdf/annotations/_markup_annotations.py index 2654cd1d14..c6b5eda1b4 100644 --- a/pypdf/annotations/_markup_annotations.py +++ b/pypdf/annotations/_markup_annotations.py @@ -1,5 +1,6 @@ import sys from abc import ABC +import inspect from typing import Any, Optional, Union from ..constants import AnnotationFlag @@ -189,16 +190,22 @@ def __init__( class PolyLine(MarkupAnnotation): def __init__( self, - vertices: list[Vertex], + vertices: list[Vertex] | ArrayObject[NumberObject], **kwargs: Any, ) -> None: super().__init__(**kwargs) - if len(vertices) == 0: - raise ValueError("A polygon needs at least 1 vertex with two coordinates") + if len(vertices) == 0 or len(vertices) % 2 != 0: + raise ValueError("A polygon needs at least 1 vertex," \ + " containing 1 horizontal and 1 vertical position") coord_list = [] - for x, y in vertices: - coord_list.append(NumberObject(x)) - coord_list.append(NumberObject(y)) + if type(vertices) is ArrayObject: + import itertools + coord_list = vertices + vertices = [vertex for vertex in itertools.batched(vertices, 2)] + else: + for x, y in vertices: + coord_list.append(NumberObject(x)) + coord_list.append(NumberObject(y)) self.update( { NameObject("/Subtype"): NameObject("/PolyLine"), @@ -283,17 +290,23 @@ def __init__( class Polygon(MarkupAnnotation): def __init__( self, - vertices: list[tuple[float, float]], + vertices: list[Vertex] | ArrayObject[NumberObject], **kwargs: Any, ) -> None: super().__init__(**kwargs) - if len(vertices) == 0: - raise ValueError("A polygon needs at least 1 vertex with two coordinates") + if len(vertices) == 0 or len(vertices) % 2 != 0: + raise ValueError("A polygon needs at least 1 vertex," \ + " containing 1 horizontal and 1 vertical position") coord_list = [] - for x, y in vertices: - coord_list.append(NumberObject(x)) - coord_list.append(NumberObject(y)) + if type(vertices) is ArrayObject: + import itertools + coord_list = vertices + vertices = [vertex for vertex in itertools.batched(vertices, 2)] + else: + for x, y in vertices: + coord_list.append(NumberObject(x)) + coord_list.append(NumberObject(y)) self.update( { NameObject("/Type"): NameObject("/Annot"), diff --git a/pypdf/generic/_data_structures.py b/pypdf/generic/_data_structures.py index 78d39af951..28332c70d2 100644 --- a/pypdf/generic/_data_structures.py +++ b/pypdf/generic/_data_structures.py @@ -291,9 +291,16 @@ def clone( pass visited: set[tuple[int, int]] = set() # (idnum, generation) + import inspect + kwargs = {} + for arg_name in inspect.getfullargspec(self.__class__.__init__).args: + for key, val in self.items(): + if key.removeprefix("/").lower() == arg_name: + kwargs[arg_name] = val + d__ = cast( "DictionaryObject", - self._reference_clone(self.__class__(), pdf_dest, force_duplicate), + self._reference_clone(self.__class__(**kwargs), pdf_dest, force_duplicate), ) if ignore_fields is None: ignore_fields = [] From 01a52a2bcaf4175b31f284941fa53d702c15267a Mon Sep 17 00:00:00 2001 From: HSY-999 Date: Tue, 25 Nov 2025 20:28:08 +0100 Subject: [PATCH 02/17] MAINT: made inspector capable of accepting keyword only args --- pypdf/generic/_data_structures.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pypdf/generic/_data_structures.py b/pypdf/generic/_data_structures.py index 28332c70d2..e09b7d7de8 100644 --- a/pypdf/generic/_data_structures.py +++ b/pypdf/generic/_data_structures.py @@ -293,10 +293,12 @@ def clone( visited: set[tuple[int, int]] = set() # (idnum, generation) import inspect kwargs = {} - for arg_name in inspect.getfullargspec(self.__class__.__init__).args: - for key, val in self.items(): - if key.removeprefix("/").lower() == arg_name: - kwargs[arg_name] = val + inspector = inspect.getfullargspec(self.__class__.__init__) + + for key, val in self.items(): + key_stripped = key.removeprefix("/").lower() + if key_stripped in inspector.args or key_stripped in inspector.kwonlyargs: + kwargs[key_stripped] = val d__ = cast( "DictionaryObject", From f82d39d30448315f11e107842d9fbd3bd268049e Mon Sep 17 00:00:00 2001 From: HSY-999 Date: Tue, 25 Nov 2025 21:32:18 +0100 Subject: [PATCH 03/17] STY: refactored Polygon and PolyLine with shared abstract class --- pypdf/annotations/_markup_annotations.py | 54 ++++++++++++------------ 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/pypdf/annotations/_markup_annotations.py b/pypdf/annotations/_markup_annotations.py index c6b5eda1b4..3b3a8ea877 100644 --- a/pypdf/annotations/_markup_annotations.py +++ b/pypdf/annotations/_markup_annotations.py @@ -53,6 +53,28 @@ def __init__(self, *, title_bar: Optional[str] = None) -> None: self[NameObject("/T")] = TextStringObject(title_bar) +class AbstractPolyLine(MarkupAnnotation, ABC): + def __init__(self, vertices: list[Vertex] | ArrayObject[NumberObject], **kwargs): + super().__init__(**kwargs) + if len(vertices) == 0 or len(vertices) % 2 != 0: + raise ValueError("A polygon needs at least 1 vertex," \ + " containing 1 horizontal and 1 vertical position") + + @staticmethod + def determineVertices(vertices: Union[list[Vertex], ArrayObject[NumberObject]]) -> tuple[list[Vertex], list[NumberObject]]: + coord_list = [] + if type(vertices) is ArrayObject: + import itertools + coord_list = vertices + vertices = [vertex for vertex in itertools.batched(vertices, 2)] + else: + for x, y in vertices: + coord_list.append(NumberObject(x)) + coord_list.append(NumberObject(y)) + + return vertices, coord_list + + class Text(MarkupAnnotation): """ A text annotation. @@ -187,25 +209,15 @@ def __init__( ) -class PolyLine(MarkupAnnotation): +class PolyLine(AbstractPolyLine): def __init__( self, vertices: list[Vertex] | ArrayObject[NumberObject], **kwargs: Any, ) -> None: super().__init__(**kwargs) - if len(vertices) == 0 or len(vertices) % 2 != 0: - raise ValueError("A polygon needs at least 1 vertex," \ - " containing 1 horizontal and 1 vertical position") - coord_list = [] - if type(vertices) is ArrayObject: - import itertools - coord_list = vertices - vertices = [vertex for vertex in itertools.batched(vertices, 2)] - else: - for x, y in vertices: - coord_list.append(NumberObject(x)) - coord_list.append(NumberObject(y)) + + vertices, coord_list = self.determineVertices(vertices) self.update( { NameObject("/Subtype"): NameObject("/PolyLine"), @@ -287,26 +299,14 @@ def __init__( ) -class Polygon(MarkupAnnotation): +class Polygon(AbstractPolyLine): def __init__( self, vertices: list[Vertex] | ArrayObject[NumberObject], **kwargs: Any, ) -> None: super().__init__(**kwargs) - if len(vertices) == 0 or len(vertices) % 2 != 0: - raise ValueError("A polygon needs at least 1 vertex," \ - " containing 1 horizontal and 1 vertical position") - - coord_list = [] - if type(vertices) is ArrayObject: - import itertools - coord_list = vertices - vertices = [vertex for vertex in itertools.batched(vertices, 2)] - else: - for x, y in vertices: - coord_list.append(NumberObject(x)) - coord_list.append(NumberObject(y)) + vertices, coord_list = self.determineVertices(vertices) self.update( { NameObject("/Type"): NameObject("/Annot"), From b47c25e5349e2acd11a0e3be74e4120edfd3e272 Mon Sep 17 00:00:00 2001 From: HSY-999 Date: Tue, 25 Nov 2025 21:39:27 +0100 Subject: [PATCH 04/17] MAINT: Improved type check and pass in vertices as arg --- pypdf/annotations/_markup_annotations.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pypdf/annotations/_markup_annotations.py b/pypdf/annotations/_markup_annotations.py index 3b3a8ea877..318c28a635 100644 --- a/pypdf/annotations/_markup_annotations.py +++ b/pypdf/annotations/_markup_annotations.py @@ -63,7 +63,7 @@ def __init__(self, vertices: list[Vertex] | ArrayObject[NumberObject], **kwargs) @staticmethod def determineVertices(vertices: Union[list[Vertex], ArrayObject[NumberObject]]) -> tuple[list[Vertex], list[NumberObject]]: coord_list = [] - if type(vertices) is ArrayObject: + if isinstance(vertices, ArrayObject): import itertools coord_list = vertices vertices = [vertex for vertex in itertools.batched(vertices, 2)] @@ -215,7 +215,7 @@ def __init__( vertices: list[Vertex] | ArrayObject[NumberObject], **kwargs: Any, ) -> None: - super().__init__(**kwargs) + super().__init__(vertices=vertices, **kwargs) vertices, coord_list = self.determineVertices(vertices) self.update( @@ -305,7 +305,7 @@ def __init__( vertices: list[Vertex] | ArrayObject[NumberObject], **kwargs: Any, ) -> None: - super().__init__(**kwargs) + super().__init__(vertices=vertices, **kwargs) vertices, coord_list = self.determineVertices(vertices) self.update( { From 4817e85e86903ac38841bf8d20aca4f7da4e9107 Mon Sep 17 00:00:00 2001 From: HSY-999 Date: Tue, 25 Nov 2025 22:34:16 +0100 Subject: [PATCH 05/17] MAINT: Reworked determineVertices function to be 3.9 compliant --- pypdf/annotations/_markup_annotations.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pypdf/annotations/_markup_annotations.py b/pypdf/annotations/_markup_annotations.py index 318c28a635..3c91b88673 100644 --- a/pypdf/annotations/_markup_annotations.py +++ b/pypdf/annotations/_markup_annotations.py @@ -64,9 +64,10 @@ def __init__(self, vertices: list[Vertex] | ArrayObject[NumberObject], **kwargs) def determineVertices(vertices: Union[list[Vertex], ArrayObject[NumberObject]]) -> tuple[list[Vertex], list[NumberObject]]: coord_list = [] if isinstance(vertices, ArrayObject): - import itertools coord_list = vertices - vertices = [vertex for vertex in itertools.batched(vertices, 2)] + args = [iter(vertices)] * 2 # Adapted def grouper() + zip(*args) # from https://docs.python.org/3.9/library/itertools.html#itertools-recipes + else: for x, y in vertices: coord_list.append(NumberObject(x)) From e82dd433aa413da561b04a2806a6481e2ae4cd17 Mon Sep 17 00:00:00 2001 From: HSY-999 Date: Wed, 26 Nov 2025 20:30:28 +0100 Subject: [PATCH 06/17] MAINT: use union type hinting to 3.9+ compliance, use snake case --- pypdf/annotations/_markup_annotations.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pypdf/annotations/_markup_annotations.py b/pypdf/annotations/_markup_annotations.py index 3c91b88673..d667099b1c 100644 --- a/pypdf/annotations/_markup_annotations.py +++ b/pypdf/annotations/_markup_annotations.py @@ -54,14 +54,14 @@ def __init__(self, *, title_bar: Optional[str] = None) -> None: class AbstractPolyLine(MarkupAnnotation, ABC): - def __init__(self, vertices: list[Vertex] | ArrayObject[NumberObject], **kwargs): + def __init__(self, vertices: Union[list[Vertex], ArrayObject[NumberObject]], **kwargs): super().__init__(**kwargs) if len(vertices) == 0 or len(vertices) % 2 != 0: raise ValueError("A polygon needs at least 1 vertex," \ " containing 1 horizontal and 1 vertical position") @staticmethod - def determineVertices(vertices: Union[list[Vertex], ArrayObject[NumberObject]]) -> tuple[list[Vertex], list[NumberObject]]: + def _determine_vertices(vertices: Union[list[Vertex], ArrayObject[NumberObject]]) -> tuple[list[Vertex], list[NumberObject]]: coord_list = [] if isinstance(vertices, ArrayObject): coord_list = vertices @@ -213,12 +213,12 @@ def __init__( class PolyLine(AbstractPolyLine): def __init__( self, - vertices: list[Vertex] | ArrayObject[NumberObject], + vertices: Union[list[Vertex], ArrayObject[NumberObject]], **kwargs: Any, ) -> None: super().__init__(vertices=vertices, **kwargs) - vertices, coord_list = self.determineVertices(vertices) + vertices, coord_list = self._determine_vertices(vertices) self.update( { NameObject("/Subtype"): NameObject("/PolyLine"), @@ -303,11 +303,11 @@ def __init__( class Polygon(AbstractPolyLine): def __init__( self, - vertices: list[Vertex] | ArrayObject[NumberObject], + vertices: Union[list[Vertex], ArrayObject[NumberObject]], **kwargs: Any, ) -> None: super().__init__(vertices=vertices, **kwargs) - vertices, coord_list = self.determineVertices(vertices) + vertices, coord_list = self._determine_vertices(vertices) self.update( { NameObject("/Type"): NameObject("/Annot"), From cbd58d9b9c9a0f9048dade9e17c25c6f80340eab Mon Sep 17 00:00:00 2001 From: HSY-999 Date: Mon, 1 Dec 2025 18:22:52 +0100 Subject: [PATCH 07/17] fixed missing assignment to vertices variable --- pypdf/annotations/_markup_annotations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pypdf/annotations/_markup_annotations.py b/pypdf/annotations/_markup_annotations.py index d667099b1c..9fc767234a 100644 --- a/pypdf/annotations/_markup_annotations.py +++ b/pypdf/annotations/_markup_annotations.py @@ -66,7 +66,7 @@ def _determine_vertices(vertices: Union[list[Vertex], ArrayObject[NumberObject]] if isinstance(vertices, ArrayObject): coord_list = vertices args = [iter(vertices)] * 2 # Adapted def grouper() - zip(*args) # from https://docs.python.org/3.9/library/itertools.html#itertools-recipes + vertices = list(zip(*args)) # from https://docs.python.org/3.9/library/itertools.html#itertools-recipes else: for x, y in vertices: From 2560d169d1235ed5407999efb3380d319e605769 Mon Sep 17 00:00:00 2001 From: HSY-999 Date: Mon, 1 Dec 2025 18:23:25 +0100 Subject: [PATCH 08/17] add test case for issue #3467 --- tests/generic/test_files.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/generic/test_files.py b/tests/generic/test_files.py index 9d488e0681..ed631426ad 100644 --- a/tests/generic/test_files.py +++ b/tests/generic/test_files.py @@ -8,6 +8,7 @@ import pytest from pypdf import PdfReader, PdfWriter +from pypdf.annotations._markup_annotations import Polygon from pypdf.constants import AFRelationship from pypdf.errors import PdfReadError, PyPdfError from pypdf.generic import ( @@ -575,3 +576,28 @@ def test_embedded_file__order(): "test.txt", attachment4.pdf_object.indirect_reference, "xyz.txt", attachment3.pdf_object.indirect_reference, ] + + +def test_merge_page_with_annotation(): + # added and adapted from issue #3467 + writer = PdfWriter() + writer2 = PdfWriter() + writer.add_blank_page(100, 100) + writer2.add_blank_page(100, 100) + + annotation = Polygon( + vertices=[(50, 550), (200, 650), (70, 750), (50, 700)], + ) + + writer.add_annotation(0, annotation) + + page1 = writer.pages[0] + page2 = writer2.pages[0] + page2.merge_page(page1) + + assert page2.annotations[0].get_object()['/Type'] == annotation['/Type'] + assert page2.annotations[0].get_object()['/Subtype'] == annotation['/Subtype'] + assert page2.annotations[0].get_object()['/Vertices'] == annotation['/Vertices'] + assert page2.annotations[0].get_object()['/IT'] == annotation['/IT'] + assert page2.annotations[0].get_object()['/Rect'] == annotation['/Rect'] + From 2a7e5b32b7edaa788fa8ce1d808dce341ae6653c Mon Sep 17 00:00:00 2001 From: HSY-999 Date: Mon, 1 Dec 2025 19:24:25 +0100 Subject: [PATCH 09/17] revert change to vertices arg check for polyline/polygon --- pypdf/annotations/_markup_annotations.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pypdf/annotations/_markup_annotations.py b/pypdf/annotations/_markup_annotations.py index 9fc767234a..b40e437bee 100644 --- a/pypdf/annotations/_markup_annotations.py +++ b/pypdf/annotations/_markup_annotations.py @@ -56,9 +56,8 @@ def __init__(self, *, title_bar: Optional[str] = None) -> None: class AbstractPolyLine(MarkupAnnotation, ABC): def __init__(self, vertices: Union[list[Vertex], ArrayObject[NumberObject]], **kwargs): super().__init__(**kwargs) - if len(vertices) == 0 or len(vertices) % 2 != 0: - raise ValueError("A polygon needs at least 1 vertex," \ - " containing 1 horizontal and 1 vertical position") + if len(vertices) == 0: + raise ValueError(f"A {type(self).__name__} needs at least 1 vertex with two coordinates") @staticmethod def _determine_vertices(vertices: Union[list[Vertex], ArrayObject[NumberObject]]) -> tuple[list[Vertex], list[NumberObject]]: From 8d82c2decdfe815c1118836556be473d63b7a4d0 Mon Sep 17 00:00:00 2001 From: HSY-999 Date: Mon, 1 Dec 2025 20:33:43 +0100 Subject: [PATCH 10/17] precommit style changes --- pypdf/annotations/_markup_annotations.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/pypdf/annotations/_markup_annotations.py b/pypdf/annotations/_markup_annotations.py index b40e437bee..ca128e6ff6 100644 --- a/pypdf/annotations/_markup_annotations.py +++ b/pypdf/annotations/_markup_annotations.py @@ -1,6 +1,5 @@ import sys from abc import ABC -import inspect from typing import Any, Optional, Union from ..constants import AnnotationFlag @@ -54,24 +53,30 @@ def __init__(self, *, title_bar: Optional[str] = None) -> None: class AbstractPolyLine(MarkupAnnotation, ABC): - def __init__(self, vertices: Union[list[Vertex], ArrayObject[NumberObject]], **kwargs): + def __init__( + self, + vertices: Union[list[Vertex], ArrayObject], + **kwargs: Any + ) -> None: super().__init__(**kwargs) if len(vertices) == 0: raise ValueError(f"A {type(self).__name__} needs at least 1 vertex with two coordinates") - + @staticmethod - def _determine_vertices(vertices: Union[list[Vertex], ArrayObject[NumberObject]]) -> tuple[list[Vertex], list[NumberObject]]: - coord_list = [] + def _determine_vertices( + vertices: Union[list[Vertex], ArrayObject] + ) -> tuple[list[Vertex], list[NumberObject]]: + coord_list: ArrayObject = ArrayObject() if isinstance(vertices, ArrayObject): coord_list = vertices - args = [iter(vertices)] * 2 # Adapted def grouper() + args = [iter(vertices)] * 2 # Adapted def grouper() vertices = list(zip(*args)) # from https://docs.python.org/3.9/library/itertools.html#itertools-recipes else: for x, y in vertices: coord_list.append(NumberObject(x)) coord_list.append(NumberObject(y)) - + return vertices, coord_list @@ -212,7 +217,7 @@ def __init__( class PolyLine(AbstractPolyLine): def __init__( self, - vertices: Union[list[Vertex], ArrayObject[NumberObject]], + vertices: Union[list[Vertex], ArrayObject], **kwargs: Any, ) -> None: super().__init__(vertices=vertices, **kwargs) @@ -302,7 +307,7 @@ def __init__( class Polygon(AbstractPolyLine): def __init__( self, - vertices: Union[list[Vertex], ArrayObject[NumberObject]], + vertices: Union[list[Vertex], ArrayObject], **kwargs: Any, ) -> None: super().__init__(vertices=vertices, **kwargs) From f5f56046fcf01fbf5c9036a9a6fc093683087df9 Mon Sep 17 00:00:00 2001 From: HSY-999 Date: Mon, 1 Dec 2025 20:42:40 +0100 Subject: [PATCH 11/17] additional precommit style changes in tests and _data_structures --- pypdf/generic/_data_structures.py | 3 ++- tests/generic/test_files.py | 11 +++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pypdf/generic/_data_structures.py b/pypdf/generic/_data_structures.py index d7a6e75293..7f4ade3cb3 100644 --- a/pypdf/generic/_data_structures.py +++ b/pypdf/generic/_data_structures.py @@ -29,6 +29,7 @@ __author__ = "Mathieu Fenniak" __author_email__ = "biziqe@mathieu.fenniak.net" +import inspect import logging import re import sys @@ -291,7 +292,7 @@ def clone( pass visited: set[tuple[int, int]] = set() # (idnum, generation) - import inspect + kwargs = {} inspector = inspect.getfullargspec(self.__class__.__init__) diff --git a/tests/generic/test_files.py b/tests/generic/test_files.py index ed631426ad..8282a9def7 100644 --- a/tests/generic/test_files.py +++ b/tests/generic/test_files.py @@ -595,9 +595,8 @@ def test_merge_page_with_annotation(): page2 = writer2.pages[0] page2.merge_page(page1) - assert page2.annotations[0].get_object()['/Type'] == annotation['/Type'] - assert page2.annotations[0].get_object()['/Subtype'] == annotation['/Subtype'] - assert page2.annotations[0].get_object()['/Vertices'] == annotation['/Vertices'] - assert page2.annotations[0].get_object()['/IT'] == annotation['/IT'] - assert page2.annotations[0].get_object()['/Rect'] == annotation['/Rect'] - + assert page2.annotations[0].get_object()["/Type"] == annotation["/Type"] + assert page2.annotations[0].get_object()["/Subtype"] == annotation["/Subtype"] + assert page2.annotations[0].get_object()["/Vertices"] == annotation["/Vertices"] + assert page2.annotations[0].get_object()["/IT"] == annotation["/IT"] + assert page2.annotations[0].get_object()["/Rect"] == annotation["/Rect"] From 81e38cd0fa208f109f60760c93af782fb57c2174 Mon Sep 17 00:00:00 2001 From: HSY-999 Date: Fri, 5 Dec 2025 20:56:06 +0100 Subject: [PATCH 12/17] added lines to fix for autogen documentation --- pypdf/annotations/__init__.py | 2 ++ pypdf/annotations/_markup_annotations.py | 19 +++++++++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/pypdf/annotations/__init__.py b/pypdf/annotations/__init__.py index 44ed1dab5c..247f792c9d 100644 --- a/pypdf/annotations/__init__.py +++ b/pypdf/annotations/__init__.py @@ -13,6 +13,7 @@ from ._base import NO_FLAGS, AnnotationDictionary from ._markup_annotations import ( + AbstractPolyLine, Ellipse, FreeText, Highlight, @@ -27,6 +28,7 @@ __all__ = [ "NO_FLAGS", + "AbstractPolyLine", "AnnotationDictionary", "Ellipse", "FreeText", diff --git a/pypdf/annotations/_markup_annotations.py b/pypdf/annotations/_markup_annotations.py index ca128e6ff6..354ba48ce6 100644 --- a/pypdf/annotations/_markup_annotations.py +++ b/pypdf/annotations/_markup_annotations.py @@ -38,14 +38,14 @@ def _get_bounding_rectangle(vertices: list[Vertex]) -> RectangleObject: class MarkupAnnotation(AnnotationDictionary, ABC): - """ - Base class for all markup annotations. + # """ + # Base class for all markup annotations. - Args: - title_bar: Text to be displayed in the title bar of the annotation; - by convention this is the name of the author + # Args: + # title_bar: Text to be displayed in the title bar of the annotation; + # by convention this is the name of the author - """ + # """ def __init__(self, *, title_bar: Optional[str] = None) -> None: if title_bar is not None: @@ -53,6 +53,13 @@ def __init__(self, *, title_bar: Optional[str] = None) -> None: class AbstractPolyLine(MarkupAnnotation, ABC): + # """ + # Base class for Polygon and PolyLine + + # Args: + # vertices: List of coordinates of each vertex; + + # """ def __init__( self, vertices: Union[list[Vertex], ArrayObject], From 0ae775bba652c658e7da8fedef4def1a3285a91d Mon Sep 17 00:00:00 2001 From: HSY-999 Date: Fri, 5 Dec 2025 22:11:35 +0100 Subject: [PATCH 13/17] lower string to pass regex test --- pypdf/annotations/_markup_annotations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pypdf/annotations/_markup_annotations.py b/pypdf/annotations/_markup_annotations.py index 354ba48ce6..6900d915a4 100644 --- a/pypdf/annotations/_markup_annotations.py +++ b/pypdf/annotations/_markup_annotations.py @@ -67,7 +67,7 @@ def __init__( ) -> None: super().__init__(**kwargs) if len(vertices) == 0: - raise ValueError(f"A {type(self).__name__} needs at least 1 vertex with two coordinates") + raise ValueError(f"A {type(self).__name__.lower()} needs at least 1 vertex with two coordinates") @staticmethod def _determine_vertices( From a5d4b6c41905b0c3bca9cf893004d469a99c9bf4 Mon Sep 17 00:00:00 2001 From: HSY-999 Date: Fri, 5 Dec 2025 22:32:35 +0100 Subject: [PATCH 14/17] uncommented class descriptions strings --- pypdf/annotations/_markup_annotations.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pypdf/annotations/_markup_annotations.py b/pypdf/annotations/_markup_annotations.py index 6900d915a4..3de7c6785c 100644 --- a/pypdf/annotations/_markup_annotations.py +++ b/pypdf/annotations/_markup_annotations.py @@ -38,14 +38,14 @@ def _get_bounding_rectangle(vertices: list[Vertex]) -> RectangleObject: class MarkupAnnotation(AnnotationDictionary, ABC): - # """ - # Base class for all markup annotations. + """ + Base class for all markup annotations. - # Args: - # title_bar: Text to be displayed in the title bar of the annotation; - # by convention this is the name of the author + Args: + title_bar: Text to be displayed in the title bar of the annotation; + by convention this is the name of the author - # """ + """ def __init__(self, *, title_bar: Optional[str] = None) -> None: if title_bar is not None: @@ -53,13 +53,13 @@ def __init__(self, *, title_bar: Optional[str] = None) -> None: class AbstractPolyLine(MarkupAnnotation, ABC): - # """ - # Base class for Polygon and PolyLine + """ + Base class for Polygon and PolyLine - # Args: - # vertices: List of coordinates of each vertex; + Args: + vertices: List of coordinates of each vertex; - # """ + """ def __init__( self, vertices: Union[list[Vertex], ArrayObject], From 817e981655f9ab9bb3bba514c40579f6a8a34d62 Mon Sep 17 00:00:00 2001 From: HSY-999 Date: Mon, 8 Dec 2025 21:46:36 +0100 Subject: [PATCH 15/17] add option to map different keyword to arg name if necessary --- pypdf/generic/_data_structures.py | 40 ++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/pypdf/generic/_data_structures.py b/pypdf/generic/_data_structures.py index 7f4ade3cb3..aa88105fc0 100644 --- a/pypdf/generic/_data_structures.py +++ b/pypdf/generic/_data_structures.py @@ -264,6 +264,40 @@ def read_from_stream( class DictionaryObject(dict[Any, Any], PdfObject): + + _init_alt_arg_names_: Optional[dict[str, str]] = None + + + """ + Used to map DictionaryObject keyword to __init__ arg names + when cloning. key -> arg_name. + + Args: + key: string identifying DictionaryObject keyword + arg_name: string identifying argument name (value) + """ + def set_alt_arg_name(self, key: str, arg_name: str) -> None: + if self._init_alt_arg_names_ is None: + self._init_alt_arg_names_ = {} + + if not isinstance(key, str) or not isinstance(arg_name, str): + raise TypeError + + self._init_alt_arg_names_[key] = arg_name + + """ + Used to return __init__ arg names when cloning + Args: + key: string identifying DictionaryObject keyword + Returns: + Returns None if not found, else the string representing the + argument name + """ + def get_alt_arg_name(self, key: str) -> Optional[str]: + if self._init_alt_arg_names_ is None or not isinstance(key, str): + return None + return self._init_alt_arg_names_.get(key, None) + def replicate( self, pdf_dest: PdfWriterProtocol, @@ -297,8 +331,12 @@ def clone( inspector = inspect.getfullargspec(self.__class__.__init__) for key, val in self.items(): + alt_arg_name = self.get_alt_arg_name(key) key_stripped = key.removeprefix("/").lower() - if key_stripped in inspector.args or key_stripped in inspector.kwonlyargs: + + if alt_arg_name: + kwargs[alt_arg_name] = val + elif key_stripped in inspector.args or key_stripped in inspector.kwonlyargs: kwargs[key_stripped] = val d__ = cast( From 330f2533c2bfce83e0e03442792657f337d1744d Mon Sep 17 00:00:00 2001 From: HSY-999 Date: Tue, 9 Dec 2025 18:25:53 +0100 Subject: [PATCH 16/17] added test case for setting alt arg names --- tests/test_generic.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test_generic.py b/tests/test_generic.py index fde9ddd791..4dfc15a318 100644 --- a/tests/test_generic.py +++ b/tests/test_generic.py @@ -54,6 +54,18 @@ RESOURCE_ROOT = PROJECT_ROOT / "resources" +class DummyDictObject(DictionaryObject): + def __init__(self, test_name: str) -> None: + super().__init__() + self.set_alt_arg_name("/TestName", "test_name") + + self.update( + { + NameObject("/TestName"): test_name + } + ) + + class ChildDummy(DictionaryObject): @property def indirect_reference(self): @@ -1282,6 +1294,14 @@ def test_coverage_arrayobject(): assert isinstance(k, int) assert isinstance(v, PdfObject) +def test_alt_keyword_when_cloning(): + obj = DummyDictObject(test_name="testval") + obj2: DummyDictObject = None + + clone_obj = obj.clone(obj2) + + assert clone_obj.get("/TestName") == "testval" + def test_coverage_streamobject(): writer = PdfWriter() From bb710e165ddabd2cebe6798d79be9241230737cf Mon Sep 17 00:00:00 2001 From: HSY-999 Date: Tue, 9 Dec 2025 18:52:12 +0100 Subject: [PATCH 17/17] add test case for type error --- tests/test_generic.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_generic.py b/tests/test_generic.py index 4dfc15a318..b06c8975df 100644 --- a/tests/test_generic.py +++ b/tests/test_generic.py @@ -1302,6 +1302,14 @@ def test_alt_keyword_when_cloning(): assert clone_obj.get("/TestName") == "testval" +def test_type_error_when_using_alt_arg_keys(): + dict_obj = DictionaryObject() + with pytest.raises(TypeError): + dict_obj.set_alt_arg_name(1, "test_key") + + with pytest.raises(TypeError): + dict_obj.set_alt_arg_name("test_key", 1) + def test_coverage_streamobject(): writer = PdfWriter()