Skip to content

Commit

Permalink
ENH: Add AnnotationBuilder.popup (#1665)
Browse files Browse the repository at this point in the history
Fixes #1195

See #107
  • Loading branch information
pubpub-zz authored Mar 26, 2023
1 parent 3ecdedb commit 4fc0040
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 9 deletions.
34 changes: 34 additions & 0 deletions docs/user/adding-pdf-annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,40 @@ with open("annotated-pdf.pdf", "wb") as fp:
writer.write(fp)
```

## Popup

Manage the Popup windows for markups. looks like this:

![](annotation-popup.png)

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

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

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

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

popup_annotation = AnnotationBuilder.popup(
rect=(50, 550, 200, 650),
open=True,
parent=text_annotation, # use the output of add_annotation
)

writer.write("annotated-pdf-popup.pdf")
```

## Link

If you want to add a link, you can use
Expand Down
Binary file added docs/user/annotation-popup.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
44 changes: 36 additions & 8 deletions pypdf/_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2135,7 +2135,7 @@ def add_link(
border: Optional[ArrayObject] = None,
fit: FitType = "/Fit",
*args: ZoomArgType,
) -> None:
) -> DictionaryObject:
deprecation_with_replacement(
"add_link", "add_annotation(AnnotationBuilder.link(...))"
)
Expand Down Expand Up @@ -2175,7 +2175,7 @@ def addLink(
deprecate_with_replacement(
"addLink", "add_annotation(AnnotationBuilder.link(...))", "4.0.0"
)
return self.add_link(pagenum, page_destination, rect, border, fit, *args)
self.add_link(pagenum, page_destination, rect, border, fit, *args)

_valid_layouts = (
"/NoLayout",
Expand Down Expand Up @@ -2421,17 +2421,40 @@ def pageMode(self, mode: PagemodeType) -> None: # deprecated
deprecation_with_replacement("pageMode", "page_mode", "3.0.0")
self.page_mode = mode

def add_annotation(self, page_number: int, annotation: Dict[str, Any]) -> None:
def add_annotation(
self,
page_number: Union[int, PageObject],
annotation: Dict[str, Any],
) -> DictionaryObject:
"""
Add a single annotation to the page.
The added annotation must be a new annotation.
It can not be recycled.
Args:
page_number: PageObject or page index.
annotation: Annotation to be added (created with annotation).
Returns:
The inserted object
This can be used for pop-up creation, for example
"""
page = page_number
if isinstance(page, int):
page = self.pages[page]
elif not isinstance(page, PageObject):
raise TypeError("page: invalid type")

to_add = cast(DictionaryObject, _pdf_objectify(annotation))
to_add[NameObject("/P")] = self.get_object(self._pages)["/Kids"][page_number] # type: ignore
page = self.pages[page_number]
to_add[NameObject("/P")] = page.indirect_reference

if page.annotations is None:
page[NameObject("/Annots")] = ArrayObject()
assert page.annotations is not None

# Internal link annotations need the correct object type for the
# destination
if to_add.get("/Subtype") == "/Link" and NameObject("/Dest") in to_add:
if to_add.get("/Subtype") == "/Link" and "/Dest" in to_add:
tmp = cast(dict, to_add[NameObject("/Dest")])
dest = Destination(
NameObject("/LinkName"),
Expand All @@ -2442,9 +2465,14 @@ def add_annotation(self, page_number: int, annotation: Dict[str, Any]) -> None:
)
to_add[NameObject("/Dest")] = dest.dest_array

ind_obj = self._add_object(to_add)
page.annotations.append(self._add_object(to_add))

if to_add.get("/Subtype") == "/Popup" and NameObject("/Parent") in to_add:
cast(DictionaryObject, to_add["/Parent"].get_object())[
NameObject("/Popup")
] = to_add.indirect_reference

page.annotations.append(ind_obj)
return to_add

def clean_page(self, page: Union[PageObject, IndirectObject]) -> PageObject:
"""
Expand Down
48 changes: 47 additions & 1 deletion pypdf/generic/_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from ._data_structures import ArrayObject, DictionaryObject
from ._fit import DEFAULT_FIT, Fit
from ._rectangle import RectangleObject
from ._utils import hex_to_rgb
from ._utils import hex_to_rgb, logger_warning


def _get_bounding_rectangle(vertices: List[Tuple[float, float]]) -> RectangleObject:
Expand Down Expand Up @@ -143,6 +143,52 @@ def free_text(
)
return free_text

@staticmethod
def popup(
rect: Union[RectangleObject, Tuple[float, float, float, float]],
flags: int = 0,
parent: Optional[DictionaryObject] = None,
open: bool = False,
) -> DictionaryObject:
"""
Add a popup to the document.
Args:
rect:
Specifies the clickable rectangular area as `[xLL, yLL, xUR, yUR]`
flags:
1 - invisible, 2 - hidden, 3 - print, 4 - no zoom,
5 - no rotate, 6 - no view, 7 - read only, 8 - locked,
9 - toggle no view, 10 - locked contents
open:
Whether the popup should be shown directly (default is False).
parent:
The contents of the popup. Create this via the AnnotationBuilder.
Returns:
A dictionary object representing the annotation.
"""
popup_obj = DictionaryObject(
{
NameObject("/Type"): NameObject("/Annot"),
NameObject("/Subtype"): NameObject("/Popup"),
NameObject("/Rect"): RectangleObject(rect),
NameObject("/Open"): BooleanObject(open),
NameObject("/Flags"): NumberObject(flags),
}
)
if parent:
# This needs to be an indirect object
try:
popup_obj[NameObject("/Parent")] = parent.indirect_reference
except AttributeError:
logger_warning(
"Unregistered Parent object : No Parent field set",
__name__,
)

return popup_obj

@staticmethod
def line(
p1: Tuple[float, float],
Expand Down
29 changes: 29 additions & 0 deletions tests/test_generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -939,6 +939,35 @@ def test_annotation_builder_text(pdf_file_path):
writer.write(fp)


def test_annotation_builder_popup():
# Arrange
pdf_path = RESOURCE_ROOT / "outline-without-title.pdf"
reader = PdfReader(pdf_path)
page = reader.pages[0]
writer = PdfWriter()
writer.add_page(page)

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

popup_annotation = AnnotationBuilder.popup(
rect=(50, 550, 200, 650),
open=True,
parent=ta, # prefer to use for evolutivity
)

writer.add_annotation(writer.pages[0], popup_annotation)

target = "annotated-pdf-popup.pdf"
writer.write(target)
Path(target).unlink() # comment this out for manual inspection


def test_checkboxradiobuttonattributes_opt():
assert "/Opt" in CheckboxRadioButtonAttributes.attributes_dict()

Expand Down

0 comments on commit 4fc0040

Please sign in to comment.