
com.github.tommyettinger.anim8.AnimatedGif Maven / Gradle / Ivy
package com.github.tommyettinger.anim8;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.FloatArray;
import com.badlogic.gdx.utils.StreamUtils;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
/**
* GIF encoder using standard LZW compression; can write animated and non-animated GIF images.
* An instance can be reused to encode multiple GIFs with minimal allocation.
*
* You can configure the target palette and how this can dither colors via the {@link #palette} field, which is a
* {@link PaletteReducer} object that defaults to null and can be reused. If you assign a PaletteReducer to palette, the
* methods {@link PaletteReducer#exact(Color[])} or {@link PaletteReducer#analyze(Pixmap)} can be used to make the
* target palette match a specific set of colors or the colors in an existing image. If palette is null, this will
* compute a palette for each GIF that closely fits its set of given animation frames. If the palette isn't an exact
* match for the colors used in an animation (GIF can store at most 256 colors), this will dither pixels so that from a
* distance, they look closer to the original colors. You can us {@link PaletteReducer#setDitherStrength(float)} to
* reduce (or increase) dither strength, typically between 0 and 2; the dithering algorithm used here by default is
* based on blue noise and a quasi-random pattern ({@link DitherAlgorithm#BLUE_NOISE}), but you can select alternatives
* with {@link #setDitherAlgorithm(DitherAlgorithm)}, like a modified version of Jorge Jimenez' Gradient Interleaved
* Noise using {@link DitherAlgorithm#GRADIENT_NOISE}, or no dither at all with {@link DitherAlgorithm#NONE}.
*
* You can write non-animated GIFs with this, but libGDX can't read them back in, so you may want to prefer {@link PNG8}
* for images with 256 or fewer colors and no animation (libGDX can read in non-animated PNG files, as well as the first
* frame of animated PNG files). If you have an animation that doesn't look good with dithering or has multiple levels
* of transparency (GIF only supports one fully transparent color), you can use {@link AnimatedPNG} to output a
* full-color animation. If you have a non-animated image that you want to save in lossless full-color, just use
* {@link com.badlogic.gdx.graphics.PixmapIO.PNG}; the API is slightly different, but the PNG code here is based on it.
*
* Based on Nick Badal's Android port ( https://github.com/nbadal/android-gif-encoder/blob/master/GifEncoder.java ) of
* Alessandro La Rossa's J2ME port ( http://www.jappit.com/blog/2008/12/04/j2me-animated-gif-encoder/ ) of this pure
* Java animated GIF encoder by Kevin Weiner ( http://www.java2s.com/Code/Java/2D-Graphics-GUI/AnimatedGifEncoder.htm ).
* The original has no copyright asserted, so this file continues that tradition and does not assert copyright either.
*/
public class AnimatedGif implements AnimationWriter, Dithered {
/**
* Writes the given Pixmap values in {@code frames}, in order, to an animated GIF at {@code file}. Always writes at
* 30 frames per second, so if frames has less than 30 items, this animation will be under a second long.
* @param file the FileHandle to write to; should generally not be internal because it must be writable
* @param frames an Array of Pixmap frames that should all be the same size, to be written in order
*/
@Override
public void write(FileHandle file, Array frames) {
write(file, frames, 30);
}
/**
* Writes the given Pixmap values in {@code frames}, in order, to an animated GIF at {@code file}. The resulting GIF
* will play back at {@code fps} frames per second.
* @param file the FileHandle to write to; should generally not be internal because it must be writable
* @param frames an Array of Pixmap frames that should all be the same size, to be written in order
* @param fps how many frames (from {@code frames}) to play back per second
*/
@Override
public void write(FileHandle file, Array frames, int fps) {
OutputStream output = file.write(false);
try {
write(output, frames, fps);
} finally {
StreamUtils.closeQuietly(output);
}
}
/**
* Writes the given Pixmap values in {@code frames}, in order, to an animated GIF in the OutputStream
* {@code output}. The resulting GIF will play back at {@code fps} frames per second.
* @param output the OutputStream to write to; will not be closed by this method
* @param frames an Array of Pixmap frames that should all be the same size, to be written in order
* @param fps how many frames (from {@code frames}) to play back per second
*/
@Override
public void write(OutputStream output, Array frames, int fps) {
boolean clearPalette;
if (clearPalette = (palette == null))
palette = new PaletteReducer(frames);
if(!start(output)) return;
setFrameRate(fps);
for (int i = 0; i < frames.size; i++) {
addFrame(frames.get(i));
}
finish();
if(clearPalette)
palette = null;
}
protected DitherAlgorithm ditherAlgorithm = DitherAlgorithm.SCATTER;
protected int width; // image size
protected int height;
protected int x = 0;
protected int y = 0;
protected boolean flipY = true;
protected int transIndex = -1; // transparent index in color table
protected int repeat = 0; // loop repeat
protected int delay = 16; // frame delay (thousandths)
protected boolean started = false; // ready to output frames
protected OutputStream out;
protected Pixmap image; // current frame
protected byte[] indexedPixels; // converted frame indexed to palette
protected int colorDepth; // number of bit planes
protected byte[] colorTab; // RGB palette, 3 bytes per color
protected boolean[] usedEntry = new boolean[256]; // active palette entries
protected int palSize = 7; // color table size (bits-1)
protected int dispose = -1; // disposal code (-1 = use default)
protected boolean closeStream = false; // close stream when finished
protected boolean firstFrame = true;
protected boolean sizeSet = false; // if false, get size from first frame
protected int seq = 0;
public PaletteReducer palette;
/**
* Gets the PaletteReducer this uses to lower the color count in an image. If the PaletteReducer is null, this
* should try to assign itself a PaletteReducer when given a new image.
*
* @return the PaletteReducer this uses; may be null
*/
@Override
public PaletteReducer getPalette() {
return palette;
}
/**
* Sets the PaletteReducer this uses to bring a high-color or different-palette image down to a smaller palette
* size. If {@code palette} is null, this should try to assign itself a PaletteReducer when given a new image.
*
* @param palette a PaletteReducer that is often pre-configured with a specific palette; null is usually allowed
*/
@Override
public void setPalette(PaletteReducer palette) {
this.palette = palette;
}
/**
* Sets the delay time between each frame, or changes it for subsequent frames
* (applies to last frame added).
*
* @param ms int delay time in milliseconds
*/
public void setDelay(int ms) {
delay = ms;
}
/**
* Sets the GIF frame disposal code for the last added frame and any
* subsequent frames. Default is 0 if no transparent color has been set,
* otherwise 2.
*
* @param code int disposal code.
*/
public void setDispose(int code) {
if (code >= 0) {
dispose = code;
}
}
/**
* Sets the number of times the set of GIF frames should be played. Default is
* 1; 0 means play indefinitely. Must be invoked before the first image is
* added.
*
* @param iter int number of iterations.
*/
public void setRepeat(int iter) {
if (iter >= 0) {
repeat = iter;
}
}
/**
* Returns true if the output is flipped top-to-bottom from the inputs (the default); otherwise returns false.
* @return true if the inputs are flipped on writing, false otherwise
*/
public boolean isFlipY() {
return flipY;
}
/**
* Sets whether this should flip inputs top-to-bottom (true, the default setting), or leave as-is (false).
* @param flipY true if this should flip inputs top-to-bottom when writing, false otherwise
*/
public void setFlipY(boolean flipY) {
this.flipY = flipY;
}
/**
* Gets the {@link DitherAlgorithm} this is currently using.
* @return which dithering algorithm this currently uses.
*/
public DitherAlgorithm getDitherAlgorithm() {
return ditherAlgorithm;
}
/**
* Sets the dither algorithm (or disables it) using an enum constant from {@link DitherAlgorithm}. If this
* is given null, it instead does nothing.
* @param ditherAlgorithm which {@link DitherAlgorithm} to use for upcoming output
*/
public void setDitherAlgorithm(DitherAlgorithm ditherAlgorithm) {
if(ditherAlgorithm != null)
this.ditherAlgorithm = ditherAlgorithm;
}
/**
* Adds next GIF frame. The frame is not written immediately, but is actually
* deferred until the next frame is received so that timing data can be
* inserted. Invoking finish()
flushes all frames. If
* setSize
was not invoked, the size of the first image is used
* for all subsequent frames.
*
* @param im BufferedImage containing frame to write.
* @return true if successful.
*/
public boolean addFrame(Pixmap im) {
if ((im == null) || !started) {
return false;
}
boolean ok = true;
try {
if (!sizeSet) {
// use first frame's size
setSize(im.getWidth(), im.getHeight());
}
++seq;
image = im;
getImagePixels(); // convert to correct format if necessary
analyzePixels(); // build color table & map pixels
if (firstFrame) {
writeLSD(); // logical screen descriptior
writePalette(); // global color table
if (repeat >= 0) {
// use NS app extension to indicate reps
writeNetscapeExt();
}
}
writeGraphicCtrlExt(); // write graphic control extension
writeImageDesc(); // image descriptor
if (!firstFrame) {
writePalette(); // local color table
}
writePixels(); // encode and write pixel data
firstFrame = false;
} catch (IOException e) {
ok = false;
}
return ok;
}
/**
* Flushes any pending data and closes output file. If writing to an
* OutputStream, the stream is not closed.
*/
public boolean finish() {
if (!started)
return false;
boolean ok = true;
started = false;
try {
out.write(0x3b); // gif trailer
out.flush();
if (closeStream) {
out.close();
}
} catch (IOException e) {
ok = false;
}
// reset for subsequent use
transIndex = -1;
out = null;
image = null;
indexedPixels = null;
colorTab = null;
closeStream = false;
sizeSet = false;
firstFrame = true;
seq = 0;
return ok;
}
/**
* Sets frame rate in frames per second. Equivalent to
* setDelay(1000.0f / fps)
.
*
* @param fps float frame rate (frames per second)
*/
public void setFrameRate(float fps) {
if (fps != 0f) {
delay = (int) (1000f / fps);
}
}
/**
* Sets the GIF frame size. The default size is the size of the first frame
* added if this method is not invoked.
*
* @param w int frame width.
* @param h int frame width.
*/
public void setSize(int w, int h) {
width = w;
height = h;
if (width < 1)
width = 320;
if (height < 1)
height = 240;
sizeSet = true;
}
/**
* Sets the GIF frame position. The position is 0,0 by default.
* Useful for only updating a section of the image
*
* @param x int frame x position.
* @param y int frame y position.
*/
public void setPosition(int x, int y) {
this.x = x;
this.y = y;
}
/**
* Initiates GIF file creation on the given stream. The stream is not closed
* automatically.
*
* @param os OutputStream on which GIF images are written.
* @return false if initial write failed.
*/
public boolean start(OutputStream os) {
if (os == null)
return false;
boolean ok = true;
closeStream = false;
out = os;
try {
writeString("GIF89a"); // header
} catch (IOException e) {
ok = false;
Gdx.app.error("anim8", e.getMessage());
}
return started = ok;
}
/**
* Analyzes image colors and creates color map.
*/
protected void analyzePixels() {
int nPix = width * height;
indexedPixels = new byte[nPix];
// palette.analyze(image);
final int[] paletteArray = palette.paletteArray;
final byte[] paletteMapping = palette.paletteMapping;
// initialize quantizer
colorTab = new byte[256 * 3]; // create reduced palette
for (int i = 0, bi = 0; i < 256; i++) {
int pa = paletteArray[i];
colorTab[bi++] = (byte) (pa >>> 24);
colorTab[bi++] = (byte) (pa >>> 16);
colorTab[bi++] = (byte) (pa >>> 8);
usedEntry[i] = false;
}
// map image pixels to new palette
int color, used, flipped = flipY ? height - 1 : 0, flipDir = flipY ? -1 : 1;
boolean hasTransparent = paletteArray[0] == 0;
switch (ditherAlgorithm) {
case NONE: {
for (int y = 0, i = 0; y < height && i < nPix; y++) {
for (int px = 0; px < width & i < nPix; px++) {
color = image.getPixel(px, flipped + flipDir * y) & 0xF8F8F880;
if ((color & 0x80) == 0 && hasTransparent)
indexedPixels[i++] = 0;
else {
usedEntry[(indexedPixels[i] = paletteMapping[
(color >>> 17 & 0x7C00)
| (color >>> 14 & 0x3E0)
| ((color >>> 11 & 0x1F))]) & 255] = true;
i++;
}
}
}
}
break;
case PATTERN:
{
int cr, cg, cb, usedIndex;
final float errorMul = (float) (palette.ditherStrength * palette.populationBias);
for (int y = 0, i = 0; y < height && i < nPix; y++) {
for (int px = 0; px < width & i < nPix; px++) {
color = image.getPixel(px, flipped + flipDir * y);
if ((color & 0x80) == 0 && hasTransparent)
indexedPixels[i++] = 0;
else {
int er = 0, eg = 0, eb = 0;
cr = (color >>> 24);
cg = (color >>> 16 & 0xFF);
cb = (color >>> 8 & 0xFF);
for (int c = 0; c < 16; c++) {
int rr = Math.min(Math.max((int) (cr + er * errorMul), 0), 255);
int gg = Math.min(Math.max((int) (cg + eg * errorMul), 0), 255);
int bb = Math.min(Math.max((int) (cb + eb * errorMul), 0), 255);
usedIndex = paletteMapping[((rr << 7) & 0x7C00)
| ((gg << 2) & 0x3E0)
| ((bb >>> 3))] & 0xFF;
palette.candidates[c | 16] = PaletteReducer.shrink(used = palette.candidates[c] = paletteArray[usedIndex]);
er += cr - (used >>> 24);
eg += cg - (used >>> 16 & 0xFF);
eb += cb - (used >>> 8 & 0xFF);
}
PaletteReducer.sort16(palette.candidates);
usedEntry[(indexedPixels[i] = (byte) palette.reverseMap.get(palette.candidates[
PaletteReducer.thresholdMatrix16[((px & 3) | (y & 3) << 2)]], 1)
) & 255] = true;
i++;
}
}
}
}
break;
case CHAOTIC_NOISE: {
double adj, strength = palette.ditherStrength * palette.populationBias * 1.5;
long s = 0xC13FA9A902A6328FL * seq;
for (int y = 0, i = 0; y < height && i < nPix; y++) {
for (int px = 0; px < width & i < nPix; px++) {
color = image.getPixel(px, flipped + flipDir * y) & 0xF8F8F880;
if ((color & 0x80) == 0 && hasTransparent)
indexedPixels[i++] = 0;
else {
color |= (color >>> 5 & 0x07070700) | 0xFF;
int rr = ((color >>> 24) );
int gg = ((color >>> 16) & 0xFF);
int bb = ((color >>> 8) & 0xFF);
used = paletteArray[paletteMapping[((rr << 7) & 0x7C00)
| ((gg << 2) & 0x3E0)
| ((bb >>> 3))] & 0xFF];
adj = ((PaletteReducer.RAW_BLUE_NOISE[(px & 63) | (y & 63) << 6] + 0.5f) * 0.007843138f);
adj *= adj * adj;
//// Complicated... This starts with a checkerboard of -0.5 and 0.5, times a tiny fraction.
//// The next 3 lines generate 3 low-quality-random numbers based on s, which should be
//// different as long as the colors encountered so far were different. The numbers can
//// each be positive or negative, and are reduced to a manageable size, summed, and
//// multiplied by the earlier tiny fraction. Summing 3 random values gives us a curved
//// distribution, centered on about 0.0 and weighted so most results are close to 0.
//// Two of the random numbers use an XLCG, and the last uses an LCG.
adj += ((px + y & 1) - 0.5f) * 0x1.8p-49 * strength *
(((s ^ 0x9E3779B97F4A7C15L) * 0xC6BC279692B5CC83L >> 15) +
((~s ^ 0xDB4F0B9175AE2165L) * 0xD1B54A32D192ED03L >> 15) +
((s = (s ^ color) * 0xD1342543DE82EF95L + 0x91E10DA5C79E7B1DL) >> 15));
rr = Math.min(Math.max((int) (rr + (adj * ((rr - (used >>> 24))))), 0), 0xFF);
gg = Math.min(Math.max((int) (gg + (adj * ((gg - (used >>> 16 & 0xFF))))), 0), 0xFF);
bb = Math.min(Math.max((int) (bb + (adj * ((bb - (used >>> 8 & 0xFF))))), 0), 0xFF);
usedEntry[(indexedPixels[i] = paletteMapping[((rr << 7) & 0x7C00)
| ((gg << 2) & 0x3E0)
| ((bb >>> 3))]) & 255] = true;
i++;
}
}
}
}
break;
case GRADIENT_NOISE: {
float pos, adj;
final float strength = (float) (palette.ditherStrength * palette.populationBias * 3.333);
for (int y = 0, i = 0; y < height && i < nPix; y++) {
for (int px = 0; px < width & i < nPix; px++) {
color = image.getPixel(px, flipped + flipDir * y) & 0xF8F8F880;
if ((color & 0x80) == 0 && hasTransparent)
indexedPixels[i++] = 0;
else {
color |= (color >>> 5 & 0x07070700) | 0xFE;
int rr = ((color >>> 24));
int gg = ((color >>> 16) & 0xFF);
int bb = ((color >>> 8) & 0xFF);
used = paletteArray[paletteMapping[((rr << 7) & 0x7C00)
| ((gg << 2) & 0x3E0)
| ((bb >>> 3))] & 0xFF];
pos = (px * 0.06711056f + y * 0.00583715f);
pos -= (int) pos;
pos *= 52.9829189f;
pos -= (int) pos;
adj = MathUtils.sin(pos * 2f - 1f) * strength;
// adj = (pos * pos - 0.3f) * strength;
rr = Math.min(Math.max((int) (rr + (adj * (rr - (used >>> 24)))), 0), 0xFF);
gg = Math.min(Math.max((int) (gg + (adj * (gg - (used >>> 16 & 0xFF)))), 0), 0xFF);
bb = Math.min(Math.max((int) (bb + (adj * (bb - (used >>> 8 & 0xFF)))), 0), 0xFF);
usedEntry[(indexedPixels[i] = paletteMapping[((rr << 7) & 0x7C00)
| ((gg << 2) & 0x3E0)
| ((bb >>> 3))]) & 255] = true;
i++;
}
}
}
}
break;
case DIFFUSION: {
final int w = width;
float rdiff, gdiff, bdiff;
float er, eg, eb;
byte paletteIndex;
float w1 = (float)(palette.ditherStrength * 4.0), w3 = w1 * 3f, w5 = w1 * 5f, w7 = w1 * 7f;
float[] curErrorRed, nextErrorRed, curErrorGreen, nextErrorGreen, curErrorBlue, nextErrorBlue;
if (palette.curErrorRedFloats == null) {
curErrorRed = (palette.curErrorRedFloats = new FloatArray(w)).items;
nextErrorRed = (palette.nextErrorRedFloats = new FloatArray(w)).items;
curErrorGreen = (palette.curErrorGreenFloats = new FloatArray(w)).items;
nextErrorGreen = (palette.nextErrorGreenFloats = new FloatArray(w)).items;
curErrorBlue = (palette.curErrorBlueFloats = new FloatArray(w)).items;
nextErrorBlue = (palette.nextErrorBlueFloats = new FloatArray(w)).items;
} else {
curErrorRed = palette.curErrorRedFloats.ensureCapacity(w);
nextErrorRed = palette.nextErrorRedFloats.ensureCapacity(w);
curErrorGreen = palette.curErrorGreenFloats.ensureCapacity(w);
nextErrorGreen = palette.nextErrorGreenFloats.ensureCapacity(w);
curErrorBlue = palette.curErrorBlueFloats.ensureCapacity(w);
nextErrorBlue = palette.nextErrorBlueFloats.ensureCapacity(w);
Arrays.fill(nextErrorRed, (byte) 0);
Arrays.fill(nextErrorGreen, (byte) 0);
Arrays.fill(nextErrorBlue, (byte) 0);
}
for (int y = 0, i = 0; y < height && i < nPix; y++) {
System.arraycopy(nextErrorRed, 0, curErrorRed, 0, w);
System.arraycopy(nextErrorGreen, 0, curErrorGreen, 0, w);
System.arraycopy(nextErrorBlue, 0, curErrorBlue, 0, w);
Arrays.fill(nextErrorRed, (byte) 0);
Arrays.fill(nextErrorGreen, (byte) 0);
Arrays.fill(nextErrorBlue, (byte) 0);
int py = flipped + flipDir * y,
ny = y + 1;
for (int px = 0; px < width & i < nPix; px++) {
color = image.getPixel(px, py) & 0xF8F8F880;
if ((color & 0x80) == 0 && hasTransparent)
indexedPixels[i++] = 0;
else {
er = curErrorRed[px];
eg = curErrorGreen[px];
eb = curErrorBlue[px];
color |= (color >>> 5 & 0x07070700) | 0xFF;
int rr = Math.min(Math.max((int)(((color >>> 24) ) + er + 0.5f), 0), 0xFF);
int gg = Math.min(Math.max((int)(((color >>> 16) & 0xFF) + eg + 0.5f), 0), 0xFF);
int bb = Math.min(Math.max((int)(((color >>> 8) & 0xFF) + eb + 0.5f), 0), 0xFF);
usedEntry[(indexedPixels[i] = paletteIndex =
paletteMapping[((rr << 7) & 0x7C00)
| ((gg << 2) & 0x3E0)
| ((bb >>> 3))]) & 255] = true;
used = paletteArray[paletteIndex & 0xFF];
rdiff = OtherMath.cbrtShape(0x1.8p-8f * ((color>>>24)- (used>>>24)) );
gdiff = OtherMath.cbrtShape(0x1.8p-8f * ((color>>>16&255)-(used>>>16&255)));
bdiff = OtherMath.cbrtShape(0x1.8p-8f * ((color>>>8&255)- (used>>>8&255)) );
if(px < w - 1)
{
curErrorRed[px+1] += rdiff * w7;
curErrorGreen[px+1] += gdiff * w7;
curErrorBlue[px+1] += bdiff * w7;
}
if(ny < height)
{
if(px > 0)
{
nextErrorRed[px-1] += rdiff * w3;
nextErrorGreen[px-1] += gdiff * w3;
nextErrorBlue[px-1] += bdiff * w3;
}
if(px < w - 1)
{
nextErrorRed[px+1] += rdiff * w1;
nextErrorGreen[px+1] += gdiff * w1;
nextErrorBlue[px+1] += bdiff * w1;
}
nextErrorRed[px] += rdiff * w5;
nextErrorGreen[px] += gdiff * w5;
nextErrorBlue[px] += bdiff * w5;
}
i++;
}
}
}
}
break;
case BLUE_NOISE: {
float adj, strength = (float) (palette.ditherStrength * palette.populationBias * 1.5);
for (int y = 0, i = 0; y < height && i < nPix; y++) {
for (int px = 0; px < width & i < nPix; px++) {
color = image.getPixel(px, flipped + flipDir * y) & 0xF8F8F880;
if ((color & 0x80) == 0 && hasTransparent)
indexedPixels[i++] = 0;
else {
color |= (color >>> 5 & 0x07070700) | 0xFF;
int rr = ((color >>> 24) );
int gg = ((color >>> 16) & 0xFF);
int bb = ((color >>> 8) & 0xFF);
used = paletteArray[paletteMapping[((rr << 7) & 0x7C00)
| ((gg << 2) & 0x3E0)
| ((bb >>> 3))] & 0xFF];
adj = ((PaletteReducer.RAW_BLUE_NOISE[(px & 63) | (y & 63) << 6] + 0.5f) * 0.007843138f); // 0.007843138f is 1f / 127.5f
adj += ((px + y & 1) - 0.5f) * (0.5f + PaletteReducer.RAW_BLUE_NOISE[(px * 19 & 63) | (y * 23 & 63) << 6]) * -0x1.6p-10f;
adj *= strength;
rr = Math.min(Math.max((int) (rr + (adj * ((rr - (used >>> 24))))), 0), 0xFF);
gg = Math.min(Math.max((int) (gg + (adj * ((gg - (used >>> 16 & 0xFF))))), 0), 0xFF);
bb = Math.min(Math.max((int) (bb + (adj * ((bb - (used >>> 8 & 0xFF))))), 0), 0xFF);
usedEntry[(indexedPixels[i] = paletteMapping[((rr << 7) & 0x7C00)
| ((gg << 2) & 0x3E0)
| ((bb >>> 3))]) & 255] = true;
i++;
}
}
}
}
break;
default:
case SCATTER: {
final int w = width;
float rdiff, gdiff, bdiff;
float er, eg, eb;
byte paletteIndex;
float w1 = (float)(palette.ditherStrength * 3.5), w3 = w1 * 3f, w5 = w1 * 5f, w7 = w1 * 7f;
float[] curErrorRed, nextErrorRed, curErrorGreen, nextErrorGreen, curErrorBlue, nextErrorBlue;
if (palette.curErrorRedFloats == null) {
curErrorRed = (palette.curErrorRedFloats = new FloatArray(w)).items;
nextErrorRed = (palette.nextErrorRedFloats = new FloatArray(w)).items;
curErrorGreen = (palette.curErrorGreenFloats = new FloatArray(w)).items;
nextErrorGreen = (palette.nextErrorGreenFloats = new FloatArray(w)).items;
curErrorBlue = (palette.curErrorBlueFloats = new FloatArray(w)).items;
nextErrorBlue = (palette.nextErrorBlueFloats = new FloatArray(w)).items;
} else {
curErrorRed = palette.curErrorRedFloats.ensureCapacity(w);
nextErrorRed = palette.nextErrorRedFloats.ensureCapacity(w);
curErrorGreen = palette.curErrorGreenFloats.ensureCapacity(w);
nextErrorGreen = palette.nextErrorGreenFloats.ensureCapacity(w);
curErrorBlue = palette.curErrorBlueFloats.ensureCapacity(w);
nextErrorBlue = palette.nextErrorBlueFloats.ensureCapacity(w);
Arrays.fill(nextErrorRed, (byte) 0);
Arrays.fill(nextErrorGreen, (byte) 0);
Arrays.fill(nextErrorBlue, (byte) 0);
}
for (int y = 0, i = 0; y < height && i < nPix; y++) {
System.arraycopy(nextErrorRed, 0, curErrorRed, 0, w);
System.arraycopy(nextErrorGreen, 0, curErrorGreen, 0, w);
System.arraycopy(nextErrorBlue, 0, curErrorBlue, 0, w);
Arrays.fill(nextErrorRed, (byte) 0);
Arrays.fill(nextErrorGreen, (byte) 0);
Arrays.fill(nextErrorBlue, (byte) 0);
int py = flipped + flipDir * y,
ny = y + 1;
for (int px = 0; px < width & i < nPix; px++) {
color = image.getPixel(px, py) & 0xF8F8F880;
if ((color & 0x80) == 0 && hasTransparent)
indexedPixels[i++] = 0;
else {
float tbn = PaletteReducer.TRI_BLUE_NOISE_MULTIPLIERS[(px & 63) | ((y << 6) & 0xFC0)];
er = curErrorRed[px] * tbn;
eg = curErrorGreen[px] * tbn;
eb = curErrorBlue[px] * tbn;
color |= (color >>> 5 & 0x07070700) | 0xFF;
int rr = Math.min(Math.max((int)(((color >>> 24) ) + er + 0.5f), 0), 0xFF);
int gg = Math.min(Math.max((int)(((color >>> 16) & 0xFF) + eg + 0.5f), 0), 0xFF);
int bb = Math.min(Math.max((int)(((color >>> 8) & 0xFF) + eb + 0.5f), 0), 0xFF);
usedEntry[(indexedPixels[i] = paletteIndex =
paletteMapping[((rr << 7) & 0x7C00)
| ((gg << 2) & 0x3E0)
| ((bb >>> 3))]) & 255] = true;
used = paletteArray[paletteIndex & 0xFF];
rdiff = OtherMath.cbrtShape(0x2.Ep-8f * ((color>>>24)- (used>>>24)) );
gdiff = OtherMath.cbrtShape(0x2.Ep-8f * ((color>>>16&255)-(used>>>16&255)));
bdiff = OtherMath.cbrtShape(0x2.Ep-8f * ((color>>>8&255)- (used>>>8&255)) );
if(px < w - 1)
{
curErrorRed[px+1] += rdiff * w7;
curErrorGreen[px+1] += gdiff * w7;
curErrorBlue[px+1] += bdiff * w7;
}
if(ny < height)
{
if(px > 0)
{
nextErrorRed[px-1] += rdiff * w3;
nextErrorGreen[px-1] += gdiff * w3;
nextErrorBlue[px-1] += bdiff * w3;
}
if(px < w - 1)
{
nextErrorRed[px+1] += rdiff * w1;
nextErrorGreen[px+1] += gdiff * w1;
nextErrorBlue[px+1] += bdiff * w1;
}
nextErrorRed[px] += rdiff * w5;
nextErrorGreen[px] += gdiff * w5;
nextErrorBlue[px] += bdiff * w5;
}
i++;
}
}
}
}
break;
}
colorDepth = 8;
palSize = 7;
// get closest match to transparent color if specified
if (hasTransparent) {
transIndex = 0;
}
}
/**
* Extracts image pixels into byte array "pixels"
*/
protected void getImagePixels() {
int w = image.getWidth();
int h = image.getHeight();
if ((w != width) || (h != height)) {
// create new image with right size/format
Pixmap temp = new Pixmap(width, height, Pixmap.Format.RGBA8888);
temp.drawPixmap(image, 0, 0);
image = temp;
}
}
/**
* Writes Graphic Control Extension
*/
protected void writeGraphicCtrlExt() throws IOException {
out.write(0x21); // extension introducer
out.write(0xf9); // GCE label
out.write(4); // data block size
int transp, disp;
if (transIndex == -1) {
transp = 0;
disp = 0; // dispose = no action
} else {
transp = 1;
disp = 2; // force clear if using transparent color
}
if (dispose >= 0) {
disp = dispose & 7; // user override
}
disp <<= 2;
// packed fields
out.write(0 | // 1:3 reserved
disp | // 4:6 disposal
0 | // 7 user input - 0 = none
transp); // 8 transparency flag
writeShort(Math.round(delay/10f)); // delay x 1/100 sec
out.write(transIndex); // transparent color index
out.write(0); // block terminator
}
/**
* Writes Image Descriptor
*/
protected void writeImageDesc() throws IOException {
out.write(0x2c); // image separator
writeShort(x); // image position x,y = 0,0
writeShort(y);
writeShort(width); // image size
writeShort(height);
// packed fields
if (firstFrame) {
// no LCT - GCT is used for first (or only) frame
out.write(0);
} else {
// specify normal LCT
out.write(0x80 | // 1 local color table 1=yes
0 | // 2 interlace - 0=no
0 | // 3 sorted - 0=no
0 | // 4-5 reserved
palSize); // 6-8 size of color table
}
}
/**
* Writes Logical Screen Descriptor
*/
protected void writeLSD() throws IOException {
// logical screen size
writeShort(width);
writeShort(height);
// packed fields
out.write((0x80 | // 1 : global color table flag = 1 (gct used)
0x70 | // 2-4 : color resolution = 7
0x00 | // 5 : gct sort flag = 0
palSize)); // 6-8 : gct size
out.write(0); // background color index
out.write(0); // pixel aspect ratio - assume 1:1
}
/**
* Writes Netscape application extension to define repeat count.
*/
protected void writeNetscapeExt() throws IOException {
out.write(0x21); // extension introducer
out.write(0xff); // app extension label
out.write(11); // block size
writeString("NETSCAPE" + "2.0"); // app id + auth code
out.write(3); // sub-block size
out.write(1); // loop sub-block id
writeShort(repeat); // loop count (extra iterations, 0=repeat forever)
out.write(0); // block terminator
}
/**
* Writes color table
*/
protected void writePalette() throws IOException {
out.write(colorTab, 0, colorTab.length);
int n = (3 * 256) - colorTab.length;
for (int i = 0; i < n; i++) {
out.write(0);
}
}
/**
* Encodes and writes pixel data
*/
protected void writePixels() throws IOException {
LZWEncoder encoder = new LZWEncoder(width, height, indexedPixels, colorDepth);
encoder.encode(out);
}
/**
* Write 16-bit value to output stream, LSB first
*/
protected void writeShort(int value) throws IOException {
out.write(value & 0xff);
out.write((value >>> 8) & 0xff);
}
/**
* Writes string to output stream
*/
protected void writeString(String s) throws IOException {
for (int i = 0; i < s.length(); i++) {
out.write((byte) s.charAt(i));
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy