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

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

118 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 .._font import Font 

12from .._utils import is_char_neutral, is_char_rtl 

13from ..generic import DictionaryObject, TextStringObject, encode_pdfdocencoding 

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( 

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) -> tuple[str, str, list[float], list[float]]: 

116 cm_prev = cmtm_prev[0] 

117 tm_prev = cmtm_prev[1] 

118 cm_matrix = cmtm_matrix[0] 

119 tm_matrix = cmtm_matrix[1] 

120 memo_cm = memo_cmtm[0] 

121 memo_tm = memo_cmtm[1] 

122 

123 m_prev = mult(tm_prev, cm_prev) 

124 m = mult(tm_matrix, cm_matrix) 

125 orientation = orient(m) 

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

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

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

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

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

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

132 # matrices instead of the text matrices alone. 

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

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

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

136 cm_prev = m 

137 

138 if orientation not in orientations: 

139 raise OrientationNotFoundError 

140 if orientation in (0, 180): 

141 moved_height: float = delta_y 

142 moved_width: float = delta_x 

143 elif orientation in (90, 270): 

144 moved_height = delta_x 

145 moved_width = delta_y 

146 try: 

147 if abs(moved_height) > 0.8 * min(str_height * scale_prev_y, font_size * scale_y): 

148 if (output + text)[-1] != "\n": 

149 output += text + "\n" 

150 if visitor_text is not None: 

151 visitor_text( 

152 text + "\n", 

153 memo_cm, 

154 memo_tm, 

155 font_resource, 

156 font_size, 

157 ) 

158 text = "" 

159 elif ( 

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

161 and (output + text)[-1] != " " 

162 ): 

163 text += " " 

164 except Exception: 

165 pass 

166 tm_prev = tm_matrix.copy() 

167 cm_prev = cm_matrix.copy() 

168 return text, output, cm_prev, tm_prev 

169 

170 

171def get_text_operands( 

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

173 cm_matrix: list[float], 

174 tm_matrix: list[float], 

175 font: Font, 

176 orientations: tuple[int, ...] 

177) -> tuple[str, bool]: 

178 t: str = "" 

179 is_str_operands = False 

180 m = mult(tm_matrix, cm_matrix) 

181 orientation = orient(m) 

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

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

184 t = operands[0] 

185 is_str_operands = True 

186 else: 

187 t = "" 

188 tt: bytes = ( 

189 encode_pdfdocencoding(operands[0]) 

190 if isinstance(operands[0], str) 

191 else operands[0] 

192 ) 

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

194 try: 

195 t = tt.decode(font.encoding, "surrogatepass") 

196 except Exception: 

197 # The data does not match the expectation, 

198 # we use "charmap" encoding as an alternative; 

199 # text extraction may not be good. 

200 t = tt.decode("charmap", "surrogatepass") 

201 else: # Apply dict encoding 

202 t = "".join( 

203 [font.encoding[x] if x in font.encoding else bytes((x,)).decode() for x in tt] 

204 ) 

205 return (t, is_str_operands) 

206 

207 

208def get_display_str( 

209 text: str, 

210 cm_matrix: list[float], 

211 tm_matrix: list[float], 

212 font_resource: Optional[DictionaryObject], 

213 font: Font, 

214 text_operands: str, 

215 font_size: float, 

216 rtl_dir: bool, 

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

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

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

220 widths: float = 0.0 

221 for raw_character in text_operands: 

222 widths += font.space_width if raw_character == font.space_char else font.get_text_width(raw_character) 

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

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

225 if len(x) == 1: 

226 if ( 

227 # Cases where the current inserting order is kept 

228 is_char_neutral(x, CUSTOM_RTL_SPECIAL_CHARS) 

229 ): 

230 text = x + text if rtl_dir else text + x 

231 elif ( 

232 # Right-to-left characters 

233 is_char_rtl(x, CUSTOM_RTL_MIN, CUSTOM_RTL_MAX) 

234 ): 

235 if not rtl_dir: 

236 rtl_dir = True 

237 if visitor_text is not None: 

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

239 text = "" 

240 text = x + text 

241 else: 

242 # Left-to-right characters 

243 if rtl_dir: 

244 rtl_dir = False 

245 if visitor_text is not None: 

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

247 text = "" 

248 text = text + x 

249 else: 

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

251 text = x + text if rtl_dir else text + x 

252 return text, rtl_dir, widths