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

100 statements  

1"""Action types""" 

2import sys 

3import uuid 

4from abc import ABC 

5from enum import Enum, unique 

6from typing import ( 

7 TYPE_CHECKING, 

8 cast, 

9) 

10 

11from .._utils import logger_warning 

12from ..constants import CatalogAttributes 

13from ..errors import ParseError 

14from ..generic import ( 

15 ArrayObject, 

16 DictionaryObject, 

17 NameObject, 

18 NullObject, 

19 TextStringObject, 

20 create_string_object, 

21 is_null_or_none, 

22) 

23 

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

25 from enum import StrEnum 

26else: 

27 class StrEnum(str, Enum): 

28 def __str__(self) -> str: 

29 return str(self.value) 

30 

31if TYPE_CHECKING: 

32 from .._page import PageObject 

33 from .._writer import PdfWriter 

34 

35 

36@unique 

37class PageTrigger(StrEnum): 

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

39 OPEN = "open" 

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

41 CLOSE = "close" 

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

43 

44 @property 

45 def _name_object(self) -> NameObject: 

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

47 mapping = { 

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

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

50 } 

51 return mapping[self] 

52 

53 

54class Action(DictionaryObject, ABC): 

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

56 def __init__(self) -> None: 

57 super().__init__() 

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

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

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

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

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

63 

64 @classmethod 

65 def _create_open_action(cls, writer: "PdfWriter", action: "Action") -> None: 

66 """ 

67 Create an action that shall be performed when the document is opened. 

68 

69 Args: 

70 writer: The writer to add the action to. 

71 action: The action to add. 

72 """ 

73 if "/Names" not in writer.root_object: 

74 writer.root_object[NameObject(CatalogAttributes.NAMES)] = DictionaryObject() 

75 

76 names = cast(DictionaryObject, writer.root_object[CatalogAttributes.NAMES]) 

77 if "/JavaScript" not in names: 

78 names[NameObject("/JavaScript")] = DictionaryObject( 

79 {NameObject("/Names"): ArrayObject()} 

80 ) 

81 

82 js = cast( 

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

84 ) 

85 

86 # We need a name for parameterized JavaScript in the PDF file, but it can be anything 

87 js.append(create_string_object(str(uuid.uuid4()))) 

88 js.append(writer._add_object(action)) 

89 

90 @classmethod 

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

92 """ 

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

94 

95 Args: 

96 page: The page to add the action to. 

97 trigger: An open or close trigger. 

98 action: The action to be done. 

99 """ 

100 trigger_name = trigger._name_object 

101 

102 if "/AA" not in page: 

103 # Additional actions key not present 

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

105 {trigger_name: action} 

106 ) 

107 return 

108 

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

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

111 

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

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

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

115 raise ParseError( 

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

117 f"received type {current_type}." 

118 ) 

119 logger_warning( 

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

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

122 source=__name__, 

123 type=current_type 

124 ) 

125 return 

126 

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

128 

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

130 additional_actions.update({trigger_name: action}) 

131 return 

132 

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

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

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

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

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

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

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

140 head = current = additional_actions.get(trigger_name) 

141 if not isinstance(head, DictionaryObject): 

142 raise ParseError( 

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

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

145 ) 

146 current = cast(DictionaryObject, current) 

147 

148 visited = set() 

149 while True: 

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

151 

152 if is_null_or_none(next_node): 

153 break 

154 

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

156 raise TypeError( 

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

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

159 ) 

160 

161 id_next = id(next_node) 

162 if id_next in visited: 

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

164 break 

165 visited.add(id_next) 

166 

167 if isinstance(next_node, ArrayObject): 

168 current = next_node[-1] 

169 else: 

170 current = next_node 

171 

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

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

174 

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

176 additional_actions.update({trigger_name: head}) 

177 

178 @classmethod 

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

180 """ 

181 Delete an action from the page. 

182 

183 Args: 

184 page: The page to delete the action. 

185 trigger: An open or close trigger. 

186 """ 

187 if "/AA" not in page: 

188 return 

189 

190 trigger_name = trigger._name_object 

191 

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

193 

194 if trigger_name not in additional_actions: 

195 return 

196 

197 del additional_actions[trigger_name] 

198 

199 if not additional_actions: 

200 del page["/AA"] 

201 

202 

203class JavaScript(Action): 

204 """ 

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

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

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

208 

209 Args: 

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

211 """ 

212 

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

214 """Initialize JavaScript with a string.""" 

215 super().__init__() 

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

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