1mod cachedoutlines;
6pub(crate) mod colorpainter;
7pub(crate) mod colorrenderer;
8pub mod encodedglyphs;
9pub mod renderer;
10pub mod utils;
11pub mod wordlists;
12pub use crate::structs::{Difference, GlyphDiff};
13use crate::{
14 dfont::DFont,
15 render::{utils::count_differences, wordlists::direction_from_script},
16};
17use cfg_if::cfg_if;
18use colorrenderer::ColorRenderer;
19use harfrust::Script;
20use renderer::Renderer;
21use skrifa::raw::TableProvider;
22use static_lang_word_lists::WordList;
23use std::{
24 collections::{BTreeMap, HashSet},
25 str::FromStr,
26};
27
28cfg_if! {
29 if #[cfg(not(target_family = "wasm"))] {
30 use indicatif::ParallelProgressIterator;
31 use rayon::iter::ParallelIterator;
32 use thread_local::ThreadLocal;
33 use std::cell::RefCell;
34 use std::sync::RwLock;
35 }
36}
37
38pub const DEFAULT_WORDS_FONT_SIZE: f32 = 16.0;
39pub const DEFAULT_GLYPHS_FONT_SIZE: f32 = 32.0;
40pub const DEFAULT_WORDS_THRESHOLD: usize = 8;
46pub const DEFAULT_GLYPHS_THRESHOLD: usize = 16;
47pub const DEFAULT_GRAY_FUZZ: u8 = 8;
49
50fn font_has_colr(dfont: &DFont) -> bool {
52 dfont.fontref().colr().is_ok()
53}
54
55pub fn test_font_words(
61 font_a: &DFont,
62 font_b: &DFont,
63 custom_inputs: &[WordList],
64) -> BTreeMap<String, Vec<Difference>> {
65 let mut map: BTreeMap<String, Vec<Difference>> = BTreeMap::new();
66 let mut jobs: Vec<&WordList> = vec![];
67
68 let shared_codepoints = font_a
69 .codepoints
70 .intersection(&font_b.codepoints)
71 .copied()
72 .collect();
73
74 let supported_a = font_a.supported_scripts();
75 let supported_b = font_b.supported_scripts();
76
77 for script in supported_a.intersection(&supported_b) {
79 if let Some(wordlist) = wordlists::get_wordlist(script) {
80 jobs.push(wordlist);
81 }
82 }
83 jobs.extend(custom_inputs.iter());
84 for job in jobs.iter_mut() {
86 let results = diff_many_words(
87 font_a,
88 font_b,
89 DEFAULT_WORDS_FONT_SIZE,
90 job,
91 Some(&shared_codepoints),
92 DEFAULT_WORDS_THRESHOLD,
93 );
94 if !results.is_empty() {
95 map.insert(job.name().to_string(), results);
96 }
97 }
98 map
99}
100
101impl From<Difference> for GlyphDiff {
102 fn from(diff: Difference) -> Self {
103 if let Some(c) = diff.word.chars().next() {
104 GlyphDiff {
105 string: diff.word,
106 name: unicode_names2::name(c)
107 .map(|n| n.to_string())
108 .unwrap_or_default(),
109 unicode: format!("U+{:04X}", c as i32),
110 differing_pixels: diff.differing_pixels,
111 }
112 } else {
113 GlyphDiff {
114 string: "".to_string(),
115 name: "".to_string(),
116 unicode: "".to_string(),
117 differing_pixels: 0,
118 }
119 }
120 }
121}
122
123#[cfg(not(target_family = "wasm"))]
125pub(crate) fn diff_many_words(
129 font_a: &DFont,
130 font_b: &DFont,
131 font_size: f32,
132 wordlist: &WordList,
133 shared_codepoints: Option<&HashSet<u32>>,
134 threshold: usize,
135) -> Vec<Difference> {
136 let script = wordlist.script().and_then(|x| Script::from_str(x).ok());
137 let direction = script.and_then(direction_from_script);
138 let seen_glyphs = RwLock::new(HashSet::new());
139 let use_color = font_has_colr(font_a) || font_has_colr(font_b);
140
141 let differences: Vec<Option<Difference>> = if use_color {
142 let tl_a: ThreadLocal<RefCell<ColorRenderer>> = ThreadLocal::new();
143 let tl_b: ThreadLocal<RefCell<ColorRenderer>> = ThreadLocal::new();
144 wordlist
145 .par_iter()
146 .progress()
147 .filter(|word| {
148 shared_codepoints
149 .as_ref()
150 .is_none_or(|scp| word.chars().all(|c| scp.contains(&(c as u32))))
151 })
152 .map(|word| {
153 let renderer_a = tl_a.get_or(|| {
154 RefCell::new(ColorRenderer::new(font_a, font_size, direction, script))
155 });
156 let renderer_b = tl_b.get_or(|| {
157 RefCell::new(ColorRenderer::new(font_b, font_size, direction, script))
158 });
159
160 let (buffer_a, img_a) = renderer_a.borrow_mut().render_string(word)?;
161 if buffer_a
162 .split('|')
163 .all(|glyph| seen_glyphs.read().unwrap().contains(glyph))
164 {
165 return None;
166 }
167 for glyph in buffer_a.split('|') {
168 seen_glyphs.write().unwrap().insert(glyph.to_string());
169 }
170 let (buffer_b, img_b) = renderer_b.borrow_mut().render_string(word)?;
171 let differing_pixels = count_differences(img_a, img_b, DEFAULT_GRAY_FUZZ);
172 let buffers_same = buffer_a == buffer_b;
173
174 Some(Difference {
175 word: word.to_string(),
176 buffer_a,
177 buffer_b: if buffers_same { None } else { Some(buffer_b) },
178 differing_pixels,
179 ot_features: "".to_string(),
180 lang: "".to_string(),
181 })
182 })
183 .collect()
184 } else {
185 let tl_a: ThreadLocal<RefCell<Renderer>> = ThreadLocal::new();
186 let tl_b: ThreadLocal<RefCell<Renderer>> = ThreadLocal::new();
187 wordlist
188 .par_iter()
189 .progress()
190 .filter(|word| {
191 shared_codepoints
192 .as_ref()
193 .is_none_or(|scp| word.chars().all(|c| scp.contains(&(c as u32))))
194 })
195 .map(|word| {
196 let renderer_a = tl_a
197 .get_or(|| RefCell::new(Renderer::new(font_a, font_size, direction, script)));
198 let renderer_b = tl_b
199 .get_or(|| RefCell::new(Renderer::new(font_b, font_size, direction, script)));
200
201 let (buffer_a, commands_a) =
202 renderer_a.borrow_mut().string_to_positioned_glyphs(word)?;
203 if buffer_a
204 .split('|')
205 .all(|glyph| seen_glyphs.read().unwrap().contains(glyph))
206 {
207 return None;
208 }
209 for glyph in buffer_a.split('|') {
210 seen_glyphs.write().unwrap().insert(glyph.to_string());
211 }
212 let (buffer_b, commands_b) =
213 renderer_b.borrow_mut().string_to_positioned_glyphs(word)?;
214 if commands_a == commands_b {
215 return None;
216 }
217 let img_a = renderer_a
218 .borrow_mut()
219 .render_positioned_glyphs(&commands_a);
220 let img_b = renderer_b
221 .borrow_mut()
222 .render_positioned_glyphs(&commands_b);
223 let differing_pixels = count_differences(img_a, img_b, DEFAULT_GRAY_FUZZ);
224 let buffers_same = buffer_a == buffer_b;
225
226 Some(Difference {
227 word: word.to_string(),
228 buffer_a,
229 buffer_b: if buffers_same { None } else { Some(buffer_b) },
230 differing_pixels,
231 ot_features: "".to_string(),
232 lang: "".to_string(),
233 })
234 })
235 .collect()
236 };
237
238 let mut diffs: Vec<Difference> = differences
239 .into_iter()
240 .flatten()
241 .filter(|diff| diff.differing_pixels > threshold)
242 .collect();
243 diffs.sort_by_key(|x| -(x.differing_pixels as i32));
244 diffs
245}
246
247#[cfg(target_family = "wasm")]
249pub(crate) fn diff_many_words(
250 font_a: &DFont,
251 font_b: &DFont,
252 font_size: f32,
253 wordlist: &WordList,
254 shared_codepoints: Option<&HashSet<u32>>,
255 threshold: usize,
256) -> Vec<Difference> {
257 let script = wordlist.script().and_then(|x| Script::from_str(x).ok());
258 let direction = script.and_then(|s| direction_from_script(s));
259 let use_color = font_has_colr(font_a) || font_has_colr(font_b);
260 let mut seen_glyphs: HashSet<String> = HashSet::new();
261 let mut differences: Vec<Difference> = vec![];
262
263 if use_color {
264 let mut renderer_a = ColorRenderer::new(font_a, font_size, direction, script);
265 let mut renderer_b = ColorRenderer::new(font_b, font_size, direction, script);
266
267 for word in wordlist.iter() {
268 if let Some(scp) = shared_codepoints {
269 if !word.chars().all(|c| scp.contains(&(c as u32))) {
270 continue;
271 }
272 }
273 let Some((buffer_a, img_a)) = renderer_a.render_string(&word) else {
274 continue;
275 };
276 if buffer_a.split('|').all(|glyph| seen_glyphs.contains(glyph)) {
277 continue;
278 }
279 for glyph in buffer_a.split('|') {
280 seen_glyphs.insert(glyph.to_string());
281 }
282 let Some((buffer_b, img_b)) = renderer_b.render_string(&word) else {
283 continue;
284 };
285 let buffers_same = buffer_a == buffer_b;
286 let differing_pixels = count_differences(img_a, img_b, DEFAULT_GRAY_FUZZ);
287 if differing_pixels > threshold {
288 differences.push(Difference {
289 word: word.to_string(),
290 buffer_a,
291 buffer_b: if buffers_same { None } else { Some(buffer_b) },
292 ot_features: "".to_string(),
293 lang: "".to_string(),
294 differing_pixels,
295 });
296 }
297 }
298 } else {
299 let mut renderer_a = Renderer::new(font_a, font_size, direction, script);
300 let mut renderer_b = Renderer::new(font_b, font_size, direction, script);
301
302 for word in wordlist.iter() {
303 if let Some(scp) = shared_codepoints {
304 if !word.chars().all(|c| scp.contains(&(c as u32))) {
305 continue;
306 }
307 }
308 let Some((buffer_a, commands_a)) = renderer_a.string_to_positioned_glyphs(&word) else {
309 continue;
310 };
311 if buffer_a.split('|').all(|glyph| seen_glyphs.contains(glyph)) {
312 continue;
313 }
314 for glyph in buffer_a.split('|') {
315 seen_glyphs.insert(glyph.to_string());
316 }
317 let Some((buffer_b, commands_b)) = renderer_b.string_to_positioned_glyphs(&word) else {
318 continue;
319 };
320 if commands_a == commands_b {
321 continue;
322 }
323 let buffers_same = buffer_a == buffer_b;
324 let img_a = renderer_a.render_positioned_glyphs(&commands_a);
325 let img_b = renderer_b.render_positioned_glyphs(&commands_b);
326 let differing_pixels = count_differences(img_a, img_b, DEFAULT_GRAY_FUZZ);
327 if differing_pixels > threshold {
328 differences.push(Difference {
329 word: word.to_string(),
330 buffer_a,
331 buffer_b: if buffers_same { None } else { Some(buffer_b) },
332 ot_features: "".to_string(),
333 lang: "".to_string(),
334 differing_pixels,
335 });
336 }
337 }
338 }
339
340 differences.sort_by_key(|x| -(x.differing_pixels as i32));
341 differences
342}