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/console.h" |
13 | #include "app/context.h" |
14 | #include "app/doc.h" |
15 | #include "app/file/file.h" |
16 | #include "app/file/file_format.h" |
17 | #include "app/file/tga_options.h" |
18 | #include "base/cfile.h" |
19 | #include "base/convert_to.h" |
20 | #include "base/file_handle.h" |
21 | #include "doc/doc.h" |
22 | #include "doc/image_bits.h" |
23 | #include "tga/tga.h" |
24 | #include "ui/combobox.h" |
25 | #include "ui/listitem.h" |
26 | |
27 | #include "tga_options.xml.h" |
28 | |
29 | namespace app { |
30 | |
31 | using namespace base; |
32 | |
33 | class TgaFormat : public FileFormat { |
34 | const char* onGetName() const override { |
35 | return "tga" ; |
36 | } |
37 | |
38 | void onGetExtensions(base::paths& exts) const override { |
39 | exts.push_back("tga" ); |
40 | } |
41 | |
42 | dio::FileFormat onGetDioFormat() const override { |
43 | return dio::FileFormat::TARGA_IMAGE; |
44 | } |
45 | |
46 | int onGetFlags() const override { |
47 | return |
48 | FILE_SUPPORT_LOAD | |
49 | FILE_SUPPORT_SAVE | |
50 | FILE_SUPPORT_RGB | |
51 | FILE_SUPPORT_RGBA | |
52 | FILE_SUPPORT_GRAY | |
53 | FILE_SUPPORT_INDEXED | |
54 | FILE_SUPPORT_SEQUENCES | |
55 | FILE_SUPPORT_GET_FORMAT_OPTIONS | |
56 | FILE_SUPPORT_PALETTE_WITH_ALPHA | |
57 | FILE_ENCODE_ABSTRACT_IMAGE; |
58 | } |
59 | |
60 | bool onLoad(FileOp* fop) override; |
61 | #ifdef ENABLE_SAVE |
62 | bool onSave(FileOp* fop) override; |
63 | #endif |
64 | |
65 | FormatOptionsPtr onAskUserForFormatOptions(FileOp* fop) override; |
66 | }; |
67 | |
68 | FileFormat* CreateTgaFormat() |
69 | { |
70 | return new TgaFormat; |
71 | } |
72 | |
73 | namespace { |
74 | |
75 | class TgaDelegate : public tga::Delegate { |
76 | public: |
77 | TgaDelegate(FileOp* fop) : m_fop(fop) { } |
78 | bool notifyProgress(double progress) override { |
79 | m_fop->setProgress(progress); |
80 | return !m_fop->isStop(); |
81 | } |
82 | private: |
83 | FileOp* m_fop; |
84 | }; |
85 | |
86 | bool (const tga::Header& , ImageSpec& spec) |
87 | { |
88 | switch (header.imageType) { |
89 | |
90 | case tga::UncompressedIndexed: |
91 | case tga::RleIndexed: |
92 | if (header.bitsPerPixel != 8) |
93 | return false; |
94 | spec = ImageSpec(ColorMode::INDEXED, |
95 | header.width, |
96 | header.height); |
97 | return true; |
98 | |
99 | case tga::UncompressedRgb: |
100 | case tga::RleRgb: |
101 | if (header.bitsPerPixel != 15 && |
102 | header.bitsPerPixel != 16 && |
103 | header.bitsPerPixel != 24 && |
104 | header.bitsPerPixel != 32) |
105 | return false; |
106 | spec = ImageSpec(ColorMode::RGB, |
107 | header.width, |
108 | header.height); |
109 | return true; |
110 | |
111 | case tga::UncompressedGray: |
112 | case tga::RleGray: |
113 | if (header.bitsPerPixel != 8) |
114 | return false; |
115 | spec = ImageSpec(ColorMode::GRAYSCALE, |
116 | header.width, |
117 | header.height); |
118 | return true; |
119 | } |
120 | return false; |
121 | } |
122 | |
123 | } // anonymous namespace |
124 | |
125 | bool TgaFormat::onLoad(FileOp* fop) |
126 | { |
127 | FileHandle handle(open_file_with_exception(fop->filename(), "rb" )); |
128 | tga::StdioFileInterface finterface(handle.get()); |
129 | tga::Decoder decoder(&finterface); |
130 | tga::Header ; |
131 | if (!decoder.readHeader(header)) { |
132 | fop->setError("Invalid TGA header\n" ); |
133 | return false; |
134 | } |
135 | |
136 | ImageSpec spec(ColorMode::RGB, 1, 1); |
137 | if (!get_image_spec(header, spec)) { |
138 | fop->setError("Unsupported color depth in TGA file: %d bpp, image type=%d.\n" , |
139 | header.bitsPerPixel, |
140 | header.imageType); |
141 | return false; |
142 | } |
143 | |
144 | // Palette from TGA file |
145 | if (header.hasColormap()) { |
146 | const tga::Colormap& pal = header.colormap; |
147 | for (int i=0; i<pal.size(); ++i) { |
148 | tga::color_t c = pal[i]; |
149 | fop->sequenceSetColor(i, |
150 | tga::getr(c), |
151 | tga::getg(c), |
152 | tga::getb(c)); |
153 | if (tga::geta(c) < 255) { |
154 | fop->sequenceSetAlpha(i, tga::geta(c)); |
155 | fop->sequenceSetHasAlpha(true); // Is a transparent sprite |
156 | } |
157 | } |
158 | } |
159 | // Generate grayscale palette |
160 | else if (header.isGray()) { |
161 | for (int i=0; i<256; ++i) |
162 | fop->sequenceSetColor(i, i, i, i); |
163 | } |
164 | |
165 | if (decoder.hasAlpha()) |
166 | fop->sequenceSetHasAlpha(true); |
167 | |
168 | ImageRef image = fop->sequenceImage((doc::PixelFormat)spec.colorMode(), |
169 | spec.width(), |
170 | spec.height()); |
171 | if (!image) |
172 | return false; |
173 | |
174 | tga::Image tgaImage; |
175 | tgaImage.pixels = image->getPixelAddress(0, 0); |
176 | tgaImage.rowstride = image->getRowStrideSize(); |
177 | tgaImage.bytesPerPixel = image->getRowStrideSize(1); |
178 | |
179 | // Read image |
180 | TgaDelegate delegate(fop); |
181 | if (!decoder.readImage(header, tgaImage, &delegate)) { |
182 | fop->setError("Error loading image data from TGA file.\n" ); |
183 | return false; |
184 | } |
185 | |
186 | // Fix alpha values for RGB images |
187 | decoder.postProcessImage(header, tgaImage); |
188 | |
189 | // Post process gray image pixels (because we use grayscale images |
190 | // with alpha). |
191 | if (header.isGray()) { |
192 | doc::LockImageBits<GrayscaleTraits> bits(image.get()); |
193 | for (auto it=bits.begin(), end=bits.end(); it != end; ++it) { |
194 | *it = doc::graya(*it, 255); |
195 | } |
196 | } |
197 | |
198 | if (decoder.hasAlpha()) |
199 | fop->sequenceSetHasAlpha(true); |
200 | |
201 | // Set default options for this TGA |
202 | auto opts = std::make_shared<TgaOptions>(); |
203 | opts->bitsPerPixel(header.bitsPerPixel); |
204 | opts->compress(header.isRle()); |
205 | fop->setLoadedFormatOptions(opts); |
206 | |
207 | if (ferror(handle.get())) { |
208 | fop->setError("Error reading file.\n" ); |
209 | return false; |
210 | } |
211 | else { |
212 | return true; |
213 | } |
214 | } |
215 | |
216 | #ifdef ENABLE_SAVE |
217 | |
218 | namespace { |
219 | |
220 | void (tga::Header& , |
221 | const doc::ImageSpec& spec, |
222 | const doc::Palette* palette, |
223 | const bool isOpaque, |
224 | const bool compressed, |
225 | int bitsPerPixel) |
226 | { |
227 | header.idLength = 0; |
228 | header.colormapType = 0; |
229 | header.imageType = tga::NoImage; |
230 | header.colormapOrigin = 0; |
231 | header.colormapLength = 0; |
232 | header.colormapDepth = 0; |
233 | header.xOrigin = 0; |
234 | header.yOrigin = 0; |
235 | header.width = spec.width(); |
236 | header.height = spec.height(); |
237 | header.bitsPerPixel = 0; |
238 | // TODO make this option configurable |
239 | header.imageDescriptor = 0x20; // Top-to-bottom |
240 | |
241 | switch (spec.colorMode()) { |
242 | case ColorMode::RGB: |
243 | header.imageType = (compressed ? tga::RleRgb: tga::UncompressedRgb); |
244 | header.bitsPerPixel = (bitsPerPixel > 8 ? |
245 | bitsPerPixel: |
246 | (isOpaque ? 24: 32)); |
247 | if (!isOpaque) { |
248 | switch (header.bitsPerPixel) { |
249 | case 16: header.imageDescriptor |= 1; break; |
250 | case 32: header.imageDescriptor |= 8; break; |
251 | } |
252 | } |
253 | break; |
254 | case ColorMode::GRAYSCALE: |
255 | // TODO if the grayscale is not opaque, we should use RGB, |
256 | // this could be done automatically in FileOp in a |
257 | // generic way for all formats when FILE_SUPPORT_RGBA is |
258 | // available and FILE_SUPPORT_GRAYA isn't. |
259 | header.imageType = (compressed ? tga::RleGray: tga::UncompressedGray); |
260 | header.bitsPerPixel = 8; |
261 | break; |
262 | case ColorMode::INDEXED: |
263 | ASSERT(palette); |
264 | |
265 | header.imageType = (compressed ? tga::RleIndexed: tga::UncompressedIndexed); |
266 | header.bitsPerPixel = 8; |
267 | header.colormapType = 1; |
268 | header.colormapLength = palette->size(); |
269 | if (palette->hasAlpha()) |
270 | header.colormapDepth = 32; |
271 | else |
272 | header.colormapDepth = 24; |
273 | |
274 | header.colormap = tga::Colormap(palette->size()); |
275 | for (int i=0; i<palette->size(); ++i) { |
276 | doc::color_t c = palette->getEntry(i); |
277 | header.colormap[i] = |
278 | tga::rgba(doc::rgba_getr(c), |
279 | doc::rgba_getg(c), |
280 | doc::rgba_getb(c), |
281 | doc::rgba_geta(c)); |
282 | } |
283 | break; |
284 | } |
285 | } |
286 | |
287 | } // anonymous namespace |
288 | |
289 | bool TgaFormat::onSave(FileOp* fop) |
290 | { |
291 | const FileAbstractImage* img = fop->abstractImage(); |
292 | const Palette* palette = fop->sequenceGetPalette(); |
293 | |
294 | FileHandle handle(open_file_with_exception_sync_on_close(fop->filename(), "wb" )); |
295 | tga::StdioFileInterface finterface(handle.get()); |
296 | tga::Encoder encoder(&finterface); |
297 | tga::Header ; |
298 | |
299 | const auto tgaOptions = std::static_pointer_cast<TgaOptions>(fop->formatOptions()); |
300 | prepare_header( |
301 | header, img->spec(), palette, |
302 | // Is alpha channel required? |
303 | fop->document()->sprite()->isOpaque(), |
304 | // Compressed by default |
305 | (tgaOptions ? tgaOptions->compress(): true), |
306 | // Bits per pixel (0 means "calculate what is best") |
307 | (tgaOptions ? tgaOptions->bitsPerPixel(): 0)); |
308 | |
309 | encoder.writeHeader(header); |
310 | |
311 | doc::ImageRef image = img->getScaledImage(); |
312 | tga::Image tgaImage; |
313 | tgaImage.pixels = image->getPixelAddress(0, 0); |
314 | tgaImage.rowstride = image->getRowStrideSize(); |
315 | tgaImage.bytesPerPixel = image->getRowStrideSize(1); |
316 | |
317 | TgaDelegate delegate(fop); |
318 | encoder.writeImage(header, tgaImage); |
319 | encoder.writeFooter(); |
320 | |
321 | if (ferror(handle.get())) { |
322 | fop->setError("Error writing file.\n" ); |
323 | return false; |
324 | } |
325 | else { |
326 | return true; |
327 | } |
328 | } |
329 | |
330 | #endif // ENABLE_SAVE |
331 | |
332 | FormatOptionsPtr TgaFormat::onAskUserForFormatOptions(FileOp* fop) |
333 | { |
334 | const bool origOpts = fop->hasFormatOptionsOfDocument(); |
335 | auto opts = fop->formatOptionsOfDocument<TgaOptions>(); |
336 | #ifdef ENABLE_UI |
337 | if (fop->context() && fop->context()->isUIAvailable()) { |
338 | try { |
339 | auto& pref = Preferences::instance(); |
340 | |
341 | // If the TGA options are not original from a TGA file, we can |
342 | // use the default options from the preferences. |
343 | if (!origOpts) { |
344 | if (pref.isSet(pref.tga.bitsPerPixel)) |
345 | opts->bitsPerPixel(pref.tga.bitsPerPixel()); |
346 | if (pref.isSet(pref.tga.compress)) |
347 | opts->compress(pref.tga.compress()); |
348 | } |
349 | |
350 | if (pref.tga.showAlert()) { |
351 | const bool isOpaque = fop->document()->sprite()->isOpaque(); |
352 | const std::string defBitsPerPixel = (isOpaque ? "24" : "32" ); |
353 | app::gen::TgaOptions win; |
354 | |
355 | if (fop->document()->colorMode() == doc::ColorMode::RGB) { |
356 | // TODO implement a better way to create ListItems with values |
357 | auto newItem = [](const char* s) -> ui::ListItem* { |
358 | auto item = new ui::ListItem(s); |
359 | item->setValue(s); |
360 | return item; |
361 | }; |
362 | |
363 | win.bitsPerPixel()->addItem(newItem("16" )); |
364 | win.bitsPerPixel()->addItem(newItem("24" )); |
365 | win.bitsPerPixel()->addItem(newItem("32" )); |
366 | |
367 | std::string v = defBitsPerPixel; |
368 | if (opts->bitsPerPixel() > 0) |
369 | v = base::convert_to<std::string>(opts->bitsPerPixel()); |
370 | win.bitsPerPixel()->setValue(v); |
371 | } |
372 | else { |
373 | win.bitsPerPixelLabel()->setVisible(false); |
374 | win.bitsPerPixel()->setVisible(false); |
375 | } |
376 | win.compress()->setSelected(opts->compress()); |
377 | |
378 | win.openWindowInForeground(); |
379 | |
380 | if (win.closer() == win.ok()) { |
381 | int bpp = base::convert_to<int>(win.bitsPerPixel()->getValue()); |
382 | |
383 | pref.tga.bitsPerPixel(bpp); |
384 | pref.tga.compress(win.compress()->isSelected()); |
385 | pref.tga.showAlert(!win.dontShow()->isSelected()); |
386 | |
387 | opts->bitsPerPixel(pref.tga.bitsPerPixel()); |
388 | opts->compress(pref.tga.compress()); |
389 | } |
390 | else { |
391 | opts.reset(); |
392 | } |
393 | } |
394 | } |
395 | catch (std::exception& e) { |
396 | Console::showException(e); |
397 | return std::shared_ptr<TgaOptions>(nullptr); |
398 | } |
399 | } |
400 | #endif // ENABLE_UI |
401 | return opts; |
402 | } |
403 | |
404 | } // namespace app |
405 | |