xref: /PHP-8.2/ext/gd/libgd/gd_crop.c (revision 28e7addb)
1 /**
2  * Title: Crop
3  *
4  * A couple of functions to crop images, automatically (auto detection of
5  * the borders color), using a given color (with or without tolerance)
6  * or using a selection.
7  *
8  * The threshold method works relatively well but it can be improved.
9  * Maybe L*a*b* and Delta-E will give better results (and a better
10  * granularity).
11  *
12  * Example:
13  * (start code)
14  *   im2 = gdImageAutoCrop(im, GD_CROP_SIDES);
15  *   if (im2) {
16 
17  *   }
18  *   gdImageDestroy(im2);
19  *  (end code)
20  **/
21 
22 #include <stdlib.h>
23 #include <string.h>
24 #include <math.h>
25 
26 #include "gd.h"
27 
28 static int gdGuessBackgroundColorFromCorners(gdImagePtr im, int *color);
29 static int gdColorMatch(gdImagePtr im, int col1, int col2, float threshold);
30 
31 /**
32  * Function: gdImageCrop
33  *  Crops the src image using the area defined by the <crop> rectangle.
34  *  The result is returned as a new image.
35  *
36  *
37  * Parameters:
38  * 	src - Source image
39  *  crop - Rectangular region to crop
40  *
41  * Returns:
42  *  <gdImagePtr> on success or NULL
43  */
gdImageCrop(gdImagePtr src,const gdRectPtr crop)44 gdImagePtr gdImageCrop(gdImagePtr src, const gdRectPtr crop)
45 {
46 	gdImagePtr dst;
47 	int alphaBlendingFlag;
48 
49 	if (gdImageTrueColor(src)) {
50 		dst = gdImageCreateTrueColor(crop->width, crop->height);
51 	} else {
52 		dst = gdImageCreate(crop->width, crop->height);
53 	}
54 	if (!dst) return NULL;
55 	alphaBlendingFlag = dst->alphaBlendingFlag;
56 	gdImageAlphaBlending(dst, gdEffectReplace);
57 	gdImageCopy(dst, src, 0, 0, crop->x, crop->y, crop->width, crop->height);
58 	gdImageAlphaBlending(dst, alphaBlendingFlag);
59 
60 	return dst;
61 }
62 
63 /**
64  * Function: gdImageAutoCrop
65  *  Automatic croping of the src image using the given mode
66  *  (see <gdCropMode>)
67  *
68  *
69  * Parameters:
70  * 	im - Source image
71  *  mode - crop mode
72  *
73  * Returns:
74  *  <gdImagePtr> on success or NULL
75  *
76  * See also:
77  *  <gdCropMode>
78  */
gdImageCropAuto(gdImagePtr im,const unsigned int mode)79 gdImagePtr gdImageCropAuto(gdImagePtr im, const unsigned int mode)
80 {
81 	const int width = gdImageSX(im);
82 	const int height = gdImageSY(im);
83 
84 	int x,y;
85 	int color, match;
86 	gdRect crop;
87 
88 	crop.x = 0;
89 	crop.y = 0;
90 	crop.width = 0;
91 	crop.height = 0;
92 
93 	switch (mode) {
94 		case GD_CROP_TRANSPARENT:
95 			color = gdImageGetTransparent(im);
96 			break;
97 
98 		case GD_CROP_BLACK:
99 			color = gdImageColorClosestAlpha(im, 0, 0, 0, 0);
100 			break;
101 
102 		case GD_CROP_WHITE:
103 			color = gdImageColorClosestAlpha(im, 255, 255, 255, 0);
104 			break;
105 
106 		case GD_CROP_SIDES:
107 			gdGuessBackgroundColorFromCorners(im, &color);
108 			break;
109 
110 		case GD_CROP_DEFAULT:
111 		default:
112 			color = gdImageGetTransparent(im);
113 			break;
114 	}
115 
116 	/* TODO: Add gdImageGetRowPtr and works with ptr at the row level
117 	 * for the true color and palette images
118 	 * new formats will simply work with ptr
119 	 */
120 	match = 1;
121 	for (y = 0; match && y < height; y++) {
122 		for (x = 0; match && x < width; x++) {
123 			int c2 = gdImageGetPixel(im, x, y);
124 			match = (color == c2);
125 		}
126 	}
127 
128 	/* Whole image would be cropped > bye */
129 	if (match) {
130 		return NULL;
131 	}
132 
133 	crop.y = y - 1;
134 
135 	match = 1;
136 	for (y = height - 1; match && y >= 0; y--) {
137 		for (x = 0; match && x < width; x++) {
138 			match = (color == gdImageGetPixel(im, x,y));
139 		}
140 	}
141 	crop.height = y - crop.y + 2;
142 
143 	match = 1;
144 	for (x = 0; match && x < width; x++) {
145 		for (y = 0; match && y < crop.y + crop.height; y++) {
146 			match = (color == gdImageGetPixel(im, x,y));
147 		}
148 	}
149 	crop.x = x - 1;
150 
151 	match = 1;
152 	for (x = width - 1; match && x >= 0; x--) {
153 		for (y = 0; match &&  y < crop.y + crop.height; y++) {
154 			match = (color == gdImageGetPixel(im, x,y));
155 		}
156 	}
157 	crop.width = x - crop.x + 2;
158 
159 	return gdImageCrop(im, &crop);
160 }
161 /*TODOs: Implement DeltaE instead, way better perceptual differences */
162 /**
163  * Function: gdImageThresholdCrop
164  *  Crop an image using a given color. The threshold argument defines
165  *  the tolerance to be used while comparing the image color and the
166  *  color to crop. The method used to calculate the color difference
167  *  is based on the color distance in the RGB(a) cube.
168  *
169  *
170  * Parameters:
171  * 	im - Source image
172  *  color - color to crop
173  *  threshold - tolerance (0..100)
174  *
175  * Returns:
176  *  <gdImagePtr> on success or NULL
177  *
178  * See also:
179  *  <gdCropMode>, <gdImageAutoCrop> or <gdImageCrop>
180  */
gdImageCropThreshold(gdImagePtr im,const unsigned int color,const float threshold)181 gdImagePtr gdImageCropThreshold(gdImagePtr im, const unsigned int color, const float threshold)
182 {
183 	const int width = gdImageSX(im);
184 	const int height = gdImageSY(im);
185 
186 	int x,y;
187 	int match;
188 	gdRect crop;
189 
190 	crop.x = 0;
191 	crop.y = 0;
192 	crop.width = 0;
193 	crop.height = 0;
194 
195 	/* Pierre: crop everything sounds bad */
196 	if (threshold > 100.0) {
197 		return NULL;
198 	}
199 
200 	if (!gdImageTrueColor(im) && color >= gdImageColorsTotal(im)) {
201 		return NULL;
202 	}
203 
204 	/* TODO: Add gdImageGetRowPtr and works with ptr at the row level
205 	 * for the true color and palette images
206 	 * new formats will simply work with ptr
207 	 */
208 	match = 1;
209 	for (y = 0; match && y < height; y++) {
210 		for (x = 0; match && x < width; x++) {
211 			match = (gdColorMatch(im, color, gdImageGetPixel(im, x,y), threshold)) > 0;
212 		}
213 	}
214 
215 	/* Whole image would be cropped > bye */
216 	if (match) {
217 		return NULL;
218 	}
219 
220 	crop.y = y - 1;
221 
222 	match = 1;
223 	for (y = height - 1; match && y >= 0; y--) {
224 		for (x = 0; match && x < width; x++) {
225 			match = (gdColorMatch(im, color, gdImageGetPixel(im, x, y), threshold)) > 0;
226 		}
227 	}
228 	crop.height = y - crop.y + 2;
229 
230 	match = 1;
231 	for (x = 0; match && x < width; x++) {
232 		for (y = 0; match && y < crop.y + crop.height; y++) {
233 			match = (gdColorMatch(im, color, gdImageGetPixel(im, x,y), threshold)) > 0;
234 		}
235 	}
236 	crop.x = x - 1;
237 
238 	match = 1;
239 	for (x = width - 1; match && x >= 0; x--) {
240 		for (y = 0; match &&  y < crop.y + crop.height; y++) {
241 			match = (gdColorMatch(im, color, gdImageGetPixel(im, x,y), threshold)) > 0;
242 		}
243 	}
244 	crop.width = x - crop.x + 2;
245 
246 	return gdImageCrop(im, &crop);
247 }
248 
249 /* This algorithm comes from pnmcrop (http://netpbm.sourceforge.net/)
250  * Three steps:
251  *  - if 3 corners are equal.
252  *  - if two are equal.
253  *  - Last solution: average the colors
254  */
gdGuessBackgroundColorFromCorners(gdImagePtr im,int * color)255 static int gdGuessBackgroundColorFromCorners(gdImagePtr im, int *color)
256 {
257 	const int tl = gdImageGetPixel(im, 0, 0);
258 	const int tr = gdImageGetPixel(im, gdImageSX(im) - 1, 0);
259 	const int bl = gdImageGetPixel(im, 0, gdImageSY(im) -1);
260 	const int br = gdImageGetPixel(im, gdImageSX(im) - 1, gdImageSY(im) -1);
261 
262 	if (tr == bl && tr == br) {
263 		*color = tr;
264 		return 3;
265 	} else if (tl == bl && tl == br) {
266 		*color = tl;
267 		return 3;
268 	} else if (tl == tr &&  tl == br) {
269 		*color = tl;
270 		return 3;
271 	} else if (tl == tr &&  tl == bl) {
272 		*color = tl;
273 		return 3;
274 	} else if (tl == tr  || tl == bl || tl == br) {
275 		*color = tl;
276 		return 2;
277 	} else if (tr == bl || tr == br) {
278 		*color = tr;
279 		return 2;
280 	} else if (br == bl) {
281 		*color = bl;
282 		return 2;
283 	} else {
284 		register int r,b,g,a;
285 
286 		r = (int)(0.5f + (gdImageRed(im, tl) + gdImageRed(im, tr) + gdImageRed(im, bl) + gdImageRed(im, br)) / 4);
287 		g = (int)(0.5f + (gdImageGreen(im, tl) + gdImageGreen(im, tr) + gdImageGreen(im, bl) + gdImageGreen(im, br)) / 4);
288 		b = (int)(0.5f + (gdImageBlue(im, tl) + gdImageBlue(im, tr) + gdImageBlue(im, bl) + gdImageBlue(im, br)) / 4);
289 		a = (int)(0.5f + (gdImageAlpha(im, tl) + gdImageAlpha(im, tr) + gdImageAlpha(im, bl) + gdImageAlpha(im, br)) / 4);
290 		*color = gdImageColorClosestAlpha(im, r, g, b, a);
291 		return 0;
292 	}
293 }
294 
gdColorMatch(gdImagePtr im,int col1,int col2,float threshold)295 static int gdColorMatch(gdImagePtr im, int col1, int col2, float threshold)
296 {
297 	const int dr = gdImageRed(im, col1) - gdImageRed(im, col2);
298 	const int dg = gdImageGreen(im, col1) - gdImageGreen(im, col2);
299 	const int db = gdImageBlue(im, col1) - gdImageBlue(im, col2);
300 	const int da = gdImageAlpha(im, col1) - gdImageAlpha(im, col2);
301 	const int dist = dr * dr + dg * dg + db * db + da * da;
302 
303 	return (100.0 * dist / 195075) < threshold;
304 }
305 
306 /*
307  * To be implemented when we have more image formats.
308  * Buffer like gray8 gray16 or rgb8 will require some tweak
309  * and can be done in this function (called from the autocrop
310  * function. (Pierre)
311  */
312 #if 0
313 static int colors_equal (const int col1, const in col2)
314 {
315 
316 }
317 #endif
318