Skip to main content

diffenator3_lib/render/
mod.rs

1/// Rendering and comparison of fonts
2///
3/// The routines in this file handle the rendering and comparison of text
4/// strings; the actual rendering proper is done in the `renderer` module.
5mod 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;
36/// Number of differing pixels after which two images are considered different
37///
38/// This is a count rather than a percentage, because a percentage would mean
39/// that significant differences could "hide" inside a long word. This should
40/// be adjusted to the size of the font and the expected differences.
41pub const DEFAULT_WORDS_THRESHOLD: usize = 8;
42pub const DEFAULT_GLYPHS_THRESHOLD: usize = 16;
43/// Gray pixels which differ by less than this amount are considered the same
44pub const DEFAULT_GRAY_FUZZ: u8 = 8;
45
46/// Compare two fonts by rendering a list of words and comparing the images
47///
48/// Word lists are gathered for all scripts which are supported by both fonts.
49/// The return value is a BTreeMap where each key is a script tag and the
50/// value is a list of  [Difference] objects.
51pub 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    // Create the jobs
69    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    // Process the jobs
76    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// A fast but complicated version
115#[cfg(not(target_family = "wasm"))]
116/// Compare two fonts by rendering a list of words and comparing the images
117///
118/// This function is parallelized and uses rayon to speed up the process.
119///
120/// # Arguments
121///
122/// * `font_a` - The first font to compare
123/// * `font_b` - The second font to compare
124/// * `font_size` - The size of the font to render
125/// * `wordlist` - A list of words to render
126/// * `threshold` - The percentage of differing pixels to consider a difference
127/// * `direction` - The direction of the text
128/// * `script` - The script of the text
129///
130/// # Returns
131///
132/// A list of [Difference] objects representing the differences between the two renderings.
133pub(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    // The cache should not be thread local
146    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                // diff_map,
191                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// A slow and simple version
207#[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                // diff_map,
258                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// #[cfg(test)]
270// mod tests {
271//     use std::{
272//         fs::File,
273//         io::{BufRead, BufReader},
274//     };
275
276//     use super::*;
277
278//     #[test]
279//     fn test_it_works() {
280//         let file = File::open("test-data/Latin.txt").expect("no such file");
281//         let buf = BufReader::new(file);
282//         let wordlist = buf
283//             .lines()
284//             .map(|l| l.expect("Could not parse line"))
285//             .collect();
286//         use std::time::Instant;
287//         let now = Instant::now();
288
289//         let mut results = diff_many_words_parallel(
290//             "test-data/NotoSansArabic-Old.ttf",
291//             "test-data/NotoSansArabic-New.ttf",
292//             20.0,
293//             wordlist,
294//             10.0,
295//         );
296//         results.sort_by_key(|f| (f.percent * 100.0) as u32);
297//         // for res in results {
298//         //     println!("{}: {}%", res.word, res.percent)
299//         // }
300//         let elapsed = now.elapsed();
301//         println!("Elapsed: {:.2?}", elapsed);
302//     }
303
304//     #[test]
305//     fn test_render() {
306//         let mut renderer_a = Renderer::new("test-data/NotoSansArabic-New.ttf", 40.0);
307//         let mut renderer_b = Renderer::new("test-data/NotoSansArabic-Old.ttf", 40.0);
308//         let (_, image_a) = renderer_a.render_string("پسے").unwrap(); // ď Ŭ
309//         let (_, image_b) = renderer_b.render_string("پسے").unwrap();
310//         let (image_a, image_b) = make_same_size(image_a, image_b);
311//         image_a.save("image_a.png").expect("Can't save");
312//         image_b.save("image_b.png").expect("Can't save");
313//         let differing_pixels = count_differences(image_a, image_b);
314//         println!("Ŏ Pixel differences: {:.2?}", differing_pixels);
315//         let threshold = 0.5;
316//         assert!(differing_pixels < threshold);
317//     }
318
319//     // #[test]
320//     // fn test_ascent() {
321//     //     let path = "Gulzar-Regular.ttf";
322//     //     let font_size = 100.0;
323//     //     let face = Face::from_file(path, 0).expect("No font");
324//     //     let data = std::fs::read(path).unwrap();
325//     //     let font = FontVec::try_from_vec(data).unwrap_or_else(|_| {
326//     //         panic!("error constructing a Font from data at {:?}", path);
327//     //     });
328//     //     let mut hb_font = HBFont::new(face);
329//     //     hb_font.set_scale(font_size as i32, font_size as i32);
330//     //     let extents = hb_font.get_font_h_extents().unwrap();
331//     //     let scaled_font = font.as_scaled(100.0);
332//     //     println!("factor: {}", factor);
333//     //     assert_eq!(scaled_font.ascent() / factor, extents.ascender as f32);
334//     // }
335// }