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
29namespace app {
30
31using namespace base;
32
33class 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
68FileFormat* CreateTgaFormat()
69{
70 return new TgaFormat;
71}
72
73namespace {
74
75class TgaDelegate : public tga::Delegate {
76public:
77 TgaDelegate(FileOp* fop) : m_fop(fop) { }
78 bool notifyProgress(double progress) override {
79 m_fop->setProgress(progress);
80 return !m_fop->isStop();
81 }
82private:
83 FileOp* m_fop;
84};
85
86bool get_image_spec(const tga::Header& 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
125bool 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 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
218namespace {
219
220void prepare_header(tga::Header& 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
289bool 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 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
332FormatOptionsPtr 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