com.twelvemonkeys.image.IndexImage Maven / Gradle / Ivy
The newest version!
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * 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.
* * Neither the name "TwelveMonkeys" nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT OWNER 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.
*/
/*
******************************************************************************
*
* ============================================================================
* The Apache Software License, Version 1.1
* ============================================================================
*
* Copyright (C) 2000 The Apache Software Foundation. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modifica-
* tion, 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.
*
* 3. The end-user documentation included with the redistribution, if any, must
* include the following acknowledgment: "This product includes software
* developed by the Apache Software Foundation (http://www.apache.org/)."
* Alternately, this acknowledgment may appear in the software itself, if
* and wherever such third-party acknowledgments normally appear.
*
* 4. The names "Batik" and "Apache Software Foundation" must not be used to
* endorse or promote products derived from this software without prior
* written permission. For written permission, please contact
* [email protected].
*
* 5. Products derived from this software may not be called "Apache", nor may
* "Apache" appear in their name, without prior written permission of the
* Apache Software Foundation.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED 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
* APACHE SOFTWARE FOUNDATION OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLU-
* DING, 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.
*
* This software consists of voluntary contributions made by many individuals
* on behalf of the Apache Software Foundation. For more information on the
* Apache Software Foundation, please see .
*
******************************************************************************
*
*/
package com.twelvemonkeys.image;
import com.twelvemonkeys.io.FileUtil;
import com.twelvemonkeys.lang.StringUtil;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.IndexColorModel;
import java.awt.image.RenderedImage;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* This class implements an adaptive pallete generator to reduce images
* to a variable number of colors.
* It can also render images into fixed color pallettes.
*
* Support for the default JVM (ordered/pattern) dither, Floyd-Steinberg like
* error-diffusion and no dither, controlled by the hints
* {@link #DITHER_DIFFUSION},
* {@link #DITHER_NONE} and
* {@link #DITHER_DEFAULT}.
*
* Color selection speed/accuracy can be controlled using the hints
* {@link #COLOR_SELECTION_FAST},
* {@link #COLOR_SELECTION_QUALITY} and
* {@link #COLOR_SELECTION_DEFAULT}.
*
* Transparency support can be controlled using the hints
* {@link #TRANSPARENCY_OPAQUE},
* {@link #TRANSPARENCY_BITMASK} and
* {@link #TRANSPARENCY_TRANSLUCENT}.
*
*
*
*
* This product includes software developed by the Apache Software Foundation.
*
* This software consists of voluntary contributions made by many individuals
* on behalf of the Apache Software Foundation. For more information on the
* Apache Software Foundation, please see http://www.apache.org/
*
*
* @author Thomas DeWeese
* @author Jun Inamori
* @author Harald Kuhr
* @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/IndexImage.java#1 $
* @see DiffusionDither
*/
class IndexImage {
/**
* Dither mask
*/
protected final static int DITHER_MASK = 0xFF;
/**
* Java default dither
*/
public final static int DITHER_DEFAULT = 0x00;
/**
* No dither
*/
public final static int DITHER_NONE = 0x01;
/**
* Error diffusion dither
*/
public final static int DITHER_DIFFUSION = 0x02;
/**
* Error diffusion dither with alternating scans
*/
public final static int DITHER_DIFFUSION_ALTSCANS = 0x03;
/**
* Color Selection mask
*/
protected final static int COLOR_SELECTION_MASK = 0xFF00;
/**
* Default color selection
*/
public final static int COLOR_SELECTION_DEFAULT = 0x0000;
/**
* Prioritize speed
*/
public final static int COLOR_SELECTION_FAST = 0x0100;
/**
* Prioritize quality
*/
public final static int COLOR_SELECTION_QUALITY = 0x0200;
/**
* Transparency mask
*/
protected final static int TRANSPARENCY_MASK = 0xFF0000;
/**
* Default transparency (none)
*/
public final static int TRANSPARENCY_DEFAULT = 0x000000;
/**
* Discard any alpha information
*/
public final static int TRANSPARENCY_OPAQUE = 0x010000;
/**
* Convert alpha to bitmask
*/
public final static int TRANSPARENCY_BITMASK = 0x020000;
/**
* Keep original alpha (not supported yet)
*/
protected final static int TRANSPARENCY_TRANSLUCENT = 0x030000;
/**
* Used to track a color and the number of pixels of that colors
*/
private static class Counter {
/**
* Field val
*/
public int val;
/**
* Field count
*/
public int count = 1;
/**
* Constructor Counter
*
* @param val the initial value
*/
public Counter(int val) {
this.val = val;
}
/**
* Method add
*
* @param val the new value
* @return {@code true} if the value was added, otherwise {@code false}
*/
public boolean add(int val) {
// See if the value matches us...
if (this.val != val) {
return false;
}
count++;
return true;
}
}
/**
* Used to define a cube of the colorspace. The cube can be split
* approximagely in half to generate two cubes.
*/
private static class Cube {
int[] min = {0, 0, 0}, max = {255, 255, 255};
boolean done = false;
List[] colors = null;
int count = 0;
static final int RED = 0;
static final int GRN = 1;
static final int BLU = 2;
/**
* Define a new cube.
*
* @param colors contains the 3D color histogram to be subdivided
* @param count the total number of pixels in the 3D histogram.
*/
public Cube(List[] colors, int count) {
this.colors = colors;
this.count = count;
}
/**
* If this returns true then the cube can not be subdivided any
* further
*
* @return true if cube can not be subdivided any further
*/
public boolean isDone() {
return done;
}
/**
* Splits the cube into two parts. This cube is
* changed to be one half and the returned cube is the other half.
* This tries to pick the right channel to split on.
*
* @return the {@code Cube} containing the other half
*/
public Cube split() {
int dr = max[0] - min[0] + 1;
int dg = max[1] - min[1] + 1;
int db = max[2] - min[2] + 1;
int c0, c1, splitChannel;
// Figure out which axis is the longest and split along
// that axis (this tries to keep cubes square-ish).
if (dr >= dg) {
c0 = GRN;
if (dr >= db) {
splitChannel = RED;
c1 = BLU;
}
else {
splitChannel = BLU;
c1 = RED;
}
}
else if (dg >= db) {
splitChannel = GRN;
c0 = RED;
c1 = BLU;
}
else {
splitChannel = BLU;
c0 = RED;
c1 = GRN;
}
Cube ret;
ret = splitChannel(splitChannel, c0, c1);
if (ret != null) {
return ret;
}
ret = splitChannel(c0, splitChannel, c1);
if (ret != null) {
return ret;
}
ret = splitChannel(c1, splitChannel, c0);
if (ret != null) {
return ret;
}
done = true;
return null;
}
/**
* Splits the image according to the parameters. It tries
* to find a location where half the pixels are on one side
* and half the pixels are on the other.
*
* @param splitChannel split channel
* @param c0 channel 0
* @param c1 channel 1
* @return the {@code Cube} containing the other half
*/
public Cube splitChannel(int splitChannel, int c0, int c1) {
if (min[splitChannel] == max[splitChannel]) {
return null;
}
int splitSh4 = (2 - splitChannel) * 4;
int c0Sh4 = (2 - c0) * 4;
int c1Sh4 = (2 - c1) * 4;
// int splitSh8 = (2-splitChannel)*8;
// int c0Sh8 = (2-c0)*8;
// int c1Sh8 = (2-c1)*8;
//
int half = count / 2;
// Each entry is the number of pixels that have that value
// in the split channel within the cube (so pixels
// that have that value in the split channel aren't counted
// if they are outside the cube in the other color channels.
int counts[] = new int[256];
int tcount = 0;
// System.out.println("Cube: [" +
// min[0] + "-" + max[0] + "] [" +
// min[1] + "-" + max[1] + "] [" +
// min[2] + "-" + max[2] + "]");
int[] minIdx = {min[0] >> 4, min[1] >> 4, min[2] >> 4};
int[] maxIdx = {max[0] >> 4, max[1] >> 4, max[2] >> 4};
int minR = min[0], minG = min[1], minB = min[2];
int maxR = max[0], maxG = max[1], maxB = max[2];
int val;
int[] vals = {0, 0, 0};
for (int i = minIdx[splitChannel]; i <= maxIdx[splitChannel]; i++) {
int idx1 = i << splitSh4;
for (int j = minIdx[c0]; j <= maxIdx[c0]; j++) {
int idx2 = idx1 | (j << c0Sh4);
for (int k = minIdx[c1]; k <= maxIdx[c1]; k++) {
int idx = idx2 | (k << c1Sh4);
List v = colors[idx];
if (v == null) {
continue;
}
Iterator itr = v.iterator();
Counter c;
while (itr.hasNext()) {
c = (Counter) itr.next();
val = c.val;
vals[0] = (val & 0xFF0000) >> 16;
vals[1] = (val & 0xFF00) >> 8;
vals[2] = (val & 0xFF);
if (((vals[0] >= minR) && (vals[0] <= maxR)) && ((vals[1] >= minG) && (vals[1] <= maxG))
&& ((vals[2] >= minB) && (vals[2] <= maxB))) {
// The val lies within this cube so count it.
counts[vals[splitChannel]] += c.count;
tcount += c.count;
}
}
}
}
// We've found the half way point. Note that the
// rest of counts is not filled out.
if (tcount >= half) {
break;
}
}
tcount = 0;
int lastAdd = -1;
// These indicate what the top value for the low cube and
// the low value of the high cube should be in the split channel
// (they may not be one off if there are 'dead' spots in the
// counts array.)
int splitLo = min[splitChannel], splitHi = max[splitChannel];
for (int i = min[splitChannel]; i <= max[splitChannel]; i++) {
int c = counts[i];
if (c == 0) {
// No counts below this so move up bottom of cube.
if ((tcount == 0) && (i < max[splitChannel])) {
this.min[splitChannel] = i + 1;
}
continue;
}
if (tcount + c < half) {
lastAdd = i;
tcount += c;
continue;
}
if ((half - tcount) <= ((tcount + c) - half)) {
// Then lastAdd is a better top idx for this then i.
if (lastAdd == -1) {
// No lower place to break.
if (c == this.count) {
// All pixels are at this value so make min/max
// reflect that.
this.max[splitChannel] = i;
return null;// no split to make.
}
else {
// There are values about this one so
// split above.
splitLo = i;
splitHi = i + 1;
break;
}
}
splitLo = lastAdd;
splitHi = i;
}
else {
if (i == this.max[splitChannel]) {
if (c == this.count) {
// would move min up but that should
// have happened already.
return null;// no split to make.
}
else {
// Would like to break between i and i+1
// but no i+1 so use lastAdd and i;
splitLo = lastAdd;
splitHi = i;
break;
}
}
// Include c in counts
tcount += c;
splitLo = i;
splitHi = i + 1;
}
break;
}
// System.out.println("Split: " + splitChannel + "@"
// + splitLo + "-"+splitHi +
// " Count: " + tcount + " of " + count +
// " LA: " + lastAdd);
// Create the new cube and update everyone's bounds & counts.
Cube ret = new Cube(colors, tcount);
this.count = this.count - tcount;
ret.min[splitChannel] = this.min[splitChannel];
ret.max[splitChannel] = splitLo;
this.min[splitChannel] = splitHi;
ret.min[c0] = this.min[c0];
ret.max[c0] = this.max[c0];
ret.min[c1] = this.min[c1];
ret.max[c1] = this.max[c1];
return ret;
}
/**
* Returns the average color for this cube
*
* @return the average
*/
public int averageColor() {
if (this.count == 0) {
return 0;
}
float red = 0, grn = 0, blu = 0;
int minR = min[0], minG = min[1], minB = min[2];
int maxR = max[0], maxG = max[1], maxB = max[2];
int[] minIdx = {minR >> 4, minG >> 4, minB >> 4};
int[] maxIdx = {maxR >> 4, maxG >> 4, maxB >> 4};
int val, ired, igrn, iblu;
float weight;
for (int i = minIdx[0]; i <= maxIdx[0]; i++) {
int idx1 = i << 8;
for (int j = minIdx[1]; j <= maxIdx[1]; j++) {
int idx2 = idx1 | (j << 4);
for (int k = minIdx[2]; k <= maxIdx[2]; k++) {
int idx = idx2 | k;
List v = colors[idx];
if (v == null) {
continue;
}
Iterator itr = v.iterator();
Counter c;
while (itr.hasNext()) {
c = (Counter) itr.next();
val = c.val;
ired = (val & 0xFF0000) >> 16;
igrn = (val & 0x00FF00) >> 8;
iblu = (val & 0x0000FF);
if (((ired >= minR) && (ired <= maxR)) && ((igrn >= minG) && (igrn <= maxG)) && ((iblu >= minB) && (iblu <= maxB))) {
weight = (c.count / (float) this.count);
red += ((float) ired) * weight;
grn += ((float) igrn) * weight;
blu += ((float) iblu) * weight;
}
}
}
}
}
// System.out.println("RGB: [" + red + ", " +
// grn + ", " + blu + "]");
return (((int) (red + 0.5f)) << 16 | ((int) (grn + 0.5f)) << 8 | ((int) (blu + 0.5f)));
}
}// end Cube
/**
* You cannot create this
*/
private IndexImage() {
}
/**
* @param pImage the image to get {@code IndexColorModel} from
* @param pNumberOfColors the number of colors for the {@code IndexColorModel}
* @param pFast {@code true} if fast
* @return an {@code IndexColorModel}
* @see #getIndexColorModel(Image,int,int)
*
* @deprecated Use {@link #getIndexColorModel(Image,int,int)} instead!
* This version will be removed in a later version of the API.
*/
public static IndexColorModel getIndexColorModel(Image pImage, int pNumberOfColors, boolean pFast) {
return getIndexColorModel(pImage, pNumberOfColors, pFast
? COLOR_SELECTION_FAST
: COLOR_SELECTION_QUALITY);
}
/**
* Gets an {@code IndexColorModel} from the given image. If the image has an
* {@code IndexColorModel}, this will be returned. Otherwise, an {@code IndexColorModel}
* is created, using an adaptive pallete.
*
* @param pImage the image to get {@code IndexColorModel} from
* @param pNumberOfColors the number of colors for the {@code IndexColorModel}
* @param pHints one of {@link #COLOR_SELECTION_FAST},
* {@link #COLOR_SELECTION_QUALITY} or
* {@link #COLOR_SELECTION_DEFAULT}.
* @return The {@code IndexColorModel} from the given image, or a newly created
* {@code IndexColorModel} using an adaptive palette.
* @throws ImageConversionException if an exception occurred during color
* model extraction.
*/
public static IndexColorModel getIndexColorModel(Image pImage, int pNumberOfColors, int pHints) throws ImageConversionException {
IndexColorModel icm = null;
RenderedImage image = null;
if (pImage instanceof RenderedImage) {
image = (RenderedImage) pImage;
ColorModel cm = image.getColorModel();
if (cm instanceof IndexColorModel) {
// Test if we have right number of colors
if (((IndexColorModel) cm).getMapSize() <= pNumberOfColors) {
//System.out.println("IndexColorModel from BufferedImage");
icm = (IndexColorModel) cm;// Done
}
}
// Else create from buffered image, hard way, see below
}
else {
// Create from image using BufferedImageFactory
BufferedImageFactory factory = new BufferedImageFactory(pImage);
ColorModel cm = factory.getColorModel();
if ((cm instanceof IndexColorModel) && ((IndexColorModel) cm).getMapSize() <= pNumberOfColors) {
//System.out.println("IndexColorModel from Image");
icm = (IndexColorModel) cm;// Done
}
else {
// Else create from (buffered) image, hard way
image = factory.getBufferedImage();
}
}
// We now have at least a buffered image, create model from it
if (icm == null) {
icm = createIndexColorModel(ImageUtil.toBuffered(image), pNumberOfColors, pHints);
//System.out.println("IndexColorModel created from colors.");
}
else if (!(icm instanceof InverseColorMapIndexColorModel)) {
// If possible, use faster code
//System.out.println("Wrappimg IndexColorModel in InverseColorMapIndexColorModel");
icm = new InverseColorMapIndexColorModel(icm);
}
//else {
//System.out.println("Allredy InverseColorMapIndexColorModel");
//}
return icm;
}
/**
* Creates an {@code IndexColorModel} from the given image, using an adaptive
* pallete.
*
* @param pImage the image to get {@code IndexColorModel} from
* @param pNumberOfColors the number of colors for the {@code IndexColorModel}
* @param pHints use fast mode if possible (might give slightly lower
* quality)
* @return a new {@code IndexColorModel} created from the given image
*/
private static IndexColorModel createIndexColorModel(BufferedImage pImage, int pNumberOfColors, int pHints) {
// TODO: Use ImageUtil.hasTransparentPixels(pImage, true) ||
// -- haraldK, 20021024, experimental, try to use one transparent pixel
boolean useTransparency = isTransparent(pHints);
if (useTransparency) {
pNumberOfColors--;
}
//System.out.println("Transp: " + useTransparency + " colors: " + pNumberOfColors);
int width = pImage.getWidth();
int height = pImage.getHeight();
// Using 4 bits from R, G & B.
List[] colors = new List[1 << 12];// [4096]
// Speedup, doesn't decrease image quality much
int step = 1;
if (isFast(pHints)) {
step += (width * height / 16384);// 128x128px
}
int sampleCount = 0;
int rgb;
//for (int x = 0; x < width; x++) {
//for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
for (int y = x % step; y < height; y += step) {
// Count the number of color samples
sampleCount++;
// Get ARGB pixel from image
rgb = (pImage.getRGB(x, y) & 0xFFFFFF);
// Get index from high four bits of each component.
int index = (((rgb & 0xF00000) >>> 12) | ((rgb & 0x00F000) >>> 8) | ((rgb & 0x0000F0) >>> 4));
// Get the 'hash vector' for that key.
List v = colors[index];
if (v == null) {
// No colors in this bin yet so create vector and
// add color.
v = new ArrayList();
v.add(new Counter(rgb));
colors[index] = v;
}
else {
// Find our color in the bin or create a counter for it.
Iterator i = v.iterator();
while (true) {
if (i.hasNext()) {
// try adding our color to each counter...
if (((Counter) i.next()).add(rgb)) {
break;
}
}
else {
v.add(new Counter(rgb));
break;
}
}
}
}
}
// All colours found, reduce to pNumberOfColors
int numberOfCubes = 1;
int fCube = 0;
Cube[] cubes = new Cube[pNumberOfColors];
cubes[0] = new Cube(colors, sampleCount);
//cubes[0] = new Cube(colors, width * height);
while (numberOfCubes < pNumberOfColors) {
while (cubes[fCube].isDone()) {
fCube++;
if (fCube == numberOfCubes) {
break;
}
}
if (fCube == numberOfCubes) {
break;
}
Cube cube = cubes[fCube];
Cube newCube = cube.split();
if (newCube != null) {
if (newCube.count > cube.count) {
Cube tmp = cube;
cube = newCube;
newCube = tmp;
}
int j = fCube;
int count = cube.count;
for (int i = fCube + 1; i < numberOfCubes; i++) {
if (cubes[i].count < count) {
break;
}
cubes[j++] = cubes[i];
}
cubes[j++] = cube;
count = newCube.count;
while (j < numberOfCubes) {
if (cubes[j].count < count) {
break;
}
j++;
}
for (int i = numberOfCubes; i > j; i--) {
cubes[i] = cubes[i - 1];
}
cubes[j/*++*/] = newCube;
numberOfCubes++;
}
}
// Create RGB arrays with correct number of colors
// If we have transparency, the last color will be the transparent one
byte[] r = new byte[useTransparency ? numberOfCubes + 1 : numberOfCubes];
byte[] g = new byte[useTransparency ? numberOfCubes + 1 : numberOfCubes];
byte[] b = new byte[useTransparency ? numberOfCubes + 1 : numberOfCubes];
for (int i = 0; i < numberOfCubes; i++) {
int val = cubes[i].averageColor();
r[i] = (byte) ((val >> 16) & 0xFF);
g[i] = (byte) ((val >> 8) & 0xFF);
b[i] = (byte) ((val) & 0xFF);
//System.out.println("Color [" + i + "]: #" +
// (((val>>16)<16)?"0":"") +
// Integer.toHexString(val));
}
// For some reason using less than 8 bits causes a bug in the dither
// - transparency added to all totally black colors?
int numOfBits = 8;
// -- haraldK, 20021024, as suggested by Thomas E Deweese
// plus adding a transparent pixel
IndexColorModel icm;
if (useTransparency) {
//icm = new IndexColorModel(numOfBits, r.length, r, g, b, r.length - 1);
icm = new InverseColorMapIndexColorModel(numOfBits, r.length, r, g, b, r.length - 1);
}
else {
//icm = new IndexColorModel(numOfBits, r.length, r, g, b);
icm = new InverseColorMapIndexColorModel(numOfBits, r.length, r, g, b);
}
return icm;
}
/**
* Converts the input image (must be {@code TYPE_INT_RGB} or
* {@code TYPE_INT_ARGB}) to an indexed image. Generating an adaptive
* pallete (8 bit) from the color data in the image, and uses default
* dither.
*
* The image returned is a new image, the input image is not modified.
*
* @param pImage the BufferedImage to index and get color information from.
* @return the indexed BufferedImage. The image will be of type
* {@code BufferedImage.TYPE_BYTE_INDEXED}, and use an
* {@code IndexColorModel}.
* @see BufferedImage#TYPE_BYTE_INDEXED
* @see IndexColorModel
*/
public static BufferedImage getIndexedImage(BufferedImage pImage) {
return getIndexedImage(pImage, 256, DITHER_DEFAULT);
}
/**
* Tests if the hint {@code COLOR_SELECTION_QUALITY} is not
* set.
*
* @param pHints hints
* @return true if the hint {@code COLOR_SELECTION_QUALITY}
* is not set.
*/
private static boolean isFast(int pHints) {
return (pHints & COLOR_SELECTION_MASK) != COLOR_SELECTION_QUALITY;
}
/**
* Tests if the hint {@code TRANSPARENCY_BITMASK} or
* {@code TRANSPARENCY_TRANSLUCENT} is set.
*
* @param pHints hints
* @return true if the hint {@code TRANSPARENCY_BITMASK} or
* {@code TRANSPARENCY_TRANSLUCENT} is set.
*/
static boolean isTransparent(int pHints) {
return (pHints & TRANSPARENCY_BITMASK) != 0 || (pHints & TRANSPARENCY_TRANSLUCENT) != 0;
}
/**
* Converts the input image (must be {@code TYPE_INT_RGB} or
* {@code TYPE_INT_ARGB}) to an indexed image. If the palette image
* uses an {@code IndexColorModel}, this will be used. Otherwise, generating an
* adaptive pallete (8 bit) from the given palette image.
* Dithering, transparency and color selection is controlled with the
* {@code pHints}parameter.
*
* The image returned is a new image, the input image is not modified.
*
* @param pImage the BufferedImage to index
* @param pPalette the Image to read color information from
* @param pMatte the background color, used where the original image was
* transparent
* @param pHints mHints that control output quality and speed.
* @return the indexed BufferedImage. The image will be of type
* {@code BufferedImage.TYPE_BYTE_INDEXED} or
* {@code BufferedImage.TYPE_BYTE_BINARY}, and use an
* {@code IndexColorModel}.
* @throws ImageConversionException if an exception occurred during color
* model extraction.
* @see #DITHER_DIFFUSION
* @see #DITHER_NONE
* @see #COLOR_SELECTION_FAST
* @see #COLOR_SELECTION_QUALITY
* @see #TRANSPARENCY_OPAQUE
* @see #TRANSPARENCY_BITMASK
* @see BufferedImage#TYPE_BYTE_INDEXED
* @see BufferedImage#TYPE_BYTE_BINARY
* @see IndexColorModel
*/
public static BufferedImage getIndexedImage(BufferedImage pImage, Image pPalette, Color pMatte, int pHints)
throws ImageConversionException {
return getIndexedImage(pImage, getIndexColorModel(pPalette, 256, pHints), pMatte, pHints);
}
/**
* Converts the input image (must be {@code TYPE_INT_RGB} or
* {@code TYPE_INT_ARGB}) to an indexed image. Generating an adaptive
* pallete with the given number of colors.
* Dithering, transparency and color selection is controlled with the
* {@code pHints}parameter.
*
* The image returned is a new image, the input image is not modified.
*
* @param pImage the BufferedImage to index
* @param pNumberOfColors the number of colors for the image
* @param pMatte the background color, used where the original image was
* transparent
* @param pHints mHints that control output quality and speed.
* @return the indexed BufferedImage. The image will be of type
* {@code BufferedImage.TYPE_BYTE_INDEXED} or
* {@code BufferedImage.TYPE_BYTE_BINARY}, and use an
* {@code IndexColorModel}.
* @see #DITHER_DIFFUSION
* @see #DITHER_NONE
* @see #COLOR_SELECTION_FAST
* @see #COLOR_SELECTION_QUALITY
* @see #TRANSPARENCY_OPAQUE
* @see #TRANSPARENCY_BITMASK
* @see BufferedImage#TYPE_BYTE_INDEXED
* @see BufferedImage#TYPE_BYTE_BINARY
* @see IndexColorModel
*/
public static BufferedImage getIndexedImage(BufferedImage pImage, int pNumberOfColors, Color pMatte, int pHints) {
// NOTE: We need to apply matte before creating colormodel, otherwise we
// won't have colors for potential faded transitions
IndexColorModel icm;
if (pMatte != null) {
icm = getIndexColorModel(createSolid(pImage, pMatte), pNumberOfColors, pHints);
}
else {
icm = getIndexColorModel(pImage, pNumberOfColors, pHints);
}
// If we found less colors, then no need to dither
if ((pHints & DITHER_MASK) != DITHER_NONE && (icm.getMapSize() < pNumberOfColors)) {
pHints = (pHints & ~DITHER_MASK) | DITHER_NONE;
}
return getIndexedImage(pImage, icm, pMatte, pHints);
}
/**
* Converts the input image (must be {@code TYPE_INT_RGB} or
* {@code TYPE_INT_ARGB}) to an indexed image. Using the supplied
* {@code IndexColorModel}'s pallete.
* Dithering, transparency and color selection is controlled with the
* {@code pHints} parameter.
*
* The image returned is a new image, the input image is not modified.
*
* @param pImage the BufferedImage to index
* @param pColors an {@code IndexColorModel} containing the color information
* @param pMatte the background color, used where the original image was
* transparent. Also note that any transparent antialias will be
* rendered against this color.
* @param pHints RenderingHints that control output quality and speed.
* @return the indexed BufferedImage. The image will be of type
* {@code BufferedImage.TYPE_BYTE_INDEXED} or
* {@code BufferedImage.TYPE_BYTE_BINARY}, and use an
* {@code IndexColorModel}.
* @see #DITHER_DIFFUSION
* @see #DITHER_NONE
* @see #COLOR_SELECTION_FAST
* @see #COLOR_SELECTION_QUALITY
* @see #TRANSPARENCY_OPAQUE
* @see #TRANSPARENCY_BITMASK
* @see BufferedImage#TYPE_BYTE_INDEXED
* @see BufferedImage#TYPE_BYTE_BINARY
* @see IndexColorModel
*/
public static BufferedImage getIndexedImage(BufferedImage pImage, IndexColorModel pColors, Color pMatte, int pHints) {
// TODO: Consider:
/*
if (pImage.getType() == BufferedImage.TYPE_BYTE_INDEXED
|| pImage.getType() == BufferedImage.TYPE_BYTE_BINARY) {
pImage = ImageUtil.toBufferedImage(pImage, BufferedImage.TYPE_INT_ARGB);
}
*/
// Get dimensions
final int width = pImage.getWidth();
final int height = pImage.getHeight();
// Support transparancy?
boolean transparency = isTransparent(pHints) && (pImage.getColorModel().getTransparency() != Transparency.OPAQUE) && (pColors.getTransparency() != Transparency.OPAQUE);
// Create image with solid background
BufferedImage solid = pImage;
if (pMatte != null) {// transparency doesn't really matter
solid = createSolid(pImage, pMatte);
}
BufferedImage indexed;
// Support TYPE_BYTE_BINARY, but only for 2 bit images, as the default
// dither does not work with TYPE_BYTE_BINARY it seems...
if (pColors.getMapSize() > 2) {
indexed = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, pColors);
}
else {
indexed = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_BINARY, pColors);
}
// Apply dither if requested
switch (pHints & DITHER_MASK) {
case DITHER_DIFFUSION:
case DITHER_DIFFUSION_ALTSCANS:
// Create a DiffusionDither to apply dither to indexed
DiffusionDither dither = new DiffusionDither(pColors);
if ((pHints & DITHER_MASK) == DITHER_DIFFUSION_ALTSCANS) {
dither.setAlternateScans(true);
}
dither.filter(solid, indexed);
break;
case DITHER_NONE:
// Just copy pixels, without dither
// NOTE: This seems to be slower than the method below, using
// Graphics2D.drawImage, and VALUE_DITHER_DISABLE,
// however you possibly end up getting a dithered image anyway,
// therefore, do it slower and produce correct result. :-)
CopyDither copy = new CopyDither(pColors);
copy.filter(solid, indexed);
break;
case DITHER_DEFAULT:
// This is the default
default:
// Render image data onto indexed image, using default
// (probably we get dither, but it depends on the GFX engine).
Graphics2D g2d = indexed.createGraphics();
try {
RenderingHints hints = new RenderingHints(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
g2d.setRenderingHints(hints);
g2d.drawImage(solid, 0, 0, null);
}
finally {
g2d.dispose();
}
break;
}
// Transparency support, this approach seems lame, but it's the only
// solution I've found until now (that actually works).
// Got anything to do with isPremultiplied? Hmm...
if (transparency) {
// Re-apply the alpha-channel of the original image
applyAlpha(indexed, pImage);
}
// Return the indexed BufferedImage
return indexed;
}
/**
* Converts the input image (must be {@code TYPE_INT_RGB} or
* {@code TYPE_INT_ARGB}) to an indexed image. Generating an adaptive
* pallete with the given number of colors.
* Dithering, transparency and color selection is controlled with the
* {@code pHints}parameter.
*
* The image returned is a new image, the input image is not modified.
*
* @param pImage the BufferedImage to index
* @param pNumberOfColors the number of colors for the image
* @param pHints mHints that control output quality and speed.
* @return the indexed BufferedImage. The image will be of type
* {@code BufferedImage.TYPE_BYTE_INDEXED} or
* {@code BufferedImage.TYPE_BYTE_BINARY}, and use an
* {@code IndexColorModel}.
* @see #DITHER_DIFFUSION
* @see #DITHER_NONE
* @see #COLOR_SELECTION_FAST
* @see #COLOR_SELECTION_QUALITY
* @see #TRANSPARENCY_OPAQUE
* @see #TRANSPARENCY_BITMASK
* @see BufferedImage#TYPE_BYTE_INDEXED
* @see BufferedImage#TYPE_BYTE_BINARY
* @see IndexColorModel
*/
public static BufferedImage getIndexedImage(BufferedImage pImage, int pNumberOfColors, int pHints) {
return getIndexedImage(pImage, pNumberOfColors, null, pHints);
}
/**
* Converts the input image (must be {@code TYPE_INT_RGB} or
* {@code TYPE_INT_ARGB}) to an indexed image. Using the supplied
* {@code IndexColorModel}'s pallete.
* Dithering, transparency and color selection is controlled with the
* {@code pHints}parameter.
*
* The image returned is a new image, the input image is not modified.
*
* @param pImage the BufferedImage to index
* @param pColors an {@code IndexColorModel} containing the color information
* @param pHints RenderingHints that control output quality and speed.
* @return the indexed BufferedImage. The image will be of type
* {@code BufferedImage.TYPE_BYTE_INDEXED} or
* {@code BufferedImage.TYPE_BYTE_BINARY}, and use an
* {@code IndexColorModel}.
* @see #DITHER_DIFFUSION
* @see #DITHER_NONE
* @see #COLOR_SELECTION_FAST
* @see #COLOR_SELECTION_QUALITY
* @see #TRANSPARENCY_OPAQUE
* @see #TRANSPARENCY_BITMASK
* @see BufferedImage#TYPE_BYTE_INDEXED
* @see BufferedImage#TYPE_BYTE_BINARY
* @see IndexColorModel
*/
public static BufferedImage getIndexedImage(BufferedImage pImage, IndexColorModel pColors, int pHints) {
return getIndexedImage(pImage, pColors, null, pHints);
}
/**
* Converts the input image (must be {@code TYPE_INT_RGB} or
* {@code TYPE_INT_ARGB}) to an indexed image. If the palette image
* uses an {@code IndexColorModel}, this will be used. Otherwise, generating an
* adaptive pallete (8 bit) from the given palette image.
* Dithering, transparency and color selection is controlled with the
* {@code pHints}parameter.
*
* The image returned is a new image, the input image is not modified.
*
* @param pImage the BufferedImage to index
* @param pPalette the Image to read color information from
* @param pHints mHints that control output quality and speed.
* @return the indexed BufferedImage. The image will be of type
* {@code BufferedImage.TYPE_BYTE_INDEXED} or
* {@code BufferedImage.TYPE_BYTE_BINARY}, and use an
* {@code IndexColorModel}.
* @see #DITHER_DIFFUSION
* @see #DITHER_NONE
* @see #COLOR_SELECTION_FAST
* @see #COLOR_SELECTION_QUALITY
* @see #TRANSPARENCY_OPAQUE
* @see #TRANSPARENCY_BITMASK
* @see BufferedImage#TYPE_BYTE_INDEXED
* @see BufferedImage#TYPE_BYTE_BINARY
* @see IndexColorModel
*/
public static BufferedImage getIndexedImage(BufferedImage pImage, Image pPalette, int pHints) {
return getIndexedImage(pImage, pPalette, null, pHints);
}
/**
* Creates a copy of the given image, with a solid background
*
* @param pOriginal the original image
* @param pBackground the background color
* @return a new {@code BufferedImage}
*/
private static BufferedImage createSolid(BufferedImage pOriginal, Color pBackground) {
// Create a temporary image of same dimension and type
BufferedImage solid = new BufferedImage(pOriginal.getColorModel(), pOriginal.copyData(null), pOriginal.isAlphaPremultiplied(), null);
Graphics2D g = solid.createGraphics();
try {
// Clear in background color
g.setColor(pBackground);
g.setComposite(AlphaComposite.DstOver);// Paint "underneath"
g.fillRect(0, 0, pOriginal.getWidth(), pOriginal.getHeight());
}
finally {
g.dispose();
}
return solid;
}
/**
* Applies the alpha-component of the alpha image to the given image.
* The given image is modified in place.
*
* @param pImage the image to apply alpha to
* @param pAlpha the image containing the alpha
*/
private static void applyAlpha(BufferedImage pImage, BufferedImage pAlpha) {
// Apply alpha as transparancy, using threshold of 25%
for (int y = 0; y < pAlpha.getHeight(); y++) {
for (int x = 0; x < pAlpha.getWidth(); x++) {
// Get alpha component of pixel, if less than 25% opaque
// (0x40 = 64 => 25% of 256), the pixel will be transparent
if (((pAlpha.getRGB(x, y) >> 24) & 0xFF) < 0x40) {
pImage.setRGB(x, y, 0x00FFFFFF);// 100% transparent
}
}
}
}
/*
* This class is also a command-line utility.
*/
public static void main(String pArgs[]) {
// Defaults
int argIdx = 0;
int speedTest = -1;
boolean overWrite = false;
boolean monochrome = false;
boolean gray = false;
int numColors = 256;
String dither = null;
String quality = null;
String format = null;
Color background = null;
boolean transparency = false;
String paletteFileName = null;
boolean errArgs = false;
// Parse args
while ((argIdx < pArgs.length) && (pArgs[argIdx].charAt(0) == '-') && (pArgs[argIdx].length() >= 2)) {
if ((pArgs[argIdx].charAt(1) == 's') || pArgs[argIdx].equals("--speedtest")) {
argIdx++;
// Get number of iterations
if ((pArgs.length > argIdx) && (pArgs[argIdx].charAt(0) != '-')) {
try {
speedTest = Integer.parseInt(pArgs[argIdx++]);
}
catch (NumberFormatException nfe) {
errArgs = true;
break;
}
}
else {
// Default to 10 iterations
speedTest = 10;
}
}
else
if ((pArgs[argIdx].charAt(1) == 'w') || pArgs[argIdx].equals("--overwrite")) {
overWrite = true;
argIdx++;
}
else
if ((pArgs[argIdx].charAt(1) == 'c') || pArgs[argIdx].equals("--colors")) {
argIdx++;
try {
numColors = Integer.parseInt(pArgs[argIdx++]);
}
catch (NumberFormatException nfe) {
errArgs = true;
break;
}
}
else
if ((pArgs[argIdx].charAt(1) == 'g') || pArgs[argIdx].equals("--grayscale")) {
argIdx++;
gray = true;
}
else
if ((pArgs[argIdx].charAt(1) == 'm') || pArgs[argIdx].equals("--monochrome")) {
argIdx++;
numColors = 2;
monochrome = true;
}
else
if ((pArgs[argIdx].charAt(1) == 'd') || pArgs[argIdx].equals("--dither")) {
argIdx++;
dither = pArgs[argIdx++];
}
else
if ((pArgs[argIdx].charAt(1) == 'p') || pArgs[argIdx].equals("--palette")) {
argIdx++;
paletteFileName = pArgs[argIdx++];
}
else
if ((pArgs[argIdx].charAt(1) == 'q') || pArgs[argIdx].equals("--quality")) {
argIdx++;
quality = pArgs[argIdx++];
}
else
if ((pArgs[argIdx].charAt(1) == 'b') || pArgs[argIdx].equals("--bgcolor")) {
argIdx++;
try {
background = StringUtil.toColor(pArgs[argIdx++]);
}
catch (Exception e) {
errArgs = true;
break;
}
}
else
if ((pArgs[argIdx].charAt(1) == 't') || pArgs[argIdx].equals("--transparency")) {
argIdx++;
transparency = true;
}
else
if ((pArgs[argIdx].charAt(1) == 'f') || pArgs[argIdx].equals("--outputformat")) {
argIdx++;
format = StringUtil.toLowerCase(pArgs[argIdx++]);
}
else
if ((pArgs[argIdx].charAt(1) == 'h') || pArgs[argIdx].equals("--help")) {
argIdx++;
// Setting errArgs to true, to print usage
errArgs = true;
}
else {
System.err.println("Unknown option \"" + pArgs[argIdx++] + "\"");
}
}
if (errArgs || (pArgs.length < (argIdx + 1))) {
System.err.println("Usage: IndexImage [--help|-h] [--speedtest|-s ] [--bgcolor|-b ] [--colors|-c | --grayscale|g | --monochrome|-m | --palette|-p ] [--dither|-d (default|diffusion|none)] [--quality|-q (default|high|low)] [--transparency|-t] [--outputformat|-f (gif|jpeg|png|wbmp|...)] [--overwrite|-w] [