Skip to content
Open
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
44 changes: 44 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,48 @@ def annotations(self, value: Optional[ArrayObject]) -> None:
else:
self[NameObject("/Annots")] = value

def add_action(
self,
event: Literal["O", "C"] = "O",
action_type: Literal["JavaScript"] = "JavaScript",
action: str = ""
) -> None:
r"""
Add action which will launch on the open or close event of this
page.

Args:
event: "/O" or "/C", for open or close action 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 event on this page.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These limitations still sound like we should further refine the public API. add_action should add the new action in a defined manner, not overwrite existing ones - especially when the specification supports having both events defined on the page, but our API design only allows one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this needs to be robust; and extensible to the many action types. May be worth posting as a discussion to get ideas.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can of course open a discussion, but with JavaScript being ignored by many PDF readers and (at least from my experience) limited adoption, I guess there will not be much feedback.

This is one of the reasons why we usually ask to open issues first to discuss new APIs, although we might have missed this there as well and additional feedback would most likely have been sparse as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, feedback and demand may be limited. Although this is initially for JavaScript, it will be extensible and generic for other action types. The many and varied action types means this is a route to upgrading the interactive features that pypdf can provide.

Currently only an open or close event can be added, not both.
"""
if event not in {"/O", "/C"}:
raise ValueError('event 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(event)] = 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_js(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] == 'event 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