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 crate::surface_utils::shared_surface::{SharedImageSurface, SurfaceType};
16
use crate::test_utils::{render_document, setup_font_map, SurfaceSize};
17
use crate::{CairoRenderer, Loader};
18

            
19
use super::compare_surfaces::{compare_surfaces, BufferDiff, Diff};
20
use super::load_svg;
21

            
22
pub struct Reference(SharedImageSurface);
23

            
24
impl Reference {
25
735
    pub fn from_png<P>(path: P) -> Self
26
    where
27
        P: AsRef<Path>,
28
    {
29
735
        let msg = format!("read reference PNG {}", path.as_ref().to_string_lossy());
30
735
        let file = File::open(path).expect(&msg);
31

            
32
735
        let mut reader = BufReader::new(file);
33
735
        let surface = surface_from_png(&mut reader).expect("decode reference PNG");
34
735
        Self::from_surface(surface)
35
735
    }
36

            
37
814
    pub fn from_surface(surface: cairo::ImageSurface) -> Self {
38
814
        let shared = SharedImageSurface::wrap(surface, SurfaceType::SRgb)
39
            .expect("wrap Cairo surface with SharedImageSurface");
40
814
        Self(shared)
41
814
    }
42
}
43

            
44
pub trait Compare {
45
    fn compare(self, surface: &SharedImageSurface) -> Result<BufferDiff, cairo::IoError>;
46
}
47

            
48
impl Compare for &Reference {
49
814
    fn compare(self, surface: &SharedImageSurface) -> Result<BufferDiff, cairo::IoError> {
50
814
        compare_surfaces(&self.0, surface).map_err(cairo::IoError::from)
51
814
    }
52
}
53

            
54
pub trait Evaluate {
55
    fn evaluate(&self, output_surface: &SharedImageSurface, output_base_name: &str);
56
}
57

            
58
impl Evaluate for BufferDiff {
59
    /// Evaluates a BufferDiff and panics if there are relevant differences
60
    ///
61
    /// The `output_base_name` is used to write test results if the
62
    /// surfaces are different.  If this is `foo`, this will write
63
    /// `foo-out.png` with the `output_surf` and `foo-diff.png` with a
64
    /// visual diff between `output_surf` and the `Reference` that this
65
    /// diff was created from.
66
    ///
67
    /// # Panics
68
    ///
69
    /// Will panic if the surfaces are too different to be acceptable.
70
801
    fn evaluate(&self, output_surf: &SharedImageSurface, output_base_name: &str) {
71
801
        match self {
72
            BufferDiff::DifferentSizes => unreachable!("surfaces should be of the same size"),
73

            
74
801
            BufferDiff::Diff(diff) => {
75
801
                if diff.distinguishable() {
76
3
                    println!(
77
                        "{}: {} pixels changed with maximum difference of {}",
78
                        output_base_name, diff.num_pixels_changed, diff.max_diff,
79
                    );
80

            
81
3
                    write_to_file(output_surf, output_base_name, "out");
82
3
                    write_to_file(&diff.surface, output_base_name, "diff");
83

            
84
3
                    if diff.inacceptable() {
85
                        panic!("surfaces are too different");
86
                    }
87
                }
88
            }
89
        }
90
801
    }
91
}
92

            
93
impl Evaluate for Result<BufferDiff, cairo::IoError> {
94
1602
    fn evaluate(&self, output_surface: &SharedImageSurface, output_base_name: &str) {
95
1602
        self.as_ref()
96
2403
            .map(|diff| diff.evaluate(output_surface, output_base_name))
97
            .unwrap();
98
801
    }
99
}
100

            
101
6
fn write_to_file(input: &SharedImageSurface, output_base_name: &str, suffix: &str) {
102
6
    let path = output_dir().join(format!("{}-{}.png", output_base_name, suffix));
103
6
    println!("{}: {}", suffix, path.to_string_lossy());
104
6
    let mut output_file = File::create(path).unwrap();
105
6
    input
106
        .clone()
107
        .into_image_surface()
108
        .unwrap()
109
        .write_to_png(&mut output_file)
110
6
        .unwrap();
111
6
}
112

            
113
/// Creates a directory for test output and returns its path.
114
///
115
/// The location for the output directory is taken from the `TESTS_OUTPUT_DIR` environment
116
/// variable if that is set. Otherwise std::env::temp_dir() will be used, which is
117
/// a platform dependent location for temporary files.
118
///
119
/// # Panics
120
///
121
/// Will panic if the output directory can not be created.
122
6
pub fn output_dir() -> PathBuf {
123
6
    let tempdir = || {
124
6
        let mut path = env::temp_dir();
125
6
        path.push("rsvg-test-output");
126
6
        path
127
6
    };
128
6
    let path = env::var_os("TESTS_OUTPUT_DIR").map_or_else(tempdir, PathBuf::from);
129

            
130
6
    fs::create_dir_all(&path).expect("could not create output directory for tests");
131

            
132
6
    path
133
6
}
134

            
135
16
fn tolerable_difference() -> u8 {
136
    static mut TOLERANCE: u8 = 8;
137

            
138
    static ONCE: Once = Once::new();
139
18
    ONCE.call_once(|| unsafe {
140
2
        if let Ok(str) = env::var("RSVG_TEST_TOLERANCE") {
141
            let value: usize = str
142
                .parse()
143
                .expect("Can not parse RSVG_TEST_TOLERANCE as a number");
144
            TOLERANCE =
145
                u8::try_from(value).expect("RSVG_TEST_TOLERANCE should be between 0 and 255");
146
        }
147
2
    });
148

            
149
16
    unsafe { TOLERANCE }
150
16
}
151

            
152
pub trait Deviation {
153
    fn distinguishable(&self) -> bool;
154
    fn inacceptable(&self) -> bool;
155
}
156

            
157
impl Deviation for Diff {
158
801
    fn distinguishable(&self) -> bool {
159
801
        self.max_diff > 2
160
801
    }
161

            
162
16
    fn inacceptable(&self) -> bool {
163
16
        self.max_diff > tolerable_difference()
164
16
    }
165
}
166

            
167
/// Creates a cairo::ImageSurface from a stream of PNG data.
168
///
169
/// The surface is converted to ARGB if needed. Use this helper function with `Reference`.
170
750
pub fn surface_from_png<R>(stream: &mut R) -> Result<cairo::ImageSurface, cairo::IoError>
171
where
172
    R: Read,
