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/set_pixel_format.h" |
13 | |
14 | #include "app/cmd/remove_palette.h" |
15 | #include "app/cmd/replace_image.h" |
16 | #include "app/cmd/set_cel_opacity.h" |
17 | #include "app/cmd/set_palette.h" |
18 | #include "app/doc.h" |
19 | #include "app/doc_event.h" |
20 | #include "doc/cel.h" |
21 | #include "doc/cels_range.h" |
22 | #include "doc/document.h" |
23 | #include "doc/layer.h" |
24 | #include "doc/palette.h" |
25 | #include "doc/rgbmap.h" |
26 | #include "doc/sprite.h" |
27 | #include "doc/tilesets.h" |
28 | #include "render/quantization.h" |
29 | #include "render/task_delegate.h" |
30 | |
31 | namespace app { |
32 | namespace cmd { |
33 | |
34 | using namespace doc; |
35 | |
36 | namespace { |
37 | |
38 | class SuperDelegate : public render::TaskDelegate { |
39 | public: |
40 | SuperDelegate(int nimages, render::TaskDelegate* delegate) |
41 | : m_nimages(nimages) |
42 | , m_curImage(0) |
43 | , m_delegate(delegate) { |
44 | } |
45 | |
46 | void notifyTaskProgress(double progress) override { |
47 | if (m_delegate) |
48 | m_delegate->notifyTaskProgress( |
49 | (progress + m_curImage) / m_nimages); |
50 | } |
51 | |
52 | bool continueTask() override { |
53 | if (m_delegate) |
54 | return m_delegate->continueTask(); |
55 | else |
56 | return true; |
57 | } |
58 | |
59 | void nextImage() { |
60 | ++m_curImage; |
61 | } |
62 | |
63 | private: |
64 | int m_nimages; |
65 | int m_curImage; |
66 | TaskDelegate* m_delegate; |
67 | }; |
68 | |
69 | } // anonymous namespace |
70 | |
71 | SetPixelFormat::SetPixelFormat(Sprite* sprite, |
72 | const PixelFormat newFormat, |
73 | const render::Dithering& dithering, |
74 | const doc::RgbMapAlgorithm mapAlgorithm, |
75 | doc::rgba_to_graya_func toGray, |
76 | render::TaskDelegate* delegate) |
77 | : WithSprite(sprite) |
78 | , m_oldFormat(sprite->pixelFormat()) |
79 | , m_newFormat(newFormat) |
80 | { |
81 | if (sprite->pixelFormat() == newFormat) |
82 | return; |
83 | |
84 | // Calculate the number of images to convert just to show a proper |
85 | // progress bar. |
86 | tile_index nimages = 0; |
87 | for (Cel* cel : sprite->uniqueCels()) |
88 | if (!cel->layer()->isTilemap()) |
89 | ++nimages; |
90 | if (sprite->hasTilesets()) { |
91 | for (Tileset* tileset : *sprite->tilesets()) |
92 | nimages += tileset->size(); |
93 | } |
94 | |
95 | SuperDelegate superDel(nimages, delegate); |
96 | |
97 | // Convert cel images |
98 | for (Cel* cel : sprite->uniqueCels()) { |
99 | if (cel->layer()->isTilemap()) |
100 | continue; |
101 | |
102 | ImageRef oldImage = cel->imageRef(); |
103 | convertImage(sprite, dithering, |
104 | oldImage, |
105 | cel->frame(), |
106 | cel->layer()->isBackground(), |
107 | mapAlgorithm, |
108 | toGray, |
109 | &superDel); |
110 | |
111 | superDel.nextImage(); |
112 | } |
113 | |
114 | // Convert tileset images |
115 | if (sprite->hasTilesets()) { |
116 | for (Tileset* tileset : *sprite->tilesets()) { |
117 | for (tile_index i=0; i<tileset->size(); ++i) { |
118 | ImageRef oldImage = tileset->get(i); |
119 | if (oldImage) { |
120 | convertImage(sprite, dithering, |
121 | oldImage, |
122 | 0, // TODO select a frame or generate other tilesets? |
123 | false, // TODO is background? it depends of the layer where this tileset is used |
124 | mapAlgorithm, |
125 | toGray, |
126 | &superDel); |
127 | } |
128 | superDel.nextImage(); |
129 | } |
130 | } |
131 | } |
132 | |
133 | // Set all cels opacity to 100% if we are converting to indexed. |
134 | // TODO remove this |
135 | if (newFormat == IMAGE_INDEXED) { |
136 | for (Cel* cel : sprite->uniqueCels()) { |
137 | if (cel->opacity() < 255) |
138 | m_seq.add(new cmd::SetCelOpacity(cel, 255)); |
139 | } |
140 | } |
141 | |
142 | // When we are converting to grayscale color mode, we've to destroy |
143 | // all palettes and put only one grayscaled-palette at the first |
144 | // frame. |
145 | if (newFormat == IMAGE_GRAYSCALE) { |
146 | // Add cmds to revert all palette changes. |
147 | PalettesList palettes = sprite->getPalettes(); |
148 | for (Palette* pal : palettes) |
149 | if (pal->frame() != 0) |
150 | m_seq.add(new cmd::RemovePalette(sprite, pal)); |
151 | |
152 | std::unique_ptr<Palette> graypal(Palette::createGrayscale()); |
153 | if (*graypal != *sprite->palette(0)) |
154 | m_seq.add(new cmd::SetPalette(sprite, 0, graypal.get())); |
155 | } |
156 | } |
157 | |
158 | void SetPixelFormat::onExecute() |
159 | { |
160 | m_seq.execute(context()); |
161 | setFormat(m_newFormat); |
162 | } |
163 | |
164 | void SetPixelFormat::onUndo() |
165 | { |
166 | m_seq.undo(); |
167 | setFormat(m_oldFormat); |
168 | } |
169 | |
170 | void SetPixelFormat::onRedo() |
171 | { |
172 | m_seq.redo(); |
173 | setFormat(m_newFormat); |
174 | } |
175 | |
176 | void SetPixelFormat::setFormat(PixelFormat format) |
177 | { |
178 | Sprite* sprite = this->sprite(); |
179 | |
180 | sprite->setPixelFormat(format); |
181 | if (format == IMAGE_INDEXED) { |
182 | int maskIndex = sprite->palette(0)->findMaskColor(); |
183 | sprite->setTransparentColor(maskIndex == -1 ? 0 : maskIndex); |
184 | } |
185 | else |
186 | sprite->setTransparentColor(0); |
187 | sprite->incrementVersion(); |
188 | |
189 | // Regenerate extras |
190 | Doc* doc = static_cast<Doc*>(sprite->document()); |
191 | doc->setExtraCel(ExtraCelRef(nullptr)); |
192 | |
193 | // Generate notification |
194 | DocEvent ev(doc); |
195 | ev.sprite(sprite); |
196 | doc->notify_observers<DocEvent&>(&DocObserver::onPixelFormatChanged, ev); |
197 | } |
198 | |
199 | void SetPixelFormat::convertImage(doc::Sprite* sprite, |
200 | const render::Dithering& dithering, |
201 | const doc::ImageRef& oldImage, |
202 | const doc::frame_t frame, |
203 | const bool isBackground, |
204 | const doc::RgbMapAlgorithm mapAlgorithm, |
205 | doc::rgba_to_graya_func toGray, |
206 | render::TaskDelegate* delegate) |
207 | { |
208 | ASSERT(oldImage); |
209 | ASSERT(oldImage->pixelFormat() != IMAGE_TILEMAP); |
210 | |
211 | // Making the RGBMap for Image->INDEXDED conversion. |
212 | // TODO: this is needed only when newImage |
213 | RgbMap* rgbmap; |
214 | int newMaskIndex = (isBackground ? -1 : 0); |
215 | if (m_newFormat == IMAGE_INDEXED) { |
216 | rgbmap = sprite->rgbMap(frame, sprite->rgbMapForSprite(), mapAlgorithm); |
217 | if (m_oldFormat == IMAGE_INDEXED) |
218 | newMaskIndex = sprite->transparentColor(); |
219 | else |
220 | newMaskIndex = rgbmap->maskIndex(); |
221 | } |
222 | else { |
223 | rgbmap = nullptr; |
224 | } |
225 | ImageRef newImage( |
226 | render::convert_pixel_format |
227 | (oldImage.get(), nullptr, m_newFormat, |
228 | dithering, |
229 | rgbmap, |
230 | sprite->palette(frame), |
231 | isBackground, |
232 | newMaskIndex, |
233 | toGray, |
234 | delegate)); |
235 | |
236 | m_seq.add(new cmd::ReplaceImage(sprite, oldImage, newImage)); |
237 | } |
238 | |
239 | } // namespace cmd |
240 | } // namespace app |
241 | |