Skip to content
Merged
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
13 changes: 11 additions & 2 deletions docs/user/add-javascript.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,20 @@ pypdf_test_setup("user/add-javascript", {

```{testcode}
from pypdf import PdfWriter
from pypdf.actions import JavaScript

writer = PdfWriter(clone_from="example.pdf")

# Add JavaScript to launch the print window on opening this PDF.
writer.add_js("this.print({bUI:true,bSilent:false,bShrinkToFit:true});")
# Add JavaScript to launch the print window on opening this PDF
writer.add_open_action(JavaScript("this.print({bUI:true,bSilent:false,bShrinkToFit:true});"))
Comment thread
j-t-1 marked this conversation as resolved.
# bUI: (optional) If true (the default), will cause a UI to be presented to the
# user to obtain printing information and confirm the action.

# bSilent: (optional) If true, suppresses the cancel dialog box while the
# document is printing. The default is false.

# bShrinkToFit: If true, the page is shrunk (if necessary) to fit within the
# imageable area of the printed page. If false, it is not. The default is false.

writer.write("out-print-window.pdf")
```
42 changes: 19 additions & 23 deletions pypdf/_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
import re
import struct
import sys
import uuid
from collections.abc import Iterable, Mapping, Sequence
from io import BytesIO, FileIO, IOBase
from itertools import compress
Expand Down Expand Up @@ -68,6 +67,7 @@
deprecation_no_replacement,
logger_warning,
)
from .actions import Action, JavaScript
from .constants import AnnotationDictionaryAttributes as AA
from .constants import (
CatalogAttributes,
Expand Down Expand Up @@ -795,29 +795,25 @@ def add_js(self, javascript: str) -> None:
>>> output.add_js("this.print({bUI:true,bSilent:false,bShrinkToFit:true});")

"""
# Names / JavaScript preferred to be able to add multiple scripts
if "/Names" not in self._root_object:
self._root_object[NameObject(CatalogAttributes.NAMES)] = DictionaryObject()
names = cast(DictionaryObject, self._root_object[CatalogAttributes.NAMES])
if "/JavaScript" not in names:
names[NameObject("/JavaScript")] = DictionaryObject(
{NameObject("/Names"): ArrayObject()}
)
js_list = cast(
ArrayObject, cast(DictionaryObject, names["/JavaScript"])["/Names"]
)
# We need a name for parameterized JavaScript in the PDF file,
# but it can be anything.
js_list.append(create_string_object(str(uuid.uuid4())))
deprecate_with_replacement("add_js", "add_open_action", "7.0.0")
self.add_open_action(JavaScript(javascript))

js = DictionaryObject(
{
NameObject(PagesAttributes.TYPE): NameObject("/Action"),
NameObject("/S"): NameObject("/JavaScript"),
NameObject("/JS"): TextStringObject(f"{javascript}"),
}
)
js_list.append(self._add_object(js))
def add_open_action(self, action: Action) -> None:
"""
Add an action to the document-level JavaScript name tree.

Args:
action: The action to add.

Example:
This will launch the print window when the PDF is opened.

>>> from pypdf import PdfWriter
>>> from pypdf.actions import JavaScript
>>> output = PdfWriter()
>>> output.add_open_action(JavaScript("this.print({bUI:true,bSilent:false,bShrinkToFit:true});"))
"""
return Action._create_open_action(self, action)

def add_attachment(self, filename: str, data: Union[str, bytes]) -> "EmbeddedFile":
"""
Expand Down
30 changes: 30 additions & 0 deletions pypdf/actions/_actions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Action types"""
import sys
import uuid
from abc import ABC
from enum import Enum, unique
from typing import (
Expand All @@ -8,13 +9,15 @@
)

from .._utils import logger_warning
from ..constants import CatalogAttributes
from ..errors import ParseError
from ..generic import (
ArrayObject,
DictionaryObject,
NameObject,
NullObject,
TextStringObject,
create_string_object,
is_null_or_none,
)

Expand All @@ -27,6 +30,7 @@ def __str__(self) -> str:

if TYPE_CHECKING:
from .._page import PageObject
from .._writer import PdfWriter


@unique
Expand Down Expand Up @@ -57,6 +61,32 @@ def __init__(self) -> None:
# or an array of action dictionaries that shall be performed in order.
self[NameObject("/Next")] = NullObject() # Optional

@classmethod
def _create_open_action(cls, writer: "PdfWriter", action: "Action") -> None:
"""
Create an action that shall be performed when the document is opened.

Args:
writer: The writer to add the action to.
action: The action to add.
"""
if "/Names" not in writer.root_object:
writer.root_object[NameObject(CatalogAttributes.NAMES)] = DictionaryObject()

names = cast(DictionaryObject, writer.root_object[CatalogAttributes.NAMES])
if "/JavaScript" not in names:
names[NameObject("/JavaScript")] = DictionaryObject(
{NameObject("/Names"): ArrayObject()}
)

js = cast(
ArrayObject, cast(DictionaryObject, names["/JavaScript"])["/Names"]
)

# We need a name for parameterized JavaScript in the PDF file, but it can be anything
js.append(create_string_object(str(uuid.uuid4())))
js.append(writer._add_object(action))

@classmethod
def _create_new(cls, page: "PageObject", trigger: PageTrigger, action: "Action") -> None:
"""
Expand Down
87 changes: 87 additions & 0 deletions tests/test_actions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Test the pypdf.actions submodule."""

from typing import Any

import pytest

from pypdf import PdfReader, PdfWriter
Expand All @@ -18,6 +20,91 @@ def pdf_file_writer():
return writer


def test_add_js(pdf_file_writer):
with pytest.warns(
DeprecationWarning,
match=(
r"^add_js is deprecated and will be removed in pypdf 7\.0\.0\. "
r"Use add_open_action instead\.$"
)
):
pdf_file_writer.add_js("this.print({bUI:true,bSilent:false,bShrinkToFit:true});")

assert (
"/Names" in pdf_file_writer.root_object
), "add_js should add a name catalog in the root object."
assert (
"/JavaScript" in pdf_file_writer.root_object["/Names"]
), "add_js should add a JavaScript name tree under the name catalog."


def test_added_js(pdf_file_writer):
def get_javascript_name() -> Any:
assert "/Names" in pdf_file_writer.root_object
assert "/JavaScript" in pdf_file_writer.root_object["/Names"]
assert "/Names" in pdf_file_writer.root_object["/Names"]["/JavaScript"]
return pdf_file_writer.root_object["/Names"]["/JavaScript"]["/Names"][
-2
] # return -2 in order to get the latest javascript

with pytest.warns(
DeprecationWarning,
match=(
r"^add_js is deprecated and will be removed in pypdf 7\.0\.0\. "
r"Use add_open_action instead\.$"
)
):
pdf_file_writer.add_js("this.print({bUI:true,bSilent:false,bShrinkToFit:true});")

first_js = get_javascript_name()

with pytest.warns(
DeprecationWarning,
match=(
r"^add_js is deprecated and will be removed in pypdf 7\.0\.0\. "
r"Use add_open_action instead\.$"
)
):
pdf_file_writer.add_js("this.print({bUI:true,bSilent:false,bShrinkToFit:true});")

second_js = get_javascript_name()

assert (
first_js != second_js
), "add_js should add to the previous script in the catalog."


def test_add_open_action(pdf_file_writer):
pdf_file_writer.add_open_action(JavaScript("this.print({bUI:true,bSilent:false,bShrinkToFit:true});"))

assert (
"/Names" in pdf_file_writer.root_object
), "add_open_action should add a name catalog in the root object."
assert (
"/JavaScript" in pdf_file_writer.root_object["/Names"]
), "add_open_action should add a JavaScript name tree under the name catalog."


def test_added_open_action(pdf_file_writer):
def get_javascript_name() -> Any:
assert "/Names" in pdf_file_writer.root_object
assert "/JavaScript" in pdf_file_writer.root_object["/Names"]
assert "/Names" in pdf_file_writer.root_object["/Names"]["/JavaScript"]
return pdf_file_writer.root_object["/Names"]["/JavaScript"]["/Names"][
-2
] # return the key of the most recently added JavaScript

pdf_file_writer.add_open_action(JavaScript("this.print({bUI:true,bSilent:false,bShrinkToFit:true});"))
first_js = get_javascript_name()

pdf_file_writer.add_open_action(JavaScript("this.print({bUI:true,bSilent:false,bShrinkToFit:true});"))
second_js = get_javascript_name()

assert (
first_js != second_js
), "add_open_action should add to the previous script in the catalog."


@pytest.mark.parametrize(
"action_dictionary",
[
Expand Down
46 changes: 0 additions & 46 deletions tests/test_javascript.py

This file was deleted.

3 changes: 2 additions & 1 deletion tests/test_workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

from pypdf import PdfReader, PdfWriter, Transformation
from pypdf._utils import Version
from pypdf.actions import JavaScript
from pypdf.constants import PageAttributes as PG
from pypdf.errors import PdfReadError, PdfReadWarning
from pypdf.generic import (
Expand Down Expand Up @@ -66,7 +67,7 @@ def test_basic_features(tmp_path):
# add some Javascript to launch the print window on opening this PDF.
# the password dialog may prevent the print dialog from being shown,
# comment the encryption lines, if that's the case, to try this out
writer.add_js("this.print({bUI:true,bSilent:false,bShrinkToFit:true});")
writer.add_open_action(JavaScript("this.print({bUI:true,bSilent:false,bShrinkToFit:true});"))

# encrypt your new PDF and add a password
password = "secret"
Expand Down
Loading