Home

Awesome

anim8-gdx

Support for writing animated GIF, PNG8, and animated PNG (including full-color) from libGDX

There's been support for writing some image-file types from libGDX for a while, via its PixmapIO class. PixmapIO can write full-color PNG files, plus the libGDX-specific CIM file format. It can't write any animated image formats, nor can it write any indexed-mode images (which use a palette, and tend to be smaller files). This library, anim8, allows libGDX applications to write animated GIF files, indexed-mode PNG files, and animated PNG files (with either full-color or palette-based color). The API tries to imitate the PixmapIO.PNG nested class, but supporting a palette needs some new methods. For a simple use case, here's a writeGif() method that calls render() 20 times and screenshots each frame:

public void writeGif() {
    final int frameCount = 20;
    Array<Pixmap> pixmaps = new Array<>(frameCount);
    for (int i = 0; i < frameCount; i++) {
// you could set the proper state for a frame here.

// you don't need to call render() in all cases, especially if you have Pixmaps already.
// this assumes you're calling this from a class that uses render() to draw to the screen.
        render();
// this gets a screenshot of the current window and adds it to the Array of Pixmap.
// there are two ways to do this; this way works in older libGDX versions, but it is deprecated in current libGDX: 
//        pixmaps.add(ScreenUtils.getFrameBufferPixmap(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight()));
// the newer way is only available in more-recent libGDX (I know 1.10.0 through 1.12.0 have it); it is not deprecated:
        pixmaps.add(Pixmap.createFromFrameBuffer(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight()));
    }
// AnimatedGif is from anim8; if no extra settings are specified it will calculate a 255-color palette from
// each given frame and use the most appropriate palette for each frame, dithering any colors that don't
// match. The other file-writing classes don't do this; PNG8 doesn't currently support a palette per-frame,
// while AnimatedPNG doesn't restrict colors to a palette. See Dithering Algorithms below for visual things
// to be aware of and choices you can make.
// You can also use FastGif in place of AnimatedGif; it may be a little faster, but might not have great color quality.
    AnimatedGif gif = new AnimatedGif();
// you can write to a FileHandle or an OutputStream; here, the file will be written in the current directory.
// here, pixmaps is usually an Array of Pixmap for any of the animated image types.
// 16 is how many frames per second the animated GIF should play back at.
    gif.write(Gdx.files.local("AnimatedGif.gif"), pixmaps, 16);
}

The above code uses AnimatedGif, but could also use AnimatedPNG or PNG8 to write to an animated PNG (with full-color or palette-based color, respectively). The FastGif, and FastPNG8 options are also out there, and they tend to be a little faster to run but produce larger files. There's also FastPNG, which is a replacement for PixmapIO.PNG, and does tend to be faster than it as well while producing full-color non-animated PNG images.

If you are writing an image with a palette, such as a GIF or an indexed-mode PNG (called PNG8 here), the palette is limited to using at most 255 opaque colors, plus one fully-transparent color. To adequately reduce an image to a smaller palette, the general technique is to choose or generate a fitting palette, then to dither the image to break up solid blocks of one color, and try to maintain or suggest any subtle gradients that were present before reduction. To choose an existing palette, you use PaletteReducer's exact() method, which takes an int array or similar collection of RGBA8888 colors. You might want to get a small palette from LoSpec, for example. You could go through the steps of downloading a .hex file (or another text palette) and converting it to a Java int[] syntax... or you could simply get a palette image (or any image that only uses the palette you want, with 255 colors or fewer) and call PaletteReducer.colorsFrom(Pixmap) to get an int array to pass to exact().

To generate a palette that fits an existing many-color image (or group of images), you use PaletteReducer's analyze() method, which takes a Pixmap, plus optionally a color threshold and a color count (most usage only needs a count of 256, but the threshold can vary based on the image or images). Calling analyze() isn't incredibly fast, and it can take the bulk of the time spent making an animated GIF if each frame has its own palette. Analyzing just once is sufficient for many uses, though, and as long as the threshold is right, it can produce a nicely-fitting palette. Once you have called exact() or analyze(), you can use the PaletteReducer in a PNG8 or in an AnimatedGif, or on its own if you just want to color-reduce Pixmaps. There are also two variants; FastPalette, which is like PaletteReducer but uses a possibly-faster and lower-quality way of comparing colors, and QualityPalette, which is also like PaletteReducer but uses a typically-higher-quality color difference calculation that is also slower. There's more on this topic later, since this is a major focus of the library.

