1 | // Aseprite |
2 | // Copyright (C) 2019 Igara Studio S.A. |
3 | // Copyright (C) 2001-2015 David Capello |
4 | // |
5 | // This program is distributed under the terms of |
6 | // the End-User License Agreement for Aseprite. |
7 | |
8 | #ifdef HAVE_CONFIG_H |
9 | #include "config.h" |
10 | #endif |
11 | |
12 | #include "app/util/autocrop.h" |
13 | |
14 | #include "app/snap_to_grid.h" |
15 | #include "doc/algorithm/shrink_bounds.h" |
16 | #include "doc/image.h" |
17 | #include "doc/mask.h" |
18 | #include "doc/sprite.h" |
19 | #include "render/render.h" |
20 | |
21 | #include <algorithm> |
22 | #include <memory> |
23 | |
24 | namespace app { |
25 | |
26 | using namespace doc; |
27 | |
28 | namespace { |
29 | |
30 | // We call a "solid border" when a specific color is repeated in every |
31 | // pixel of the image edge. This will return isBorder1Solid=true if |
32 | // the top (when topBottomLookUp=true) or left edge (when |
33 | // topBottomLookUp=false) is a "solid border". In the case of |
34 | // isBorder2Solid is for the bottom (or right) edge. |
35 | bool analize_if_image_has_solid_borders(const Image* image, |
36 | color_t& refColor, |
37 | bool& isBorder1Solid, |
38 | bool& isBorder2Solid, |
39 | bool topBottomLookUp) |
40 | { |
41 | isBorder1Solid = true; |
42 | isBorder2Solid = true; |
43 | |
44 | int w = image->width(); |
45 | int h = image->height(); |
46 | const color_t probableRefColor1 = get_pixel(image, 0, 0); |
47 | const color_t probableRefColor2 = get_pixel(image, w-1, h-1); |
48 | color_t currentPixel; |
49 | color_t transparentColor = image->maskColor(); |
50 | |
51 | if (probableRefColor1 == transparentColor || |
52 | probableRefColor2 == transparentColor) { |
53 | refColor = transparentColor; |
54 | return false; |
55 | } |
56 | |
57 | if (!topBottomLookUp) |
58 | std::swap(w, h); |
59 | |
60 | for (int i=0; i<w; ++i) { |
61 | if (topBottomLookUp) |
62 | currentPixel = get_pixel(image, i, 0); |
63 | else |
64 | currentPixel = get_pixel(image, 0, i); |
65 | if (currentPixel != probableRefColor1) { |
66 | isBorder1Solid = false; |
67 | if (currentPixel == transparentColor) { |
68 | refColor = transparentColor; |
69 | return false; |
70 | } |
71 | } |
72 | } |
73 | |
74 | for (int i=0; i<w; ++i) { |
75 | if (topBottomLookUp) |
76 | currentPixel = get_pixel(image, i, h-1); |
77 | else |
78 | currentPixel = get_pixel(image, h-1, i); |
79 | if (currentPixel != probableRefColor2) { |
80 | isBorder2Solid = false; |
81 | if (currentPixel == transparentColor) { |
82 | refColor = transparentColor; |
83 | return false; |
84 | } |
85 | } |
86 | } |
87 | return true; |
88 | } |
89 | |
90 | } // anonymous namespace |
91 | |
92 | bool get_shrink_rect(int *x1, int *y1, int *x2, int *y2, |
93 | Image *image, color_t refpixel) |
94 | { |
95 | #define SHRINK_SIDE(u_begin, u_op, u_final, u_add, \ |
96 | v_begin, v_op, v_final, v_add, U, V, var) \ |
97 | do { \ |
98 | for (u = u_begin; u u_op u_final; u u_add) { \ |
99 | for (v = v_begin; v v_op v_final; v v_add) { \ |
100 | if (image->getPixel(U, V) != refpixel) \ |
101 | break; \ |
102 | } \ |
103 | if (v == v_final) \ |
104 | var; \ |
105 | else \ |
106 | break; \ |
107 | } \ |
108 | } while (0) |
109 | |
110 | int u, v; |
111 | |
112 | *x1 = 0; |
113 | *y1 = 0; |
114 | *x2 = image->width()-1; |
115 | *y2 = image->height()-1; |
116 | |
117 | SHRINK_SIDE(0, <, image->width(), ++, |
118 | 0, <, image->height(), ++, u, v, (*x1)++); |
119 | |
120 | SHRINK_SIDE(0, <, image->height(), ++, |
121 | 0, <, image->width(), ++, v, u, (*y1)++); |
122 | |
123 | SHRINK_SIDE(image->width()-1, >, 0, --, |
124 | 0, <, image->height(), ++, u, v, (*x2)--); |
125 | |
126 | SHRINK_SIDE(image->height()-1, >, 0, --, |
127 | 0, <, image->width(), ++, v, u, (*y2)--); |
128 | |
129 | if ((*x1 > *x2) || (*y1 > *y2)) |
130 | return false; |
131 | else |
132 | return true; |
133 | |
134 | #undef SHRINK_SIDE |
135 | } |
136 | |
137 | bool get_shrink_rect2(int *x1, int *y1, int *x2, int *y2, |
138 | Image *image, Image *refimage) |
139 | { |
140 | #define SHRINK_SIDE(u_begin, u_op, u_final, u_add, \ |
141 | v_begin, v_op, v_final, v_add, U, V, var) \ |
142 | do { \ |
143 | for (u = u_begin; u u_op u_final; u u_add) { \ |
144 | for (v = v_begin; v v_op v_final; v v_add) { \ |
145 | if (image->getPixel(U, V) != refimage->getPixel(U, V)) \ |
146 | break; \ |
147 | } \ |
148 | if (v == v_final) \ |
149 | var; \ |
150 | else \ |
151 | break; \ |
152 | } \ |
153 | } while (0) |
154 | |
155 | int u, v; |
156 | |
157 | *x1 = 0; |
158 | *y1 = 0; |
159 | *x2 = image->width()-1; |
160 | *y2 = image->height()-1; |
161 | |
162 | SHRINK_SIDE(0, <, image->width(), ++, |
163 | 0, <, image->height(), ++, u, v, (*x1)++); |
164 | |
165 | SHRINK_SIDE(0, <, image->height(), ++, |
166 | 0, <, image->width(), ++, v, u, (*y1)++); |
167 | |
168 | SHRINK_SIDE(image->width()-1, >, 0, --, |
169 | 0, <, image->height(), ++, u, v, (*x2)--); |
170 | |
171 | SHRINK_SIDE(image->height()-1, >, 0, --, |
172 | 0, <, image->width(), ++, v, u, (*y2)--); |
173 | |
174 | if ((*x1 > *x2) || (*y1 > *y2)) |
175 | return false; |
176 | else |
177 | return true; |
178 | |
179 | #undef SHRINK_SIDE |
180 | } |
181 | |
182 | // A simple method to trim an image we have used in the past is |
183 | // selecting the top-left corner pixel as the "refColor" in |
184 | // shrink_bounds() algorithm (whatever that color is, transparent |
185 | // or non-transparent). |
186 | // |
187 | // Now we have changed it to other heuristic: we look if there are |
188 | // borders in the sprite with a solid color ("solid border"), |
189 | // i.e. a color repeating in every pixel of that specific side |
190 | // (left, top, right, or bottom). |
191 | // |
192 | // If we find a transparent pixel at the edges of the sprite, we |
193 | // automatically set "refColor" as the transparent color and it |
194 | // goes directly to shrink_bounds() function. Because this mean |
195 | // that we are in a transparent layer and the transparent color is |
196 | // the one that must be trimmed. |
197 | // |
198 | // The other case is when borders don't contain the transparent |
199 | // color, we search for a "solid border" (top border first), then |
200 | // it checks the opposite border (bottom border), then: |
201 | // |
202 | // 1) If the opposite border is equal to the first border, |
203 | // the color of both borders will be the "refColor". |
204 | // 2) If the color of the opposite border is solid, BUT different to |
205 | // the first border we will need the user intervention to select a |
206 | // valid refColor (in this case the function returns false, which |
207 | // means that we cannot automatically trim the image). |
208 | // 3) If opposite border contains differents colors, we choose the |
209 | // first border color as "refColor". |
210 | // 4) It repeats the analysis with the left and right edges. |
211 | // |
212 | // If no border has solid color, trimSprite() does nothing. |
213 | bool get_best_refcolor_for_trimming( |
214 | doc::Image* image, |
215 | color_t& refColor) |
216 | { |
217 | const color_t probableRefColor1 = get_pixel(image, 0, 0); |
218 | const color_t probableRefColor2 = get_pixel(image, image->width()-1, image->height()-1); |
219 | bool isBorder1Solid = true; |
220 | bool isBorder2Solid = true; |
221 | |
222 | refColor = probableRefColor1; |
223 | |
224 | if (analize_if_image_has_solid_borders( |
225 | image, refColor, |
226 | isBorder1Solid, |
227 | isBorder2Solid, |
228 | true)) { // Analize top vs. bottom borders |
229 | // Here, we know that analize_if_image_has_solid_borders() did not |
230 | // find transparent pixels on top and bottom borders. |
231 | if (!isBorder1Solid && |
232 | isBorder2Solid) { |
233 | refColor = probableRefColor2; |
234 | } |
235 | else if (isBorder1Solid && |
236 | isBorder2Solid && |
237 | probableRefColor1 != probableRefColor2) { |
238 | // Both border are solid but with different colors, so the |
239 | // decision should be asked to the user. |
240 | return false; |
241 | } |
242 | |
243 | if (analize_if_image_has_solid_borders( |
244 | image, refColor, |
245 | isBorder1Solid, |
246 | isBorder2Solid, |
247 | false)) { // Analize left vs. right borders |
248 | if (!isBorder1Solid && |
249 | isBorder2Solid) |
250 | refColor = probableRefColor2; |
251 | else if (isBorder1Solid && |
252 | isBorder2Solid && |
253 | probableRefColor1 != probableRefColor2) |
254 | // Both border are solid but with different colors, so the |
255 | // decision should be asked to the user. |
256 | return false; |
257 | } |
258 | } |
259 | return true; |
260 | } |
261 | |
262 | gfx::Rect get_trimmed_bounds( |
263 | const doc::Sprite* sprite, |
264 | const bool byGrid) |
265 | { |
266 | gfx::Rect bounds; |
267 | |
268 | std::unique_ptr<Image> image_wrap(Image::create(sprite->spec())); |
269 | Image* image = image_wrap.get(); |
270 | |
271 | render::Render render; |
272 | |
273 | for (frame_t frame(0); frame<sprite->totalFrames(); ++frame) { |
274 | render.renderSprite(image, sprite, frame); |
275 | |
276 | gfx::Rect frameBounds; |
277 | doc::color_t refColor; |
278 | if (get_best_refcolor_for_trimming(image, refColor) && |
279 | doc::algorithm::shrink_bounds(image, refColor, nullptr, frameBounds)) { |
280 | bounds = bounds.createUnion(frameBounds); |
281 | } |
282 | |
283 | // TODO merge this code with the code in DocExporter::captureSamples() |
284 | if (byGrid) { |
285 | const gfx::Rect& gridBounds = sprite->gridBounds(); |
286 | gfx::Point posTopLeft = |
287 | snap_to_grid(gridBounds, |
288 | bounds.origin(), |
289 | PreferSnapTo::FloorGrid); |
290 | gfx::Point posBottomRight = |
291 | snap_to_grid(gridBounds, |
292 | bounds.point2(), |
293 | PreferSnapTo::CeilGrid); |
294 | bounds = gfx::Rect(posTopLeft, posBottomRight); |
295 | } |
296 | } |
297 | return bounds; |
298 | } |
299 | |
300 | } // namespace app |
301 | |