Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pypdf/_text_extraction/_text_extractor.py: 17%

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

155 statements  

1# Copyright (c) 2006, Mathieu Fenniak 

2# Copyright (c) 2007, Ashish Kulkarni <kulkarni.ashish@gmail.com> 

3# 

4# All rights reserved. 

5# 

6# Redistribution and use in source and binary forms, with or without 

7# modification, are permitted provided that the following conditions are 

8# met: 

9# 

10# * Redistributions of source code must retain the above copyright notice, 

11# this list of conditions and the following disclaimer. 

12# * Redistributions in binary form must reproduce the above copyright notice, 

13# this list of conditions and the following disclaimer in the documentation 

14# and/or other materials provided with the distribution. 

15# * The name of the author may not be used to endorse or promote products 

16# derived from this software without specific prior written permission. 

17# 

18# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 

19# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 

20# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 

21# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 

22# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 

23# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 

24# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 

25# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 

26# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 

27# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 

28# POSSIBILITY OF SUCH DAMAGE. 

29 

30from typing import Any, Callable, Optional, Union 

31 

32from .._codecs import encoding_dict_from_named_encoding 

33from ..generic import DictionaryObject, TextStringObject 

34from ..generic._font import Font, FontDescriptor 

35from . import OrientationNotFoundError, crlf_space_check, get_display_str, get_text_operands, mult 

36 

37 

38def _get_number_operand(operands: list[Any], index: int, default: float) -> float: 

39 """Return the operand at index as a float, or default if it is missing or not a number.""" 

40 if index < len(operands) and isinstance(operands[index], (int, float)): 

41 return float(operands[index]) 

42 return default 

43 

44 

45class TextExtraction: 

46 """ 

47 A class to handle PDF text extraction operations. 

48 

49 This class encapsulates all the state and operations needed for extracting 

50 text from PDF content streams, replacing the nested functions and nonlocal 

51 variables in the original implementation. 

52 """ 

53 

54 def __init__(self) -> None: 

55 # Text extraction state variables 

56 self.cm_matrix: list[float] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0] 

57 self.tm_matrix: list[float] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0] 

58 self.cm_stack: list[ 

59 tuple[ 

60 list[float], 

61 Optional[DictionaryObject], 

62 Font, 

63 float, 

64 float, 

65 float, 

66 float, 

67 ] 

68 ] = [] 

69 

70 # Store the last modified matrices; can be an intermediate position 

71 self.cm_prev: list[float] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0] 

72 self.tm_prev: list[float] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0] 

73 

74 # Store the position at the beginning of building the text 

75 self.memo_cm: list[float] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0] 

76 self.memo_tm: list[float] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0] 

77 

78 self.char_scale = 1.0 

79 self.space_scale = 1.0 

80 self._space_width: float = 500.0 # will be set correctly at first Tf 

81 self._actual_str_size: dict[str, float] = { 

82 "str_widths": 0.0, 

83 "str_height": 0.0, 

84 } # will be set to string length calculation result 

85 self.TL = 0.0 

86 self.font_size = 12.0 # init just in case of 

87 # (axis index, min, max) of baselines kept on the current extracted line. 

88 self._line_span: Optional[tuple[int, float, float]] = None 

89 

90 # Text extraction variables 

91 self.text: str = "" 

92 self.output: str = "" 

93 self.rtl_dir: bool = False # right-to-left 

94 self.font_resource: Optional[DictionaryObject] = None 

95 self.font = Font( 

96 name = "NotInitialized", 

97 sub_type="Unknown", 

98 encoding=encoding_dict_from_named_encoding("cp1252"), # WinAnsiEncoding 

99 font_descriptor=FontDescriptor(), 

100 ) 

101 self.orientations: tuple[int, ...] = (0, 90, 180, 270) 

102 self.visitor_text: Optional[Callable[[Any, Any, Any, Any, Any], None]] = None 

103 self.font_resources: dict[str, DictionaryObject] = {} 

