mirror of
https://github.com/aseprite/aseprite.git
synced 2025-03-29 19:20:09 +00:00
Fix gif format encoding
This commit is contained in:
parent
2af6a0493e
commit
7ed83c10cc
@ -320,6 +320,7 @@
|
||||
<option id="show_alert" type="bool" default="true" />
|
||||
<option id="interlaced" type="bool" default="false" />
|
||||
<option id="loop" type="bool" default="true" />
|
||||
<option id="preserve_palette_order" type="bool" default="true" />
|
||||
</section>
|
||||
<section id="jpeg">
|
||||
<option id="show_alert" type="bool" default="true" />
|
||||
|
@ -672,6 +672,7 @@ title = GIF Options
|
||||
general_options = General Options:
|
||||
interlaced = &Interlaced
|
||||
animation_loop = Animation &Loop
|
||||
preserve_palette_order = &Preserve palette order
|
||||
ok = &OK
|
||||
cancel = &Cancel
|
||||
|
||||
|
@ -1,11 +1,13 @@
|
||||
<!-- Aseprite -->
|
||||
<!-- Copyright (C) 2014-2018 by David Capello -->
|
||||
<!-- Copyright (C) 2020 Igara Studio S.A. -->
|
||||
<!-- Copyright (C) 2014-2018 David Capello -->
|
||||
<gui>
|
||||
<window id="gif_options" text="@.title">
|
||||
<vbox>
|
||||
<separator text="@.general_options" left="true" horizontal="true" />
|
||||
<check text="@.interlaced" id="interlaced" />
|
||||
<check text="@.animation_loop" id="loop" />
|
||||
<check text="@.preserve_palette_order" id="preserve_palette_order" />
|
||||
|
||||
<separator horizontal="true" />
|
||||
|
||||
|
@ -901,10 +901,36 @@ public:
|
||||
, m_document(fop->document())
|
||||
, m_sprite(fop->document()->sprite())
|
||||
, m_spriteBounds(m_sprite->bounds())
|
||||
, m_hasBackground(m_sprite->backgroundLayer() ? true: false)
|
||||
, m_hasBackground(m_sprite->isOpaque())
|
||||
, m_bitsPerPixel(1)
|
||||
, m_globalColormap(nullptr)
|
||||
, m_globalColormapPalette(*(m_sprite->palette(0)))
|
||||
, m_quantizeColormaps(false) {
|
||||
|
||||
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);
|
||||
|
||||
if (m_sprite->pixelFormat() == PixelFormat::IMAGE_INDEXED) {
|
||||
if (m_hasBackground) {
|
||||
m_preservePaletteOrder = true;
|
||||
}
|
||||
// Only for transparent-indexed images we can select if we
|
||||
// preserve or not the palette order.
|
||||
else {
|
||||
m_preservePaletteOrder = gifOptions->preservePaletteOrder();
|
||||
}
|
||||
}
|
||||
else
|
||||
m_preservePaletteOrder = false;
|
||||
|
||||
m_lastFrameBounds = m_spriteBounds;
|
||||
m_lastDisposal = DisposalMethod::NONE;
|
||||
|
||||
if (m_sprite->pixelFormat() == IMAGE_INDEXED) {
|
||||
for (Palette* palette : m_sprite->getPalettes()) {
|
||||
int bpp = GifBitSizeLimited(palette->size());
|
||||
@ -931,7 +957,7 @@ public:
|
||||
}
|
||||
|
||||
if (!m_quantizeColormaps) {
|
||||
m_globalColormap = createColorMap(m_sprite->palette(0));
|
||||
m_globalColormap = createColorMap(&m_globalColormapPalette);
|
||||
m_bgIndex = m_sprite->transparentColor();
|
||||
}
|
||||
else
|
||||
@ -944,18 +970,39 @@ public:
|
||||
|
||||
m_transparentIndex = (m_hasBackground ? -1: m_bgIndex);
|
||||
|
||||
if (m_hasBackground)
|
||||
m_clearColor = m_sprite->palette(0)->getEntry(m_bgIndex);
|
||||
else
|
||||
m_clearColor = rgba(0, 0, 0, 0);
|
||||
|
||||
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);
|
||||
// INDEXED image without Background case: we need to know if 0 color is loaded in the palette:
|
||||
// Color 0 will be used in the gif file composition when disposal method is DO_NOT_DISPOSE.
|
||||
// You may think, "Why we do that? In INDEXED we never surpass 256 colors, and we
|
||||
// simply replacing the entire frame with RESTORE_BGCOLOR is enought".
|
||||
// Answer: because we want a tiny file size, replacing the entire frame, every frame
|
||||
// can result in large file sizes. So, we will include the transparent color in
|
||||
// the final palette, in our case, always the color zero (0).
|
||||
if (m_globalColormap) {
|
||||
Palette newPalette(*m_sprite->palette(0));
|
||||
bool maskColorFounded = false;
|
||||
for (int i=0; i<newPalette.size(); i++) {
|
||||
if (newPalette.getEntry(i) == 0) {
|
||||
maskColorFounded = true;
|
||||
m_transparentIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!maskColorFounded && !m_preservePaletteOrder && !m_hasBackground) {
|
||||
Palette(0, 255).copyColorsTo(&newPalette);
|
||||
render::create_palette_from_sprite(m_sprite,
|
||||
0,
|
||||
totalFrames()-1,
|
||||
false,
|
||||
&newPalette,
|
||||
nullptr,
|
||||
m_fop->newBlend(),
|
||||
false);
|
||||
newPalette.addEntry(0);
|
||||
newPalette.copyColorsTo(&m_globalColormapPalette);
|
||||
m_transparentIndex = m_globalColormapPalette.size() - 1;
|
||||
m_globalColormap = createColorMap(&m_globalColormapPalette);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i=0; i<3; ++i)
|
||||
m_images[i].reset(Image::create(IMAGE_RGB,
|
||||
@ -1004,25 +1051,22 @@ public:
|
||||
if (gifFrame+1 < nframes)
|
||||
renderFrame(*frame_it, m_nextImage);
|
||||
|
||||
gfx::Rect frameBounds;
|
||||
DisposalMethod disposal;
|
||||
calculateBestDisposalMethod(gifFrame, frameBounds, disposal);
|
||||
gfx::Rect frameBounds(0, 0, m_spriteBounds.w, m_spriteBounds.h);
|
||||
DisposalMethod disposal = DisposalMethod::DO_NOT_DISPOSE;
|
||||
|
||||
// TODO We could join both frames in a longer one (with more duration)
|
||||
if (frameBounds.isEmpty())
|
||||
frameBounds = gfx::Rect(0, 0, 1, 1);
|
||||
// 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).
|
||||
|
||||
calculateDeltaImageFrameBoundsDisposal(gifFrame, frameBounds, disposal);
|
||||
|
||||
writeImage(gifFrame, frame, frameBounds, disposal,
|
||||
// Only the last frame in the animation needs the fix
|
||||
(fix_last_frame_duration && gifFrame == nframes-1));
|
||||
|
||||
// Dispose/clear frame content
|
||||
process_disposal_method(m_previousImage,
|
||||
m_currentImage,
|
||||
disposal,
|
||||
frameBounds,
|
||||
m_clearColor);
|
||||
|
||||
m_fop->setProgress(double(gifFrame+1) / double(nframes));
|
||||
}
|
||||
return true;
|
||||
@ -1030,6 +1074,122 @@ public:
|
||||
|
||||
private:
|
||||
|
||||
void calculateDeltaImageFrameBoundsDisposal(gifframe_t gifFrame,
|
||||
gfx::Rect& frameBounds,
|
||||
DisposalMethod& disposal) {
|
||||
if (gifFrame == 0) {
|
||||
m_deltaImage.reset(Image::createCopy(m_currentImage));
|
||||
frameBounds = gfx::Rect(0, 0, m_spriteBounds.w, m_spriteBounds.h);
|
||||
// The first frame (frame 0) is good to force to disposal = DO_NOT_DISPOSE,
|
||||
// but when the next frame (frame 1) has a pixel clearing,
|
||||
// we must change disposal to RESTORE_BGCOLOR.
|
||||
|
||||
// Pixel clearing detection:
|
||||
if (!m_hasBackground) {
|
||||
const LockImageBits<RgbTraits> bits2(m_currentImage);
|
||||
const LockImageBits<RgbTraits> bits3(m_nextImage);
|
||||
typename LockImageBits<RgbTraits>::const_iterator it2, it3, end2, end3;
|
||||
int i = 0;
|
||||
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;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
int x1 = 0;
|
||||
int y1 = 0;
|
||||
int x2 = 0;
|
||||
int y2 = 0;
|
||||
// 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;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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) {
|
||||
*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;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_preservePaletteOrder)
|
||||
disposal = DisposalMethod::RESTORE_BGCOLOR;
|
||||
frameBounds = gfx::Rect(x1, y1, x2-x1+1, y2-y1+1);
|
||||
|
||||
// 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));
|
||||
m_lastFrameBounds = frameBounds;
|
||||
}
|
||||
else {
|
||||
m_deltaImage.reset(crop_image(m_deltaImage.get(), frameBounds, 0));
|
||||
disposal = DisposalMethod::DO_NOT_DISPOSE;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
};
|
||||
|
||||
doc::frame_t totalFrames() const {
|
||||
return m_fop->roi().frames();
|
||||
}
|
||||
@ -1122,99 +1282,46 @@ private:
|
||||
return frameBounds;
|
||||
}
|
||||
|
||||
void calculateBestDisposalMethod(gifframe_t gifFrame, gfx::Rect& frameBounds,
|
||||
DisposalMethod& disposal) {
|
||||
if (m_hasBackground) {
|
||||
disposal = DisposalMethod::DO_NOT_DISPOSE;
|
||||
}
|
||||
else {
|
||||
disposal = DisposalMethod::RESTORE_BGCOLOR;
|
||||
}
|
||||
|
||||
if (gifFrame == 0) {
|
||||
frameBounds = m_spriteBounds;
|
||||
}
|
||||
else {
|
||||
gfx::Rect prev, next;
|
||||
|
||||
if (gifFrame-1 >= 0)
|
||||
prev = calculateFrameBounds(m_currentImage, m_previousImage);
|
||||
|
||||
if (!m_hasBackground &&
|
||||
gifFrame+1 < totalFrames())
|
||||
next = calculateFrameBounds(m_currentImage, m_nextImage);
|
||||
|
||||
frameBounds = prev.createUnion(next);
|
||||
|
||||
// Special case were it's better to restore the previous frame
|
||||
// when we dispose the current one than clearing with the bg
|
||||
// color.
|
||||
if (m_hasBackground && !prev.isEmpty()) {
|
||||
gfx::Rect prevNext = calculateFrameBounds(m_previousImage, m_nextImage);
|
||||
if (!prevNext.isEmpty() &&
|
||||
frameBounds.contains(prevNext) &&
|
||||
prevNext.w*prevNext.h < frameBounds.w*frameBounds.h) {
|
||||
disposal = DisposalMethod::RESTORE_PREVIOUS;
|
||||
}
|
||||
}
|
||||
|
||||
GIF_TRACE("GIF: frameBounds=%d %d %d %d prev=%d %d %d %d next=%d %d %d %d\n",
|
||||
frameBounds.x, frameBounds.y, frameBounds.w, frameBounds.h,
|
||||
prev.x, prev.y, prev.w, prev.h,
|
||||
next.x, next.y, next.w, next.h);
|
||||
}
|
||||
}
|
||||
|
||||
void writeImage(const gifframe_t gifFrame,
|
||||
const frame_t frame,
|
||||
const gfx::Rect& frameBounds,
|
||||
const DisposalMethod disposal,
|
||||
const bool fixDuration) {
|
||||
std::unique_ptr<Palette> framePaletteRef;
|
||||
|
||||
std::unique_ptr<RgbMap> rgbmapRef;
|
||||
Palette* framePalette = m_sprite->palette(frame);
|
||||
RgbMap* rgbmap = m_sprite->rgbMap(frame);
|
||||
|
||||
// Create optimized palette for RGB/Grayscale images
|
||||
if (m_quantizeColormaps) {
|
||||
framePaletteRef.reset(createOptimizedPalette(frameBounds));
|
||||
framePalette = framePaletteRef.get();
|
||||
Palette framePalette(*m_sprite->palette(0));
|
||||
RgbMap* rgbmap(m_sprite->rgbMap(0));
|
||||
|
||||
if (m_globalColormap) {
|
||||
m_globalColormapPalette.copyColorsTo(&framePalette);
|
||||
rgbmapRef.reset(new RgbMap);
|
||||
rgbmap = rgbmapRef.get();
|
||||
rgbmap->regenerate(framePalette, m_transparentIndex);
|
||||
rgbmapRef.get()->regenerate(&framePalette, m_transparentIndex);
|
||||
}
|
||||
else
|
||||
m_sprite->palette(frame)->copyColorsTo(&framePalette);
|
||||
|
||||
|
||||
if (m_quantizeColormaps) {
|
||||
calculatePalette(frameBounds, disposal).copyColorsTo(&framePalette);
|
||||
rgbmapRef.reset(new RgbMap);
|
||||
rgbmapRef.get()->regenerate(&framePalette, m_transparentIndex);
|
||||
}
|
||||
|
||||
// We will store the frameBounds pixels in frameImage, with the
|
||||
// indexes that must be stored in the GIF file for this specific
|
||||
// frame.
|
||||
if (!m_frameImageBuf)
|
||||
m_frameImageBuf.reset(new ImageBuffer);
|
||||
|
||||
rgbmap = rgbmapRef.get();
|
||||
ImageRef frameImage(Image::create(IMAGE_INDEXED,
|
||||
frameBounds.w,
|
||||
frameBounds.h,
|
||||
m_frameImageBuf));
|
||||
|
||||
// Convert the frameBounds area of m_currentImage (RGB) to frameImage (Indexed)
|
||||
// bool needsTransparent = false;
|
||||
PalettePicks usedColors(framePalette->size());
|
||||
|
||||
// If the sprite needs a transparent color we mark it as used so
|
||||
// the palette includes a spot for it. It doesn't matter if the
|
||||
// image doesn't use the transparent index, if the sprite isn't
|
||||
// opaque we need the transparent index anyway.
|
||||
if (m_transparentIndex >= 0) {
|
||||
int i = m_transparentIndex;
|
||||
if (i >= usedColors.size())
|
||||
usedColors.resize(i+1);
|
||||
usedColors[i] = true;
|
||||
}
|
||||
// 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());
|
||||
|
||||
{
|
||||
const LockImageBits<RgbTraits> srcBits(m_currentImage, frameBounds);
|
||||
LockImageBits<IndexedTraits> dstBits(
|
||||
frameImage.get(), gfx::Rect(0, 0, frameBounds.w, frameBounds.h));
|
||||
const LockImageBits<RgbTraits> srcBits(m_deltaImage.get());
|
||||
LockImageBits<IndexedTraits> dstBits(frameImage.get());
|
||||
|
||||
auto srcIt = srcBits.begin();
|
||||
auto dstIt = dstBits.begin();
|
||||
@ -1228,7 +1335,7 @@ private:
|
||||
int i;
|
||||
|
||||
if (rgba_geta(color) >= 128) {
|
||||
i = framePalette->findExactMatch(
|
||||
i = framePalette.findExactMatch(
|
||||
rgba_getr(color),
|
||||
rgba_getg(color),
|
||||
rgba_getb(color),
|
||||
@ -1241,7 +1348,6 @@ private:
|
||||
255);
|
||||
}
|
||||
else {
|
||||
ASSERT(m_transparentIndex >= 0);
|
||||
if (m_transparentIndex >= 0)
|
||||
i = m_transparentIndex;
|
||||
else
|
||||
@ -1273,9 +1379,9 @@ private:
|
||||
if (!colormap) {
|
||||
Palette reducedPalette(0, usedNColors);
|
||||
|
||||
for (int i=0, j=0; i<framePalette->size(); ++i) {
|
||||
for (int i=0, j=0; i<framePalette.size(); ++i) {
|
||||
if (usedColors[i]) {
|
||||
reducedPalette.setEntry(j, framePalette->getEntry(i));
|
||||
reducedPalette.setEntry(j, framePalette.getEntry(i));
|
||||
remap.map(i, j);
|
||||
++j;
|
||||
}
|
||||
@ -1337,20 +1443,168 @@ private:
|
||||
GifFreeMapObject(colormap);
|
||||
}
|
||||
|
||||
Palette* createOptimizedPalette(const gfx::Rect& frameBounds) {
|
||||
Palette calculatePalette(const gfx::Rect& frameBounds, DisposalMethod disposal) {
|
||||
// First, we must check the palette color count in m_deltaImage (our best shot
|
||||
// to find the smaller palette color count)
|
||||
Palette pal(createOptimizedPalette(m_deltaImage.get(), m_deltaImage->bounds(), 257));
|
||||
if (pal.size() == 256) {
|
||||
// Here, the palette has 256 colors, there is no place to include the 0 color ( remember,
|
||||
// createOptimizedPalette do not create entry for it).
|
||||
//
|
||||
// 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.
|
||||
Palette auxPalette(pal);
|
||||
if (disposal == DisposalMethod::DO_NOT_DISPOSE)
|
||||
createOptimizedPalette(m_currentImage, frameBounds, 257).copyColorsTo(&auxPalette);
|
||||
|
||||
if (auxPalette.size() <= 256) {
|
||||
// We are fine with color count in m_currentImage contained in frameBouns (we got 256 or less colors):
|
||||
m_transparentIndex = -1;
|
||||
auxPalette.copyColorsTo(&pal);
|
||||
if (disposal == DisposalMethod::DO_NOT_DISPOSE)
|
||||
m_deltaImage.reset(crop_image(m_currentImage, frameBounds, 0));
|
||||
}
|
||||
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:
|
||||
// TODO: develop a better criterion, based on big color areas, or the opposite: do not take count of lone pixels.
|
||||
// Find a palette of high precision 220 colors in the inner border square
|
||||
// into m_deltaImage, then find 35 more truncated colors in the center square.
|
||||
//
|
||||
// m_currentImage__ __ m_deltaImage (same rectangle size as frameBounds)
|
||||
// | |
|
||||
// --------------*----|------------
|
||||
// | | |
|
||||
// | --------------*- |
|
||||
// | | ________ | |
|
||||
// | | | | | |
|
||||
// | | | | *---------------inner border square (we will collect
|
||||
// | | |________| | | high precision colors from this area)
|
||||
// | |________________| |
|
||||
// | |
|
||||
// |_______________________________|
|
||||
//
|
||||
|
||||
int thicknessTop = m_deltaImage->bounds().h / 4;
|
||||
int thicknessLeft = m_deltaImage->bounds().w / 4;
|
||||
int lastThicknessTop = thicknessTop;
|
||||
int lastThicknessLeft = thicknessLeft;
|
||||
while (true) {
|
||||
|
||||
render::PaletteOptimizer optimizer;
|
||||
gfx::Rect auxRect(0, 0, m_deltaImage->bounds().w, thicknessTop);
|
||||
optimizer.feedWithImage(Image::createCopy(crop_image(m_deltaImage.get(), auxRect, m_transparentIndex)), false);
|
||||
// ----------------
|
||||
// |________________|
|
||||
// | | | |
|
||||
// | | | |
|
||||
// | |________| |
|
||||
// |________________|
|
||||
|
||||
auxRect = gfx::Rect(0, m_deltaImage->bounds().h - thicknessTop, m_deltaImage->bounds().w, thicknessTop);
|
||||
optimizer.feedWithImage(Image::createCopy(crop_image(m_deltaImage.get(), auxRect, m_transparentIndex)), false);
|
||||
// ----------------
|
||||
// | ________ |
|
||||
// | | | |
|
||||
// | | | |
|
||||
// |___|________|___|
|
||||
// |________________|
|
||||
|
||||
auxRect = gfx::Rect(0, thicknessTop, thicknessLeft, m_deltaImage->bounds().h - 2 * thicknessTop);
|
||||
optimizer.feedWithImage(Image::createCopy(crop_image(m_deltaImage.get(), auxRect, m_transparentIndex)), false);
|
||||
// ----------------
|
||||
// |____________ |
|
||||
// | | | |
|
||||
// | | | |
|
||||
// |___|________| |
|
||||
// |________________|
|
||||
|
||||
auxRect = gfx::Rect(m_deltaImage->bounds().w - thicknessLeft, thicknessTop, thicknessLeft, m_deltaImage->bounds().h - 2 * thicknessTop);
|
||||
optimizer.feedWithImage(Image::createCopy(crop_image(m_deltaImage.get(), auxRect, m_transparentIndex)), false);
|
||||
// ----------------
|
||||
// | _____________|
|
||||
// | | | |
|
||||
// | | | |
|
||||
// | |________|___|
|
||||
// |________________|
|
||||
|
||||
if (optimizer.isHighPrecision()) {
|
||||
if (optimizer.highPrecisionSize() >= 220) { // 220 colors is an arbitrary number
|
||||
lastThicknessTop = thicknessTop;
|
||||
lastThicknessLeft = thicknessLeft;
|
||||
// Put the high precision colors to the palette:
|
||||
optimizer.calculate(&pal, 0);
|
||||
break;
|
||||
}
|
||||
else if (m_deltaImage->bounds().h - thicknessTop * 2 <= m_deltaImage->bounds().h / 4 ||
|
||||
m_deltaImage->bounds().w - thicknessLeft * 2 <= m_deltaImage->bounds().w / 4) {
|
||||
// Put the high precision colors to the palette:
|
||||
optimizer.calculate(&pal, 0);
|
||||
break;
|
||||
}
|
||||
thicknessTop += thicknessTop / 2;
|
||||
thicknessLeft += thicknessLeft / 2;
|
||||
}
|
||||
else {
|
||||
if (thicknessTop <= m_deltaImage->bounds().h / 16 ||
|
||||
thicknessLeft <= m_deltaImage->bounds().w / 16) {
|
||||
// TODO: we need to catch this LAST posibility.
|
||||
// Put the high precision colors to the palette:
|
||||
optimizer.calculate(&pal, 0);
|
||||
break;
|
||||
}
|
||||
thicknessTop -= thicknessTop / 2;
|
||||
thicknessLeft -= thicknessLeft / 2;
|
||||
}
|
||||
|
||||
lastThicknessTop = thicknessTop;
|
||||
lastThicknessLeft = thicknessLeft;
|
||||
}
|
||||
gfx::Rect centerRect(lastThicknessLeft,
|
||||
lastThicknessTop,
|
||||
m_deltaImage->bounds().w - 1 - lastThicknessLeft,
|
||||
m_deltaImage->bounds().h - 1 - lastThicknessTop);
|
||||
// Find the center colors (aproximation colors)
|
||||
Palette centerPalette(createOptimizedPalette(m_deltaImage.get(), centerRect, 255 - pal.size()));
|
||||
// Adding the center colors to pal
|
||||
for (int i=0; i < centerPalette.size(); i++)
|
||||
pal.addEntry(centerPalette.getEntry(i));
|
||||
// Add the transparent color:
|
||||
pal.addEntry(0);
|
||||
m_transparentIndex = pal.size() - 1;
|
||||
}
|
||||
}
|
||||
// We are fine, we got 255 or less:
|
||||
else if (pal.size() <= 255) {
|
||||
pal.addEntry(0);
|
||||
m_transparentIndex = pal.size() - 1;
|
||||
}
|
||||
return pal;
|
||||
}
|
||||
|
||||
Palette createOptimizedPalette(Image* image, gfx::Rect bounds, int ncolors = 256) {
|
||||
render::PaletteOptimizer optimizer;
|
||||
|
||||
// Feed the palette optimizer with pixels inside frameBounds
|
||||
for (const auto& color : LockImageBits<RgbTraits>(m_currentImage, frameBounds)) {
|
||||
if (rgba_geta(color) >= 128)
|
||||
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
|
||||
optimizer.feedWithRgbaColor(
|
||||
rgba(rgba_getr(color),
|
||||
rgba_getg(color),
|
||||
rgba_getb(color), 255));
|
||||
}
|
||||
|
||||
Palette* palette = new Palette(0, 256);
|
||||
optimizer.calculate(palette, m_transparentIndex);
|
||||
Palette palette(0, ncolors);
|
||||
optimizer.calculate(&palette, -1);
|
||||
return palette;
|
||||
}
|
||||
|
||||
@ -1359,7 +1613,7 @@ private:
|
||||
render.setNewBlend(m_fop->newBlend());
|
||||
|
||||
render.setBgType(render::BgType::NONE);
|
||||
clear_image(dst, m_clearColor);
|
||||
clear_image(dst, 0);
|
||||
render.renderSprite(dst, m_sprite, frame);
|
||||
}
|
||||
|
||||
@ -1397,18 +1651,22 @@ private:
|
||||
gfx::Rect m_spriteBounds;
|
||||
bool m_hasBackground;
|
||||
int m_bgIndex;
|
||||
color_t m_clearColor;
|
||||
int m_transparentIndex;
|
||||
int m_bitsPerPixel;
|
||||
ColorMapObject* m_globalColormap;
|
||||
Palette m_globalColormapPalette;
|
||||
bool m_quantizeColormaps;
|
||||
bool m_interlaced;
|
||||
int m_loop;
|
||||
bool m_preservePaletteOrder;
|
||||
gfx::Rect m_lastFrameBounds;
|
||||
DisposalMethod m_lastDisposal;
|
||||
ImageBufferPtr m_frameImageBuf;
|
||||
ImageRef m_images[3];
|
||||
Image* m_previousImage;
|
||||
Image* m_currentImage;
|
||||
Image* m_nextImage;
|
||||
std::unique_ptr<Image> m_deltaImage;
|
||||
};
|
||||
|
||||
bool GifFormat::onSave(FileOp* fop)
|
||||
@ -1447,21 +1705,37 @@ FormatOptionsPtr GifFormat::onAskUserForFormatOptions(FileOp* fop)
|
||||
opts->setInterlaced(pref.gif.interlaced());
|
||||
if (pref.isSet(pref.gif.loop))
|
||||
opts->setLoop(pref.gif.loop());
|
||||
if (pref.isSet(pref.gif.preservePaletteOrder))
|
||||
opts->setPreservePaletteOrder(pref.gif.preservePaletteOrder());
|
||||
|
||||
if (pref.gif.showAlert()) {
|
||||
app::gen::GifOptions win;
|
||||
win.interlaced()->setSelected(opts->interlaced());
|
||||
win.loop()->setSelected(opts->loop());
|
||||
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);
|
||||
}
|
||||
|
||||
win.openWindowInForeground();
|
||||
|
||||
if (win.closer() == win.ok()) {
|
||||
pref.gif.interlaced(win.interlaced()->isSelected());
|
||||
pref.gif.loop(win.loop()->isSelected());
|
||||
pref.gif.preservePaletteOrder(win.preservePaletteOrder()->isSelected());
|
||||
pref.gif.showAlert(!win.dontShow()->isSelected());
|
||||
|
||||
opts->setInterlaced(pref.gif.interlaced());
|
||||
opts->setLoop(pref.gif.loop());
|
||||
opts->setPreservePaletteOrder(pref.gif.preservePaletteOrder());
|
||||
}
|
||||
else {
|
||||
opts.reset();
|
||||
|
@ -1,4 +1,5 @@
|
||||
// Aseprite
|
||||
// Copyright (C) 2020 Igara Studio S.A.
|
||||
// Copyright (C) 2001-2017 David Capello
|
||||
//
|
||||
// This program is distributed under the terms of
|
||||
@ -18,20 +19,25 @@ namespace app {
|
||||
public:
|
||||
GifOptions(
|
||||
bool interlaced = false,
|
||||
bool loop = true)
|
||||
bool loop = true,
|
||||
bool preservePaletteOrder = true)
|
||||
: m_interlaced(interlaced)
|
||||
, m_loop(loop) {
|
||||
, m_loop(loop)
|
||||
, m_preservePaletteOrder(preservePaletteOrder) {
|
||||
}
|
||||
|
||||
bool interlaced() const { return m_interlaced; }
|
||||
bool loop() const { return m_loop; }
|
||||
bool preservePaletteOrder() const { return m_preservePaletteOrder; }
|
||||
|
||||
void setInterlaced(bool interlaced) { m_interlaced = interlaced; }
|
||||
void setLoop(bool loop) { m_loop = loop; }
|
||||
void setPreservePaletteOrder(bool preservePaletteOrder) {m_preservePaletteOrder = preservePaletteOrder; }
|
||||
|
||||
private:
|
||||
bool m_interlaced;
|
||||
bool m_loop;
|
||||
bool m_preservePaletteOrder;
|
||||
};
|
||||
|
||||
} // namespace app
|
||||
|
@ -1,4 +1,5 @@
|
||||
// Aseprite Render Library
|
||||
// Copyright (c) 2020 Igara Studio S.A.
|
||||
// Copyright (c) 2001-2015 David Capello
|
||||
//
|
||||
// This file is released under the terms of the MIT license.
|
||||
@ -102,6 +103,9 @@ namespace render {
|
||||
}
|
||||
}
|
||||
|
||||
bool isHighPrecision() { return m_useHighPrecision; }
|
||||
int highPrecisionSize() { return m_highPrecision.size(); }
|
||||
|
||||
private:
|
||||
// Converts input color in a index for the histogram. It reduces
|
||||
// each 8-bit component to the resolution given in the template
|
||||
|
@ -44,7 +44,8 @@ Palette* create_palette_from_sprite(
|
||||
const bool withAlpha,
|
||||
Palette* palette,
|
||||
TaskDelegate* delegate,
|
||||
const bool newBlend)
|
||||
const bool newBlend,
|
||||
const bool calculateWithTransparent)
|
||||
{
|
||||
PaletteOptimizer optimizer;
|
||||
|
||||
@ -75,8 +76,9 @@ Palette* create_palette_from_sprite(
|
||||
optimizer.calculate(
|
||||
palette,
|
||||
// Transparent color is needed if we have transparent layers
|
||||
(sprite->backgroundLayer() &&
|
||||
sprite->allLayersCount() == 1 ? -1: sprite->transparentColor()));
|
||||
((sprite->backgroundLayer() &&
|
||||
sprite->allLayersCount() == 1) ||
|
||||
!calculateWithTransparent)? -1: sprite->transparentColor());
|
||||
|
||||
return palette;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
// Aseprite Rener Library
|
||||
// Copyright (c) 2019 Igara Studio S.A.
|
||||
// Copyright (c) 2019-2020 Igara Studio S.A.
|
||||
// Copyright (c) 2001-2017 David Capello
|
||||
//
|
||||
// This file is released under the terms of the MIT license.
|
||||
@ -31,6 +31,8 @@ namespace render {
|
||||
void feedWithImage(doc::Image* image, bool withAlpha);
|
||||
void feedWithRgbaColor(doc::color_t color);
|
||||
void calculate(doc::Palette* palette, int maskIndex);
|
||||
bool isHighPrecision() { return m_histogram.isHighPrecision(); }
|
||||
int highPrecisionSize() { return m_histogram.highPrecisionSize(); }
|
||||
|
||||
private:
|
||||
render::ColorHistogram<5, 6, 5, 5> m_histogram;
|
||||
@ -45,7 +47,8 @@ namespace render {
|
||||
const bool withAlpha,
|
||||
doc::Palette* newPalette, // Can be NULL to create a new palette
|
||||
TaskDelegate* delegate,
|
||||
const bool newBlend);
|
||||
const bool newBlend,
|
||||
const bool calculateWithTransparent = true);
|
||||
|
||||
// Changes the image pixel format. The dithering method is used only
|
||||
// when you want to convert from RGB to Indexed.
|
||||
|
Loading…
x
Reference in New Issue
Block a user