aseprite/src/file/gif_format.cpp

671 lines
24 KiB
C++

/* ASEPRITE
* Copyright (C) 2001-2012 David Capello
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include "config.h"
#include "base/unique_ptr.h"
#include "document.h"
#include "file/file.h"
#include "file/file_format.h"
#include "file/format_options.h"
#include "modules/gui.h"
#include "raster/raster.h"
#include "ui/alert.h"
#include "util/autocrop.h"
#include <gif_lib.h>
enum DisposalMethod {
DISPOSAL_METHOD_NONE,
DISPOSAL_METHOD_DO_NOT_DISPOSE,
DISPOSAL_METHOD_RESTORE_BGCOLOR,
DISPOSAL_METHOD_RESTORE_PREVIOUS,
};
struct GifFrame
{
int x, y;
int duration;
int mask_index;
Image* image;
Palette* palette;
DisposalMethod disposal_method;
GifFrame()
: x(0), y(0)
, mask_index(-1)
, image(0)
, palette(0)
, disposal_method(DISPOSAL_METHOD_NONE) {
}
};
typedef std::vector<GifFrame> GifFrames;
struct GifData
{
int sprite_w;
int sprite_h;
int bgcolor_index;
GifFrames frames;
};
class GifFormat : public FileFormat
{
const char* onGetName() const { return "gif"; }
const char* onGetExtensions() const { return "gif"; }
int onGetFlags() const {
return
FILE_SUPPORT_LOAD |
FILE_SUPPORT_SAVE |
FILE_SUPPORT_RGB |
FILE_SUPPORT_RGBA |
FILE_SUPPORT_GRAY |
FILE_SUPPORT_GRAYA |
FILE_SUPPORT_INDEXED |
FILE_SUPPORT_FRAMES |
FILE_SUPPORT_PALETTES;
}
bool onLoad(FileOp* fop);
bool onPostLoad(FileOp* fop) OVERRIDE;
void onDestroyData(FileOp* fop) OVERRIDE;
bool onSave(FileOp* fop);
};
FileFormat* CreateGifFormat()
{
return new GifFormat;
}
static int interlaced_offset[] = { 0, 4, 2, 1 };
static int interlaced_jumps[] = { 8, 8, 4, 2 };
bool GifFormat::onLoad(FileOp* fop)
{
UniquePtr<GifFileType, int(*)(GifFileType*)> gif_file(DGifOpenFileName(fop->filename.c_str()),
DGifCloseFile);
if (!gif_file) {
fop_error(fop, "Error loading GIF header.\n");
return false;
}
GifData* data = new GifData;
fop->format_data = reinterpret_cast<void*>(data);
data->sprite_w = gif_file->SWidth;
data->sprite_h = gif_file->SHeight;
UniquePtr<Palette> current_palette(new Palette(FrameNumber(0), 256));
UniquePtr<Palette> previous_palette(new Palette(FrameNumber(0), 256));
// If the GIF image has a global palette, it has a valid
// background color (so the GIF is not transparent).
if (gif_file->SColorMap != NULL) {
data->bgcolor_index = gif_file->SBackGroundColor;
// Setup the first palette using the global color map.
ColorMapObject* colormap = gif_file->SColorMap;
for (int i=0; i<colormap->ColorCount; ++i) {
current_palette->setEntry(i, _rgba(colormap->Colors[i].Red,
colormap->Colors[i].Green,
colormap->Colors[i].Blue, 255));
}
}
else {
data->bgcolor_index = -1;
}
// Scan the content of the GIF file (read record by record)
GifRecordType record_type;
FrameNumber frame_num(0);
DisposalMethod disposal_method = DISPOSAL_METHOD_NONE;
int transparent_index = -1;
int frame_delay = -1;
do {
if (DGifGetRecordType(gif_file, &record_type) == GIF_ERROR)
throw base::Exception("Invalid GIF record in file.\n");
switch (record_type) {
case IMAGE_DESC_RECORD_TYPE: {
if (DGifGetImageDesc(gif_file) == GIF_ERROR)
throw base::Exception("Invalid GIF image descriptor.\n");
// These are the bounds of the image to read.
int frame_x = gif_file->Image.Left;
int frame_y = gif_file->Image.Top;
int frame_w = gif_file->Image.Width;
int frame_h = gif_file->Image.Height;
if (frame_x < 0 || frame_y < 0 ||
frame_x + frame_w > data->sprite_w ||
frame_y + frame_h > data->sprite_h)
throw base::Exception("Image %d is out of sprite bounds.\n", (int)frame_num);
// Add a new frames.
if (frame_num >= FrameNumber(data->frames.size()))
data->frames.resize(frame_num.next());
data->frames[frame_num].x = frame_x;
data->frames[frame_num].y = frame_y;
// Set frame delay (1/100th seconds to milliseconds)
if (frame_delay >= 0)
data->frames[frame_num].duration = frame_delay*10;
// Update palette for this frame (the first frame always need a palette).
if (gif_file->Image.ColorMap) {
ColorMapObject* colormap = gif_file->Image.ColorMap;
for (int i=0; i<colormap->ColorCount; ++i) {
current_palette->setEntry(i, _rgba(colormap->Colors[i].Red,
colormap->Colors[i].Green,
colormap->Colors[i].Blue, 255));
}
}
if (frame_num == 0 || previous_palette->countDiff(current_palette, NULL, NULL)) {
current_palette->setFrame(frame_num);
data->frames[frame_num].palette = new Palette(*current_palette);
data->frames[frame_num].palette->setFrame(frame_num);
current_palette->copyColorsTo(previous_palette);
}
// Create a temporary image to load frame pixels.
UniquePtr<Image> frame_image(Image::create(IMAGE_INDEXED, frame_w, frame_h));
IndexedTraits::address_t addr;
if (gif_file->Image.Interlace) {
// Need to perform 4 passes on the images.
for (int i=0; i<4; ++i)
for (int y = interlaced_offset[i]; y < frame_h; y += interlaced_jumps[i]) {
addr = image_address_fast<IndexedTraits>(frame_image, 0, y);
if (DGifGetLine(gif_file, addr, frame_w) == GIF_ERROR)
throw base::Exception("Invalid interlaced image data.");
}
}
else {
for (int y = 0; y < frame_h; ++y) {
addr = image_address_fast<IndexedTraits>(frame_image, 0, y);
if (DGifGetLine(gif_file, addr, frame_w) == GIF_ERROR)
throw base::Exception("Invalid image data (%d).\n", GifLastError());
}
}
// Detach the pointer of the frame-image and put it in the list of frames.
data->frames[frame_num].image = frame_image.release();
data->frames[frame_num].disposal_method = disposal_method;
data->frames[frame_num].mask_index = transparent_index;
PRINTF("Frame[%d] transparent index = %d\n", (int)frame_num, transparent_index);
++frame_num;
disposal_method = DISPOSAL_METHOD_NONE;
transparent_index = -1;
frame_delay = -1;
break;
}
case EXTENSION_RECORD_TYPE: {
GifByteType* extension;
int ext_code;
if (DGifGetExtension(gif_file, &ext_code, &extension) == GIF_ERROR)
throw base::Exception("Invalid GIF extension record.\n");
if (ext_code == GRAPHICS_EXT_FUNC_CODE) {
if (extension[0] >= 4) {
disposal_method = (DisposalMethod)((extension[1] >> 2) & 7);
transparent_index = (extension[1] & 1) ? extension[4]: -1;
frame_delay = (extension[3] << 8) | extension[2];
TRACE("Disposal method: %d\nTransparent index: %d\nFrame delay: %d\n",
disposal_method, transparent_index, frame_delay);
}
}
while (extension != NULL) {
if (DGifGetExtensionNext(gif_file, &extension) == GIF_ERROR)
throw base::Exception("Invalid GIF extension record.\n");
}
break;
}
case TERMINATE_RECORD_TYPE:
break;
default:
break;
}
// Just one frame?
if (frame_num > 0 && fop->oneframe)
break;
if (fop_is_stop(fop))
break;
} while (record_type != TERMINATE_RECORD_TYPE);
fop->document = new Document(NULL);
return true;
}
bool GifFormat::onPostLoad(FileOp* fop)
{
GifData* data = reinterpret_cast<GifData*>(fop->format_data);
if (!data)
return true;
PixelFormat pixelFormat = IMAGE_INDEXED;
bool askForConversion = false;
if (!fop->oneframe) {
int global_mask_index = -1;
for (GifFrames::iterator
frame_it=data->frames.begin(),
frame_end=data->frames.end(); frame_it != frame_end; ++frame_it) {
// Convert the indexed image to RGB
for (int y=0; y<frame_it->image->h; ++y) {
for (int x=0; x<frame_it->image->w; ++x) {
int pixel_index = image_getpixel_fast<IndexedTraits>(frame_it->image, x, y);
if (pixel_index >= 0 && pixel_index < 256) {
// This pixel matches the frame's transparent color
if (pixel_index == frame_it->mask_index) {
// If we haven't set a background color yet, this is our new background color.
if (global_mask_index < 0) {
global_mask_index = pixel_index;
}
}
else {
// Drawing the mask color
if (global_mask_index == pixel_index) {
askForConversion = true;
goto done;
}
}
}
}
}
}
// New background color
data->bgcolor_index = global_mask_index;
done:;
}
if (askForConversion) {
int result =
ui::Alert::show("GIF Conversion"
"<<The selected file: %s"
"<<is a transparent GIF image which uses multiple background colors."
"<<ASEPRITE cannot handle this kind of GIF correctly in Indexed format."
"<<What would you like to do?"
"||Convert to &RGBA||Keep &Indexed||&Cancel",
fop->document->getFilename());
if (result == 1)
pixelFormat = IMAGE_RGB;
else if (result != 2)
return false;
}
// Create the sprite with the GIF dimension
UniquePtr<Sprite> sprite(new Sprite(pixelFormat, data->sprite_w, data->sprite_h, 256));
// Create the main layer
LayerImage* layer = new LayerImage(sprite);
sprite->getFolder()->addLayer(layer);
if (pixelFormat == IMAGE_INDEXED) {
if (data->bgcolor_index >= 0)
sprite->setTransparentColor(data->bgcolor_index);
else
layer->configureAsBackground();
}
// The previous image is used to support the special disposal method
// of GIF frames DISPOSAL_METHOD_RESTORE_PREVIOUS (number 3 in
// Graphics Extension)
UniquePtr<Image> current_image(Image::create(pixelFormat, data->sprite_w, data->sprite_h));
UniquePtr<Image> previous_image(Image::create(pixelFormat, data->sprite_w, data->sprite_h));
// Clear both images with the transparent color (alpha = 0).
uint32_t bgcolor = (pixelFormat == IMAGE_RGB ? _rgba(0, 0, 0, 0):
(data->bgcolor_index >= 0 ? data->bgcolor_index: 0));
image_clear(current_image, bgcolor);
image_clear(previous_image, bgcolor);
// Add all frames in the sprite.
sprite->setTotalFrames(FrameNumber(data->frames.size()));
Palette* current_palette = NULL;
FrameNumber frame_num(0);
for (GifFrames::iterator
frame_it=data->frames.begin(),
frame_end=data->frames.end(); frame_it != frame_end; ++frame_it, ++frame_num) {
// Set frame duration
sprite->setFrameDuration(frame_num, frame_it->duration);
// Set frame palette
if (frame_it->palette) {
sprite->setPalette(frame_it->palette, true);
current_palette = frame_it->palette;
}
switch (pixelFormat) {
case IMAGE_INDEXED:
for (int y = 0; y < frame_it->image->h; ++y)
for (int x = 0; x < frame_it->image->w; ++x) {
int pixel_index = image_getpixel_fast<IndexedTraits>(frame_it->image, x, y);
if (pixel_index != frame_it->mask_index)
image_putpixel_fast<IndexedTraits>(current_image,
frame_it->x + x,
frame_it->y + y,
pixel_index);
}
break;
case IMAGE_RGB:
// Convert the indexed image to RGB
for (int y = 0; y < frame_it->image->h; ++y)
for (int x = 0; x < frame_it->image->w; ++x) {
int pixel_index = image_getpixel_fast<IndexedTraits>(frame_it->image, x, y);
if (pixel_index != frame_it->mask_index)
image_putpixel_fast<RgbTraits>(current_image,
frame_it->x + x,
frame_it->y + y,
current_palette->getEntry(pixel_index));
}
break;
}
// Create a new Cel and a image with the whole content of "current_image"
Cel* cel = new Cel(frame_num, 0);
try {
Image* cel_image = Image::createCopy(current_image);
try {
// Add the image in the sprite's stock and update the cel's
// reference to the new stock's image.
cel->setImage(sprite->getStock()->addImage(cel_image));
}
catch (...) {
delete cel_image;
throw;
}
layer->addCel(cel);
}
catch (...) {
delete cel;
throw;
}
// The current_image was already copied to represent the
// current frame (frame_num), so now we have to clear the
// area occupied by frame_image using the desired disposal
// method.
switch (frame_it->disposal_method) {
case DISPOSAL_METHOD_NONE:
case DISPOSAL_METHOD_DO_NOT_DISPOSE:
// Do nothing
break;
case DISPOSAL_METHOD_RESTORE_BGCOLOR:
image_rectfill(current_image,
frame_it->x,
frame_it->y,
frame_it->x+frame_it->image->w-1,
frame_it->y+frame_it->image->h-1,
bgcolor);
break;
case DISPOSAL_METHOD_RESTORE_PREVIOUS:
image_copy(current_image, previous_image, 0, 0);
break;
}
// Update previous_image with current_image only if the
// disposal method is not "restore previous" (which means
// that we have already updated current_image from
// previous_image).
if (frame_it->disposal_method != DISPOSAL_METHOD_RESTORE_PREVIOUS)
image_copy(previous_image, current_image, 0, 0);
}
fop->document->addSprite(sprite);
sprite.release(); // Now the sprite is owned by fop->document
return true;
}
void GifFormat::onDestroyData(FileOp* fop)
{
GifData* data = reinterpret_cast<GifData*>(fop->format_data);
if (data) {
GifFrames::iterator frame_it = data->frames.begin();
GifFrames::iterator frame_end = data->frames.end();
for (; frame_it != frame_end; ++frame_it) {
delete frame_it->image;
delete frame_it->palette;
}
delete data;
}
}
bool GifFormat::onSave(FileOp* fop)
{
UniquePtr<GifFileType, int(*)(GifFileType*)> gif_file(EGifOpenFileName(fop->filename.c_str(), 0),
EGifCloseFile);
if (!gif_file)
throw base::Exception("Error creating GIF file.\n");
Sprite* sprite = fop->document->getSprite();
int sprite_w = sprite->getWidth();
int sprite_h = sprite->getHeight();
PixelFormat sprite_format = sprite->getPixelFormat();
bool interlace = false;
int loop = 0;
int background_color = (sprite_format == IMAGE_INDEXED ? sprite->getTransparentColor(): 0);
int transparent_index = (sprite->getBackgroundLayer() ? -1: sprite->getTransparentColor());
Palette* current_palette = sprite->getPalette(FrameNumber(0));
Palette* previous_palette = current_palette;
ColorMapObject* color_map = MakeMapObject(current_palette->size(), NULL);
for (int i = 0; i < current_palette->size(); ++i) {
color_map->Colors[i].Red = _rgba_getr(current_palette->getEntry(i));
color_map->Colors[i].Green = _rgba_getg(current_palette->getEntry(i));
color_map->Colors[i].Blue = _rgba_getb(current_palette->getEntry(i));
}
if (EGifPutScreenDesc(gif_file, sprite_w, sprite_h,
color_map->BitsPerPixel,
background_color, color_map) == GIF_ERROR)
throw base::Exception("Error writing GIF header.\n");
UniquePtr<Image> buffer_image;
UniquePtr<Image> current_image(Image::create(IMAGE_INDEXED, sprite_w, sprite_h));
UniquePtr<Image> previous_image(Image::create(IMAGE_INDEXED, sprite_w, sprite_h));
int frame_x, frame_y, frame_w, frame_h;
int u1, v1, u2, v2;
int i1, j1, i2, j2;
// If the sprite is not Indexed type, we will need a temporary
// buffer to render the full RGB or Grayscale sprite.
if (sprite_format != IMAGE_INDEXED)
buffer_image.reset(Image::create(sprite_format, sprite_w, sprite_h));
image_clear(current_image, background_color);
image_clear(previous_image, background_color);
for (FrameNumber frame_num(0); frame_num<sprite->getTotalFrames(); ++frame_num) {
current_palette = sprite->getPalette(frame_num);
// If the sprite is RGB or Grayscale, we must to convert it to Indexed on the fly.
if (sprite_format != IMAGE_INDEXED) {
image_clear(buffer_image, 0);
layer_render(sprite->getFolder(), buffer_image, 0, 0, frame_num);
switch (sprite_format) {
// Convert the RGB image to Indexed
case IMAGE_RGB:
for (int y = 0; y < sprite_h; ++y)
for (int x = 0; x < sprite_w; ++x) {
uint32_t pixel_value = image_getpixel_fast<RgbTraits>(buffer_image, x, y);
image_putpixel_fast<IndexedTraits>(current_image, x, y,
(_rgba_geta(pixel_value) >= 128) ?
current_palette->findBestfit(_rgba_getr(pixel_value),
_rgba_getg(pixel_value),
_rgba_getb(pixel_value)):
transparent_index);
}
break;
// Convert the Grayscale image to Indexed
case IMAGE_GRAYSCALE:
for (int y = 0; y < sprite_h; ++y)
for (int x = 0; x < sprite_w; ++x) {
uint16_t pixel_value = image_getpixel_fast<GrayscaleTraits>(buffer_image, x, y);
image_putpixel_fast<IndexedTraits>(current_image, x, y,
(_graya_geta(pixel_value) >= 128) ?
current_palette->findBestfit(_graya_getv(pixel_value),
_graya_getv(pixel_value),
_graya_getv(pixel_value)):
transparent_index);
}
break;
}
}
// If the sprite is Indexed, we can render directly into "current_image".
else {
image_clear(current_image, background_color);
layer_render(sprite->getFolder(), current_image, 0, 0, frame_num);
}
if (frame_num == 0) {
frame_x = 0;
frame_y = 0;
frame_w = sprite->getWidth();
frame_h = sprite->getHeight();
}
else {
// Get the rectangle where start differences with the previous frame.
if (get_shrink_rect2(&u1, &v1, &u2, &v2, current_image, previous_image)) {
// Check the minimal area with the background color.
if (get_shrink_rect(&i1, &j1, &i2, &j2, current_image, background_color)) {
frame_x = MIN(u1, i1);
frame_y = MIN(v1, j1);
frame_w = MAX(u2, i2) - MIN(u1, i1) + 1;
frame_h = MAX(v2, j2) - MIN(v1, j1) + 1;
}
}
}
// Specify loop extension.
if (frame_num == 0 && loop >= 0) {
unsigned char extension_bytes[11];
memcpy(extension_bytes, "NETSCAPE2.0", 11);
if (EGifPutExtensionFirst(gif_file, APPLICATION_EXT_FUNC_CODE, 11, extension_bytes) == GIF_ERROR)
throw base::Exception("Error writing GIF graphics extension record for frame %d.\n", (int)frame_num);
extension_bytes[0] = 1;
extension_bytes[1] = (loop & 0xff);
extension_bytes[2] = (loop >> 8) & 0xff;
if (EGifPutExtensionNext(gif_file, APPLICATION_EXT_FUNC_CODE, 3, extension_bytes) == GIF_ERROR)
throw base::Exception("Error writing GIF graphics extension record for frame %d.\n", (int)frame_num);
if (EGifPutExtensionLast(gif_file, APPLICATION_EXT_FUNC_CODE, 0, NULL) == GIF_ERROR)
throw base::Exception("Error writing GIF graphics extension record for frame %d.\n", (int)frame_num);
}
// Write graphics extension record (to save the duration of the
// frame and maybe the transparency index).
{
unsigned char extension_bytes[5];
int disposal_method = (sprite->getBackgroundLayer() ? DISPOSAL_METHOD_DO_NOT_DISPOSE:
DISPOSAL_METHOD_RESTORE_BGCOLOR);
int frame_delay = sprite->getFrameDuration(frame_num) / 10;
extension_bytes[0] = (((disposal_method & 7) << 2) |
(transparent_index >= 0 ? 1: 0));
extension_bytes[1] = (frame_delay & 0xff);
extension_bytes[2] = (frame_delay >> 8) & 0xff;
extension_bytes[3] = (transparent_index >= 0 ? transparent_index: 0);
if (EGifPutExtension(gif_file, GRAPHICS_EXT_FUNC_CODE, 4, extension_bytes) == GIF_ERROR)
throw base::Exception("Error writing GIF graphics extension record for frame %d.\n", (int)frame_num);
}
// Image color map
ColorMapObject* image_color_map = NULL;
if (current_palette != previous_palette) {
image_color_map = MakeMapObject(current_palette->size(), NULL);
for (int i = 0; i < current_palette->size(); ++i) {
image_color_map->Colors[i].Red = _rgba_getr(current_palette->getEntry(i));
image_color_map->Colors[i].Green = _rgba_getg(current_palette->getEntry(i));
image_color_map->Colors[i].Blue = _rgba_getb(current_palette->getEntry(i));
}
previous_palette = current_palette;
}
// Write the image record.
if (EGifPutImageDesc(gif_file,
frame_x, frame_y,
frame_w, frame_h, interlace ? 1: 0,
image_color_map) == GIF_ERROR)
throw base::Exception("Error writing GIF frame %d.\n", (int)frame_num);
// Write the image data (pixels).
if (interlace) {
// Need to perform 4 passes on the images.
for (int i=0; i<4; ++i)
for (int y = interlaced_offset[i]; y < frame_h; y += interlaced_jumps[i]) {
IndexedTraits::address_t addr = image_address_fast<IndexedTraits>(current_image, frame_x, frame_y + y);
if (EGifPutLine(gif_file, addr, frame_w) == GIF_ERROR)
throw base::Exception("Error writing GIF image scanlines for frame %d.\n", (int)frame_num);
}
}
else {
// Write all image scanlines (not interlaced in this case).
for (int y = 0; y < frame_h; ++y) {
IndexedTraits::address_t addr = image_address_fast<IndexedTraits>(current_image, frame_x, frame_y + y);
if (EGifPutLine(gif_file, addr, frame_w) == GIF_ERROR)
throw base::Exception("Error writing GIF image scanlines for frame %d.\n", (int)frame_num);
}
}
image_copy(previous_image, current_image, 0, 0);
}
return true;
}