-
Notifications
You must be signed in to change notification settings - Fork 1.6k
ENH: Add actions base class #3552
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
base: main
Are you sure you want to change the base?
Changes from 174 commits
d2a57ab
388b902
695a2c9
ef335cc
efbb46e
82c9e5d
5ea4e6c
7f4077a
dc27124
52cee63
03db2c6
a44f4af
4593f1c
486b215
77cceb0
84b5215
a3fe3c3
61975f0
6e0b128
4c5b1fe
e74df20
2def725
be4a454
3215f49
6b2220c
c3a0612
fd9a071
40f00cc
b271cd0
055a0d1
9512011
ae04f7d
b51c376
5e5a1d9
5b77d55
91149f5
dbaa92f
bbe9979
2dc05b5
0894957
990f7a1
50d1632
eafc441
cfb068a
d76f5d0
eaca2be
449b2a6
e039151
4657e44
0c4f51a
b5a7126
b7d8dcc
68687e4
68c8859
7495ace
8c44c37
2a1e285
ecdd69c
4560c46
397eda5
5050c99
dac0a1d
884b534
328dc0a
d774d44
a350a48
ad86aa2
c3e08dd
b37338e
a381dfc
8b05fec
0ca5987
4a4b6d9
dd57257
dbd97ff
0248f1d
b558acf
b9ad57d
b8f040d
1ad0939
8530c35
0e88167
6d6ec5b
81a9daa
cda8f63
ee42260
3f51390
14bca27
b955577
29f2234
a79df96
e0951b8
4f0d20f
afacb0e
6179167
1770fd2
252f9c8
5d68369
5df6084
54491f2
7decde9
7c35f98
114e29b
c72d531
f9b7028
3bbb842
6541b91
d70790a
090a8bd
d8ea299
236107a
ddb87dd
d8da741
a658c71
0a64847
5f1c587
df84a93
c8b595b
1b6eebe
838af1e
2ef3ea2
0b06a27
7518d55
f666a7b
1a79bc3
7b5c9b5
152c3ea
824d4e2
ea38b9c
8855b57
f1b9127
2bd082e
012ea3e
6a13653
670a7cc
fc1d687
3fdcc0d
d185997
868e6c5
29b7f8e
75bf15c
e938ade
368d964
4389848
1fd79dd
c720eab
2c4bc57
5f6ee96
0bc854b
6fdfd70
c946f03
73cc4ad
7253b20
65d4fc2
d4be0bf
165e0ca
01a1618
6f87edd
20f27fc
332ea19
048d672
161e438
5f5feab
05cb20c
ff17a3f
669cfd8
3979839
ba619a4
e486125
5330235
5f03dfd
3e7e714
c8cae88
a0756d2
efcd908
689e6c9
82d8bb6
9d02048
86a7c40
c3a4fab
2a5bd04
c9c13f2
552a64f
d95ba3e
4fee484
a6bdf24
1165401
ebf964d
3422ecf
30bda9e
937ff1f
28485d2
7c531ad
7075025
01b7c8b
2474478
8097221
f414ed3
2d39b9e
392ead8
dde8004
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| Actions | ||
| ------- | ||
|
|
||
| .. automodule:: pypdf.actions | ||
| :members: | ||
| :undoc-members: | ||
| :show-inheritance: |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -58,6 +58,7 @@ | |
| logger_warning, | ||
| matrix_multiply, | ||
| ) | ||
| from .actions import Action | ||
| from .constants import ( | ||
| _INLINE_IMAGE_KEY_MAPPING, | ||
| _INLINE_IMAGE_VALUE_MAPPING, | ||
|
|
@@ -2168,6 +2169,47 @@ def annotations(self, value: Optional[ArrayObject]) -> None: | |
| else: | ||
| self[NameObject("/Annots")] = value | ||
|
|
||
| def add_action(self, trigger: str, action: Action) -> None: | ||
| """ | ||
| Add an action which will launch on the given trigger event of this page. | ||
|
|
||
| Args: | ||
| trigger: The trigger event. | ||
| action: An :py:class:`~pypdf.actions.Action` object. | ||
|
|
||
| Example: | ||
| >>> from pypdf import PdfWriter | ||
| >>> from pypdf.actions import JavaScript | ||
| >>> writer = PdfWriter() | ||
| >>> page = writer.add_blank_page(595, 842) | ||
| >>> # Display the page number when the page is opened | ||
| >>> page.add_action("open", JavaScript("app.alert('This is page ' + this.pageNum);")) | ||
| >>> # Display the page number when the page is closed | ||
| >>> page.add_action("close", JavaScript("app.alert('This is page ' + this.pageNum);")) | ||
| """ | ||
| Action._create_new(self, trigger, action) | ||
|
j-t-1 marked this conversation as resolved.
Outdated
|
||
|
|
||
| def delete_action(self, trigger: str) -> None: | ||
| """ | ||
| Delete an action associated with an open or close trigger event of this page. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't this wrong, as this will delete ALL actions associated with the given event for the current page? Maybe we even need to use |
||
|
|
||
| Args: | ||
| trigger: "open" or "close" trigger event. | ||
|
|
||
| Example: | ||
| >>> from pypdf import PdfWriter | ||
| >>> from pypdf.actions import JavaScript | ||
| >>> writer = PdfWriter() | ||
| >>> page = writer.add_blank_page(595, 842) | ||
| >>> page.add_action("open", JavaScript("app.alert('This is page ' + this.pageNum);")) | ||
| >>> page.add_action("close", JavaScript("app.alert('This is page ' + this.pageNum);")) | ||
| >>> # Delete all actions triggered by a page open | ||
| >>> page.delete_action("open") | ||
| >>> # Delete all actions triggered by a page close | ||
| >>> page.delete_action("close") | ||
| """ | ||
| Action._delete(self, trigger) | ||
|
j-t-1 marked this conversation as resolved.
Outdated
|
||
|
|
||
|
|
||
| class _VirtualList(Sequence[PageObject]): | ||
| def __init__( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| """ | ||
| PDF includes a wide variety of standard action types, whose characteristics and | ||
| behaviour are defined by an action dictionary. These are defined in this | ||
| submodule. | ||
|
|
||
| Trigger events are the other component of actions, and are specific to their | ||
| associated object. | ||
| """ | ||
|
|
||
|
|
||
| from ._actions import Action, JavaScript, PageTrigger | ||
|
|
||
| __all__ = [ | ||
| "Action", | ||
| "JavaScript", | ||
| "PageTrigger", | ||
| ] |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,174 @@ | ||||||
| """Action types""" | ||||||
| import sys | ||||||
| from abc import ABC | ||||||
| from enum import Enum, unique | ||||||
| from typing import ( | ||||||
| TYPE_CHECKING, | ||||||
| cast, | ||||||
| ) | ||||||
|
|
||||||
| from .._utils import logger_warning | ||||||
| from ..generic import ( | ||||||
| ArrayObject, | ||||||
| DictionaryObject, | ||||||
| NameObject, | ||||||
| NullObject, | ||||||
| TextStringObject, | ||||||
| is_null_or_none, | ||||||
| ) | ||||||
|
|
||||||
| if sys.version_info >= (3, 11): | ||||||
| from enum import StrEnum | ||||||
| else: | ||||||
| class StrEnum(str, Enum): | ||||||
| def __str__(self) -> str: | ||||||
| return str(self.value) | ||||||
|
|
||||||
| if TYPE_CHECKING: | ||||||
| from .._page import PageObject | ||||||
|
|
||||||
|
|
||||||
| @unique | ||||||
| class PageTrigger(StrEnum): | ||||||
|
j-t-1 marked this conversation as resolved.
|
||||||
| """Trigger event entries in a page object's additional-actions dictionary.""" | ||||||
|
|
||||||
| OPEN = "open" # An action that shall be performed when the page is opened | ||||||
|
stefan6419846 marked this conversation as resolved.
Outdated
|
||||||
| CLOSE = "close" # An action that shall be performed when the page is closed | ||||||
|
|
||||||
|
|
||||||
| class Action(DictionaryObject, ABC): | ||||||
| """An action dictionary defines the characteristics and behaviour of an action.""" | ||||||
| def __init__(self) -> None: | ||||||
| super().__init__() | ||||||
| self[NameObject("/Type")] = NameObject("/Action") | ||||||
| # The next action or sequence of actions that shall be performed after the action | ||||||
| # represented by this dictionary. The value is either a single action dictionary | ||||||
| # or an array of action dictionaries that shall be performed in order. | ||||||
| self[NameObject("/Next")] = NullObject() # Optional | ||||||
|
|
||||||
| @classmethod | ||||||
| def _create_new(cls, page: "PageObject", trigger: str, action: "Action") -> None: | ||||||
|
stefan6419846 marked this conversation as resolved.
Outdated
|
||||||
| """ | ||||||
| Create a new action and add it to the page. | ||||||
|
|
||||||
| Args: | ||||||
| page: The page to add the action. | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| trigger: The trigger event. | ||||||
| action: An :py:class:`~pypdf.actions.Action` object. | ||||||
| """ | ||||||
| valid_values = [trigger.value for trigger in PageTrigger] | ||||||
|
|
||||||
| if trigger not in valid_values: | ||||||
| raise ValueError(f"The trigger must be one of {valid_values}") | ||||||
|
|
||||||
| trigger_name = NameObject("/O") if PageTrigger(trigger).value == PageTrigger.OPEN else NameObject("/C") | ||||||
|
|
||||||
| if "/AA" not in page: | ||||||
| # Additional actions key not present | ||||||
| page[NameObject("/AA")] = DictionaryObject( | ||||||
| {trigger_name: action} | ||||||
| ) | ||||||
| return | ||||||
|
|
||||||
| if isinstance(page["/AA"], NullObject): | ||||||
| page[NameObject("/AA")] = DictionaryObject() | ||||||
|
|
||||||
| if not isinstance(page["/AA"], DictionaryObject): | ||||||
|
j-t-1 marked this conversation as resolved.
Outdated
|
||||||
| if page.pdf is not None and hasattr(page.pdf, "strict") and page.pdf.strict: | ||||||
|
stefan6419846 marked this conversation as resolved.
Outdated
|
||||||
| raise ValueError("The PageObject has an AA entry whose value is not a DictionaryObject.") | ||||||
|
stefan6419846 marked this conversation as resolved.
Outdated
|
||||||
| logger_warning( | ||||||
| "The PageObject has an AA entry whose value is not a DictionaryObject.", | ||||||
|
stefan6419846 marked this conversation as resolved.
Outdated
|
||||||
| source=__name__, | ||||||
| ) | ||||||
| return | ||||||
|
|
||||||
| additional_actions: DictionaryObject = page["/AA"] | ||||||
|
j-t-1 marked this conversation as resolved.
Outdated
|
||||||
|
|
||||||
| if is_null_or_none(additional_actions.get(trigger_name)): | ||||||
| additional_actions.update({trigger_name: action}) | ||||||
| return | ||||||
|
|
||||||
| """ | ||||||
|
stefan6419846 marked this conversation as resolved.
Outdated
|
||||||
| The action dictionary's Next entry allows sequences of actions to be | ||||||
| chained together. For example, the effect of clicking a link | ||||||
| annotation with the mouse can be to play a sound, jump to a new | ||||||
| page, and start up a movie. Note that the Next entry is not | ||||||
| restricted to a single action but can contain an array of actions, | ||||||
| each of which in turn can have a Next entry of its own. | ||||||
| §12.6.2 Action dictionaries ISO 32000-2:2020 | ||||||
| """ | ||||||
| head = current = additional_actions.get(trigger_name) | ||||||
| if not isinstance(head, DictionaryObject): | ||||||
| raise TypeError( | ||||||
|
j-t-1 marked this conversation as resolved.
Outdated
|
||||||
| f"The type in a page object's additional-actions key must be a DictionaryObject: " | ||||||
| f"received type {type(head)}" | ||||||
| ) | ||||||
| current = cast(DictionaryObject, current) | ||||||
|
|
||||||
| visited = set() | ||||||
| while True: | ||||||
| next_ = current.get("/Next", None) | ||||||
|
|
||||||
| if is_null_or_none(next_): | ||||||
| break | ||||||
|
|
||||||
| if not isinstance(next_, (ArrayObject, DictionaryObject)): | ||||||
| raise TypeError( | ||||||
| f"An action dictionary’s Next entry must be an Action dictionary " | ||||||
| f"or an array of Action dictionaries: received type {type(next_)}" | ||||||
| ) | ||||||
|
|
||||||
| id_ = id(next_) | ||||||
| if id_ in visited: | ||||||
| logger_warning("Detected cycle in the action tree for %(current)s", source=__name__, current=current) | ||||||
| break | ||||||
| visited.add(id_) | ||||||
|
|
||||||
| if isinstance(next_, ArrayObject): | ||||||
| current = next_[-1] | ||||||
| else: | ||||||
| current = next_ | ||||||
|
|
||||||
| if not is_null_or_none(next_ := current.get("/Next")) and id(next_) in visited: | ||||||
| logger_warning("Detected cycle in the action tree for %(current)s", source=__name__, current=current) | ||||||
|
|
||||||
| current[NameObject("/Next")] = action | ||||||
| additional_actions.update({trigger_name: head}) | ||||||
|
|
||||||
| @classmethod | ||||||
| def _delete(cls, page: "PageObject", trigger: str) -> None: | ||||||
| valid_values = [trigger.value for trigger in PageTrigger] | ||||||
|
|
||||||
| if trigger not in valid_values: | ||||||
| raise ValueError(f"The trigger must be one of {valid_values}") | ||||||
|
|
||||||
| trigger_name = NameObject("/O") if PageTrigger(trigger).value == PageTrigger.OPEN else NameObject("/C") | ||||||
|
|
||||||
| if "/AA" not in page: | ||||||
|
j-t-1 marked this conversation as resolved.
|
||||||
| return | ||||||
|
|
||||||
| additional_actions: DictionaryObject = cast(DictionaryObject, page["/AA"]) | ||||||
|
stefan6419846 marked this conversation as resolved.
Outdated
|
||||||
|
|
||||||
| if trigger_name in additional_actions: | ||||||
|
j-t-1 marked this conversation as resolved.
Outdated
|
||||||
| del additional_actions[trigger_name] | ||||||
|
|
||||||
| if not additional_actions: | ||||||
| del page["/AA"] | ||||||
|
|
||||||
|
|
||||||
| class JavaScript(Action): | ||||||
|
stefan6419846 marked this conversation as resolved.
|
||||||
| """ | ||||||
| Upon invocation of an ECMAScript action, a PDF processor shall execute a | ||||||
| script that is written in the ECMAScript programming language. ECMAScript | ||||||
| extensions described in ISO/DIS 21757-1 shall also be allowed. | ||||||
| """ | ||||||
| def __init__(self, js: str) -> None: | ||||||
|
j-t-1 marked this conversation as resolved.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why has this been resolved? |
||||||
| """ | ||||||
| Initialize JavaScript with a string. | ||||||
|
|
||||||
| Args: | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This does not appear in the docs: https://pypdf--3552.org.readthedocs.build/en/3552/modules/actions.html Please check other cases and correct accordingly.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is still unsolved? |
||||||
| js (str): A text string containing the ECMAScript script to be executed. | ||||||
|
stefan6419846 marked this conversation as resolved.
Outdated
|
||||||
| """ | ||||||
| super().__init__() | ||||||
| self[NameObject("/S")] = NameObject("/JavaScript") | ||||||
| self[NameObject("/JS")] = TextStringObject(js) | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -78,3 +78,7 @@ | |
| "/Projection", | ||
| "/RichMedia", | ||
| ] | ||
|
|
||
| ActionSubtype: TypeAlias = Literal[ | ||
| "/JavaScript", | ||
| ] | ||
Uh oh!
There was an error while loading. Please reload this page.