104 self.fonts: dict[str, Font] = {} 

105 

106 self.operation_handlers = { 

107 b"BT": self._handle_bt, 

108 b"ET": self._handle_et, 

109 b"q": self._handle_save_graphics_state, 

110 b"Q": self._handle_restore_graphics_state, 

111 b"cm": self._handle_cm, 

112 b"Tz": self._handle_tz, 

113 b"Tw": self._handle_tw, 

114 b"TL": self._handle_tl, 

115 b"Tf": self._handle_tf, 

116 b"Td": self._handle_td, 

117 b"Tm": self._handle_tm, 

118 b"T*": self._handle_t_star, 

119 b"Tj": self._handle_tj_operation, 

120 } 

121 

122 def initialize_extraction( 

123 self, 

124 orientations: tuple[int, ...] = (0, 90, 180, 270), 

125 visitor_text: Optional[Callable[[Any, Any, Any, Any, Any], None]] = None, 

126 font_resources: Optional[dict[str, DictionaryObject]] = None, 

127 fonts: Optional[dict[str, Font]] = None 

128 ) -> None: 

129 """Initialize the extractor with extraction parameters.""" 

130 self.orientations = orientations 

131 self.visitor_text = visitor_text 

132 self.font_resources = font_resources or {} 

133 self.fonts = fonts or {} 

134 

135 # Reset state 

136 self.text = "" 

137 self.output = "" 

138 self.rtl_dir = False 

139 self._line_span = None 

140 

141 def compute_str_widths(self, str_widths: float) -> float: 

142 return str_widths / 1000 

143 

144 def process_operation(self, operator: bytes, operands: list[Any]) -> None: 

145 if operator in self.operation_handlers: 

146 handler = self.operation_handlers[operator] 

147 str_widths = handler(operands) 

148 

149 # Post-process operations that affect text positioning 

150 if operator in {b"Td", b"Tm", b"T*", b"Tj"}: 

151 self._post_process_text_operation(str_widths or 0.0) 

152 

153 def _post_process_text_operation(self, str_widths: float) -> None: 

154 """Handle common post-processing for text positioning operations.""" 

155 text_was_empty = self.text == "" 

156 try: 

157 self.text, self.output, self.cm_prev, self.tm_prev, self._line_span = crlf_space_check( 

158 self.text, 

159 (self.cm_prev, self.tm_prev), 

160 (self.cm_matrix, self.tm_matrix), 

161 (self.memo_cm, self.memo_tm), 

162 self.font_resource, 

163 self.orientations, 

164 self.output, 

165 self.font_size, 

166 self.visitor_text, 

167 str_widths, 

168 self.compute_str_widths(self.font_size * self._space_width), 

169 self._actual_str_size["str_height"], 

170 self._line_span, 

171 ) 

172 if text_was_empty or self.text == "": 

173 self.memo_cm = self.cm_matrix.copy() 

174 self.memo_tm = self.tm_matrix.copy() 

175 except OrientationNotFoundError: 

176 pass 

177 

178 def _handle_tj( 

179 self, 

180 text: str, 

181 operands: list[Union[str, TextStringObject]], 

182 cm_matrix: list[float], 

183 tm_matrix: list[float], 

184 font_resource: Optional[DictionaryObject], 

185 font: Font, 

186 orientations: tuple[int, ...], 

187 font_size: float, 

188 rtl_dir: bool, 

189 visitor_text: Optional[Callable[[Any, Any, Any, Any, Any], None]], 

190 actual_str_size: dict[str, float], 

191 ) -> tuple[str, bool, dict[str, float]]: 

192 text_operands, is_str_operands, font_widths = get_text_operands( 

193 operands, cm_matrix, tm_matrix, font, orientations 

194 ) 

195 if is_str_operands: 

196 text += text_operands 

197 else: 

198 text, rtl_dir, completed = get_display_str( 

199 text, 

200 cm_matrix, 

201 tm_matrix, # text matrix 

202 font_resource, 

203 font, 

204 text_operands, 

205 font_size, 

206 rtl_dir, 

207 visitor_text, 

208 ) 

