1 #[cfg(test)]
2 pub mod test_glyph_defs;
3 
4 use read_fonts::{
5     tables::colr::{CompositeMode, Extend},
6     types::{BoundingBox, GlyphId, Point},
7     FontRef,
8 };
9 use serde::{Deserialize, Serialize};
10 
11 use std::{
12     env,
13     fs::{File, OpenOptions},
14     io::{self, BufRead, Write},
15     string::String,
16 };
17 
18 use crate::{
19     color::{
20         transform::Transform, traversal_tests::test_glyph_defs::*, Brush, ColorPainter, ColorStop,
21     },
22     setting::VariationSetting,
23     MetadataProvider,
24 };
25 
26 #[derive(Serialize, Deserialize, Default, PartialEq)]
27 struct PaintDump {
28     glyph_id: u16,
29     ops: Vec<PaintOps>,
30 }
31 
32 impl From<Brush<'_>> for BrushParams {
from(brush: Brush) -> Self33     fn from(brush: Brush) -> Self {
34         match brush {
35             Brush::Solid {
36                 palette_index,
37                 alpha,
38             } => BrushParams::Solid {
39                 palette_index,
40                 alpha,
41             },
42             Brush::LinearGradient {
43                 p0,
44                 p1,
45                 color_stops,
46                 extend,
47             } => BrushParams::LinearGradient {
48                 p0,
49                 p1,
50                 color_stops: color_stops.to_vec(),
51                 extend,
52             },
53             Brush::RadialGradient {
54                 c0,
55                 r0,
56                 c1,
57                 r1,
58                 color_stops,
59                 extend,
60             } => BrushParams::RadialGradient {
61                 c0,
62                 r0,
63                 c1,
64                 r1,
65                 color_stops: color_stops.to_vec(),
66                 extend,
67             },
68             Brush::SweepGradient {
69                 c0,
70                 start_angle,
71                 end_angle,
72                 color_stops,
73                 extend,
74             } => BrushParams::SweepGradient {
75                 c0,
76                 start_angle,
77                 end_angle,
78                 color_stops: color_stops.to_vec(),
79                 extend,
80             },
81         }
82     }
83 }
84 
85 // Needed as a mirror struct with owned ColorStops for serialization, deserialization.
86 #[derive(Serialize, Deserialize, PartialEq)]
87 pub enum BrushParams {
88     Solid {
89         palette_index: u16,
90         alpha: f32,
91     },
92     // Normalized to a straight line between points p0 and p1,
93     // color stops normalized to align with both points.
94     LinearGradient {
95         p0: Point<f32>,
96         p1: Point<f32>,
97         color_stops: Vec<ColorStop>,
98         extend: Extend,
99     },
100     RadialGradient {
101         c0: Point<f32>,
102         r0: f32,
103         c1: Point<f32>,
104         r1: f32,
105         color_stops: Vec<ColorStop>,
106         extend: Extend,
107     },
108     SweepGradient {
109         c0: Point<f32>,
110         start_angle: f32,
111         end_angle: f32,
112         color_stops: Vec<ColorStop>,
113         extend: Extend,
114     },
115 }
116 
117 // Wrapping Transform for tests, as the results of trigonometric functions, in
118 // particular the tan() cases in PaintSkew need floating point PartialEq
119 // comparisons with an epsilon because the result of the tan() function differs
120 // on different platforms/archictectures.
121 #[derive(Serialize, Deserialize)]
122 struct DumpTransform(Transform);
123 
124 // Using the same value as in SK_ScalarNearlyZero from Skia (see SkScalar.h).
125 const NEARLY_EQUAL_TOLERANCE: f32 = 1.0 / (1 << 12) as f32;
126 
nearly_equal(a: f32, b: f32) -> bool127 fn nearly_equal(a: f32, b: f32) -> bool {
128     (a - b).abs() < NEARLY_EQUAL_TOLERANCE
129 }
130 
131 impl PartialEq<DumpTransform> for DumpTransform {
eq(&self, other: &DumpTransform) -> bool132     fn eq(&self, other: &DumpTransform) -> bool {
133         nearly_equal(self.0.xx, other.0.xx)
134             && nearly_equal(self.0.xy, other.0.xy)
135             && nearly_equal(self.0.yx, other.0.yx)
136             && nearly_equal(self.0.yy, other.0.yy)
137             && nearly_equal(self.0.dx, other.0.dx)
138             && nearly_equal(self.0.dy, other.0.dy)
139     }
140 }
141 
142 impl From<Transform> for DumpTransform {
from(value: Transform) -> Self143     fn from(value: Transform) -> Self {
144         Self(value)
145     }
146 }
147 
148 #[derive(Serialize, Deserialize, PartialEq)]
149 enum PaintOps {
150     PushTransform {
151         transform: DumpTransform,
152     },
153     PopTransform,
154     PushClipGlyph {
155         gid: u16,
156     },
157     PushClipBox {
158         clip_box: BoundingBox<f32>,
159     },
160     PopClip,
161     FillBrush {
162         brush: BrushParams,
163     },
164     FillGlyph {
165         gid: u16,
166         transform: DumpTransform,
167         brush: BrushParams,
168     },
169     PushLayer {
170         composite_mode: u8,
171     },
172     PopLayer,
173 }
174 
175 impl ColorPainter for PaintDump {
push_transform(&mut self, transform: Transform)176     fn push_transform(&mut self, transform: Transform) {
177         self.ops.push(PaintOps::PushTransform {
178             transform: transform.into(),
179         });
180     }
pop_transform(&mut self)181     fn pop_transform(&mut self) {
182         self.ops.push(PaintOps::PopTransform);
183     }
184 
push_clip_glyph(&mut self, glyph: GlyphId)185     fn push_clip_glyph(&mut self, glyph: GlyphId) {
186         self.ops.push(PaintOps::PushClipGlyph {
187             gid: glyph.to_u16(),
188         });
189     }
190 
push_clip_box(&mut self, clip_box: BoundingBox<f32>)191     fn push_clip_box(&mut self, clip_box: BoundingBox<f32>) {
192         self.ops.push(PaintOps::PushClipBox { clip_box });
193     }
194 
pop_clip(&mut self)195     fn pop_clip(&mut self) {
196         self.ops.push(PaintOps::PopClip);
197     }
198 
fill(&mut self, brush: Brush)199     fn fill(&mut self, brush: Brush) {
200         self.ops.push(PaintOps::FillBrush {
201             brush: brush.into(),
202         });
203     }
204 
fill_glyph(&mut self, glyph_id: GlyphId, transform: Option<Transform>, brush: Brush)205     fn fill_glyph(&mut self, glyph_id: GlyphId, transform: Option<Transform>, brush: Brush) {
206         self.ops.push(PaintOps::FillGlyph {
207             gid: glyph_id.to_u16(),
208             transform: transform.unwrap_or_default().into(),
209             brush: brush.into(),
210         });
211     }
212 
push_layer(&mut self, composite_mode: CompositeMode)213     fn push_layer(&mut self, composite_mode: CompositeMode) {
214         self.ops.push(PaintOps::PushLayer {
215             composite_mode: composite_mode as u8,
216         });
217     }
pop_layer(&mut self)218     fn pop_layer(&mut self) {
219         self.ops.push(PaintOps::PopLayer);
220     }
221 }
222 
223 impl PaintDump {
new(gid: u16) -> Self224     pub fn new(gid: u16) -> Self {
225         Self {
226             glyph_id: gid,
227             ..Default::default()
228         }
229     }
230 }
231 
location_to_filename<I>(set_name: &str, settings: I) -> String where I: IntoIterator, I::Item: Into<VariationSetting>,232 fn location_to_filename<I>(set_name: &str, settings: I) -> String
233 where
234     I: IntoIterator,
235     I::Item: Into<VariationSetting>,
236 {
237     let formatted_settings: Vec<String> = settings
238         .into_iter()
239         .map(|entry| {
240             let entry_setting = entry.into();
241             format!("{:}_{:}", entry_setting.selector, entry_setting.value)
242         })
243         .collect();
244     let suffix = match formatted_settings.len() {
245         0 => String::new(),
246         _ => format!("_{}", formatted_settings.join("_")),
247     };
248     format!("colrv1_{}{}", set_name.to_lowercase(), suffix)
249 }
250 
should_rebaseline() -> bool251 fn should_rebaseline() -> bool {
252     env::var("REBASELINE_COLRV1_TESTS").is_ok()
253 }
254 
255 // To regenerate the baselines, set the enviroment variable `REBASELINE_COLRV1_TESTS`
256 // when running tests, for example like this:
257 // $ REBASELINE_COLRV1_TESTS=1 cargo test color::traversal
colrv1_traversal_test( set_name: &str, test_chars: &[char], settings: &[(&str, f32)], required_format: crate::color::ColorGlyphFormat, )258 fn colrv1_traversal_test(
259     set_name: &str,
260     test_chars: &[char],
261     settings: &[(&str, f32)],
262     required_format: crate::color::ColorGlyphFormat,
263 ) {
264     let colr_font = font_test_data::COLRV0V1_VARIABLE;
265     let font = FontRef::new(colr_font).unwrap();
266 
267     let location = font.axes().location(settings);
268 
269     let dumpfile_path = format!(
270         "../font-test-data/test_data/colrv1_json/{}",
271         location_to_filename(set_name, settings)
272     );
273 
274     let test_gids = test_chars
275         .iter()
276         .map(|codepoint| font.charmap().map(*codepoint).unwrap());
277 
278     let paint_dumps_iter = test_gids.map(|gid| {
279         let mut color_painter = PaintDump::new(gid.to_u16());
280 
281         let color_glyph = font.color_glyphs().get_with_format(gid, required_format);
282 
283         assert!(color_glyph.is_some());
284 
285         let result = color_glyph
286             .unwrap()
287             .paint(location.coords(), &mut color_painter);
288 
289         assert!(result.is_ok());
290 
291         color_painter
292     });
293 
294     if should_rebaseline() {
295         let mut file = OpenOptions::new()
296             .write(true)
297             .create(true)
298             .open(dumpfile_path)
299             .unwrap();
300 
301         paint_dumps_iter.for_each(|dump| {
302             writeln!(file, "{}", serde_json::to_string(&dump).unwrap())
303                 .expect("Writing to file failed.")
304         });
305     } else {
306         let file = File::open(dumpfile_path).expect("Unable to open expectations file.");
307         let mut lines = io::BufReader::new(file).lines();
308         for dump in paint_dumps_iter {
309             match lines.next() {
310                 Some(line) => {
311                     assert!(
312                         dump == serde_json::from_str(
313                             line.as_ref().expect("Failed to read expectations line from file.")
314                         )
315                         .expect("Failed to parse expectations line."),
316                         "Result did not match expectation for glyph id: {}\nActual: {}\nExpected: {}\n",
317                         dump.glyph_id, serde_json::to_string(&dump).unwrap(), &line.unwrap()
318                     )
319                 }
320                 None => panic!("Expectation not found for glyph id: {}", dump.glyph_id),
321             }
322         }
323     }
324 }
325 
326 macro_rules! colrv1_traversal_tests {
327         ($($test_name:ident: $glyph_set:ident, $settings:expr,)*) => {
328         $(
329             #[test]
330             fn $test_name() {
331                 colrv1_traversal_test(stringify!($glyph_set), $glyph_set, $settings, crate::color::ColorGlyphFormat::ColrV1);
332             }
333         )*
334     }
335 }
336 
337 colrv1_traversal_tests!(
338 clipbox_default:CLIPBOX,&[],
339 clipbox_var_1:CLIPBOX, &[("CLIO", 200.0)],
340 comp_mode_default:COMPOSITE_MODE,&[],
341 extend_mode_default:EXTEND_MODE,&[],
342 extend_mode_var1:EXTEND_MODE,&[("COL1", -0.25), ("COL3", 0.25)],
343 extend_mode_var2:EXTEND_MODE,&[("COL1", 0.5), ("COL3", -0.5)],
344 extend_mode_var3:EXTEND_MODE,&[("COL3", 0.5)],
345 extend_mode_var4:EXTEND_MODE,&[("COL3", 1.0)],
346 extend_mode_var5:EXTEND_MODE,&[("COL1", -1.5)],
347 extend_mode_var6:EXTEND_MODE,&[("GRR0", -200.0), ("GRR1", -300.0)],
348 extend_mode_var7:EXTEND_MODE,&[("GRX0", -1000.0), ("GRX1", -1000.0), ("GRR0", -1000.0), ("GRR1", -900.0)],
349 extend_mode_var8:EXTEND_MODE,&[("GRX0", 1000.0), ("GRX1", -1000.0), ("GRR0", -1000.0), ("GRR1", 200.0)],
350 extend_mode_var9:EXTEND_MODE,&[("GRR0", -50.0), ("COL3", -2.0), ("COL2", -2.0), ("COL1", -0.9)],
351 extend_mode_var10:EXTEND_MODE,&[("GRR0", -50.0), ("COL3", -2.0), ("COL2", -2.0), ("COL1", -1.1)],
352 extend_mode_var11:EXTEND_MODE,&[("COL3", 1.0), ("COL2", 1.5), ("COL1", 2.0)],
353 extend_mode_var12:EXTEND_MODE,&[("COL2", -0.3)],
354 extend_mode_var13:EXTEND_MODE,&[("GRR0", 430.0), ("GRR1", 40.0)],
355 foreground_color_default:FOREGROUND_COLOR,&[],
356 gradient_skewed:GRADIENT_P2_SKEWED,&[],
357 gradient_stops_repeat:GRADIENT_STOPS_REPEAT,&[],
358 paint_rotate_default:PAINT_ROTATE,&[],
359 paint_rotate_var1:PAINT_ROTATE,&[("ROTA", 40.0)],
360 paint_rotate_var2:PAINT_ROTATE,&[("ROTX", -250.0), ("ROTY", -250.0)],
361 paint_scale_default:PAINT_SCALE,&[],
362 paint_scale_var1:PAINT_SCALE,&[("SCOX", 200.0), ("SCOY", 200.0)],
363 paint_scale_var2:PAINT_SCALE,&[("SCSX", 0.25), ("SCOY", 0.25)],
364 paint_scale_var3:PAINT_SCALE,&[("SCSX", -1.0), ("SCOY", -1.0)],
365 paint_skew_default:PAINT_SKEW,&[],
366 paint_skew_var1:PAINT_SKEW,&[("SKXA", 20.0)],
367 paint_skew_var2:PAINT_SKEW,&[("SKYA", 20.0)],
368 paint_skew_var3:PAINT_SKEW,&[("SKCX", 200.0),("SKCY", 200.0)],
369 paint_transform_default:PAINT_TRANSFORM,&[],
370 paint_translate_default:PAINT_TRANSLATE,&[],
371 paint_translate_var_1:PAINT_TRANSLATE,&[("TLDX", 100.0), ("TLDY", 100.0)],
372 paint_sweep_default:SWEEP_VARSWEEP,&[],
373 paint_sweep_var1:SWEEP_VARSWEEP,&[("SWPS", 0.0)],
374 paint_sweep_var2:SWEEP_VARSWEEP,&[("SWPS", 90.0)],
375 paint_sweep_var3:SWEEP_VARSWEEP,&[("SWPE", -90.0)],
376 paint_sweep_var4:SWEEP_VARSWEEP,&[("SWPE", -45.0)],
377 paint_sweep_var5:SWEEP_VARSWEEP,&[("SWPS", -45.0),("SWPE", 45.0)],
378 paint_sweep_var6:SWEEP_VARSWEEP,&[("SWC1", -0.25), ("SWC2", 0.083333333), ("SWC3", 0.083333333), ("SWC4", 0.25)],
379 paint_sweep_var7:SWEEP_VARSWEEP,&[("SWPS", 45.0), ("SWPE", -45.0), ("SWC1", -0.25), ("SWC2", -0.416687), ("SWC3", -0.583313), ("SWC4", -0.75)],
380 variable_alpha_default:VARIABLE_ALPHA,&[],
381 variable_alpha_var1:VARIABLE_ALPHA,&[("APH1", -0.7)],
382 variable_alpha_var2:VARIABLE_ALPHA,&[("APH2", -0.7), ("APH3", -0.2)],
383 nocycle_multi_colrglyph:NO_CYCLE_MULTI_COLRGLYPH,&[],
384 sweep_coincident:SWEEP_COINCIDENT,&[],
385 paint_glyph_nested:PAINT_GLYPH_NESTED,&[],
386 );
387 
388 macro_rules! colrv0_traversal_tests {
389     ($($test_name:ident: $glyph_set:ident,)*) => {
390     $(
391         #[test]
392         fn $test_name() {
393             colrv1_traversal_test(stringify!($glyph_set), $glyph_set, &[], crate::color::ColorGlyphFormat::ColrV0);
394         }
395     )*
396 }
397 }
398 
399 colrv0_traversal_tests!(
400     colored_circles:COLORED_CIRCLES_V0,
401 );
402