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

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

149 statements  

1""" 

2Code related to text extraction. 

3 

4Some parts are still in _page.py. In doubt, they will stay there. 

5""" 

6 

7import math 

8from collections.abc import Mapping 

9from typing import Any, Callable, Literal, Optional, Union 

10 

11from .._utils import is_char_neutral, is_char_rtl 

12from ..generic import DictionaryObject, TextStringObject, encode_pdfdocencoding 

13from ..generic._font import Font 

14 

15CUSTOM_RTL_MIN: str = "" 

16CUSTOM_RTL_MAX: str = "" 

17CUSTOM_RTL_SPECIAL_CHARS: str = "" 

18LAYOUT_NEW_BT_GROUP_SPACE_WIDTHS: int = 5 

19UNICODE_LOWER_LIMIT = 0 

20UNICODE_UPPER_LIMIT = 0x10FFFF 

21 

22 

23class OrientationNotFoundError(Exception): 

24 pass 

25 

26 

27def set_custom_rtl( 

28 _min: Union[str, int, None] = "", 

29 _max: Union[str, int, None] = "", 

30 specials: Union[str, list[int], None] = None, 

31) -> tuple[str, str, str]: 

32 """ 

33 Change the Right-To-Left and special characters custom parameters. 

34 

35 Args: 

36 _min: The new minimum value for the range of custom characters that 

37 will be written right to left. 

38 If set to ``None``, the value will not be changed. 

39 If set to a valid integer, it will be converted to its corresponding character. 

40 The default value is "", which sets no additional range to be converted. 

41 _max: The new maximum value for the range of custom characters that will 

42 be written right to left. 

43 If set to ``None``, the value will not be changed. 

44 If set to a valid integer, it will be converted to its corresponding character. 

45 The default value is "", which sets no additional range to be converted. 

46 specials: The new list of special characters to be inserted in the 

47 current insertion order. 

48 If set to ``None``, the current value will not be changed. 

49 If set to a string, it will be converted to a list of ASCII codes. 

50 The default value is an empty list. 

51 

52 Returns: 

53 A tuple containing the new values for ``CUSTOM_RTL_MIN``, 

54 ``CUSTOM_RTL_MAX``, and ``CUSTOM_RTL_SPECIAL_CHARS``. 

55 

56 """ 

57 global CUSTOM_RTL_MIN, CUSTOM_RTL_MAX, CUSTOM_RTL_SPECIAL_CHARS 

58 if isinstance(_min, int): 

59 CUSTOM_RTL_MIN = chr(_min) if UNICODE_LOWER_LIMIT <= _min <= UNICODE_UPPER_LIMIT else "" 

60 elif isinstance(_min, str): 

61 CUSTOM_RTL_MIN = _min 

62 if isinstance(_max, int): 

63 CUSTOM_RTL_MAX = chr(_max) if UNICODE_LOWER_LIMIT <= _max <= UNICODE_UPPER_LIMIT else "" 

64 elif isinstance(_max, str): 

65 CUSTOM_RTL_MAX = _max 

66 if isinstance(specials, str): 

67 CUSTOM_RTL_SPECIAL_CHARS = specials 

68 elif isinstance(specials, list): 

69 CUSTOM_RTL_SPECIAL_CHARS = "".join( 

70 chr(char) for char in specials if UNICODE_LOWER_LIMIT <= char <= UNICODE_UPPER_LIMIT 

71 ) 

72 return CUSTOM_RTL_MIN, CUSTOM_RTL_MAX, CUSTOM_RTL_SPECIAL_CHARS 

73 

74 

75def mult( 

76 m: list[float], 

77 n: Union[ 

78 list[float], 

79 Mapping[Union[int, Literal["is_text", "is_render"]], Union[float, bool]], 

80 ], 

81) -> list[float]: 

82 return [ 

83 m[0] * n[0] + m[1] * n[2], 

84 m[0] * n[1] + m[1] * n[3], 

85 m[2] * n[0] + m[3] * n[2], 

86 m[2] * n[1] + m[3] * n[3], 

87 m[4] * n[0] + m[5] * n[2] + n[4], 

88 m[4] * n[1] + m[5] * n[3] + n[5], 

89 ] 

