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