209 self.output += completed 

210 actual_str_size["str_widths"] += font_widths * font_size 

211 actual_str_size["str_height"] = font_size 

212 return text, rtl_dir, actual_str_size 

213 

214 def _flush_text(self) -> None: 

215 """Flush accumulated text to output and call visitor if present.""" 

216 self.output += self.text 

217 if self.visitor_text is not None: 

218 self.visitor_text(self.text, self.memo_cm, self.memo_tm, self.font_resource, self.font_size) 

219 self.text = "" 

220 self.memo_cm = self.cm_matrix.copy() 

221 self.memo_tm = self.tm_matrix.copy() 

222 

223 # Operation handlers 

224 

225 def _handle_bt(self, operands: list[Any]) -> None: 

226 """Handle BT (Begin Text) operation - Table 5.4 page 405.""" 

227 self.tm_matrix = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0] 

228 self._flush_text() 

229 

230 def _handle_et(self, operands: list[Any]) -> None: 

231 """Handle ET (End Text) operation - Table 5.4 page 405.""" 

232 self._flush_text() 

233 

234 def _handle_save_graphics_state(self, operands: list[Any]) -> None: 

235 """Handle q (Save graphics state) operation - Table 4.7 page 219.""" 

236 self.cm_stack.append( 

237 ( 

238 self.cm_matrix, 

239 self.font_resource, 

240 self.font, 

241 self.font_size, 

242 self.char_scale, 

243 self.space_scale, 

244 self.TL, 

245 ) 

246 ) 

247 

248 def _handle_restore_graphics_state(self, operands: list[Any]) -> None: 

249 """Handle Q (Restore graphics state) operation - Table 4.7 page 219.""" 

250 try: 

251 ( 

252 self.cm_matrix, 

253 self.font_resource, 

254 self.font, 

255 self.font_size, 

256 self.char_scale, 

257 self.space_scale, 

258 self.TL, 

259 ) = self.cm_stack.pop() 

260 except Exception: 

261 self.cm_matrix = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0] 

262 

263 def _handle_cm(self, operands: list[Any]) -> None: 

264 """Handle cm (Modify current matrix) operation - Table 4.7 page 219.""" 

265 self.output += self.text 

266 if self.visitor_text is not None: 

267 self.visitor_text(self.text, self.memo_cm, self.memo_tm, self.font_resource, self.font_size) 

268 self.text = "" 

269 try: 

270 self.cm_matrix = mult([float(operand) for operand in operands[:6]], self.cm_matrix) 

271 except Exception: 

272 self.cm_matrix = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0] 

273 self.memo_cm = self.cm_matrix.copy() 

274 self.memo_tm = self.tm_matrix.copy() 

275 

276 def _handle_tz(self, operands: list[Any]) -> None: 

277 """Handle Tz (Set horizontal text scaling) operation - Table 5.2 page 398.""" 

278 self.char_scale = _get_number_operand(operands, 0, 100.0) / 100 

279 

280 def _handle_tw(self, operands: list[Any]) -> None: 

281 """Handle Tw (Set word spacing) operation - Table 5.2 page 398.""" 

282 self.space_scale = 1.0 + _get_number_operand(operands, 0, 0.0) 

283 

284 def _handle_tl(self, operands: list[Any]) -> None: 

285 """Handle TL (Set Text Leading) operation - Table 5.2 page 398.""" 

286 # The leading is measured in unscaled text space units (PDF 32000-1, 9.3.5) 

287 # and T* applies the text matrix to it, so it must not be scaled here. 

288 self.TL = _get_number_operand(operands, 0, 0.0) 

289 

290 def _handle_tf(self, operands: list[Any]) -> None: 

291 """Handle Tf (Set font size) operation - Table 5.2 page 398.""" 

292 if self.text != "": 

