Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions pypdf/_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
from .generic import (
ArrayObject,
ContentStream,
DecodedStreamObject,
DictionaryObject,
EncodedStreamObject,
FloatObject,
Expand All @@ -78,6 +79,7 @@
PdfObject,
RectangleObject,
StreamObject,
TextStringObject,
is_null_or_none,
)

Expand Down Expand Up @@ -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__(
Expand Down
25 changes: 25 additions & 0 deletions tests/test_javascript.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Loading