1/* -*- Mode: C; c-basic-offset: 2; -*- */
2/* GdkPixbuf library - test loaders
3 *
4 * Copyright (C) 2016 Martin Guy <martinwguy@gmail.com>
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, see <http://www.gnu.org/licenses/>.
18 */
19
20/*
21 * Test the two-step scaler speed optimization in gdk-pixbuf/pixops.c
22 * for result correctness. See https://bugzilla.gnome.org/show_bug.cgi?id=80925
23 *
24 * The two-step scaler kicks in when ceil(1/scale_x + 1) * ceil(1/scale_y + 1)
25 * (the number of 2KB filters created by make_filter_table()) exceeds 1000 so
26 * test cases wanting it have to do drastic image reductions, such as reducing
27 * both dimensions by a factor of more that sqrt(1000) - say by 40 both ways.
28 *
29 * There is a global boolean in the two-step-scaler allowing us to disable the
30 * two-step optimization to be able to compare the results with and without it.
31 */
32
33#include "config.h"
34#include "gdk-pixbuf/gdk-pixbuf.h"
35#include "test-common.h"
36
37/* Size of images to be scaled */
38#define SOURCE_WIDTH 400
39#define SOURCE_HEIGHT 400
40
41/* To trigger the two-step scaler, we need to reduce the total area by
42 * more than 1000. 40x40 is 1600. */
43#define SCALE_FACTOR (1.0/40.0)
44
45/* First, some functions to make test images */
46
47/*
48 * pixdata_almost_equal(): A helper function to check whether the pixels in
49 * two images are almost the same: pixel color values are allowed to differ
50 * by at most one.
51 */
52
53/* Are two values the same or differ by one? */
54#define within_one(a, b) \
55 ((a) == (b) || (a) == (b) + 1 || (a) + 1 == (b))
56
57static gboolean
58pixdata_almost_equal (GdkPixbuf *one, GdkPixbuf *two)
59{
60 guchar *one_row; /* Pointer to start of row of pixels in one */
61 guchar *one_pixel; /* Pointer to current pixel data in one */
62 guchar *two_row; /* Pointer to start of row of pixels in two */
63 guchar *two_pixel; /* Pointer to current pixel data in two */
64 guint x, y;
65 gint width_one, height_one;
66
67 width_one = gdk_pixbuf_get_width (pixbuf: one);
68 height_one = gdk_pixbuf_get_height (pixbuf: one);
69
70 g_assert_cmpint (height_one, >=, 0);
71 g_assert_cmpint (width_one, >=, 0);
72 g_assert_cmpint (gdk_pixbuf_get_height (two), >=, 0);
73 g_assert_cmpint (gdk_pixbuf_get_width (two), >=, 0);
74
75 if (width_one != gdk_pixbuf_get_width (pixbuf: two) ||
76 height_one != gdk_pixbuf_get_height (pixbuf: two))
77 {
78 g_test_fail();
79 }
80
81 for (y = 0, one_row = gdk_pixbuf_get_pixels (pixbuf: one),
82 two_row = gdk_pixbuf_get_pixels (pixbuf: two);
83 y < height_one;
84 y++, one_row += gdk_pixbuf_get_rowstride (pixbuf: one),
85 two_row += gdk_pixbuf_get_rowstride (pixbuf: two))
86 {
87 for (x = 0, one_pixel = one_row, two_pixel = two_row;
88 x < width_one;
89 x++, one_pixel += gdk_pixbuf_get_n_channels (pixbuf: one),
90 two_pixel += gdk_pixbuf_get_n_channels (pixbuf: two))
91 {
92 if (!(within_one(one_pixel[0], two_pixel[0]) &&
93 within_one(one_pixel[1], two_pixel[1]) &&
94 within_one(one_pixel[2], two_pixel[2])))
95 {
96 return FALSE;
97 }
98 }
99 }
100
101 return TRUE;
102}
103
104/* Create a checkerboard of (1,1,1) and (255,255,255) pixels and
105 * scale it to one fortieth of the image dimensions.
106 * See if the results are similar enough with and without the two-step
107 * optimization. */
108static void
109test_old_and_new (gconstpointer data)
110{
111 GdkInterpType interp_type = *(GdkInterpType *) data;
112 const GdkPixbuf *source; /* Source image */
113 gint width = SOURCE_WIDTH; /* Size of source image */
114 gint height = SOURCE_HEIGHT;
115 GdkPixbuf *one; /* Version scaled by the old code */
116 GdkPixbuf *two; /* Version scaled in two steps */
117
118 /* Use an extreme source image, checkerboard, to exacerbate any artifacts */
119 source = make_checkerboard (width, height);
120
121 /* Scale it without and with the two-step optimization */
122 g_setenv (variable: "GDK_PIXBUF_DISABLE_TWO_STEP_SCALER", value: "1", TRUE);
123 one = gdk_pixbuf_scale_simple (src: source,
124 dest_width: (int) (width * SCALE_FACTOR + 0.5),
125 dest_height: (int) (height * SCALE_FACTOR + 0.5),
126 interp_type);
127 g_unsetenv(variable: "GDK_PIXBUF_DISABLE_TWO_STEP_SCALER");
128 two = gdk_pixbuf_scale_simple (src: source,
129 dest_width: (int) (width * SCALE_FACTOR + 0.5),
130 dest_height: (int) (height * SCALE_FACTOR + 0.5),
131 interp_type);
132
133 /* Check that the results are almost the same. An error of one color value
134 * is admissible because the intermediate image's color values are stored
135 * in 8-bit color values. In practice, with the checkerboard pattern all
136 * pixels are (129,129,129) with the two-step scaler instead of (128,128,128)
137 * while the rg pattern gives identical results.
138 */
139 if (!pixdata_almost_equal(one, two))
140 g_test_fail();
141
142 g_object_unref (G_OBJECT (one));
143 g_object_unref (G_OBJECT (two));
144 g_object_unref (G_OBJECT (source));
145}
146
147/* Crop a region out of a scaled image using gdk_pixbuf_new_subpixbuf() on a
148 * scaled image and by cropping it as part of the scaling, and check that the
149 * results are identical. */
150static void
151crop_n_compare(const GdkPixbuf *source, /* The source image */
152 double scale_factor, /* is scaled by this amount */
153 gint offset_x, /* and from this offset in the scaled image */
154 gint offset_y,
155 gint width, /* a region of this size is cropped out */
156 gint height,
157 GdkInterpType interp_type)
158{
159 GdkPixbuf *whole_scaled; /* The whole image scaled but not cropped */
160 GdkPixbuf *cropped; /* The scaled-then-cropped result */
161 GdkPixbuf *scaled; /* The cropped-while-scaled result */
162 guint new_width, new_height; /* Size of whole scaled image */
163
164 /* First, scale the whole image and crop it */
165 new_width = (int) (gdk_pixbuf_get_width (pixbuf: source) * scale_factor + 0.5);
166 new_height = (int) (gdk_pixbuf_get_height (pixbuf: source) * scale_factor + 0.5);
167 g_assert_cmpint (new_width, ==, 10);
168 g_assert_cmpint (new_height, ==, 10);
169
170 whole_scaled = gdk_pixbuf_scale_simple (src: source, dest_width: new_width, dest_height: new_height, interp_type);
171 g_assert_nonnull (whole_scaled);
172 cropped = gdk_pixbuf_new_subpixbuf (src_pixbuf: whole_scaled, src_x: offset_x, src_y: offset_y, width, height);
173 g_assert_nonnull (cropped);
174
175 /* Now crop and scale it in one go */
176 scaled = gdk_pixbuf_new (colorspace: GDK_COLORSPACE_RGB, has_alpha: 0, bits_per_sample: 8, width, height);
177 g_assert_nonnull (scaled);
178 gdk_pixbuf_scale (src: source, dest: scaled,
179 dest_x: 0, dest_y: 0, /* dest_[xy] */
180 dest_width: width, dest_height: height, /* dest_width/height */
181 offset_x: -1.0 * offset_x, offset_y: -1.0 * offset_y,
182 scale_x: scale_factor, scale_y: scale_factor,
183 interp_type);
184
185 /* and check that the results are the same.
186 * We can't use pixdata_equal() because it fails if rowstride is different
187 * and gdk_pixbuf_new_subpixbuf() reuses whole_scaled's image data with its
188 * larger rowstride. */
189 {
190 guchar *scaled_row; /* Pointer to start of row of pixels in scaled */
191 guchar *scaled_pixel; /* Pointer to current pixel data in scaled */
192 guchar *cropped_row; /* Pointer to start of row of pixels in cropped */
193 guchar *cropped_pixel; /* Pointer to current pixel data in cropped */
194 guint x, y;
195 gint scaled_width, scaled_height;
196
197 scaled_width = gdk_pixbuf_get_width (pixbuf: scaled);
198 scaled_height = gdk_pixbuf_get_height (pixbuf: scaled);
199
200 g_assert (scaled_width > 0);
201 g_assert (scaled_height > 0);
202
203 if (scaled_width != gdk_pixbuf_get_width (pixbuf: cropped) ||
204 scaled_height != gdk_pixbuf_get_height (pixbuf: cropped))
205 {
206 g_test_fail();
207 }
208
209 for (y = 0, scaled_row = gdk_pixbuf_get_pixels (pixbuf: scaled),
210 cropped_row = gdk_pixbuf_get_pixels (pixbuf: cropped);
211 y < scaled_height;
212 y++, scaled_row += gdk_pixbuf_get_rowstride (pixbuf: scaled),
213 cropped_row += gdk_pixbuf_get_rowstride (pixbuf: cropped))
214 {
215 for (x = 0, scaled_pixel = scaled_row, cropped_pixel = cropped_row;
216 x < scaled_width;
217 x++, scaled_pixel += gdk_pixbuf_get_n_channels (pixbuf: scaled),
218 cropped_pixel += gdk_pixbuf_get_n_channels (pixbuf: cropped))
219 {
220 if (!(scaled_pixel[0] == cropped_pixel[0] &&
221 scaled_pixel[1] == cropped_pixel[1] &&
222 scaled_pixel[2] == cropped_pixel[2]))
223 {
224 g_test_fail();
225 }
226 }
227 }
228 }
229
230 g_object_unref (G_OBJECT (whole_scaled));
231 g_object_unref (G_OBJECT (cropped));
232 g_object_unref (G_OBJECT (scaled));
233}
234
235/* Check that offsets work.
236 * We should be able to crop a region of an image using the scaler using
237 * negative offsets. */
238static void
239test_offset (gconstpointer data)
240{
241 GdkInterpType interp_type = *(GdkInterpType *) data;
242 const GdkPixbuf *source; /* Source image */
243 gint swidth = SOURCE_WIDTH; /* Size of source image */
244 gint sheight = SOURCE_HEIGHT;
245 gint dwidth = (swidth * SCALE_FACTOR + 0.5); /* Size of scaled image */
246 gint dheight = (sheight * SCALE_FACTOR + 0.5);
247
248 source = make_rg (width: swidth, height: sheight);
249
250 /* Check that the scaler correctly crops out an image half the size of the
251 * original from each corner and from the middle */
252 crop_n_compare (source, SCALE_FACTOR, offset_x: 0, offset_y: 0, width: dwidth / 2, height: dheight / 2, interp_type);
253 crop_n_compare (source, SCALE_FACTOR, offset_x: 0, offset_y: dheight / 2, width: dwidth / 2, height: dheight / 2, interp_type);
254 crop_n_compare (source, SCALE_FACTOR, offset_x: dwidth / 2, offset_y: 0, width: dwidth / 2, height: dheight / 2, interp_type);
255 crop_n_compare (source, SCALE_FACTOR, offset_x: dwidth / 2, offset_y: dheight / 2, width: dwidth / 2, height: dheight / 2, interp_type);
256 crop_n_compare (source, SCALE_FACTOR, offset_x: dwidth / 4, offset_y: dheight / 4, width: dwidth / 2, height: dheight / 2, interp_type);
257
258 g_object_unref (G_OBJECT (source));
259}
260
261/* Test the dest_x and dest_y fields by making a copy of an image by
262 * scaling 1:1 and using dest to copy the four quadrants separately.
263 *
264 * When scaled, images are:
265 * 1) scaled by the scale factors with respect to the top-left corner
266 * 2) translated by the offsets (negative to shift the image left/up in its
267 * frame)
268 * 3) a region of size dest_width x dest-height starting at (dest_x,dest_y)
269 * in the scaled-and-offset image is copied to (dest_x,dest_y) in the
270 * destination image. See the illustration at
271 * https://developer.gnome.org/gdk-pixbuf/2.22/gdk-pixbuf-scaling.html#gdk-pixbuf-composite */
272static void
273test_dest (gconstpointer data)
274{
275 GdkInterpType interp_type = *(GdkInterpType *) data;
276 const GdkPixbuf *source; /* Source image */
277 GdkPixbuf *scaled; /* Scaled whole image */
278 GdkPixbuf *copy; /* Destination image */
279 gint swidth = SOURCE_WIDTH; /* Size of source image */
280 gint sheight = SOURCE_HEIGHT;
281 gint dwidth = swidth * SCALE_FACTOR; /* Size of scaled image */
282 gint dheight = sheight * SCALE_FACTOR;
283
284 source = make_checkerboard (width: swidth, height: sheight);
285
286 copy = gdk_pixbuf_new (colorspace: GDK_COLORSPACE_RGB, has_alpha: 0, bits_per_sample: 8, width: dwidth, height: dheight);
287 g_assert_nonnull (copy);
288
289 /* Copy the four quadrants separately */
290 gdk_pixbuf_scale (src: (const GdkPixbuf *) source, dest: copy,
291 dest_x: 0, dest_y: 0, /* dest_[xy] */
292 dest_width: dwidth / 2, dest_height: dheight / 2, /* dest_width/height */
293 offset_x: 0.0, offset_y: 0.0, /* offset_[xy] */
294 SCALE_FACTOR, SCALE_FACTOR,
295 interp_type);
296 gdk_pixbuf_scale (src: (const GdkPixbuf *) source, dest: copy,
297 dest_x: dwidth / 2, dest_y: 0, /* dest_[xy] */
298 dest_width: dwidth / 2, dest_height: dheight / 2, /* dest_width/height */
299 offset_x: 0.0, offset_y: 0.0, /* offset_[xy] */
300 SCALE_FACTOR, SCALE_FACTOR,
301 interp_type);
302 gdk_pixbuf_scale (src: (const GdkPixbuf *)source, dest: copy,
303 dest_x: 0, dest_y: dheight / 2, /* dest_[xy] */
304 dest_width: dwidth / 2, dest_height: dheight / 2, /* dest_width/height */
305 offset_x: 0.0, offset_y: 0.0, /* offset_[xy] */
306 SCALE_FACTOR, SCALE_FACTOR,
307 interp_type);
308 gdk_pixbuf_scale (src: (const GdkPixbuf *)source, dest: copy,
309 dest_x: dwidth / 2, dest_y: dheight / 2, /* dest_[xy] */
310 dest_width: dwidth / 2, dest_height: dheight / 2, /* dest_width/height */
311 offset_x: 0.0, offset_y: 0.0, /* offset_[xy] */
312 SCALE_FACTOR, SCALE_FACTOR,
313 interp_type);
314
315 scaled = gdk_pixbuf_scale_simple (src: source, dest_width: dwidth, dest_height: dheight, interp_type);
316 g_assert_nonnull (scaled);
317
318 /* Compare the all-at-once and the composite images */
319 {
320 GError *error = NULL;
321 if (!pixdata_equal(test: scaled, ref: copy, error: &error))
322 g_test_fail();
323 }
324
325 g_object_unref (G_OBJECT (source));
326 g_object_unref (G_OBJECT (copy));
327 g_object_unref (G_OBJECT (scaled));
328}
329
330int
331main (int argc, char **argv)
332{
333 GdkInterpType tiles = GDK_INTERP_TILES;
334 GdkInterpType bilinear = GDK_INTERP_BILINEAR;
335 GdkInterpType hyper = GDK_INTERP_HYPER;
336 g_test_init (argc: &argc, argv: &argv, NULL);
337
338 g_test_add_data_func (testpath: "/pixbuf/scale/two-step/tiles", test_data: &tiles, test_func: test_old_and_new);
339 g_test_add_data_func (testpath: "/pixbuf/scale/two-step/bilinear", test_data: &bilinear, test_func: test_old_and_new);
340 g_test_add_data_func (testpath: "/pixbuf/scale/two-step/hyper", test_data: &hyper, test_func: test_old_and_new);
341
342 g_test_add_data_func (testpath: "/pixbuf/scale/two-step/offset/tiles", test_data: &tiles, test_func: test_offset);
343 g_test_add_data_func (testpath: "/pixbuf/scale/two-step/offset/bilinear", test_data: &bilinear, test_func: test_offset);
344 g_test_add_data_func (testpath: "/pixbuf/scale/two-step/offset/hyper", test_data: &hyper, test_func: test_offset);
345
346 g_test_add_data_func (testpath: "/pixbuf/scale/two-step/dest/tiles", test_data: &tiles, test_func: test_dest);
347 g_test_add_data_func (testpath: "/pixbuf/scale/two-step/dest/bilinear", test_data: &bilinear, test_func: test_dest);
348 g_test_add_data_func (testpath: "/pixbuf/scale/two-step/dest/hyper", test_data: &hyper, test_func: test_dest);
349
350 return g_test_run ();
351}
352

source code of gtk/subprojects/gdk-pixbuf/tests/pixbuf-scale-two-step.c