diff --git a/pypdf/_page.py b/pypdf/_page.py index e7b47882c..942fb27eb 100644 --- a/pypdf/_page.py +++ b/pypdf/_page.py @@ -68,6 +68,7 @@ from .generic import ( ArrayObject, ContentStream, + DecodedStreamObject, DictionaryObject, EncodedStreamObject, FloatObject, @@ -78,6 +79,7 @@ PdfObject, RectangleObject, StreamObject, + TextStringObject, is_null_or_none, ) @@ -2149,6 +2151,46 @@ def annotations(self, value: Optional[ArrayObject]) -> None: else: self[NameObject("/Annots")] = value + def add_action( + self, + trigger: Literal["O", "C"] = "O", + action_type: Literal["JavaScript"] = "JavaScript", + action: str = "" + ) -> None: + r""" + Add action which will launch on the open or close trigger event of this page. + + Args: + trigger: "/O" or "/C", for open or close trigger event respectively. + action_type: "JavaScript" is currently the only available action type. + action: Your JavaScript. + + >>> output.add_action("/O", "JavaScript", 'app.alert("This is page " + this.pageNum);') + # Example: This will display the page number when the page is opened. + >>> output.add_action("/C", "JavaScript", 'app.alert("This is page " + this.pageNum);') + # Example: This will display the page number when the page is closed. + + Note that this will replace any existing open or close trigger event on this page. + Currently only an open or close event can be added, not both. + """ + if trigger not in {"/O", "/C"}: + raise ValueError('The trigger must be "/O" or "/C"') + + if action_type != "JavaScript": + raise ValueError('Currently the only action_type supported is "JavaScript"') + + additional_actions = DictionaryObject() + self[NameObject("/AA")] = additional_actions + additional_actions[NameObject(trigger)] = DictionaryObject( + { + NameObject("/Type"): NameObject("/Action"), + NameObject("/S"): NameObject("/JavaScript"), + NameObject("/JS"): TextStringObject(f"{action}"), + } + ) + action_object = DecodedStreamObject() + action_object.set_data(action.encode()) + class _VirtualList(Sequence[PageObject]): def __init__( diff --git a/tests/test_javascript.py b/tests/test_javascript.py index 094f8126d..f398818f8 100644 --- a/tests/test_javascript.py +++ b/tests/test_javascript.py @@ -5,6 +5,7 @@ import pytest from pypdf import PdfReader, PdfWriter +from pypdf.generic import NameObject # Configure path environment TESTS_ROOT = Path(__file__).parent.resolve() @@ -49,3 +50,27 @@ def get_javascript_name() -> Any: assert ( first_js != second_js ), "add_js should add to the previous script in the catalog." + + +def test_page_add_action(pdf_file_writer): + page = pdf_file_writer.pages[0] + + with pytest.raises(ValueError) as exc: + page.add_action("/xyzzy", "JavaScript", 'app.alert("This is page " + this.pageNum);') + assert ( + exc.value.args[0] == 'The trigger must be "/O" or "/C"' + ) + + with pytest.raises(ValueError) as exc: + page.add_action("/O", "xyzzy", 'app.alert("This is page " + this.pageNum);') + assert ( + exc.value.args[0] == 'Currently the only action_type supported is "JavaScript"' + ) + + page.add_action("/O", "JavaScript", 'app.alert("This is page " + this.pageNum);') + expected = {"/O": {"/Type": "/Action", "/S": "/JavaScript", "/JS": 'app.alert("This is page " + this.pageNum);'}} + assert page[NameObject("/AA")] == expected + + page.add_action("/C", "JavaScript", 'app.alert("This is page " + this.pageNum);') + expected = {"/C": {"/Type": "/Action", "/S": "/JavaScript", "/JS": 'app.alert("This is page " + this.pageNum);'}} + assert page[NameObject("/AA")] == expected