Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pypdf/_text_extraction/_layout_mode/_text_state_manager.py: 40%

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

81 statements  

1"""manage the PDF transform stack during "layout" mode text extraction""" 

2 

3from collections import ChainMap, Counter 

4from collections import ChainMap as ChainMapType 

5from collections import Counter as CounterType 

6from collections.abc import MutableMapping 

7from typing import Any, Literal, Union 

8 

9from ..._font import Font 

10from ...errors import PdfReadError 

11from .. import mult 

12from ._text_state_params import TextStateParams 

13 

14TextStateManagerKeyType = Union[int, Literal["is_text", "is_render"]] 

15TextStateManagerChainMapType = ChainMapType[TextStateManagerKeyType, Union[float, bool]] 

16TextStateManagerDictType = MutableMapping[TextStateManagerKeyType, Union[float, bool]] 

17 

18 

19class TextStateManager: 

20 """ 

21 Tracks the current text state including cm/tm/trm transformation matrices. 

22 

23 Attributes: 

24 transform_stack (ChainMap): ChainMap of cm/tm transformation matrices 

25 q_queue (Counter[int]): Counter of q operators 

26 q_depth (List[int]): list of q operator nesting levels 

27 Tc (float): character spacing 

28 Tw (float): word spacing 

29 Tz (int): horizontal scaling 

30 TL (float): leading 

31 Ts (float): text rise 

32 font (Font): font object 

33 font_size (int | float): font size 

34 

35 """ 

36 

37 def __init__(self) -> None: 

38 self.transform_stack: TextStateManagerChainMapType = ChainMap( 

39 self.new_transform() 

40 ) 

41 self.q_queue: CounterType[int] = Counter() 

42 self.q_depth = [0] 

43 self.Tc: float = 0.0 

44 self.Tw: float = 0.0 

45 self.Tz: float = 100.0 

46 self.TL: float = 0.0 

47 self.Ts: float = 0.0 

48 self.font_stack: list[tuple[Union[Font, None], Union[int, float]]] = [] 

49 self.font: Union[Font, None] = None 

50 self.font_size: Union[int, float] = 0 

51 

52 def set_state_param(self, op: bytes, value: Union[float, list[Any]]) -> None: 

53 """ 

54 Set a text state parameter. Supports Tc, Tz, Tw, TL, and Ts operators. 

55 

56 Args: 

57 op: operator read from PDF stream as bytes. No action is taken 

58 for unsupported operators (see supported operators above). 

59 value (float | List[Any]): new parameter value. If a list, 

60 value[0] is used. 

61 

62 """ 

63 if op not in [b"Tc", b"Tz", b"Tw", b"TL", b"Ts"]: 

64 return 

65 self.__setattr__(op.decode(), value[0] if isinstance(value, list) else value) 

66 

67 def set_font(self, font: Font, size: float) -> None: 

68 """ 

69 Set the current font and font_size. 

70 

71 Args: 

72 font (Font): a layout mode Font 

73 size (float): font size 

74 

75 """ 

76 self.font = font 

77 self.font_size = size 

78 

79 def text_state_params(self, value: Union[bytes, str] = "") -> TextStateParams: 

80 """ 

81 Create a TextStateParams instance to display a text string. Type[bytes] values 

82 will be decoded implicitly. 

83 

84 Args: 

85 value (str | bytes): text to associate with the captured state. 

86 

87 Raises: 

88 PdfReadError: if font not set (no Tf operator in incoming pdf content stream) 

89 

90 Returns: 

91 TextStateParams: current text state parameters 

92 

93 """ 

94 if not isinstance(self.font, Font): 

95 raise PdfReadError( 

96 "font not set: is PDF missing a Tf operator?" 

97 ) # pragma: no cover 

98 return TextStateParams( 

99 value, 

100 self.font, 

101 self.font_size, 

102 self.Tc, 

103 self.Tw, 

104 self.Tz, 

105 self.TL, 

106 self.Ts, 

107 self.effective_transform, 

108 ) 

109 

110 @staticmethod 

111 def raw_transform( 

112 _a: float = 1.0, 

113 _b: float = 0.0, 

114 _c: float = 0.0, 

115 _d: float = 1.0, 

116 _e: float = 0.0, 

117 _f: float = 0.0, 

118 ) -> TextStateManagerDictType: 

