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/app.h"
13#include "app/cmd/flatten_layers.h"
14#include "app/cmd/set_pixel_format.h"
15#include "app/commands/command.h"
16#include "app/commands/params.h"
17#include "app/context_access.h"
18#include "app/extensions.h"
19#include "app/i18n/strings.h"
20#include "app/load_matrix.h"
21#include "app/modules/editors.h"
22#include "app/modules/gui.h"
23#include "app/modules/palettes.h"
24#include "app/sprite_job.h"
25#include "app/transaction.h"
26#include "app/ui/dithering_selector.h"
27#include "app/ui/editor/editor.h"
28#include "app/ui/editor/editor_render.h"
29#include "app/ui/rgbmap_algorithm_selector.h"
30#include "app/ui/skin/skin_theme.h"
31#include "base/thread.h"
32#include "doc/image.h"
33#include "doc/layer.h"
34#include "doc/sprite.h"
35#include "fmt/format.h"
36#include "render/dithering.h"
37#include "render/dithering_algorithm.h"
38#include "render/ordered_dither.h"
39#include "render/quantization.h"
40#include "render/render.h"
41#include "render/task_delegate.h"
42#include "ui/listitem.h"
43#include "ui/paint_event.h"
44#include "ui/size_hint_event.h"
45
46#include "color_mode.xml.h"
47#include <string>
48
49namespace app {
50
51using namespace ui;
52
53namespace {
54
55rgba_to_graya_func get_gray_func(gen::ToGrayAlgorithm toGray) {
56 switch (toGray) {
57 case gen::ToGrayAlgorithm::LUMA: return &rgba_to_graya_using_luma;
58 case gen::ToGrayAlgorithm::HSV: return &rgba_to_graya_using_hsv;
59 case gen::ToGrayAlgorithm::HSL: return &rgba_to_graya_using_hsl;
60 }
61 return nullptr;
62}
63
64class ConvertThread : public render::TaskDelegate {
65public:
66 ConvertThread(const doc::ImageRef& dstImage,
67 const doc::Sprite* sprite,
68 const doc::frame_t frame,
69 const doc::PixelFormat pixelFormat,
70 const render::Dithering& dithering,
71 const doc::RgbMapAlgorithm rgbMapAlgorithm,
72 const gen::ToGrayAlgorithm toGray,
73 const gfx::Point& pos,
74 const bool newBlend)
75 : m_image(dstImage)
76 , m_pos(pos)
77 , m_running(true)
78 , m_stopFlag(false)
79 , m_progress(0.0)
80 , m_thread(
81 [this,
82 sprite, frame,
83 pixelFormat,
84 dithering,
85 rgbMapAlgorithm,
86 toGray,
87 newBlend]() { // Copy the matrix
88 run(sprite, frame,
89 pixelFormat,
90 dithering,
91 rgbMapAlgorithm,
92 toGray,
93 newBlend);
94 })
95 {
96 }
97
98 void stop() {
99 m_stopFlag = true;
100 m_thread.join();
101 }
102
103 bool isRunning() const {
104 return m_running;
105 }
106
107 double progress() const {
108 return m_progress;
109 }
110
111private:
112 void run(const Sprite* sprite,
113 const doc::frame_t frame,
114 const doc::PixelFormat pixelFormat,
115 const render::Dithering& dithering,
116 const doc::RgbMapAlgorithm rgbMapAlgorithm,
117 const gen::ToGrayAlgorithm toGray,
118 const bool newBlend) {
119 doc::ImageRef tmp(
120 Image::create(sprite->pixelFormat(),
121 m_image->width(),
122 m_image->height()));
123
124 render::Render render;
125 render.setNewBlend(newBlend);
126 render.renderSprite(
127 tmp.get(), sprite, frame,
128 gfx::Clip(0, 0,
129 m_pos.x, m_pos.y,
130 m_image->width(),
131 m_image->height()));
132
133 render::convert_pixel_format(
134 tmp.get(),
135 m_image.get(),
136 pixelFormat,
137 dithering,
138 sprite->rgbMap(frame,
139 sprite->rgbMapForSprite(),
140 rgbMapAlgorithm),
141 sprite->palette(frame),
142 (sprite->backgroundLayer() != nullptr),
143 0,
144 get_gray_func(toGray),
145 this);
146
147 m_running = false;
148 }
149
150private:
151 // render::TaskDelegate impl
152 bool continueTask() override {
153 return !m_stopFlag;
154 }
155
156 void notifyTaskProgress(double progress) override {
157 m_progress = progress;
158 }
159
160 doc::ImageRef m_image;
161 gfx::Point m_pos;
162 bool m_running;
163 bool m_stopFlag;
164 double m_progress;
165 base::thread m_thread;
166};
167
168#ifdef ENABLE_UI
169
170class ConversionItem : public ListItem {
171public:
172 ConversionItem(const doc::PixelFormat pixelFormat)
173 : m_pixelFormat(pixelFormat) {
174 std::string toMode;
175 switch (pixelFormat) {
176 case IMAGE_RGB: toMode = Strings::commands_ChangePixelFormat_RGB(); break;
177 case IMAGE_GRAYSCALE: toMode = Strings::commands_ChangePixelFormat_Grayscale(); break;
178 case IMAGE_INDEXED: toMode = Strings::commands_ChangePixelFormat_Indexed(); break;
179 }
180 setText(fmt::format("-> {}", toMode));
181 }
182 doc::PixelFormat pixelFormat() const { return m_pixelFormat; }
183private:
184 doc::PixelFormat m_pixelFormat;
185};
186
187class ColorModeWindow : public app::gen::ColorMode {
188public:
189 ColorModeWindow(Editor* editor)
190 : m_timer(100)
191 , m_editor(editor)
192 , m_image(nullptr)
193 , m_imageBuffer(new doc::ImageBuffer)
194 , m_selectedItem(nullptr)
195 , m_ditheringSelector(nullptr)
196 , m_mapAlgorithmSelector(nullptr)
197 , m_imageJustCreated(true)
198 {
199 const auto& pref = Preferences::instance();
200 const doc::PixelFormat from = m_editor->sprite()->pixelFormat();
201
202 // Add the color mode in the window title
203 std::string fromMode;
204 switch (from) {
205 case IMAGE_RGB: fromMode = Strings::commands_ChangePixelFormat_RGB(); break;
206 case IMAGE_GRAYSCALE: fromMode = Strings::commands_ChangePixelFormat_Grayscale(); break;
207 case IMAGE_INDEXED: fromMode = Strings::commands_ChangePixelFormat_Indexed(); break;
208 }
209 setText(fmt::format("{}: {}", text(), fromMode));
210
211 // Add conversion items
212 if (from != IMAGE_RGB)
213 colorMode()->addChild(new ConversionItem(IMAGE_RGB));
214 if (from != IMAGE_INDEXED) {
215 colorMode()->addChild(new ConversionItem(IMAGE_INDEXED));
216
217 m_ditheringSelector = new DitheringSelector(DitheringSelector::SelectBoth);
218 m_ditheringSelector->setExpansive(true);
219
220 m_mapAlgorithmSelector = new RgbMapAlgorithmSelector;
221 m_mapAlgorithmSelector->setExpansive(true);
222
223 // Select default dithering method
224 {
225 int index = m_ditheringSelector->findItemIndex(
226 pref.quantization.ditheringAlgorithm());
227 if (index >= 0)
228 m_ditheringSelector->setSelectedItemIndex(index);
229 }
230
231 // Select default RgbMap algorithm
232 m_mapAlgorithmSelector->algorithm(pref.quantization.rgbmapAlgorithm());
233
234 ditheringPlaceholder()->addChild(m_ditheringSelector);
235 rgbmapAlgorithmPlaceholder()->addChild(m_mapAlgorithmSelector);
236
237 const bool adv = pref.quantization.advanced();
238 advancedCheck()->setSelected(adv);
239 advanced()->setVisible(adv);
240
241 // Signals
242 m_ditheringSelector->Change.connect([this]{ onIndexParamChange(); });
243 m_mapAlgorithmSelector->Change.connect([this]{ onIndexParamChange(); });
244 factor()->Change.connect([this]{ onIndexParamChange(); });
245
246 advancedCheck()->Click.connect(
247 [this](ui::Event&){
248 advanced()->setVisible(advancedCheck()->isSelected());
249 expandWindow(sizeHint());
250 });
251 }
252 else {
253 amount()->setVisible(false);
254 advancedCheck()->setVisible(false);
255 advanced()->setVisible(false);
256 }
257 if (from != IMAGE_GRAYSCALE) {
258 colorMode()->addChild(new ConversionItem(IMAGE_GRAYSCALE));
259
260 toGrayCombobox()->Change.connect([this]{ onToGrayChange(); });
261 }
262
263 colorModeView()->setMinSize(
264 colorModeView()->sizeHint() +
265 colorMode()->sizeHint());
266
267 colorMode()->Change.connect([this]{ onChangeColorMode(); });
268 m_timer.Tick.connect([this]{ onMonitorProgress(); });
269
270 progress()->setReadOnly(true);
271
272 // Default dithering factor
273 factor()->setValue(pref.quantization.ditheringFactor());
274
275 // Select first option
276 colorMode()->selectIndex(0);
277 }
278
279 ~ColorModeWindow() {
280 stop();
281 }
282
283 doc::PixelFormat pixelFormat() const {
284 ASSERT(m_selectedItem);
285 return m_selectedItem->pixelFormat();
286 }
287
288 render::Dithering dithering() const {
289 render::Dithering d;
290 if (m_ditheringSelector) {
291 d.algorithm(m_ditheringSelector->ditheringAlgorithm());
292 d.matrix(m_ditheringSelector->ditheringMatrix());
293 }
294 d.factor(double(factor()->getValue()) / 100.0);
295 return d;
296 }
297
298 doc::RgbMapAlgorithm rgbMapAlgorithm() const {
299 if (m_mapAlgorithmSelector)
300 return m_mapAlgorithmSelector->algorithm();
301 else
302 return doc::RgbMapAlgorithm::DEFAULT;
303 }
304
305 gen::ToGrayAlgorithm toGray() const {
306 static_assert(
307 int(gen::ToGrayAlgorithm::LUMA) == 0 &&
308 int(gen::ToGrayAlgorithm::HSV) == 1 &&
309 int(gen::ToGrayAlgorithm::HSL) == 2,
310 "Check that 'to_gray_combobox' combobox items matches these indexes in color_mode.xml");
311 return (gen::ToGrayAlgorithm)toGrayCombobox()->getSelectedItemIndex();
312 }
313
314 bool flattenEnabled() const {
315 return flatten()->isSelected();
316 }
317
318 void saveOptions() {
319 auto& pref = Preferences::instance();
320
321 // Save the dithering method used for the future
322 if (m_ditheringSelector) {
323 if (auto item = m_ditheringSelector->getSelectedItem()) {
324 pref.quantization.ditheringAlgorithm(
325 item->text());
326
327 if (m_ditheringSelector->ditheringAlgorithm() ==
328 render::DitheringAlgorithm::ErrorDiffusion) {
329 pref.quantization.ditheringFactor(
330 factor()->getValue());
331 }
332 }
333 }
334
335 if (m_mapAlgorithmSelector)
336 pref.quantization.advanced(advancedCheck()->isSelected());
337 }
338
339private:
340
341 void stop() {
342 m_editor->renderEngine().removePreviewImage();
343 m_editor->invalidate();
344
345 m_timer.stop();
346 if (m_bgThread) {
347 m_bgThread->stop();
348 m_bgThread.reset(nullptr);
349 }
350 }
351
352 void onChangeColorMode() {
353 ConversionItem* item =
354 static_cast<ConversionItem*>(colorMode()->getSelectedChild());
355 if (item == m_selectedItem) // Avoid restarting the conversion process for the same option
356 return;
357 m_selectedItem = item;
358
359 stop();
360
361 gfx::Rect visibleBounds = m_editor->getVisibleSpriteBounds();
362 if (visibleBounds.isEmpty())
363 return;
364
365 doc::PixelFormat dstPixelFormat = item->pixelFormat();
366
367 if (m_ditheringSelector) {
368 const bool toIndexed = (dstPixelFormat == doc::IMAGE_INDEXED);
369 m_ditheringSelector->setVisible(toIndexed);
370
371 const bool errorDiff =
372 (m_ditheringSelector->ditheringAlgorithm() ==
373 render::DitheringAlgorithm::ErrorDiffusion);
374 amount()->setVisible(toIndexed && errorDiff);
375 }
376
377 {
378 const bool toGray = (dstPixelFormat == doc::IMAGE_GRAYSCALE);
379 toGrayCombobox()->setVisible(toGray);
380 }
381
382 m_image.reset(
383 Image::create(dstPixelFormat,
384 visibleBounds.w,
385 visibleBounds.h,
386 m_imageBuffer));
387 if (m_imageJustCreated) {
388 m_imageJustCreated = false;
389 m_image->clear(0);
390 }
391
392 m_editor->renderEngine().setPreviewImage(
393 nullptr,
394 m_editor->frame(),
395 m_image.get(),
396 nullptr,
397 visibleBounds.origin(),
398 doc::BlendMode::SRC);
399
400 m_editor->invalidate();
401 progress()->setValue(0);
402 progress()->setVisible(false);
403 layout();
404
405 m_bgThread.reset(
406 new ConvertThread(
407 m_image,
408 m_editor->sprite(),
409 m_editor->frame(),
410 dstPixelFormat,
411 dithering(),
412 rgbMapAlgorithm(),
413 toGray(),
414 visibleBounds.origin(),
415 Preferences::instance().experimental.newBlend()));
416
417 m_timer.start();
418 }
419
420 void onIndexParamChange() {
421 stop();
422 m_selectedItem = nullptr;
423 onChangeColorMode();
424 }
425
426 void onToGrayChange() {
427 stop();
428 m_selectedItem = nullptr;
429 onChangeColorMode();
430 }
431
432 void onMonitorProgress() {
433 ASSERT(m_bgThread);
434 if (!m_bgThread)
435 return;
436
437 if (!m_bgThread->isRunning()) {
438 m_timer.stop();
439 m_bgThread->stop();
440 m_bgThread.reset(nullptr);
441
442 progress()->setVisible(false);
443 layout();
444 }
445 else {
446 int v = int(100 * m_bgThread->progress());
447 if (v > 0) {
448 progress()->setValue(v);
449 if (!progress()->isVisible()) {
450 progress()->setVisible(true);
451 layout();
452 }
453 }
454 }
455
456 m_editor->invalidate();
457 }
458
459 Timer m_timer;
460 Editor* m_editor;
461 doc::ImageRef m_image;
462 doc::ImageBufferPtr m_imageBuffer;
463 std::unique_ptr<ConvertThread> m_bgThread;
464 ConversionItem* m_selectedItem;
465 DitheringSelector* m_ditheringSelector;
466 RgbMapAlgorithmSelector* m_mapAlgorithmSelector;
467 bool m_imageJustCreated;
468};
469
470#endif // ENABLE_UI
471
472} // anonymous namespace
473
474class ChangePixelFormatCommand : public Command {
475public:
476 ChangePixelFormatCommand();
477
478protected:
479 void onLoadParams(const Params& params) override;
480 bool onEnabled(Context* context) override;
481 bool onChecked(Context* context) override;
482 void onExecute(Context* context) override;
483 std::string onGetFriendlyName() const override;
484
485private:
486 bool m_useUI;
487 doc::PixelFormat m_format;
488 render::Dithering m_dithering;
489 doc::RgbMapAlgorithm m_rgbmap;
490 gen::ToGrayAlgorithm m_toGray;
491};
492
493ChangePixelFormatCommand::ChangePixelFormatCommand()
494 : Command(CommandId::ChangePixelFormat(), CmdUIOnlyFlag)
495{
496 m_useUI = true;
497 m_format = IMAGE_RGB;
498 m_dithering = render::Dithering();
499 m_rgbmap = doc::RgbMapAlgorithm::DEFAULT;
500 m_toGray = gen::ToGrayAlgorithm::DEFAULT;
501}
502
503void ChangePixelFormatCommand::onLoadParams(const Params& params)
504{
505 m_useUI = false;
506
507 std::string format = params.get("format");
508 if (format == "rgb") m_format = IMAGE_RGB;
509 else if (format == "grayscale" ||
510 format == "gray") m_format = IMAGE_GRAYSCALE;
511 else if (format == "indexed") m_format = IMAGE_INDEXED;
512 else
513 m_useUI = true;
514
515 std::string dithering = params.get("dithering");
516 if (dithering == "ordered")
517 m_dithering.algorithm(render::DitheringAlgorithm::Ordered);
518 else if (dithering == "old")
519 m_dithering.algorithm(render::DitheringAlgorithm::Old);
520 else if (dithering == "error-diffusion")
521 m_dithering.algorithm(render::DitheringAlgorithm::ErrorDiffusion);
522 else
523 m_dithering.algorithm(render::DitheringAlgorithm::None);
524
525 std::string matrix = params.get("dithering-matrix");
526 if (!matrix.empty()) {
527 // Try to get the matrix from the extensions
528 const render::DitheringMatrix* knownMatrix =
529 App::instance()->extensions().ditheringMatrix(matrix);
530 if (knownMatrix) {
531 m_dithering.matrix(*knownMatrix);
532 }
533 // Then, if the matrix doesn't exist we try to load it from a file
534 else {
535 render::DitheringMatrix ditMatrix;
536 if (!load_dithering_matrix_from_sprite(matrix, ditMatrix))
537 throw std::runtime_error("Invalid matrix name");
538 m_dithering.matrix(ditMatrix);
539 }
540 }
541 // Default dithering matrix is BayerMatrix(8)
542 else {
543 // TODO object slicing here (from BayerMatrix -> DitheringMatrix)
544 m_dithering.matrix(render::BayerMatrix(8));
545 }
546
547 // TODO change this with NewParams as in ColorQuantizationParams
548 std::string rgbmap = params.get("rgbmap");
549 if (rgbmap == "octree")
550 m_rgbmap = doc::RgbMapAlgorithm::OCTREE;
551 else if (rgbmap == "rgb5a3")
552 m_rgbmap = doc::RgbMapAlgorithm::RGB5A3;
553 else if (rgbmap == "default")
554 m_rgbmap = doc::RgbMapAlgorithm::DEFAULT;
555 else {
556 // Use the configured algorithm by default.
557 m_rgbmap = Preferences::instance().quantization.rgbmapAlgorithm();
558 }
559
560 std::string toGray = params.get("toGray");
561 if (toGray == "luma")
562 m_toGray = gen::ToGrayAlgorithm::LUMA;
563 else if (dithering == "hsv")
564 m_toGray = gen::ToGrayAlgorithm::HSV;
565 else if (dithering == "hsl")
566 m_toGray = gen::ToGrayAlgorithm::HSL;
567 else
568 m_toGray = gen::ToGrayAlgorithm::DEFAULT;
569}
570
571bool ChangePixelFormatCommand::onEnabled(Context* context)
572{
573 if (!context->checkFlags(ContextFlags::ActiveDocumentIsWritable |
574 ContextFlags::HasActiveSprite))
575 return false;
576
577 const ContextReader reader(context);
578 const Sprite* sprite(reader.sprite());
579
580 if (!sprite)
581 return false;
582
583 if (m_useUI)
584 return true;
585
586 if (sprite->pixelFormat() == IMAGE_INDEXED &&
587 m_format == IMAGE_INDEXED &&
588 m_dithering.algorithm() != render::DitheringAlgorithm::None)
589 return false;
590
591 return true;
592}
593
594bool ChangePixelFormatCommand::onChecked(Context* context)
595{
596 if (m_useUI)
597 return false;
598
599 const ContextReader reader(context);
600 const Sprite* sprite = reader.sprite();
601
602 if (sprite &&
603 sprite->pixelFormat() == IMAGE_INDEXED &&
604 m_format == IMAGE_INDEXED &&
605 m_dithering.algorithm() != render::DitheringAlgorithm::None)
606 return false;
607
608 return
609 (sprite &&
610 sprite->pixelFormat() == m_format);
611}
612
613void ChangePixelFormatCommand::onExecute(Context* context)
614{
615 bool flatten = false;
616
617#ifdef ENABLE_UI
618 if (m_useUI) {
619 ColorModeWindow window(current_editor);
620
621 window.remapWindow();
622 window.centerWindow();
623
624 load_window_pos(&window, "ChangePixelFormat");
625 window.openWindowInForeground();
626 save_window_pos(&window, "ChangePixelFormat");
627
628 if (window.closer() != window.ok())
629 return;
630
631 m_format = window.pixelFormat();
632 m_dithering = window.dithering();
633 m_rgbmap = window.rgbMapAlgorithm();
634 m_toGray = window.toGray();
635 flatten = window.flattenEnabled();
636
637 window.saveOptions();
638 }
639#endif // ENABLE_UI
640
641 // No conversion needed
642 if (context->activeDocument()->sprite()->pixelFormat() == m_format)
643 return;
644
645 {
646 const ContextReader reader(context);
647 SpriteJob job(reader, Strings::color_mode_title().c_str());
648 Sprite* sprite(job.sprite());
649
650 // TODO this was moved in the main UI thread because
651 // cmd::FlattenLayers() generates a EditorObserver::onAfterLayerChanged()
652 // event, and that event is an UI event.
653 // We should refactor the whole app to separate doc changes <-> UI changes,
654 // but that is for the future:
655 // https://github.com/aseprite/aseprite/issues/509
656 // https://github.com/aseprite/aseprite/issues/378
657 if (flatten) {
658 const bool newBlend = Preferences::instance().experimental.newBlend();
659 SelectedLayers selLayers;
660 for (auto layer : sprite->root()->layers())
661 selLayers.insert(layer);
662 job.tx()(new cmd::FlattenLayers(sprite, selLayers, newBlend));
663 }
664
665 job.startJobWithCallback(
666 [this, &job, sprite] {
667 job.tx()(
668 new cmd::SetPixelFormat(
669 sprite, m_format,
670 m_dithering,
671 m_rgbmap,
672 get_gray_func(m_toGray),
673 &job)); // SpriteJob is a render::TaskDelegate
674 });
675 job.waitJob();
676 }
677
678 if (context->isUIAvailable())
679 app_refresh_screen();
680}
681
682std::string ChangePixelFormatCommand::onGetFriendlyName() const
683{
684 std::string conversion;
685
686 if (!m_useUI) {
687 switch (m_format) {
688 case IMAGE_RGB:
689 conversion = Strings::commands_ChangePixelFormat_RGB();
690 break;
691 case IMAGE_GRAYSCALE:
692 conversion = Strings::commands_ChangePixelFormat_Grayscale();
693 break;
694 case IMAGE_INDEXED:
695 switch (m_dithering.algorithm()) {
696 case render::DitheringAlgorithm::None:
697 conversion = Strings::commands_ChangePixelFormat_Indexed();
698 break;
699 case render::DitheringAlgorithm::Ordered:
700 conversion = Strings::commands_ChangePixelFormat_Indexed_OrderedDithering();
701 break;
702 case render::DitheringAlgorithm::Old:
703 conversion = Strings::commands_ChangePixelFormat_Indexed_OldDithering();
704 break;
705 case render::DitheringAlgorithm::ErrorDiffusion:
706 conversion = Strings::commands_ChangePixelFormat_Indexed_ErrorDifussion();
707 break;
708 }
709 break;
710 }
711 }
712 else
713 conversion = Strings::commands_ChangePixelFormat_MoreOptions();
714
715 return fmt::format(getBaseFriendlyName(), conversion);
716}
717
718Command* CommandFactory::createChangePixelFormatCommand()
719{
720 return new ChangePixelFormatCommand;
721}
722
723} // namespace app
724