From 28248bf228e5da33a697d4c307cd16923537b29f Mon Sep 17 00:00:00 2001 From: David Capello Date: Thu, 19 Mar 2020 08:41:29 -0300 Subject: [PATCH 001/116] [lua] Add Tag.color property (fix https://github.com/aseprite/api/issues/24) --- src/app/script/api_version.h | 4 ++-- src/app/script/tag_class.cpp | 28 +++++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/app/script/api_version.h b/src/app/script/api_version.h index ec60e13cc..7c49ddfca 100644 --- a/src/app/script/api_version.h +++ b/src/app/script/api_version.h @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2018-2019 Igara Studio S.A. +// Copyright (C) 2018-2020 Igara Studio S.A. // // This program is distributed under the terms of // the End-User License Agreement for Aseprite. @@ -10,6 +10,6 @@ // Increment this value if the scripting API is modified between two // released Aseprite versions. -#define API_VERSION 9 +#define API_VERSION 10 #endif diff --git a/src/app/script/tag_class.cpp b/src/app/script/tag_class.cpp index 6c7791506..bffd158c1 100644 --- a/src/app/script/tag_class.cpp +++ b/src/app/script/tag_class.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2018-2019 Igara Studio S.A. +// Copyright (C) 2018-2020 Igara Studio S.A. // Copyright (C) 2018 David Capello // // This program is distributed under the terms of @@ -10,6 +10,7 @@ #endif #include "app/cmd/set_tag_anidir.h" +#include "app/cmd/set_tag_color.h" #include "app/cmd/set_tag_name.h" #include "app/cmd/set_tag_range.h" #include "app/script/docobj.h" @@ -82,6 +83,20 @@ int Tag_get_aniDir(lua_State* L) return 1; } +int Tag_get_color(lua_State* L) +{ + auto tag = get_docobj(L, 1); + doc::color_t docColor = tag->color(); + app::Color appColor = app::Color::fromRgb(doc::rgba_getr(docColor), + doc::rgba_getg(docColor), + doc::rgba_getb(docColor), + doc::rgba_geta(docColor)); + if (appColor.getAlpha() == 0) + appColor = app::Color::fromMask(); + push_obj(L, appColor); + return 1; +} + int Tag_set_fromFrame(lua_State* L) { auto tag = get_docobj(L, 1); @@ -127,6 +142,16 @@ int Tag_set_aniDir(lua_State* L) return 0; } +int Tag_set_color(lua_State* L) +{ + auto tag = get_docobj(L, 1); + doc::color_t docColor = convert_args_into_pixel_color(L, 2, doc::IMAGE_RGB); + Tx tx; + tx(new cmd::SetTagColor(tag, docColor)); + tx.commit(); + return 0; +} + const luaL_Reg Tag_methods[] = { { "__eq", Tag_eq }, { nullptr, nullptr } @@ -139,6 +164,7 @@ const Property Tag_properties[] = { { "frames", Tag_get_frames, nullptr }, { "name", Tag_get_name, Tag_set_name }, { "aniDir", Tag_get_aniDir, Tag_set_aniDir }, + { "color", Tag_get_color, Tag_set_color }, { nullptr, nullptr, nullptr } }; From 941219363bcd3371f96d4aa78de8ca78a192e103 Mon Sep 17 00:00:00 2001 From: David Capello Date: Thu, 19 Mar 2020 09:50:18 -0300 Subject: [PATCH 002/116] Keep "Export File" window position saved on preferences (fix #2300) --- src/app/commands/cmd_save_file.cpp | 8 ++++++-- src/app/ui/export_file_window.h | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/app/commands/cmd_save_file.cpp b/src/app/commands/cmd_save_file.cpp index 525fd215b..596589452 100644 --- a/src/app/commands/cmd_save_file.cpp +++ b/src/app/commands/cmd_save_file.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2019 Igara Studio S.A. +// Copyright (C) 2019-2020 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -363,8 +363,12 @@ void SaveFileCopyAsCommand::onExecute(Context* context) return result; }); + win.remapWindow(); + load_window_pos(&win, "ExportFile"); again:; - if (!win.show()) + const bool result = win.show(); + save_window_pos(&win, "ExportFile"); + if (!result) return; outputFilename = win.outputFilenameValue(); diff --git a/src/app/ui/export_file_window.h b/src/app/ui/export_file_window.h index 886fcbeb0..9aa1338a2 100644 --- a/src/app/ui/export_file_window.h +++ b/src/app/ui/export_file_window.h @@ -1,4 +1,5 @@ // Aseprite +// Copyright (C) 2020 Igara Studio S.A. // Copyright (C) 2018 David Capello // // This program is distributed under the terms of @@ -18,7 +19,7 @@ namespace app { class Doc; - class ExportFileWindow : protected app::gen::ExportFile { + class ExportFileWindow : public app::gen::ExportFile { public: ExportFileWindow(const Doc* doc); From de1019342a8f6b3fae9ff66384012c8024dc82a8 Mon Sep 17 00:00:00 2001 From: David Capello Date: Thu, 19 Mar 2020 12:13:51 -0300 Subject: [PATCH 003/116] Fixes for aseprite.desktop file (fix #2295) --- src/desktop/linux/aseprite.desktop | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/desktop/linux/aseprite.desktop b/src/desktop/linux/aseprite.desktop index 5381f3e4b..e23999db8 100644 --- a/src/desktop/linux/aseprite.desktop +++ b/src/desktop/linux/aseprite.desktop @@ -1,14 +1,13 @@ [Desktop Entry] Type=Application -Encoding=UTF-8 Name=Aseprite GenericName=Sprite Editor Comment=Animated sprite editor & pixel art tool Icon=aseprite -Categories=Graphics;2DGraphics;RasterGraphics; +Categories=Graphics;2DGraphics;RasterGraphics Exec=aseprite %U TryExec=aseprite Terminal=false StartupNotify=false -StartupWMClass=allegro +StartupWMClass=Aseprite MimeType=image/bmp;image/gif;image/jpeg;image/png;image/x-pcx;image/x-tga;image/vnd.microsoft.icon;video/x-flic;image/webp;image/x-aseprite; From 1826d66f10c67b8bc07a240cdd8b74e33ecdd390 Mon Sep 17 00:00:00 2001 From: David Capello Date: Thu, 19 Mar 2020 20:52:03 -0300 Subject: [PATCH 004/116] Fix loading some particular TGA files (multiple scanlines compressed, invalid alpha information, etc.) --- src/app/file/tga_format.cpp | 776 +++++++++++++++++++++--------------- 1 file changed, 461 insertions(+), 315 deletions(-) diff --git a/src/app/file/tga_format.cpp b/src/app/file/tga_format.cpp index fee37b2d2..192ec5bae 100644 --- a/src/app/file/tga_format.cpp +++ b/src/app/file/tga_format.cpp @@ -1,12 +1,9 @@ // Aseprite -// Copyright (C) 2019 Igara Studio S.A. +// Copyright (C) 2019-2020 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of // the End-User License Agreement for Aseprite. -// -// tga.c - Based on the code of Tim Gunn, Michal Mertl, Salvador -// Eduardo Tropea and Peter Wang. #ifdef HAVE_CONFIG_H #include "config.h" @@ -59,150 +56,431 @@ FileFormat* CreateTgaFormat() return new TgaFormat; } -static void rle_tga_read8(FILE* f, uint8_t* address, int w, int type) -{ - uint8_t* end = address + (w * (type == 1 ? 1: 2)); - uint8_t value; - int count, g; - int c = 0; +namespace { - do { - count = fgetc(f); - if (count & 0x80) { - count = (count & 0x7F) + 1; - c += count; - value = fgetc(f); - while (count--) { - if (type == 1) { - if (address+1 < end) - *(address++) = value; +class TgaDecoder { + struct Header { + uint8_t idLength; + uint8_t palType; + uint8_t imageType; + uint16_t palOrigin; + uint16_t palLength; + uint8_t palDepth; + uint16_t xOrigin; + uint16_t yOrigin; + uint16_t width; + uint16_t height; + uint8_t bitsPerPixel; + uint8_t imageDescriptor; + }; + + enum ImageType { + NoImage = 0, + UncompressedIndexed = 1, + UncompressedRgb = 2, + UncompressedGray = 3, + RleIndexed = 9, + RleRgb = 10, + RleGray = 11, + }; + +public: + TgaDecoder(FILE* f) + : m_f(f) + , m_palette(0, 0) + , m_alphaHistogram(256) { + readHeader(); + if (m_header.palType == 1) + readPaletteData(); + } + + int imageType() const { return m_header.imageType; } + int bitsPerPixel() const { return m_header.bitsPerPixel; } + bool hasPalette() const { return (m_header.palLength > 0); } + const Palette& palette() const { return m_palette; } + bool hasAlpha() const { return m_hasAlpha; } + + bool isGray() const { + return (m_header.imageType == UncompressedGray || + m_header.imageType == RleGray); + } + + bool validPalType() const { + return + // Indexed with palette + ((m_header.imageType == UncompressedIndexed || m_header.imageType == RleIndexed) && + m_header.bitsPerPixel == 8 && + m_header.palType == 1) || + // Grayscale without palette + ((m_header.imageType == UncompressedGray || m_header.imageType == RleGray) && + m_header.bitsPerPixel == 8 && + m_header.palType == 0) || + // Non-indexed without palette + (m_header.bitsPerPixel > 8 && + m_header.palType == 0); + } + + bool getImageSpec(ImageSpec& spec) const { + switch (m_header.imageType) { + + case UncompressedIndexed: + case RleIndexed: + if (m_header.bitsPerPixel != 8) + return false; + spec = ImageSpec(ColorMode::INDEXED, + m_header.width, + m_header.height); + return true; + + case UncompressedRgb: + case RleRgb: + if (m_header.bitsPerPixel != 15 && + m_header.bitsPerPixel != 16 && + m_header.bitsPerPixel != 24 && + m_header.bitsPerPixel != 32) + return false; + spec = ImageSpec(ColorMode::RGB, + m_header.width, + m_header.height); + return true; + + case UncompressedGray: + case RleGray: + if (m_header.bitsPerPixel != 8) + return false; + spec = ImageSpec(ColorMode::GRAYSCALE, + m_header.width, + m_header.height); + return true; + } + return false; + } + + bool readImageData(FileOp* fop, Image* image) { + // Bit 4 means right-to-left, else left-to-right + // Bit 5 means top-to-bottom, else bottom-to-top + m_iterator = ImageDataIterator( + image, + (m_header.imageDescriptor & 0x10) ? false: true, + (m_header.imageDescriptor & 0x20) ? true: false, + m_header.width, + m_header.height); + + for (int y=0; y(&TgaDecoder::fget8_as_index)) + return true; + break; + + case UncompressedRgb: + switch (m_header.bitsPerPixel) { + case 15: + case 16: + if (readUncompressedData(&TgaDecoder::fget16_as_rgba)) + return true; + break; + case 24: + if (readUncompressedData(&TgaDecoder::fget24_as_rgba)) + return true; + break; + case 32: + if (readUncompressedData(&TgaDecoder::fget32_as_rgba)) + return true; + break; + default: + ASSERT(false); + break; + } + break; + + case UncompressedGray: + ASSERT(m_header.bitsPerPixel == 8); + if (readUncompressedData(&TgaDecoder::fget8_as_gray)) + return true; + break; + + case RleIndexed: + ASSERT(m_header.bitsPerPixel == 8); + if (readRleData(&TgaDecoder::fget8_as_gray)) + return true; + break; + + case RleRgb: + switch (m_header.bitsPerPixel) { + case 15: + case 16: + if (readRleData(&TgaDecoder::fget16_as_rgba)) + return true; + break; + case 24: + if (readRleData(&TgaDecoder::fget24_as_rgba)) + return true; + break; + case 32: + if (readRleData(&TgaDecoder::fget32_as_rgba)) + return true; + break; + default: + ASSERT(false); + break; + } + break; + + case RleGray: + ASSERT(m_header.bitsPerPixel == 8); + if (readRleData(&TgaDecoder::fget8_as_gray)) + return true; + break; + } + fop->setProgress(float(y) / float(m_header.height)); + if (fop->isStop()) + break; + } + + return true; + } + + // Fix alpha channel for images with invalid alpha channel values + void postProcessImageData(Image* image) { + if (image->colorMode() != ColorMode::RGB || + !m_hasAlpha) + return; + + int count = 0; + for (int i=0; i<256; ++i) + if (m_alphaHistogram[i] > 0) + ++count; + + // If all pixels are transparent (alpha=0), make all pixels opaque + // (alpha=255). + if (count == 1 && m_alphaHistogram[0] > 0) { + LockImageBits bits(image); + auto it = bits.begin(), end = bits.end(); + for (; it != end; ++it) { + color_t c = *it; + *it = rgba(rgba_getr(c), + rgba_getg(c), + rgba_getb(c), 255); + } + } + } + +private: + class ImageDataIterator { + public: + ImageDataIterator() { } + + ImageDataIterator(Image* image, + bool leftToRight, + bool topToBottom, + int w, int h) { + m_image = image; + m_w = w; + m_h = h; + m_x = (leftToRight ? 0: w-1); + m_y = (topToBottom ? 0: h-1); + m_dx = (leftToRight ? +1: -1); + m_dy = (topToBottom ? +1: -1); + calcPtr(); + } + + template + bool next(const T value) { + *((T*)m_ptr) = value; + + m_x += m_dx; + m_ptr += m_dx*sizeof(T); + + if ((m_dx < 0 && m_x < 0) || + (m_dx > 0 && m_x == m_w)) { + m_x = (m_dx > 0 ? 0: m_w-1); + m_y += m_dy; + if ((m_dy < 0 && m_y < 0) || + (m_dy > 0 && m_y == m_h)) { + return true; } - else { - if (address+2 < end) - *((uint16_t*)address) = graya(value, 255); - address += 2; + calcPtr(); + } + return false; + } + + private: + void calcPtr() { + m_ptr = m_image->getPixelAddress(m_x, m_y); + } + + Image* m_image; + int m_x, m_y; + int m_w, m_h; + int m_dx, m_dy; + uint8_t* m_ptr; + }; + + void readHeader() { + m_header.idLength = fgetc(m_f); + m_header.palType = fgetc(m_f); + m_header.imageType = fgetc(m_f); + m_header.palOrigin = fgetw(m_f); + m_header.palLength = fgetw(m_f); + m_header.palDepth = fgetc(m_f); + m_header.xOrigin = fgetw(m_f); + m_header.yOrigin = fgetw(m_f); + m_header.width = fgetw(m_f); + m_header.height = fgetw(m_f); + m_header.bitsPerPixel = fgetc(m_f); + m_header.imageDescriptor = fgetc(m_f); + + char imageId[256]; + if (m_header.idLength > 0) + fread(imageId, 1, m_header.idLength, m_f); + +#if 0 + // In the best case the "alphaBits" should be valid, but there are + // invalid TGA files out there which don't indicate the + // "alphaBits" correctly, so they could be 0 and use the alpha + // channel anyway on each pixel. + int alphaBits = (m_header.imageDescriptor & 15); + TRACEARGS("TGA: bitsPerPixel", (int)m_header.bitsPerPixel, + "alphaBits", alphaBits); + m_hasAlpha = + (m_header.bitsPerPixel == 32 && alphaBits == 8) || + (m_header.bitsPerPixel == 16 && alphaBits == 1); +#else + // So to detect if a 32bpp or 16bpp TGA image has alpha, we'll use + // the "m_alphaHistogram" to check if there are different alpha + // values. If there is only one alpha value (all 0 or all 255), + // we create an opaque image anyway. + m_hasAlpha = + (m_header.bitsPerPixel == 32) || + (m_header.bitsPerPixel == 16); +#endif + } + + void readPaletteData() { + m_palette.resize(m_header.palLength); + + for (int i=0; i> 10) & 0x1F), + scale_5bits_to_8bits((c >> 5) & 0x1F), + scale_5bits_to_8bits(c & 0x1F), 255)); + break; + } + + case 24: + case 32: { + const int b = fgetc(m_f); + const int g = fgetc(m_f); + const int r = fgetc(m_f); + int a; + if (m_header.palDepth == 32) + a = fgetc(m_f); + else + a = 255; + m_palette.setEntry(i, doc::rgba(r, g, b, a)); + break; } } } - else { - count++; - c += count; - if (type == 1) { - if (address+count < end) - fread(address, 1, count, f); - address += count; + } + + template + bool readUncompressedData(color_t (TgaDecoder::*readPixel)()) { + for (int x=0; x((this->*readPixel)())) + return true; + } + return false; + } + + // In the best case (TGA 2.0 spec) this should read just one + // scanline, but in old TGA versions (1.0) it was possible to save + // several scanlines with the same RLE data. + // + // Returns true when are are done. + template + bool readRleData(color_t (TgaDecoder::*readPixel)()) { + for (int x=0; x*readPixel)(); + while (c-- > 0) + if (m_iterator.next(pixel)) + return true; } else { - for (g=0; g 0) { + if (m_iterator.next((this->*readPixel)())) + return true; } } } - } while (c < w); -} + return false; + } -static void rle_tga_read32(FILE* f, uint32_t* address, int w, bool withAlpha) -{ - uint32_t* end = address + w; - unsigned char value[4]; - int count; - int c = 0; + color_t fget8_as_index() { + return fgetc(m_f); + } - do { - count = fgetc(f); - if (count & 0x80) { - count = (count & 0x7F) + 1; - c += count; - fread(value, 1, 4, f); - while (count--) { - if (address+1 < end) - *(address++) = rgba(value[2], value[1], value[0], - withAlpha ? value[3]: 255); - } - } + color_t fget8_as_gray() { + return doc::graya(fgetc(m_f), 255); + } + + color_t fget32_as_rgba() { + int b = fgetc(m_f); + int g = fgetc(m_f); + int r = fgetc(m_f); + uint8_t a = fgetc(m_f); + if (!m_hasAlpha) + a = 255; else { - count++; - c += count; - while (count--) { - fread(value, 1, 4, f); - if (address+1 < end) - *(address++) = rgba(value[2], value[1], value[0], - withAlpha ? value[3]: 255); - } + ++m_alphaHistogram[a]; } - } while (c < w); -} + return doc::rgba(r, g, b, a); + } -static void rle_tga_read24(FILE* f, uint32_t* address, int w) -{ - uint32_t* end = address + w; - unsigned char value[4]; - int count; - int c = 0; + color_t fget24_as_rgba() { + const int b = fgetc(m_f); + const int g = fgetc(m_f); + const int r = fgetc(m_f); + return doc::rgba(r, g, b, 255); + } - do { - count = fgetc(f); - if (count & 0x80) { - count = (count & 0x7F) + 1; - c += count; - fread(value, 1, 3, f); - while (count--) { - if (address+1 < end) - *(address++) = rgba(value[2], value[1], value[0], 255); - } + color_t fget16_as_rgba() { + const uint16_t v = fgetw(m_f); + uint8_t alpha = 255; + if (m_hasAlpha) { + if ((v & 0x8000) == 0) + alpha = 0; + ++m_alphaHistogram[alpha]; } - else { - count++; - c += count; - while (count--) { - fread(value, 1, 3, f); - if (address+1 < end) - *(address++) = rgba(value[2], value[1], value[0], 255); - } - } - } while (c < w); -} + return doc::rgba(scale_5bits_to_8bits((v >> 10) & 0x1F), + scale_5bits_to_8bits((v >> 5) & 0x1F), + scale_5bits_to_8bits(v & 0x1F), + alpha); + } -static void rle_tga_read16(uint32_t* address, int w, FILE *f) -{ - uint32_t* end = address + w; - unsigned int value; - uint32_t color; - int count; - int c = 0; + FILE* m_f; + Header m_header; + bool m_hasAlpha = false; + Palette m_palette; + ImageDataIterator m_iterator; + std::vector m_alphaHistogram; +}; - do { - count = fgetc(f); - if (count & 0x80) { - count = (count & 0x7F) + 1; - c += count; - value = fgetw(f); - color = rgba(scale_5bits_to_8bits((value >> 10) & 0x1F), - scale_5bits_to_8bits((value >> 5) & 0x1F), - scale_5bits_to_8bits(value & 0x1F), 255); - - while (count--) { - if (address+1 < end) - *(address++) = color; - } - } - else { - count++; - c += count; - while (count--) { - value = fgetw(f); - color = rgba(scale_5bits_to_8bits((value >> 10) & 0x1F), - scale_5bits_to_8bits((value >> 5) & 0x1F), - scale_5bits_to_8bits(value & 0x1F), 255); - - if (address+1 < end) - *(address++) = color; - } - } - } while (c < w); } // Loads a 256 color or 24 bit uncompressed TGA file, returning a bitmap @@ -210,192 +488,59 @@ static void rle_tga_read16(uint32_t* address, int w, FILE *f) // should be an array of at least 256 RGB structures). bool TgaFormat::onLoad(FileOp* fop) { - unsigned char image_id[256], image_palette[256][3], rgb[4]; - unsigned char id_length, palette_type, image_type, palette_entry_size; - unsigned char bpp, descriptor_bits; - short unsigned int palette_colors; - short unsigned int image_width, image_height; - unsigned int c, i, x, y, yc; - int compressed; - FileHandle handle(open_file_with_exception(fop->filename(), "rb")); FILE* f = handle.get(); - id_length = fgetc(f); - palette_type = fgetc(f); - image_type = fgetc(f); - fgetw(f); // first_color - palette_colors = fgetw(f); - palette_entry_size = fgetc(f); - fgetw(f); // "left" field - fgetw(f); // "top" field - image_width = fgetw(f); - image_height = fgetw(f); - bpp = fgetc(f); - descriptor_bits = fgetc(f); + TgaDecoder decoder(f); - fread(image_id, 1, id_length, f); - - if (palette_type == 1) { - for (i=0; i> 5) & 0x1F); - image_palette[i][2] = scale_5bits_to_8bits((c >> 10) & 0x1F); - break; - - case 24: - case 32: - image_palette[i][0] = fgetc(f); - image_palette[i][1] = fgetc(f); - image_palette[i][2] = fgetc(f); - if (palette_entry_size == 32) - fgetc(f); - break; - } - } - } - else if (palette_type != 0) { + ImageSpec spec(ColorMode::RGB, 1, 1); + if (!decoder.getImageSpec(spec)) { + fop->setError("Unsupported color depth in TGA file: %d bpp, image type=%d.\n", + decoder.bitsPerPixel(), + decoder.imageType()); return false; } - /* Image type: - * 0 = no image data - * 1 = uncompressed color mapped - * 2 = uncompressed true color - * 3 = grayscale - * 9 = RLE color mapped - * 10 = RLE true color - * 11 = RLE grayscale - */ - compressed = (image_type & 8); - image_type &= 7; - - PixelFormat pixelFormat; - bool withAlpha = false; - - switch (image_type) { - - /* paletted image */ - case 1: - if ((palette_type != 1) || (bpp != 8)) { - return false; - } - - for (i=0; isequenceSetColor(i, - image_palette[i][2], - image_palette[i][1], - image_palette[i][0]); - } - - pixelFormat = IMAGE_INDEXED; - break; - - /* truecolor image */ - case 2: - if ((palette_type != 0) || - ((bpp != 15) && (bpp != 16) && - (bpp != 24) && (bpp != 32))) { - return false; - } - withAlpha = ((descriptor_bits & 0xf) == 8); - if (withAlpha) - fop->sequenceSetHasAlpha(true); - pixelFormat = IMAGE_RGB; - break; - - /* grayscale image */ - case 3: - if ((palette_type != 0) || (bpp != 8)) { - return false; - } - - for (i=0; i<256; i++) - fop->sequenceSetColor(i, i, i, i); - - pixelFormat = IMAGE_GRAYSCALE; - break; - - default: - /* TODO add support for more TGA types? */ - return false; + if (!decoder.validPalType()) { + fop->setError("Invalid palette type in TGA file.\n"); + return false; } - Image* image = fop->sequenceImage(pixelFormat, image_width, image_height); + // Palette from TGA file + if (decoder.hasPalette()) { + const Palette& pal = decoder.palette(); + for (int i=0; isequenceSetColor(i, + doc::rgba_getr(c), + doc::rgba_getg(c), + doc::rgba_getb(c)); + if (doc::rgba_geta(c) < 255) + fop->sequenceSetAlpha(i, doc::rgba_geta(c)); + } + } + // Generate grayscale palette + else if (decoder.isGray()) { + for (int i=0; i<256; ++i) + fop->sequenceSetColor(i, i, i, i); + } + + if (decoder.hasAlpha()) + fop->sequenceSetHasAlpha(true); + + Image* image = fop->sequenceImage((doc::PixelFormat)spec.colorMode(), + spec.width(), + spec.height()); if (!image) return false; - for (y=image_height; y; y--) { - yc = (descriptor_bits & 0x20) ? image_height-y : y-1; - - switch (image_type) { - - case 1: - case 3: - if (compressed) - rle_tga_read8(f, image->getPixelAddress(0, yc), image_width, image_type); - else if (image_type == 1) - fread(image->getPixelAddress(0, yc), 1, image_width, f); - else { - for (x=0; x(image, x, yc, graya(fgetc(f), 255)); - } - break; - - case 2: - if (bpp == 32) { - if (compressed) { - rle_tga_read32(f, (uint32_t*)image->getPixelAddress(0, yc), image_width, - withAlpha); - } - else { - for (x=0; x(image, x, yc, - rgba(rgb[2], rgb[1], rgb[0], - withAlpha ? rgb[3]: 255)); - } - } - } - else if (bpp == 24) { - if (compressed) { - rle_tga_read24(f, (uint32_t*)image->getPixelAddress(0, yc), image_width); - } - else { - for (x=0; x(image, x, yc, rgba(rgb[2], rgb[1], rgb[0], 255)); - } - } - } - else { - if (compressed) { - rle_tga_read16((uint32_t*)image->getPixelAddress(0, yc), image_width, f); - } - else { - for (x=0; x( - image, x, yc, rgba(scale_5bits_to_8bits((c >> 10) & 0x1F), - scale_5bits_to_8bits((c >> 5) & 0x1F), - scale_5bits_to_8bits(c & 0x1F), 255)); - } - } - } - break; - } - - if (image_height > 1) { - fop->setProgress((float)(image_height-y) / (float)(image_height)); - if (fop->isStop()) - break; - } + if (!decoder.readImageData(fop, image)) { + fop->setError("Error loading image data from TGA file.\n"); + return false; } + decoder.postProcessImageData(image); + if (ferror(f)) { fop->setError("Error reading file.\n"); return false; @@ -419,24 +564,25 @@ bool TgaFormat::onSave(FileOp* fop) FileHandle handle(open_file_with_exception_sync_on_close(fop->filename(), "wb")); FILE* f = handle.get(); - fputc(0, f); /* id length (no id saved) */ - fputc((need_pal) ? 1 : 0, f); /* palette type */ - /* image type */ + fputc(0, f); // id length (no id saved) + fputc((need_pal) ? 1 : 0, f); // palette type + // image type fputc((image->pixelFormat() == IMAGE_RGB ) ? 2 : (image->pixelFormat() == IMAGE_GRAYSCALE) ? 3 : (image->pixelFormat() == IMAGE_INDEXED ) ? 1 : 0, f); - fputw(0, f); /* first colour */ - fputw((need_pal) ? 256 : 0, f); /* number of colours */ - fputc((need_pal) ? 24 : 0, f); /* palette entry size */ - fputw(0, f); /* left */ - fputw(0, f); /* top */ - fputw(image->width(), f); /* width */ - fputw(image->height(), f); /* height */ - fputc(depth, f); /* bits per pixel */ + fputw(0, f); // first colour + fputw((need_pal) ? 256 : 0, f); // number of colours + fputc((need_pal) ? 24 : 0, f); // palette entry size + fputw(0, f); // left + fputw(0, f); // top + fputw(image->width(), f); // width + fputw(image->height(), f); // height + fputc(depth, f); // bits per pixel - /* descriptor (bottom to top, 8-bit alpha) */ - fputc(image->pixelFormat() == IMAGE_RGB && - !fop->document()->sprite()->isOpaque() ? 8: 0, f); + // descriptor (bottom to top, 8-bit alpha) + fputc( + (image->pixelFormat() == IMAGE_RGB && + !fop->document()->sprite()->isOpaque() ? 8: 0), f); if (need_pal) { for (y=0; y<256; y++) { From 3b9274b9b51db9ce9fe9fd6823c2b53993e7fd9e Mon Sep 17 00:00:00 2001 From: David Capello Date: Fri, 20 Mar 2020 12:35:27 -0300 Subject: [PATCH 005/116] Improve TGA encoder to save files compressed --- src/app/file/file.h | 3 +- src/app/file/tga_format.cpp | 404 +++++++++++++++++++++++++++--------- 2 files changed, 304 insertions(+), 103 deletions(-) diff --git a/src/app/file/file.h b/src/app/file/file.h index 4fce89008..16decadfd 100644 --- a/src/app/file/file.h +++ b/src/app/file/file.h @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2018-2019 Igara Studio S.A. +// Copyright (C) 2018-2020 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -187,6 +187,7 @@ namespace app { void sequenceGetAlpha(int index, int* a) const; Image* sequenceImage(PixelFormat pixelFormat, int w, int h); const Image* sequenceImage() const { return m_seq.image.get(); } + const Palette* sequenceGetPalette() const { return m_seq.palette; } bool sequenceGetHasAlpha() const { return m_seq.has_alpha; } diff --git a/src/app/file/tga_format.cpp b/src/app/file/tga_format.cpp index 192ec5bae..b93132e1f 100644 --- a/src/app/file/tga_format.cpp +++ b/src/app/file/tga_format.cpp @@ -58,32 +58,32 @@ FileFormat* CreateTgaFormat() namespace { +enum TgaImageType { + NoImage = 0, + UncompressedIndexed = 1, + UncompressedRgb = 2, + UncompressedGray = 3, + RleIndexed = 9, + RleRgb = 10, + RleGray = 11, +}; + +struct TgaHeader { + uint8_t idLength; + uint8_t palType; + uint8_t imageType; + uint16_t palOrigin; + uint16_t palLength; + uint8_t palDepth; + uint16_t xOrigin; + uint16_t yOrigin; + uint16_t width; + uint16_t height; + uint8_t bitsPerPixel; + uint8_t imageDescriptor; +}; + class TgaDecoder { - struct Header { - uint8_t idLength; - uint8_t palType; - uint8_t imageType; - uint16_t palOrigin; - uint16_t palLength; - uint8_t palDepth; - uint16_t xOrigin; - uint16_t yOrigin; - uint16_t width; - uint16_t height; - uint8_t bitsPerPixel; - uint8_t imageDescriptor; - }; - - enum ImageType { - NoImage = 0, - UncompressedIndexed = 1, - UncompressedRgb = 2, - UncompressedGray = 3, - RleIndexed = 9, - RleRgb = 10, - RleGray = 11, - }; - public: TgaDecoder(FILE* f) : m_f(f) @@ -463,7 +463,7 @@ private: const uint16_t v = fgetw(m_f); uint8_t alpha = 255; if (m_hasAlpha) { - if ((v & 0x8000) == 0) + if ((v & 0x8000) == 0) // Transparent bit alpha = 0; ++m_alphaHistogram[alpha]; } @@ -474,18 +474,15 @@ private: } FILE* m_f; - Header m_header; + TgaHeader m_header; bool m_hasAlpha = false; Palette m_palette; ImageDataIterator m_iterator; std::vector m_alphaHistogram; }; -} +} // anonymous namespace -// Loads a 256 color or 24 bit uncompressed TGA file, returning a bitmap -// structure and storing the palette data in the specified palette (this -// should be an array of at least 256 RGB structures). bool TgaFormat::onLoad(FileOp* fop) { FileHandle handle(open_file_with_exception(fop->filename(), "rb")); @@ -551,86 +548,288 @@ bool TgaFormat::onLoad(FileOp* fop) } #ifdef ENABLE_SAVE -// Writes a bitmap into a TGA file, using the specified palette (this -// should be an array of at least 256 RGB structures). + +namespace { + +class TgaEncoder { +public: + TgaEncoder(FILE* f) + : m_f(f) { + } + + void prepareHeader(const Image* image, + const Palette* palette, + const bool isOpaque, + const bool compressed) { + m_header.idLength = 0; + m_header.palType = 0; + m_header.imageType = NoImage; + m_header.palOrigin = 0; + m_header.palLength = 0; + m_header.palDepth = 0; + m_header.xOrigin = 0; + m_header.yOrigin = 0; + m_header.width = image->width(); + m_header.height = image->height(); + // TODO make these options configurable + m_header.bitsPerPixel = 0; + m_header.imageDescriptor = 0x20; // Top-to-bottom + + switch (image->colorMode()) { + case ColorMode::RGB: + m_header.imageType = (compressed ? RleRgb: UncompressedRgb); + m_header.bitsPerPixel = (isOpaque ? 24: 32); + m_header.imageDescriptor |= (isOpaque ? 0: 8); + break; + case ColorMode::GRAYSCALE: + // TODO if the grayscale is not opaque, we should use RGB, + // this could be done automatically in FileOp in a + // generic way for all formats when FILE_SUPPORT_RGBA is + // available and FILE_SUPPORT_GRAYA isn't. + m_header.imageType = (compressed ? RleGray: UncompressedGray); + m_header.bitsPerPixel = 8; + break; + case ColorMode::INDEXED: + ASSERT(palette); + + m_header.imageType = (compressed ? RleIndexed: UncompressedIndexed); + m_header.bitsPerPixel = 8; + m_header.palType = 1; + m_header.palLength = palette->size(); + if (palette->hasAlpha()) + m_header.palDepth = 32; + else + m_header.palDepth = 24; + break; + } + } + + bool needsPalette() const { + return (m_header.palType == 1); + } + + void writeHeader() { + fputc(m_header.idLength, m_f); + fputc(m_header.palType, m_f); + fputc(m_header.imageType, m_f); + fputw(m_header.palOrigin, m_f); + fputw(m_header.palLength, m_f); + fputc(m_header.palDepth, m_f); + fputw(m_header.xOrigin, m_f); + fputw(m_header.yOrigin, m_f); + fputw(m_header.width, m_f); + fputw(m_header.height, m_f); + fputc(m_header.bitsPerPixel, m_f); + fputc(m_header.imageDescriptor, m_f); + } + + void writeFooter() { + const char* tga2_footer = "\0\0\0\0\0\0\0\0TRUEVISION-XFILE.\0"; + fwrite(tga2_footer, 1, 26, m_f); + } + + void writePalette(const Palette* palette) { + ASSERT(palette->size() == m_header.palLength); + + for (int i=0; isize(); ++i) { + color_t c = palette->getEntry(i); + fputc(rgba_getb(c), m_f); + fputc(rgba_getg(c), m_f); + fputc(rgba_getr(c), m_f); + if (m_header.palDepth == 32) + fputc(rgba_geta(c), m_f); + } + } + + void writeImageData(FileOp* fop, const Image* image) { + switch (m_header.imageType) { + + case UncompressedIndexed: { + for (int y=0; yheight(); ++y) { + for (int x=0; xwidth(); ++x) { + color_t c = get_pixel_fast(image, x, y); + fputc(c, m_f); + } + fop->setProgress(float(y) / float(image->height())); + } + break; + } + + case UncompressedRgb: { + for (int y=0; yheight(); ++y) { + for (int x=0; xwidth(); ++x) { + color_t c = get_pixel_fast(image, x, y); + fputc(rgba_getb(c), m_f); + fputc(rgba_getg(c), m_f); + fputc(rgba_getr(c), m_f); + fputc(rgba_geta(c), m_f); + } + fop->setProgress(float(y) / float(image->height())); + } + break; + } + + case UncompressedGray: { + for (int y=0; yheight(); ++y) { + for (int x=0; xwidth(); ++x) { + color_t c = get_pixel_fast(image, x, y); + fputc(graya_getv(c), m_f); + } + fop->setProgress(float(y) / float(image->height())); + } + break; + } + + case RleIndexed: { + for (int y=0; yheight(); ++y) { + writeRleScanline(image, y, &TgaEncoder::fput8); + fop->setProgress(float(y) / float(image->height())); + } + break; + } + + case RleRgb: { + ASSERT(m_header.bitsPerPixel == 16 || + m_header.bitsPerPixel == 24 || + m_header.bitsPerPixel == 32); + + auto fput = (m_header.bitsPerPixel == 32 ? &TgaEncoder::fput32: + m_header.bitsPerPixel == 24 ? &TgaEncoder::fput24: + &TgaEncoder::fput16); + + for (int y=0; yheight(); ++y) { + writeRleScanline(image, y, fput); + fop->setProgress(float(y) / float(image->height())); + } + break; + } + + case RleGray: { + for (int y=0; yheight(); ++y) { + writeRleScanline(image, y, &TgaEncoder::fput8); + fop->setProgress(float(y) / float(image->height())); + } + break; + } + + } + } + +private: + template + void writeRleScanline(const Image* image, int y, + void (TgaEncoder::*writePixel)(color_t)) { + int x = 0; + while (x < image->width()) { + int count, offset; + countRepeatedPixels(image, x, y, offset, count); + + // Save a sequence of pixels with different colors + while (offset > 0) { + const int n = std::min(offset, 128); + + fputc(n - 1, m_f); + for (int i=0; i(image, x++, y); + (this->*writePixel)(c); + } + offset -= n; + } + + // Save a sequence of pixels with the same color + while (count*ImageTraits::bytes_per_pixel > 1+ImageTraits::bytes_per_pixel) { + const int n = std::min(count, 128); + const color_t c = get_pixel_fast(image, x, y); + +#if _DEBUG + for (int i=0; i(image, x+i, y) == c); + } +#endif + fputc(0x80 | (n - 1), m_f); + (this->*writePixel)(c); + count -= n; + x += n; + } + } + ASSERT(x == image->width()); + } + + template + void countRepeatedPixels(const Image* image, int x0, int y, + int& offset, int& count) { + for (int x=x0; xwidth(); ) { + color_t p = get_pixel_fast(image, x, y); + + int u = x+1; + for (; uwidth(); ++u) { + color_t q = get_pixel_fast(image, u, y); + if (p != q) + break; + } + + if ((u - x)*ImageTraits::bytes_per_pixel > 1+ImageTraits::bytes_per_pixel) { + offset = x - x0; + count = u - x; + return; + } + + x = u; + } + + offset = image->width() - x0; + count = 0; + } + + void fput8(color_t c) { + fputc(c, m_f); + } + + void fput16(color_t c) { + uint16_t v = 0; + fputw(v, m_f); + } + + void fput24(color_t c) { + fputc(rgba_getb(c), m_f); + fputc(rgba_getg(c), m_f); + fputc(rgba_getr(c), m_f); + } + + void fput32(color_t c) { + fputc(rgba_getb(c), m_f); + fputc(rgba_getg(c), m_f); + fputc(rgba_getr(c), m_f); + fputc(rgba_geta(c), m_f); + } + + FILE* m_f; + TgaHeader m_header; +}; + +} // anonymous namespace + bool TgaFormat::onSave(FileOp* fop) { const Image* image = fop->sequenceImage(); - unsigned char image_palette[256][3]; - int x, y, c, r, g, b; - int depth = (image->pixelFormat() == IMAGE_RGB) ? 32 : 8; - bool need_pal = (image->pixelFormat() == IMAGE_INDEXED)? true: false; + const Palette* palette = fop->sequenceGetPalette(); FileHandle handle(open_file_with_exception_sync_on_close(fop->filename(), "wb")); FILE* f = handle.get(); - fputc(0, f); // id length (no id saved) - fputc((need_pal) ? 1 : 0, f); // palette type - // image type - fputc((image->pixelFormat() == IMAGE_RGB ) ? 2 : - (image->pixelFormat() == IMAGE_GRAYSCALE) ? 3 : - (image->pixelFormat() == IMAGE_INDEXED ) ? 1 : 0, f); - fputw(0, f); // first colour - fputw((need_pal) ? 256 : 0, f); // number of colours - fputc((need_pal) ? 24 : 0, f); // palette entry size - fputw(0, f); // left - fputw(0, f); // top - fputw(image->width(), f); // width - fputw(image->height(), f); // height - fputc(depth, f); // bits per pixel + TgaEncoder encoder(f); - // descriptor (bottom to top, 8-bit alpha) - fputc( - (image->pixelFormat() == IMAGE_RGB && - !fop->document()->sprite()->isOpaque() ? 8: 0), f); + encoder.prepareHeader(image, + palette, + fop->document()->sprite()->isOpaque(), + true); // Always compressed? + encoder.writeHeader(); - if (need_pal) { - for (y=0; y<256; y++) { - fop->sequenceGetColor(y, &r, &g, &b); - image_palette[y][2] = r; - image_palette[y][1] = g; - image_palette[y][0] = b; - } - fwrite(image_palette, 1, 768, f); - } + if (encoder.needsPalette()) + encoder.writePalette(palette); - switch (image->pixelFormat()) { - - case IMAGE_RGB: - for (y=image->height()-1; y>=0; y--) { - for (x=0; xwidth(); x++) { - c = get_pixel(image, x, y); - fputc(rgba_getb(c), f); - fputc(rgba_getg(c), f); - fputc(rgba_getr(c), f); - fputc(rgba_geta(c), f); - } - - fop->setProgress((float)(image->height()-y) / (float)(image->height())); - } - break; - - case IMAGE_GRAYSCALE: - for (y=image->height()-1; y>=0; y--) { - for (x=0; xwidth(); x++) - fputc(graya_getv(get_pixel(image, x, y)), f); - - fop->setProgress((float)(image->height()-y) / (float)(image->height())); - } - break; - - case IMAGE_INDEXED: - for (y=image->height()-1; y>=0; y--) { - for (x=0; xwidth(); x++) - fputc(get_pixel(image, x, y), f); - - fop->setProgress((float)(image->height()-y) / (float)(image->height())); - } - break; - } - - const char* tga2_footer = "\0\0\0\0\0\0\0\0TRUEVISION-XFILE.\0"; - fwrite(tga2_footer, 1, 26, f); + encoder.writeImageData(fop, image); + encoder.writeFooter(); if (ferror(f)) { fop->setError("Error writing file.\n"); @@ -640,6 +839,7 @@ bool TgaFormat::onSave(FileOp* fop) return true; } } -#endif + +#endif // ENABLE_SAVE } // namespace app From ffdb5f2e42ab4cf9b8fe54eef56c59bd79b04133 Mon Sep 17 00:00:00 2001 From: David Capello Date: Fri, 20 Mar 2020 15:04:53 -0300 Subject: [PATCH 006/116] Update status bar and mouse cursor when editor scroll changes --- src/app/ui/editor/editor.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/app/ui/editor/editor.cpp b/src/app/ui/editor/editor.cpp index a4428b8f9..37c26d33a 100644 --- a/src/app/ui/editor/editor.cpp +++ b/src/app/ui/editor/editor.cpp @@ -2479,6 +2479,12 @@ void Editor::notifyScrollChanged() ASSERT(m_state); if (m_state) m_state->onScrollChange(this); + + // Update status bar and mouse cursor + if (hasMouse()) { + updateStatusBar(); + setCursor(ui::get_mouse_position()); + } } void Editor::notifyZoomChanged() From c230e8f0ab99a7d03bd78feef951d3ea8223c538 Mon Sep 17 00:00:00 2001 From: David Capello Date: Fri, 20 Mar 2020 19:18:03 -0300 Subject: [PATCH 007/116] Add options to save TGA files with 16/24/32bpp & uncompressed/compressed --- data/pref.xml | 7 +- data/strings/en.ini | 11 ++- data/widgets/options.xml | 15 +-- src/app/commands/cmd_options.cpp | 7 +- src/app/file/file.h | 4 + src/app/file/tga_format.cpp | 163 ++++++++++++++++++++++++++----- 6 files changed, 168 insertions(+), 39 deletions(-) diff --git a/data/pref.xml b/data/pref.xml index 57a5f2a26..e26f33032 100644 --- a/data/pref.xml +++ b/data/pref.xml @@ -1,6 +1,6 @@ - + @@ -325,6 +325,11 @@