Skip to content

Commit

Permalink
ENH: Add popup annotation support
Browse files Browse the repository at this point in the history
See #107

Closes #1195
  • Loading branch information
MartinThoma committed Aug 3, 2022
1 parent 4aa9ec9 commit cda445e
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 5 deletions.
7 changes: 7 additions & 0 deletions PyPDF2/_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -1874,9 +1874,16 @@ def add_annotation(self, page_number: int, annotation: Dict[str, Any]) -> None:
*tmp["fit_args"],
)
to_add[NameObject("/Dest")] = dest.dest_array
elif to_add.get("/Subtype") == "/Popup" and NameObject("/Parent") in to_add:
tmp = cast(dict, to_add[NameObject("/Parent")])
ind_obj_parent = self._add_object(tmp)
to_add[NameObject("/Parent")] = ind_obj_parent

ind_obj = self._add_object(to_add)

if to_add.get("/Subtype") == "/Popup" and NameObject("/Parent") in to_add:
tmp[NameObject("/Popup")] = ind_obj

page.annotations.append(ind_obj)


Expand Down
95 changes: 91 additions & 4 deletions PyPDF2/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -2079,6 +2079,60 @@ def hex_to_rgb(value: str) -> Tuple[float, float, float]:
class AnnotationBuilder:
from .types import FitType, ZoomArgType

@staticmethod
def text(
rect: Union[RectangleObject, Tuple[float, float, float, float]],
text: str,
open: bool = False,
flags: int = 0,
):
"""
Add text.
:param :class:`RectangleObject<PyPDF2.generic.RectangleObject>` rect:
or array of four integers specifying the clickable rectangular area
``[xLL, yLL, xUR, yUR]``
"""
# TABLE 8.23 Additional entries specific to a text annotation
text_obj = DictionaryObject(
{
NameObject("/Type"): NameObject(PG.ANNOTS),
NameObject("/Subtype"): NameObject("/Text"),
NameObject("/Rect"): RectangleObject(rect),
NameObject("/Contents"): TextStringObject(text),
NameObject("/Open"): BooleanObject(open),
NameObject("/Flags"): NumberObject(flags),
NameObject("/AP"): DictionaryObject(
{
NameObject("/N"): DictionaryObject(
{
NameObject("/Filter"): NameObject("/FlateDecode"),
NameObject("/Type"): NameObject("/XObject"),
NameObject("/Subtype"): NameObject("/Form"),
NameObject("/FormType"): NumberObject(1),
NameObject("/BBox"): ArrayObject(
[
NumberObject(0),
NumberObject(0),
NumberObject(18),
NumberObject(22),
]
),
NameObject("/Resources"): DictionaryObject(
{
NameObject("/ProcSet"): ArrayObject(
[NameObject("/PDF")]
)
}
),
}
)
}
),
}
)
return text_obj

@staticmethod
def free_text(
text: str,
Expand All @@ -2095,9 +2149,9 @@ def free_text(
Add text in a rectangle to a page.
:param str text: Text to be added
:param :class:`RectangleObject<PyPDF2.generic.RectangleObject>` rect: or array of four
integers specifying the clickable rectangular area
``[xLL, yLL, xUR, yUR]``
:param :class:`RectangleObject<PyPDF2.generic.RectangleObject>` rect:
or array of four integers specifying the clickable rectangular area
``[xLL, yLL, xUR, yUR]``
:param str font: Name of the Font, e.g. 'Helvetica'
:param bool bold: Print the text in bold
:param bool italic: Print the text in italic
Expand Down Expand Up @@ -2270,7 +2324,7 @@ def link(

link_obj = DictionaryObject(
{
NameObject("/Type"): NameObject(PG.ANNOTS),
NameObject("/Type"): NameObject("/Annot"),
NameObject("/Subtype"): NameObject("/Link"),
NameObject("/Rect"): RectangleObject(rect),
NameObject("/Border"): ArrayObject(border_arr),
Expand Down Expand Up @@ -2298,3 +2352,36 @@ def link(
)
link_obj[NameObject("/Dest")] = dest_deferred
return link_obj

@staticmethod
def popup(
rect: Union[RectangleObject, Tuple[float, float, float, float]],
flags: int = 0,
parent: Optional[DictionaryObject] = None,
open: bool = False,
):
"""
Add a popup to the document.
:param :class:`RectangleObject<PyPDF2.generic.RectangleObject>` rect: or array of four
integers specifying the clickable rectangular area
``[xLL, yLL, xUR, yUR]``
:param int 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
:param bool open: Weather the popup should be shown directly or not (default is False)
:param dict parent: The contents of the popup. Create this via the AnnotationBuilder
"""
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
popup_obj[NameObject("/Parent")] = parent
return popup_obj
2 changes: 1 addition & 1 deletion docs/modules/AnnotationBuilder.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ The AnnotationBuilder Class

.. autoclass:: PyPDF2.generic.AnnotationBuilder
:members:
:undoc-members:
:no-undoc-members:
:show-inheritance:
3 changes: 3 additions & 0 deletions docs/user/adding-pdf-annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,6 @@ writer.add_annotation(page_number=0, annotation=annotation)
with open("annotated-pdf.pdf", "wb") as fp:
writer.write(fp)
```


## Popup
29 changes: 29 additions & 0 deletions tests/test_generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,35 @@ def test_annotation_builder_link():
with open(target, "wb") as fp:
writer.write(fp)

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


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,
)
popup_annotation = AnnotationBuilder.popup(
rect=(50, 550, 200, 650),
open=True,
parent=text_annotation,
)
writer.add_annotation(0, popup_annotation)

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

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


Expand Down

0 comments on commit cda445e

Please sign in to comment.