Skip to content

Commit

Permalink
ENH: Add PolyLine annoation support
Browse files Browse the repository at this point in the history
See #107
  • Loading branch information
MartinThoma committed Mar 19, 2023
1 parent 31b2fa0 commit cdd193a
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 9 deletions.
26 changes: 26 additions & 0 deletions docs/user/adding-pdf-annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <pypdf.generic.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:
Expand Down
Binary file added docs/user/annotation-polyline.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
52 changes: 43 additions & 9 deletions pypdf/generic/_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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]],
Expand Down Expand Up @@ -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))
Expand All @@ -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
Expand Down
28 changes: 28 additions & 0 deletions tests/test_generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit cdd193a

Please sign in to comment.