1 //! Utilities for the reference image test suite.
2 //!
3 //! This module has utility functions that are used in the test suite
4 //! to compare rendered surfaces to reference images.
5 
6 use cairo;
7 
8 use std::convert::TryFrom;
9 use std::env;
10 use std::fs::{self, File};
11 use std::io::{BufReader, Read};
12 use std::path::{Path, PathBuf};
13 use std::sync::Once;
14 
15 use librsvg::surface_utils::shared_surface::{SharedImageSurface, SurfaceType};
16 
17 use crate::compare_surfaces::{compare_surfaces, BufferDiff, Diff};
18 
19 pub struct Reference(SharedImageSurface);
20 
21 impl Reference {
from_png<P>(path: P) -> Result<Self, cairo::IoError> where P: AsRef<Path>,22     pub fn from_png<P>(path: P) -> Result<Self, cairo::IoError>
23     where
24         P: AsRef<Path>,
25     {
26         let file = File::open(path).map_err(|e| cairo::IoError::Io(e))?;
27         let mut reader = BufReader::new(file);
28         let surface = surface_from_png(&mut reader)?;
29         Self::from_surface(surface)
30     }
31 
from_surface(surface: cairo::ImageSurface) -> Result<Self, cairo::IoError>32     pub fn from_surface(surface: cairo::ImageSurface) -> Result<Self, cairo::IoError> {
33         let shared = SharedImageSurface::wrap(surface, SurfaceType::SRgb)?;
34         Ok(Self(shared))
35     }
36 }
37 
38 pub trait Compare {
compare(self, surface: &SharedImageSurface) -> Result<BufferDiff, cairo::IoError>39     fn compare(self, surface: &SharedImageSurface) -> Result<BufferDiff, cairo::IoError>;
40 }
41 
42 impl Compare for &Reference {
compare(self, surface: &SharedImageSurface) -> Result<BufferDiff, cairo::IoError>43     fn compare(self, surface: &SharedImageSurface) -> Result<BufferDiff, cairo::IoError> {
44         compare_surfaces(&self.0, surface).map_err(cairo::IoError::from)
45     }
46 }
47 
48 impl Compare for Result<Reference, cairo::IoError> {
compare(self, surface: &SharedImageSurface) -> Result<BufferDiff, cairo::IoError>49     fn compare(self, surface: &SharedImageSurface) -> Result<BufferDiff, cairo::IoError> {
50         self.map(|reference| reference.compare(surface))
51             .and_then(std::convert::identity)
52     }
53 }
54 
55 pub trait Evaluate {
evaluate(&self, output_surface: &SharedImageSurface, output_base_name: &str)56     fn evaluate(&self, output_surface: &SharedImageSurface, output_base_name: &str);
57 }
58 
59 impl Evaluate for BufferDiff {
60     /// Evaluates a BufferDiff and panics if there are relevant differences
61     ///
62     /// The `output_base_name` is used to write test results if the
63     /// surfaces are different.  If this is `foo`, this will write
64     /// `foo-out.png` with the `output_surf` and `foo-diff.png` with a
65     /// visual diff between `output_surf` and the `Reference` that this
66     /// diff was created from.
67     ///
68     /// # Panics
69     ///
70     /// Will panic if the surfaces are too different to be acceptable.
evaluate(&self, output_surf: &SharedImageSurface, output_base_name: &str)71     fn evaluate(&self, output_surf: &SharedImageSurface, output_base_name: &str) {
72         match self {
73             BufferDiff::DifferentSizes => unreachable!("surfaces should be of the same size"),
74 
75             BufferDiff::Diff(diff) => {
76                 if diff.distinguishable() {
77                     println!(
78                         "{}: {} pixels changed with maximum difference of {}",
79                         output_base_name, diff.num_pixels_changed, diff.max_diff,
80                     );
81 
82                     write_to_file(output_surf, output_base_name, "out");
83                     write_to_file(&diff.surface, output_base_name, "diff");
84 
85                     if diff.inacceptable() {
86                         panic!("surfaces are too different");
87                     }
88                 }
89             }
90         }
91     }
92 }
93 
94 impl Evaluate for Result<BufferDiff, cairo::IoError> {
evaluate(&self, output_surface: &SharedImageSurface, output_base_name: &str)95     fn evaluate(&self, output_surface: &SharedImageSurface, output_base_name: &str) {
96         self.as_ref()
97             .map(|diff| diff.evaluate(output_surface, output_base_name))
98             .unwrap();
99     }
100 }
101 
write_to_file(input: &SharedImageSurface, output_base_name: &str, suffix: &str)102 fn write_to_file(input: &SharedImageSurface, output_base_name: &str, suffix: &str) {
103     let path = output_dir().join(&format!("{}-{}.png", output_base_name, suffix));
104     println!("{}: {}", suffix, path.to_string_lossy());
105     let mut output_file = File::create(path).unwrap();
106     input
107         .clone()
108         .into_image_surface()
109         .unwrap()
110         .write_to_png(&mut output_file)
111         .unwrap();
112 }
113 
114 /// Creates a directory for test output and returns its path.
115 ///
116 /// The location for the output directory is taken from the `OUT_DIR` environment
117 /// variable if that is set. Otherwise std::env::temp_dir() will be used, which is
118 /// a platform dependent location for temporary files.
119 ///
120 /// # Panics
121 ///
122 /// Will panic if the output directory can not be created.
output_dir() -> PathBuf123 pub fn output_dir() -> PathBuf {
124     let tempdir = || {
125         let mut path = env::temp_dir();
126         path.push("rsvg-test-output");
127         path
128     };
129     let path = env::var_os("OUT_DIR").map_or_else(tempdir, PathBuf::from);
130 
131     fs::create_dir_all(&path).expect("could not create output directory for tests");
132 
133     path
134 }
135 
tolerable_difference() -> u8136 fn tolerable_difference() -> u8 {
137     static mut TOLERANCE: u8 = 2;
138 
139     static ONCE: Once = Once::new();
140     ONCE.call_once(|| unsafe {
141         if let Ok(str) = env::var("RSVG_TEST_TOLERANCE") {
142             let value: usize = str
143                 .parse()
144                 .expect("Can not parse RSVG_TEST_TOLERANCE as a number");
145             TOLERANCE =
146                 u8::try_from(value).expect("RSVG_TEST_TOLERANCE should be between 0 and 255");
147         }
148     });
149 
150     unsafe { TOLERANCE }
151 }
152 
153 pub trait Deviation {
distinguishable(&self) -> bool154     fn distinguishable(&self) -> bool;
inacceptable(&self) -> bool155     fn inacceptable(&self) -> bool;
156 }
157 
158 impl Deviation for Diff {
distinguishable(&self) -> bool159     fn distinguishable(&self) -> bool {
160         self.max_diff > 2
161     }
162 
inacceptable(&self) -> bool163     fn inacceptable(&self) -> bool {
164         self.max_diff > tolerable_difference()
165     }
166 }
167 
168 /// Creates a cairo::ImageSurface from a stream of PNG data.
169 ///
170 /// The surface is converted to ARGB if needed. Use this helper function with `Reference`.
surface_from_png<R>(stream: &mut R) -> Result<cairo::ImageSurface, cairo::IoError> where R: Read,171 pub fn surface_from_png<R>(stream: &mut R) -> Result<cairo::ImageSurface, cairo::IoError>
172 where
173     R: Read,
174 {
175     let png = cairo::ImageSurface::create_from_png(stream)?;
176     let argb = cairo::ImageSurface::create(cairo::Format::ARgb32, png.width(), png.height())?;
177     {
178         // convert to ARGB; the PNG may come as Rgb24
179         let cr = cairo::Context::new(&argb).expect("Failed to create a cairo context");
180         cr.set_source_surface(&png, 0.0, 0.0).unwrap();
181         cr.paint().unwrap();
182     }
183     Ok(argb)
184 }
185 
186 /// Macro test that compares render outputs
187 ///
188 /// Takes in SurfaceSize width and height, setting the cairo surface
189 #[macro_export]
190 macro_rules! test_compare_render_output {
191     ($test_name:ident, $width:expr, $height:expr, $test:expr, $reference:expr $(,)?) => {
192         #[test]
193         fn $test_name() {
194             crate::utils::setup_font_map();
195 
196             let sx: i32 = $width;
197             let sy: i32 = $height;
198             let svg = load_svg($test).unwrap();
199             let output_surf = render_document(
200                 &svg,
201                 SurfaceSize(sx, sy),
202                 |_| (),
203                 cairo::Rectangle {
204                     x: 0.0,
205                     y: 0.0,
206                     width: sx as f64,
207                     height: sy as f64,
208                 },
209             )
210             .unwrap();
211 
212             let reference = load_svg($reference).unwrap();
213             let reference_surf = render_document(
214                 &reference,
215                 SurfaceSize(sx, sy),
216                 |_| (),
217                 cairo::Rectangle {
218                     x: 0.0,
219                     y: 0.0,
220                     width: sx as f64,
221                     height: sy as f64,
222                 },
223             )
224             .unwrap();
225 
226             Reference::from_surface(reference_surf.into_image_surface().unwrap())
227                 .compare(&output_surf)
228                 .evaluate(&output_surf, stringify!($test_name));
229         }
230     };
231 }
232 
233 /// Render two SVG files and compare them.
234 ///
235 /// This is used to implement reference tests, or reftests.  Use it like this:
236 ///
237 /// ```ignore
238 /// test_svg_reference!(test_name, "tests/fixtures/blah/foo.svg", "tests/fixtures/blah/foo-ref.svg");
239 /// ```
240 ///
241 /// This will ensure that `foo.svg` and `foo-ref.svg` have exactly the same intrinsic dimensions,
242 /// and that they produce the same rendered output.
243 #[macro_export]
244 macro_rules! test_svg_reference {
245     ($test_name:ident, $test_filename:expr, $reference_filename:expr) => {
246         #[test]
247         fn $test_name() {
248             use crate::utils::setup_font_map;
249             use librsvg::{CairoRenderer, Loader};
250 
251             setup_font_map();
252 
253             let svg = Loader::new()
254                 .read_path($test_filename)
255                 .expect("reading SVG test file");
256             let reference = Loader::new()
257                 .read_path($reference_filename)
258                 .expect("reading reference file");
259 
260             let svg_renderer = CairoRenderer::new(&svg);
261             let ref_renderer = CairoRenderer::new(&reference);
262 
263             let svg_dim = svg_renderer.intrinsic_dimensions();
264             let ref_dim = ref_renderer.intrinsic_dimensions();
265 
266             assert_eq!(
267                 svg_dim, ref_dim,
268                 "sizes of SVG document and reference file are different"
269             );
270 
271             let pixels = svg_renderer
272                 .intrinsic_size_in_pixels()
273                 .unwrap_or((100.0, 100.0));
274 
275             let output_surf = render_document(
276                 &svg,
277                 SurfaceSize(pixels.0.ceil() as i32, pixels.1.ceil() as i32),
278                 |_| (),
279                 cairo::Rectangle {
280                     x: 0.0,
281                     y: 0.0,
282                     width: pixels.0,
283                     height: pixels.1,
284                 },
285             )
286             .unwrap();
287 
288             let reference_surf = render_document(
289                 &reference,
290                 SurfaceSize(pixels.0.ceil() as i32, pixels.1.ceil() as i32),
291                 |_| (),
292                 cairo::Rectangle {
293                     x: 0.0,
294                     y: 0.0,
295                     width: pixels.0,
296                     height: pixels.1,
297                 },
298             )
299             .unwrap();
300 
301             Reference::from_surface(reference_surf.into_image_surface().unwrap())
302                 .compare(&output_surf)
303                 .evaluate(&output_surf, stringify!($test_name));
304         }
305     };
306 }
307