diffenator3_lib/render/
mod.rs1mod cachedoutlines;
6pub mod encodedglyphs;
7pub mod renderer;
8pub mod utils;
9pub mod wordlists;
10pub use crate::structs::{Difference, GlyphDiff};
11use crate::{
12 dfont::DFont,
13 render::{utils::count_differences, wordlists::direction_from_script},
14};
15use cfg_if::cfg_if;
16use harfrust::Script;
17use renderer::Renderer;
18use static_lang_word_lists::WordList;
19use std::{
20 collections::{BTreeMap, HashSet},
21 str::FromStr,
22};
23
24cfg_if! {
25 if #[cfg(not(target_family = "wasm"))] {
26 use indicatif::ParallelProgressIterator;
27 use rayon::iter::ParallelIterator;
28 use thread_local::ThreadLocal;
29 use std::cell::RefCell;
30 use std::sync::RwLock;
31 }
32}
33
34pub const DEFAULT_WORDS_FONT_SIZE: f32 = 16.0;
35pub const DEFAULT_GLYPHS_FONT_SIZE: f32 = 32.0;
36pub const DEFAULT_WORDS_THRESHOLD: usize = 8;
42pub const DEFAULT_GLYPHS_THRESHOLD: usize = 16;
43pub const DEFAULT_GRAY_FUZZ: u8 = 8;
45
46pub fn test_font_words(
52 font_a: &DFont,
53 font_b: &DFont,
54 custom_inputs: &[WordList],
55) -> BTreeMap<String, Vec<Difference>> {
56 let mut map: BTreeMap<String, Vec<Difference>> = BTreeMap::new();
57 let mut jobs: Vec<&WordList> = vec![];
58
59 let shared_codepoints = font_a
60 .codepoints
61 .intersection(&font_b.codepoints)
62 .copied()
63 .collect();
64
65 let supported_a = font_a.supported_scripts();
66 let supported_b = font_b.supported_scripts();
67
68 for script in supported_a.intersection(&supported_b) {
70 if let Some(wordlist) = wordlists::get_wordlist(script) {
71 jobs.push(wordlist);
72 }
73 }
74 jobs.extend(custom_inputs.iter());
75 for job in jobs.iter_mut() {
77 let results = diff_many_words(
78 font_a,
79 font_b,
80 DEFAULT_WORDS_FONT_SIZE,
81 job,
82 Some(&shared_codepoints),
83 DEFAULT_WORDS_THRESHOLD,
84 );
85 if !results.is_empty() {
86 map.insert(job.name().to_string(), results);
87 }
88 }
89 map
90}
91
92impl From<Difference> for GlyphDiff {
93 fn from(diff: Difference) -> Self {
94 if let Some(c) = diff.word.chars().next() {
95 GlyphDiff {
96 string: diff.word,
97 name: unicode_names2::name(c)
98 .map(|n| n.to_string())
99 .unwrap_or_default(),
100 unicode: format!("U+{:04X}", c as i32),
101 differing_pixels: diff.differing_pixels,
102 }
103 } else {
104 GlyphDiff {
105 string: "".to_string(),
106 name: "".to_string(),
107 unicode: "".to_string(),
108 differing_pixels: 0,
109 }
110 }
111 }
112}
113
114#[cfg(not(target_family = "wasm"))]
116pub(crate) fn diff_many_words(
134 font_a: &DFont,
135 font_b: &DFont,
136 font_size: f32,
137 wordlist: &WordList,
138 shared_codepoints: Option<&HashSet<u32>>,
139 threshold: usize,
140) -> Vec<Difference> {
141 let tl_a = ThreadLocal::new();
142 let tl_b = ThreadLocal::new();
143 let script = wordlist.script().and_then(|x| Script::from_str(x).ok());
144 let direction = script.and_then(direction_from_script);
145 let seen_glyphs = RwLock::new(HashSet::new());
147 let differences: Vec<Option<Difference>> = wordlist
148 .par_iter()
149 .progress()
150 .filter(|word| {
151 shared_codepoints
152 .as_ref()
153 .is_none_or(|scp| word.chars().all(|c| scp.contains(&(c as u32))))
154 })
155 .map(|word| {
156 let renderer_a =
157 tl_a.get_or(|| RefCell::new(Renderer::new(font_a, font_size, direction, script)));
158 let renderer_b =
159 tl_b.get_or(|| RefCell::new(Renderer::new(font_b, font_size, direction, script)));
160
161 let (buffer_a, commands_a) =
162 renderer_a.borrow_mut().string_to_positioned_glyphs(word)?;
163 if buffer_a
164 .split('|')
165 .all(|glyph| seen_glyphs.read().unwrap().contains(glyph))
166 {
167 return None;
168 }
169 for glyph in buffer_a.split('|') {
170 seen_glyphs.write().unwrap().insert(glyph.to_string());
171 }
172 let (buffer_b, commands_b) =
173 renderer_b.borrow_mut().string_to_positioned_glyphs(word)?;
174 if commands_a == commands_b {
175 return None;
176 }
177 let img_a = renderer_a
178 .borrow_mut()
179 .render_positioned_glyphs(&commands_a);
180 let img_b = renderer_b
181 .borrow_mut()
182 .render_positioned_glyphs(&commands_b);
183 let differing_pixels = count_differences(img_a, img_b, DEFAULT_GRAY_FUZZ);
184 let buffers_same = buffer_a == buffer_b;
185
186 Some(Difference {
187 word: word.to_string(),
188 buffer_a,
189 buffer_b: if buffers_same { None } else { Some(buffer_b) },
190 differing_pixels,
192 ot_features: "".to_string(),
193 lang: "".to_string(),
194 })
195 })
196 .collect();
197 let mut diffs: Vec<Difference> = differences
198 .into_iter()
199 .flatten()
200 .filter(|diff| diff.differing_pixels > threshold)
201 .collect();
202 diffs.sort_by_key(|x| -(x.differing_pixels as i32));
203 diffs
204}
205
206#[cfg(target_family = "wasm")]
208pub(crate) fn diff_many_words(
209 font_a: &DFont,
210 font_b: &DFont,
211 font_size: f32,
212 wordlist: &WordList,
213 shared_codepoints: Option<&HashSet<u32>>,
214 threshold: usize,
215) -> Vec<Difference> {
216 let script = wordlist.script().and_then(|x| Script::from_str(x).ok());
217 let direction = script.and_then(|s| direction_from_script(s));
218 let mut renderer_a = Renderer::new(font_a, font_size, direction, script);
219 let mut renderer_b = Renderer::new(font_b, font_size, direction, script);
220 let mut seen_glyphs: HashSet<String> = HashSet::new();
221
222 let mut differences: Vec<Difference> = vec![];
223 for word in wordlist.iter() {
224 if let Some(scp) = shared_codepoints {
225 if !word.chars().all(|c| scp.contains(&(c as u32))) {
226 continue;
227 }
228 }
229 let result_a = renderer_a.string_to_positioned_glyphs(&word);
230 if result_a.is_none() {
231 continue;
232 }
233 let (buffer_a, commands_a) = result_a.unwrap();
234 if buffer_a.split('|').all(|glyph| seen_glyphs.contains(glyph)) {
235 continue;
236 }
237 for glyph in buffer_a.split('|') {
238 seen_glyphs.insert(glyph.to_string());
239 }
240 let result_b = renderer_b.string_to_positioned_glyphs(&word);
241 if result_b.is_none() {
242 continue;
243 }
244 let (buffer_b, commands_b) = result_b.unwrap();
245 if commands_a == commands_b {
246 continue;
247 }
248 let buffers_same = buffer_a == buffer_b;
249 let img_a = renderer_a.render_positioned_glyphs(&commands_a);
250 let img_b = renderer_b.render_positioned_glyphs(&commands_b);
251 let differing_pixels = count_differences(img_a, img_b, DEFAULT_GRAY_FUZZ);
252 if differing_pixels > threshold {
253 differences.push(Difference {
254 word: word.to_string(),
255 buffer_a,
256 buffer_b: if buffers_same { None } else { Some(buffer_b) },
257 ot_features: "".to_string(),
259 lang: "".to_string(),
260 differing_pixels,
261 })
262 }
263 }
264 differences.sort_by_key(|x| -(x.differing_pixels as i32));
265
266 differences
267}
268
269