1use crate::{
3 geometry::Box4,
4 text_snapshot::{Character, TextSnapshot},
5};
6use std::collections::HashSet;
7
8pub fn is_cjk(ch: char) -> bool {
10 matches!(ch as u32,
11 0x1100..=0x11ff | 0x2e80..=0xa4cf | 0xa960..=0xa97f | 0xac00..=0xd7ff |
12 0xf900..=0xfaff | 0xff66..=0xff9f | 0x1aff0..=0x1b16f | 0x20000..=0x323af)
13}
14
15fn local(b: Box4, angle: i32) -> Box4 {
17 match angle {
18 90 => [b[1], -b[2], b[3], -b[0]],
19 180 => [-b[2], -b[3], -b[0], -b[1]],
20 270 => [-b[3], b[0], -b[1], b[2]],
21 _ => b,
22 }
23}
24
25fn han(text: &str) -> bool {
27 let mut chars = text.chars();
28 let Some(ch) = chars.next() else { return false };
29 chars.next().is_none()
30 && matches!(ch as u32, 0x3400..=0x4dbf | 0x4e00..=0x9fff | 0xf900..=0xfaff | 0x20000..=0x323af)
31}
32
33pub fn generated_cjk_space(a: &Character, space: &Character, b: &Character) -> bool {
35 const PUNCTUATION: &str = "、。,;:?!“”‘’()【】《》〈〉「」『』〔〕[]{}—…·";
36 let punctuation = |text: &str| text.chars().count() == 1 && PUNCTUATION.contains(text);
37 if space.text != " "
38 || space.generated != Some(true)
39 || a.index.checked_add(1) != Some(space.index)
40 || space.index.checked_add(1) != Some(b.index)
41 || !((han(&a.text) && (han(&b.text) || punctuation(&b.text)))
42 || (punctuation(&a.text) && han(&b.text)))
43 || !a.writing_angle.is_finite()
44 || !b.writing_angle.is_finite()
45 {
46 return false;
47 }
48 let degrees = a.writing_angle.to_degrees().rem_euclid(360.0);
49 let angle = ((degrees / 90.0).round_ties_even() as i32 * 90) % 360;
50 if ((degrees - angle as f64 + 180.0).rem_euclid(360.0) - 180.0).abs() > 0.1
51 || ((b.writing_angle.to_degrees() - angle as f64 + 180.0).rem_euclid(360.0) - 180.0).abs()
52 > 0.1
53 {
54 return false;
55 }
56 let ab = a.tight.unwrap_or(a.bbox);
57 let bb = b.tight.unwrap_or(b.bbox);
58 if ab.iter().chain(bb.iter()).any(|v| !v.is_finite()) {
59 return false;
60 }
61 let (ab, bb) = (local(ab, angle), local(bb, angle));
62 let (ah, bh) = (ab[3] - ab[1], bb[3] - bb[1]);
63 if ah.min(bh).min(ab[2] - ab[0]).min(bb[2] - bb[0]) <= 0.0
64 || ab[3].min(bb[3]) - ab[1].max(bb[1]) < 0.5 * ah.min(bh)
65 || bb[0] - ab[2] > 0.5 * ah.max(bh)
66 {
67 return false;
68 }
69 let tolerance = 0.15 * ah.max(bh);
70 if let (Some(ao), Some(bo)) = (a.origin, b.origin) {
71 let axis = usize::from(angle != 90 && angle != 270);
72 ao.iter().chain(bo.iter()).all(|v| v.is_finite())
73 && (ao[axis] - bo[axis]).abs() <= tolerance
74 } else {
75 (ab[3] - bb[3]).abs() <= tolerance
76 }
77}
78
79pub fn ligature_expansion_space(chars: &[&Character], index: usize) -> bool {
81 if index == 0 || index + 1 >= chars.len() || chars[index].text != " " {
82 return false;
83 }
84 let right = chars[index + 1];
85 if right.text.len() != 1 || !right.text.as_bytes()[0].is_ascii_alphabetic() {
86 return false;
87 }
88 for count in 1..=index.min(3) {
89 let glyph = &chars[index - count..index];
90 let text: String = glyph.iter().map(|ch| ch.text.as_str()).collect();
91 if !matches!(
92 text.as_str(),
93 "fi" | "fl"
94 | "ff"
95 | "ffi"
96 | "ffl"
97 | "ft"
98 | "st"
99 | "fi"
100 | "fl"
101 | "ff"
102 | "ffi"
103 | "ffl"
104 | "ſt"
105 | "st"
106 ) {
107 continue;
108 }
109 let first = glyph[0];
110 let records = &chars[index - count..=index + 1];
111 if first.object.is_none()
112 || records
113 .windows(2)
114 .any(|pair| pair[0].index.checked_add(1) != Some(pair[1].index))
115 || records
116 .iter()
117 .any(|ch| ch.object != first.object || ch.font != first.font)
118 {
119 continue;
120 }
121 let degrees = first.writing_angle.to_degrees().rem_euclid(360.0);
122 let angle = ((degrees / 90.0).round_ties_even() as i32 * 90) % 360;
123 if records.iter().any(|ch| {
124 !ch.writing_angle.is_finite()
125 || ((ch.writing_angle.to_degrees() - angle as f64 + 180.0).rem_euclid(360.0)
126 - 180.0)
127 .abs()
128 > 0.1
129 || ch
130 .tight
131 .is_none_or(|bbox| bbox.iter().any(|v| !v.is_finite()))
132 || ch
133 .origin
134 .is_none_or(|point| point.iter().any(|v| !v.is_finite()))
135 }) {
136 continue;
137 }
138 let boxes: Vec<_> = records
139 .iter()
140 .map(|ch| local(ch.tight.unwrap(), angle))
141 .collect();
142 let points: Vec<_> = records
143 .iter()
144 .map(|ch| {
145 let p = ch.origin.unwrap();
146 let p = local([p[0], p[1], p[0], p[1]], angle);
147 [p[0], p[1]]
148 })
149 .collect();
150 let bbox = boxes[0];
151 let height = bbox[3] - bbox[1];
152 if height.min(bbox[2] - bbox[0]) <= 0.0
153 || (1..count).any(|i| {
154 (0..4).any(|j| (bbox[j] - boxes[i][j]).abs() > 0.01 * height)
155 || (0..2).any(|j| (points[0][j] - points[i][j]).abs() > 0.01 * height)
156 })
157 {
158 continue;
159 }
160 let next = boxes[count + 1];
161 let gap = next[0] - bbox[2];
162 if bbox[0] <= points[count][0]
163 && points[count][0] < bbox[2]
164 && points[count..]
165 .iter()
166 .all(|p| (p[1] - points[0][1]).abs() <= 0.15 * height)
167 && gap >= -0.15 * height
168 && gap <= 0.15 * height
169 && (points[count + 1][0] - bbox[2]).abs() <= 0.15 * height
170 && next[3] > next[1]
171 && bbox[3].min(next[3]) - bbox[1].max(next[1]) >= 0.5 * height.min(next[3] - next[1])
172 {
173 return true;
174 }
175 }
176 false
177}
178
179fn ordinary(text: &str, letters: &HashSet<char>) -> bool {
181 let mut chars = text.chars();
182 let Some(ch) = chars.next() else { return false };
183 chars.next().is_none() && !is_cjk(ch) && (ch.is_ascii_alphanumeric() || letters.contains(&ch))
184}
185
186pub fn needs_space(
188 data: &TextSnapshot,
189 first: usize,
190 second: usize,
191 letters: &HashSet<char>,
192 decimals: &HashSet<char>,
193) -> bool {
194 let a = &data.chars[first];
195 let b = &data.chars[second];
196 if !ordinary(&a.text, letters)
197 || !ordinary(&b.text, letters)
198 || a.index.checked_add(1) != Some(b.index)
199 {
200 return false;
201 }
202 if a.text
203 .chars()
204 .all(|c| c.is_ascii_digit() || decimals.contains(&c))
205 && b.text
206 .chars()
207 .all(|c| c.is_ascii_digit() || decimals.contains(&c))
208 {
209 return false;
210 }
211 for font in [&data.fonts[a.font], &data.fonts[b.font]] {
212 let name = font.name.to_lowercase();
213 if font.flags & 1 != 0
214 || [
215 "mono",
216 "courier",
217 "consolas",
218 "menlo",
219 "typewriter",
220 "fixed",
221 ]
222 .iter()
223 .any(|hint| name.contains(hint))
224 {
225 return false;
226 }
227 }
228 let ls = data.fonts[a.font].size;
229 let rs = data.fonts[b.font].size;
230 let em = ls.max(rs);
231 if !ls.is_finite() || !rs.is_finite() || ls.min(rs) <= 0.0 || ls.min(rs) < 0.8 * em {
232 return false;
233 }
234 if !a.writing_angle.is_finite() || !b.writing_angle.is_finite() {
235 return false;
236 }
237 let degrees = a.writing_angle.to_degrees().rem_euclid(360.0);
238 let angle = ((degrees / 90.0).round_ties_even() as i32 * 90) % 360;
239 if ((degrees - angle as f64 + 180.0).rem_euclid(360.0) - 180.0).abs() > 0.1
240 || ((b.writing_angle.to_degrees() - angle as f64 + 180.0).rem_euclid(360.0) - 180.0).abs()
241 > 0.1
242 {
243 return false;
244 }
245 let (Some(ab), Some(bb)) = (a.tight, b.tight) else {
246 return false;
247 };
248 if ab.iter().chain(bb.iter()).any(|v| !v.is_finite()) {
249 return false;
250 }
251 let ab = local(ab, angle);
252 let bb = local(bb, angle);
253 if ab[2] <= ab[0]
254 || bb[2] <= bb[0]
255 || ab[3] <= ab[1]
256 || bb[3] <= bb[1]
257 || ab[3] - ab[1] > 1.5 * ls
258 || bb[3] - bb[1] > 1.5 * rs
259 || bb[0] - ab[2] <= 0.25 * em
260 || ab[3].min(bb[3]) - ab[1].max(bb[1]) < 0.5 * (ab[3] - ab[1]).min(bb[3] - bb[1])
261 {
262 return false;
263 }
264 if let (Some(ao), Some(bo)) = (a.origin, b.origin) {
265 let axis = usize::from(angle != 90 && angle != 270);
266 if ao.iter().chain(bo.iter()).any(|v| !v.is_finite())
267 || (ao[axis] - bo[axis]).abs() > 0.15 * em
268 {
269 return false;
270 }
271 } else if (ab[3] - bb[3]).abs() > 0.15 * em {
272 return false;
273 }
274 true
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 fn character(text: &str, index: usize, angle: i32) -> Character {
283 let x = index as f64 * 6.0;
284 let bbox = local([x, 0.0, x + 10.0, 10.0], (360 - angle) % 360);
285 let point = local([x, 10.0, x, 10.0], (360 - angle) % 360);
286 Character {
287 text: text.into(),
288 bbox,
289 rotation: 0.0,
290 font: 0,
291 index,
292 sources: vec![index],
293 code: text.chars().next().unwrap() as u32,
294 generated: Some(text == " "),
295 object: None,
296 mode: None,
297 writing_angle: (angle as f64).to_radians(),
298 origin: Some([point[0], point[1]]),
299 loose: None,
300 tight: Some(bbox),
301 visible: None,
302 }
303 }
304
305 #[test]
307 fn ligature_internal_spaces_require_complete_evidence() {
308 for angle in [0, 90, 180, 270] {
309 let mut chars: Vec<_> = ["f", "i", " ", "n"]
310 .iter()
311 .enumerate()
312 .map(|(index, text)| character(text, index, angle))
313 .collect();
314 for (index, ch) in chars.iter_mut().enumerate() {
315 let (bbox, origin) = match index {
316 2 => ([12.0, 19.99, 15.0, 20.0], [12.0, 20.0]),
317 3 => ([15.3, 10.0, 20.0, 20.0], [15.2, 20.0]),
318 _ => ([10.0, 10.0, 15.0, 20.0], [10.0, 20.0]),
319 };
320 ch.tight = Some(local(bbox, (360 - angle) % 360));
321 let p = local(
322 [origin[0], origin[1], origin[0], origin[1]],
323 (360 - angle) % 360,
324 );
325 ch.origin = Some([p[0], p[1]]);
326 ch.object = Some(1);
327 ch.generated = Some(false);
328 }
329 assert!(ligature_expansion_space(
330 &chars.iter().collect::<Vec<_>>(),
331 2
332 ));
333 chars[0].object = Some(2);
334 assert!(!ligature_expansion_space(
335 &chars.iter().collect::<Vec<_>>(),
336 2
337 ));
338 chars[0].object = Some(1);
339 chars[3].tight = Some(local([18.0, 10.0, 23.0, 20.0], (360 - angle) % 360));
340 assert!(!ligature_expansion_space(
341 &chars.iter().collect::<Vec<_>>(),
342 2
343 ));
344 }
345 }
346
347 #[test]
348 fn generated_spaces_require_chinese_and_baseline_evidence() {
350 for angle in [0, 90, 180, 270] {
351 let a = character("卫", 0, angle);
352 let mut space = character(" ", 1, angle);
353 let mut b = character("星", 2, angle);
354 assert!(generated_cjk_space(&a, &space, &b));
355 b.text = "、".into();
356 assert!(generated_cjk_space(&a, &space, &b));
357 b.text = "A".into();
358 assert!(!generated_cjk_space(&a, &space, &b));
359 b.text = "星".into();
360 for value in [Some(false), None] {
361 space.generated = value;
362 assert!(!generated_cjk_space(&a, &space, &b));
363 }
364 space.generated = Some(true);
365 b.writing_angle += 0.3;
366 assert!(!generated_cjk_space(&a, &space, &b));
367 }
368 let a = character("卫", 0, 0);
369 let space = character(" ", 1, 0);
370 let mut b = character("星", 2, 0);
371 b.origin = Some([24.0, 30.0]);
372 assert!(!generated_cjk_space(&a, &space, &b));
373 b.origin = None;
374 assert!(generated_cjk_space(&a, &space, &b));
375 b.index = 3;
376 assert!(!generated_cjk_space(&a, &space, &b));
377 }
378}