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
24namespace app {
25
26using namespace doc;
27
28namespace {
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.
35bool 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
92bool 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
137bool 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.
213bool 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
262gfx::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