Skip to content

Commit

Permalink
MAINT: Cleanup of annotations (#1745)
Browse files Browse the repository at this point in the history
The goal of this PR is to create a more intuitive interface for creating annotations. The AnnotationBuild gets deprecated in favor of several annotation classes, e.g.

```python
# old
from pypdf.generic import AnnotationBuilder
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",
    background_color="cdcdcd",
)

# new
from pypdf.annotations import FreeText
annotation = FreeText(
    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",
    background_color="cdcdcd",
)
```

* `pypdf/generic/_annotations.py` ➔ `pypdf/annotations/`
* Create abstract base class AnnotationDictionary
* Create abstract base class MarkupAnnotation which inherits from AnnotationDictionary. Most annotations are MarkupAnnotations.
* Deprecated AnnotationBuilder
* Ensure the AnnotationBuilder is not used in the docs

Closes #107
  • Loading branch information
MartinThoma authored Jul 29, 2023
1 parent 8abd34a commit abd2673
Show file tree
Hide file tree
Showing 15 changed files with 1,036 additions and 618 deletions.
2 changes: 1 addition & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ You can contribute to `pypdf on GitHub <https://github.com/py-pdf/pypdf>`_.
modules/RectangleObject
modules/Field
modules/PageRange
modules/AnnotationBuilder
modules/annotations
modules/Fit
modules/PaperSize

Expand Down
7 changes: 0 additions & 7 deletions docs/modules/AnnotationBuilder.rst

This file was deleted.

7 changes: 7 additions & 0 deletions docs/modules/annotations.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
The annotations module
----------------------

.. automodule:: pypdf.annotations
:members:
:undoc-members:
:show-inheritance:
72 changes: 50 additions & 22 deletions docs/user/adding-pdf-annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ If you want to add text in a box like this

![](free-text-annotation.png)

you can use the {py:class}`AnnotationBuilder <pypdf.generic.AnnotationBuilder>`:
you can use the {py:class}`FreeText <pypdf.annotations.FreeText>`:

```python
from pypdf import PdfReader, PdfWriter
from pypdf.generic import AnnotationBuilder
from pypdf.annotations import FreeText

# Fill the writer with the pages you want
pdf_path = os.path.join(RESOURCE_ROOT, "crazyones.pdf")
Expand All @@ -36,8 +36,8 @@ writer = PdfWriter()
writer.add_page(page)

# Create the annotation and add it
annotation = AnnotationBuilder.free_text(
"Hello World\nThis is the second line!",
annotation = FreeText(
text="Hello World\nThis is the second line!",
rect=(50, 550, 200, 650),
font="Arial",
bold=True,
Expand Down Expand Up @@ -66,17 +66,20 @@ If you want to add a line like this:

![](annotation-line.png)

you can use the {py:class}`AnnotationBuilder <pypdf.generic.AnnotationBuilder>`:
you can use {py:class}`Line <pypdf.annotations.Line>`:

```python
from pypdf import PdfReader, PdfWriter
from pypdf.annotations import Line

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(
annotation = Line(
text="Hello World\nLine2",
rect=(50, 550, 200, 650),
p1=(50, 550),
Expand All @@ -95,17 +98,20 @@ If you want to add a line like this:

![](annotation-polyline.png)

you can use the {py:class}`AnnotationBuilder <pypdf.generic.AnnotationBuilder>`:
you can use {py:class}`PolyLine <pypdf.annotations.PolyLine>`:

```python
from pypdf import PdfReader, PdfWriter
from pypdf.annotations import Polyline

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(
annotation = Polyline(
vertices=[(50, 550), (200, 650), (70, 750), (50, 700)],
)
writer.add_annotation(page_number=0, annotation=annotation)
Expand All @@ -121,17 +127,20 @@ If you want to add a rectangle like this:

![](annotation-square.png)

you can use the {py:class}`AnnotationBuilder <pypdf.generic.AnnotationBuilder>`:
you can use {py:class}`Rectangle <pypdf.annotations.Rectangle>`:

```python
from pypdf import PdfReader, PdfWriter
from pypdf.annotations import Rectangle

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.rectangle(
annotation = Rectangle(
rect=(50, 550, 200, 650),
)
writer.add_annotation(page_number=0, annotation=annotation)
Expand All @@ -152,15 +161,20 @@ If you want to add a circle like this:

![](annotation-circle.png)

you can use {py:class}`Ellipse <pypdf.annotations.Ellipse>`:

```python
from pypdf import PdfReader, PdfWriter
from pypdf.annotations import Ellipse

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(
annotation = Ellipse(
rect=(50, 550, 200, 650),
)
writer.add_annotation(page_number=0, annotation=annotation)
Expand All @@ -176,17 +190,20 @@ If you want to add a polygon like this:

![](annotation-polygon.png)

you can use the {py:class}`AnnotationBuilder <pypdf.generic.AnnotationBuilder>`:
you can use {py:class}`Polygon <pypdf.annotations.Polygon>`:

```python
from pypdf import PdfReader, PdfWriter
from pypdf.annotations import Polygon

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.polygon(
annotation = Polygon(
vertices=[(50, 550), (200, 650), (70, 750), (50, 700)],
)
writer.add_annotation(page_number=0, annotation=annotation)
Expand All @@ -202,26 +219,28 @@ Manage the Popup windows for markups. looks like this:

![](annotation-popup.png)

you can use the {py:class}`AnnotationBuilder <pypdf.generic.AnnotationBuilder>`:
you can use the {py:class}`Popup <pypdf.annotations.Popup>`:

you have to use the returned result from add_annotation() to fill-up the

```python
from pypdf.annotations import Popup, Text

# Arrange
writer = pypdf.PdfWriter()
writer.append(os.path.join(RESOURCE_ROOT, "crazyones.pdf"), [0])

# Act
text_annotation = writer.add_annotation(
0,
AnnotationBuilder.text(
Text(
text="Hello World\nThis is the second line!",
rect=(50, 550, 200, 650),
open=True,
),
)

popup_annotation = AnnotationBuilder.popup(
popup_annotation = Popup(
rect=(50, 550, 200, 650),
open=True,
parent=text_annotation, # use the output of add_annotation
Expand All @@ -233,17 +252,20 @@ writer.write("annotated-pdf-popup.pdf")
## Link

If you want to add a link, you can use
the {py:class}`AnnotationBuilder <pypdf.generic.AnnotationBuilder>`:
{py:class}`Link <pypdf.annotations.Link>`:

```python
from pypdf import PdfReader, PdfWriter
from pypdf.annotations import Link

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(
annotation = Link(
rect=(50, 550, 200, 650),
url="https://martin-thoma.com/",
)
Expand All @@ -257,14 +279,17 @@ with open("annotated-pdf.pdf", "wb") as fp:
You can also add internal links:

```python
from pypdf import PdfReader, PdfWriter
from pypdf.annotations import Link

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(
annotation = Link(
rect=(50, 550, 200, 650), target_page_index=3, fit="/FitH", fit_args=(123,)
)
writer.add_annotation(page_number=0, annotation=annotation)
Expand All @@ -287,17 +312,20 @@ If you want to highlight text like this:

![](annotation-highlight.png)

you can use the {py:class}`AnnotationBuilder <pypdf.generic.AnnotationBuilder>`:
you can use the {py:class}`Highlight <pypdf.annotations.Highlight>`:

```python
from pypdf import PdfReader, PdfWriter
from pypdf.annotations import Highlight

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.polygon(
# Add the highlight
annotation = Highlight(
vertices=[(50, 550), (200, 650), (70, 750), (50, 700)],
)
writer.add_annotation(page_number=0, annotation=annotation)
Expand Down
8 changes: 4 additions & 4 deletions pypdf/_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
deprecation_with_replacement,
logger_warning,
)
from .annotations import Link
from .constants import AnnotationDictionaryAttributes as AA
from .constants import CatalogAttributes as CA
from .constants import (
Expand All @@ -90,7 +91,6 @@
from .errors import PyPdfError
from .generic import (
PAGE_FIT,
AnnotationBuilder,
ArrayObject,
BooleanObject,
ByteStringObject,
Expand Down Expand Up @@ -2352,7 +2352,7 @@ def add_link(
*args: ZoomArgType,
) -> DictionaryObject:
deprecation_with_replacement(
"add_link", "add_annotation(AnnotationBuilder.link(...))"
"add_link", "add_annotation(pypdf.annotations.Link(...))"
)

if isinstance(rect, str):
Expand All @@ -2365,7 +2365,7 @@ def add_link(
else:
rect = RectangleObject(rect)

annotation = AnnotationBuilder.link(
annotation = Link(
rect=rect,
border=border,
target_page_index=page_destination,
Expand All @@ -2388,7 +2388,7 @@ def addLink(
.. deprecated:: 1.28.0
"""
deprecate_with_replacement(
"addLink", "add_annotation(AnnotationBuilder.link(...))", "4.0.0"
"addLink", "add_annotation(pypdf.annotations.Link(...))", "4.0.0"
)
self.add_link(pagenum, page_destination, rect, border, fit, *args)

Expand Down
46 changes: 46 additions & 0 deletions pypdf/annotations/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""
PDF specifies several annotation types which pypdf makes available here.
The names of the annotations and their attributes do not reflect the names in
the specification in all cases. For example, the PDF standard defines a
'Square' annotation that does not actually need to be square. For this reason,
pypdf calls it 'Rectangle'.
At their core, all annotation types are DictionaryObjects. That means if pypdf
does not implement a feature, users can easily extend the given functionality.
"""


from ._base import NO_FLAGS, AnnotationDictionary
from ._markup_annotations import (
Ellipse,
FreeText,
Highlight,
Line,
Link,
MarkupAnnotation,
Polygon,
PolyLine,
Rectangle,
Text,
)
from ._non_markup_annotations import Popup

__all__ = [
"NO_FLAGS",
# Export abstract base classes so that they are shown in the docs
"AnnotationDictionary",
"MarkupAnnotation",
# markup annotations
"Ellipse",
"FreeText",
"Highlight",
"Line",
"Link",
"Polygon",
"PolyLine",
"Rectangle",
"Text",
# Non-markup annotations
"Popup",
]
27 changes: 27 additions & 0 deletions pypdf/annotations/_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from abc import ABC

from ..constants import AnnotationFlag
from ..generic import NameObject, NumberObject
from ..generic._data_structures import DictionaryObject


class AnnotationDictionary(DictionaryObject, ABC):
def __init__(self) -> None:
from ..generic._base import NameObject

# "rect" should not be added here as PolyLine can automatically set it
self[NameObject("/Type")] = NameObject("/Annot")
# The flags was NOT added to the constructor on purpose: We expect that
# most users don't want to change the default. If they want, they
# can use the property. The default is 0.

@property
def flags(self) -> AnnotationFlag:
return self.get(NameObject("/F"), AnnotationFlag(0))

@flags.setter
def flags(self, value: AnnotationFlag) -> None:
self[NameObject("/F")] = NumberObject(value)


NO_FLAGS = AnnotationFlag(0)
Loading

0 comments on commit abd2673

Please sign in to comment.