diff --git a/data/pref.xml b/data/pref.xml index 1f108865d..a18597331 100644 --- a/data/pref.xml +++ b/data/pref.xml @@ -330,6 +330,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" /> diff --git a/data/strings/en.ini b/data/strings/en.ini index c60179726..85a3030b8 100644 --- a/data/strings/en.ini +++ b/data/strings/en.ini @@ -704,6 +704,7 @@ title = GIF Options general_options = General Options: interlaced = &Interlaced animation_loop = Animation &Loop +preserve_palette_order = &Preserve palette order ok = &OK cancel = &Cancel diff --git a/data/widgets/gif_options.xml b/data/widgets/gif_options.xml index f045bb781..321d3d81c 100644 --- a/data/widgets/gif_options.xml +++ b/data/widgets/gif_options.xml @@ -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" /> diff --git a/src/app/file/gif_format.cpp b/src/app/file/gif_format.cpp index 4e2f7a26b..2c9340be1 100644 --- a/src/app/file/gif_format.cpp +++ b/src/app/file/gif_format.cpp @@ -356,7 +356,9 @@ private: if (!frameBounds.isEmpty()) frameImage.reset(readFrameIndexedImage(frameBounds)); - GIF_TRACE("GIF: Frame[%d] transparent index = %d\n", (int)m_frameNum, m_localTransparentIndex); + 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); if (m_frameNum == 0) { if (m_localTransparentIndex >= 0) @@ -457,8 +459,7 @@ private: else if (!m_hasLocalColormaps) { if (!global) { if (!m_firstLocalColormap) - m_firstLocalColormap = GifMakeMapObject(colormap->ColorCount, - colormap->Colors); + m_firstLocalColormap = GifMakeMapObject(256, colormap->Colors); global = m_firstLocalColormap; } @@ -526,8 +527,7 @@ private: if (m_sprite->pixelFormat() == IMAGE_INDEXED && !m_opaque && m_bgIndex != m_localTransparentIndex) { for (const auto& i : LockImageBits<IndexedTraits>(frameImage)) { - if (i == m_bgIndex && - i != m_localTransparentIndex) { + if (i == m_bgIndex) { needsExtraBgColor = true; break; } @@ -617,6 +617,18 @@ private: int i = m_bgIndex; int j = base++; palette->setEntry(j, colormap2rgba(colormap, i)); + // 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) { + // We need add this last color to m_firstLocalColormap, because + // it was not considered in the function getFrameColormap. + 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)); + } m_remap.map(i, j); } @@ -892,6 +904,34 @@ bool GifFormat::onLoad(FileOp* fop) #ifdef ENABLE_SAVE +// 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). +// class GifEncoder { public: typedef int gifframe_t; @@ -902,10 +942,22 @@ 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_quantizeColormaps(false) { + , m_globalColormapPalette(*m_sprite->palette(0)) + , m_preservePaletteOrder(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); + 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()); @@ -920,46 +972,107 @@ public: m_sprite->getPalettes().size() == 1) { // If some layer has opacity < 255 or a different blend mode, we // need to create color palettes. + bool quantizeColormaps = false; for (const Layer* layer : m_sprite->allVisibleLayers()) { if (layer->isVisible() && layer->isImage()) { const LayerImage* imageLayer = static_cast<const LayerImage*>(layer); if (imageLayer->opacity() < 255 || imageLayer->blendMode() != BlendMode::NORMAL) { - m_quantizeColormaps = true; + quantizeColormaps = true; break; } } } - if (!m_quantizeColormaps) { - m_globalColormap = createColorMap(m_sprite->palette(0)); + if (!quantizeColormaps) { + m_globalColormap = createColorMap(&m_globalColormapPalette); m_bgIndex = m_sprite->transparentColor(); + // 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(); } else m_bgIndex = 0; } else { m_bgIndex = 0; - m_quantizeColormaps = true; } + // 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. m_transparentIndex = (m_hasBackground ? -1: m_bgIndex); + if (m_globalColormap) { + // The variable m_globalColormap is != nullptr only on indexed images + ASSERT(m_sprite->pixelFormat() == IMAGE_INDEXED); - if (m_hasBackground) - m_clearColor = m_sprite->palette(0)->getEntry(m_bgIndex); - else - m_clearColor = rgba(0, 0, 0, 0); + const Palette* pal = m_sprite->palette(0); + bool maskColorFounded = false; + for (int i=0; i<pal->size(); i++) { + if (doc::rgba_geta(pal->getEntry(i)) == 0) { + maskColorFounded = true; + m_transparentIndex = i; + break; + } + } - const auto gifOptions = std::static_pointer_cast<GifOptions>(fop->formatOptions()); +#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; - LOG("GIF: Saving with options: interlaced=%d loop=%d\n", - gifOptions->interlaced(), gifOptions->loop()); + Palette newPalette(*pal); + newPalette.addEntry(0); + ASSERT(newPalette.size() <= 256); - m_interlaced = gifOptions->interlaced(); - m_loop = (gifOptions->loop() ? 0: -1); + 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(), + RgbMapAlgorithm::DEFAULT, // TODO configurable? + 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; + m_globalColormap = createColorMap(&m_globalColormapPalette); + } + } + + // Create the 3 temporary images (previous/current/next) to + // compare pixels between them. for (int i=0; i<3; ++i) - m_images[i].reset(Image::create(IMAGE_RGB, + m_images[i].reset(Image::create((m_preservePaletteOrder)? IMAGE_INDEXED : IMAGE_RGB, m_spriteBounds.w, m_spriteBounds.h)); } @@ -1005,25 +1118,22 @@ public: if (gifFrame+1 < nframes) renderFrame(*frame_it, m_nextImage); - gfx::Rect frameBounds; - DisposalMethod disposal; - calculateBestDisposalMethod(gifFrame, frameBounds, disposal); + gfx::Rect frameBounds = m_spriteBounds; + 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; @@ -1031,6 +1141,126 @@ public: private: + void calculateDeltaImageFrameBoundsDisposal(gifframe_t gifFrame, + gfx::Rect& frameBounds, + DisposalMethod& disposal) { + if (gifFrame == 0) { + m_deltaImage.reset(Image::createCopy(m_currentImage)); + frameBounds = m_spriteBounds; + + // 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 && !m_preservePaletteOrder) { + 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; + } + } + } + else if (m_preservePaletteOrder) + disposal = DisposalMethod::RESTORE_BGCOLOR; + } + else { + int x1 = 0; + int y1 = 0; + int x2 = 0; + int y2 = 0; + + 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; + } + + 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; + + bool previousImageMatchsCurrent = true; + 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) { + previousImageMatchsCurrent = false; + *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 (previousImageMatchsCurrent) + frameBounds = gfx::Rect(m_lastFrameBounds); + else + frameBounds = gfx::Rect(x1, y1, x2-x1+1, y2-y1+1); + } + else + 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; + } + m_lastFrameBounds = frameBounds; + } + + // 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(); } @@ -1123,99 +1353,38 @@ 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<RgbMapRGB5A3> rgbmapRef; - Palette* framePalette = m_sprite->palette(frame); - RgbMap* rgbmap = m_sprite->rgbMap(frame); + Palette framePalette; + if (m_globalColormap) + framePalette = m_globalColormapPalette; + else + framePalette = calculatePalette(frameBounds, disposal); - // Create optimized palette for RGB/Grayscale images - if (m_quantizeColormaps) { - framePaletteRef.reset(createOptimizedPalette(frameBounds)); - framePalette = framePaletteRef.get(); - - rgbmapRef.reset(new RgbMapRGB5A3); - rgbmapRef->regenerateMap(framePalette, m_transparentIndex); - rgbmap = rgbmapRef.get(); - } - - // 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); + RgbMapRGB5A3 rgbmap; // TODO RgbMapRGB5A3 configurable? + rgbmap.regenerateMap(&framePalette, m_transparentIndex); 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()); + // 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()); - // 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; - } + int localTransparent = m_transparentIndex; + ColorMapObject* colormap = m_globalColormap; + Remap remap(256); - { - const LockImageBits<RgbTraits> srcBits(m_currentImage, frameBounds); - LockImageBits<IndexedTraits> dstBits( - frameImage.get(), gfx::Rect(0, 0, frameBounds.w, frameBounds.h)); + if (!m_preservePaletteOrder) { + const LockImageBits<RgbTraits> srcBits(m_deltaImage.get()); + LockImageBits<IndexedTraits> dstBits(frameImage.get()); auto srcIt = srcBits.begin(); auto dstIt = dstBits.begin(); @@ -1229,19 +1398,16 @@ private: int i; if (rgba_geta(color) >= 128) { - color |= rgba_a_mask; // Set alpha=255 - - i = framePalette->findExactMatch( + i = framePalette.findExactMatch( rgba_getr(color), rgba_getg(color), rgba_getb(color), 255, m_transparentIndex); if (i < 0) - i = rgbmap->mapColor(color); + i = rgbmap.mapColor(color | rgba_a_mask); // alpha=255 } else { - ASSERT(m_transparentIndex >= 0); if (m_transparentIndex >= 0) i = m_transparentIndex; else @@ -1260,34 +1426,36 @@ private: *dstIt = i; } } - } - int usedNColors = usedColors.picks(); + int usedNColors = usedColors.picks(); - Remap remap(256); - for (int i=0; i<remap.size(); ++i) - remap.map(i, i); + for (int i=0; i<remap.size(); ++i) + remap.map(i, i); - int localTransparent = m_transparentIndex; - ColorMapObject* colormap = m_globalColormap; - if (!colormap) { - Palette reducedPalette(0, usedNColors); + if (!colormap) { + Palette reducedPalette(0, usedNColors); - for (int i=0, j=0; i<framePalette->size(); ++i) { - if (usedColors[i]) { - reducedPalette.setEntry(j, framePalette->getEntry(i)); - remap.map(i, j); - ++j; + for (int i=0, j=0; i<framePalette.size(); ++i) { + if (usedColors[i]) { + reducedPalette.setEntry(j, framePalette.getEntry(i)); + remap.map(i, j); + ++j; + } } + + colormap = createColorMap(&reducedPalette); + if (localTransparent >= 0) + localTransparent = remap[localTransparent]; } - colormap = createColorMap(&reducedPalette); - if (localTransparent >= 0) - localTransparent = remap[localTransparent]; + 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); } - - if (localTransparent >= 0 && m_transparentIndex != localTransparent) - remap.map(m_transparentIndex, localTransparent); // Write extension record. writeExtension(gifFrame, frame, localTransparent, @@ -1337,20 +1505,173 @@ private: GifFreeMapObject(colormap); } - Palette* createOptimizedPalette(const gfx::Rect& frameBounds) { + Palette calculatePalette(const gfx::Rect& frameBounds, + const 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(), 256)); + if (pal.size() == 256) { + // Here the palette has 256 colors, there is no place to include + // the 0 color (createOptimizedPalette() doesn't create an 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; + if (disposal == DisposalMethod::DO_NOT_DISPOSE) + auxPalette = createOptimizedPalette(m_currentImage, frameBounds, 257); + else + auxPalette = pal; + + if (auxPalette.size() <= 256) { + // We are fine with color count in m_currentImage contained in + // frameBounds (we got 256 or less colors): + m_transparentIndex = -1; + pal = auxPalette; + if (disposal == DisposalMethod::DO_NOT_DISPOSE) { + ASSERT(frameBounds.w >= 1); + 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: + // 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). + // + // m_currentImage__ __ m_deltaImage (same rectangle size as `frameBounds` variable) + // | | + // --------------*----|----------- + // | | | + // | --------------*- | + // | | | | + // | | ________ | | + // | | | | *--------------- square border (we will collect + // | | | | | | high precision colors from this area, less than 220) + // | | | | | | + // | | | *--------------------- center rectangle (we will to quantize + // | | | | | | colors contained in this area) + // | | |________| | | + // | | | | + // | |________________| | + // | | + // |_______________________________| + // + + const gfx::Size deltaSize = m_deltaImage->size(); + int thicknessTop = deltaSize.h / 4; + int thicknessLeft = deltaSize.w / 4; + int repeatCounter = 0; + while (repeatCounter < 10 && thicknessTop > 0 && thicknessLeft > 0) { + + // ---------------- + // |________________| + // | | | | + // | | | | + // | |________| | + // |________________| + render::PaletteOptimizer optimizer; + gfx::Rect auxRect(0, 0, deltaSize.w, thicknessTop); + optimizer.feedWithImage(m_deltaImage.get(), auxRect, false); + + // ---------------- + // | ________ | + // | | | | + // | | | | + // |___|________|___| + // |________________| + auxRect = gfx::Rect(0, deltaSize.h - thicknessTop - 1, deltaSize.w, thicknessTop); + optimizer.feedWithImage(m_deltaImage.get(), auxRect, false); + + // ---------------- + // |____________ | + // | | | | + // | | | | + // |___|________| | + // |________________| + auxRect = gfx::Rect(0, thicknessTop, thicknessLeft, deltaSize.h - 2 * thicknessTop); + optimizer.feedWithImage(m_deltaImage.get(), auxRect, false); + + // ---------------- + // | _____________| + // | | | | + // | | | | + // | |________|___| + // |________________| + auxRect = gfx::Rect(deltaSize.w - thicknessLeft - 1, thicknessTop, thicknessLeft, deltaSize.h - 2 * thicknessTop); + optimizer.feedWithImage(m_deltaImage.get(), auxRect, false); + + 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; + } + else { + thicknessTop -= thicknessTop / 2; + thicknessLeft -= thicknessLeft / 2; + } + + repeatCounter++; + } + // Quantize the colors contained into center rectangle and add these in `pal`: + if (pal.size() < 255) { + gfx::Rect centerRect(thicknessLeft, + thicknessTop, + deltaSize.w - 2 * thicknessLeft, + deltaSize.h - 2 * thicknessTop); + 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); + pal.addEntry(0); + m_transparentIndex = pal.size() - 1; + } + } + // We are fine, we got 255 or less, there is room for the transparent color + else if (pal.size() <= 255) { + pal.addEntry(0); + m_transparentIndex = pal.size() - 1; + } + return pal; + } + + static Palette createOptimizedPalette(const Image* image, + const gfx::Rect& bounds, + const int ncolors) { 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) + // Feed the palette optimizer with pixels inside the given bounds + 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 +1680,10 @@ private: render.setNewBlend(m_fop->newBlend()); render.setBgType(render::BgType::NONE); - clear_image(dst, m_clearColor); + if (m_preservePaletteOrder) + clear_image(dst, m_bgIndex); + else + clear_image(dst, 0); render.renderSprite(dst, m_sprite, frame); } @@ -1397,18 +1721,23 @@ private: gfx::Rect m_spriteBounds; bool m_hasBackground; int m_bgIndex; - color_t m_clearColor; int m_transparentIndex; int m_bitsPerPixel; + // Global palette to use on all frames, or nullptr in case that we + // have to quantize the palette on each frame. ColorMapObject* m_globalColormap; - bool m_quantizeColormaps; + Palette m_globalColormapPalette; 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 +1776,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(); diff --git a/src/app/file/gif_options.h b/src/app/file/gif_options.h index d5b468003..3dfb393d6 100644 --- a/src/app/file/gif_options.h +++ b/src/app/file/gif_options.h @@ -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 diff --git a/src/doc/palette.cpp b/src/doc/palette.cpp index 9894928d9..b8bc9ec3f 100644 --- a/src/doc/palette.cpp +++ b/src/doc/palette.cpp @@ -22,6 +22,11 @@ namespace doc { using namespace gfx; +Palette::Palette() + : Palette(0, 256) +{ +} + Palette::Palette(frame_t frame, int ncolors) : Object(ObjectType::Palette) { @@ -58,6 +63,18 @@ Palette::~Palette() { } +Palette& Palette::operator=(const Palette& that) +{ + m_frame = that.m_frame; + m_colors = that.m_colors; + m_names = that.m_names; + m_filename = that.m_filename; + m_comment = that.m_comment; + + ++m_modifications; + return *this; +} + Palette* Palette::createGrayscale() { Palette* graypal = new Palette(frame_t(0), 256); diff --git a/src/doc/palette.h b/src/doc/palette.h index 2b28d9eb7..04f713775 100644 --- a/src/doc/palette.h +++ b/src/doc/palette.h @@ -1,6 +1,6 @@ // Aseprite Document Library -// Copyright (c) 2020 Igara Studio S.A. -// Copyright (c) 2001-2018 David Capello +// Copyright (C) 2020 Igara Studio S.A. +// Copyright (C) 2001-2018 David Capello // // This file is released under the terms of the MIT license. // Read LICENSE.txt for more information. @@ -23,11 +23,14 @@ namespace doc { class Palette : public Object { public: + Palette(); Palette(frame_t frame, int ncolors); Palette(const Palette& palette); Palette(const Palette& palette, const Remap& remap); ~Palette(); + Palette& operator=(const Palette& that); + static Palette* createGrayscale(); int size() const { return (int)m_colors.size(); } diff --git a/src/render/color_histogram.h b/src/render/color_histogram.h index 77d326bc1..e591b80ff 100644 --- a/src/render/color_histogram.h +++ b/src/render/color_histogram.h @@ -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 diff --git a/src/render/quantization.cpp b/src/render/quantization.cpp index 4129e87d9..a30663d29 100644 --- a/src/render/quantization.cpp +++ b/src/render/quantization.cpp @@ -43,7 +43,8 @@ Palette* create_palette_from_sprite( Palette* palette, TaskDelegate* delegate, const bool newBlend, - const RgbMapAlgorithm mappingAlgorithm) + const RgbMapAlgorithm mappingAlgorithm, + const bool calculateWithTransparent) { PaletteOptimizer optimizer; OctreeMap octreemap; @@ -88,10 +89,13 @@ 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()); break; case RgbMapAlgorithm::OCTREE: + // TODO check calculateWithTransparent flag + if (!octreemap.makePalette(palette, palette->size())) { // We can use an 8-bit deep octree map, instead of 7-bit of the // first attempt. @@ -379,7 +383,15 @@ Image* convert_pixel_format( // Creation of optimized palette for RGB images // by David Capello -void PaletteOptimizer::feedWithImage(Image* image, bool withAlpha) +void PaletteOptimizer::feedWithImage(const Image* image, + const bool withAlpha) +{ + feedWithImage(image, image->bounds(), withAlpha); +} + +void PaletteOptimizer::feedWithImage(const Image* image, + const gfx::Rect& bounds, + const bool withAlpha) { uint32_t color; @@ -391,8 +403,8 @@ void PaletteOptimizer::feedWithImage(Image* image, bool withAlpha) case IMAGE_RGB: { - const LockImageBits<RgbTraits> bits(image); - LockImageBits<RgbTraits>::const_iterator it = bits.begin(), end = bits.end(); + const LockImageBits<RgbTraits> bits(image, bounds); + auto it = bits.begin(), end = bits.end(); for (; it != end; ++it) { color = *it; @@ -408,8 +420,8 @@ void PaletteOptimizer::feedWithImage(Image* image, bool withAlpha) case IMAGE_GRAYSCALE: { - const LockImageBits<RgbTraits> bits(image); - LockImageBits<RgbTraits>::const_iterator it = bits.begin(), end = bits.end(); + const LockImageBits<GrayscaleTraits> bits(image, bounds); + auto it = bits.begin(), end = bits.end(); for (; it != end; ++it) { color = *it; diff --git a/src/render/quantization.h b/src/render/quantization.h index 1ab8627f1..97e7e12fd 100644 --- a/src/render/quantization.h +++ b/src/render/quantization.h @@ -29,9 +29,15 @@ namespace render { class PaletteOptimizer { public: - void feedWithImage(doc::Image* image, bool withAlpha); + void feedWithImage(const doc::Image* image, + const bool withAlpha); + void feedWithImage(const doc::Image* image, + const gfx::Rect& bounds, + const 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; @@ -47,7 +53,8 @@ namespace render { doc::Palette* newPalette, // Can be NULL to create a new palette TaskDelegate* delegate, const bool newBlend, - const RgbMapAlgorithm mappingAlgorithm); + const RgbMapAlgorithm mappingAlgorithm, + const bool calculateWithTransparent = true); // Changes the image pixel format. The dithering method is used only // when you want to convert from RGB to Indexed.