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 | |
57 | static gboolean |
58 | pixdata_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. */ |
108 | static void |
109 | test_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. */ |
150 | static void |
151 | crop_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. */ |
238 | static void |
239 | test_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 */ |
272 | static void |
273 | test_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 | |
330 | int |
331 | main (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 | |