/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2008, Open Source Geospatial Foundation (OSGeo)
*
* 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;
* version 2.1 of the License.
*
* 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.
*/
package org.geotools.image.palette;
import java.awt.Transparency;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.IndexColorModel;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.awt.image.WritableRaster;
import javax.imageio.ImageTypeSpecifier;
/**
* This class implements the octree quantization method as it is described in
* the "Graphics Gems" (ISBN 0-12-286166-3, Chapter 4, pages 297-293)
*
* @author Simone Giannecchini - GeoSolutions
*/
public final class CustomPaletteBuilder {
/**
* Default value for the threshold to decide whether a pixel is opaque (>=)
* or transparent (<). Default is 1 to try to preserve antialising
*/
public static final int DEFAULT_ALPHA_TH = 1;
/**
* maximum of tree depth
*/
protected int maxLevel;
protected RenderedImage src;
protected ColorModel srcColorModel;
protected int requiredSize;
protected ColorNode root;
protected int numNodes;
protected int maxNodes;
protected int currLevel;
protected int currSize;
protected ColorNode[] reduceList;
protected ColorNode[] palette;
protected int transparency;
protected ColorNode transColor;
protected int subsampleX;
protected int subsampley;
protected int numBands;
protected int alphaThreshold;
/**
* Returns <code>true</code> if PaletteBuilder is able to create palette
* for given image type.
*
* @param type
* an instance of <code>ImageTypeSpecifier</code> to be
* indexed.
*
* @return <code>true</code> if the <code>PaletteBuilder</code> is
* likely to be able to create palette for this image type.
*
* @exception IllegalArgumentException
* if <code>type</code> is <code>null</code>.
*/
public static boolean canCreatePalette(ImageTypeSpecifier type) {
if (type == null) {
throw new IllegalArgumentException("type == null");
}
return true;
}
/**
* Returns <code>true</code> if PaletteBuilder is able to create palette
* for given rendered image.
*
* @param image
* an instance of <code>RenderedImage</code> to be indexed.
*
* @return <code>true</code> if the <code>PaletteBuilder</code> is
* likely to be able to create palette for this image type.
*
* @exception IllegalArgumentException
* if <code>image</code> is <code>null</code>.
*/
public static boolean canCreatePalette(RenderedImage image) {
if (image == null) {
throw new IllegalArgumentException("image == null");
}
ImageTypeSpecifier type = new ImageTypeSpecifier(image);
return canCreatePalette(type);
}
public RenderedImage getIndexedImage() {
// //
//
// Create the destination image
//
// //
final IndexColorModel icm = getIndexColorModel();
final WritableRaster destWr = icm.createCompatibleWritableRaster(src
.getWidth(), src.getHeight());
final BufferedImage dst = new BufferedImage(icm, destWr, false, null);
// //
//
// Filter the image out
//
// //
// //
//
// Collecting info about the source image
//
// //
final int numBands = src.getSampleModel().getNumBands();
final int rgba[] = new int[numBands];
final boolean sourceHasAlpha = (numBands % 2 == 0);
final int alphaBand = sourceHasAlpha ? numBands - 1 : -1;
final int minx_ = src.getMinX();
final int miny_ = src.getMinY();
final int srcW_ = src.getWidth();
final int srcH_ = src.getHeight();
final int maxx_ = minx_ + srcW_;
final int maxy_ = miny_ + srcH_;
final int minTileX = src.getMinTileX();
final int minTileY = src.getMinTileY();
final int tileW = src.getTileWidth();
final int tileH = src.getTileHeight();
final int maxTileX = minTileX + src.getNumXTiles();
final int maxTileY = minTileY + src.getNumYTiles();
int dstTempX = 0;
int dstTempY = 0;
for (int ty = minTileY; ty < maxTileY; ty++) {
dstTempX = 0;
int actualWidth = 0;
int actualHeight = 0;
for (int tx = minTileX; tx < maxTileX; tx++) {
// get the source raster
final Raster r = src.getTile(tx, ty);
int minx = r.getMinX();
int miny = r.getMinY();
minx = minx < minx_ ? minx_ : minx;
miny = miny < miny_ ? miny_ : miny;
int maxx = minx + tileW;
int maxy = miny + tileH;
maxx = maxx > maxx_ ? maxx_ : maxx;
maxy = maxy > maxy_ ? maxy_ : maxy;
actualWidth = maxx - minx;
actualHeight = maxy - miny;
for (int j = miny, jj = dstTempY; j < maxy; j++, jj++) {
for (int i = minx, ii = dstTempX; i < maxx; i++, ii++) {
r.getPixel(i, j, rgba);
destWr.setSample(ii, jj, 0, findColorIndex(root, rgba,
alphaBand));
}
}
dstTempX += actualWidth;
}
dstTempY += actualHeight;
}
return dst;
}
public CustomPaletteBuilder(RenderedImage src) {
this(src, 256, 1, 1, DEFAULT_ALPHA_TH);
}
public CustomPaletteBuilder(RenderedImage src, int size, int subsx,
int subsy, int alpha_th) {
if ((subsx <= 0) || (subsx >= src.getWidth())) {
throw new IllegalArgumentException("Invalid subsample x size");
}
if ((subsy <= 0) || (subsy >= src.getWidth())) {
throw new IllegalArgumentException("Invalid subsample y size");
}
this.alphaThreshold = alpha_th;
this.src = src;
this.srcColorModel = src.getColorModel();
this.numBands = srcColorModel.getNumComponents();
this.subsampleX = subsx;
this.subsampley = subsy;
this.transparency = srcColorModel.getTransparency();
if (transparency != Transparency.OPAQUE) {
transparency = Transparency.BITMASK;
// make room for the transparent color
this.requiredSize = size - 1;
transColor = new ColorNode();
transColor.isLeaf = true;
} else {
this.requiredSize = size;
}
if (this.requiredSize > 256) {
throw new IllegalArgumentException(
"Unvalid number of colors require.");
}
this.maxLevel = (int) Math.ceil(Math.log(requiredSize) / Math.log(2));
}
protected int findColorIndex(ColorNode aNode, int[] rgba, int transpBand) {
if ((transparency != Transparency.OPAQUE)
&& (rgba[transpBand] < alphaThreshold)) {
return 0; // default transparent pixel
}
try {
if (aNode.isLeaf) {
return aNode.paletteIndex;
} else {
int childIndex = getBranchIndex(rgba, aNode.level);
if (aNode.children[childIndex] == null) {
int i = 1;
for (; i < 8; i++) {
if (((childIndex + i) < 8)
&& (aNode.children[childIndex + i] != null)) {
childIndex += i;
break;
}
if (((childIndex - i) >= 0)
&& (aNode.children[childIndex - i] != null)) {
childIndex -= i;
break;
}
}
}
return findColorIndex(aNode.children[childIndex], rgba,
transpBand);
}
} catch (Exception e) {
}
return 0;
}
public CustomPaletteBuilder buildPalette() {
reduceList = new ColorNode[maxLevel + 1];
for (int i = 0; i < reduceList.length; i++) {
reduceList[i] = null;
}
numNodes = 0;
maxNodes = 0;
root = null;
currSize = 0;
currLevel = maxLevel;
// //
//
// Collecting info about the source image
//
// //
final int numBands = src.getSampleModel().getNumBands();
final int rgba[] = new int[numBands];
final boolean discriminantTransparency = transparency != Transparency.OPAQUE;
final int transpBand = numBands - 1;
final int minx_ = src.getMinX();
final int miny_ = src.getMinY();
final int srcW_ = src.getWidth();
final int srcH_ = src.getHeight();
final int maxx_ = minx_ + srcW_;
final int maxy_ = miny_ + srcH_;
final int minTileX = src.getMinTileX();
final int minTileY = src.getMinTileY();
final int tileW = src.getTileWidth();
final int tileH = src.getTileHeight();
final int maxTileX = minTileX + src.getNumXTiles();
final int maxTileY = minTileY + src.getNumYTiles();
for (int ty = minTileY; ty < maxTileY; ty++) {
for (int tx = minTileX; tx < maxTileX; tx++) {
// get the source raster
final Raster r = src.getTile(tx, ty);
int minx = r.getMinX();
int miny = r.getMinY();
minx = minx < minx_ ? minx_ : minx;
miny = miny < miny_ ? miny_ : miny;
int maxx = minx + tileW;
int maxy = miny + tileH;
maxx = maxx > maxx_ ? maxx_ : maxx;
maxy = maxy > maxy_ ? maxy_ : maxy;
for (int j = miny; j < maxy; j++) {
if ((subsampley > 1) && ((j % subsampley) != 0)) {
continue;
}
for (int i = minx; i < maxx; i++) {
if ((subsampleX > 1) && ((i % subsampleX) != 0)) {
continue;
}
r.getPixel(i, j, rgba);
/*
* If transparency of given image is not opaque we
* assume all colors with alpha less than 1.0 as fully
* transparent.
*/
if (discriminantTransparency
&& (rgba[transpBand] < alphaThreshold)) {
transColor = insertNode(transColor, rgba, 0);
} else {
root = insertNode(root, rgba, 0);
}
if (currSize > requiredSize) {
reduceTree();
}
}
}
}
}
return this;
}
protected ColorNode insertNode(ColorNode aNode, int[] rgba, int aLevel) {
if (aNode == null) {
aNode = new ColorNode();
numNodes++;
if (numNodes > maxNodes) {
maxNodes = numNodes;
}
aNode.level = aLevel;
aNode.isLeaf = (aLevel > maxLevel);
if (aNode.isLeaf) {
currSize++;
}
}
aNode.colorCount++;
aNode.red += rgba[0];
aNode.green += rgba[1];
aNode.blue += rgba[2];
if (!aNode.isLeaf) {
int branchIndex = getBranchIndex(rgba, aLevel);
if (aNode.children[branchIndex] == null) {
aNode.childCount++;
if (aNode.childCount == 2) {
aNode.nextReducible = reduceList[aLevel];
reduceList[aLevel] = aNode;
}
}
aNode.children[branchIndex] = insertNode(
aNode.children[branchIndex], rgba, aLevel + 1);
}
return aNode;
}
public IndexColorModel getIndexColorModel() {
int size = currSize;
if (transparency == Transparency.BITMASK) {
size++; // we need place for transparent color;
}
// if the palette size happens to be just one (happens with a fully transparent image)
// then increase the size to two, otherwise the png encoders will go mad
if(size < 2)
size = 2;
final byte[] red = new byte[size];
final byte[] green = new byte[size];
final byte[] blue = new byte[size];
int index = 0;
palette = new ColorNode[size];
if (transparency == Transparency.BITMASK) {
index++;
}
findPaletteEntry(root, index, red, green, blue);
if (transparency == Transparency.BITMASK) {
return new IndexColorModel(8, size, red, green, blue, 0);
}
return new IndexColorModel(8, currSize, red, green, blue);
}
protected int findPaletteEntry(ColorNode aNode, int index, byte[] red,
byte[] green, byte[] blue) {
if (aNode == null) {
return index;
}
if (aNode.isLeaf) {
red[index] = (byte) (aNode.red / aNode.colorCount);
green[index] = (byte) (aNode.green / aNode.colorCount);
blue[index] = (byte) (aNode.blue / aNode.colorCount);
aNode.paletteIndex = index;
palette[index] = aNode;
index++;
} else {
for (int i = 0; i < 8; i++) {
if (aNode.children[i] != null) {
index = findPaletteEntry(aNode.children[i], index, red,
green, blue);
}
}
}
return index;
}
protected int getBranchIndex(int[] rgba, int aLevel) {
if ((aLevel > maxLevel) || (aLevel < 0)) {
throw new IllegalArgumentException("Invalid octree node depth: "
+ aLevel);
}
int shift = maxLevel - aLevel;
int red_index = 0x1 & ((0xff & rgba[0]) >> shift);
int green_index = 0x1 & ((0xff & rgba[1]) >> shift);
int blue_index = 0x1 & ((0xff & rgba[2]) >> shift);
int index = (red_index << 2) | (green_index << 1) | blue_index;
return index;
}
protected void reduceTree() {
int level = reduceList.length - 1;
while ((reduceList[level] == null) && (level >= 0)) {
level--;
}
ColorNode thisNode = reduceList[level];
if (thisNode == null) {
// nothing to reduce
return;
}
// look for element with lower color count
ColorNode pList = thisNode;
int minColorCount = pList.colorCount;
int cnt = 1;
while (pList.nextReducible != null) {
if (minColorCount > pList.nextReducible.colorCount) {
thisNode = pList;
minColorCount = pList.colorCount;
}
pList = pList.nextReducible;
cnt++;
}
// save pointer to first reducible node
// NB: current color count for node could be changed in future
if (thisNode == reduceList[level]) {
reduceList[level] = thisNode.nextReducible;
} else {
pList = thisNode.nextReducible; // we need to process it
thisNode.nextReducible = pList.nextReducible;
thisNode = pList;
}
if (thisNode.isLeaf) {
return;
}
// reduce node
int leafChildCount = thisNode.getLeafChildCount();
thisNode.isLeaf = true;
currSize -= (leafChildCount - 1);
final int aDepth = thisNode.level;
for (int i = 0; i < 8; i++) {
thisNode.children[i] = freeTree(thisNode.children[i]);
}
thisNode.childCount = 0;
}
protected ColorNode freeTree(ColorNode aNode) {
if (aNode == null) {
return null;
}
for (int i = 0; i < 8; i++) {
aNode.children[i] = freeTree(aNode.children[i]);
}
numNodes--;
return null;
}
/**
* The node of color tree.
*/
protected class ColorNode {
public boolean isLeaf;
public int childCount;
public ColorNode[] children;
public int colorCount;
public long red;
public long blue;
public long green;
public int paletteIndex;
public int level;
public ColorNode nextReducible;
public ColorNode() {
isLeaf = false;
level = 0;
childCount = 0;
children = new ColorNode[8];
for (int i = 0; i < 8; i++) {
children[i] = null;
}
colorCount = 0;
red = green = blue = 0;
paletteIndex = 0;
}
public int getLeafChildCount() {
if (isLeaf) {
return 0;
}
int cnt = 0;
for (int i = 0; i < children.length; i++) {
if (children[i] != null) {
if (children[i].isLeaf) {
cnt++;
} else {
cnt += children[i].getLeafChildCount();
}
}
}
return cnt;
}
public int getRGB() {
int r = (int) red / colorCount;
int g = (int) green / colorCount;
int b = (int) blue / colorCount;
int c = (0xff << 24) | ((0xff & r) << 16) | ((0xff & g) << 8)
| (0xff & b);
return c;
}
}
public int findNearestColorIndex(int[] rgba, int transparentBand) {
return findColorIndex(root, rgba, transparentBand);
}
}