1use crate::{
3 dedup,
4 geometry::{Box4, Size},
5};
6use std::collections::{HashMap, HashSet};
7
8#[derive(Clone, Debug)]
9pub struct Font {
10 pub name: String,
11 pub name_id: usize,
12 pub flags: i32,
13 pub size: f64,
14 pub weight: i32,
15}
16#[derive(Clone, Debug)]
17pub struct Character {
18 pub text: String,
19 pub bbox: Box4,
20 pub rotation: f64,
21 pub font: usize,
22 pub index: usize,
23 pub sources: Vec<usize>,
24 pub code: u32,
25 pub generated: Option<bool>,
26 pub object: Option<usize>,
27 pub mode: Option<i32>,
28 pub writing_angle: f64,
29 pub origin: Option<Size>,
30 pub loose: Option<Box4>,
31 pub tight: Option<Box4>,
32 pub visible: Option<bool>,
33}
34pub struct InputCharacter {
35 pub character: Character,
36 pub admitted: bool,
37 pub clip: Option<Box4>,
38}
39#[derive(Clone, Default)]
40pub struct Properties {
41 pub space: bool,
42 pub canonical: String,
43 pub han: bool,
44}
45#[derive(Clone, Copy)]
46pub struct Angle {
47 pub cos: f64,
48 pub sin: f64,
49 pub rounded: u64,
50 pub bucket: i64,
51}
52#[derive(Clone)]
53pub struct Glyph {
54 pub chars: Vec<Character>,
55 pub bbox: Option<Box4>,
56 pub text: String,
57 pub angle: f64,
58}
59#[derive(Debug)]
60pub enum SnapshotError {
61 MissingSource(usize),
62 Invalid(&'static str),
63}
64pub struct TextSnapshot {
65 pub chars: Vec<Character>,
66 pub fonts: Vec<Font>,
67 pub extended: bool,
68 pub visible_only: bool,
69}
70
71pub fn is_space(text: &str, properties: &HashMap<String, Properties>) -> bool {
73 !text.is_empty()
74 && text
75 .chars()
76 .all(|ch| properties.get(&ch.to_string()).is_some_and(|p| p.space))
77}
78fn min(a: f64, b: f64) -> f64 {
80 if b < a {
81 b
82 } else {
83 a
84 }
85}
86fn max(a: f64, b: f64) -> f64 {
88 if b > a {
89 b
90 } else {
91 a
92 }
93}
94fn intersection(a: Box4, b: Box4) -> Box4 {
96 [
97 max(a[0], b[0]),
98 max(a[1], b[1]),
99 min(a[2], b[2]),
100 min(a[3], b[3]),
101 ]
102}
103fn nonempty(b: Box4) -> bool {
105 b[2] > b[0] && b[3] > b[1]
106}
107fn clip_character(
109 ch: &mut Character,
110 clip: Box4,
111 properties: &HashMap<String, Properties>,
112) -> bool {
113 let ink = ch.tight.unwrap_or(ch.bbox);
114 if is_space(&ch.text, properties) {
115 let x = (ink[0] + ink[2]) / 2.0;
116 let y = (ink[1] + ink[3]) / 2.0;
117 return clip[0] <= x && x <= clip[2] && clip[1] <= y && y <= clip[3];
118 }
119 let visible = intersection(ink, clip);
120 if !nonempty(visible) {
121 return false;
122 }
123 if ink != visible {
124 let clipped = intersection(ch.bbox, clip);
125 ch.bbox = if nonempty(clipped) { clipped } else { visible };
126 for value in [&mut ch.loose, &mut ch.tight] {
127 if let Some(bbox) = *value {
128 let clipped = intersection(bbox, clip);
129 *value = Some(if nonempty(clipped) { clipped } else { visible });
130 }
131 }
132 }
133 true
134}
135pub fn initialize(
137 input: Vec<InputCharacter>,
138 properties: &HashMap<String, Properties>,
139) -> Vec<Character> {
140 let chars: Vec<Character> = input
141 .into_iter()
142 .filter_map(|item| {
143 if !item.admitted {
144 return None;
145 }
146 let mut ch = item.character;
147 if item
148 .clip
149 .is_some_and(|clip| !clip_character(&mut ch, clip, properties))
150 {
151 return None;
152 }
153 Some(ch)
154 })
155 .collect();
156 chars
157}
158
159pub struct WritingRun {
160 start: usize,
161 end: usize,
162 next: usize,
163}
164pub fn writing_runs(chars: &[Character]) -> Vec<WritingRun> {
166 let mut start = 0;
167 let mut runs = Vec::new();
168 while start < chars.len() {
169 let mut end = start + 1;
170 if let Some(object) = chars[start].object {
171 while end < chars.len() && chars[end].object == Some(object) {
172 end += 1;
173 }
174 if chars[start].origin.is_some() {
175 runs.push(WritingRun {
176 start,
177 end,
178 next: start + 1,
179 });
180 }
181 }
182 start = end;
183 }
184 runs
185}
186pub fn advance_writing_angles(
188 chars: &mut [Character],
189 runs: &mut [WritingRun],
190 directions: &HashMap<(u64, u64), (bool, f64)>,
191) -> Vec<(u64, u64)> {
192 let mut pending = HashSet::new();
193 let mut requested = Vec::new();
194 for run in runs {
195 let origin = chars[run.start].origin.unwrap();
196 while run.next < run.end {
197 let Some(next) = chars[run.next].origin else {
198 run.next += 1;
199 continue;
200 };
201 let key = (
202 (next[0] - origin[0]).to_bits(),
203 (next[1] - origin[1]).to_bits(),
204 );
205 let Some(&(valid, angle)) = directions.get(&key) else {
206 if pending.insert(key) {
207 requested.push(key);
208 }
209 break;
210 };
211 run.next += 1;
212 if valid {
213 for ch in &mut chars[run.start..run.end] {
214 ch.writing_angle = angle;
215 }
216 run.next = run.end;
217 }
218 }
219 }
220 requested
221}
222
223pub fn restore_surrogates(
225 chars: Vec<Character>,
226 raw_count: usize,
227) -> Result<Vec<Character>, SnapshotError> {
228 let codes: HashMap<usize, u32> = chars.iter().map(|ch| (ch.index, ch.code)).collect();
229 let mut consumed = HashSet::new();
230 let mut output = Vec::with_capacity(chars.len());
231 for mut ch in chars {
232 if consumed.contains(&ch.index) {
233 continue;
234 }
235 if ch.text == "\u{fffd}" && ch.index < raw_count {
236 let mut pair = None;
237 if (0xd800..=0xdbff).contains(&ch.code) && ch.index + 1 < raw_count {
238 let low = *codes
239 .get(&(ch.index + 1))
240 .ok_or(SnapshotError::MissingSource(ch.index + 1))?;
241 if (0xdc00..=0xdfff).contains(&low) {
242 consumed.insert(ch.index + 1);
243 pair = Some((ch.code, low, ch.index + 1));
244 }
245 } else if (0xdc00..=0xdfff).contains(&ch.code) && ch.index > 0 {
246 let high = *codes
247 .get(&(ch.index - 1))
248 .ok_or(SnapshotError::MissingSource(ch.index - 1))?;
249 if (0xd800..=0xdbff).contains(&high) {
250 pair = Some((high, ch.code, ch.index - 1));
251 }
252 }
253 if let Some((high, low, source)) = pair {
254 ch.text = char::from_u32(0x10000 + ((high - 0xd800) << 10) + (low - 0xdc00))
255 .unwrap()
256 .to_string();
257 ch.sources.push(source);
258 ch.sources.sort_unstable();
259 ch.sources.dedup();
260 }
261 }
262 output.push(ch);
263 }
264 Ok(output)
265}
266fn same_mapping(a: &Character, b: &Character, properties: &HashMap<String, Properties>) -> bool {
268 a.object.is_some()
269 && a.object == b.object
270 && a.sources.iter().max().and_then(|i| i.checked_add(1)) == Some(b.index)
271 && a.font == b.font
272 && a.rotation == b.rotation
273 && close(a.origin, b.origin)
274 && close(Some(a.bbox), Some(b.bbox))
275 && !a.text.is_empty()
276 && !b.text.is_empty()
277 && !is_space(&a.text, properties)
278 && !is_space(&b.text, properties)
279}
280fn close<const N: usize>(a: Option<[f64; N]>, b: Option<[f64; N]>) -> bool {
282 matches!((a, b), (Some(a), Some(b)) if a.iter().zip(b).all(|(x,y)| x.is_finite() && y.is_finite() && (*x-y).abs() <= 0.001))
283}
284fn sources<'a>(chars: impl IntoIterator<Item = &'a Character>) -> Vec<usize> {
286 let mut values: Vec<usize> = chars
287 .into_iter()
288 .flat_map(|ch| ch.sources.iter().copied())
289 .collect();
290 values.sort_unstable();
291 values.dedup();
292 values
293}
294pub fn mapping_metadata(
296 chars: &[Character],
297 properties: &HashMap<String, Properties>,
298) -> Vec<String> {
299 let mut start = 0;
300 let mut output = Vec::new();
301 let mut seen = HashSet::new();
302 while start < chars.len() {
303 let mut end = start + 1;
304 while end < chars.len() && same_mapping(&chars[end - 1], &chars[end], properties) {
305 end += 1;
306 }
307 if end > start + 1
308 && chars[start + 1..end]
309 .iter()
310 .any(|ch| ch.text != chars[start].text)
311 {
312 for ch in &chars[start..end] {
313 if seen.insert(ch.text.clone()) {
314 output.push(ch.text.clone());
315 }
316 }
317 }
318 start = end;
319 }
320 output
321}
322pub fn mapping_groups(
324 chars: Vec<Character>,
325 properties: &HashMap<String, Properties>,
326) -> Vec<Glyph> {
327 let mut groups: Vec<Vec<Character>> = Vec::new();
328 for ch in chars {
329 if groups
330 .last()
331 .is_some_and(|group| same_mapping(group.last().unwrap(), &ch, properties))
332 {
333 groups.last_mut().unwrap().push(ch);
334 } else {
335 groups.push(vec![ch]);
336 }
337 }
338 groups
339 .into_iter()
340 .map(|mut group| {
341 if group.len() > 1 && group.iter().any(|ch| ch.text != group[0].text) {
342 let canonical = properties
343 .get(&group[0].text)
344 .map(|p| p.canonical.as_str())
345 .unwrap_or(&group[0].text);
346 if properties.get(canonical).is_some_and(|p| p.han)
347 && group.iter().all(|ch| {
348 properties
349 .get(&ch.text)
350 .map(|p| p.canonical.as_str())
351 .unwrap_or(&ch.text)
352 == canonical
353 })
354 {
355 let mut head = group[0].clone();
356 head.text = canonical.to_owned();
357 head.sources = sources(group.iter());
358 group = vec![head];
359 }
360 }
361 let head = &group[0];
362 let angle = head.writing_angle;
363 let bbox = (angle.is_finite()
364 && head.bbox.iter().all(|v| v.is_finite())
365 && nonempty(head.bbox))
366 .then_some(head.bbox);
367 let text = group.iter().map(|ch| ch.text.as_str()).collect();
368 Glyph {
369 chars: group,
370 bbox,
371 text,
372 angle,
373 }
374 })
375 .collect()
376}
377fn merge_sources(retained: &mut Glyph, duplicate: &Glyph) {
379 if retained.chars.len() == duplicate.chars.len() {
380 for (a, b) in retained.chars.iter_mut().zip(&duplicate.chars) {
381 a.sources = sources([&*a, b]);
382 }
383 } else {
384 retained.chars[0].sources = sources(retained.chars.iter().chain(&duplicate.chars));
385 }
386}
387fn retained(
389 glyphs: Vec<Glyph>,
390 removed: &HashSet<usize>,
391 properties: &HashMap<String, Properties>,
392) -> Vec<Glyph> {
393 if removed.is_empty() {
394 return glyphs;
395 }
396 let mut trailing = vec![true; glyphs.len()];
397 let mut following = true;
398 for i in (0..glyphs.len()).rev() {
399 trailing[i] = following;
400 if !is_space(&glyphs[i].text, properties) {
401 following = removed.contains(&i);
402 }
403 }
404 let mut previous = true;
405 glyphs
406 .into_iter()
407 .enumerate()
408 .filter_map(|(i, g)| {
409 let space = is_space(&g.text, properties);
410 let orphaned = space && previous && trailing[i];
411 if !space {
412 previous = removed.contains(&i);
413 }
414 (!removed.contains(&i) && !orphaned).then_some(g)
415 })
416 .collect()
417}
418pub fn collapse_paints(
420 mut glyphs: Vec<Glyph>,
421 angles: &HashMap<u64, Angle>,
422 properties: &HashMap<String, Properties>,
423) -> Result<Vec<Glyph>, SnapshotError> {
424 let mut signatures = HashMap::new();
425 let records = glyphs
426 .iter()
427 .map(|g| {
428 let h = &g.chars[0];
429 let active = g.bbox.is_some()
430 && !g.text.is_empty()
431 && !is_space(&g.text, properties)
432 && h.object.is_some()
433 && h.origin.is_some()
434 && h.mode.is_some_and(|v| (0..=6).contains(&v));
435 if !active {
436 return (None, None, 0, 0, false);
437 }
438 let key = (
439 g.text.clone(),
440 h.font,
441 angles[&g.angle.to_bits()].rounded,
442 h.mode,
443 h.visible,
444 );
445 let next = signatures.len();
446 let signature = *signatures.entry(key).or_insert(next);
447 (g.bbox, h.origin, signature, h.object.unwrap(), true)
448 })
449 .collect();
450 let (exact, offsets) =
451 dedup::paint_pairs(records).ok_or(SnapshotError::Invalid("unsupported paint geometry"))?;
452 let active: HashSet<usize> = offsets.iter().map(|p| p.0).collect();
453 let mut texts = HashMap::new();
454 let mut angle_ids = HashMap::new();
455 let evidence = glyphs
456 .iter()
457 .enumerate()
458 .map(|(index, g)| {
459 let next = texts.len();
460 let text = *texts.entry(g.text.as_str()).or_insert(next);
461 let (projected, normal, angle_id) = if active.contains(&index) {
462 let angle = angles[&g.angle.to_bits()];
463 let origin = g.chars[0].origin.unwrap();
464 let next = angle_ids.len();
465 let id = *angle_ids.entry(angle.bucket).or_insert(next);
466 (
467 Some(dedup::project(g.bbox.unwrap(), angle.cos, angle.sin)),
468 -angle.sin * origin[0] + angle.cos * origin[1],
469 id,
470 )
471 } else {
472 (None, 0.0, 0)
473 };
474 (
475 projected,
476 normal,
477 angle_id,
478 text,
479 is_space(&g.text, properties),
480 )
481 })
482 .collect();
483 let confirmed = dedup::confirmed_offsets(evidence, offsets, exact.clone());
484 let roots = dedup::components(
485 glyphs.len(),
486 &exact.into_iter().chain(confirmed).collect::<Vec<_>>(),
487 );
488 for index in (0..glyphs.len()).rev() {
489 if roots[index] != index {
490 let duplicate = glyphs[index].clone();
491 merge_sources(&mut glyphs[roots[index]], &duplicate);
492 }
493 }
494 let removed = roots
495 .iter()
496 .enumerate()
497 .filter_map(|(i, &root)| (root != i).then_some(i))
498 .collect();
499 Ok(retained(glyphs, &removed, properties))
500}
501pub type HiddenPairs = Vec<(Vec<usize>, Vec<usize>)>;
503
504pub fn hidden_pairs(
506 glyphs: &[Glyph],
507 angles: &HashMap<u64, Angle>,
508 properties: &HashMap<String, Properties>,
509) -> Result<HiddenPairs, SnapshotError> {
510 if !glyphs.iter().any(|g| g.chars[0].mode == Some(3)) {
511 return Ok(Vec::new());
512 }
513 let mut objects = HashMap::new();
514 let records = glyphs
515 .iter()
516 .map(|g| {
517 let head = &g.chars[0];
518 let next = objects.len();
519 let object = *objects.entry(head.object).or_insert(next);
520 let angle = angles[&g.angle.to_bits()];
521 let mode = head.mode.filter(|v| (0..=6).contains(v)).unwrap_or(-1) as i8;
522 (
523 g.bbox,
524 head.origin,
525 object,
526 mode,
527 head.visible.unwrap_or(true),
528 !g.text.is_empty() && !is_space(&g.text, properties),
529 g.angle,
530 angle.cos,
531 angle.sin,
532 head.index,
533 )
534 })
535 .collect();
536 dedup::hidden_candidates(records)
537 .ok_or(SnapshotError::Invalid("unsupported hidden-text geometry"))
538}
539pub fn suppress_hidden(
541 mut glyphs: Vec<Glyph>,
542 pairs: HiddenPairs,
543 normalized: &HashMap<String, String>,
544 properties: &HashMap<String, Properties>,
545) -> Vec<Character> {
546 let mut removed = HashSet::new();
547 for (run, matches) in pairs {
548 let a: String = run.iter().map(|&i| glyphs[i].text.as_str()).collect();
549 let b: String = matches.iter().map(|&i| glyphs[i].text.as_str()).collect();
550 let a = &normalized[&a];
551 let b = &normalized[&b];
552 let aa: Vec<char> = a.chars().collect();
553 let bb: Vec<char> = b.chars().collect();
554 if aa.is_empty() || aa.len() != bb.len() {
555 continue;
556 }
557 if aa != bb {
558 let count = aa.iter().zip(&bb).filter(|(a, b)| a == b).count();
559 if count < 6
560 || (count as f64) / (aa.len() as f64) < 0.85
561 || !aa.iter().zip(&bb).all(|(a, b)| {
562 a == b
563 || (properties.get(&a.to_string()).is_some_and(|p| p.han)
564 && properties.get(&b.to_string()).is_some_and(|p| p.han))
565 })
566 {
567 continue;
568 }
569 }
570 removed.extend(run.iter().copied());
571 if run.len() == matches.len()
572 && run
573 .iter()
574 .zip(&matches)
575 .all(|(&a, &b)| glyphs[a].chars.len() == glyphs[b].chars.len())
576 {
577 for (&a, &b) in run.iter().zip(&matches) {
578 let duplicate = glyphs[a].clone();
579 merge_sources(&mut glyphs[b], &duplicate);
580 }
581 } else {
582 let values = sources(
583 glyphs[matches[0]]
584 .chars
585 .iter()
586 .chain(run.iter().flat_map(|&i| glyphs[i].chars.iter())),
587 );
588 glyphs[matches[0]].chars[0].sources = values;
589 }
590 }
591 retained(glyphs, &removed, properties)
592 .into_iter()
593 .flat_map(|g| g.chars)
594 .collect()
595}