119 """Only a/b/c/d/e/f matrix params""" 

120 return dict(zip(range(6), map(float, (_a, _b, _c, _d, _e, _f)))) 

121 

122 @staticmethod 

123 def new_transform( 

124 _a: float = 1.0, 

125 _b: float = 0.0, 

126 _c: float = 0.0, 

127 _d: float = 1.0, 

128 _e: float = 0.0, 

129 _f: float = 0.0, 

130 is_text: bool = False, 

131 is_render: bool = False, 

132 ) -> TextStateManagerDictType: 

133 """Standard a/b/c/d/e/f matrix params + 'is_text' and 'is_render' keys""" 

134 result = TextStateManager.raw_transform(_a, _b, _c, _d, _e, _f) 

135 result.update({"is_text": is_text, "is_render": is_render}) 

136 return result 

137 

138 def reset_tm(self) -> TextStateManagerChainMapType: 

139 """Clear all transforms from chainmap having is_text==True or is_render==True""" 

140 while ( 

141 self.transform_stack.maps[0]["is_text"] 

142 or self.transform_stack.maps[0]["is_render"] 

143 ): 

144 self.transform_stack = self.transform_stack.parents 

145 return self.transform_stack 

146 

147 def reset_trm(self) -> TextStateManagerChainMapType: 

148 """Clear all transforms from chainmap having is_render==True""" 

149 while self.transform_stack.maps[0]["is_render"]: 

150 self.transform_stack = self.transform_stack.parents 

151 return self.transform_stack 

152 

153 def remove_q(self) -> TextStateManagerChainMapType: 

154 """Rewind to stack prior state after closing a 'q' with internal 'cm' ops""" 

155 self.font, self.font_size = self.font_stack.pop(-1) 

156 self.transform_stack = self.reset_tm() 

157 self.transform_stack.maps = self.transform_stack.maps[ 

158 self.q_queue.pop(self.q_depth.pop(), 0) : 

159 ] 

160 return self.transform_stack 

161 

162 def add_q(self) -> None: 

163 """Add another level to q_queue""" 

164 self.font_stack.append((self.font, self.font_size)) 

165 self.q_depth.append(len(self.q_depth)) 

166 

167 def add_cm(self, *args: Any) -> TextStateManagerChainMapType: 

168 """Concatenate an additional transform matrix""" 

169 self.transform_stack = self.reset_tm() 

170 self.q_queue.update(self.q_depth[-1:]) 

171 self.transform_stack = self.transform_stack.new_child(self.new_transform(*args)) 

172 return self.transform_stack 

173 

174 def _complete_matrix(self, operands: list[float]) -> list[float]: 

175 """Adds a, b, c, and d to an "e/f only" operand set (e.g Td)""" 

176 if len(operands) == 2: # this is a Td operator or equivalent 

177 operands = [1.0, 0.0, 0.0, 1.0, *operands] 

178 return operands 

179 

180 def add_tm(self, operands: list[float]) -> TextStateManagerChainMapType: 

181 """Append a text transform matrix""" 

182 self.transform_stack = self.transform_stack.new_child( 

183 self.new_transform( # type: ignore[misc] 

184 *self._complete_matrix(operands), is_text=True # type: ignore[arg-type] 

185 ) 

186 ) 

187 return self.transform_stack 

188 

189 def add_trm(self, operands: list[float]) -> TextStateManagerChainMapType: 

190 """Append a text rendering transform matrix""" 

191 self.transform_stack = self.transform_stack.new_child( 

192 self.new_transform( # type: ignore[misc] 

193 *self._complete_matrix(operands), is_text=True, is_render=True # type: ignore[arg-type] 

194 ) 

195 ) 

196 return self.transform_stack 

197 

198 @property 

199 def effective_transform(self) -> list[float]: 

200 """Current effective transform accounting for cm, tm, and trm transforms""" 

201 eff_transform = [*self.transform_stack.maps[0].values()] 

202 for transform in self.transform_stack.maps[1:]: 

203 eff_transform = mult(eff_transform, transform) 

204 return eff_transform