javajs.img.GifEncoder Maven / Gradle / Ivy
Show all versions of jmol Show documentation
/* $RCSfile$
* $Author: hansonr $
* $Date: 2007-06-02 12:14:13 -0500 (Sat, 02 Jun 2007) $
* $Revision: 7831 $
*
* Copyright (C) 2000-2005 The Jmol Development Team
*
* Contact: [email protected]
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*/
// Final encoding code from http://acme.com/resources/classes/Acme/JPM/Encoders/GifEncoder.java
//
// GifEncoder - write out an image as a GIF
//
//
// Transparency handling and variable bit size courtesy of Jack Palevich.
//
// Copyright (C)1996,1998 by Jef Poskanzer . All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
// 1. Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE.
//
// Visit the ACME Labs Java page for up-to-date versions of this and other
// fine Java utilities: http://www.acme.com/java/
//
/// Write out an image as a GIF.
//
// Fetch the software.
// Fetch the entire Acme package.
//
// @see ToGif
package javajs.img;
import javajs.util.CU;
import javajs.util.Lst;
import javajs.util.M3;
import javajs.util.P3;
import java.util.Hashtable;
import java.util.Map;
import java.io.IOException;
/**
*
* GifEncoder extensively adapted for Jmol by Bob Hanson
*
* Color quantization roughly follows the GIMP method
* "dither Floyd-Steinberg (normal)" but with some twists. (For example, we
* exclude the background color.)
*
* Note that although GIMP code annotation refers to "median-cut", it is really
* using MEAN-cut. That is what I use here as well.
*
* -- commented code allows visualization of the color space using Jmol. Very
* enlightening!
*
* -- much simplified interface with ImageEncoder
*
* -- uses simple Hashtable with Integer() to catalog colors
*
* -- allows progressive production of animated GIF via Jmol CAPTURE command
*
* -- uses general purpose javajs.util.OutputChannel for byte-handling options
* such as posting to a server, writing to disk, and retrieving bytes.
*
* -- allows JavaScript port
*
* -- Bob Hanson, first try: 24 Sep 2013; final coding: 9 Nov 2014
*
*
* @author Bob Hanson [email protected]
*/
public class GifEncoder extends ImageEncoder {
private Map params;
private P3[] palette;
private int backgroundColor;
private boolean interlaced;
private boolean addHeader = true;
private boolean addImage = true;
private boolean addTrailer = true;
private boolean isTransparent;
private boolean floydSteinberg = true;
private boolean capturing;
private boolean looping;
private int delayTime100ths = -1;
private int bitsPerPixel = 1;
private int byteCount;
/**
* we allow for animated GIF by being able to re-enter the code with different
* parameters held in params
*
*
*/
@Override
protected void setParams(Map params) {
this.params = params;
Integer ic = (Integer) params.get("transparentColor");
if (ic == null) {
ic = (Integer) params.get("backgroundColor");
if (ic != null)
backgroundColor = ic.intValue();
} else {
backgroundColor = ic.intValue();
isTransparent = true;
}
interlaced = (Boolean.TRUE == params.get("interlaced"));
if (params.containsKey("captureRootExt") // file0000.gif
|| !params.containsKey("captureMode")) // animated gif
return;
interlaced = false;
capturing = true;
try {
byteCount = ((Integer) params.get("captureByteCount")).intValue();
} catch (Exception e) {
// ignore
}
switch ("maec"
.indexOf(((String) params.get("captureMode")).substring(0, 1))) {
case 0: //"movie"
params.put("captureMode", "add");
addImage = false;
addTrailer = false;
break;
case 1: // add
addHeader = false;
addTrailer = false;
int fps = Math.abs(((Integer) params.get("captureFps")).intValue());
delayTime100ths = (fps == 0 ? 0 : 100 / fps);
looping = (Boolean.FALSE != params.get("captureLooping"));
break;
case 2: // end
addHeader = false;
addImage = false;
break;
case 3: // cancel
addHeader = false;
addImage = false;
out.cancel();
break;
}
}
@Override
protected void generate() throws IOException {
if (addHeader)
writeHeader();
addHeader = false; // only one header
if (addImage) {
createPalette();
writeGraphicControlExtension();
if (delayTime100ths >= 0 && looping)
writeNetscapeLoopExtension();
writeImage();
}
}
@Override
protected void close() {
if (addTrailer) {
writeTrailer();
} else {
doClose = false;
}
if (capturing)
params.put("captureByteCount", Integer.valueOf(byteCount));
}
////////////// 256-color quantization //////////////
/**
* a color point in normalized L*a*b space with a flag indicating whether it
* is the background color
*/
private class ColorItem extends P3 {
protected boolean isBackground;
ColorItem(int rgb, boolean isBackground) {
this.isBackground = isBackground;
setT(toLABnorm(rgb));
}
}
/**
* A list of normalized L*a*b points with an index and a center and volume
*
*/
private class ColorCell extends Lst {
protected int index;
protected P3 center;
private float volume;
ColorCell(int index) {
this.index = index;
}
/**
* @param doVisualize
* debugging only
* @return volume in normalized L*a*b space
*/
public float getVolume(boolean doVisualize) {
if (volume != 0)
return volume;
if (size() < 2)
return -1;
//if (true)
//return lst.size();
//float d;
float maxx = -Integer.MAX_VALUE;
float minx = Integer.MAX_VALUE;
float maxy = -Integer.MAX_VALUE;
float miny = Integer.MAX_VALUE;
float maxz = -Integer.MAX_VALUE;
float minz = Integer.MAX_VALUE;
int n = size();
for (int i = n; --i >= 0;) {
P3 xyz = get(i);
if (xyz.x < minx)
minx = xyz.x;
if (xyz.y < miny)
miny = xyz.y;
if (xyz.z < minz)
minz = xyz.z;
if (xyz.x > maxx)
maxx = xyz.x;
if (xyz.y > maxy)
maxy = xyz.y;
if (xyz.z > maxz)
maxz = xyz.z;
}
float dx = (maxx - minx);
float dy = (maxy - miny);
float dz = (maxz - minz);
// Jmol visualization only
// if (doVisualize) {
// P3 ptRGB = toRGB(center);
// drawPt(index, -size(), ptRGB);
// //for (int i = n; --i >= 0;)
// //drawPt(index, i, toRGB(get(i)));
// P3 pt0 = toRGB(P3.new3(Math.max(minx, 0), Math.max(miny, 0),
// Math.max(minz, 0)));
// P3 pt1 = toRGB(P3.new3(Math.min(maxx, 100), Math.min(maxy, 100),
// Math.min(maxz, 100)));
// rgbToXyz(pt0, pt0);
// xyzToLab(pt0, pt0);
// rgbToXyz(pt1, pt1);
// xyzToLab(pt1, pt1);
// System.out.println("boundbox corners " + pt0 + " " + pt1);
// System.out.println("draw d" + index + " boundbox color " + ptRGB
// + " mesh nofill");
// }
return volume = dx * dx + dy * dy + dz * dz;
}
// // Jmol visualization only
// private void drawPt(int index, int i, P3 rgb) {
// boolean isMain = (i < 0);
// P3 lab = rgbToXyz(rgb, null);
// xyzToLab(lab, lab);
// System.out.println("draw d" + index + (isMain ? "_" : "_" + i) + " width "
// + (isMain ? 1.0 : 0.2) + " " + lab
// + " color " + rgb + (isMain ? " '" + -i + "'" : ""));
// }
/**
* Set the average normalized L*a*b value for this cell and return its RGB point
*
* @return RGB point
*
*/
protected P3 setColor() {
int count = size();
center = new P3();
for (int i = count; --i >= 0;)
center.add(get(i));
center.scale(1f / count);
// Jmol visualization only
//volume = 0;
//getVolume(true);
return toRGB(center);
}
/**
* use median_cut algorithm to split the cell, creating a doubly linked
* list.
*
* Paul Heckbert, MIT thesis COLOR IMAGE QUANTIZATION FOR FRAME BUFFER
* DISPLAY https://www.cs.cmu.edu/~ph/ciq_thesis
*
* except, as in GIMP, we use center (not median) here.
*
* @param cells
* @return true if split
*/
protected boolean splitCell(Lst cells) {
int n = size();
if (n < 2)
return false;
int newIndex = cells.size();
ColorCell newCell = new ColorCell(newIndex);
cells.addLast(newCell);
float[][] ranges = new float[3][3];
for (int ic = 0; ic < 3; ic++) {
float low = Float.MAX_VALUE;
float high = -Float.MAX_VALUE;
for (int i = n; --i >= 0;) {
P3 lab = get(i);
float v = (ic == 0 ? lab.x : ic == 1 ? lab.y : lab.z);
if (low > v)
low = v;
if (high < v)
high = v;
}
ranges[0][ic] = low;
ranges[1][ic] = high;
ranges[2][ic] = high - low;
}
float[] r = ranges[2];
int mode = (r[0] >= r[1] ? (r[0] >= r[2] ? 0 : 2) : r[1] >= r[2] ? 1 : 2);
float val = ranges[0][mode] + ranges[2][mode] / 2;
volume = 0; // recalculate volume if needed
switch (mode) {
case 0:
for (int i = n; --i >= 0;)
if (get(i).x >= val)
newCell.addLast(removeItemAt(i));
break;
case 1:
for (int i = n; --i >= 0;)
if (get(i).y >= val)
newCell.addLast(removeItemAt(i));
break;
case 2:
for (int i = size(); --i >= 0;)
if (get(i).z >= val)
newCell.addLast(removeItemAt(i));
break;
}
return true;
}
}
/**
* Quantize all colors and create the final palette;
* replace pixels[] with an array of color indices.
*
*/
private void createPalette() {
// catalog all pixel colors
Lst tempColors = new Lst();
Map ciHash = new Hashtable();
for (int i = 0, n = pixels.length; i < n; i++) {
int rgb = pixels[i];
Integer key = Integer.valueOf(rgb);
ColorItem item = ciHash.get(key);
if (item == null) {
item = new ColorItem(rgb, rgb == backgroundColor);
ciHash.put(key, item);
tempColors.addLast(item);
}
}
int nColors = tempColors.size();
System.out.println("GIF total image colors: " + nColors);
ciHash = null;
// create a set of <= 256 color cells
Lst cells = quantizeColors(tempColors);
nColors = cells.size();
System.out.println("GIF final color count: " + nColors);
// generate the palette and map each cell's rgb color to itself
Map colorMap = new Hashtable();
bitsPerPixel = (nColors <= 2 ? 1 : nColors <= 4 ? 2 : nColors <= 16 ? 4 : 8);
palette = new P3[1 << bitsPerPixel];
for (int i = 0; i < nColors; i++) {
ColorCell c = cells.get(i);
colorMap.put(
Integer.valueOf(CU.colorPtToFFRGB(palette[i] = c.setColor())), c);
}
// index all pixels to a pallete color
pixels = indexPixels(cells, colorMap);
}
/**
* Quantize colors by generating a set of cells in normalized L*a*b space
* containing all colors. Start with just two cells -- fixed background color
* and all others. Keep splitting cells while there are fewer than 256 and
* some with multiple colors in them.
*
* It is possible that we will end up with fewer than 256 colors.
*
* @param tempColors
* @return final list of colors
*/
private Lst quantizeColors(Lst tempColors) {
int n = tempColors.size();
Lst cells = new Lst();
ColorCell cc = new ColorCell(0);
cc.addLast(new ColorItem(backgroundColor, true));
cells.addLast(cc);
cc = new ColorCell(1);
if (n > 256)
cells.addLast(cc);
for (int i = 0; i < n; i++) {
ColorItem c = tempColors.get(i);
if (c.isBackground)
continue;
cc.addLast(c);
if (n <= 256) {
cells.addLast(cc);
cc = new ColorCell(cells.size());
}
}
tempColors.clear();
if (n > 256)
while ((n = cells.size()) < 256) {
float maxVol = 0;
ColorCell maxCell = null;
for (int i = n; --i >= 1;) {
ColorCell c = cells.get(i);
float v = c.getVolume(false);
if (v > maxVol) {
maxVol = v;
maxCell = c;
}
}
if (maxCell == null || !maxCell.splitCell(cells))
break;
}
return cells;
}
/**
*
* Assign all colors to their closest approximation and return an array of
* color indexes.
*
* Uses Floyd-Steinberg dithering, finding the closest known color and then
* spreading out the error over four leading pixels. Limits error to +/- 75
* percent in normalized L*a*b space.
*
* @param cells
* quantized color cells
* @param colorMap
* map of quantized rgb to its cell
* @return array of color indexes, one for each pixel
*
*/
private int[] indexPixels(Lst cells,
Map colorMap) {
// We need a strip only width+2 wide to process all the errors.
// Errors are added to the next pixel and the next row's pixels
// only through p + width + 1:
// p +1
// +w-1 +w +w+1
// so including p as well, we need a total of width + 2 errors.
//
// as p moves through the pixels, we just use mod to cycle through
// this strip.
//
int w2 = width + 2;
P3[] errors = new P3[w2];
// We should replace, not overwrite, pixels
// as this may be the raw canvas.buf32.
int[] newPixels = new int[pixels.length];
P3 err = new P3();
P3 lab;
int rgb;
Map nearestCell = new Hashtable();
for (int i = 0, p = 0; i < height; ++i) {
boolean notLastRow = (i != height - 1);
for (int j = 0; j < width; ++j, p++) {
if (pixels[p] == backgroundColor) {
// leave as 0
continue;
}
P3 pe = errors[p % w2];
if (pe == null || pe.x == Float.MAX_VALUE) {
lab = null;
rgb = pixels[p];
} else {
lab = toLABnorm(pixels[p]);
err = pe;
// important not to round the clamp here -- full floating precision
err.x = clamp(err.x, -75, 75);
err.y = clamp(err.y, -75, 75);
err.z = clamp(err.z, -75, 75);
lab.add(err);
rgb = CU.colorPtToFFRGB(toRGB(lab));
}
Integer key = Integer.valueOf(rgb);
ColorCell cell = colorMap.get(key);
if (cell == null) {
// critical to generate normalized L*a*b from RGB here for nearestCell mapping.
// otherwise future RGB keys may match the wrong cell
lab = toLABnorm(rgb);
cell = nearestCell.get(key);
if (cell == null) {
// find nearest cell
float maxerr = Float.MAX_VALUE;
// skip 0 0 0
for (int ib = cells.size(); --ib >= 1;) {
ColorCell c = cells.get(ib);
err.sub2(lab, c.center);
float d = err.lengthSquared();
if (d < maxerr) {
maxerr = d;
cell = c;
}
}
nearestCell.put(key, cell);
}
if (floydSteinberg) {
// dither
err.sub2(lab, cell.center);
boolean notLastCol = (j < width - 1);
if (notLastCol)
addError(err, 7, errors, p + 1, w2);
if (notLastRow) {
if (j > 0)
addError(err, 3, errors, p + width - 1, w2);
addError(err, 5, errors, p + width, w2);
if (notLastCol)
addError(err, 1, errors, p + width + 1, w2);
}
}
err.x = Float.MAX_VALUE; // used; flag for resetting to 0
}
newPixels[p] = cell.index;
}
}
return newPixels;
}
private void addError(P3 err, int f, P3[] errors, int p, int w2) {
// GIMP will allow changing the background color.
if (pixels[p] == backgroundColor)
return;
p %= w2;
P3 errp = errors[p];
if (errp == null)
errp = errors[p] = new P3();
else if (errp.x == Float.MAX_VALUE) // reuse
errp.set(0, 0, 0);
errp.scaleAdd2(f / 16f, err, errp);
}
///////////////////////// CIE L*a*b / XYZ / sRGB conversion methods /////////
// these could be static, but that just makes for more JavaScript code
protected P3 toLABnorm(int rgb) {
P3 lab = CU.colorPtFromInt(rgb, null);
rgbToXyz(lab, lab);
xyzToLab(lab, lab);
// normalize to 0-100
lab.y = (lab.y + 86.185f) / (98.254f + 86.185f) * 100f;
lab.z = (lab.z + 107.863f) / (94.482f + 107.863f) * 100f;
return lab;
}
protected P3 toRGB(P3 lab) {
P3 xyz = P3.newP(lab);
// normalized to 0-100
xyz.y = xyz.y / 100f * (98.254f + 86.185f) - 86.185f;
xyz.z = xyz.z / 100f * (94.482f + 107.863f) - 107.863f;
labToXyz(xyz, xyz);
return xyzToRgb(xyz, xyz);
}
private static M3 xyz2rgb;
private static M3 rgb2xyz;
static {
rgb2xyz = M3.newA9(new float[] { 0.4124f, 0.3576f, 0.1805f, 0.2126f,
0.7152f, 0.0722f, 0.0193f, 0.1192f, 0.9505f });
xyz2rgb = M3.newA9(new float[] { 3.2406f, -1.5372f, -0.4986f, -0.9689f,
1.8758f, 0.0415f, 0.0557f, -0.2040f, 1.0570f });
}
public P3 rgbToXyz(P3 rgb, P3 xyz) {
// http://en.wikipedia.org/wiki/CIE_1931_color_space
// http://rsb.info.nih.gov/ij/plugins/download/Color_Space_Converter.java
if (xyz == null)
xyz = new P3();
xyz.x = sxyz(rgb.x);
xyz.y = sxyz(rgb.y);
xyz.z = sxyz(rgb.z);
rgb2xyz.rotate(xyz);
return xyz;
}
private float sxyz(float x) {
x /= 255;
return (float) (x <= 0.04045 ? x / 12.92 : Math.pow(((x + 0.055) / 1.055),
2.4)) * 100;
}
public P3 xyzToRgb(P3 xyz, P3 rgb) {
// http://en.wikipedia.org/wiki/CIE_1931_color_space
// http://rsb.info.nih.gov/ij/plugins/download/Color_Space_Converter.java
if (rgb == null)
rgb = new P3();
rgb.setT(xyz);
rgb.scale(0.01f);
xyz2rgb.rotate(rgb);
rgb.x = clamp(srgb(rgb.x), 0, 255);
rgb.y = clamp(srgb(rgb.y), 0, 255);
rgb.z = clamp(srgb(rgb.z), 0, 255);
return rgb;
}
private float srgb(float x) {
return (float) (x > 0.0031308f ? (1.055 * Math.pow(x, 1.0 / 2.4)) - 0.055
: x * 12.92) * 255;
}
public P3 xyzToLab(P3 xyz, P3 lab) {
// http://en.wikipedia.org/wiki/Lab_color_space
// http://rsb.info.nih.gov/ij/plugins/download/Color_Space_Converter.java
// Lab([0..100], [-86.185..98.254], [-107.863..94.482])
// XYZn = D65 = {95.0429, 100.0, 108.8900};
if (lab == null)
lab = new P3();
float x = flab(xyz.x / 95.0429f);
float y = flab(xyz.y / 100);
float z = flab(xyz.z / 108.89f);
lab.x = (116 * y) - 16;
lab.y = 500 * (x - y);
lab.z = 200 * (y - z);
return lab;
}
private float flab(float t) {
return (float) (t > 8.85645168E-3 /* (24/116)^3 */? Math.pow(t,
0.333333333) : 7.78703704 /* 1/3*116/24*116/24 */* t + 0.137931034 /* 16/116 */
);
}
public P3 labToXyz(P3 lab, P3 xyz) {
// http://en.wikipedia.org/wiki/Lab_color_space
// http://rsb.info.nih.gov/ij/plugins/download/Color_Space_Converter.java
// XYZn = D65 = {95.0429, 100.0, 108.8900};
if (xyz == null)
xyz = new P3();
xyz.setT(lab);
float y = (xyz.x + 16) / 116;
float x = xyz.y / 500 + y;
float z = y - xyz.z / 200;
xyz.x = fxyz(x) * 95.0429f;
xyz.y = fxyz(y) * 100;
xyz.z = fxyz(z) * 108.89f;
return xyz;
}
private float fxyz(float t) {
return (float) (t > 0.206896552 /* (24/116) */? t * t * t
: 0.128418549 /* 3*24/116*24/116 */* (t - 0.137931034 /* 16/116 */));
}
private float clamp(float c, float min, float max) {
c = (c < min ? min : c > max ? max : c);
return (min == 0 ? Math.round(c) : c);
}
///////////////////////// GifEncoder writing methods ////////////////////////
/**
* includes logical screen descriptor
*
* @throws IOException
*/
private void writeHeader() throws IOException {
putString("GIF89a");
putWord(width);
putWord(height);
putByte(0); // no global color table -- using local instead
putByte(0); // no background
putByte(0); // no pixel aspect ratio given
}
private void writeGraphicControlExtension() {
if (isTransparent || delayTime100ths >= 0) {
putByte(0x21); // graphic control extension
putByte(0xf9); // graphic control label
putByte(4); // block size
putByte((isTransparent ? 9 : 0) | (delayTime100ths > 0 ? 2 : 0)); // packed bytes
putWord(delayTime100ths > 0 ? delayTime100ths : 0);
putByte(0); // transparent index
putByte(0); // end-of-block
}
}
// see http://www.vurdalakov.net/misc/gif/netscape-looping-application-extension
// +---------------+
// 0 | 0x21 | Extension Label
// +---------------+
// 1 | 0xFF | Application Extension Label
// +---------------+
// 2 | 0x0B | Block Size
// +---------------+
// 3 | |
// +- -+
// 4 | |
// +- -+
// 5 | |
// +- -+
// 6 | |
// +- NETSCAPE -+ Application Identifier (8 bytes)
// 7 | |
// +- -+
// 8 | |
// +- -+
// 9 | |
// +- -+
// 10 | |
// +---------------+
// 11 | |
// +- -+
// 12 | 2.0 | Application Authentication Code (3 bytes)
// +- -+
// 13 | |
// +===============+ --+
// 14 | 0x03 | Sub-block Data Size |
// +---------------+ |
// 15 | 0x01 | Sub-block ID |
// +---------------+ | Application Data Sub-block
// 16 | | |
// +- -+ Loop Count (2 bytes) |
// 17 | | |
// +===============+ --+
// 18 | 0x00 | Block Terminator
// +---------------+
private void writeNetscapeLoopExtension() {
putByte(0x21); // graphic control extension
putByte(0xff); // netscape loop extension
putByte(0x0B); // block size
putString("NETSCAPE2.0");
putByte(3);
putByte(1);
putWord(0); // loop indefinitely
putByte(0); // end-of-block
}
private int initCodeSize;
private int curpt;
private void writeImage() {
putByte(0x2C);
putWord(0); //left
putWord(0); //top
putWord(width);
putWord(height);
// = LISx xZZZ
// L Local Color Table Flag
// I Interlace Flag
// S Sort Flag
// x Reserved
// ZZZ Size of Local Color Table
int packedFields = 0x80 | (interlaced ? 0x40 : 0) | (bitsPerPixel - 1);
putByte(packedFields);
int colorMapSize = 1 << bitsPerPixel;
P3 p = new P3();
for (int i = 0; i < colorMapSize; i++) {
if (palette[i] != null)
p = palette[i];
putByte((int) p.x);
putByte((int) p.y);
putByte((int) p.z);
}
putByte(initCodeSize = (bitsPerPixel <= 1 ? 2 : bitsPerPixel));
compress();
putByte(0);
}
private void writeTrailer() {
// Write the GIF file terminator
putByte(0x3B);
}
///// compression routines /////
private static final int EOF = -1;
// Return the next pixel from the image
private int nextPixel() {
if (countDown-- == 0)
return EOF;
int colorIndex = pixels[curpt];
// Bump the current X position
++curx;
if (curx == width) {
// If we are at the end of a scan line, set curx back to the beginning
// If we are interlaced, bump the cury to the appropriate spot,
// otherwise, just increment it.
curx = 0;
if (interlaced)
updateY(INTERLACE_PARAMS[pass], INTERLACE_PARAMS[pass + 4]);
else
++cury;
}
curpt = cury * width + curx;
return colorIndex & 0xff;
}
private static final int[] INTERLACE_PARAMS = { 8, 8, 4, 2, 4, 2, 1, 0 };
/**
*
* Group 1 : Every 8th. row, starting with row 0. (Pass 1)
*
* Group 2 : Every 8th. row, starting with row 4. (Pass 2)
*
* Group 3 : Every 4th. row, starting with row 2. (Pass 3)
*
* Group 4 : Every 2nd. row, starting with row 1. (Pass 4)
*
* @param yNext
* @param yNew
*/
private void updateY(int yNext, int yNew) {
cury += yNext;
if (yNew >= 0 && cury >= height) {
cury = yNew;
++pass;
}
}
// Write out a word to the GIF file
private void putWord(int w) {
putByte(w);
putByte(w >> 8);
}
// GIFCOMPR.C - GIF Image compression routines
//
// Lempel-Ziv compression based on 'compress'. GIF modifications by
// David Rowley ([email protected])
// General DEFINEs
private static final int BITS = 12;
private static final int HSIZE = 5003; // 80% occupancy
// GIF Image compression - modified 'compress'
//
// Based on: compress.c - File compression ala IEEE Computer, June 1984.
//
// By Authors: Spencer W. Thomas (decvax!harpo!utah-cs!utah-gr!thomas)
// Jim McKie (decvax!mcvax!jim)
// Steve Davies (decvax!vax135!petsd!peora!srd)
// Ken Turkowski (decvax!decwrl!turtlevax!ken)
// James A. Woods (decvax!ihnp4!ames!jaw)
// Joe Orost (decvax!vax135!petsd!joe)
private int nBits; // number of bits/code
private int maxbits = BITS; // user settable max # bits/code
private int maxcode; // maximum code, given n_bits
private int maxmaxcode = 1 << BITS; // should NEVER generate this code
private final static int MAXCODE(int nBits) {
return (1 << nBits) - 1;
}
private int[] htab = new int[HSIZE];
private int[] codetab = new int[HSIZE];
private int hsize = HSIZE; // for dynamic table sizing
private int freeEnt = 0; // first unused entry
// block compression parameters -- after all codes are used up,
// and compression rate changes, start over.
private boolean clearFlag = false;
// Algorithm: use open addressing double hashing (no chaining) on the
// prefix code / next character combination. We do a variant of Knuth's
// algorithm D (vol. 3, sec. 6.4) along with G. Knott's relatively-prime
// secondary probe. Here, the modular division first probe is gives way
// to a faster exclusive-or manipulation. Also do block compression with
// an adaptive reset, whereby the code table is cleared when the compression
// ratio decreases, but after the table fills. The variable-length output
// codes are re-sized at this point, and a special CLEAR code is generated
// for the decompressor. Late addition: construct the table according to
// file size for noticeable speed improvement on small files. Please direct
// questions about this implementation to ames!jaw.
private int clearCode;
private int EOFCode;
private int countDown;
private int pass = 0;
private int curx, cury;
private void compress() {
// Calculate number of bits we are expecting
countDown = width * height;
// Indicate which pass we are on (if interlace)
pass = 0;
// Set up the current x and y position
curx = 0;
cury = 0;
// Set up the necessary values
clearFlag = false;
nBits = initCodeSize + 1;
maxcode = MAXCODE(nBits);
clearCode = 1 << initCodeSize;
EOFCode = clearCode + 1;
freeEnt = clearCode + 2;
// Set up the 'byte output' routine
bufPt = 0;
int ent = nextPixel();
int hshift = 0;
int fcode;
for (fcode = hsize; fcode < 65536; fcode *= 2)
++hshift;
hshift = 8 - hshift; // set hash code range bound
int hsizeReg = hsize;
clearHash(hsizeReg); // clear hash table
output(clearCode);
int c;
outer_loop: while ((c = nextPixel()) != EOF) {
fcode = (c << maxbits) + ent;
int i = (c << hshift) ^ ent; // xor hashing
if (htab[i] == fcode) {
ent = codetab[i];
continue;
} else if (htab[i] >= 0) // non-empty slot
{
int disp = hsizeReg - i; // secondary hash (after G. Knott)
if (i == 0)
disp = 1;
do {
if ((i -= disp) < 0)
i += hsizeReg;
if (htab[i] == fcode) {
ent = codetab[i];
continue outer_loop;
}
} while (htab[i] >= 0);
}
output(ent);
ent = c;
if (freeEnt < maxmaxcode) {
codetab[i] = freeEnt++; // code -> hashtable
htab[i] = fcode;
} else {
clearBlock();
}
}
// Put out the final code.
output(ent);
output(EOFCode);
}
// output
//
// Output the given code.
// Inputs:
// code: A n_bits-bit integer. If == -1, then EOF. This assumes
// that n_bits =< wordsize - 1.
// Outputs:
// Outputs code to the file.
// Assumptions:
// Chars are 8 bits long.
// Algorithm:
// Maintain a BITS character long buffer (so that 8 codes will
// fit in it exactly). Use the VAX insv instruction to insert each
// code in turn. When the buffer fills up empty it and start over.
private int curAccum = 0;
private int curBits = 0;
private int masks[] = { 0x0000, 0x0001, 0x0003, 0x0007, 0x000F, 0x001F,
0x003F, 0x007F, 0x00FF, 0x01FF, 0x03FF, 0x07FF, 0x0FFF, 0x1FFF, 0x3FFF,
0x7FFF, 0xFFFF };
private void output(int code) {
curAccum &= masks[curBits];
if (curBits > 0)
curAccum |= (code << curBits);
else
curAccum = code;
curBits += nBits;
while (curBits >= 8) {
byteOut((byte) (curAccum & 0xff));
curAccum >>= 8;
curBits -= 8;
}
// If the next entry is going to be too big for the code size,
// then increase it, if possible.
if (freeEnt > maxcode || clearFlag) {
if (clearFlag) {
maxcode = MAXCODE(nBits = initCodeSize + 1);
clearFlag = false;
} else {
++nBits;
if (nBits == maxbits)
maxcode = maxmaxcode;
else
maxcode = MAXCODE(nBits);
}
}
if (code == EOFCode) {
// At EOF, write the rest of the buffer.
while (curBits > 0) {
byteOut((byte) (curAccum & 0xff));
curAccum >>= 8;
curBits -= 8;
}
flushBytes();
}
}
// Clear out the hash table
// table clear for block compress
private void clearBlock() {
clearHash(hsize);
freeEnt = clearCode + 2;
clearFlag = true;
output(clearCode);
}
// reset code table
private void clearHash(int hsize) {
for (int i = 0; i < hsize; ++i)
htab[i] = -1;
}
// GIF-specific routines (byte array buffer)
// Number of bytes so far in this 'packet'
private int bufPt;
// Define the storage for the packet accumulator
final private byte[] buf = new byte[256];
// Add a byte to the end of the current packet, and if it is 254
// byte, flush the packet to disk.
private void byteOut(byte c) {
buf[bufPt++] = c;
if (bufPt >= 254)
flushBytes();
}
// Flush the packet to disk, and reset the accumulator
protected void flushBytes() {
if (bufPt > 0) {
putByte(bufPt);
out.write(buf, 0, bufPt);
byteCount += bufPt;
bufPt = 0;
}
}
}