From cda445e30055917ebe6d0a55dc56acf1a71cc3f3 Mon Sep 17 00:00:00 2001 From: Martin Thoma Date: Wed, 3 Aug 2022 22:48:02 +0200 Subject: [PATCH] ENH: Add popup annotation support See #107 Closes #1195 --- PyPDF2/_writer.py | 7 +++ PyPDF2/generic.py | 95 +++++++++++++++++++++++++++-- docs/modules/AnnotationBuilder.rst | 2 +- docs/user/adding-pdf-annotations.md | 3 + tests/test_generic.py | 29 +++++++++ 5 files changed, 131 insertions(+), 5 deletions(-) diff --git a/PyPDF2/_writer.py b/PyPDF2/_writer.py index d9448d1bf..2cd547a56 100644 --- a/PyPDF2/_writer.py +++ b/PyPDF2/_writer.py @@ -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) diff --git a/PyPDF2/generic.py b/PyPDF2/generic.py index 16c904a1c..ed29020cf 100644 --- a/PyPDF2/generic.py +++ b/PyPDF2/generic.py @@ -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` 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, @@ -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` rect: or array of four - integers specifying the clickable rectangular area - ``[xLL, yLL, xUR, yUR]`` + :param :class:`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 @@ -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), @@ -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` 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 diff --git a/docs/modules/AnnotationBuilder.rst b/docs/modules/AnnotationBuilder.rst index 198f06501..8ad7e54a5 100644 --- a/docs/modules/AnnotationBuilder.rst +++ b/docs/modules/AnnotationBuilder.rst @@ -3,5 +3,5 @@ The AnnotationBuilder Class .. autoclass:: PyPDF2.generic.AnnotationBuilder :members: - :undoc-members: + :no-undoc-members: :show-inheritance: diff --git a/docs/user/adding-pdf-annotations.md b/docs/user/adding-pdf-annotations.md index 890dfde1c..1af4fc9cd 100644 --- a/docs/user/adding-pdf-annotations.md +++ b/docs/user/adding-pdf-annotations.md @@ -126,3 +126,6 @@ writer.add_annotation(page_number=0, annotation=annotation) with open("annotated-pdf.pdf", "wb") as fp: writer.write(fp) ``` + + +## Popup diff --git a/tests/test_generic.py b/tests/test_generic.py index 350ab8aef..a0bfde59b 100644 --- a/tests/test_generic.py +++ b/tests/test_generic.py @@ -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