From 25550a1ae23b0814170c6ca183bc76cec714f31c Mon Sep 17 00:00:00 2001 From: j-t-1 <120829237+j-t-1@users.noreply.github.com> Date: Wed, 16 Jul 2025 10:37:40 +0000 Subject: [PATCH 01/15] ENH: Add page-level actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sets entries in a page object’s additional-actions dictionary. --- pypdf/_page.py | 33 +++++++++++++++++++++++++++++++++ tests/test_javascript.py | 11 +++++++++++ 2 files changed, 44 insertions(+) diff --git a/pypdf/_page.py b/pypdf/_page.py index 8dda9cf109..9786e55765 100644 --- a/pypdf/_page.py +++ b/pypdf/_page.py @@ -74,6 +74,7 @@ from .generic import ( ArrayObject, ContentStream, + DecodedStreamObject, DictionaryObject, EncodedStreamObject, FloatObject, @@ -84,6 +85,7 @@ PdfObject, RectangleObject, StreamObject, + TextStringObject, is_null_or_none, ) @@ -2155,6 +2157,37 @@ def annotations(self, value: Optional[ArrayObject]) -> None: del self[NameObject("/Annots")] else: self[NameObject("/Annots")] = value + + def add_js(self, javascript: str, /, *, open_action: bool = True) -> None: + """ + Add JavaScript which will launch on the open or close action of this + page. + + Args: + javascript: Your JavaScript. + + >>> output.add_js("app.alert(\"This is page \" + this.pageNum);") + # Example: This will display the page number when the page is opened. + >>> output.add_js("app.alert(\"This is page \" + this.pageNum);", open_action = False) + # 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. + """ + + open_or_close = NameObject('/O') if open_action else NameObject('/C') + + action = DictionaryObject() + self[NameObject('/AA')] = action + action[open_or_close] = DictionaryObject( + { + NameObject("/Type"): NameObject("/Action"), + NameObject("/S"): NameObject("/JavaScript"), + NameObject("/JS"): TextStringObject(f"{javascript}"), + } + + javascript_object = DecodedStreamObject() + javascript_object.set_data(javascript) class _VirtualList(Sequence[PageObject]): diff --git a/tests/test_javascript.py b/tests/test_javascript.py index 094f8126d1..f83ca97d63 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,13 @@ 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] + + page.add_js("app.alert(\"This is page \" + this.pageNum);", open_action = True) + assert page[NameObject('/AA')] == {'/O': {'/Type': '/Action', '/S': '/JavaScript', '/JS': 'app.alert("This is page " + this.pageNum);'}} + + page.add_js("app.alert(\"This is page \" + this.pageNum);", open_action = False) + assert page[NameObject('/AA')] == {'/C': {'/Type': '/Action', '/S': '/JavaScript', '/JS': 'app.alert("This is page " + this.pageNum);'}} \ No newline at end of file From 98608ae80671ff8efcfa21b9c130aa9c58ab111e Mon Sep 17 00:00:00 2001 From: j-t-1 <120829237+j-t-1@users.noreply.github.com> Date: Wed, 16 Jul 2025 10:48:27 +0000 Subject: [PATCH 02/15] Fix errors. --- pypdf/_page.py | 3 ++- tests/test_javascript.py | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pypdf/_page.py b/pypdf/_page.py index 9786e55765..df0532fed6 100644 --- a/pypdf/_page.py +++ b/pypdf/_page.py @@ -2157,7 +2157,7 @@ def annotations(self, value: Optional[ArrayObject]) -> None: del self[NameObject("/Annots")] else: self[NameObject("/Annots")] = value - + def add_js(self, javascript: str, /, *, open_action: bool = True) -> None: """ Add JavaScript which will launch on the open or close action of this @@ -2185,6 +2185,7 @@ def add_js(self, javascript: str, /, *, open_action: bool = True) -> None: NameObject("/S"): NameObject("/JavaScript"), NameObject("/JS"): TextStringObject(f"{javascript}"), } + ) javascript_object = DecodedStreamObject() javascript_object.set_data(javascript) diff --git a/tests/test_javascript.py b/tests/test_javascript.py index f83ca97d63..e0ffd9d58c 100644 --- a/tests/test_javascript.py +++ b/tests/test_javascript.py @@ -56,7 +56,9 @@ def test_page_add_js(pdf_file_writer): page = pdf_file_writer.pages[0] page.add_js("app.alert(\"This is page \" + this.pageNum);", open_action = True) - assert page[NameObject('/AA')] == {'/O': {'/Type': '/Action', '/S': '/JavaScript', '/JS': '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_js("app.alert(\"This is page \" + this.pageNum);", open_action = False) - assert page[NameObject('/AA')] == {'/C': {'/Type': '/Action', '/S': '/JavaScript', '/JS': 'app.alert("This is page " + this.pageNum);'}} \ No newline at end of file + expected = {"/C": {"/Type": "/Action", "/S": "/JavaScript", "/JS": "app.alert(\"This is page \" + this.pageNum);"}} + assert page[NameObject('/AA')] == expected \ No newline at end of file From 19a527a6f11b0d03185f5da646031e6fb1794110 Mon Sep 17 00:00:00 2001 From: j-t-1 <120829237+j-t-1@users.noreply.github.com> Date: Wed, 16 Jul 2025 10:54:08 +0000 Subject: [PATCH 03/15] Fix more errors. --- pypdf/_page.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pypdf/_page.py b/pypdf/_page.py index df0532fed6..79ddcc6bcd 100644 --- a/pypdf/_page.py +++ b/pypdf/_page.py @@ -2157,9 +2157,9 @@ def annotations(self, value: Optional[ArrayObject]) -> None: del self[NameObject("/Annots")] else: self[NameObject("/Annots")] = value - + def add_js(self, javascript: str, /, *, open_action: bool = True) -> None: - """ + r""" Add JavaScript which will launch on the open or close action of this page. @@ -2174,11 +2174,10 @@ def add_js(self, javascript: str, /, *, open_action: bool = True) -> None: 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. """ - - open_or_close = NameObject('/O') if open_action else NameObject('/C') + open_or_close = NameObject("/O") if open_action else NameObject("/C") action = DictionaryObject() - self[NameObject('/AA')] = action + self[NameObject("/AA")] = action action[open_or_close] = DictionaryObject( { NameObject("/Type"): NameObject("/Action"), From 769ccc44ee63adfceaf5e74cf0ee03766e036f66 Mon Sep 17 00:00:00 2001 From: j-t-1 <120829237+j-t-1@users.noreply.github.com> Date: Wed, 16 Jul 2025 10:57:50 +0000 Subject: [PATCH 04/15] Fix some more errors. --- tests/test_javascript.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_javascript.py b/tests/test_javascript.py index e0ffd9d58c..2e5f0ab190 100644 --- a/tests/test_javascript.py +++ b/tests/test_javascript.py @@ -55,10 +55,10 @@ def get_javascript_name() -> Any: def test_page_add_js(pdf_file_writer): page = pdf_file_writer.pages[0] - page.add_js("app.alert(\"This is page \" + this.pageNum);", open_action = True) - 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);', open_action = True) + 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);", open_action = False) - expected = {"/C": {"/Type": "/Action", "/S": "/JavaScript", "/JS": "app.alert(\"This is page \" + this.pageNum);"}} - assert page[NameObject('/AA')] == expected \ No newline at end of file + page.add_js('app.alert("This is page " + this.pageNum);', open_action = False) + expected = {"/C": {"/Type": "/Action", "/S": "/JavaScript", "/JS": 'app.alert("This is page " + this.pageNum);'}} + assert page[NameObject("/AA")] == expected \ No newline at end of file From ac16b2b17c18c6984f6dfa25faafb6203a71bb91 Mon Sep 17 00:00:00 2001 From: j-t-1 <120829237+j-t-1@users.noreply.github.com> Date: Wed, 16 Jul 2025 11:00:02 +0000 Subject: [PATCH 05/15] Add trailing newline --- tests/test_javascript.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_javascript.py b/tests/test_javascript.py index 2e5f0ab190..8b6bbe7361 100644 --- a/tests/test_javascript.py +++ b/tests/test_javascript.py @@ -61,4 +61,4 @@ def test_page_add_js(pdf_file_writer): page.add_js('app.alert("This is page " + this.pageNum);', open_action = False) expected = {"/C": {"/Type": "/Action", "/S": "/JavaScript", "/JS": 'app.alert("This is page " + this.pageNum);'}} - assert page[NameObject("/AA")] == expected \ No newline at end of file + assert page[NameObject("/AA")] == expected From d90407c36d1a6e0742f299c4a7e68069ee44fc5a Mon Sep 17 00:00:00 2001 From: j-t-1 <120829237+j-t-1@users.noreply.github.com> Date: Wed, 16 Jul 2025 11:04:07 +0000 Subject: [PATCH 06/15] Fix incompatible type "str", expected "bytes" error --- pypdf/_page.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pypdf/_page.py b/pypdf/_page.py index 79ddcc6bcd..bf73bc2f47 100644 --- a/pypdf/_page.py +++ b/pypdf/_page.py @@ -2187,7 +2187,7 @@ def add_js(self, javascript: str, /, *, open_action: bool = True) -> None: ) javascript_object = DecodedStreamObject() - javascript_object.set_data(javascript) + javascript_object.set_data(javascript.encode()) class _VirtualList(Sequence[PageObject]): From f7679e75db42de1b1b08ed2cf60b17fe6d65e4e0 Mon Sep 17 00:00:00 2001 From: j-t-1 <120829237+j-t-1@users.noreply.github.com> Date: Wed, 16 Jul 2025 15:50:37 +0000 Subject: [PATCH 07/15] Update to use NameObject directly --- pypdf/_page.py | 12 ++++++------ tests/test_javascript.py | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pypdf/_page.py b/pypdf/_page.py index bf73bc2f47..bbace09a48 100644 --- a/pypdf/_page.py +++ b/pypdf/_page.py @@ -2158,27 +2158,27 @@ def annotations(self, value: Optional[ArrayObject]) -> None: else: self[NameObject("/Annots")] = value - def add_js(self, javascript: str, /, *, open_action: bool = True) -> None: + def add_js(self, javascript: str, /, action_type: NameObject = NameObject("/O")) -> None: r""" Add JavaScript which will launch on the open or close action of this page. Args: javascript: Your JavaScript. + action_type: NameObject("/O") or NameObject("/C"), for open or close + action respectively. - >>> output.add_js("app.alert(\"This is page \" + this.pageNum);") + >>> output.add_js("app.alert(\"This is page \" + this.pageNum);", NameObject("/O")) # Example: This will display the page number when the page is opened. - >>> output.add_js("app.alert(\"This is page \" + this.pageNum);", open_action = False) + >>> output.add_js("app.alert(\"This is page \" + this.pageNum);", NameObject("/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. """ - open_or_close = NameObject("/O") if open_action else NameObject("/C") - action = DictionaryObject() self[NameObject("/AA")] = action - action[open_or_close] = DictionaryObject( + action[action_type] = DictionaryObject( { NameObject("/Type"): NameObject("/Action"), NameObject("/S"): NameObject("/JavaScript"), diff --git a/tests/test_javascript.py b/tests/test_javascript.py index 8b6bbe7361..ff43b1fe10 100644 --- a/tests/test_javascript.py +++ b/tests/test_javascript.py @@ -55,10 +55,10 @@ def get_javascript_name() -> Any: def test_page_add_js(pdf_file_writer): page = pdf_file_writer.pages[0] - page.add_js('app.alert("This is page " + this.pageNum);', open_action = True) + page.add_js("app.alert(\"This is page \" + this.pageNum);", NameObject("/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);', open_action = False) + page.add_js("app.alert(\"This is page \" + this.pageNum);", NameObject("/C")) expected = {"/C": {"/Type": "/Action", "/S": "/JavaScript", "/JS": 'app.alert("This is page " + this.pageNum);'}} assert page[NameObject("/AA")] == expected From 06b31d2897404a090f3a10d220cfb82bf6df4119 Mon Sep 17 00:00:00 2001 From: j-t-1 <120829237+j-t-1@users.noreply.github.com> Date: Wed, 16 Jul 2025 16:24:10 +0000 Subject: [PATCH 08/15] Fix errors. --- pypdf/_page.py | 14 ++++++++------ tests/test_javascript.py | 4 ++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/pypdf/_page.py b/pypdf/_page.py index bbace09a48..efeb298986 100644 --- a/pypdf/_page.py +++ b/pypdf/_page.py @@ -2158,27 +2158,29 @@ def annotations(self, value: Optional[ArrayObject]) -> None: else: self[NameObject("/Annots")] = value - def add_js(self, javascript: str, /, action_type: NameObject = NameObject("/O")) -> None: + 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: NameObject("/O") or NameObject("/C"), for open or close - action respectively. + action_type: "/O" or "/C", for open or close action respectively. - >>> output.add_js("app.alert(\"This is page \" + this.pageNum);", NameObject("/O")) + >>> 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);", NameObject("/C")) + >>> 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 {"open", "close"}: + raise ValueError("action_type must be 'open' or 'close'") + action = DictionaryObject() self[NameObject("/AA")] = action - action[action_type] = DictionaryObject( + action[NameObject(action_type)] = DictionaryObject( { NameObject("/Type"): NameObject("/Action"), NameObject("/S"): NameObject("/JavaScript"), diff --git a/tests/test_javascript.py b/tests/test_javascript.py index ff43b1fe10..e9d6a0acd0 100644 --- a/tests/test_javascript.py +++ b/tests/test_javascript.py @@ -55,10 +55,10 @@ def get_javascript_name() -> Any: def test_page_add_js(pdf_file_writer): page = pdf_file_writer.pages[0] - page.add_js("app.alert(\"This is page \" + this.pageNum);", NameObject("/O")) + 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);", NameObject("/C")) + 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 From 2f5b28af6702892b2f3ea342eff24788a6033ba4 Mon Sep 17 00:00:00 2001 From: j-t-1 <120829237+j-t-1@users.noreply.github.com> Date: Wed, 16 Jul 2025 16:35:06 +0000 Subject: [PATCH 09/15] Fix error --- pypdf/_page.py | 4 ++-- tests/test_javascript.py | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pypdf/_page.py b/pypdf/_page.py index efeb298986..1de374d725 100644 --- a/pypdf/_page.py +++ b/pypdf/_page.py @@ -2175,8 +2175,8 @@ def add_js(self, javascript: str, /, action_type: str = "O") -> None: 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 {"open", "close"}: - raise ValueError("action_type must be 'open' or 'close'") + if action_type not in {"/O", "/C"}: + raise ValueError('action_type must be "/O" or "/C"') action = DictionaryObject() self[NameObject("/AA")] = action diff --git a/tests/test_javascript.py b/tests/test_javascript.py index e9d6a0acd0..dcbccd58e6 100644 --- a/tests/test_javascript.py +++ b/tests/test_javascript.py @@ -55,6 +55,12 @@ def get_javascript_name() -> Any: 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 From 0ce99e76b81910ef7f9dc812d89a40020cedd2e6 Mon Sep 17 00:00:00 2001 From: j-t-1 <120829237+j-t-1@users.noreply.github.com> Date: Tue, 26 Aug 2025 17:14:36 +0100 Subject: [PATCH 10/15] Use a Literal type --- pypdf/_page.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pypdf/_page.py b/pypdf/_page.py index 1de374d725..77a9e6b867 100644 --- a/pypdf/_page.py +++ b/pypdf/_page.py @@ -2158,7 +2158,7 @@ def annotations(self, value: Optional[ArrayObject]) -> None: else: self[NameObject("/Annots")] = value - def add_js(self, javascript: str, /, action_type: str = "O") -> None: + def add_js(self, javascript: str, /, action_type: Literal['O', 'C'] = "O") -> None: r""" Add JavaScript which will launch on the open or close action of this page. From bcd4ae351e52234b19fd7d6e8c8a6c006f722ae9 Mon Sep 17 00:00:00 2001 From: j-t-1 <120829237+j-t-1@users.noreply.github.com> Date: Tue, 26 Aug 2025 17:16:54 +0100 Subject: [PATCH 11/15] Use double quotes --- pypdf/_page.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pypdf/_page.py b/pypdf/_page.py index 77a9e6b867..35e7f59c04 100644 --- a/pypdf/_page.py +++ b/pypdf/_page.py @@ -2158,7 +2158,7 @@ def annotations(self, value: Optional[ArrayObject]) -> None: else: self[NameObject("/Annots")] = value - def add_js(self, javascript: str, /, action_type: Literal['O', 'C'] = "O") -> None: + def add_js(self, javascript: str, /, action_type: Literal["O", "C"] = "O") -> None: r""" Add JavaScript which will launch on the open or close action of this page. From 651265872facf0822bfef45758f6de86c51b9fb8 Mon Sep 17 00:00:00 2001 From: j-t-1 <120829237+j-t-1@users.noreply.github.com> Date: Wed, 17 Sep 2025 08:42:57 +0000 Subject: [PATCH 12/15] Change function name, to make it generalizable --- pypdf/_page.py | 6 +++--- tests/test_javascript.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pypdf/_page.py b/pypdf/_page.py index 0eeeb2497b..f06e0ad85d 100644 --- a/pypdf/_page.py +++ b/pypdf/_page.py @@ -2151,7 +2151,7 @@ def annotations(self, value: Optional[ArrayObject]) -> None: else: self[NameObject("/Annots")] = value - def add_js(self, javascript: str, /, action_type: Literal["O", "C"] = "O") -> None: + def add_action(self, javascript: str, /, action_type: Literal["O", "C"] = "O") -> None: r""" Add JavaScript which will launch on the open or close action of this page. @@ -2160,9 +2160,9 @@ def add_js(self, javascript: str, /, action_type: Literal["O", "C"] = "O") -> No 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") + >>> output.add_action('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") + >>> output.add_action('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. diff --git a/tests/test_javascript.py b/tests/test_javascript.py index dcbccd58e6..ec41d01dc4 100644 --- a/tests/test_javascript.py +++ b/tests/test_javascript.py @@ -56,15 +56,15 @@ 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") + page.add_action('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") + page.add_action('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") + page.add_action('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 From 533e4f577740ada5be9d8f4bc283caad481aee0c Mon Sep 17 00:00:00 2001 From: j-t-1 <120829237+j-t-1@users.noreply.github.com> Date: Wed, 17 Sep 2025 10:33:21 +0000 Subject: [PATCH 13/15] Make function extensible for future action types --- pypdf/_page.py | 30 +++++++++++++++++------------- tests/test_javascript.py | 14 ++++++++++---- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/pypdf/_page.py b/pypdf/_page.py index f06e0ad85d..395634382b 100644 --- a/pypdf/_page.py +++ b/pypdf/_page.py @@ -2151,38 +2151,42 @@ def annotations(self, value: Optional[ArrayObject]) -> None: else: self[NameObject("/Annots")] = value - def add_action(self, javascript: str, /, action_type: Literal["O", "C"] = "O") -> None: + def add_action(self, event: Literal["O", "C"] = "O", action_type: Literal["JavaScript"] = "JavaScript", action: str = "") -> None: r""" - Add JavaScript which will launch on the open or close action of this + Add action which will launch on the open or close event of this page. Args: - javascript: Your JavaScript. - action_type: "/O" or "/C", for open or close action respectively. + 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('app.alert("This is page " + this.pageNum);', "/O") + >>> 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('app.alert("This is page " + this.pageNum);', "/C") + >>> 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 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"') + if event not in {"/O", "/C"}: + raise ValueError('event must be "/O" or "/C"') - action = DictionaryObject() - self[NameObject("/AA")] = action - action[NameObject(action_type)] = DictionaryObject( + 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"{javascript}"), + NameObject("/JS"): TextStringObject(f"{action}"), } ) javascript_object = DecodedStreamObject() - javascript_object.set_data(javascript.encode()) + javascript_object.set_data(action.encode()) class _VirtualList(Sequence[PageObject]): diff --git a/tests/test_javascript.py b/tests/test_javascript.py index ec41d01dc4..79489b603b 100644 --- a/tests/test_javascript.py +++ b/tests/test_javascript.py @@ -56,15 +56,21 @@ def test_page_add_js(pdf_file_writer): page = pdf_file_writer.pages[0] with pytest.raises(ValueError) as exc: - page.add_action('app.alert("This is page " + this.pageNum);', "/xyzzy") + page.add_action("/xyzzy", "JavaScript", 'app.alert("This is page " + this.pageNum);', ) assert ( - exc.value.args[0] == 'action_type must be "/O" or "/C"' + exc.value.args[0] == 'event must be "/O" or "/C"' ) - page.add_action('app.alert("This is page " + this.pageNum);', "/O") + 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('app.alert("This is page " + this.pageNum);', "/C") + 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 From 444ea7e2eac2bcc764a2978f9b37c37d0b091c77 Mon Sep 17 00:00:00 2001 From: j-t-1 <120829237+j-t-1@users.noreply.github.com> Date: Wed, 17 Sep 2025 10:38:05 +0000 Subject: [PATCH 14/15] Fix errors --- pypdf/_page.py | 6 +++++- tests/test_javascript.py | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pypdf/_page.py b/pypdf/_page.py index 395634382b..5d443871ac 100644 --- a/pypdf/_page.py +++ b/pypdf/_page.py @@ -2151,7 +2151,11 @@ 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: + 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. diff --git a/tests/test_javascript.py b/tests/test_javascript.py index 79489b603b..4b319e6911 100644 --- a/tests/test_javascript.py +++ b/tests/test_javascript.py @@ -56,13 +56,13 @@ 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);', ) + 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);', ) + 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"' ) From 925f68efad1846e15cffabc76598a76f24c96019 Mon Sep 17 00:00:00 2001 From: j-t-1 <120829237+j-t-1@users.noreply.github.com> Date: Wed, 17 Sep 2025 16:57:53 +0100 Subject: [PATCH 15/15] Increase readability --- pypdf/_page.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pypdf/_page.py b/pypdf/_page.py index 5d443871ac..e1e54a89dd 100644 --- a/pypdf/_page.py +++ b/pypdf/_page.py @@ -2154,7 +2154,8 @@ def annotations(self, value: Optional[ArrayObject]) -> None: def add_action( self, event: Literal["O", "C"] = "O", - action_type: Literal["JavaScript"] = "JavaScript", action: str = "" + action_type: Literal["JavaScript"] = "JavaScript", + action: str = "" ) -> None: r""" Add action which will launch on the open or close event of this @@ -2170,8 +2171,8 @@ def add_action( >>> 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 action on this page. - Currently only an open or close action can be added, not both. + Note that this will replace any existing open or close event on this page. + 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"') @@ -2189,8 +2190,8 @@ def add_action( } ) - javascript_object = DecodedStreamObject() - javascript_object.set_data(action.encode()) + action_object = DecodedStreamObject() + action_object.set_data(action.encode()) class _VirtualList(Sequence[PageObject]):