Skip to content

ENH: Add page-level actions #3382

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
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
35 changes: 35 additions & 0 deletions pypdf/_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
from .generic import (
ArrayObject,
ContentStream,
DecodedStreamObject,
DictionaryObject,
EncodedStreamObject,
FloatObject,
Expand All @@ -84,6 +85,7 @@
PdfObject,
RectangleObject,
StreamObject,
TextStringObject,
is_null_or_none,
)

Expand Down Expand Up @@ -2156,6 +2158,39 @@ def annotations(self, value: Optional[ArrayObject]) -> None:
else:
self[NameObject("/Annots")] = value

def add_js(self, javascript: str, /, action_type: str = "O") -> None:
r"""
Add JavaScript which will launch on the open or close action of this
page.

Args:
javascript: Your JavaScript.
action_type: "/O" or "/C", for open or close action respectively.

>>> output.add_js('app.alert("This is page " + this.pageNum);', "/O")
# Example: This will display the page number when the page is opened.
>>> output.add_js('app.alert("This is page " + this.pageNum);', "/C")
# Example: This will display the page number when the page is closed.

Note that this will replace any existing open or close action on this page.
Currently only an open or close action can be added, not both.
"""
if action_type not in {"/O", "/C"}:
raise ValueError('action_type must be "/O" or "/C"')

action = DictionaryObject()
self[NameObject("/AA")] = action
action[NameObject(action_type)] = DictionaryObject(
{
NameObject("/Type"): NameObject("/Action"),
NameObject("/S"): NameObject("/JavaScript"),
NameObject("/JS"): TextStringObject(f"{javascript}"),
}
)

javascript_object = DecodedStreamObject()
javascript_object.set_data(javascript.encode())


class _VirtualList(Sequence[PageObject]):
def __init__(
Expand Down
19 changes: 19 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,21 @@ 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_js('app.alert("This is page " + this.pageNum);', "/xyzzy")
assert (
exc.value.args[0] == 'action_type must be "/O" or "/C"'
)

page.add_js('app.alert("This is page " + this.pageNum);', "/O")
expected = {"/O": {"/Type": "/Action", "/S": "/JavaScript", "/JS": 'app.alert("This is page " + this.pageNum);'}}
assert page[NameObject("/AA")] == expected

page.add_js('app.alert("This is page " + this.pageNum);', "/C")
expected = {"/C": {"/Type": "/Action", "/S": "/JavaScript", "/JS": 'app.alert("This is page " + this.pageNum);'}}
assert page[NameObject("/AA")] == expected
Loading