293 self.output += self.text # .translate(cmap) 

294 if self.visitor_text is not None: 

295 self.visitor_text(self.text, self.memo_cm, self.memo_tm, self.font_resource, self.font_size) 

296 self.text = "" 

297 self.memo_cm = self.cm_matrix.copy() 

298 self.memo_tm = self.tm_matrix.copy() 

299 try: 

300 self.font_resource = self.font_resources[operands[0]] 

301 self.font = self.fonts[operands[0]] 

302 except (KeyError, IndexError): # font not found / operand missing 

303 self.font_resource = None 

304 font_descriptor = FontDescriptor() 

305 self.font = Font( 

306 "Unknown", 

307 space_width=250, 

308 encoding=dict.fromkeys(range(256), "�"), 

309 font_descriptor=font_descriptor, 

310 character_map={}, 

311 ) 

312 

313 self._space_width = self.font.space_width / 2 # Actually the width of _half_ a space... 

314 try: 

315 self.font_size = float(operands[1]) 

316 except Exception: 

317 pass # keep previous size 

318 

319 def _handle_td(self, operands: list[Any]) -> float: 

320 """Handle Td (Move text position) operation - Table 5.5 page 406.""" 

321 # A special case is a translating only tm: 

322 # tm = [1, 0, 0, 1, e, f] 

323 # i.e. tm[4] += tx, tm[5] += ty. 

324 tx = _get_number_operand(operands, 0, 0.0) 

325 ty = _get_number_operand(operands, 1, 0.0) 

326 self.tm_matrix[4] += tx * self.tm_matrix[0] + ty * self.tm_matrix[2] 

327 self.tm_matrix[5] += tx * self.tm_matrix[1] + ty * self.tm_matrix[3] 

328 str_widths = self.compute_str_widths(self._actual_str_size["str_widths"]) 

329 self._actual_str_size["str_widths"] = 0.0 

330 return str_widths 

331 

332 def _handle_tm(self, operands: list[Any]) -> float: 

333 """Handle Tm (Set text matrix) operation - Table 5.5 page 406.""" 

334 try: 

335 tm_matrix = [float(operand) for operand in operands[:6]] 

336 except (TypeError, ValueError): 

337 tm_matrix = [] 

338 # Fall back to the identity matrix when Tm carries the wrong number of 

339 # operands, mirroring _handle_cm, so the text matrix stays a six-element 

340 # list and later positioning operators do not read past its end. 

341 self.tm_matrix = tm_matrix if len(tm_matrix) == 6 else [1.0, 0.0, 0.0, 1.0, 0.0, 0.0] 

342 str_widths = self.compute_str_widths(self._actual_str_size["str_widths"]) 

343 self._actual_str_size["str_widths"] = 0.0 

344 return str_widths 

345 

346 def _handle_t_star(self, operands: list[Any]) -> float: 

347 """Handle T* (Move to next line) operation - Table 5.5 page 406.""" 

348 self.tm_matrix[4] -= self.TL * self.tm_matrix[2] 

349 self.tm_matrix[5] -= self.TL * self.tm_matrix[3] 

350 str_widths = self.compute_str_widths(self._actual_str_size["str_widths"]) 

351 self._actual_str_size["str_widths"] = 0.0 

352 return str_widths 

353 

354 def _handle_tj_operation(self, operands: list[Any]) -> float: 

355 """Handle Tj (Show text) operation - Table 5.5 page 406.""" 

356 if not operands or not isinstance(operands[0], (str, bytes)): 

357 return 0.0 

358 self.text, self.rtl_dir, self._actual_str_size = self._handle_tj( 

359 self.text, 

360 operands, 

361 self.cm_matrix, 

362 self.tm_matrix, 

363 self.font_resource, 

364 self.font, 

365 self.orientations, 

366 self.font_size, 

367 self.rtl_dir, 

368 self.visitor_text, 

369 self._actual_str_size, 

370 ) 

371 return 0.0 # str_widths will be handled in post-processing