173
{
174
750
    let png = cairo::ImageSurface::create_from_png(stream)?;
175
750
    let argb = cairo::ImageSurface::create(cairo::Format::ARgb32, png.width(), png.height())?;
176
    {
177
        // convert to ARGB; the PNG may come as Rgb24
178
748
        let cr = cairo::Context::new(&argb).expect("Failed to create a cairo context");
179
748
        cr.set_source_surface(&png, 0.0, 0.0).unwrap();
180
748
        cr.paint().unwrap();
181
748
    }
182
748
    Ok(argb)
183
748
}
184

            
185
/// Macro test that compares render outputs
186
///
187
/// Takes in SurfaceSize width and height, setting the cairo surface
188
#[macro_export]
189
macro_rules! test_compare_render_output {
190
    ($test_name:ident, $width:expr, $height:expr, $test:expr, $reference:expr $(,)?) => {
191
        #[test]
192
42
        fn $test_name() {
193
21
            $crate::test_utils::reference_utils::compare_render_output(
194
                stringify!($test_name),
195
                $width,
196
                $height,
197
                $test,
198
                $reference,
199
            );
200
42
        }
201
    };
202
}
203

            
204
21
pub fn compare_render_output(
205
    test_name: &str,
206
    width: i32,
207
    height: i32,
208
    test: &'static [u8],
209
    reference: &'static [u8],
210
) {
211
21
    setup_font_map();
212

            
213
21
    let svg = load_svg(test).unwrap();
214
21
    let output_surf = render_document(
215
        &svg,
216
        SurfaceSize(width, height),
217
21
        |_| (),
218
21
        cairo::Rectangle::new(0.0, 0.0, f64::from(width), f64::from(height)),
219
    )
220
    .unwrap();
221

            
222
21
    let reference = load_svg(reference).unwrap();
223
21
    let reference_surf = render_document(
224
        &reference,
225
        SurfaceSize(width, height),
226
21
        |_| (),
227
21
        cairo::Rectangle::new(0.0, 0.0, f64::from(width), f64::from(height)),
228
    )
229
    .unwrap();
230

            
231
21
    Reference::from_surface(reference_surf.into_image_surface().unwrap())
232
        .compare(&output_surf)
233
21
        .evaluate(&output_surf, test_name);
234
21
}
235

            
236
/// Render two SVG files and compare them.
237
///
238
/// This is used to implement reference tests, or reftests.  Use it like this:
239
///
240
/// ```no_run
241
/// use rsvg::test_svg_reference;
242
/// test_svg_reference!(test_name, "tests/fixtures/blah/foo.svg", "tests/fixtures/blah/foo-ref.svg");
243
/// ```
244
///
245
/// This will ensure that `foo.svg` and `foo-ref.svg` have exactly the same intrinsic dimensions,
246
/// and that they produce the same rendered output.
247
#[macro_export]
248
macro_rules! test_svg_reference {
249
    ($test_name:ident, $test_filename:expr, $reference_filename:expr) => {
250
        #[test]
251
62
        fn $test_name() {
252
31
            $crate::test_utils::reference_utils::svg_reference_test(
253
                stringify!($test_name),
254
                $test_filename,
255
                $reference_filename,
256
            );
257
62
        }
258
    };
259
}
260

            
261
31
pub fn svg_reference_test(test_name: &str, test_filename: &str, reference_filename: &str) {
262
31
    setup_font_map();
263

            
264
31
    let svg = Loader::new()
265
        .read_path(test_filename)
266
        .expect("reading SVG test file");
267
31
    let reference = Loader::new()
268
        .read_path(reference_filename)
269
        .expect("reading reference file");
270

            
271
31
    let svg_renderer = CairoRenderer::new(&svg);
272
31
    let ref_renderer = CairoRenderer::new(&reference);
273

            
274
31
    let svg_dim = svg_renderer.intrinsic_dimensions();
275
31
    let ref_dim = ref_renderer.intrinsic_dimensions();
276

            
277
31
    assert_eq!(
278
        svg_dim, ref_dim,
279
        "sizes of SVG document and reference file are different"
280
    );
281

            
282
31
    let pixels = svg_renderer
283
        .intrinsic_size_in_pixels()
284
        .unwrap_or((100.0, 100.0));
285

            
286
31
    let output_surf = render_document(
287
        &svg,
288
31
        SurfaceSize(pixels.0.ceil() as i32, pixels.1.ceil() as i32),
289
31
        |_| (),
290
31
        cairo::Rectangle::new(0.0, 0.0, pixels.0, pixels.1),
291
    )
292
    .unwrap();
293

            
294
31
    let reference_surf = render_document(
295
        &reference,
296
31
        SurfaceSize(pixels.0.ceil() as i32, pixels.1.ceil() as i32),
297
31
        |_| (),
298
31
        cairo::Rectangle::new(0.0, 0.0, pixels.0, pixels.1),
299
    )
300
    .unwrap();
301

            
302
31
    Reference::from_surface(reference_surf.into_image_surface().unwrap())
303
        .compare(&output_surf)
304
31
        .evaluate(&output_surf, test_name);
305
31
}