2015-02-12 23:16:25 +08:00
|
|
|
// Aseprite
|
2020-04-08 23:03:32 +08:00
|
|
|
// Copyright (C) 2018-2020 Igara Studio S.A.
|
2018-02-08 01:35:12 +08:00
|
|
|
// Copyright (C) 2001-2018 David Capello
|
2015-02-12 23:16:25 +08:00
|
|
|
//
|
2016-08-27 04:02:58 +08:00
|
|
|
// This program is distributed under the terms of
|
|
|
|
// the End-User License Agreement for Aseprite.
|
2007-09-19 07:57:02 +08:00
|
|
|
|
2013-08-06 08:20:19 +08:00
|
|
|
#ifdef HAVE_CONFIG_H
|
2007-09-19 07:57:02 +08:00
|
|
|
#include "config.h"
|
2013-08-06 08:20:19 +08:00
|
|
|
#endif
|
2007-09-19 07:57:02 +08:00
|
|
|
|
2018-10-19 02:29:16 +08:00
|
|
|
#include "app/color_spaces.h"
|
2014-07-20 09:01:39 +08:00
|
|
|
#include "app/console.h"
|
|
|
|
#include "app/context.h"
|
2018-07-07 22:54:44 +08:00
|
|
|
#include "app/doc.h"
|
2013-08-06 08:20:19 +08:00
|
|
|
#include "app/file/file.h"
|
|
|
|
#include "app/file/file_format.h"
|
|
|
|
#include "app/file/format_options.h"
|
2018-03-16 22:03:50 +08:00
|
|
|
#include "app/file/gif_format.h"
|
2014-07-20 09:01:39 +08:00
|
|
|
#include "app/file/gif_options.h"
|
2013-08-06 08:20:19 +08:00
|
|
|
#include "app/modules/gui.h"
|
2018-02-08 01:35:12 +08:00
|
|
|
#include "app/pref/preferences.h"
|
2014-02-18 08:43:20 +08:00
|
|
|
#include "app/util/autocrop.h"
|
|
|
|
#include "base/file_handle.h"
|
2015-07-17 23:26:11 +08:00
|
|
|
#include "base/fs.h"
|
2014-10-21 09:21:31 +08:00
|
|
|
#include "doc/doc.h"
|
2019-12-03 20:16:38 +08:00
|
|
|
#include "gfx/clip.h"
|
2019-04-04 06:32:24 +08:00
|
|
|
#include "render/dithering.h"
|
2017-05-20 02:49:31 +08:00
|
|
|
#include "render/ordered_dither.h"
|
2014-12-28 22:06:11 +08:00
|
|
|
#include "render/quantization.h"
|
|
|
|
#include "render/render.h"
|
2014-07-20 09:01:39 +08:00
|
|
|
#include "ui/button.h"
|
|
|
|
|
2015-09-23 03:33:49 +08:00
|
|
|
#include "gif_options.xml.h"
|
2011-01-19 07:42:43 +08:00
|
|
|
|
2020-04-08 23:03:32 +08:00
|
|
|
#include <algorithm>
|
|
|
|
|
2011-01-19 07:42:43 +08:00
|
|
|
#include <gif_lib.h>
|
2007-09-19 07:57:02 +08:00
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
#ifdef _WIN32
|
|
|
|
#include <io.h>
|
|
|
|
#define posix_lseek _lseek
|
|
|
|
#else
|
|
|
|
#include <unistd.h>
|
2015-07-23 04:30:11 +08:00
|
|
|
#define posix_lseek lseek
|
2015-07-17 23:26:11 +08:00
|
|
|
#endif
|
|
|
|
|
2015-06-09 07:21:50 +08:00
|
|
|
#if GIFLIB_MAJOR < 5
|
|
|
|
#define GifMakeMapObject MakeMapObject
|
2015-07-23 04:30:11 +08:00
|
|
|
#define GifFreeMapObject FreeMapObject
|
|
|
|
#define GifBitSize BitSize
|
2015-06-09 07:21:50 +08:00
|
|
|
#endif
|
|
|
|
|
2019-06-05 21:30:18 +08:00
|
|
|
#define GIF_TRACE(...)
|
|
|
|
|
2015-07-31 05:46:56 +08:00
|
|
|
// GifBitSize can return 9 (it's a bug in giflib)
|
2020-04-08 23:03:32 +08:00
|
|
|
#define GifBitSizeLimited(v) (std::min(GifBitSize(v), 8))
|
2015-07-31 05:46:56 +08:00
|
|
|
|
2013-08-06 08:20:19 +08:00
|
|
|
namespace app {
|
|
|
|
|
2014-02-18 08:43:20 +08:00
|
|
|
using namespace base;
|
|
|
|
|
2015-07-16 04:13:37 +08:00
|
|
|
enum class DisposalMethod {
|
|
|
|
NONE,
|
|
|
|
DO_NOT_DISPOSE,
|
|
|
|
RESTORE_BGCOLOR,
|
|
|
|
RESTORE_PREVIOUS,
|
2011-01-19 07:42:43 +08:00
|
|
|
};
|
2007-09-19 07:57:02 +08:00
|
|
|
|
2013-08-06 08:20:19 +08:00
|
|
|
class GifFormat : public FileFormat {
|
2014-07-20 09:01:39 +08:00
|
|
|
|
2018-02-21 21:39:30 +08:00
|
|
|
const char* onGetName() const override {
|
|
|
|
return "gif";
|
|
|
|
}
|
|
|
|
|
|
|
|
void onGetExtensions(base::paths& exts) const override {
|
|
|
|
exts.push_back("gif");
|
|
|
|
}
|
|
|
|
|
|
|
|
dio::FileFormat onGetDioFormat() const override {
|
|
|
|
return dio::FileFormat::GIF_ANIMATION;
|
|
|
|
}
|
|
|
|
|
2015-10-01 03:34:43 +08:00
|
|
|
int onGetFlags() const override {
|
2012-01-06 06:45:03 +08:00
|
|
|
return
|
2011-01-17 04:27:18 +08:00
|
|
|
FILE_SUPPORT_LOAD |
|
|
|
|
FILE_SUPPORT_SAVE |
|
2011-01-19 07:42:43 +08:00
|
|
|
FILE_SUPPORT_RGB |
|
|
|
|
FILE_SUPPORT_RGBA |
|
|
|
|
FILE_SUPPORT_GRAY |
|
|
|
|
FILE_SUPPORT_GRAYA |
|
2011-01-17 04:27:18 +08:00
|
|
|
FILE_SUPPORT_INDEXED |
|
|
|
|
FILE_SUPPORT_FRAMES |
|
2014-07-20 09:01:39 +08:00
|
|
|
FILE_SUPPORT_PALETTES |
|
|
|
|
FILE_SUPPORT_GET_FORMAT_OPTIONS;
|
2011-01-17 04:27:18 +08:00
|
|
|
}
|
|
|
|
|
2015-10-01 03:34:43 +08:00
|
|
|
bool onLoad(FileOp* fop) override;
|
2014-04-10 11:33:28 +08:00
|
|
|
#ifdef ENABLE_SAVE
|
2014-08-15 10:07:47 +08:00
|
|
|
bool onSave(FileOp* fop) override;
|
2014-04-10 11:33:28 +08:00
|
|
|
#endif
|
2019-08-02 07:20:02 +08:00
|
|
|
FormatOptionsPtr onAskUserForFormatOptions(FileOp* fop) override;
|
2007-09-19 07:57:02 +08:00
|
|
|
};
|
|
|
|
|
2011-01-17 04:27:18 +08:00
|
|
|
FileFormat* CreateGifFormat()
|
|
|
|
{
|
|
|
|
return new GifFormat;
|
|
|
|
}
|
|
|
|
|
2011-01-19 07:42:43 +08:00
|
|
|
static int interlaced_offset[] = { 0, 4, 2, 1 };
|
|
|
|
static int interlaced_jumps[] = { 8, 8, 4, 2 };
|
2007-09-19 07:57:02 +08:00
|
|
|
|
2018-06-26 02:58:28 +08:00
|
|
|
// TODO this should be part of a GifEncoder instance
|
2018-03-16 22:03:50 +08:00
|
|
|
// True if the GifEncoder should save the animation for Twitter:
|
|
|
|
// * Frames duration >= 2, and
|
|
|
|
// * Last frame 1/4 of its duration
|
|
|
|
static bool fix_last_frame_duration = false;
|
|
|
|
|
|
|
|
GifEncoderDurationFix::GifEncoderDurationFix(bool state)
|
|
|
|
{
|
|
|
|
fix_last_frame_duration = state;
|
|
|
|
}
|
|
|
|
|
|
|
|
GifEncoderDurationFix::~GifEncoderDurationFix()
|
|
|
|
{
|
|
|
|
fix_last_frame_duration = false;
|
|
|
|
}
|
|
|
|
|
2014-06-03 09:10:08 +08:00
|
|
|
struct GifFilePtr {
|
|
|
|
public:
|
2015-06-09 07:21:50 +08:00
|
|
|
#if GIFLIB_MAJOR >= 5
|
2014-06-03 09:10:08 +08:00
|
|
|
typedef int (*CloseFunc)(GifFileType*, int*);
|
2015-06-09 07:21:50 +08:00
|
|
|
#else
|
|
|
|
typedef int (*CloseFunc)(GifFileType*);
|
|
|
|
#endif
|
2014-06-03 09:10:08 +08:00
|
|
|
|
|
|
|
GifFilePtr(GifFileType* ptr, CloseFunc closeFunc) :
|
|
|
|
m_ptr(ptr), m_closeFunc(closeFunc) {
|
|
|
|
}
|
|
|
|
|
|
|
|
~GifFilePtr() {
|
2015-06-09 07:21:50 +08:00
|
|
|
#if GIFLIB_MAJOR >= 5
|
2014-06-03 09:10:08 +08:00
|
|
|
int errCode;
|
|
|
|
m_closeFunc(m_ptr, &errCode);
|
2015-06-09 07:21:50 +08:00
|
|
|
#else
|
|
|
|
m_closeFunc(m_ptr);
|
|
|
|
#endif
|
2014-06-03 09:10:08 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
operator GifFileType*() {
|
|
|
|
return m_ptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
GifFileType* operator->() {
|
|
|
|
return m_ptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
GifFileType* m_ptr;
|
|
|
|
CloseFunc m_closeFunc;
|
|
|
|
};
|
|
|
|
|
2015-07-23 03:40:44 +08:00
|
|
|
static void process_disposal_method(const Image* previous,
|
|
|
|
Image* current,
|
|
|
|
const DisposalMethod disposal,
|
|
|
|
const gfx::Rect& frameBounds,
|
|
|
|
const color_t clearColor)
|
|
|
|
{
|
|
|
|
switch (disposal) {
|
|
|
|
|
|
|
|
case DisposalMethod::NONE:
|
|
|
|
case DisposalMethod::DO_NOT_DISPOSE:
|
|
|
|
// Do nothing
|
|
|
|
break;
|
|
|
|
|
|
|
|
case DisposalMethod::RESTORE_BGCOLOR:
|
|
|
|
fill_rect(current,
|
|
|
|
frameBounds.x,
|
|
|
|
frameBounds.y,
|
|
|
|
frameBounds.x+frameBounds.w-1,
|
|
|
|
frameBounds.y+frameBounds.h-1,
|
|
|
|
clearColor);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case DisposalMethod::RESTORE_PREVIOUS:
|
|
|
|
current->copy(previous, gfx::Clip(frameBounds));
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-09-25 10:45:10 +08:00
|
|
|
static inline doc::color_t colormap2rgba(ColorMapObject* colormap, int i) {
|
|
|
|
return doc::rgba(
|
|
|
|
colormap->Colors[i].Red,
|
|
|
|
colormap->Colors[i].Green,
|
|
|
|
colormap->Colors[i].Blue, 255);
|
|
|
|
}
|
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
// Decodes a GIF file trying to keep the image in Indexed format. If
|
|
|
|
// it's not possible to handle it as Indexed (e.g. it contains more
|
|
|
|
// than 256 colors), the file will be automatically converted to RGB.
|
|
|
|
//
|
|
|
|
// This is a complex process because GIF files are made to be composed
|
|
|
|
// over RGB output. Each frame is composed over the previous frame,
|
|
|
|
// and combinations of local colormaps can output any number of
|
|
|
|
// colors, not just 256. So previous RGB colors must be kept and
|
|
|
|
// merged with new colormaps.
|
|
|
|
class GifDecoder {
|
|
|
|
public:
|
2017-09-27 03:40:54 +08:00
|
|
|
GifDecoder(FileOp* fop, GifFileType* gifFile, int fd, size_t filesize)
|
2015-07-17 23:26:11 +08:00
|
|
|
: m_fop(fop)
|
|
|
|
, m_gifFile(gifFile)
|
|
|
|
, m_fd(fd)
|
|
|
|
, m_filesize(filesize)
|
|
|
|
, m_sprite(nullptr)
|
|
|
|
, m_spriteBounds(0, 0, m_gifFile->SWidth, m_gifFile->SHeight)
|
|
|
|
, m_frameNum(0)
|
|
|
|
, m_opaque(false)
|
|
|
|
, m_disposalMethod(DisposalMethod::NONE)
|
|
|
|
, m_bgIndex(m_gifFile->SBackGroundColor >= 0 ? m_gifFile->SBackGroundColor: 0)
|
|
|
|
, m_localTransparentIndex(-1)
|
|
|
|
, m_frameDelay(1)
|
2015-07-31 21:53:51 +08:00
|
|
|
, m_remap(256)
|
2015-08-10 23:40:39 +08:00
|
|
|
, m_hasLocalColormaps(false)
|
|
|
|
, m_firstLocalColormap(nullptr) {
|
2019-06-05 21:30:18 +08:00
|
|
|
GIF_TRACE("GIF: background index=%d\n", (int)m_gifFile->SBackGroundColor);
|
|
|
|
GIF_TRACE("GIF: global colormap=%d, ncolors=%d\n",
|
|
|
|
(m_gifFile->SColorMap ? 1: 0),
|
|
|
|
(m_gifFile->SColorMap ? m_gifFile->SColorMap->ColorCount: 0));
|
2015-07-17 23:26:11 +08:00
|
|
|
}
|
2014-02-18 08:43:20 +08:00
|
|
|
|
2015-08-10 23:40:39 +08:00
|
|
|
~GifDecoder() {
|
|
|
|
if (m_firstLocalColormap)
|
|
|
|
GifFreeMapObject(m_firstLocalColormap);
|
|
|
|
}
|
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
Sprite* releaseSprite() {
|
|
|
|
return m_sprite.release();
|
2007-09-19 07:57:02 +08:00
|
|
|
}
|
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
bool decode() {
|
|
|
|
GifRecordType recType;
|
2011-06-26 04:12:08 +08:00
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
// Read record by record
|
|
|
|
while ((recType = readRecordType()) != TERMINATE_RECORD_TYPE) {
|
2015-07-23 03:40:44 +08:00
|
|
|
readRecord(recType);
|
2011-01-19 07:42:43 +08:00
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
// Just one frame?
|
2015-09-29 22:27:00 +08:00
|
|
|
if (m_fop->isOneFrame() && m_frameNum > 0)
|
2015-07-17 23:26:11 +08:00
|
|
|
break;
|
2011-03-23 08:11:25 +08:00
|
|
|
|
2015-09-29 22:27:00 +08:00
|
|
|
if (m_fop->isStop())
|
2015-07-17 23:26:11 +08:00
|
|
|
break;
|
2011-03-23 08:11:25 +08:00
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
if (m_filesize > 0) {
|
|
|
|
int pos = posix_lseek(m_fd, 0, SEEK_CUR);
|
2015-09-29 22:27:00 +08:00
|
|
|
m_fop->setProgress(double(pos) / double(m_filesize));
|
2015-07-17 23:26:11 +08:00
|
|
|
}
|
2011-01-19 07:42:43 +08:00
|
|
|
}
|
2015-07-17 23:26:11 +08:00
|
|
|
|
2015-07-30 23:36:27 +08:00
|
|
|
if (m_sprite) {
|
|
|
|
// Add entries to include the transparent color
|
|
|
|
if (m_bgIndex >= m_sprite->palette(0)->size())
|
|
|
|
m_sprite->palette(0)->resize(m_bgIndex+1);
|
2015-07-23 03:40:44 +08:00
|
|
|
|
2015-08-10 23:40:39 +08:00
|
|
|
switch (m_sprite->pixelFormat()) {
|
|
|
|
|
|
|
|
case IMAGE_INDEXED: {
|
|
|
|
// Use the original global color map
|
|
|
|
ColorMapObject* global = m_gifFile->SColorMap;
|
|
|
|
if (!global)
|
|
|
|
global = m_firstLocalColormap;
|
|
|
|
if (global &&
|
|
|
|
global->ColorCount >= m_sprite->palette(0)->size() &&
|
|
|
|
!m_hasLocalColormaps) {
|
|
|
|
remapToGlobalColormap(global);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
case IMAGE_RGB:
|
|
|
|
// Avoid huge color palettes
|
|
|
|
if (m_sprite->palette(0)->size() > 256) {
|
|
|
|
reduceToAnOptimizedPalette();
|
|
|
|
}
|
|
|
|
break;
|
2015-07-31 22:06:26 +08:00
|
|
|
}
|
2015-07-31 21:53:51 +08:00
|
|
|
|
2015-07-30 23:36:27 +08:00
|
|
|
if (m_layer && m_opaque)
|
|
|
|
m_layer->configureAsBackground();
|
2015-07-17 23:26:11 +08:00
|
|
|
|
2018-10-19 02:29:16 +08:00
|
|
|
// sRGB is the default color space for GIF files
|
|
|
|
m_sprite->setColorSpace(gfx::ColorSpace::MakeSRGB());
|
|
|
|
|
2015-07-30 23:36:27 +08:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
return false;
|
2011-03-23 08:11:25 +08:00
|
|
|
}
|
2007-09-19 07:57:02 +08:00
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
private:
|
|
|
|
|
|
|
|
GifRecordType readRecordType() {
|
|
|
|
GifRecordType type;
|
|
|
|
if (DGifGetRecordType(m_gifFile, &type) == GIF_ERROR)
|
2014-02-18 08:43:20 +08:00
|
|
|
throw Exception("Invalid GIF record in file.\n");
|
2011-03-23 08:11:25 +08:00
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
return type;
|
|
|
|
}
|
2012-01-06 06:45:03 +08:00
|
|
|
|
2015-07-23 03:40:44 +08:00
|
|
|
void readRecord(GifRecordType recordType) {
|
2015-07-17 23:26:11 +08:00
|
|
|
switch (recordType) {
|
2012-01-06 06:45:03 +08:00
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
case IMAGE_DESC_RECORD_TYPE:
|
2015-07-23 03:40:44 +08:00
|
|
|
readImageDescRecord();
|
2015-07-17 23:26:11 +08:00
|
|
|
break;
|
2012-01-06 06:45:03 +08:00
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
case EXTENSION_RECORD_TYPE:
|
2015-07-23 03:40:44 +08:00
|
|
|
readExtensionRecord();
|
2015-07-17 23:26:11 +08:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2012-01-06 06:45:03 +08:00
|
|
|
|
2015-07-23 03:40:44 +08:00
|
|
|
void readImageDescRecord() {
|
2015-07-17 23:26:11 +08:00
|
|
|
if (DGifGetImageDesc(m_gifFile) == GIF_ERROR)
|
|
|
|
throw Exception("Invalid GIF image descriptor.\n");
|
2012-01-06 06:45:03 +08:00
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
// These are the bounds of the image to read.
|
|
|
|
gfx::Rect frameBounds(
|
|
|
|
m_gifFile->Image.Left,
|
|
|
|
m_gifFile->Image.Top,
|
|
|
|
m_gifFile->Image.Width,
|
|
|
|
m_gifFile->Image.Height);
|
2015-07-16 02:42:54 +08:00
|
|
|
|
2019-12-03 20:16:38 +08:00
|
|
|
#if 0 // Generally GIF files should contain frame bounds inside the
|
|
|
|
// canvas bounds (in other case the GIF will contain pixels that
|
|
|
|
// are not visible). In case that some app creates an invalid
|
|
|
|
// GIF files with bounds outside the canvas, we should support
|
|
|
|
// to load the GIF file anyway (which is what is done by other
|
|
|
|
// apps).
|
2015-07-17 23:26:11 +08:00
|
|
|
if (!m_spriteBounds.contains(frameBounds))
|
|
|
|
throw Exception("Image %d is out of sprite bounds.\n", (int)m_frameNum);
|
2019-12-03 20:16:38 +08:00
|
|
|
#endif
|
2012-01-06 06:45:03 +08:00
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
// Create sprite if this is the first frame
|
|
|
|
if (!m_sprite)
|
|
|
|
createSprite();
|
2012-01-06 06:45:03 +08:00
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
// Add a frame if it's necessary
|
|
|
|
if (m_sprite->lastFrame() < m_frameNum)
|
|
|
|
m_sprite->addFrame(m_frameNum);
|
2012-01-06 06:45:03 +08:00
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
// Create a temporary image loading the frame pixels from the GIF file
|
2019-12-03 20:16:38 +08:00
|
|
|
std::unique_ptr<Image> frameImage;
|
|
|
|
// We don't know if a GIF file could contain empty bounds (width
|
|
|
|
// or height=0), but we check this just in case.
|
|
|
|
if (!frameBounds.isEmpty())
|
|
|
|
frameImage.reset(readFrameIndexedImage(frameBounds));
|
2011-01-19 07:42:43 +08:00
|
|
|
|
2020-04-16 19:53:56 +08:00
|
|
|
GIF_TRACE("GIF: Frame[%d] transparentIndex=%d localMap=%d\n",
|
|
|
|
(int)m_frameNum, m_localTransparentIndex,
|
|
|
|
m_gifFile->Image.ColorMap ? m_gifFile->Image.ColorMap->ColorCount: 0);
|
2012-01-06 06:45:03 +08:00
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
if (m_frameNum == 0) {
|
|
|
|
if (m_localTransparentIndex >= 0)
|
|
|
|
m_opaque = false;
|
|
|
|
else
|
|
|
|
m_opaque = true;
|
|
|
|
}
|
2012-01-06 06:45:03 +08:00
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
// Merge this frame colors with the current palette
|
2019-12-03 20:16:38 +08:00
|
|
|
if (frameImage)
|
|
|
|
updatePalette(frameImage.get());
|
2012-01-06 06:45:03 +08:00
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
// Convert the sprite to RGB if we have more than 256 colors
|
|
|
|
if ((m_sprite->pixelFormat() == IMAGE_INDEXED) &&
|
|
|
|
(m_sprite->palette(m_frameNum)->size() > 256)) {
|
2019-06-05 21:30:18 +08:00
|
|
|
GIF_TRACE("GIF: Converting to RGB because we have %d colors\n",
|
|
|
|
m_sprite->palette(m_frameNum)->size());
|
2015-09-25 10:49:01 +08:00
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
convertIndexedSpriteToRgb();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Composite frame with previous frame
|
2019-12-03 20:16:38 +08:00
|
|
|
if (frameImage) {
|
|
|
|
if (m_sprite->pixelFormat() == IMAGE_INDEXED) {
|
|
|
|
compositeIndexedImageToIndexed(frameBounds, frameImage.get());
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
compositeIndexedImageToRgb(frameBounds, frameImage.get());
|
|
|
|
}
|
2015-07-17 23:26:11 +08:00
|
|
|
}
|
2012-01-06 06:45:03 +08:00
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
// Create cel
|
|
|
|
createCel();
|
|
|
|
|
|
|
|
// Dispose/clear frame content
|
2015-07-23 03:40:44 +08:00
|
|
|
process_disposal_method(m_previousImage.get(),
|
|
|
|
m_currentImage.get(),
|
|
|
|
m_disposalMethod,
|
|
|
|
frameBounds,
|
|
|
|
m_bgIndex);
|
|
|
|
|
|
|
|
// Copy the current image into previous image
|
|
|
|
copy_image(m_previousImage.get(), m_currentImage.get());
|
2015-07-17 23:26:11 +08:00
|
|
|
|
|
|
|
// Set frame delay (1/100th seconds to milliseconds)
|
|
|
|
if (m_frameDelay >= 0)
|
|
|
|
m_sprite->setFrameDuration(m_frameNum, m_frameDelay*10);
|
|
|
|
|
|
|
|
// Reset extension variables
|
|
|
|
m_disposalMethod = DisposalMethod::NONE;
|
|
|
|
m_localTransparentIndex = -1;
|
|
|
|
m_frameDelay = 1;
|
|
|
|
|
|
|
|
// Next frame
|
|
|
|
++m_frameNum;
|
|
|
|
}
|
|
|
|
|
|
|
|
Image* readFrameIndexedImage(const gfx::Rect& frameBounds) {
|
2018-08-09 04:27:26 +08:00
|
|
|
std::unique_ptr<Image> frameImage(
|
2015-07-17 23:26:11 +08:00
|
|
|
Image::create(IMAGE_INDEXED, frameBounds.w, frameBounds.h));
|
|
|
|
|
|
|
|
IndexedTraits::address_t addr;
|
|
|
|
|
|
|
|
if (m_gifFile->Image.Interlace) {
|
|
|
|
// Need to perform 4 passes on the image
|
|
|
|
for (int i=0; i<4; ++i)
|
|
|
|
for (int y = interlaced_offset[i]; y < frameBounds.h; y += interlaced_jumps[i]) {
|
|
|
|
addr = frameImage->getPixelAddress(0, y);
|
|
|
|
if (DGifGetLine(m_gifFile, addr, frameBounds.w) == GIF_ERROR)
|
|
|
|
throw Exception("Invalid interlaced image data.");
|
2012-01-06 06:45:03 +08:00
|
|
|
}
|
2015-07-17 23:26:11 +08:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
for (int y = 0; y < frameBounds.h; ++y) {
|
|
|
|
addr = frameImage->getPixelAddress(0, y);
|
|
|
|
if (DGifGetLine(m_gifFile, addr, frameBounds.w) == GIF_ERROR)
|
|
|
|
throw Exception("Invalid image data (%d).\n"
|
|
|
|
#if GIFLIB_MAJOR >= 5
|
|
|
|
, m_gifFile->Error
|
|
|
|
#else
|
|
|
|
, GifLastError()
|
|
|
|
#endif
|
|
|
|
);
|
2011-01-19 07:42:43 +08:00
|
|
|
}
|
2015-07-17 23:26:11 +08:00
|
|
|
}
|
2011-01-24 06:35:22 +08:00
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
return frameImage.release();
|
|
|
|
}
|
2011-01-24 06:35:22 +08:00
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
ColorMapObject* getFrameColormap() {
|
2015-07-31 21:53:51 +08:00
|
|
|
ColorMapObject* global = m_gifFile->SColorMap;
|
2015-07-17 23:26:11 +08:00
|
|
|
ColorMapObject* colormap = m_gifFile->Image.ColorMap;
|
2015-07-31 21:53:51 +08:00
|
|
|
|
|
|
|
if (!colormap) {
|
|
|
|
// Doesn't have local map, use the global one
|
|
|
|
colormap = global;
|
|
|
|
}
|
2015-08-10 23:40:39 +08:00
|
|
|
else if (!m_hasLocalColormaps) {
|
|
|
|
if (!global) {
|
2020-07-08 03:17:07 +08:00
|
|
|
if (!m_firstLocalColormap) {
|
|
|
|
m_firstLocalColormap = GifMakeMapObject(256, nullptr);
|
|
|
|
for (int i=0; i<colormap->ColorCount; ++i) {
|
|
|
|
m_firstLocalColormap->Colors[i].Red = colormap->Colors[i].Red;
|
|
|
|
m_firstLocalColormap->Colors[i].Green = colormap->Colors[i].Green;
|
|
|
|
m_firstLocalColormap->Colors[i].Blue = colormap->Colors[i].Blue;
|
|
|
|
}
|
|
|
|
}
|
2015-08-10 23:40:39 +08:00
|
|
|
global = m_firstLocalColormap;
|
|
|
|
}
|
|
|
|
|
2015-07-31 23:25:33 +08:00
|
|
|
if (global->ColorCount != colormap->ColorCount)
|
2015-07-31 21:53:51 +08:00
|
|
|
m_hasLocalColormaps = true;
|
|
|
|
else {
|
|
|
|
for (int i=0; i<colormap->ColorCount; ++i) {
|
|
|
|
if (global->Colors[i].Red != colormap->Colors[i].Red ||
|
|
|
|
global->Colors[i].Green != colormap->Colors[i].Green ||
|
|
|
|
global->Colors[i].Blue != colormap->Colors[i].Blue) {
|
|
|
|
m_hasLocalColormaps = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
if (!colormap)
|
|
|
|
throw Exception("There is no color map.");
|
2015-07-31 21:53:51 +08:00
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
return colormap;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Adds colors used in the GIF frame so we can draw it over
|
|
|
|
// m_currentImage. If the frame contains a local colormap, we try to
|
|
|
|
// find them in the current sprite palette (using
|
|
|
|
// Palette::findExactMatch()) so we don't add duplicated entries.
|
|
|
|
// To do so we use a Remap (m_remap variable) which matches the
|
|
|
|
// original GIF frame colors with the current sprite colors.
|
|
|
|
void updatePalette(const Image* frameImage) {
|
|
|
|
ColorMapObject* colormap = getFrameColormap();
|
|
|
|
int ncolors = colormap->ColorCount;
|
|
|
|
bool isLocalColormap = (m_gifFile->Image.ColorMap ? true: false);
|
|
|
|
|
2019-06-05 21:30:18 +08:00
|
|
|
GIF_TRACE("GIF: Local colormap=%d, ncolors=%d\n", isLocalColormap, ncolors);
|
2015-09-25 10:49:01 +08:00
|
|
|
|
|
|
|
// We'll calculate the list of used colormap indexes in this
|
|
|
|
// frameImage.
|
2015-07-17 23:26:11 +08:00
|
|
|
PalettePicks usedEntries(ncolors);
|
|
|
|
if (isLocalColormap) {
|
2015-09-23 01:39:47 +08:00
|
|
|
// With this we avoid discarding the transparent index when a
|
|
|
|
// frame indicates that it uses a specific index as transparent
|
|
|
|
// but the image is completely opaque anyway.
|
2020-07-08 03:17:07 +08:00
|
|
|
if (!m_opaque && m_frameNum == 0 && m_localTransparentIndex >= 0 &&
|
2015-09-23 01:39:47 +08:00
|
|
|
m_localTransparentIndex < ncolors) {
|
|
|
|
usedEntries[m_localTransparentIndex] = true;
|
|
|
|
}
|
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
for (const auto& i : LockImageBits<IndexedTraits>(frameImage)) {
|
2020-07-08 03:17:07 +08:00
|
|
|
if (i >= 0 && i < ncolors && i != m_localTransparentIndex)
|
2015-07-17 23:26:11 +08:00
|
|
|
usedEntries[i] = true;
|
|
|
|
}
|
|
|
|
}
|
2015-09-25 10:49:01 +08:00
|
|
|
// Mark all entries as used if the colormap is global.
|
|
|
|
else {
|
2015-07-17 23:26:11 +08:00
|
|
|
usedEntries.all();
|
2015-09-25 10:49:01 +08:00
|
|
|
}
|
2015-07-17 23:26:11 +08:00
|
|
|
|
2015-09-25 10:49:01 +08:00
|
|
|
// Number of colors (indexes) used in the frame image.
|
2015-07-23 03:40:44 +08:00
|
|
|
int usedNColors = usedEntries.picks();
|
2015-07-17 23:26:11 +08:00
|
|
|
|
|
|
|
// Check if we need an extra color equal to the bg color in a
|
|
|
|
// transparent frameImage.
|
|
|
|
bool needsExtraBgColor = false;
|
2020-07-08 03:17:07 +08:00
|
|
|
bool needCheckLocalTransparent = m_bgIndex != m_localTransparentIndex ||
|
|
|
|
(ncolors > m_localTransparentIndex
|
|
|
|
&& m_localTransparentIndex >= 0
|
|
|
|
&& usedEntries[m_localTransparentIndex]);
|
|
|
|
|
2015-09-25 10:50:44 +08:00
|
|
|
if (m_sprite->pixelFormat() == IMAGE_INDEXED &&
|
2020-07-08 03:17:07 +08:00
|
|
|
!m_opaque &&
|
|
|
|
needCheckLocalTransparent) {
|
2015-07-17 23:26:11 +08:00
|
|
|
for (const auto& i : LockImageBits<IndexedTraits>(frameImage)) {
|
2020-06-04 02:28:51 +08:00
|
|
|
if (i == m_bgIndex) {
|
2015-07-17 23:26:11 +08:00
|
|
|
needsExtraBgColor = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2011-03-23 08:11:25 +08:00
|
|
|
}
|
|
|
|
|
2018-08-09 04:27:26 +08:00
|
|
|
std::unique_ptr<Palette> palette;
|
2015-07-17 23:26:11 +08:00
|
|
|
if (m_frameNum == 0)
|
2015-07-23 03:40:44 +08:00
|
|
|
palette.reset(new Palette(m_frameNum, usedNColors + (needsExtraBgColor ? 1: 0)));
|
2015-07-17 23:26:11 +08:00
|
|
|
else {
|
|
|
|
palette.reset(new Palette(*m_sprite->palette(m_frameNum-1)));
|
|
|
|
palette->setFrame(m_frameNum);
|
|
|
|
}
|
2020-04-08 23:03:32 +08:00
|
|
|
resetRemap(std::max(ncolors, palette->size()));
|
2011-06-26 04:12:08 +08:00
|
|
|
|
2015-09-25 10:49:01 +08:00
|
|
|
// Number of colors in the colormap that are part of the current
|
|
|
|
// sprite palette.
|
2015-07-17 23:26:11 +08:00
|
|
|
int found = 0;
|
|
|
|
if (m_frameNum > 0) {
|
|
|
|
for (int i=0; i<ncolors; ++i) {
|
|
|
|
if (!usedEntries[i])
|
|
|
|
continue;
|
2011-06-26 04:12:08 +08:00
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
int j = palette->findExactMatch(
|
|
|
|
colormap->Colors[i].Red,
|
|
|
|
colormap->Colors[i].Green,
|
|
|
|
colormap->Colors[i].Blue, 255,
|
|
|
|
(m_opaque ? -1: m_bgIndex));
|
|
|
|
if (j >= 0) {
|
|
|
|
m_remap.map(i, j);
|
|
|
|
++found;
|
2012-01-06 06:45:03 +08:00
|
|
|
}
|
2011-06-26 04:12:08 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-09-25 10:49:01 +08:00
|
|
|
// All needed colors in the colormap are present in the current
|
|
|
|
// palette.
|
2015-07-23 03:40:44 +08:00
|
|
|
if (found == usedNColors)
|
2015-07-17 23:26:11 +08:00
|
|
|
return;
|
2011-06-26 04:12:08 +08:00
|
|
|
|
2015-09-25 10:49:01 +08:00
|
|
|
// In other case, we need to add the missing colors...
|
|
|
|
|
|
|
|
// First index that acts like a base for new colors in palette.
|
2015-07-17 23:26:11 +08:00
|
|
|
int base = (m_frameNum == 0 ? 0: palette->size());
|
2015-09-25 10:49:01 +08:00
|
|
|
|
|
|
|
// Number of colors in the image that aren't in the palette.
|
|
|
|
int missing = (usedNColors - found);
|
|
|
|
|
2019-06-05 21:30:18 +08:00
|
|
|
GIF_TRACE("GIF: Bg index=%d,\n"
|
|
|
|
" Local transparent index=%d,\n"
|
|
|
|
" Need extra index to show bg color=%d,\n "
|
|
|
|
" Found colors in palette=%d,\n"
|
|
|
|
" Used colors in local pixels=%d,\n"
|
|
|
|
" Base for new colors in palette=%d,\n"
|
|
|
|
" Colors in the image missing in the palette=%d,\n"
|
|
|
|
" New palette size=%d\n",
|
|
|
|
m_bgIndex, m_localTransparentIndex, needsExtraBgColor,
|
|
|
|
found, usedNColors, base, missing,
|
|
|
|
base + missing + (needsExtraBgColor ? 1: 0));
|
2015-09-25 10:49:01 +08:00
|
|
|
|
|
|
|
Palette oldPalette(*palette);
|
2015-07-17 23:26:11 +08:00
|
|
|
palette->resize(base + missing + (needsExtraBgColor ? 1: 0));
|
2020-04-08 23:03:32 +08:00
|
|
|
resetRemap(std::max(ncolors, palette->size()));
|
2011-06-26 04:12:08 +08:00
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
for (int i=0; i<ncolors; ++i) {
|
|
|
|
if (!usedEntries[i])
|
|
|
|
continue;
|
2011-06-26 04:12:08 +08:00
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
int j = -1;
|
2015-09-25 10:45:10 +08:00
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
if (m_frameNum > 0) {
|
|
|
|
j = oldPalette.findExactMatch(
|
|
|
|
colormap->Colors[i].Red,
|
|
|
|
colormap->Colors[i].Green,
|
|
|
|
colormap->Colors[i].Blue, 255,
|
|
|
|
(m_opaque ? -1: m_bgIndex));
|
|
|
|
}
|
2011-06-26 04:12:08 +08:00
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
if (j < 0) {
|
|
|
|
j = base++;
|
2015-09-25 10:45:10 +08:00
|
|
|
palette->setEntry(j, colormap2rgba(colormap, i));
|
2015-07-17 23:26:11 +08:00
|
|
|
}
|
|
|
|
m_remap.map(i, j);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (needsExtraBgColor) {
|
2015-09-25 10:47:33 +08:00
|
|
|
int i = m_bgIndex;
|
2015-07-17 23:26:11 +08:00
|
|
|
int j = base++;
|
2015-09-25 10:47:33 +08:00
|
|
|
palette->setEntry(j, colormap2rgba(colormap, i));
|
2020-06-04 02:28:51 +08:00
|
|
|
// m_firstLocalColorMap, is used only if we have no global color map in the gif source,
|
|
|
|
// and we want to preserve original color indexes, as much we can.
|
|
|
|
// If the palette size is > 256, m_firstLocalColormal is no more useful, because
|
|
|
|
// the sprite pixel format will be converted in RGBA image, and the colors will
|
|
|
|
// be picked from the sprite palette, instead of m_firstLocalColorMap.
|
|
|
|
if (m_firstLocalColormap && m_firstLocalColormap->ColorCount > j) {
|
2020-07-08 03:17:07 +08:00
|
|
|
// We need add this extra color to m_firstLocalColormap, because
|
|
|
|
// it might has not been considered in the first getFrameColormap execution.
|
|
|
|
// (this happen when: in the first execution of getFrameColormap function
|
|
|
|
// an extra color was not needed)
|
2020-06-04 02:28:51 +08:00
|
|
|
m_firstLocalColormap->Colors[j].Red = rgba_getr(palette->getEntry(j));
|
|
|
|
m_firstLocalColormap->Colors[j].Green = rgba_getg(palette->getEntry(j));
|
|
|
|
m_firstLocalColormap->Colors[j].Blue = rgba_getb(palette->getEntry(j));
|
|
|
|
}
|
2015-09-25 10:47:33 +08:00
|
|
|
m_remap.map(i, j);
|
2015-07-17 23:26:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
ASSERT(base == palette->size());
|
2018-08-09 04:27:26 +08:00
|
|
|
m_sprite->setPalette(palette.get(), false);
|
2011-06-26 04:12:08 +08:00
|
|
|
}
|
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
void compositeIndexedImageToIndexed(const gfx::Rect& frameBounds,
|
|
|
|
const Image* frameImage) {
|
2019-12-03 20:16:38 +08:00
|
|
|
gfx::Clip clip(frameBounds.x, frameBounds.y, 0, 0,
|
|
|
|
frameBounds.w, frameBounds.h);
|
|
|
|
if (!clip.clip(m_currentImage->width(),
|
|
|
|
m_currentImage->height(),
|
|
|
|
frameImage->width(),
|
|
|
|
frameImage->height()))
|
|
|
|
return;
|
2017-09-27 04:58:55 +08:00
|
|
|
|
2019-12-03 20:16:38 +08:00
|
|
|
const LockImageBits<IndexedTraits> srcBits(frameImage, clip.srcBounds());
|
|
|
|
LockImageBits<IndexedTraits> dstBits(m_currentImage.get(), clip.dstBounds());
|
2017-09-27 04:58:55 +08:00
|
|
|
|
2019-12-03 20:16:38 +08:00
|
|
|
auto srcIt = srcBits.begin(), srcEnd = srcBits.end();
|
|
|
|
auto dstIt = dstBits.begin(), dstEnd = dstBits.end();
|
2015-07-17 23:26:11 +08:00
|
|
|
|
2019-12-03 20:16:38 +08:00
|
|
|
// Compose the frame image with the previous frame
|
|
|
|
for (; srcIt != srcEnd && dstIt != dstEnd; ++srcIt, ++dstIt) {
|
|
|
|
color_t i = *srcIt;
|
|
|
|
if (int(i) == m_localTransparentIndex)
|
|
|
|
continue;
|
2017-09-27 04:58:55 +08:00
|
|
|
|
2019-12-03 20:16:38 +08:00
|
|
|
i = m_remap[i];
|
|
|
|
*dstIt = i;
|
2011-06-26 04:12:08 +08:00
|
|
|
}
|
2019-12-03 20:16:38 +08:00
|
|
|
|
|
|
|
ASSERT(srcIt == srcEnd);
|
|
|
|
ASSERT(dstIt == dstEnd);
|
2015-07-17 23:26:11 +08:00
|
|
|
}
|
2011-06-26 04:12:08 +08:00
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
void compositeIndexedImageToRgb(const gfx::Rect& frameBounds,
|
|
|
|
const Image* frameImage) {
|
2019-12-03 20:16:38 +08:00
|
|
|
gfx::Clip clip(frameBounds.x, frameBounds.y, 0, 0,
|
|
|
|
frameBounds.w, frameBounds.h);
|
|
|
|
if (!clip.clip(m_currentImage->width(),
|
|
|
|
m_currentImage->height(),
|
|
|
|
frameImage->width(),
|
|
|
|
frameImage->height()))
|
|
|
|
return;
|
|
|
|
|
|
|
|
const LockImageBits<IndexedTraits> srcBits(frameImage, clip.srcBounds());
|
|
|
|
LockImageBits<RgbTraits> dstBits(m_currentImage.get(), clip.dstBounds());
|
2017-09-27 04:58:55 +08:00
|
|
|
|
2019-12-03 20:16:38 +08:00
|
|
|
auto srcIt = srcBits.begin(), srcEnd = srcBits.end();
|
|
|
|
auto dstIt = dstBits.begin(), dstEnd = dstBits.end();
|
2017-09-27 04:58:55 +08:00
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
ColorMapObject* colormap = getFrameColormap();
|
2011-06-26 04:12:08 +08:00
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
// Compose the frame image with the previous frame
|
2019-12-03 20:16:38 +08:00
|
|
|
for (; srcIt != srcEnd && dstIt != dstEnd; ++srcIt, ++dstIt) {
|
|
|
|
color_t i = *srcIt;
|
|
|
|
if (int(i) == m_localTransparentIndex)
|
|
|
|
continue;
|
2011-06-26 04:12:08 +08:00
|
|
|
|
2019-12-03 20:16:38 +08:00
|
|
|
i = rgba(
|
|
|
|
colormap->Colors[i].Red,
|
|
|
|
colormap->Colors[i].Green,
|
|
|
|
colormap->Colors[i].Blue, 255);
|
2015-07-17 23:26:11 +08:00
|
|
|
|
2019-12-03 20:16:38 +08:00
|
|
|
*dstIt = i;
|
2011-06-26 04:12:08 +08:00
|
|
|
}
|
2019-12-03 20:16:38 +08:00
|
|
|
|
|
|
|
ASSERT(srcIt == srcEnd);
|
|
|
|
ASSERT(dstIt == dstEnd);
|
2015-07-17 23:26:11 +08:00
|
|
|
}
|
2011-06-26 04:12:08 +08:00
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
void createCel() {
|
|
|
|
Cel* cel = new Cel(m_frameNum, ImageRef(0));
|
2011-06-26 04:12:08 +08:00
|
|
|
try {
|
2015-07-17 23:26:11 +08:00
|
|
|
ImageRef celImage(Image::createCopy(m_currentImage.get()));
|
2011-06-26 04:12:08 +08:00
|
|
|
try {
|
2019-03-30 02:57:10 +08:00
|
|
|
cel->data()->setImage(celImage, m_layer);
|
2011-06-26 04:12:08 +08:00
|
|
|
}
|
|
|
|
catch (...) {
|
2012-01-06 06:45:03 +08:00
|
|
|
throw;
|
2011-06-26 04:12:08 +08:00
|
|
|
}
|
2015-07-17 23:26:11 +08:00
|
|
|
m_layer->addCel(cel);
|
2011-06-26 04:12:08 +08:00
|
|
|
}
|
|
|
|
catch (...) {
|
|
|
|
delete cel;
|
|
|
|
throw;
|
|
|
|
}
|
2015-07-17 23:26:11 +08:00
|
|
|
}
|
2011-06-26 04:12:08 +08:00
|
|
|
|
2015-07-23 03:40:44 +08:00
|
|
|
void readExtensionRecord() {
|
2015-07-17 23:26:11 +08:00
|
|
|
int extCode;
|
|
|
|
GifByteType* extension;
|
|
|
|
if (DGifGetExtension(m_gifFile, &extCode, &extension) == GIF_ERROR)
|
|
|
|
throw Exception("Invalid GIF extension record.\n");
|
2015-07-16 02:09:07 +08:00
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
if (extCode == GRAPHICS_EXT_FUNC_CODE) {
|
|
|
|
if (extension[0] >= 4) {
|
|
|
|
m_disposalMethod = (DisposalMethod)((extension[1] >> 2) & 7);
|
|
|
|
m_localTransparentIndex = (extension[1] & 1) ? extension[4]: -1;
|
|
|
|
m_frameDelay = (extension[3] << 8) | extension[2];
|
|
|
|
|
2019-06-05 21:30:18 +08:00
|
|
|
GIF_TRACE("GIF: Disposal method: %d\n Transparent index: %d\n Frame delay: %d\n",
|
|
|
|
m_disposalMethod, m_localTransparentIndex, m_frameDelay);
|
2015-07-17 23:26:11 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
while (extension) {
|
|
|
|
if (DGifGetExtensionNext(m_gifFile, &extension) == GIF_ERROR)
|
|
|
|
throw Exception("Invalid GIF extension record.\n");
|
|
|
|
}
|
2011-06-26 04:12:08 +08:00
|
|
|
}
|
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
void createSprite() {
|
|
|
|
ColorMapObject* colormap = nullptr;
|
|
|
|
if (m_gifFile->SColorMap) {
|
|
|
|
colormap = m_gifFile->SColorMap;
|
|
|
|
}
|
|
|
|
else if (m_gifFile->Image.ColorMap) {
|
|
|
|
colormap = m_gifFile->Image.ColorMap;
|
|
|
|
}
|
|
|
|
int ncolors = (colormap ? colormap->ColorCount: 1);
|
|
|
|
int w = m_spriteBounds.w;
|
2015-09-30 23:53:56 +08:00
|
|
|
int h = m_spriteBounds.h;
|
2007-09-19 07:57:02 +08:00
|
|
|
|
2018-10-26 02:23:01 +08:00
|
|
|
m_sprite.reset(new Sprite(ImageSpec(ColorMode::INDEXED, w, h), ncolors));
|
2015-07-17 23:26:11 +08:00
|
|
|
m_sprite->setTransparentColor(m_bgIndex);
|
2007-09-19 07:57:02 +08:00
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
m_currentImage.reset(Image::create(IMAGE_INDEXED, w, h));
|
|
|
|
m_previousImage.reset(Image::create(IMAGE_INDEXED, w, h));
|
2015-09-25 10:51:58 +08:00
|
|
|
m_currentImage->setMaskColor(m_bgIndex);
|
|
|
|
m_previousImage->setMaskColor(m_bgIndex);
|
2015-07-17 23:26:11 +08:00
|
|
|
clear_image(m_currentImage.get(), m_bgIndex);
|
|
|
|
clear_image(m_previousImage.get(), m_bgIndex);
|
|
|
|
|
|
|
|
m_layer = new LayerImage(m_sprite.get());
|
2016-06-08 06:38:56 +08:00
|
|
|
m_sprite->root()->addLayer(m_layer);
|
2015-07-17 23:26:11 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
void resetRemap(int ncolors) {
|
|
|
|
m_remap = Remap(ncolors);
|
|
|
|
for (int i=0; i<ncolors; ++i)
|
|
|
|
m_remap.map(i, i);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Converts the whole sprite read so far because it contains more
|
|
|
|
// than 256 colors at the same time.
|
|
|
|
void convertIndexedSpriteToRgb() {
|
|
|
|
for (Cel* cel : m_sprite->uniqueCels()) {
|
|
|
|
Image* oldImage = cel->image();
|
|
|
|
ImageRef newImage(
|
|
|
|
render::convert_pixel_format
|
2020-04-20 21:18:12 +08:00
|
|
|
(oldImage, nullptr, IMAGE_RGB,
|
2019-04-04 06:32:24 +08:00
|
|
|
render::Dithering(),
|
2015-09-25 10:42:51 +08:00
|
|
|
nullptr,
|
2015-07-17 23:26:11 +08:00
|
|
|
m_sprite->palette(cel->frame()),
|
2015-09-25 10:42:51 +08:00
|
|
|
m_opaque,
|
2020-04-20 21:18:12 +08:00
|
|
|
m_bgIndex,
|
|
|
|
nullptr));
|
2015-07-17 23:26:11 +08:00
|
|
|
|
|
|
|
m_sprite->replaceImage(oldImage->id(), newImage);
|
2011-06-26 04:12:08 +08:00
|
|
|
}
|
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
m_currentImage.reset(
|
|
|
|
render::convert_pixel_format
|
2017-05-16 03:25:09 +08:00
|
|
|
(m_currentImage.get(), NULL, IMAGE_RGB,
|
2019-04-04 06:32:24 +08:00
|
|
|
render::Dithering(),
|
2015-09-25 10:42:51 +08:00
|
|
|
nullptr,
|
2015-07-17 23:26:11 +08:00
|
|
|
m_sprite->palette(m_frameNum),
|
|
|
|
m_opaque,
|
|
|
|
m_bgIndex));
|
|
|
|
|
|
|
|
m_previousImage.reset(
|
|
|
|
render::convert_pixel_format
|
2017-05-16 03:25:09 +08:00
|
|
|
(m_previousImage.get(), NULL, IMAGE_RGB,
|
2019-04-04 06:32:24 +08:00
|
|
|
render::Dithering(),
|
2015-09-25 10:42:51 +08:00
|
|
|
nullptr,
|
2020-04-08 23:03:32 +08:00
|
|
|
m_sprite->palette(std::max(0, m_frameNum-1)),
|
2015-07-17 23:26:11 +08:00
|
|
|
m_opaque,
|
|
|
|
m_bgIndex));
|
|
|
|
|
|
|
|
m_sprite->setPixelFormat(IMAGE_RGB);
|
|
|
|
}
|
|
|
|
|
2015-08-10 23:40:39 +08:00
|
|
|
void remapToGlobalColormap(ColorMapObject* colormap) {
|
2015-07-31 21:53:51 +08:00
|
|
|
Palette* oldPalette = m_sprite->palette(0);
|
|
|
|
Palette newPalette(0, colormap->ColorCount);
|
|
|
|
|
|
|
|
for (int i=0; i<colormap->ColorCount; ++i) {
|
2015-09-25 10:45:10 +08:00
|
|
|
newPalette.setEntry(i, colormap2rgba(colormap, i));;
|
2015-07-31 21:53:51 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
Remap remap = create_remap_to_change_palette(
|
2015-08-01 00:32:23 +08:00
|
|
|
oldPalette, &newPalette, m_bgIndex,
|
2015-08-10 23:40:39 +08:00
|
|
|
m_opaque); // We cannot remap the transparent color if the
|
|
|
|
// sprite isn't opaque, because we
|
|
|
|
// cannot write the header again
|
2015-07-31 21:53:51 +08:00
|
|
|
|
|
|
|
for (Cel* cel : m_sprite->uniqueCels())
|
|
|
|
doc::remap_image(cel->image(), remap);
|
|
|
|
|
|
|
|
m_sprite->setPalette(&newPalette, false);
|
|
|
|
}
|
|
|
|
|
2015-07-31 22:06:26 +08:00
|
|
|
void reduceToAnOptimizedPalette() {
|
|
|
|
render::PaletteOptimizer optimizer;
|
|
|
|
const Palette* palette = m_sprite->palette(0);
|
|
|
|
|
|
|
|
// Feed the palette optimizer with pixels inside frameBounds
|
|
|
|
for (int i=0; i<palette->size(); ++i) {
|
|
|
|
optimizer.feedWithRgbaColor(palette->getEntry(i));
|
|
|
|
}
|
|
|
|
|
|
|
|
Palette newPalette(0, 256);
|
2017-05-18 02:25:40 +08:00
|
|
|
optimizer.calculate(&newPalette, m_bgIndex);
|
2015-07-31 22:06:26 +08:00
|
|
|
m_sprite->setPalette(&newPalette, false);
|
|
|
|
}
|
|
|
|
|
2015-07-17 23:26:11 +08:00
|
|
|
FileOp* m_fop;
|
|
|
|
GifFileType* m_gifFile;
|
|
|
|
int m_fd;
|
2017-09-27 03:40:54 +08:00
|
|
|
size_t m_filesize;
|
2018-08-09 04:27:26 +08:00
|
|
|
std::unique_ptr<Sprite> m_sprite;
|
2015-07-17 23:26:11 +08:00
|
|
|
gfx::Rect m_spriteBounds;
|
|
|
|
LayerImage* m_layer;
|
|
|
|
int m_frameNum;
|
|
|
|
bool m_opaque;
|
|
|
|
DisposalMethod m_disposalMethod;
|
|
|
|
int m_bgIndex;
|
|
|
|
int m_localTransparentIndex;
|
|
|
|
int m_frameDelay;
|
|
|
|
ImageRef m_currentImage;
|
|
|
|
ImageRef m_previousImage;
|
|
|
|
Remap m_remap;
|
2015-07-31 21:53:51 +08:00
|
|
|
bool m_hasLocalColormaps; // Indicates that this fila contains local colormaps
|
2015-08-10 23:40:39 +08:00
|
|
|
|
|
|
|
// This is a copy of the first local color map. It's used to see if
|
|
|
|
// all local colormaps are the same, so we can use it as a global
|
|
|
|
// colormap.
|
|
|
|
ColorMapObject* m_firstLocalColormap;
|
2015-07-17 23:26:11 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
bool GifFormat::onLoad(FileOp* fop)
|
|
|
|
{
|
|
|
|
// The filesize is used only to report some progress when we decode
|
|
|
|
// the GIF file.
|
2017-09-27 03:40:54 +08:00
|
|
|
size_t filesize = base::file_size(fop->filename());
|
2015-07-17 23:26:11 +08:00
|
|
|
|
|
|
|
#if GIFLIB_MAJOR >= 5
|
|
|
|
int errCode = 0;
|
|
|
|
#endif
|
2015-09-29 22:27:00 +08:00
|
|
|
int fd = open_file_descriptor_with_exception(fop->filename(), "rb");
|
2015-07-17 23:26:11 +08:00
|
|
|
GifFilePtr gif_file(DGifOpenFileHandle(fd
|
|
|
|
#if GIFLIB_MAJOR >= 5
|
|
|
|
, &errCode
|
|
|
|
#endif
|
|
|
|
), &DGifCloseFile);
|
|
|
|
|
|
|
|
if (!gif_file) {
|
2015-09-29 22:27:00 +08:00
|
|
|
fop->setError("Error loading GIF header.\n");
|
2015-07-17 23:26:11 +08:00
|
|
|
return false;
|
2011-06-26 04:12:08 +08:00
|
|
|
}
|
2015-07-17 23:26:11 +08:00
|
|
|
|
|
|
|
GifDecoder decoder(fop, gif_file, fd, filesize);
|
|
|
|
if (decoder.decode()) {
|
|
|
|
fop->createDocument(decoder.releaseSprite());
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
return false;
|
2011-06-26 04:12:08 +08:00
|
|
|
}
|
|
|
|
|
2014-04-10 11:33:28 +08:00
|
|
|
#ifdef ENABLE_SAVE
|
2015-07-17 23:57:32 +08:00
|
|
|
|
2020-03-27 03:19:48 +08:00
|
|
|
// Our stragegy to encode GIF files depends of the sprite color mode:
|
|
|
|
//
|
|
|
|
// 1) If the sprite is indexed, we have two paths:
|
|
|
|
// * For opaque an opaque sprite we can save it as it is (with the
|
|
|
|
// same indexes/pixels and same color palette). This brings us
|
|
|
|
// the best possible to compress the GIF file (using the best
|
|
|
|
// disposal method to update only the differences between each
|
|
|
|
// frame).
|
|
|
|
// * For transparent sprites we offer to the user the option to
|
|
|
|
// preserve the original palette or not
|
|
|
|
// (m_preservePaletteOrders). If the palette must be preserve,
|
|
|
|
// some level of compression will be sacrificed.
|
|
|
|
//
|
|
|
|
// 2) For RGB sprites the palette is created on each frame depending
|
|
|
|
// on the updated rectangle between frames, i.e. each to new frame
|
|
|
|
// incorporates a minimal rectangular region with changes from the
|
|
|
|
// previous frame, we can calculate the palette required for this
|
|
|
|
// rectangle and use it as a local colormap for the frame (if each
|
|
|
|
// frame uses previous color in the palette there is no need to
|
|
|
|
// introduce a new palette).
|
|
|
|
//
|
|
|
|
// Note: In the following algorithm you will find the "pixel clearing"
|
|
|
|
// term, this happens when we need to clear an opaque color with the
|
|
|
|
// gif transparent bg color. This is the worst possible case, because
|
|
|
|
// on transparent gif files, the only way to get the transparent color
|
|
|
|
// (bg color) is using the RESTORE_BGCOLOR disposal method (so we lost
|
|
|
|
// the chance to use DO_NOT_DISPOSE in these cases).
|
|
|
|
//
|
2015-07-23 03:40:44 +08:00
|
|
|
class GifEncoder {
|
|
|
|
public:
|
2016-09-17 12:01:28 +08:00
|
|
|
typedef int gifframe_t;
|
|
|
|
|
2015-07-23 03:40:44 +08:00
|
|
|
GifEncoder(FileOp* fop, GifFileType* gifFile)
|
|
|
|
: m_fop(fop)
|
|
|
|
, m_gifFile(gifFile)
|
2018-10-19 02:29:16 +08:00
|
|
|
, m_document(fop->document())
|
2015-09-29 22:27:00 +08:00
|
|
|
, m_sprite(fop->document()->sprite())
|
2015-07-23 03:40:44 +08:00
|
|
|
, m_spriteBounds(m_sprite->bounds())
|
2020-03-26 01:32:43 +08:00
|
|
|
, m_hasBackground(m_sprite->isOpaque())
|
2015-07-23 03:40:44 +08:00
|
|
|
, m_bitsPerPixel(1)
|
2015-09-30 23:53:56 +08:00
|
|
|
, m_globalColormap(nullptr)
|
2020-04-22 22:23:16 +08:00
|
|
|
, m_globalColormapPalette(*m_sprite->palette(0))
|
|
|
|
, m_preservePaletteOrder(false) {
|
2020-03-26 01:32:43 +08:00
|
|
|
|
|
|
|
const auto gifOptions = std::static_pointer_cast<GifOptions>(fop->formatOptions());
|
|
|
|
|
|
|
|
LOG("GIF: Saving with options: interlaced=%d loop=%d\n",
|
|
|
|
gifOptions->interlaced(), gifOptions->loop());
|
|
|
|
|
|
|
|
m_interlaced = gifOptions->interlaced();
|
|
|
|
m_loop = (gifOptions->loop() ? 0: -1);
|
|
|
|
m_lastFrameBounds = m_spriteBounds;
|
|
|
|
m_lastDisposal = DisposalMethod::NONE;
|
|
|
|
|
2015-07-23 03:40:44 +08:00
|
|
|
if (m_sprite->pixelFormat() == IMAGE_INDEXED) {
|
|
|
|
for (Palette* palette : m_sprite->getPalettes()) {
|
2015-07-31 05:46:56 +08:00
|
|
|
int bpp = GifBitSizeLimited(palette->size());
|
2020-04-08 23:03:32 +08:00
|
|
|
m_bitsPerPixel = std::max(m_bitsPerPixel, bpp);
|
2015-07-23 03:40:44 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
m_bitsPerPixel = 8;
|
2015-07-17 23:45:59 +08:00
|
|
|
}
|
|
|
|
|
2015-07-23 03:40:44 +08:00
|
|
|
if (m_sprite->pixelFormat() == IMAGE_INDEXED &&
|
|
|
|
m_sprite->getPalettes().size() == 1) {
|
2015-09-30 23:53:56 +08:00
|
|
|
// If some layer has opacity < 255 or a different blend mode, we
|
|
|
|
// need to create color palettes.
|
2020-03-27 03:19:48 +08:00
|
|
|
bool quantizeColormaps = false;
|
2016-06-29 21:59:37 +08:00
|
|
|
for (const Layer* layer : m_sprite->allVisibleLayers()) {
|
2015-09-30 23:53:56 +08:00
|
|
|
if (layer->isVisible() && layer->isImage()) {
|
|
|
|
const LayerImage* imageLayer = static_cast<const LayerImage*>(layer);
|
|
|
|
if (imageLayer->opacity() < 255 ||
|
|
|
|
imageLayer->blendMode() != BlendMode::NORMAL) {
|
2020-03-27 03:19:48 +08:00
|
|
|
quantizeColormaps = true;
|
2015-09-30 23:53:56 +08:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-27 03:19:48 +08:00
|
|
|
if (!quantizeColormaps) {
|
2020-03-26 01:32:43 +08:00
|
|
|
m_globalColormap = createColorMap(&m_globalColormapPalette);
|
2015-09-30 23:53:56 +08:00
|
|
|
m_bgIndex = m_sprite->transparentColor();
|
2020-04-22 22:23:16 +08:00
|
|
|
// For indexed and opaque sprite, we can preserve the exact
|
|
|
|
// palette order without lossing compression rate.
|
|
|
|
if (m_hasBackground)
|
|
|
|
m_preservePaletteOrder = true;
|
|
|
|
// Only for transparent indexed images the user can choose to
|
|
|
|
// preserve or not the palette order.
|
|
|
|
else
|
|
|
|
m_preservePaletteOrder = gifOptions->preservePaletteOrder();
|
2015-09-30 23:53:56 +08:00
|
|
|
}
|
|
|
|
else
|
|
|
|
m_bgIndex = 0;
|
2015-07-23 03:40:44 +08:00
|
|
|
}
|
2015-09-30 23:53:56 +08:00
|
|
|
else {
|
2015-07-23 03:40:44 +08:00
|
|
|
m_bgIndex = 0;
|
2015-09-30 23:53:56 +08:00
|
|
|
}
|
2014-02-18 08:43:20 +08:00
|
|
|
|
2020-03-27 03:19:48 +08:00
|
|
|
// This is the transparent index to use as "local transparent"
|
|
|
|
// index for each gif frame. In case that we use a global colormap
|
|
|
|
// (and we don't need to preserve the original palette), we can
|
|
|
|
// try to find a place for a global transparent index.
|
2015-07-23 03:40:44 +08:00
|
|
|
m_transparentIndex = (m_hasBackground ? -1: m_bgIndex);
|
2020-03-26 01:32:43 +08:00
|
|
|
if (m_globalColormap) {
|
2020-03-27 03:19:48 +08:00
|
|
|
// The variable m_globalColormap is != nullptr only on indexed images
|
|
|
|
ASSERT(m_sprite->pixelFormat() == IMAGE_INDEXED);
|
|
|
|
|
|
|
|
const Palette* pal = m_sprite->palette(0);
|
2020-03-26 01:32:43 +08:00
|
|
|
bool maskColorFounded = false;
|
2020-03-27 03:19:48 +08:00
|
|
|
for (int i=0; i<pal->size(); i++) {
|
|
|
|
if (doc::rgba_geta(pal->getEntry(i)) == 0) {
|
2020-03-26 01:32:43 +08:00
|
|
|
maskColorFounded = true;
|
|
|
|
m_transparentIndex = i;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2007-09-19 07:57:02 +08:00
|
|
|
|
2020-03-27 03:19:48 +08:00
|
|
|
#if 0
|
|
|
|
// If the palette contains room for one extra color for the
|
|
|
|
// mask, we can use that index.
|
|
|
|
if (!maskColorFounded && pal->size() < 256) {
|
|
|
|
maskColorFounded = true;
|
2018-02-08 01:35:12 +08:00
|
|
|
|
2020-03-27 03:19:48 +08:00
|
|
|
Palette newPalette(*pal);
|
2020-03-26 01:32:43 +08:00
|
|
|
newPalette.addEntry(0);
|
2020-03-27 03:19:48 +08:00
|
|
|
ASSERT(newPalette.size() <= 256);
|
2018-02-08 01:35:12 +08:00
|
|
|
|
2020-03-27 03:19:48 +08:00
|
|
|
m_transparentIndex = newPalette.size() - 1;
|
|
|
|
m_globalColormapPalette = newPalette;
|
|
|
|
m_globalColormap = createColorMap(&m_globalColormapPalette);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
#endif
|
|
|
|
if (// If all colors are opaque/used in the sprite
|
|
|
|
!maskColorFounded &&
|
|
|
|
// We aren't obligated to preserve the original palette
|
|
|
|
!m_preservePaletteOrder &&
|
|
|
|
// And the sprite is transparent
|
|
|
|
!m_hasBackground) {
|
|
|
|
// We create a new palette with 255 colors + one extra entry
|
|
|
|
// for the transparent color
|
|
|
|
Palette newPalette(0, 255);
|
|
|
|
render::create_palette_from_sprite(
|
|
|
|
m_sprite,
|
|
|
|
0,
|
|
|
|
totalFrames()-1,
|
|
|
|
false,
|
|
|
|
&newPalette,
|
|
|
|
nullptr,
|
|
|
|
m_fop->newBlend(),
|
2020-06-18 10:52:35 +08:00
|
|
|
RgbMapAlgorithm::DEFAULT, // TODO configurable?
|
2020-03-27 03:19:48 +08:00
|
|
|
false); // Do not add the transparent color yet
|
|
|
|
|
|
|
|
// We will use the last palette entry (e.g. index=255) as the
|
|
|
|
// transparent index
|
|
|
|
newPalette.addEntry(0);
|
|
|
|
ASSERT(newPalette.size() <= 256);
|
|
|
|
|
|
|
|
m_transparentIndex = newPalette.size() - 1;
|
|
|
|
m_globalColormapPalette = newPalette;
|
2020-03-26 01:32:43 +08:00
|
|
|
m_globalColormap = createColorMap(&m_globalColormapPalette);
|
|
|
|
}
|
|
|
|
}
|
2015-07-23 03:40:44 +08:00
|
|
|
|
2020-03-27 03:19:48 +08:00
|
|
|
// Create the 3 temporary images (previous/current/next) to
|
|
|
|
// compare pixels between them.
|
2015-07-23 03:40:44 +08:00
|
|
|
for (int i=0; i<3; ++i)
|
2020-04-22 22:23:16 +08:00
|
|
|
m_images[i].reset(Image::create((m_preservePaletteOrder)? IMAGE_INDEXED : IMAGE_RGB,
|
2015-07-23 03:40:44 +08:00
|
|
|
m_spriteBounds.w,
|
|
|
|
m_spriteBounds.h));
|
|
|
|
}
|
|
|
|
|
|
|
|
~GifEncoder() {
|
|
|
|
if (m_globalColormap)
|
|
|
|
GifFreeMapObject(m_globalColormap);
|
|
|
|
}
|
2015-07-17 23:45:59 +08:00
|
|
|
|
2015-07-23 03:40:44 +08:00
|
|
|
bool encode() {
|
|
|
|
writeHeader();
|
|
|
|
if (m_loop >= 0)
|
|
|
|
writeLoopExtension();
|
|
|
|
|
|
|
|
// Previous and next images are used to decide the best disposal
|
|
|
|
// method (e.g. if it's more convenient to restore the background
|
|
|
|
// color or to restore the previous frame to reach the next one).
|
|
|
|
m_previousImage = m_images[0].get();
|
|
|
|
m_currentImage = m_images[1].get();
|
|
|
|
m_nextImage = m_images[2].get();
|
|
|
|
|
2016-09-17 12:01:28 +08:00
|
|
|
auto frame_beg = m_fop->roi().selectedFrames().begin();
|
2017-04-20 06:25:57 +08:00
|
|
|
#if _DEBUG
|
2016-09-17 12:01:28 +08:00
|
|
|
auto frame_end = m_fop->roi().selectedFrames().end();
|
2017-04-20 06:25:57 +08:00
|
|
|
#endif
|
2016-09-17 12:01:28 +08:00
|
|
|
auto frame_it = frame_beg;
|
|
|
|
|
|
|
|
// In this code "gifFrame" will be the GIF frame, and "frame" will
|
|
|
|
// be the doc::Sprite frame.
|
|
|
|
gifframe_t nframes = totalFrames();
|
|
|
|
for (gifframe_t gifFrame=0; gifFrame<nframes; ++gifFrame) {
|
|
|
|
ASSERT(frame_it != frame_end);
|
|
|
|
frame_t frame = *frame_it;
|
|
|
|
++frame_it;
|
|
|
|
|
|
|
|
if (gifFrame == 0)
|
|
|
|
renderFrame(frame, m_nextImage);
|
|
|
|
else
|
2015-07-23 03:40:44 +08:00
|
|
|
std::swap(m_previousImage, m_currentImage);
|
|
|
|
|
2016-09-17 12:01:28 +08:00
|
|
|
// Render next frame
|
2015-07-23 03:40:44 +08:00
|
|
|
std::swap(m_currentImage, m_nextImage);
|
2016-09-17 12:01:28 +08:00
|
|
|
if (gifFrame+1 < nframes)
|
|
|
|
renderFrame(*frame_it, m_nextImage);
|
2015-07-23 03:40:44 +08:00
|
|
|
|
2020-03-27 03:19:48 +08:00
|
|
|
gfx::Rect frameBounds = m_spriteBounds;
|
2020-03-26 01:32:43 +08:00
|
|
|
DisposalMethod disposal = DisposalMethod::DO_NOT_DISPOSE;
|
2015-07-23 03:40:44 +08:00
|
|
|
|
2020-03-27 03:19:48 +08:00
|
|
|
// Creation of the deltaImage (difference image result respect
|
|
|
|
// to current VS previous frame image). At the same time we
|
|
|
|
// must scan the next image, to check if some pixel turns to
|
|
|
|
// transparent (0), if the case, we need to force disposal
|
|
|
|
// method of the current image to RESTORE_BG. Further, at the
|
|
|
|
// same time, we must check if we can go without color zero (0).
|
2015-07-23 03:40:44 +08:00
|
|
|
|
2020-03-26 01:32:43 +08:00
|
|
|
calculateDeltaImageFrameBoundsDisposal(gifFrame, frameBounds, disposal);
|
2015-07-23 03:40:44 +08:00
|
|
|
|
2018-03-16 22:03:50 +08:00
|
|
|
writeImage(gifFrame, frame, frameBounds, disposal,
|
|
|
|
// Only the last frame in the animation needs the fix
|
|
|
|
(fix_last_frame_duration && gifFrame == nframes-1));
|
2015-07-23 03:40:44 +08:00
|
|
|
|
2016-09-17 12:01:28 +08:00
|
|
|
m_fop->setProgress(double(gifFrame+1) / double(nframes));
|
2014-07-20 09:01:39 +08:00
|
|
|
}
|
2015-07-23 03:40:44 +08:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
2007-09-19 07:57:02 +08:00
|
|
|
|
2020-03-26 01:32:43 +08:00
|
|
|
void calculateDeltaImageFrameBoundsDisposal(gifframe_t gifFrame,
|
|
|
|
gfx::Rect& frameBounds,
|
|
|
|
DisposalMethod& disposal) {
|
|
|
|
if (gifFrame == 0) {
|
|
|
|
m_deltaImage.reset(Image::createCopy(m_currentImage));
|
2020-03-27 03:19:48 +08:00
|
|
|
frameBounds = m_spriteBounds;
|
|
|
|
|
2020-03-26 01:32:43 +08:00
|
|
|
// The first frame (frame 0) is good to force to disposal = DO_NOT_DISPOSE,
|
2020-03-27 03:19:48 +08:00
|
|
|
// but when the next frame (frame 1) has a "pixel clearing",
|
2020-03-26 01:32:43 +08:00
|
|
|
// we must change disposal to RESTORE_BGCOLOR.
|
|
|
|
|
2020-03-27 03:19:48 +08:00
|
|
|
// "Pixel clearing" detection:
|
2020-04-22 22:23:16 +08:00
|
|
|
if (!m_hasBackground && !m_preservePaletteOrder) {
|
2020-03-26 01:32:43 +08:00
|
|
|
const LockImageBits<RgbTraits> bits2(m_currentImage);
|
|
|
|
const LockImageBits<RgbTraits> bits3(m_nextImage);
|
|
|
|
typename LockImageBits<RgbTraits>::const_iterator it2, it3, end2, end3;
|
|
|
|
for (it2 = bits2.begin(), end2 = bits2.end(),
|
|
|
|
it3 = bits3.begin(), end3 = bits3.end();
|
|
|
|
it2 != end2 && it3 != end3; ++it2, ++it3) {
|
|
|
|
if (*it2 != 0 && *it3 == 0) {
|
|
|
|
disposal = DisposalMethod::RESTORE_BGCOLOR;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-04-22 22:23:16 +08:00
|
|
|
else if (m_preservePaletteOrder)
|
|
|
|
disposal = DisposalMethod::RESTORE_BGCOLOR;
|
2020-03-26 01:32:43 +08:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
int x1 = 0;
|
|
|
|
int y1 = 0;
|
|
|
|
int x2 = 0;
|
|
|
|
int y2 = 0;
|
|
|
|
|
2020-04-22 22:23:16 +08:00
|
|
|
if (!m_preservePaletteOrder) {
|
|
|
|
// When m_lastDisposal was RESTORE_BGBOLOR it implies
|
|
|
|
// we will have to cover with colors the entire previous frameBounds plus
|
|
|
|
// the current frameBounds due to color changes, so we must start with
|
|
|
|
// a frameBounds equal to the previous frame iteration (saved in m_lastFrameBounds).
|
|
|
|
// Then we must cover all the resultant frameBounds with full color
|
|
|
|
// in m_currentImage, the output image will be saved in deltaImage.
|
|
|
|
if (m_lastDisposal == DisposalMethod::RESTORE_BGCOLOR) {
|
|
|
|
x1 = m_lastFrameBounds.x;
|
|
|
|
y1 = m_lastFrameBounds.y;
|
|
|
|
x2 = m_lastFrameBounds.x + m_lastFrameBounds.w - 1;
|
|
|
|
y2 = m_lastFrameBounds.y + m_lastFrameBounds.h - 1;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
x1 = m_spriteBounds.w - 1;
|
|
|
|
y1 = m_spriteBounds.h - 1;
|
2020-03-26 01:32:43 +08:00
|
|
|
}
|
|
|
|
|
2020-04-22 22:23:16 +08:00
|
|
|
int i = 0;
|
|
|
|
int x, y;
|
|
|
|
const LockImageBits<RgbTraits> bits1(m_previousImage);
|
|
|
|
const LockImageBits<RgbTraits> bits2(m_currentImage);
|
|
|
|
const LockImageBits<RgbTraits> bits3(m_nextImage);
|
|
|
|
m_deltaImage.reset(Image::create(PixelFormat::IMAGE_RGB, m_spriteBounds.w, m_spriteBounds.h));
|
|
|
|
clear_image(m_deltaImage.get(), 0);
|
|
|
|
LockImageBits<RgbTraits> deltaBits(m_deltaImage.get());
|
|
|
|
typename LockImageBits<RgbTraits>::iterator deltaIt;
|
|
|
|
typename LockImageBits<RgbTraits>::const_iterator it1, it2, it3, end1, end2, end3, deltaEnd;
|
|
|
|
|
2020-06-10 04:06:03 +08:00
|
|
|
bool previousImageMatchsCurrent = true;
|
2020-04-22 22:23:16 +08:00
|
|
|
for (it1 = bits1.begin(), end1 = bits1.end(),
|
|
|
|
it2 = bits2.begin(), end2 = bits2.end(),
|
|
|
|
it3 = bits3.begin(), end2 = bits3.end(),
|
|
|
|
deltaIt = deltaBits.begin();
|
|
|
|
it1 != end1 && it2 != end2; ++it1, ++it2, ++it3, ++deltaIt, ++i) {
|
|
|
|
x = i % m_spriteBounds.w;
|
|
|
|
y = i / m_spriteBounds.w;
|
|
|
|
// While we are checking color differences,
|
|
|
|
// we enlarge the frameBounds where the color differences take place
|
|
|
|
if (*it1 != *it2 || *it3 == 0) {
|
2020-06-10 04:06:03 +08:00
|
|
|
previousImageMatchsCurrent = false;
|
2020-04-22 22:23:16 +08:00
|
|
|
*deltaIt = *it2;
|
|
|
|
if (x < x1) x1 = x;
|
|
|
|
if (x > x2) x2 = x;
|
|
|
|
if (y < y1) y1 = y;
|
|
|
|
if (y > y2) y2 = y;
|
|
|
|
}
|
|
|
|
|
|
|
|
// We need to change disposal mode DO_NOT_DISPOSE to RESTORE_BGCOLOR only
|
|
|
|
// if we found a "pixel clearing" in the next Image. RESTORE_BGCOLOR is
|
|
|
|
// our way to clear pixels.
|
|
|
|
if (*it2 != 0 && *it3 == 0) {
|
|
|
|
disposal = DisposalMethod::RESTORE_BGCOLOR;
|
|
|
|
}
|
2020-03-26 01:32:43 +08:00
|
|
|
}
|
2020-06-10 04:06:03 +08:00
|
|
|
if (previousImageMatchsCurrent)
|
|
|
|
frameBounds = gfx::Rect(m_lastFrameBounds);
|
|
|
|
else
|
|
|
|
frameBounds = gfx::Rect(x1, y1, x2-x1+1, y2-y1+1);
|
2020-03-26 01:32:43 +08:00
|
|
|
}
|
2020-04-22 22:23:16 +08:00
|
|
|
else
|
2020-03-26 01:32:43 +08:00
|
|
|
disposal = DisposalMethod::RESTORE_BGCOLOR;
|
|
|
|
|
|
|
|
// We need to conditionate the deltaImage to the next step: 'writeImage()'
|
|
|
|
// To do it, we need to crop deltaImage in frameBounds.
|
|
|
|
// If disposal method changed to RESTORE_BGCOLOR deltaImage we need to reproduce ALL the colors of m_currentImage
|
|
|
|
// contained in frameBounds (so, we will overwrite delta image with a cropped current image).
|
|
|
|
// In the other hand, if disposal is still DO_NOT_DISPOSAL, delta image will be a cropped image
|
|
|
|
// from itself in frameBounds.
|
|
|
|
if (disposal == DisposalMethod::RESTORE_BGCOLOR || m_lastDisposal == DisposalMethod::RESTORE_BGCOLOR) {
|
|
|
|
m_deltaImage.reset(crop_image(m_currentImage, frameBounds, 0));
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
m_deltaImage.reset(crop_image(m_deltaImage.get(), frameBounds, 0));
|
|
|
|
disposal = DisposalMethod::DO_NOT_DISPOSE;
|
|
|
|
}
|
2020-06-10 04:06:03 +08:00
|
|
|
m_lastFrameBounds = frameBounds;
|
2020-03-26 01:32:43 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// TODO We could join both frames in a longer one (with more duration)
|
|
|
|
if (frameBounds.isEmpty())
|
|
|
|
frameBounds = gfx::Rect(0, 0, 1, 1);
|
|
|
|
|
|
|
|
m_lastDisposal = disposal;
|
2020-03-27 03:19:48 +08:00
|
|
|
}
|
2020-03-26 01:32:43 +08:00
|
|
|
|
2016-06-02 04:25:09 +08:00
|
|
|
doc::frame_t totalFrames() const {
|
|
|
|
return m_fop->roi().frames();
|
|
|
|
}
|
|
|
|
|
2015-07-23 03:40:44 +08:00
|
|
|
void writeHeader() {
|
|
|
|
if (EGifPutScreenDesc(m_gifFile,
|
|
|
|
m_spriteBounds.w,
|
|
|
|
m_spriteBounds.h,
|
|
|
|
m_bitsPerPixel,
|
|
|
|
m_bgIndex, m_globalColormap) == GIF_ERROR)
|
|
|
|
throw Exception("Error writing GIF header.\n");
|
2014-07-20 09:01:39 +08:00
|
|
|
}
|
|
|
|
|
2015-07-23 03:40:44 +08:00
|
|
|
void writeLoopExtension() {
|
|
|
|
#if GIFLIB_MAJOR >= 5
|
|
|
|
if (EGifPutExtensionLeader(m_gifFile, APPLICATION_EXT_FUNC_CODE) == GIF_ERROR)
|
|
|
|
throw Exception("Error writing GIF graphics extension record (header section).");
|
2007-09-19 07:57:02 +08:00
|
|
|
|
2015-07-23 03:40:44 +08:00
|
|
|
unsigned char extension_bytes[11];
|
|
|
|
memcpy(extension_bytes, "NETSCAPE2.0", 11);
|
|
|
|
if (EGifPutExtensionBlock(m_gifFile, 11, extension_bytes) == GIF_ERROR)
|
|
|
|
throw Exception("Error writing GIF graphics extension record (first block).");
|
2007-09-19 07:57:02 +08:00
|
|
|
|
2015-07-23 03:40:44 +08:00
|
|
|
extension_bytes[0] = 1;
|
|
|
|
extension_bytes[1] = (m_loop & 0xff);
|
|
|
|
extension_bytes[2] = (m_loop >> 8) & 0xff;
|
|
|
|
if (EGifPutExtensionBlock(m_gifFile, 3, extension_bytes) == GIF_ERROR)
|
|
|
|
throw Exception("Error writing GIF graphics extension record (second block).");
|
2011-01-19 07:42:43 +08:00
|
|
|
|
2015-07-23 03:40:44 +08:00
|
|
|
if (EGifPutExtensionTrailer(m_gifFile) == GIF_ERROR)
|
|
|
|
throw Exception("Error writing GIF graphics extension record (trailer section).");
|
2011-01-19 07:42:43 +08:00
|
|
|
|
2015-07-23 03:40:44 +08:00
|
|
|
#else
|
|
|
|
unsigned char extension_bytes[11];
|
2011-01-19 07:42:43 +08:00
|
|
|
|
2015-07-23 03:40:44 +08:00
|
|
|
memcpy(extension_bytes, "NETSCAPE2.0", 11);
|
|
|
|
if (EGifPutExtensionFirst(m_gifFile, APPLICATION_EXT_FUNC_CODE, 11, extension_bytes) == GIF_ERROR)
|
2015-07-23 04:41:14 +08:00
|
|
|
throw Exception("Error writing GIF graphics extension record.\n");
|
2014-12-28 22:06:11 +08:00
|
|
|
|
2015-07-23 03:40:44 +08:00
|
|
|
extension_bytes[0] = 1;
|
|
|
|
extension_bytes[1] = (m_loop & 0xff);
|
|
|
|
extension_bytes[2] = (m_loop >> 8) & 0xff;
|
|
|
|
if (EGifPutExtensionNext(m_gifFile, APPLICATION_EXT_FUNC_CODE, 3, extension_bytes) == GIF_ERROR)
|
2015-07-23 04:41:14 +08:00
|
|
|
throw Exception("Error writing GIF graphics extension record.\n");
|
2015-07-23 03:40:44 +08:00
|
|
|
|
|
|
|
if (EGifPutExtensionLast(m_gifFile, APPLICATION_EXT_FUNC_CODE, 0, NULL) == GIF_ERROR)
|
2015-07-23 04:41:14 +08:00
|
|
|
throw Exception("Error writing GIF graphics extension record.\n");
|
2015-07-23 03:40:44 +08:00
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
|
|
|
// Writes graphics extension record (to save the duration of the
|
|
|
|
// frame and maybe the transparency index).
|
2018-03-16 22:03:50 +08:00
|
|
|
void writeExtension(const gifframe_t gifFrame,
|
|
|
|
const frame_t frame,
|
|
|
|
const int transparentIndex,
|
|
|
|
const DisposalMethod disposalMethod,
|
|
|
|
const bool fixDuration) {
|
2015-07-23 03:40:44 +08:00
|
|
|
unsigned char extension_bytes[5];
|
2016-09-17 12:01:28 +08:00
|
|
|
int frameDelay = m_sprite->frameDuration(frame) / 10;
|
2014-08-09 23:51:11 +08:00
|
|
|
|
2018-03-16 22:03:50 +08:00
|
|
|
// Fix duration for Twitter. It looks like the last frame must be
|
|
|
|
// 1/4 of its duration for some strange reason in the Twitter
|
|
|
|
// conversion from GIF to video.
|
|
|
|
if (fixDuration)
|
2020-04-08 23:03:32 +08:00
|
|
|
frameDelay = std::max(2, frameDelay/4);
|
2018-03-16 22:03:50 +08:00
|
|
|
if (fix_last_frame_duration)
|
2020-04-08 23:03:32 +08:00
|
|
|
frameDelay = std::max(2, frameDelay);
|
2018-03-16 22:03:50 +08:00
|
|
|
|
2015-07-23 03:40:44 +08:00
|
|
|
extension_bytes[0] = (((int(disposalMethod) & 7) << 2) |
|
|
|
|
(transparentIndex >= 0 ? 1: 0));
|
|
|
|
extension_bytes[1] = (frameDelay & 0xff);
|
|
|
|
extension_bytes[2] = (frameDelay >> 8) & 0xff;
|
|
|
|
extension_bytes[3] = (transparentIndex >= 0 ? transparentIndex: 0);
|
2014-08-09 23:51:11 +08:00
|
|
|
|
2015-07-23 03:40:44 +08:00
|
|
|
if (EGifPutExtension(m_gifFile, GRAPHICS_EXT_FUNC_CODE, 4, extension_bytes) == GIF_ERROR)
|
2016-09-17 12:01:28 +08:00
|
|
|
throw Exception("Error writing GIF graphics extension record for frame %d.\n", gifFrame);
|
2014-08-09 23:51:11 +08:00
|
|
|
}
|
|
|
|
|
2015-07-23 03:40:44 +08:00
|
|
|
static gfx::Rect calculateFrameBounds(Image* a, Image* b) {
|
|
|
|
gfx::Rect frameBounds;
|
|
|
|
int x1, y1, x2, y2;
|
2011-01-19 07:42:43 +08:00
|
|
|
|
2015-07-23 03:40:44 +08:00
|
|
|
if (get_shrink_rect2(&x1, &y1, &x2, &y2, a, b)) {
|
|
|
|
frameBounds.x = x1;
|
|
|
|
frameBounds.y = y1;
|
|
|
|
frameBounds.w = x2 - x1 + 1;
|
|
|
|
frameBounds.h = y2 - y1 + 1;
|
|
|
|
}
|
2014-07-20 09:01:39 +08:00
|
|
|
|
2015-07-23 03:40:44 +08:00
|
|
|
return frameBounds;
|
|
|
|
}
|
|
|
|
|
2007-09-19 07:57:02 +08:00
|
|
|
|
2018-03-16 22:03:50 +08:00
|
|
|
void writeImage(const gifframe_t gifFrame,
|
|
|
|
const frame_t frame,
|
|
|
|
const gfx::Rect& frameBounds,
|
|
|
|
const DisposalMethod disposal,
|
|
|
|
const bool fixDuration) {
|
2020-03-27 03:19:48 +08:00
|
|
|
Palette framePalette;
|
|
|
|
if (m_globalColormap)
|
|
|
|
framePalette = m_globalColormapPalette;
|
2020-03-26 01:32:43 +08:00
|
|
|
else
|
2020-03-27 03:19:48 +08:00
|
|
|
framePalette = calculatePalette(frameBounds, disposal);
|
2014-06-04 11:27:34 +08:00
|
|
|
|
2020-06-18 10:52:35 +08:00
|
|
|
RgbMapRGB5A3 rgbmap; // TODO RgbMapRGB5A3 configurable?
|
|
|
|
rgbmap.regenerateMap(&framePalette, m_transparentIndex);
|
2015-06-09 07:21:50 +08:00
|
|
|
|
2015-07-23 03:40:44 +08:00
|
|
|
ImageRef frameImage(Image::create(IMAGE_INDEXED,
|
|
|
|
frameBounds.w,
|
|
|
|
frameBounds.h,
|
|
|
|
m_frameImageBuf));
|
2015-06-09 07:21:50 +08:00
|
|
|
|
2020-03-26 01:32:43 +08:00
|
|
|
// Every frame might use a small portion of the global palette,
|
|
|
|
// to optimize the gif file size, we will analize which colors
|
|
|
|
// will be used in each processed frame.
|
|
|
|
PalettePicks usedColors(framePalette.size());
|
2015-09-23 01:39:47 +08:00
|
|
|
|
2020-04-22 22:23:16 +08:00
|
|
|
int localTransparent = m_transparentIndex;
|
|
|
|
ColorMapObject* colormap = m_globalColormap;
|
|
|
|
Remap remap(256);
|
2015-09-23 01:39:47 +08:00
|
|
|
|
2020-04-22 22:23:16 +08:00
|
|
|
if (!m_preservePaletteOrder) {
|
2020-03-26 01:32:43 +08:00
|
|
|
const LockImageBits<RgbTraits> srcBits(m_deltaImage.get());
|
|
|
|
LockImageBits<IndexedTraits> dstBits(frameImage.get());
|
2017-09-27 04:58:55 +08:00
|
|
|
|
|
|
|
auto srcIt = srcBits.begin();
|
|
|
|
auto dstIt = dstBits.begin();
|
|
|
|
|
2015-07-23 03:40:44 +08:00
|
|
|
for (int y=0; y<frameBounds.h; ++y) {
|
2017-09-27 04:58:55 +08:00
|
|
|
for (int x=0; x<frameBounds.w; ++x, ++srcIt, ++dstIt) {
|
|
|
|
ASSERT(srcIt != srcBits.end());
|
|
|
|
ASSERT(dstIt != dstBits.end());
|
2015-07-23 03:40:44 +08:00
|
|
|
|
2017-09-27 04:58:55 +08:00
|
|
|
color_t color = *srcIt;
|
2015-07-23 03:40:44 +08:00
|
|
|
int i;
|
|
|
|
|
|
|
|
if (rgba_geta(color) >= 128) {
|
2020-03-26 01:32:43 +08:00
|
|
|
i = framePalette.findExactMatch(
|
2015-07-23 03:40:44 +08:00
|
|
|
rgba_getr(color),
|
|
|
|
rgba_getg(color),
|
|
|
|
rgba_getb(color),
|
|
|
|
255,
|
|
|
|
m_transparentIndex);
|
|
|
|
if (i < 0)
|
2020-06-18 10:52:35 +08:00
|
|
|
i = rgbmap.mapColor(color | rgba_a_mask); // alpha=255
|
2015-07-23 03:40:44 +08:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
if (m_transparentIndex >= 0)
|
|
|
|
i = m_transparentIndex;
|
|
|
|
else
|
|
|
|
i = m_bgIndex;
|
|
|
|
}
|
2015-06-09 07:21:50 +08:00
|
|
|
|
2015-07-23 03:40:44 +08:00
|
|
|
ASSERT(i >= 0);
|
2015-06-09 07:21:50 +08:00
|
|
|
|
2015-07-23 03:40:44 +08:00
|
|
|
// This can happen when transparent color is outside the
|
|
|
|
// palette range (TODO something that shouldn't be possible
|
|
|
|
// from the program).
|
|
|
|
if (i >= usedColors.size())
|
|
|
|
usedColors.resize(i+1);
|
|
|
|
usedColors[i] = true;
|
2014-06-04 11:27:34 +08:00
|
|
|
|
2017-09-27 04:58:55 +08:00
|
|
|
*dstIt = i;
|
2015-07-23 03:40:44 +08:00
|
|
|
}
|
2011-01-19 07:42:43 +08:00
|
|
|
}
|
2014-07-20 09:01:39 +08:00
|
|
|
|
2020-04-22 22:23:16 +08:00
|
|
|
int usedNColors = usedColors.picks();
|
2015-07-17 23:45:59 +08:00
|
|
|
|
2020-04-22 22:23:16 +08:00
|
|
|
for (int i=0; i<remap.size(); ++i)
|
|
|
|
remap.map(i, i);
|
2015-07-23 03:40:44 +08:00
|
|
|
|
2020-04-22 22:23:16 +08:00
|
|
|
if (!colormap) {
|
|
|
|
Palette reducedPalette(0, usedNColors);
|
2015-07-23 03:40:44 +08:00
|
|
|
|
2020-04-22 22:23:16 +08:00
|
|
|
for (int i=0, j=0; i<framePalette.size(); ++i) {
|
|
|
|
if (usedColors[i]) {
|
|
|
|
reducedPalette.setEntry(j, framePalette.getEntry(i));
|
|
|
|
remap.map(i, j);
|
|
|
|
++j;
|
|
|
|
}
|
2015-07-23 03:40:44 +08:00
|
|
|
}
|
2020-04-22 22:23:16 +08:00
|
|
|
|
|
|
|
colormap = createColorMap(&reducedPalette);
|
|
|
|
if (localTransparent >= 0)
|
|
|
|
localTransparent = remap[localTransparent];
|
2014-07-20 09:01:39 +08:00
|
|
|
}
|
|
|
|
|
2020-04-22 22:23:16 +08:00
|
|
|
if (localTransparent >= 0 && m_transparentIndex != localTransparent)
|
|
|
|
remap.map(m_transparentIndex, localTransparent);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
frameImage.reset(Image::createCopy(m_deltaImage.get()));
|
|
|
|
for (int i=0; i<colormap->ColorCount; ++i)
|
|
|
|
remap.map(i, i);
|
2011-01-19 07:42:43 +08:00
|
|
|
}
|
2015-07-23 03:40:44 +08:00
|
|
|
|
|
|
|
// Write extension record.
|
2018-03-16 22:03:50 +08:00
|
|
|
writeExtension(gifFrame, frame, localTransparent,
|
|
|
|
disposal, fixDuration);
|
2015-07-23 03:40:44 +08:00
|
|
|
|
2011-01-19 07:42:43 +08:00
|
|
|
// Write the image record.
|
2015-07-23 03:40:44 +08:00
|
|
|
if (EGifPutImageDesc(m_gifFile,
|
|
|
|
frameBounds.x, frameBounds.y,
|
|
|
|
frameBounds.w, frameBounds.h,
|
|
|
|
m_interlaced ? 1: 0,
|
2015-07-31 23:49:04 +08:00
|
|
|
(colormap != m_globalColormap ? colormap: nullptr)) == GIF_ERROR) {
|
2016-09-17 12:01:28 +08:00
|
|
|
throw Exception("Error writing GIF frame %d.\n", gifFrame);
|
2015-07-23 03:40:44 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
std::vector<uint8_t> scanline(frameBounds.w);
|
2011-01-19 07:42:43 +08:00
|
|
|
|
|
|
|
// Write the image data (pixels).
|
2015-07-23 03:40:44 +08:00
|
|
|
if (m_interlaced) {
|
2011-01-19 07:42:43 +08:00
|
|
|
// Need to perform 4 passes on the images.
|
|
|
|
for (int i=0; i<4; ++i)
|
2015-07-23 03:40:44 +08:00
|
|
|
for (int y=interlaced_offset[i]; y<frameBounds.h; y+=interlaced_jumps[i]) {
|
2013-11-10 06:59:05 +08:00
|
|
|
IndexedTraits::address_t addr =
|
2015-07-23 03:40:44 +08:00
|
|
|
(IndexedTraits::address_t)frameImage->getPixelAddress(0, y);
|
2013-11-10 06:59:05 +08:00
|
|
|
|
2015-07-23 03:40:44 +08:00
|
|
|
for (int i=0; i<frameBounds.w; ++i, ++addr)
|
|
|
|
scanline[i] = remap[*addr];
|
|
|
|
|
|
|
|
if (EGifPutLine(m_gifFile, &scanline[0], frameBounds.w) == GIF_ERROR)
|
2016-09-17 12:01:28 +08:00
|
|
|
throw Exception("Error writing GIF image scanlines for frame %d.\n", gifFrame);
|
2012-01-06 06:45:03 +08:00
|
|
|
}
|
2011-01-19 07:42:43 +08:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
// Write all image scanlines (not interlaced in this case).
|
2015-07-23 03:40:44 +08:00
|
|
|
for (int y=0; y<frameBounds.h; ++y) {
|
2013-11-10 06:59:05 +08:00
|
|
|
IndexedTraits::address_t addr =
|
2015-07-23 03:40:44 +08:00
|
|
|
(IndexedTraits::address_t)frameImage->getPixelAddress(0, y);
|
|
|
|
|
|
|
|
for (int i=0; i<frameBounds.w; ++i, ++addr)
|
|
|
|
scanline[i] = remap[*addr];
|
2013-11-10 06:59:05 +08:00
|
|
|
|
2015-07-23 03:40:44 +08:00
|
|
|
if (EGifPutLine(m_gifFile, &scanline[0], frameBounds.w) == GIF_ERROR)
|
2016-09-17 12:01:28 +08:00
|
|
|
throw Exception("Error writing GIF image scanlines for frame %d.\n", gifFrame);
|
2007-09-19 07:57:02 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-07-23 03:40:44 +08:00
|
|
|
if (colormap && colormap != m_globalColormap)
|
|
|
|
GifFreeMapObject(colormap);
|
2007-09-19 07:57:02 +08:00
|
|
|
}
|
2007-09-30 23:32:21 +08:00
|
|
|
|
2020-03-27 03:19:48 +08:00
|
|
|
Palette calculatePalette(const gfx::Rect& frameBounds,
|
|
|
|
const DisposalMethod disposal) {
|
2020-03-26 01:32:43 +08:00
|
|
|
// First, we must check the palette color count in m_deltaImage (our best shot
|
|
|
|
// to find the smaller palette color count)
|
2020-03-27 03:19:48 +08:00
|
|
|
Palette pal(createOptimizedPalette(m_deltaImage.get(), m_deltaImage->bounds(), 256));
|
2020-03-26 01:32:43 +08:00
|
|
|
if (pal.size() == 256) {
|
2020-03-27 03:19:48 +08:00
|
|
|
// Here the palette has 256 colors, there is no place to include
|
|
|
|
// the 0 color (createOptimizedPalette() doesn't create an entry
|
|
|
|
// for it).
|
2020-03-26 01:32:43 +08:00
|
|
|
//
|
|
|
|
// We have two paths:
|
|
|
|
// 1- Giving a try to palette generation on m_currentImage in frameBouns limits.
|
|
|
|
// 2- If the previous step is not possible (color count > 256), we will to start
|
|
|
|
// to approximate colors from m_deltaImage with some criterion. Final target:
|
|
|
|
// to approximate the palette to 255 colors + clear color (0)).
|
|
|
|
|
|
|
|
// 1- Giving a try to palette generation on m_currentImage in frameBouns limits.
|
|
|
|
// if disposal == RESTORE_BGCOLOR m_deltaImage already is a cropped copy of m_currentImage.
|
2020-03-27 03:19:48 +08:00
|
|
|
Palette auxPalette;
|
2020-03-26 01:32:43 +08:00
|
|
|
if (disposal == DisposalMethod::DO_NOT_DISPOSE)
|
2020-03-27 03:19:48 +08:00
|
|
|
auxPalette = createOptimizedPalette(m_currentImage, frameBounds, 257);
|
|
|
|
else
|
|
|
|
auxPalette = pal;
|
2020-03-26 01:32:43 +08:00
|
|
|
|
|
|
|
if (auxPalette.size() <= 256) {
|
2020-03-27 03:19:48 +08:00
|
|
|
// We are fine with color count in m_currentImage contained in
|
|
|
|
// frameBounds (we got 256 or less colors):
|
2020-03-26 01:32:43 +08:00
|
|
|
m_transparentIndex = -1;
|
2020-03-27 03:19:48 +08:00
|
|
|
pal = auxPalette;
|
2020-06-10 04:06:03 +08:00
|
|
|
if (disposal == DisposalMethod::DO_NOT_DISPOSE) {
|
|
|
|
ASSERT(frameBounds.w >= 1);
|
2020-03-26 01:32:43 +08:00
|
|
|
m_deltaImage.reset(crop_image(m_currentImage, frameBounds, 0));
|
2020-06-10 04:06:03 +08:00
|
|
|
}
|
2020-03-26 01:32:43 +08:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
// 2- If the previous step fails, we will to start to approximate colors from m_deltaImage
|
|
|
|
// with some criterion:
|
|
|
|
|
|
|
|
// Final target: to approximate the palette to 255 colors + clear color (0)).
|
|
|
|
// CRITERION:
|
2020-04-02 20:16:05 +08:00
|
|
|
// Find a palette of 220 or less colors (in high precision) into the square border
|
|
|
|
// contained in m_deltaImage, then into the center square quantize the remaining colors
|
|
|
|
// to complete a palette of 255 colors, finally add the transparent color (0).
|
2020-03-26 01:32:43 +08:00
|
|
|
//
|
2020-04-02 20:16:05 +08:00
|
|
|
// m_currentImage__ __ m_deltaImage (same rectangle size as `frameBounds` variable)
|
2020-03-26 01:32:43 +08:00
|
|
|
// | |
|
2020-04-02 20:16:05 +08:00
|
|
|
// --------------*----|-----------
|
2020-03-26 01:32:43 +08:00
|
|
|
// | | |
|
|
|
|
// | --------------*- |
|
2020-04-02 20:16:05 +08:00
|
|
|
// | | | |
|
2020-03-26 01:32:43 +08:00
|
|
|
// | | ________ | |
|
2020-04-02 20:16:05 +08:00
|
|
|
// | | | | *--------------- square border (we will collect
|
|
|
|
// | | | | | | high precision colors from this area, less than 220)
|
2020-03-26 01:32:43 +08:00
|
|
|
// | | | | | |
|
2020-04-02 20:16:05 +08:00
|
|
|
// | | | *--------------------- center rectangle (we will to quantize
|
|
|
|
// | | | | | | colors contained in this area)
|
|
|
|
// | | |________| | |
|
|
|
|
// | | | |
|
2020-03-26 01:32:43 +08:00
|
|
|
// | |________________| |
|
|
|
|
// | |
|
|
|
|
// |_______________________________|
|
|
|
|
//
|
|
|
|
|
2020-03-27 03:19:48 +08:00
|
|
|
const gfx::Size deltaSize = m_deltaImage->size();
|
|
|
|
int thicknessTop = deltaSize.h / 4;
|
|
|
|
int thicknessLeft = deltaSize.w / 4;
|
2020-04-11 03:13:18 +08:00
|
|
|
int repeatCounter = 0;
|
|
|
|
while (repeatCounter < 10 && thicknessTop > 0 && thicknessLeft > 0) {
|
2020-03-26 01:32:43 +08:00
|
|
|
|
|
|
|
// ----------------
|
|
|
|
// |________________|
|
|
|
|
// | | | |
|
|
|
|
// | | | |
|
|
|
|
// | |________| |
|
|
|
|
// |________________|
|
2020-04-02 20:16:05 +08:00
|
|
|
render::PaletteOptimizer optimizer;
|
|
|
|
gfx::Rect auxRect(0, 0, deltaSize.w, thicknessTop);
|
2020-03-27 03:19:48 +08:00
|
|
|
optimizer.feedWithImage(m_deltaImage.get(), auxRect, false);
|
2020-04-02 20:16:05 +08:00
|
|
|
|
2020-03-26 01:32:43 +08:00
|
|
|
// ----------------
|
|
|
|
// | ________ |
|
|
|
|
// | | | |
|
|
|
|
// | | | |
|
|
|
|
// |___|________|___|
|
|
|
|
// |________________|
|
2020-04-02 20:16:05 +08:00
|
|
|
auxRect = gfx::Rect(0, deltaSize.h - thicknessTop - 1, deltaSize.w, thicknessTop);
|
2020-03-27 03:19:48 +08:00
|
|
|
optimizer.feedWithImage(m_deltaImage.get(), auxRect, false);
|
2020-04-02 20:16:05 +08:00
|
|
|
|
2020-03-26 01:32:43 +08:00
|
|
|
// ----------------
|
|
|
|
// |____________ |
|
|
|
|
// | | | |
|
|
|
|
// | | | |
|
|
|
|
// |___|________| |
|
|
|
|
// |________________|
|
2020-04-02 20:16:05 +08:00
|
|
|
auxRect = gfx::Rect(0, thicknessTop, thicknessLeft, deltaSize.h - 2 * thicknessTop);
|
2020-03-27 03:19:48 +08:00
|
|
|
optimizer.feedWithImage(m_deltaImage.get(), auxRect, false);
|
2020-04-02 20:16:05 +08:00
|
|
|
|
2020-03-26 01:32:43 +08:00
|
|
|
// ----------------
|
|
|
|
// | _____________|
|
|
|
|
// | | | |
|
|
|
|
// | | | |
|
|
|
|
// | |________|___|
|
|
|
|
// |________________|
|
2020-04-02 20:16:05 +08:00
|
|
|
auxRect = gfx::Rect(deltaSize.w - thicknessLeft - 1, thicknessTop, thicknessLeft, deltaSize.h - 2 * thicknessTop);
|
|
|
|
optimizer.feedWithImage(m_deltaImage.get(), auxRect, false);
|
2020-03-26 01:32:43 +08:00
|
|
|
|
2020-04-11 03:13:18 +08:00
|
|
|
int maxBorderColorCount = 220;
|
|
|
|
if (optimizer.isHighPrecision() && (optimizer.highPrecisionSize() < maxBorderColorCount)) {
|
|
|
|
pal.resize(optimizer.highPrecisionSize());
|
|
|
|
optimizer.calculate(&pal, -1);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
else if (thicknessTop <= 1 || thicknessLeft <= 1) {
|
|
|
|
pal.resize(0);
|
|
|
|
thicknessTop = 0;
|
|
|
|
thicknessLeft = 0;
|
|
|
|
break;
|
2020-03-26 01:32:43 +08:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
thicknessTop -= thicknessTop / 2;
|
|
|
|
thicknessLeft -= thicknessLeft / 2;
|
|
|
|
}
|
|
|
|
|
2020-04-11 03:13:18 +08:00
|
|
|
repeatCounter++;
|
2020-03-26 01:32:43 +08:00
|
|
|
}
|
2020-04-02 20:16:05 +08:00
|
|
|
// Quantize the colors contained into center rectangle and add these in `pal`:
|
|
|
|
if (pal.size() < 255) {
|
2020-04-11 03:13:18 +08:00
|
|
|
gfx::Rect centerRect(thicknessLeft,
|
|
|
|
thicknessTop,
|
|
|
|
deltaSize.w - 2 * thicknessLeft,
|
|
|
|
deltaSize.h - 2 * thicknessTop);
|
2020-04-02 20:16:05 +08:00
|
|
|
Palette centerPalette(0, 255 - pal.size());
|
|
|
|
centerPalette = createOptimizedPalette(m_deltaImage.get(),
|
|
|
|
centerRect, 255 - pal.size());
|
|
|
|
for (int i=0; i < centerPalette.size(); i++)
|
|
|
|
pal.addEntry(centerPalette.getEntry(i));
|
|
|
|
}
|
|
|
|
// Finally add transparent color:
|
|
|
|
ASSERT(pal.size() <= 255);
|
2020-03-26 01:32:43 +08:00
|
|
|
pal.addEntry(0);
|
|
|
|
m_transparentIndex = pal.size() - 1;
|
|
|
|
}
|
|
|
|
}
|
2020-03-27 03:19:48 +08:00
|
|
|
// We are fine, we got 255 or less, there is room for the transparent color
|
2020-03-26 01:32:43 +08:00
|
|
|
else if (pal.size() <= 255) {
|
|
|
|
pal.addEntry(0);
|
|
|
|
m_transparentIndex = pal.size() - 1;
|
|
|
|
}
|
|
|
|
return pal;
|
|
|
|
}
|
|
|
|
|
2020-03-27 03:19:48 +08:00
|
|
|
static Palette createOptimizedPalette(const Image* image,
|
|
|
|
const gfx::Rect& bounds,
|
|
|
|
const int ncolors) {
|
2015-07-23 03:40:44 +08:00
|
|
|
render::PaletteOptimizer optimizer;
|
|
|
|
|
2020-03-27 03:19:48 +08:00
|
|
|
// Feed the palette optimizer with pixels inside the given bounds
|
2020-03-26 01:32:43 +08:00
|
|
|
for (const auto& color : LockImageBits<RgbTraits>(image, bounds)) {
|
|
|
|
if (rgba_geta(color) >= 128) // Note: the mask color won't be part of the final palette
|
2015-07-23 03:40:44 +08:00
|
|
|
optimizer.feedWithRgbaColor(
|
|
|
|
rgba(rgba_getr(color),
|
|
|
|
rgba_getg(color),
|
|
|
|
rgba_getb(color), 255));
|
|
|
|
}
|
|
|
|
|
2020-03-26 01:32:43 +08:00
|
|
|
Palette palette(0, ncolors);
|
|
|
|
optimizer.calculate(&palette, -1);
|
2015-07-23 03:40:44 +08:00
|
|
|
return palette;
|
|
|
|
}
|
|
|
|
|
2016-09-17 12:01:28 +08:00
|
|
|
void renderFrame(frame_t frame, Image* dst) {
|
2015-07-23 03:40:44 +08:00
|
|
|
render::Render render;
|
2019-04-02 02:29:48 +08:00
|
|
|
render.setNewBlend(m_fop->newBlend());
|
2019-02-26 05:02:58 +08:00
|
|
|
|
2015-07-23 03:40:44 +08:00
|
|
|
render.setBgType(render::BgType::NONE);
|
2020-04-22 22:23:16 +08:00
|
|
|
if (m_preservePaletteOrder)
|
|
|
|
clear_image(dst, m_bgIndex);
|
|
|
|
else
|
|
|
|
clear_image(dst, 0);
|
2016-09-17 12:01:28 +08:00
|
|
|
render.renderSprite(dst, m_sprite, frame);
|
2015-07-23 03:40:44 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
2018-10-19 02:29:16 +08:00
|
|
|
ColorMapObject* createColorMap(const Palette* palette) {
|
2015-07-31 05:46:56 +08:00
|
|
|
int n = 1 << GifBitSizeLimited(palette->size());
|
2015-07-23 03:40:44 +08:00
|
|
|
ColorMapObject* colormap = GifMakeMapObject(n, nullptr);
|
|
|
|
|
2018-10-19 02:29:16 +08:00
|
|
|
// Color space conversions
|
|
|
|
ConvertCS convert = convert_from_custom_to_srgb(
|
|
|
|
m_document->osColorSpace());
|
|
|
|
|
2015-07-23 03:40:44 +08:00
|
|
|
for (int i=0; i<n; ++i) {
|
|
|
|
color_t color;
|
|
|
|
if (i < palette->size())
|
|
|
|
color = palette->getEntry(i);
|
|
|
|
else
|
|
|
|
color = rgba(0, 0, 0, 255);
|
|
|
|
|
2018-10-19 02:29:16 +08:00
|
|
|
color = convert(color);
|
|
|
|
|
2015-07-23 03:40:44 +08:00
|
|
|
colormap->Colors[i].Red = rgba_getr(color);
|
|
|
|
colormap->Colors[i].Green = rgba_getg(color);
|
|
|
|
colormap->Colors[i].Blue = rgba_getb(color);
|
|
|
|
}
|
|
|
|
|
|
|
|
return colormap;
|
|
|
|
}
|
|
|
|
|
|
|
|
FileOp* m_fop;
|
|
|
|
GifFileType* m_gifFile;
|
2018-10-19 02:29:16 +08:00
|
|
|
const Doc* m_document;
|
2015-09-29 22:27:00 +08:00
|
|
|
const Sprite* m_sprite;
|
2015-07-23 03:40:44 +08:00
|
|
|
gfx::Rect m_spriteBounds;
|
|
|
|
bool m_hasBackground;
|
|
|
|
int m_bgIndex;
|
|
|
|
int m_transparentIndex;
|
|
|
|
int m_bitsPerPixel;
|
2020-03-27 03:19:48 +08:00
|
|
|
// Global palette to use on all frames, or nullptr in case that we
|
|
|
|
// have to quantize the palette on each frame.
|
2015-07-23 03:40:44 +08:00
|
|
|
ColorMapObject* m_globalColormap;
|
2020-03-26 01:32:43 +08:00
|
|
|
Palette m_globalColormapPalette;
|
2015-07-23 03:40:44 +08:00
|
|
|
bool m_interlaced;
|
|
|
|
int m_loop;
|
2020-03-26 01:32:43 +08:00
|
|
|
bool m_preservePaletteOrder;
|
|
|
|
gfx::Rect m_lastFrameBounds;
|
|
|
|
DisposalMethod m_lastDisposal;
|
2015-07-23 03:40:44 +08:00
|
|
|
ImageBufferPtr m_frameImageBuf;
|
|
|
|
ImageRef m_images[3];
|
|
|
|
Image* m_previousImage;
|
|
|
|
Image* m_currentImage;
|
|
|
|
Image* m_nextImage;
|
2020-03-26 01:32:43 +08:00
|
|
|
std::unique_ptr<Image> m_deltaImage;
|
2015-07-23 03:40:44 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
bool GifFormat::onSave(FileOp* fop)
|
|
|
|
{
|
|
|
|
#if GIFLIB_MAJOR >= 5
|
|
|
|
int errCode = 0;
|
2014-04-10 11:33:28 +08:00
|
|
|
#endif
|
2018-03-08 04:55:11 +08:00
|
|
|
int fd = base::open_file_descriptor_with_exception(fop->filename(), "wb");
|
|
|
|
GifFilePtr gif_file(EGifOpenFileHandle(fd
|
2015-07-23 03:40:44 +08:00
|
|
|
#if GIFLIB_MAJOR >= 5
|
|
|
|
, &errCode
|
|
|
|
#endif
|
|
|
|
), &EGifCloseFile);
|
|
|
|
|
|
|
|
if (!gif_file)
|
|
|
|
throw Exception("Error creating GIF file.\n");
|
|
|
|
|
|
|
|
GifEncoder encoder(fop, gif_file);
|
2018-03-08 04:55:11 +08:00
|
|
|
bool result = encoder.encode();
|
|
|
|
if (result)
|
|
|
|
base::sync_file_descriptor(fd);
|
|
|
|
return result;
|
2015-06-09 07:21:50 +08:00
|
|
|
}
|
2013-08-06 08:20:19 +08:00
|
|
|
|
2015-07-23 03:40:44 +08:00
|
|
|
#endif // ENABLE_SAVE
|
|
|
|
|
2019-08-02 07:20:02 +08:00
|
|
|
FormatOptionsPtr GifFormat::onAskUserForFormatOptions(FileOp* fop)
|
2014-07-20 09:01:39 +08:00
|
|
|
{
|
2019-08-02 07:20:02 +08:00
|
|
|
auto opts = fop->formatOptionsOfDocument<GifOptions>();
|
2018-05-07 11:11:50 +08:00
|
|
|
#ifdef ENABLE_UI
|
|
|
|
if (fop->context() && fop->context()->isUIAvailable()) {
|
|
|
|
try {
|
|
|
|
auto& pref = Preferences::instance();
|
2014-07-20 09:01:39 +08:00
|
|
|
|
2018-05-07 11:11:50 +08:00
|
|
|
if (pref.isSet(pref.gif.interlaced))
|
2019-08-02 07:20:02 +08:00
|
|
|
opts->setInterlaced(pref.gif.interlaced());
|
2018-05-07 11:11:50 +08:00
|
|
|
if (pref.isSet(pref.gif.loop))
|
2019-08-02 07:20:02 +08:00
|
|
|
opts->setLoop(pref.gif.loop());
|
2020-03-26 01:32:43 +08:00
|
|
|
if (pref.isSet(pref.gif.preservePaletteOrder))
|
|
|
|
opts->setPreservePaletteOrder(pref.gif.preservePaletteOrder());
|
2014-07-20 09:01:39 +08:00
|
|
|
|
2018-05-07 11:11:50 +08:00
|
|
|
if (pref.gif.showAlert()) {
|
|
|
|
app::gen::GifOptions win;
|
2019-08-02 07:20:02 +08:00
|
|
|
win.interlaced()->setSelected(opts->interlaced());
|
|
|
|
win.loop()->setSelected(opts->loop());
|
2020-03-26 01:32:43 +08:00
|
|
|
win.preservePaletteOrder()->setSelected(opts->preservePaletteOrder());
|
|
|
|
|
|
|
|
if (fop->document()->sprite()->pixelFormat() == PixelFormat::IMAGE_INDEXED &&
|
|
|
|
!fop->document()->sprite()->isOpaque())
|
|
|
|
win.preservePaletteOrder()->setEnabled(true);
|
|
|
|
else {
|
|
|
|
win.preservePaletteOrder()->setEnabled(false);
|
|
|
|
if (fop->document()->sprite()->pixelFormat() == PixelFormat::IMAGE_INDEXED && fop->document()->sprite()->isOpaque())
|
|
|
|
win.preservePaletteOrder()->setSelected(true);
|
|
|
|
else
|
|
|
|
win.preservePaletteOrder()->setSelected(false);
|
|
|
|
}
|
2014-07-20 09:01:39 +08:00
|
|
|
|
2018-05-07 11:11:50 +08:00
|
|
|
win.openWindowInForeground();
|
2014-07-20 09:01:39 +08:00
|
|
|
|
2018-05-07 11:11:50 +08:00
|
|
|
if (win.closer() == win.ok()) {
|
|
|
|
pref.gif.interlaced(win.interlaced()->isSelected());
|
|
|
|
pref.gif.loop(win.loop()->isSelected());
|
2020-03-26 01:32:43 +08:00
|
|
|
pref.gif.preservePaletteOrder(win.preservePaletteOrder()->isSelected());
|
2018-05-07 11:11:50 +08:00
|
|
|
pref.gif.showAlert(!win.dontShow()->isSelected());
|
2014-07-20 09:01:39 +08:00
|
|
|
|
2019-08-02 07:20:02 +08:00
|
|
|
opts->setInterlaced(pref.gif.interlaced());
|
|
|
|
opts->setLoop(pref.gif.loop());
|
2020-03-26 01:32:43 +08:00
|
|
|
opts->setPreservePaletteOrder(pref.gif.preservePaletteOrder());
|
2018-05-07 11:11:50 +08:00
|
|
|
}
|
|
|
|
else {
|
2019-08-02 07:20:02 +08:00
|
|
|
opts.reset();
|
2018-05-07 11:11:50 +08:00
|
|
|
}
|
2018-02-08 01:35:12 +08:00
|
|
|
}
|
2014-07-20 09:01:39 +08:00
|
|
|
}
|
2018-05-07 11:11:50 +08:00
|
|
|
catch (std::exception& e) {
|
|
|
|
Console::showException(e);
|
2019-08-02 06:14:46 +08:00
|
|
|
return std::shared_ptr<GifOptions>(nullptr);
|
2018-05-07 11:11:50 +08:00
|
|
|
}
|
2014-07-20 09:01:39 +08:00
|
|
|
}
|
2018-05-07 11:11:50 +08:00
|
|
|
#endif // ENABLE_UI
|
2019-08-02 07:20:02 +08:00
|
|
|
return opts;
|
2014-07-20 09:01:39 +08:00
|
|
|
}
|
|
|
|
|
2013-08-06 08:20:19 +08:00
|
|
|
} // namespace app
|