1use crate::{geometry::Box4, text_snapshot::TextSnapshot};
3use std::collections::{HashMap, HashSet};
4
5pub struct Content {
6 pub text: Option<String>,
7 pub private_count: usize,
8 pub text_count: usize,
9 pub private_run: usize,
10}
11
12pub fn groups(assignments: &[Option<usize>], span_count: usize) -> Vec<Vec<usize>> {
14 let mut output = vec![Vec::new(); span_count];
15 for (index, owner) in assignments.iter().enumerate() {
16 if let Some(owner) = owner {
17 output[*owner].push(index);
18 }
19 }
20 output
21}
22
23pub fn ordered(data: &TextSnapshot, group: &[usize]) -> Vec<usize> {
25 let mut values = group.to_vec();
26 if values
27 .windows(2)
28 .any(|pair| data.chars[pair[0]].index > data.chars[pair[1]].index)
29 {
30 values.sort_by_key(|&index| data.chars[index].index);
31 }
32 values
33}
34
35fn overlap(a: Box4, b: Box4, axis: usize) -> f64 {
37 let denominator = (a[axis + 2] - a[axis]).min(b[axis + 2] - b[axis]);
38 if denominator <= 0.0 {
39 return 0.0;
40 }
41 (a[axis + 2].min(b[axis + 2]) - a[axis].max(b[axis])).max(0.0) / denominator
42}
43
44pub fn composition_pair(
46 data: &TextSnapshot,
47 first: usize,
48 second: usize,
49 modifiers: &HashMap<String, String>,
50 threshold: f64,
51) -> Option<(usize, usize)> {
52 let (base, modifier) = if modifiers.contains_key(&data.chars[first].text) {
53 (second, first)
54 } else if modifiers.contains_key(&data.chars[second].text) {
55 (first, second)
56 } else {
57 return None;
58 };
59 let a = &data.chars[base];
60 let b = &data.chars[modifier];
61 (a.text.chars().count() == 1
62 && overlap(a.bbox, b.bbox, 0) >= threshold
63 && overlap(a.bbox, b.bbox, 1) >= threshold)
64 .then_some((base, modifier))
65}
66
67fn clean(text: &str) -> String {
69 let controls = text.replace("\r\n", "").replace('\u{2}', "-");
70 let mut output = String::with_capacity(controls.len());
71 for ch in controls.chars() {
72 match ch {
73 '\u{fb01}' => output.push_str("fi"),
74 '\u{fb02}' => output.push_str("fl"),
75 '\u{fb00}' => output.push_str("ff"),
76 '\u{fb03}' => output.push_str("ffi"),
77 '\u{fb04}' => output.push_str("ffl"),
78 '\u{fb05}' => output.push_str("ft"),
79 '\u{fb06}' => output.push_str("st"),
80 _ => output.push(ch),
81 }
82 }
83 output
84}
85
86pub struct Rules<'a> {
88 pub ordinary: &'a HashSet<char>,
89 pub decimals: &'a HashSet<char>,
90 pub tight_spacing: &'a [bool],
91 pub modifiers: &'a HashMap<String, String>,
92 pub threshold: f64,
93 pub compositions: &'a HashMap<(String, String), String>,
94 pub spaces: &'a HashSet<char>,
95 pub breaks: &'a HashSet<String>,
96 pub private_ranges: &'a [(u32, u32)],
97}
98
99pub fn build(data: &TextSnapshot, groups: &[Vec<usize>], rules: &Rules<'_>) -> Vec<Content> {
101 let Rules {
102 ordinary,
103 decimals,
104 tight_spacing,
105 modifiers,
106 threshold,
107 compositions,
108 spaces,
109 breaks,
110 private_ranges,
111 } = rules;
112 groups
113 .iter()
114 .enumerate()
115 .map(|(group_index, group)| {
116 let mut result = Content {
117 text: None,
118 private_count: 0,
119 text_count: 0,
120 private_run: 0,
121 };
122 let mut run = 0;
123 for &index in group {
124 for ch in data.chars[index].text.chars() {
125 if spaces.contains(&ch) {
126 run = 0;
127 continue;
128 }
129 result.text_count += 1;
130 if private_ranges
131 .iter()
132 .any(|&(start, end)| start <= ch as u32 && ch as u32 <= end)
133 {
134 result.private_count += 1;
135 run += 1;
136 result.private_run = result.private_run.max(run);
137 } else {
138 run = 0;
139 }
140 }
141 }
142 if group.is_empty() {
143 return result;
144 }
145 let members = ordered(data, group);
146 let mut chars: Vec<(&str, Box4, usize)> = Vec::with_capacity(members.len());
147 let mut cursor = 0;
148 while cursor < members.len() {
149 if cursor + 1 < members.len() {
150 if let Some((base, modifier)) = composition_pair(
151 data,
152 members[cursor],
153 members[cursor + 1],
154 modifiers,
155 *threshold,
156 ) {
157 let a = &data.chars[base];
158 let b = &data.chars[modifier];
159 if let Some(composed) = compositions.get(&(a.text.clone(), b.text.clone()))
160 {
161 chars.push((composed, a.bbox, base));
162 cursor += 2;
163 continue;
164 }
165 }
166 }
167 let ch = &data.chars[members[cursor]];
168 chars.push((&ch.text, ch.bbox, members[cursor]));
169 cursor += 1;
170 }
171 let mut widths: Vec<_> = chars.iter().map(|(_, b, _)| b[2] - b[0]).collect();
172 widths.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
173 let median = if widths.len() % 2 == 1 {
174 widths[widths.len() / 2]
175 } else {
176 (widths[widths.len() / 2 - 1] + widths[widths.len() / 2]) / 2.0
177 };
178 let mut text = String::new();
179 let spacing_chars: Vec<_> = chars
180 .iter()
181 .map(|(_, _, source)| &data.chars[*source])
182 .collect();
183 for (index, &(value, bbox, source)) in chars.iter().enumerate() {
184 if breaks.contains(value) {
185 continue;
186 }
187 if tight_spacing.get(group_index).copied().unwrap_or(false)
188 && index > 0
189 && index + 1 < chars.len()
190 && (crate::text_spacing::generated_cjk_space(
191 &data.chars[chars[index - 1].2],
192 &data.chars[source],
193 &data.chars[chars[index + 1].2],
194 ) || crate::text_spacing::ligature_expansion_space(&spacing_chars, index))
195 {
196 continue;
197 }
198 text.push_str(value);
199 if let Some(&(next, next_box, next_source)) = chars.get(index + 1) {
200 if (next_box[0] - bbox[2] > median * 0.25
201 || (tight_spacing.get(group_index).copied().unwrap_or(false)
202 && crate::text_spacing::needs_space(
203 data,
204 source,
205 next_source,
206 ordinary,
207 decimals,
208 )))
209 && value != " "
210 && next != " "
211 {
212 text.push(' ');
213 }
214 }
215 }
216 result.text = Some(clean(&text));
217 result
218 })
219 .collect()
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225 use crate::text_snapshot::{Character, Font};
226
227 #[test]
229 fn supplementary_private_ranges_preserve_text() {
230 let text = "A\u{e000}\u{f0000} \u{1001b0}B\u{f900}\u{ffffe}\u{20000}";
231 let data = TextSnapshot {
232 chars: text
233 .chars()
234 .enumerate()
235 .map(|(index, ch)| Character {
236 text: ch.to_string(),
237 bbox: [index as f64 * 5.0, 0.0, index as f64 * 5.0 + 5.0, 10.0],
238 rotation: 0.0,
239 font: 0,
240 index,
241 sources: vec![index],
242 code: ch as u32,
243 generated: Some(false),
244 object: Some(0),
245 mode: Some(0),
246 writing_angle: 0.0,
247 origin: None,
248 loose: None,
249 tight: None,
250 visible: Some(true),
251 })
252 .collect(),
253 fonts: vec![Font {
254 name: "TestFont".into(),
255 name_id: 0,
256 flags: 0,
257 size: 10.0,
258 weight: 400,
259 }],
260 extended: true,
261 visible_only: false,
262 };
263 let rules = Rules {
264 ordinary: &HashSet::new(),
265 decimals: &HashSet::new(),
266 tight_spacing: &[false],
267 modifiers: &HashMap::new(),
268 threshold: 0.5,
269 compositions: &HashMap::new(),
270 spaces: &HashSet::from([' ']),
271 breaks: &HashSet::new(),
272 private_ranges: &[(0xe000, 0xf8ff), (0xf0000, 0xffffd), (0x100000, 0x10fffd)],
273 };
274 let result = build(&data, &[(0..data.chars.len()).collect()], &rules);
275 assert_eq!(result[0].text.as_deref(), Some(text));
276 assert_eq!(result[0].text_count, 8);
277 assert_eq!(result[0].private_count, 3);
278 assert_eq!(result[0].private_run, 2);
279 }
280}