90 

91 

92def orient(m: list[float]) -> int: 

93 if m[3] > 1e-6: 

94 return 0 

95 if m[3] < -1e-6: 

96 return 180 

97 if m[1] > 0: 

98 return 90 

99 return 270 

100 

101 

102def crlf_space_check( # noqa: PLR0913, PLR0917 

103 text: str, 

104 cmtm_prev: tuple[list[float], list[float]], 

105 cmtm_matrix: tuple[list[float], list[float]], 

106 memo_cmtm: tuple[list[float], list[float]], 

107 font_resource: Optional[DictionaryObject], 

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

109 output: str, 

110 font_size: float, 

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

112 str_widths: float, 

113 spacewidth: float, 

114 str_height: float, 

115 line_span: Optional[tuple[int, float, float]], 

116) -> tuple[str, str, list[float], list[float], Optional[tuple[int, float, float]]]: 

117 cm_prev = cmtm_prev[0] 

118 tm_prev = cmtm_prev[1] 

119 cm_matrix = cmtm_matrix[0] 

120 tm_matrix = cmtm_matrix[1] 

121 memo_cm = memo_cmtm[0] 

122 memo_tm = memo_cmtm[1] 

123 

124 m_prev = mult(tm_prev, cm_prev) 

125 m = mult(tm_matrix, cm_matrix) 

126 orientation = orient(m) 

127 delta_x = m[4] - m_prev[4] 

128 delta_y = m[5] - m_prev[5] 

129 # Table 108 of the 1.7 reference ("Text positioning operators") 

130 # delta_x/delta_y are expressed in the coordinate system produced by 

131 # text matrix x current transformation matrix, so the scaling factors 

132 # they get compared against have to be taken from the same combined 

133 # matrices instead of the text matrices alone. 

134 scale_prev_x = math.sqrt(m_prev[0]**2 + m_prev[1]**2) 

135 scale_prev_y = math.sqrt(m_prev[2]**2 + m_prev[3]**2) 

136 scale_y = math.sqrt(m[2]**2 + m[3]**2) 

137 cm_prev = m 

138 

139 if orientation not in orientations: 

140 raise OrientationNotFoundError 

141 if orientation in (0, 180): 

142 moved_height: float = delta_y 

143 moved_width: float = delta_x 

144 elif orientation in (90, 270): 

145 moved_height = delta_x 

146 moved_width = delta_y 

147 # Keep track of the baseline range of the current extracted line. A centered 

148 # row label can fall between the two baselines of a wrapped cell (#4130), so 

149 # comparing only with the immediately preceding fragment can incorrectly 

150 # insert a line break. Use the established baseline range for that case. 

151 axis_index = 5 if orientation in (0, 180) else 4 

152 axis = m[axis_index] 

153 if line_span is None or line_span[0] != axis_index: 

154 distance = abs(moved_height) 

155 else: 

156 lower, upper = line_span[1], line_span[2] 

157 distance = 0.0 if lower <= axis <= upper else min(abs(axis - lower), abs(axis - upper)) 

158 last = (output + text)[-1:] 

159 if distance > 0.8 * min(str_height * scale_prev_y, font_size * scale_y): 

160 if last not in ("", "\n"): 

161 output += text + "\n" 

162 if visitor_text is not None: 

163 visitor_text( 

164 text + "\n", 

165 memo_cm, 

166 memo_tm, 

167 font_resource, 

168 font_size, 

169 ) 

170 text = "" 

171 line_span = (axis_index, axis, axis) 

172 else: 

173 if ( 

174 (moved_width >= (spacewidth + str_widths) * scale_prev_x) 

175 and last not in ("", " ") 

176 ): 

177 text += " " 

178 if last in ("", "\n") or line_span is None or line_span[0] != axis_index: 

179 line_span = (axis_index, axis, axis) 

180 else: 

181 line_span = (axis_index, min(line_span[1], axis), max(line_span[2], axis)) 

182 tm_prev = tm_matrix.copy() 

183 cm_prev = cm_matrix.copy() 

184 return text, output, cm_prev, tm_prev, line_span 

185 

186 

