Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pypdf/actions/_actions.py: 31%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

87 statements  

1"""Action types""" 

2import sys 

3from abc import ABC 

4from enum import Enum, unique 

5from typing import ( 

6 TYPE_CHECKING, 

7 cast, 

8) 

9 

10from .._utils import logger_warning 

11from ..errors import ParseError 

12from ..generic import ( 

13 ArrayObject, 

14 DictionaryObject, 

15 NameObject, 

16 NullObject, 

17 TextStringObject, 

18 is_null_or_none, 

19) 

20 

21if sys.version_info >= (3, 11): 

22 from enum import StrEnum 

23else: 

24 class StrEnum(str, Enum): 

25 def __str__(self) -> str: 

26 return str(self.value) 

27 

28if TYPE_CHECKING: 

29 from .._page import PageObject 

30 

31 

32@unique 

33class PageTrigger(StrEnum): 

34 """Trigger event entries in a page object's additional-actions dictionary.""" 

35 OPEN = "open" 

36 """Trigger an action when the page is opened.""" 

37 CLOSE = "close" 

38 """Trigger an action when the page is closed.""" 

39 

40 @property 

41 def _name_object(self) -> NameObject: 

42 """Returns the corresponding NameObject for the trigger.""" 

43 mapping = { 

44 PageTrigger.OPEN: NameObject("/O"), 

45 PageTrigger.CLOSE: NameObject("/C"), 

46 } 

47 return mapping[self] 

48 

49 

50class Action(DictionaryObject, ABC): 

51 """An action dictionary defines the characteristics and behaviour of an action.""" 

52 def __init__(self) -> None: 

53 super().__init__() 

54 self[NameObject("/Type")] = NameObject("/Action") 

55 # The next action or sequence of actions that shall be performed after the action 

56 # represented by this dictionary. The value is either a single action dictionary 

57 # or an array of action dictionaries that shall be performed in order. 

58 self[NameObject("/Next")] = NullObject() # Optional 

59 

60 @classmethod 

61 def _create_new(cls, page: "PageObject", trigger: PageTrigger, action: "Action") -> None: 

62 """ 

63 Create a new action and add it to the page. 

64 

65 Args: 

66 page: The page to add the action to. 

67 trigger: An open or close trigger. 

68 action: The action to be done. 

69 """ 

70 trigger_name = trigger._name_object 

71 

72 if "/AA" not in page: 

73 # Additional actions key not present 

74 page[NameObject("/AA")] = DictionaryObject( 

75 {trigger_name: action} 

76 ) 

77 return 

78 

79 if isinstance(page["/AA"], NullObject): 

80 page[NameObject("/AA")] = DictionaryObject() 

81 

82 if not isinstance(page["/AA"].get_object(), DictionaryObject): 

83 current_type = type(page["/AA"]).__name__ 

84 if page.pdf is not None and getattr(page.pdf, "strict", False): 

85 raise ParseError( 

86 f"The type in a page object's additional-actions key must be a DictionaryObject: " 

87 f"received type {current_type}." 

88 ) 

89 logger_warning( 

90 "The type in a page object's additional-actions key must be a DictionaryObject: " 

91 "received type %(type)s.", 

92 source=__name__, 

93 type=current_type 

94 ) 

95 return 

96 

97 additional_actions = cast(DictionaryObject, page["/AA"]) 

98 

99 if is_null_or_none(additional_actions.get(trigger_name)): 

100 additional_actions.update({trigger_name: action}) 

101 return 

102 

103 # The action dictionary's Next entry allows sequences of actions to be 

104 # chained together. For example, the effect of clicking a link 

105 # annotation with the mouse can be to play a sound, jump to a new 

106 # page, and start up a movie. Note that the Next entry is not 

107 # restricted to a single action but can contain an array of actions, 

108 # each of which in turn can have a Next entry of its own. 

109 # §12.6.2 Action dictionaries ISO 32000-2:2020 

110 head = current = additional_actions.get(trigger_name) 

111 if not isinstance(head, DictionaryObject): 

112 raise ParseError( 

113 f"The type in a page object's additional-actions key must be a DictionaryObject: " 

114 f"received type {type(head).__name__}." 

115 ) 

116 current = cast(DictionaryObject, current) 

117 

118 visited = set() 

119 while True: 

120 next_node = current.get("/Next", None) 

121 

122 if is_null_or_none(next_node): 

123 break 

124 

125 if not isinstance(next_node, (ArrayObject, DictionaryObject)): 

126 raise TypeError( 

127 f"An action dictionary’s Next entry must be an Action dictionary " 

128 f"or an array of Action dictionaries: received type {type(next_node).__name__}." 

129 ) 

130 

131 id_next = id(next_node) 

132 if id_next in visited: 

133 logger_warning("Detected cycle in the action tree for %(current)s", source=__name__, current=current) 

134 break 

135 visited.add(id_next) 

136 

137 if isinstance(next_node, ArrayObject): 

138 current = next_node[-1] 

139 else: 

140 current = next_node 

141 

142 if not is_null_or_none(next_node := current.get("/Next")) and id(next_node) in visited: 

143 logger_warning("Detected cycle in the action tree for %(current)s", source=__name__, current=current) 

144 

145 current[NameObject("/Next")] = action 

146 additional_actions.update({trigger_name: head}) 

147 

148 @classmethod 

149 def _delete(cls, page: "PageObject", trigger: PageTrigger) -> None: 

150 """ 

151 Delete an action from the page. 

152 

153 Args: 

154 page: The page to delete the action. 

155 trigger: An open or close trigger. 

156 """ 

157 if "/AA" not in page: 

158 return 

159 

160 trigger_name = trigger._name_object 

161 

162 additional_actions = cast(DictionaryObject, page["/AA"]) 

163 

164 if trigger_name not in additional_actions: 

165 return 

166 

167 del additional_actions[trigger_name] 

168 

169 if not additional_actions: 

170 del page["/AA"] 

171 

172 

173class JavaScript(Action): 

174 """ 

175 Upon invocation of an ECMAScript action, a PDF processor shall execute a 

176 script that is written in the ECMAScript programming language. ECMAScript 

177 extensions described in ISO/DIS 21757-1 shall also be allowed. 

178 

179 Args: 

180 java_script: A text string containing the ECMAScript script to be executed. 

181 """ 

182 

183 def __init__(self, java_script: str) -> None: 

184 """Initialize JavaScript with a string.""" 

185 super().__init__() 

186 self[NameObject("/S")] = NameObject("/JavaScript") 

187 self[NameObject("/JS")] = TextStringObject(java_script)