Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: Add circle annotation support #1556

Merged
merged 7 commits into from
Jan 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion docs/user/adding-pdf-annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ page = reader.pages[0]
writer = PdfWriter()
writer.add_page(page)

# Add the line
# Add the rectangle
annotation = AnnotationBuilder.rectangle(
rect=(50, 550, 200, 650),
)
Expand All @@ -119,6 +119,30 @@ If you want the rectangle to be filled, use the `interiour_color="ff0000"` param

This method uses the "square" annotation type of the PDF format.


## Ellipse

If you want to add a circle like this:

![](annotation-circle.png)

```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 rectangle
annotation = AnnotationBuilder.ellipse(
rect=(50, 550, 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)
```

## Polygon

If you want to add a polygon like this:
Expand Down
Binary file added docs/user/annotation-circle.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
71 changes: 52 additions & 19 deletions pypdf/generic/_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ def text(
Add text annotation.

Args:
rect:
or array of four integers specifying the clickable rectangular area
``[xLL, yLL, xUR, yUR]``
rect: array of four integers ``[xLL, yLL, xUR, yUR]``
specifying the clickable rectangular area
text: The text that is added to the document
open:
flags:

Expand Down Expand Up @@ -76,15 +76,15 @@ def free_text(

Args:
text: Text to be added
rect: or array of four integers
specifying the clickable rectangular area ``[xLL, yLL, xUR, yUR]``
rect: array of four integers ``[xLL, yLL, xUR, yUR]``
specifying the clickable rectangular area
font: Name of the Font, e.g. 'Helvetica'
bold: Print the text in bold
italic: Print the text in italic
font_size: How big the text will be, e.g. '14pt'
font_color: Hex-string for the color
border_color: Hex-string for the border color
background_color: Hex-string for the background of the annotation
font_color: Hex-string for the color, e.g. cdcdcd
border_color: Hex-string for the border color, e.g. cdcdcd
background_color: Hex-string for the background of the annotation, e.g. cdcdcd

Returns:
A dictionary object representing the annotation.
Expand Down Expand Up @@ -135,9 +135,8 @@ def line(
Args:
p1: First point
p2: Second point
rect: or array of four
integers specifying the clickable rectangular area
``[xLL, yLL, xUR, yUR]``
rect: array of four integers ``[xLL, yLL, xUR, yUR]``
specifying the clickable rectangular area
text: Text to be displayed as the line annotation
title_bar: Text to be displayed in the title bar of the
annotation; by convention this is the name of the author
Expand Down Expand Up @@ -185,12 +184,13 @@ def rectangle(
"""
Draw a rectangle on the PDF.

This method uses the /Square annotation type of the PDF format.

Args:
rect: or array of four
integers specifying the clickable rectangular area
``[xLL, yLL, xUR, yUR]``
rect:
interiour_color:
rect: array of four integers ``[xLL, yLL, xUR, yUR]``
specifying the clickable rectangular area
interiour_color: None or hex-string for the color, e.g. cdcdcd
If None is used, the interiour is transparent.

Returns:
A dictionary object representing the annotation.
Expand All @@ -210,6 +210,40 @@ def rectangle(

return square_obj

@staticmethod
def ellipse(
rect: Union[RectangleObject, Tuple[float, float, float, float]],
interiour_color: Optional[str] = None,
) -> DictionaryObject:
"""
Draw a rectangle on the PDF.

This method uses the /Circle annotation type of the PDF format.

Args:
rect: array of four integers ``[xLL, yLL, xUR, yUR]`` specifying
the bounding box of the ellipse
interiour_color: None or hex-string for the color, e.g. cdcdcd
If None is used, the interiour is transparent.

Returns:
A dictionary object representing the annotation.
"""
ellipse_obj = DictionaryObject(
{
NameObject("/Type"): NameObject("/Annot"),
NameObject("/Subtype"): NameObject("/Circle"),
NameObject("/Rect"): RectangleObject(rect),
}
)

if interiour_color:
ellipse_obj[NameObject("/IC")] = ArrayObject(
[FloatObject(n) for n in hex_to_rgb(interiour_color)]
)

return ellipse_obj

@staticmethod
def polygon(vertices: List[Tuple[float, float]]) -> DictionaryObject:
if len(vertices) == 0:
Expand Down Expand Up @@ -254,9 +288,8 @@ def link(
An internal link requires the target_page_index, fit, and fit args.

Args:
rect: or array of four
integers specifying the clickable rectangular area
``[xLL, yLL, xUR, yUR]``
rect: array of four integers ``[xLL, yLL, xUR, yUR]``
specifying the clickable rectangular area
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.
Expand Down
28 changes: 28 additions & 0 deletions tests/test_generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,34 @@ def test_annotation_builder_square():
os.remove(target) # comment this out for manual inspection


def test_annotation_builder_circle():
# Arrange
pdf_path = RESOURCE_ROOT / "crazyones.pdf"
reader = PdfReader(pdf_path)
page = reader.pages[0]
writer = PdfWriter()
writer.add_page(page)

# Act
circle_annotation = AnnotationBuilder.ellipse(
rect=(50, 550, 200, 650), interiour_color="ff0000"
)
writer.add_annotation(0, circle_annotation)

diameter = 100
circle_annotation = AnnotationBuilder.ellipse(
rect=(110, 500, 110 + diameter, 500 + diameter),
)
writer.add_annotation(0, circle_annotation)

# Assert: You need to inspect the file manually
target = "annotated-pdf-circle.pdf"
with open(target, "wb") as fp:
writer.write(fp)

os.remove(target) # comment this out for manual inspection


def test_annotation_builder_link():
# Arrange
pdf_path = RESOURCE_ROOT / "outline-without-title.pdf"
Expand Down