187def get_text_operands( 

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

189 cm_matrix: list[float], 

190 tm_matrix: list[float], 

191 font: Font, 

192 orientations: tuple[int, ...] 

193) -> tuple[str, bool, float]: 

194 text: str = "" 

195 is_str_operands = False 

196 widths: float = 0.0 

197 width_cache: dict[str, float] = {} 

198 m = mult(tm_matrix, cm_matrix) 

199 orientation = orient(m) 

200 raw_characters: str = "" 

201 if orientation in orientations and len(operands) > 0: 

202 if isinstance(operands[0], str): 

203 text = operands[0] 

204 is_str_operands = True 

205 else: 

206 text = "" 

207 tt: bytes = ( 

208 encode_pdfdocencoding(operands[0]) 

209 if isinstance(operands[0], str) 

210 else operands[0] 

211 ) 

212 if isinstance(font.encoding, str): # Apply named encoding 

213 try: 

214 text = tt.decode(font.encoding, "surrogatepass") 

215 except UnicodeDecodeError: 

216 # Fallback for odd byte counts or unmapped 16-bit GIDs/CIDs 

217 text = tt.decode(font.encoding, "surrogateescape") 

218 else: # Apply dict encoding 

219 text = "".join(font.encoding.get(x, chr(x)) for x in tt) 

220 raw_characters = "".join(chr(byte) for byte in tt) 

221 for char in (raw_characters or text): 

222 if char == font.space_char: 

223 widths += font.space_width 

224 else: 

225 if char not in width_cache: 

226 width_cache[char] = font.get_text_width(char) 

227 widths += width_cache[char] 

228 

229 width_cache.clear() 

230 return (text, is_str_operands, widths) 

231 

232 

233def get_display_str( 

234 text: str, 

235 cm_matrix: list[float], 

236 tm_matrix: list[float], 

237 font_resource: Optional[DictionaryObject], 

238 font: Font, 

239 text_operands: str, 

240 font_size: float, 

241 rtl_dir: bool, 

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

243) -> tuple[str, bool, str]: 

244 """ 

245 Add the characters of ``text_operands`` to ``text`` in display order. 

246 

247 Returns: 

248 A tuple containing the current text run, its direction, and any text runs completed 

249 because of a direction change. The completed runs precede the current run in display order. 

250 

251 """ 

252 # "\u0590 - \u08FF \uFB50 - \uFDFF" 

253 completed_text: list[str] = [] 

254 neutral_cache: dict[str, bool] = {} 

255 rtl_cache: dict[str, bool] = {} 

256 

257 def clear_character_caches() -> None: 

258 neutral_cache.clear() 

259 rtl_cache.clear() 

260 

261 for raw_character in text_operands: 

262 x = font.character_map.get(raw_character, raw_character) 

263 # Test whether x is a sequence of bytes; ex: habibi.pdf 

264 if len(x) == 1: 

265 if x not in neutral_cache: 

266 neutral_cache[x] = is_char_neutral(x, CUSTOM_RTL_SPECIAL_CHARS) 

267 if neutral_cache[x]: 

268 # Cases where the current inserting order is kept 

269 text = x + text if rtl_dir else text + x 

270 else: 

271 if x not in rtl_cache: 

272 rtl_cache[x] = is_char_rtl(x, CUSTOM_RTL_MIN, CUSTOM_RTL_MAX) 

273 if rtl_cache[x]: 

274 # Right-to-left characters 

275 if not rtl_dir: 

276 rtl_dir = True 

277 if visitor_text is not None: 

278 visitor_text(text, cm_matrix, tm_matrix, font_resource, font_size) 

279 clear_character_caches() 

280 completed_text.append(text) 

281 text = "" 

282 text = x + text 

283 else: 

284 # Left-to-right characters 

285 if rtl_dir: 

286 rtl_dir = False 

287 if visitor_text is not None: 

288 visitor_text(text, cm_matrix, tm_matrix, font_resource, font_size) 

289 clear_character_caches() 

290 completed_text.append(text) 

291 text = "" 

292 text = text + x 

293 else: 

294 # Treat a sequence of bytes as a neutral character. 

295 text = x + text if rtl_dir else text + x 

296 return text, rtl_dir, "".join(completed_text)