mirror of
https://github.com/aseprite/aseprite.git
synced 2025-04-09 18:44:46 +00:00
846 lines
27 KiB
C++
846 lines
27 KiB
C++
// Aseprite
|
|
// Copyright (C) 2001-2015 David Capello
|
|
//
|
|
// This program is free software; you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License version 2 as
|
|
// published by the Free Software Foundation.
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "config.h"
|
|
#endif
|
|
|
|
#include "app/console.h"
|
|
#include "app/context.h"
|
|
#include "app/document.h"
|
|
#include "app/file/file.h"
|
|
#include "app/file/file_format.h"
|
|
#include "app/file/format_options.h"
|
|
#include "app/file/gif_options.h"
|
|
#include "app/ini_file.h"
|
|
#include "app/modules/gui.h"
|
|
#include "app/util/autocrop.h"
|
|
#include "base/file_handle.h"
|
|
#include "base/unique_ptr.h"
|
|
#include "doc/doc.h"
|
|
#include "render/quantization.h"
|
|
#include "render/render.h"
|
|
#include "ui/alert.h"
|
|
#include "ui/button.h"
|
|
|
|
#include "generated_gif_options.h"
|
|
|
|
#include <gif_lib.h>
|
|
|
|
namespace app {
|
|
|
|
using namespace base;
|
|
|
|
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 |
|
|
FILE_SUPPORT_GET_FORMAT_OPTIONS;
|
|
}
|
|
|
|
bool onLoad(FileOp* fop);
|
|
bool onPostLoad(FileOp* fop) override;
|
|
void onDestroyData(FileOp* fop) override;
|
|
#ifdef ENABLE_SAVE
|
|
bool onSave(FileOp* fop) override;
|
|
#endif
|
|
base::SharedPtr<FormatOptions> onGetFormatOptions(FileOp* fop) override;
|
|
};
|
|
|
|
FileFormat* CreateGifFormat()
|
|
{
|
|
return new GifFormat;
|
|
}
|
|
|
|
static int interlaced_offset[] = { 0, 4, 2, 1 };
|
|
static int interlaced_jumps[] = { 8, 8, 4, 2 };
|
|
|
|
struct GifFilePtr {
|
|
public:
|
|
typedef int (*CloseFunc)(GifFileType*, int*);
|
|
|
|
GifFilePtr(GifFileType* ptr, CloseFunc closeFunc) :
|
|
m_ptr(ptr), m_closeFunc(closeFunc) {
|
|
}
|
|
|
|
~GifFilePtr() {
|
|
int errCode;
|
|
m_closeFunc(m_ptr, &errCode);
|
|
}
|
|
|
|
operator GifFileType*() {
|
|
return m_ptr;
|
|
}
|
|
|
|
GifFileType* operator->() {
|
|
return m_ptr;
|
|
}
|
|
|
|
private:
|
|
GifFileType* m_ptr;
|
|
CloseFunc m_closeFunc;
|
|
};
|
|
|
|
bool GifFormat::onLoad(FileOp* fop)
|
|
{
|
|
int errCode;
|
|
GifFilePtr gif_file(DGifOpenFileHandle(open_file_descriptor_with_exception(fop->filename, "rb"), &errCode), &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(frame_t(0), 256));
|
|
UniquePtr<Palette> previous_palette(new Palette(frame_t(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;
|
|
frame_t 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 Exception("Invalid GIF record in file.\n");
|
|
|
|
switch (record_type) {
|
|
|
|
case IMAGE_DESC_RECORD_TYPE: {
|
|
if (DGifGetImageDesc(gif_file) == GIF_ERROR)
|
|
throw 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 Exception("Image %d is out of sprite bounds.\n", (int)frame_num);
|
|
|
|
// Add a new frames.
|
|
if (frame_num >= frame_t(data->frames.size()))
|
|
data->frames.resize(frame_num+1);
|
|
|
|
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 = frame_image->getPixelAddress(0, y);
|
|
if (DGifGetLine(gif_file, addr, frame_w) == GIF_ERROR)
|
|
throw Exception("Invalid interlaced image data.");
|
|
}
|
|
}
|
|
else {
|
|
for (int y = 0; y < frame_h; ++y) {
|
|
addr = frame_image->getPixelAddress(0, y);
|
|
if (DGifGetLine(gif_file, addr, frame_w) == GIF_ERROR)
|
|
throw Exception("Invalid image data (%d).\n", gif_file->Error);
|
|
}
|
|
}
|
|
|
|
// 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 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];
|
|
|
|
// PRINTF("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 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->createDocument(NULL); // The sprite is set in onPostLoad()
|
|
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->height(); ++y) {
|
|
for (int x=0; x<frame_it->image->width(); ++x) {
|
|
int pixel_index = get_pixel_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."
|
|
"<<" PACKAGE " cannot handle this kind of GIF correctly in Indexed format."
|
|
"<<What would you like to do?"
|
|
"||Convert to &RGBA||Keep &Indexed||&Cancel",
|
|
fop->document->name().c_str());
|
|
|
|
if (result == 1)
|
|
pixelFormat = IMAGE_RGB;
|
|
else if (result != 2)
|
|
return false;
|
|
}
|
|
|
|
// Create the sprite with the GIF dimension
|
|
// TODO instead of 256 use the number of colors from the document
|
|
UniquePtr<Sprite> sprite(new Sprite(pixelFormat, data->sprite_w, data->sprite_h, 256));
|
|
|
|
// Create the main layer
|
|
LayerImage* layer = new LayerImage(sprite);
|
|
sprite->folder()->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));
|
|
clear_image(current_image, bgcolor);
|
|
clear_image(previous_image, bgcolor);
|
|
|
|
// Add all frames in the sprite.
|
|
sprite->setTotalFrames(frame_t(data->frames.size()));
|
|
Palette* current_palette = NULL;
|
|
|
|
frame_t 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->height(); ++y)
|
|
for (int x = 0; x < frame_it->image->width(); ++x) {
|
|
int pixel_index = get_pixel_fast<IndexedTraits>(frame_it->image, x, y);
|
|
if (pixel_index != frame_it->mask_index)
|
|
put_pixel_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->height(); ++y)
|
|
for (int x = 0; x < frame_it->image->width(); ++x) {
|
|
int pixel_index = get_pixel_fast<IndexedTraits>(frame_it->image, x, y);
|
|
if (pixel_index != frame_it->mask_index)
|
|
put_pixel_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, ImageRef(0));
|
|
try {
|
|
ImageRef 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->data()->setImage(cel_image);
|
|
}
|
|
catch (...) {
|
|
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:
|
|
fill_rect(current_image,
|
|
frame_it->x,
|
|
frame_it->y,
|
|
frame_it->x+frame_it->image->width()-1,
|
|
frame_it->y+frame_it->image->height()-1,
|
|
bgcolor);
|
|
break;
|
|
|
|
case DISPOSAL_METHOD_RESTORE_PREVIOUS:
|
|
copy_image(current_image, previous_image);
|
|
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)
|
|
copy_image(previous_image, current_image);
|
|
}
|
|
|
|
fop->document->sprites().add(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;
|
|
}
|
|
}
|
|
|
|
#ifdef ENABLE_SAVE
|
|
bool GifFormat::onSave(FileOp* fop)
|
|
{
|
|
int errCode;
|
|
GifFilePtr gif_file(EGifOpenFileHandle(open_file_descriptor_with_exception(fop->filename, "wb"), &errCode), &EGifCloseFile);
|
|
|
|
if (!gif_file)
|
|
throw Exception("Error creating GIF file.\n");
|
|
|
|
base::SharedPtr<GifOptions> gif_options = fop->seq.format_options;
|
|
Sprite* sprite = fop->document->sprite();
|
|
int sprite_w = sprite->width();
|
|
int sprite_h = sprite->height();
|
|
PixelFormat sprite_format = sprite->pixelFormat();
|
|
bool interlaced = gif_options->interlaced();
|
|
int loop = (gif_options->loop() ? 0: -1);
|
|
bool has_background = (sprite->backgroundLayer() && sprite->backgroundLayer()->isVisible());
|
|
int background_color = (sprite_format == IMAGE_INDEXED ? sprite->transparentColor(): 0);
|
|
int transparent_index = (has_background ? -1: sprite->transparentColor());
|
|
|
|
Palette current_palette = *sprite->palette(frame_t(0));
|
|
Palette previous_palette(current_palette);
|
|
RgbMap rgbmap;
|
|
|
|
// The color map must be a power of two.
|
|
int color_map_size = current_palette.size();
|
|
for (int i = 30; i >= 0; --i) {
|
|
if (color_map_size & (1 << i)) {
|
|
color_map_size = (1 << (i + (color_map_size & (1 << (i - 1)) ? 1: 0)));
|
|
break;
|
|
}
|
|
}
|
|
ASSERT(color_map_size > 0 && color_map_size <= 256);
|
|
|
|
ColorMapObject* color_map = NULL;
|
|
int bpp;
|
|
|
|
// We use a global color map only if this is a transparent GIF
|
|
if (!has_background) {
|
|
color_map = GifMakeMapObject(color_map_size, NULL);
|
|
if (color_map == NULL)
|
|
throw std::bad_alloc();
|
|
|
|
int i;
|
|
for (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));
|
|
}
|
|
for (; i < color_map_size; ++i) {
|
|
color_map->Colors[i].Red = 0;
|
|
color_map->Colors[i].Green = 0;
|
|
color_map->Colors[i].Blue = 0;
|
|
}
|
|
|
|
bpp = color_map->BitsPerPixel;
|
|
}
|
|
else
|
|
bpp = 8;
|
|
|
|
if (EGifPutScreenDesc(gif_file, sprite_w, sprite_h, bpp,
|
|
background_color, color_map) == GIF_ERROR)
|
|
throw 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));
|
|
|
|
clear_image(current_image, background_color);
|
|
clear_image(previous_image, background_color);
|
|
|
|
ColorMapObject* image_color_map = NULL;
|
|
|
|
render::Render render;
|
|
render.setBgType(render::BgType::NONE);
|
|
|
|
// Check if the user wants one optimized palette for all frames.
|
|
if (sprite_format != IMAGE_INDEXED &&
|
|
gif_options->quantize() == GifOptions::QuantizeAll) {
|
|
// Feed the optimizer with all rendered frames.
|
|
render::PaletteOptimizer optimizer;
|
|
for (frame_t frame_num(0); frame_num<sprite->totalFrames(); ++frame_num) {
|
|
clear_image(buffer_image, background_color);
|
|
render.renderSprite(buffer_image, sprite, frame_num);
|
|
optimizer.feedWithImage(buffer_image);
|
|
}
|
|
|
|
current_palette.makeBlack();
|
|
optimizer.calculate(¤t_palette, has_background);
|
|
|
|
rgbmap.regenerate(¤t_palette, transparent_index);
|
|
}
|
|
|
|
for (frame_t frame_num(0); frame_num<sprite->totalFrames(); ++frame_num) {
|
|
// If the sprite is RGB or Grayscale, we must to convert it to Indexed on the fly.
|
|
if (sprite_format != IMAGE_INDEXED) {
|
|
clear_image(buffer_image, background_color);
|
|
render.renderSprite(buffer_image, sprite, frame_num);
|
|
|
|
switch (gif_options->quantize()) {
|
|
case GifOptions::NoQuantize:
|
|
sprite->palette(frame_num)->copyColorsTo(¤t_palette);
|
|
rgbmap.regenerate(¤t_palette, transparent_index);
|
|
break;
|
|
case GifOptions::QuantizeEach:
|
|
{
|
|
current_palette.makeBlack();
|
|
|
|
std::vector<Image*> imgarray(1);
|
|
imgarray[0] = buffer_image;
|
|
render::create_palette_from_images(imgarray, ¤t_palette, has_background);
|
|
rgbmap.regenerate(¤t_palette, transparent_index);
|
|
}
|
|
break;
|
|
case GifOptions::QuantizeAll:
|
|
// Do nothing, we've already calculate the palette for all frames.
|
|
break;
|
|
}
|
|
|
|
render::convert_pixel_format(
|
|
buffer_image,
|
|
current_image,
|
|
IMAGE_INDEXED,
|
|
gif_options->dithering(),
|
|
&rgbmap,
|
|
¤t_palette,
|
|
has_background);
|
|
}
|
|
// If the sprite is Indexed, we can render directly into "current_image".
|
|
else {
|
|
clear_image(current_image, background_color);
|
|
render.renderSprite(current_image, sprite, frame_num);
|
|
}
|
|
|
|
if (frame_num == 0) {
|
|
frame_x = 0;
|
|
frame_y = 0;
|
|
frame_w = sprite->width();
|
|
frame_h = sprite->height();
|
|
}
|
|
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) {
|
|
if (EGifPutExtensionLeader(gif_file, APPLICATION_EXT_FUNC_CODE) == GIF_ERROR)
|
|
throw Exception("Error writing GIF graphics extension record (header section).");
|
|
|
|
unsigned char extension_bytes[11];
|
|
memcpy(extension_bytes, "NETSCAPE2.0", 11);
|
|
if (EGifPutExtensionBlock(gif_file, 11, extension_bytes) == GIF_ERROR)
|
|
throw Exception("Error writing GIF graphics extension record (first block).");
|
|
|
|
extension_bytes[0] = 1;
|
|
extension_bytes[1] = (loop & 0xff);
|
|
extension_bytes[2] = (loop >> 8) & 0xff;
|
|
if (EGifPutExtensionBlock(gif_file, 3, extension_bytes) == GIF_ERROR)
|
|
throw Exception("Error writing GIF graphics extension record (second block).");
|
|
|
|
if (EGifPutExtensionTrailer(gif_file) == GIF_ERROR)
|
|
throw Exception("Error writing GIF graphics extension record (trailer section).");
|
|
}
|
|
|
|
// 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->backgroundLayer() ? DISPOSAL_METHOD_DO_NOT_DISPOSE:
|
|
DISPOSAL_METHOD_RESTORE_BGCOLOR);
|
|
int frame_delay = sprite->frameDuration(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 Exception("Error writing GIF graphics extension record for frame %d.\n", (int)frame_num);
|
|
}
|
|
|
|
// Image color map
|
|
if ((!color_map && frame_num == 0) ||
|
|
(current_palette.countDiff(&previous_palette, NULL, NULL) > 0)) {
|
|
if (!image_color_map) {
|
|
image_color_map = GifMakeMapObject(current_palette.size(), NULL);
|
|
if (image_color_map == NULL)
|
|
throw std::bad_alloc();
|
|
}
|
|
|
|
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));
|
|
}
|
|
|
|
current_palette.copyColorsTo(&previous_palette);
|
|
}
|
|
|
|
// Write the image record.
|
|
if (EGifPutImageDesc(gif_file,
|
|
frame_x, frame_y,
|
|
frame_w, frame_h, interlaced ? 1: 0,
|
|
image_color_map) == GIF_ERROR)
|
|
throw Exception("Error writing GIF frame %d.\n", (int)frame_num);
|
|
|
|
// Write the image data (pixels).
|
|
if (interlaced) {
|
|
// 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 =
|
|
(IndexedTraits::address_t)current_image->getPixelAddress(frame_x, frame_y + y);
|
|
|
|
if (EGifPutLine(gif_file, addr, frame_w) == GIF_ERROR)
|
|
throw 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 =
|
|
(IndexedTraits::address_t)current_image->getPixelAddress(frame_x, frame_y + y);
|
|
|
|
if (EGifPutLine(gif_file, addr, frame_w) == GIF_ERROR)
|
|
throw Exception("Error writing GIF image scanlines for frame %d.\n", (int)frame_num);
|
|
}
|
|
}
|
|
|
|
copy_image(previous_image, current_image);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
base::SharedPtr<FormatOptions> GifFormat::onGetFormatOptions(FileOp* fop)
|
|
{
|
|
base::SharedPtr<GifOptions> gif_options;
|
|
if (fop->document->getFormatOptions())
|
|
gif_options = base::SharedPtr<GifOptions>(fop->document->getFormatOptions());
|
|
|
|
if (!gif_options)
|
|
gif_options.reset(new GifOptions);
|
|
|
|
// Non-interactive mode
|
|
if (!fop->context || !fop->context->isUiAvailable())
|
|
return gif_options;
|
|
|
|
try {
|
|
// Configuration parameters
|
|
gif_options->setQuantize((GifOptions::Quantize)get_config_int("GIF", "Quantize", (int)gif_options->quantize()));
|
|
gif_options->setInterlaced(get_config_bool("GIF", "Interlaced", gif_options->interlaced()));
|
|
gif_options->setLoop(get_config_bool("GIF", "Loop", gif_options->loop()));
|
|
gif_options->setDithering((doc::DitheringMethod)get_config_int("GIF", "Dither", (int)gif_options->dithering()));
|
|
|
|
// Load the window to ask to the user the GIF options he wants.
|
|
|
|
app::gen::GifOptions win;
|
|
win.rgbOptions()->setVisible(fop->document->sprite()->pixelFormat() != IMAGE_INDEXED);
|
|
|
|
switch (gif_options->quantize()) {
|
|
case GifOptions::NoQuantize: win.noQuantize()->setSelected(true); break;
|
|
case GifOptions::QuantizeEach: win.quantizeEach()->setSelected(true); break;
|
|
case GifOptions::QuantizeAll: win.quantizeAll()->setSelected(true); break;
|
|
}
|
|
win.interlaced()->setSelected(gif_options->interlaced());
|
|
win.loop()->setSelected(gif_options->loop());
|
|
|
|
win.dither()->setEnabled(true);
|
|
win.dither()->setSelected(gif_options->dithering() == doc::DitheringMethod::ORDERED);
|
|
|
|
win.openWindowInForeground();
|
|
|
|
if (win.getKiller() == win.ok()) {
|
|
if (win.quantizeAll()->isSelected())
|
|
gif_options->setQuantize(GifOptions::QuantizeAll);
|
|
else if (win.quantizeEach()->isSelected())
|
|
gif_options->setQuantize(GifOptions::QuantizeEach);
|
|
else if (win.noQuantize()->isSelected())
|
|
gif_options->setQuantize(GifOptions::NoQuantize);
|
|
|
|
gif_options->setInterlaced(win.interlaced()->isSelected());
|
|
gif_options->setLoop(win.loop()->isSelected());
|
|
gif_options->setDithering(win.dither()->isSelected() ?
|
|
doc::DitheringMethod::ORDERED:
|
|
doc::DitheringMethod::NONE);
|
|
|
|
set_config_int("GIF", "Quantize", gif_options->quantize());
|
|
set_config_bool("GIF", "Interlaced", gif_options->interlaced());
|
|
set_config_bool("GIF", "Loop", gif_options->loop());
|
|
set_config_int("GIF", "Dither", int(gif_options->dithering()));
|
|
}
|
|
else {
|
|
gif_options.reset(NULL);
|
|
}
|
|
|
|
return gif_options;
|
|
}
|
|
catch (std::exception& e) {
|
|
Console::showException(e);
|
|
return base::SharedPtr<GifOptions>(0);
|
|
}
|
|
}
|
|
|
|
} // namespace app
|