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