Install

A typical Gradle dependency on anim8 looks like this (in the core module's dependencies for a typical libGDX project):

dependencies {
  //... other dependencies are here, like libGDX 1.9.11 or higher
  // libGDX 1.12.1 is recommended currently, but versions as old as 1.9.11 work.
  api "com.github.tommyettinger:anim8-gdx:0.5.0"
}

You can also get a specific commit using JitPack, by following the instructions on JitPack's page for anim8. (You usually want to select a recent commit, unless you are experiencing problems with one in particular.)

A .gwt.xml file is present in the sources jar, and because GWT needs it, you can depend on the sources jar with implementation "com.github.tommyettinger:anim8-gdx:0.5.0:sources". The PNG-related code isn't available on GWT because it needs java.util.zip, which is unavailable there, but PaletteReducer and AnimatedGif should both work, as should QualityPalette. The classes FastGif and FastPalette should work on GWT, but no other "Fast" classes will. The GWT inherits line, which is needed in GdxDefinition.gwt.xml, is:

<inherits name="com.github.tommyettinger.anim8" />

Dithering Algorithms

You have a choice between several dithering algorithms if you write to GIF or PNG8; you can also avoid choosing one entirely by using AnimatedPNG (it uses full color) or libGDX's PixmapIO.PNG (which isn't animated and has a slightly different API). You could also use FastPNG, which is like PixmapIO's code but tends to write larger files, do so more quickly, and avoid losing any color information.

All dithering algorithms except NONE, CHAOTIC_NOISE, and PATTERN changed appearance significantly in version 0.5.0 because that version includes at least an attempt at gamma-correcting the images, and earlier versions did not. That means 0.5.0 should usually have closer lightness in the dither to what the original image had, relative to earlier anim8-gdx versions. History from ancient versions of anim8-gdx has been removed from this section for clarity.

You can set the strength of most of these dithers using PaletteReducer's, PNG8's, or AnimatedGif's setDitherStrength(float) methods (use the method on the class that is producing output). For NONE, there's no effect. For CHAOTIC_NOISE, there's almost no effect. For anything else, setting dither strength to close to 0 will approach the appearance of NONE, setting it close to 1.0 is the default, and strengths higher than 1 will make the dither much stronger and may make the image less legible. NEUE, SCATTER, DODGY, and DIFFUSION sometimes have trouble with very high dither strengths, though how much trouble varies based on the palette, and they also tend to look good just before major issues appear. NEUE is calibrated to look best at dither strength 1.0, as is DODGY, but NEUE may stay looking good at higher strengths for longer than SCATTER or DODGY do. GOURD is quite sensitive to changes in ditherStrength; it usually doesn't look very good with strength less than 0.75f.

Palette Generation

You can create a PaletteReducer object by manually specifying an exact palette (useful for pixel art), attempting to analyze an existing image or animation (which can work well for large palette sizes, but not small sizes), or using the default palette (called "SNUGGLY", it nicely fits 255 colors plus transparent). Of these, using analyze() is the trickiest, and it generally should be permitted all 256 colors to work with. With analyze(), you can specify the threshold between colors for it to consider adding one to the palette, and this is a challenging value to set that depends on the image being dithered. Typically, between 50 and 200 are used, with higher values for smaller or more diverse palettes (that is, ones with fewer similar colors to try to keep). Usually you will do just fine with the default "SNUGGLY" palette, or almost any practical 250+ color palette, because with so many colors it's hard to go wrong. Creating a PaletteReducer without arguments, or calling setDefaultPalette() later, will set it to use SNUGGLY.

