1// Aseprite
2// Copyright (C) 2019-2022 Igara Studio S.A.
3// Copyright (C) 2001-2018 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/cmd/replace_tileset.h"
13#include "app/cmd/set_cel_bounds.h"
14#include "app/cmd/set_slice_key.h"
15#include "app/commands/command.h"
16#include "app/commands/new_params.h"
17#include "app/commands/params.h"
18#include "app/doc_api.h"
19#include "app/ini_file.h"
20#include "app/i18n/strings.h"
21#include "app/modules/gui.h"
22#include "app/modules/palettes.h"
23#include "app/sprite_job.h"
24#include "app/util/resize_image.h"
25#include "base/convert_to.h"
26#include "doc/algorithm/resize_image.h"
27#include "doc/cel.h"
28#include "doc/cels_range.h"
29#include "doc/image.h"
30#include "doc/layer.h"
31#include "doc/layer_tilemap.h"
32#include "doc/mask.h"
33#include "doc/primitives.h"
34#include "doc/slice.h"
35#include "doc/sprite.h"
36#include "doc/tilesets.h"
37#include "ui/ui.h"
38
39#include "sprite_size.xml.h"
40
41#include <algorithm>
42
43#define PERC_FORMAT "%.4g"
44
45namespace app {
46
47using namespace ui;
48using doc::algorithm::ResizeMethod;
49
50struct SpriteSizeParams : public NewParams {
51 Param<bool> ui { this, true, { "ui", "use-ui" } };
52 Param<int> width { this, 0, "width" };
53 Param<int> height { this, 0, "height" };
54 Param<bool> lockRatio { this, false, "lockRatio" };
55 Param<double> scale { this, 1.0, "scale" };
56 Param<double> scaleX { this, 1.0, "scaleX" };
57 Param<double> scaleY { this, 1.0, "scaleY" };
58 Param<ResizeMethod> method { this, ResizeMethod::RESIZE_METHOD_NEAREST_NEIGHBOR, { "method", "resize-method" } };
59};
60
61class SpriteSizeJob : public SpriteJob {
62 int m_new_width;
63 int m_new_height;
64 ResizeMethod m_resize_method;
65
66 template<typename T>
67 T scale_x(T x) const { return x * T(m_new_width) / T(sprite()->width()); }
68
69 template<typename T>
70 T scale_y(T y) const { return y * T(m_new_height) / T(sprite()->height()); }
71
72 template<typename T>
73 gfx::RectT<T> scale_rect(const gfx::RectT<T>& rc) const {
74 T x1 = scale_x(rc.x);
75 T y1 = scale_y(rc.y);
76 return gfx::RectT<T>(x1, y1,
77 scale_x(rc.x2()) - x1,
78 scale_y(rc.y2()) - y1);
79 }
80
81public:
82
83 SpriteSizeJob(const ContextReader& reader, int new_width, int new_height, ResizeMethod resize_method)
84 : SpriteJob(reader, Strings::sprite_size_title().c_str()) {
85 m_new_width = new_width;
86 m_new_height = new_height;
87 m_resize_method = resize_method;
88 }
89
90protected:
91
92 // [working thread]
93 void onJob() override {
94 DocApi api = writer().document()->getApi(tx());
95 Tilesets* tilesets = sprite()->tilesets();
96
97 int img_count = 0;
98 if (tilesets) {
99 for (Tileset* tileset : *tilesets) {
100 img_count += tileset->size();
101 }
102 }
103 for (Cel* cel : sprite()->uniqueCels()) { // TODO add size() member function to CelsRange
104 (void)cel;
105 ++img_count;
106 }
107
108 int progress = 0;
109 const gfx::SizeF scale(
110 double(m_new_width) / double(sprite()->width()),
111 double(m_new_height) / double(sprite()->height()));
112
113 // Resize tilesets
114 if (tilesets) {
115 for (tileset_index tsi=0; tsi<tilesets->size(); ++tsi) {
116 Tileset* tileset = tilesets->get(tsi);
117 ASSERT(tileset);
118
119 gfx::Size newGridSize = tileset->grid().tileSize();
120 newGridSize.w *= scale.w;
121 newGridSize.h *= scale.h;
122 if (newGridSize.w < 1) newGridSize.w = 1;
123 if (newGridSize.h < 1) newGridSize.h = 1;
124 doc::Grid newGrid(newGridSize);
125
126 auto newTileset = new doc::Tileset(sprite(), newGrid, tileset->size());
127 doc::tile_index idx = 0;
128 for (doc::ImageRef tileImg : *tileset) {
129 if (idx != 0) {
130 doc::ImageRef newTileImg(
131 resize_image(
132 tileImg.get(),
133 scale,
134 m_resize_method,
135 sprite()->palette(0),
136 sprite()->rgbMap(0))); // TODO first frame?
137
138 newTileset->set(idx, newTileImg);
139 }
140
141 jobProgress((float)progress / img_count);
142 ++progress;
143 ++idx;
144 }
145 tx()(new cmd::ReplaceTileset(sprite(), tsi, newTileset));
146
147 // Cancel all the operation?
148 if (isCanceled())
149 return; // Tx destructor will undo all operations
150 }
151 }
152
153 // For each cel...
154 for (Cel* cel : sprite()->uniqueCels()) {
155 // We need to adjust only the origin/position of tilemap cels
156 // (because tiles are resized automatically when we resize the
157 // tileset).
158 if (cel->layer()->isTilemap()) {
159 Tileset* tileset = static_cast<LayerTilemap*>(cel->layer())->tileset();
160 gfx::Size canvasSize =
161 tileset->grid().tilemapSizeToCanvas(
162 gfx::Size(cel->image()->width(),
163 cel->image()->height()));
164 gfx::Rect newBounds(cel->x()*scale.w,
165 cel->y()*scale.h,
166 canvasSize.w,
167 canvasSize.h);
168 tx()(new cmd::SetCelBoundsF(cel, newBounds));
169 }
170 else {
171 resize_cel_image(
172 tx(), cel, scale,
173 m_resize_method,
174 cel->layer()->isReference() ?
175 -cel->boundsF().origin():
176 gfx::PointF(-cel->bounds().origin()));
177 }
178
179 jobProgress((float)progress / img_count);
180 ++progress;
181
182 // Cancel all the operation?
183 if (isCanceled())
184 return; // Tx destructor will undo all operations
185 }
186
187 // Resize mask
188 if (document()->isMaskVisible()) {
189 ImageRef old_bitmap
190 (crop_image(document()->mask()->bitmap(), -1, -1,
191 document()->mask()->bitmap()->width()+2,
192 document()->mask()->bitmap()->height()+2, 0));
193
194 int w = scale_x(old_bitmap->width());
195 int h = scale_y(old_bitmap->height());
196 std::unique_ptr<Mask> new_mask(new Mask);
197 new_mask->replace(
198 gfx::Rect(
199 scale_x(document()->mask()->bounds().x-1),
200 scale_y(document()->mask()->bounds().y-1),
201 std::max(1, w),
202 std::max(1, h)));
203
204 // Always use the nearest-neighbor method to resize the bitmap
205 // mask.
206 algorithm::resize_image(
207 old_bitmap.get(), new_mask->bitmap(),
208 doc::algorithm::RESIZE_METHOD_NEAREST_NEIGHBOR,
209 sprite()->palette(0), // Ignored
210 sprite()->rgbMap(0), // Ignored
211 -1); // Ignored
212
213 // Reshrink
214 new_mask->intersect(new_mask->bounds());
215
216 // Copy new mask
217 api.copyToCurrentMask(new_mask.get());
218 }
219
220 // Resize slices
221 for (auto& slice : sprite()->slices()) {
222 for (auto& k : *slice) {
223 const SliceKey& key = *k.value();
224 if (key.isEmpty())
225 continue;
226
227 SliceKey newKey = key;
228 newKey.setBounds(scale_rect(newKey.bounds()));
229
230 if (newKey.hasCenter())
231 newKey.setCenter(scale_rect(newKey.center()));
232
233 if (newKey.hasPivot())
234 newKey.setPivot(gfx::Point(scale_x(newKey.pivot().x),
235 scale_y(newKey.pivot().y)));
236
237 tx()(new cmd::SetSliceKey(slice, k.frame(), newKey));
238 }
239 }
240
241 // Resize Sprite
242 api.setSpriteSize(sprite(), m_new_width, m_new_height);
243 }
244
245};
246
247#ifdef ENABLE_UI
248
249class SpriteSizeWindow : public app::gen::SpriteSize {
250public:
251 SpriteSizeWindow(Context* ctx, const SpriteSizeParams& params) : m_ctx(ctx) {
252 lockRatio()->Click.connect([this]{ onLockRatioClick(); });
253 widthPx()->Change.connect([this]{ onWidthPxChange(); });
254 heightPx()->Change.connect([this]{ onHeightPxChange(); });
255 widthPerc()->Change.connect([this]{ onWidthPercChange(); });
256 heightPerc()->Change.connect([this]{ onHeightPercChange(); });
257
258 widthPx()->setTextf("%d", params.width());
259 heightPx()->setTextf("%d", params.height());
260 widthPerc()->setTextf(PERC_FORMAT, params.scaleX() * 100.0);
261 heightPerc()->setTextf(PERC_FORMAT, params.scaleY() * 100.0);
262
263 static_assert(doc::algorithm::RESIZE_METHOD_NEAREST_NEIGHBOR == 0 &&
264 doc::algorithm::RESIZE_METHOD_BILINEAR == 1 &&
265 doc::algorithm::RESIZE_METHOD_ROTSPRITE == 2,
266 "ResizeMethod enum has changed");
267 method()->addItem(Strings::sprite_size_method_nearest_neighbor());
268 method()->addItem(Strings::sprite_size_method_bilinear());
269 method()->addItem(Strings::sprite_size_method_rotsprite());
270 int resize_method;
271 if (params.method.isSet())
272 resize_method = (int)params.method();
273 else
274 resize_method = get_config_int("SpriteSize", "Method",
275 doc::algorithm::RESIZE_METHOD_NEAREST_NEIGHBOR);
276 method()->setSelectedItemIndex(resize_method);
277 const bool lock = (params.lockRatio.isSet())? params.lockRatio() : get_config_bool("SpriteSize", "LockRatio", false);
278 lockRatio()->setSelected(lock);
279 }
280
281private:
282
283 void onLockRatioClick() {
284 const ContextReader reader(m_ctx);
285 onWidthPxChange();
286 }
287
288 void onWidthPxChange() {
289 const ContextReader reader(m_ctx);
290 const Sprite* sprite(reader.sprite());
291 int width = widthPx()->textInt();
292 double perc = 100.0 * width / sprite->width();
293
294 widthPerc()->setTextf(PERC_FORMAT, perc);
295
296 if (lockRatio()->isSelected()) {
297 heightPerc()->setTextf(PERC_FORMAT, perc);
298 heightPx()->setTextf("%d", sprite->height() * width / sprite->width());
299 }
300 }
301
302 void onHeightPxChange() {
303 const ContextReader reader(m_ctx);
304 const Sprite* sprite(reader.sprite());
305 int height = heightPx()->textInt();
306 double perc = 100.0 * height / sprite->height();
307
308 heightPerc()->setTextf(PERC_FORMAT, perc);
309
310 if (lockRatio()->isSelected()) {
311 widthPerc()->setTextf(PERC_FORMAT, perc);
312 widthPx()->setTextf("%d", sprite->width() * height / sprite->height());
313 }
314 }
315
316 void onWidthPercChange() {
317 const ContextReader reader(m_ctx);
318 const Sprite* sprite(reader.sprite());
319 double width = widthPerc()->textDouble();
320
321 widthPx()->setTextf("%d", (int)(sprite->width() * width / 100));
322
323 if (lockRatio()->isSelected()) {
324 heightPx()->setTextf("%d", (int)(sprite->height() * width / 100));
325 heightPerc()->setText(widthPerc()->text());
326 }
327 }
328
329 void onHeightPercChange() {
330 const ContextReader reader(m_ctx);
331 const Sprite* sprite(reader.sprite());
332 double height = heightPerc()->textDouble();
333
334 heightPx()->setTextf("%d", (int)(sprite->height() * height / 100));
335
336 if (lockRatio()->isSelected()) {
337 widthPx()->setTextf("%d", (int)(sprite->width() * height / 100));
338 widthPerc()->setText(heightPerc()->text());
339 }
340 }
341
342 Context* m_ctx;
343};
344#endif // ENABLE_UI
345
346class SpriteSizeCommand : public CommandWithNewParams<SpriteSizeParams> {
347public:
348 SpriteSizeCommand();
349
350protected:
351 bool onEnabled(Context* context) override;
352 void onExecute(Context* context) override;
353};
354
355SpriteSizeCommand::SpriteSizeCommand()
356 : CommandWithNewParams<SpriteSizeParams>(CommandId::SpriteSize(), CmdRecordableFlag)
357{
358}
359
360bool SpriteSizeCommand::onEnabled(Context* context)
361{
362 return context->checkFlags(ContextFlags::ActiveDocumentIsWritable |
363 ContextFlags::HasActiveSprite);
364}
365
366void SpriteSizeCommand::onExecute(Context* context)
367{
368#ifdef ENABLE_UI
369 const bool ui = (params().ui() && context->isUIAvailable());
370#endif
371 const ContextReader reader(context);
372 const Sprite* sprite(reader.sprite());
373 auto& params = this->params();
374
375 double ratio = sprite->width() / double(sprite->height());
376 if (params.scale.isSet()) {
377 params.width(int(sprite->width() * params.scale()));
378 params.height(int(sprite->height() * params.scale()));
379 params.scaleX(params.scale());
380 params.scaleY(params.scale());
381 }
382 else if (params.lockRatio()) {
383 if (params.width.isSet()) {
384 params.height(int(params.width() / ratio));
385 params.scaleX(params.width() / double(sprite->width()));
386 params.scaleY(params.scaleX());
387 }
388 else if (params.height.isSet()) {
389 params.width(int(params.height() * ratio));
390 params.scaleY(params.height() / double(sprite->height()));
391 params.scaleX(params.scaleY());
392 }
393 else if (params.scaleX.isSet()) {
394 params.width(int(params.scaleX() * sprite->width()));
395 params.height(int(params.scaleX() * sprite->height()));
396 params.scaleY(params.scaleX());
397 }
398 else if (params.scaleY.isSet()) {
399 params.width(int(params.scaleY() * sprite->width()));
400 params.height(int(params.scaleY() * sprite->height()));
401 params.scaleX(params.scaleY());
402 }
403 else {
404 params.width(sprite->width());
405 params.height(sprite->height());
406 }
407 }
408 else {
409 if (params.width.isSet()) {
410 params.scaleX(params.width() / double(sprite->width()));
411 }
412 else if (params.scaleX.isSet()) {
413 params.width(int(params.scaleX() * sprite->width()));
414 }
415 else {
416 params.width(sprite->width());
417 }
418 if (params.height.isSet()) {
419 params.scaleY(params.height() / double(sprite->height()));
420 }
421 else if (params.scaleY.isSet()) {
422 params.height(int(params.scaleY() * sprite->height()));
423 }
424 else {
425 params.height(sprite->height());
426 }
427 }
428 int new_width = params.width();
429 int new_height = params.height();
430 ResizeMethod resize_method = params.method();
431
432#ifdef ENABLE_UI
433 if (ui) {
434 SpriteSizeWindow window(context, params);
435 window.remapWindow();
436 window.centerWindow();
437
438 load_window_pos(&window, "SpriteSize");
439 window.setVisible(true);
440 window.openWindowInForeground();
441 save_window_pos(&window, "SpriteSize");
442
443 if (window.closer() != window.ok())
444 return;
445
446 new_width = window.widthPx()->textInt();
447 new_height = window.heightPx()->textInt();
448 resize_method = (ResizeMethod)window.method()->getSelectedItemIndex();
449
450 set_config_int("SpriteSize", "Method", resize_method);
451 set_config_bool("SpriteSize", "LockRatio", window.lockRatio()->isSelected());
452 }
453#endif // ENABLE_UI
454
455 new_width = std::clamp(new_width, 1, DOC_SPRITE_MAX_WIDTH);
456 new_height = std::clamp(new_height, 1, DOC_SPRITE_MAX_HEIGHT);
457
458 {
459 SpriteSizeJob job(reader, new_width, new_height, resize_method);
460 job.startJob();
461 job.waitJob();
462 }
463
464#ifdef ENABLE_UI
465 update_screen_for_document(reader.document());
466#endif
467}
468
469Command* CommandFactory::createSpriteSizeCommand()
470{
471 return new SpriteSizeCommand;
472}
473
474} // namespace app
475