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
pixdata_almost_equal(GdkPixbuf * one,GdkPixbuf * two)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 (one);
68   height_one = gdk_pixbuf_get_height (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 (two) ||
76       height_one != gdk_pixbuf_get_height (two))
77     {
78       g_test_fail();
79     }
80 
81   for (y = 0, one_row = gdk_pixbuf_get_pixels (one),
82               two_row = gdk_pixbuf_get_pixels (two);
83        y < height_one;
84        y++, one_row += gdk_pixbuf_get_rowstride (one),
85             two_row += gdk_pixbuf_get_rowstride (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 (one),
90 	        two_pixel += gdk_pixbuf_get_n_channels (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
test_old_and_new(gconstpointer data)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 ("GDK_PIXBUF_DISABLE_TWO_STEP_SCALER", "1", TRUE);
123   one = gdk_pixbuf_scale_simple (source,
124 				 (int) (width * SCALE_FACTOR + 0.5),
125 				 (int) (height * SCALE_FACTOR + 0.5),
126 				 interp_type);
127   g_unsetenv("GDK_PIXBUF_DISABLE_TWO_STEP_SCALER");
128   two = gdk_pixbuf_scale_simple (source,
129 				 (int) (width * SCALE_FACTOR + 0.5),
130 				 (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
crop_n_compare(const GdkPixbuf * source,double scale_factor,gint offset_x,gint offset_y,gint width,gint height,GdkInterpType interp_type)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 (source) * scale_factor + 0.5);
166   new_height = (int) (gdk_pixbuf_get_height (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 (source, new_width, new_height, interp_type);
171   g_assert_nonnull (whole_scaled);
172   cropped = gdk_pixbuf_new_subpixbuf (whole_scaled, offset_x, offset_y, width, height);
173   g_assert_nonnull (cropped);
174 
175   /* Now crop and scale it in one go */
176   scaled = gdk_pixbuf_new (GDK_COLORSPACE_RGB, 0, 8, width, height);
177   g_assert_nonnull (scaled);
178   gdk_pixbuf_scale (source, scaled,
179 		    0, 0,                              /* dest_[xy] */
180 		    width, height,                     /* dest_width/height */
181 		    -1.0 * offset_x, -1.0 * offset_y,
182 		    scale_factor, 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 (scaled);
198     scaled_height = gdk_pixbuf_get_height (scaled);
199 
200     g_assert (scaled_width > 0);
201     g_assert (scaled_height > 0);
202 
203     if (scaled_width != gdk_pixbuf_get_width (cropped) ||
204 	scaled_height != gdk_pixbuf_get_height (cropped))
205       {
206         g_test_fail();
207       }
208 
209     for (y = 0, scaled_row = gdk_pixbuf_get_pixels (scaled),
210 	       cropped_row = gdk_pixbuf_get_pixels (cropped);
211 	 y < scaled_height;
212 	 y++, scaled_row += gdk_pixbuf_get_rowstride (scaled),
213 	      cropped_row += gdk_pixbuf_get_rowstride (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 (scaled),
218 	         cropped_pixel += gdk_pixbuf_get_n_channels (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
test_offset(gconstpointer data)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 (swidth, 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, 0,          0,           dwidth / 2, dheight / 2, interp_type);
253   crop_n_compare (source, SCALE_FACTOR, 0,          dheight / 2, dwidth / 2, dheight / 2, interp_type);
254   crop_n_compare (source, SCALE_FACTOR, dwidth / 2, 0,           dwidth / 2, dheight / 2, interp_type);
255   crop_n_compare (source, SCALE_FACTOR, dwidth / 2, dheight / 2, dwidth / 2, dheight / 2, interp_type);
256   crop_n_compare (source, SCALE_FACTOR, dwidth / 4, dheight / 4, dwidth / 2, 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
test_dest(gconstpointer data)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 (swidth, sheight);
285 
286   copy = gdk_pixbuf_new (GDK_COLORSPACE_RGB, 0, 8, dwidth, dheight);
287   g_assert_nonnull (copy);
288 
289   /* Copy the four quadrants separately */
290   gdk_pixbuf_scale ((const GdkPixbuf *) source, copy,
291 		    0, 0,                    /* dest_[xy] */
292 		    dwidth / 2, dheight / 2, /* dest_width/height */
293 		    0.0, 0.0,                /* offset_[xy] */
294 		    SCALE_FACTOR,  SCALE_FACTOR,
295 		    interp_type);
296   gdk_pixbuf_scale ((const GdkPixbuf *) source, copy,
297 		    dwidth / 2, 0,           /* dest_[xy] */
298 		    dwidth / 2, dheight / 2, /* dest_width/height */
299 		    0.0, 0.0,                /* offset_[xy] */
300 		    SCALE_FACTOR,  SCALE_FACTOR,
301 		    interp_type);
302   gdk_pixbuf_scale ((const GdkPixbuf *)source, copy,
303 		    0, dheight / 2,          /* dest_[xy] */
304 		    dwidth / 2, dheight / 2, /* dest_width/height */
305 		    0.0, 0.0,                /* offset_[xy] */
306 		    SCALE_FACTOR,  SCALE_FACTOR,
307 		    interp_type);
308   gdk_pixbuf_scale ((const GdkPixbuf *)source, copy,
309 		    dwidth / 2, dheight / 2, /* dest_[xy] */
310 		    dwidth / 2, dheight / 2, /* dest_width/height */
311 		    0.0, 0.0,                /* offset_[xy] */
312 		    SCALE_FACTOR,  SCALE_FACTOR,
313 		    interp_type);
314 
315   scaled = gdk_pixbuf_scale_simple (source, dwidth, 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(scaled, copy, &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
main(int argc,char ** argv)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, &argv, NULL);
337 
338   g_test_add_data_func ("/pixbuf/scale/two-step/tiles", &tiles, test_old_and_new);
339   g_test_add_data_func ("/pixbuf/scale/two-step/bilinear", &bilinear, test_old_and_new);
340   g_test_add_data_func ("/pixbuf/scale/two-step/hyper", &hyper, test_old_and_new);
341 
342   g_test_add_data_func ("/pixbuf/scale/two-step/offset/tiles", &tiles, test_offset);
343   g_test_add_data_func ("/pixbuf/scale/two-step/offset/bilinear", &bilinear, test_offset);
344   g_test_add_data_func ("/pixbuf/scale/two-step/offset/hyper", &hyper, test_offset);
345 
346   g_test_add_data_func ("/pixbuf/scale/two-step/dest/tiles", &tiles, test_dest);
347   g_test_add_data_func ("/pixbuf/scale/two-step/dest/bilinear", &bilinear, test_dest);
348   g_test_add_data_func ("/pixbuf/scale/two-step/dest/hyper", &hyper, test_dest);
349 
350   return g_test_run ();
351 }
352