diff --git a/docs/user/adding-pdf-annotations.md b/docs/user/adding-pdf-annotations.md index 9db376375..b65892c53 100644 --- a/docs/user/adding-pdf-annotations.md +++ b/docs/user/adding-pdf-annotations.md @@ -89,6 +89,32 @@ with open("annotated-pdf.pdf", "wb") as fp: writer.write(fp) ``` +## PolyLine + +If you want to add a line like this: + +![](annotation-polyline.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 polyline +annotation = AnnotationBuilder.polyline( + vertices=[(50, 550), (200, 650), (70, 750), (50, 700)], +) +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) +``` + ## Rectangle If you want to add a rectangle like this: diff --git a/docs/user/annotation-polyline.png b/docs/user/annotation-polyline.png new file mode 100644 index 000000000..5d06d9191 Binary files /dev/null and b/docs/user/annotation-polyline.png differ diff --git a/pypdf/generic/_annotations.py b/pypdf/generic/_annotations.py index 68e31a250..7ea2db0fc 100644 --- a/pypdf/generic/_annotations.py +++ b/pypdf/generic/_annotations.py @@ -13,6 +13,18 @@ from ._utils import hex_to_rgb +def _get_bounding_rectangle(vertices: List[Tuple[float, float]]) -> RectangleObject: + x_min, y_min = vertices[0][0], vertices[0][1] + x_max, y_max = vertices[0][0], vertices[0][1] + for x, y in vertices: + x_min = min(x_min, x) + y_min = min(y_min, y) + x_max = min(x_max, x) + y_max = min(y_max, y) + rect = RectangleObject((x_min, y_min, x_max, y_max)) + return rect + + class AnnotationBuilder: """ The AnnotationBuilder creates dictionaries representing PDF annotations. @@ -186,6 +198,35 @@ def line( ) return line_obj + @staticmethod + def polyline( + vertices: List[Tuple[float, float]], + ) -> DictionaryObject: + """ + Draw a polyline on the PDF. + + Args: + vertices: Array specifying the vertices (x, y) coordinates of the poly-line. + + Returns: + A dictionary object representing the annotation. + """ + if len(vertices) == 0: + raise ValueError("A polygon needs at least 1 vertex with two coordinates") + coord_list = [] + for x, y in vertices: + coord_list.append(NumberObject(x)) + coord_list.append(NumberObject(y)) + polyline_obj = DictionaryObject( + { + NameObject("/Type"): NameObject("/Annot"), + NameObject("/Subtype"): NameObject("/PolyLine"), + NameObject("/Vertices"): ArrayObject(coord_list), + NameObject("/Rect"): RectangleObject(_get_bounding_rectangle(vertices)), + } + ) + return polyline_obj + @staticmethod def rectangle( rect: Union[RectangleObject, Tuple[float, float, float, float]], @@ -258,14 +299,7 @@ def ellipse( def polygon(vertices: List[Tuple[float, float]]) -> DictionaryObject: if len(vertices) == 0: raise ValueError("A polygon needs at least 1 vertex with two coordinates") - x_min, y_min = vertices[0][0], vertices[0][1] - x_max, y_max = vertices[0][0], vertices[0][1] - for x, y in vertices: - x_min = min(x_min, x) - y_min = min(y_min, y) - x_max = min(x_max, x) - y_max = min(y_max, y) - rect = RectangleObject((x_min, y_min, x_max, y_max)) + coord_list = [] for x, y in vertices: coord_list.append(NumberObject(x)) @@ -276,7 +310,7 @@ def polygon(vertices: List[Tuple[float, float]]) -> DictionaryObject: NameObject("/Subtype"): NameObject("/Polygon"), NameObject("/Vertices"): ArrayObject(coord_list), NameObject("/IT"): NameObject("PolygonCloud"), - NameObject("/Rect"): RectangleObject(rect), + NameObject("/Rect"): RectangleObject(_get_bounding_rectangle(vertices)), } ) return obj diff --git a/tests/test_generic.py b/tests/test_generic.py index ef6b6eac9..10c7a9db1 100644 --- a/tests/test_generic.py +++ b/tests/test_generic.py @@ -781,6 +781,34 @@ def test_annotation_builder_polygon(): Path(target).unlink() # comment this out for manual inspection +def test_annotation_builder_polyline(): + # Arrange + pdf_path = RESOURCE_ROOT / "crazyones.pdf" + reader = PdfReader(pdf_path) + page = reader.pages[0] + writer = PdfWriter() + writer.add_page(page) + + # Act + with pytest.raises(ValueError) as exc: + AnnotationBuilder.polyline( + vertices=[], + ) + assert exc.value.args[0] == "A polygon needs at least 1 vertex with two coordinates" + + annotation = AnnotationBuilder.polyline( + vertices=[(50, 550), (200, 650), (70, 750), (50, 700)], + ) + writer.add_annotation(0, annotation) + + # Assert: You need to inspect the file manually + target = "annotated-pdf.pdf" + with open(target, "wb") as fp: + writer.write(fp) + + Path(target).unlink() # comment this out for manual inspection + + def test_annotation_builder_line(): # Arrange pdf_path = RESOURCE_ROOT / "crazyones.pdf"