GIF supports using a different palette for each frame of an animation, analyzing colors separately for each frame. This supplements the previous behavior where a palette would analyze all frames of an animation and find a 255-color palette that approximates the whole set of all frames well-enough. PNG8 still uses the previous behavior, and you can use it with AnimatedGif by creating a PaletteReducer with an Array<Pixmap> or calling PaletteReducer.analyze(Array<Pixmap>). To analyze each frame separately, just make sure the palette field of your AnimatedGif is null when you start writing a GIF. The fastAnalysis field on an AnimatedGif object determines whether (if true) it uses a fast but approximate algorithm per frame, or (if false) it uses the same analysis for each frame that it normally would for a still image. You can also create a PaletteReducer, passing it an Array<Pixmap>, and assign that to the palette field; this is reasonably fast and also ensures every frame will use the same palette (which means regions of solid color that don't change in the source won't change in the GIF; this isn't true if palette is null).

You can use any of the PaletteReducer.analyzeHueWise() methods to analyze the palette of a Pixmap or multiple Pixmaps. This approach works well with rather small palettes (about 16 colors) because it tries to ensure some colors from every hue present in the image will be available in the palette. It stops being noticeably better than analyze() at around 25-30 colors in a palette (this can vary based on the image), and is almost always slower than analyze(). Thanks to caramel for (very quickly) devising this algorithm for palette construction. analyzeHueWise() is available in FastPalette, but not optimized any differently from in PaletteReducer.

You can use PaletteReducer.analyzeReductive() as an alternative to PaletteReducer.analyze() or other ways. It does rather well on small palettes (such as a 16-color reduction). This analysis involves trimming down a huge 1024-color palette until it (in theory) contains only colors that match the current image well. For smaller palettes, it can do considerably better than analyze() or analyzeHueWise(), but there isn't much difference at 256 colors. The actual palette this trims down is essentially a 4x-expanded version of the default SNUGGLY255 palette, and like it, was created by deterministically sampling the Oklab color space until enough colors were found, then Lloyd-relaxing the Voronoi cells around each color in Oklab space. (No one needs to understand that last sentence.)

All these color analysis techniques use comparable threshold values, defaulting to 100. Some palettes may need a higher or lower threshold only with some methods, though.

Samples

Some animations, using 255 colors taken from the most-used in the animation (analyze(), which does well here because it can use all the colors), are here on Imgur. These are all indexed-color animated PNG files, produced with the AnimatedGif class and converted to animated PNG with a separate tool; using this approach seems to avoid lossy compression on Imgur. Those use AnimatedGif's new fastAnalysis option; you can compare them with fastAnalysis set to false here on Imgur. Running with fastAnalysis set to true (and also generating APNG images on the side) took about 40 seconds; with fastAnalysis false, about 129 seconds.

If the animated PNG files aren't... animating... you can blame Imgur for that. If I can get GIF files to upload losslessly there or somewhere else, I will try some alternative. The previews also aren't up-to-date with the most recent dithering algorithms here, such as a changed version of LOAF and the new OVERBOARD dither.

Some more .gif animations were made with the new fastAnalysis option; you can compare with fastAnalysis set to true here on Imgur, and with fastAnalysis false here on Imgur. Like before, these were all converted to APNG so Imgur won't compress them, but they kept the same palette(s). Running with fastAnalysis set to true took about 25 seconds; with false, over 130 seconds.

Some .gif animations that reduce the colors of the "flashy" animation shown are here on Imgur, reduced to black and white, and here on Imgur, reduced to 4-color "green-scale".

And some .png animations, using full color (made with the AnimatedPNG class):

Flashy Full-Color PNG

Pastel Full-Color PNG

Green Ogre Full-Color PNG

A more intense usage is to encode a high-color video as an indexed-color GIF; why you might do this, I don't know, but someone probably wants videos as GIFs. There's some test footage here from "Video Of A Market" by Olivier Polome, which is freely licensed without requirements. You can run the test "VideoConvertDemo" to generate various GIFs locally. I can't reasonably host the large GIF files with Git.

Animated PNG can support full alpha as well (though file sizes can be large):

Full-Color PNG with Alpha

Anim8 also can be used to support writing non-animated GIF images and indexed-mode PNG images. Here's a retouched version of the Mona Lisa, source on Wikimedia Commons here, and various 8-color dithers using polyphrog's Prospecal palette:

Original (full-color):

Overboard (the current default):

Oceanic:

Seaside:

Burkes:

Gourd:

Wren:

Neue:

Dodgy:

Woven:

Pattern:

Diffusion:

Gradient Noise:

Blue Noise:

Chaotic Noise:

Scatter:

Roberts:

Loaf:

None (no dither):

This doesn't call the analyze() method on the original image, and instead uses exact() with the aforementioned Prospecal palette. If you are using analyze(), it works best when permitted all 255 colors available to it. Restricting this oil painting to 8 colors is very challenging to dither well, and some algorithms definitely do a better job than others with such a small palette. However, with a 255-color palette, most of the algorithms are similar, and you mostly want to pick one with few or no artifacts that affect your image(s).

(If the Wikimedia Commons source file is deleted, the original is available in the history of this other image).

License

The code in this project is licensed under Apache 2.0 (see LICENSE). The test images have their own licenses, though most are public-domain. Of the test images used in the src/test/resources/ folder and its subfolders...