mirror of
https://github.com/haraldk/TwelveMonkeys.git
synced 2026-05-20 00:00:03 -04:00
196081a317
* Added the `@Deprecated` tag to instances where is was mentioned in documentation, but not for the actual code itself. Changed one documentation link pointing at a non-existing item. * As per PR suggestion.
1528 lines
56 KiB
Java
Executable File
1528 lines
56 KiB
Java
Executable File
/*
|
|
* 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 of the copyright holder 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 HOLDER 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
|
|
* apache@apache.org.
|
|
*
|
|
* 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 <http://www.apache.org/>.
|
|
*
|
|
******************************************************************************
|
|
*
|
|
*/
|
|
|
|
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.io.File;
|
|
import java.io.IOException;
|
|
import java.util.ArrayList;
|
|
import java.util.Iterator;
|
|
import java.util.List;
|
|
|
|
/**
|
|
* This class implements an adaptive palette generator to reduce images
|
|
* to a variable number of colors.
|
|
* It can also render images into fixed color pallettes.
|
|
* <p>
|
|
* 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}.
|
|
* </p>
|
|
* <p>
|
|
* Color selection speed/accuracy can be controlled using the hints
|
|
* {@link #COLOR_SELECTION_FAST},
|
|
* {@link #COLOR_SELECTION_QUALITY} and
|
|
* {@link #COLOR_SELECTION_DEFAULT}.
|
|
* </p>
|
|
* <p>
|
|
* Transparency support can be controlled using the hints
|
|
* {@link #TRANSPARENCY_OPAQUE},
|
|
* {@link #TRANSPARENCY_BITMASK} and
|
|
* {@link #TRANSPARENCY_TRANSLUCENT}.
|
|
* </p>
|
|
* <hr>
|
|
* <p>
|
|
* <pre>
|
|
* 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 <A href="http://www.apache.org/">http://www.apache.org/</A>
|
|
* </pre>
|
|
* </p>
|
|
*
|
|
* @author <A href="mailto:deweese@apache.org">Thomas DeWeese</A>
|
|
* @author <A href="mailto:jun@oop-reserch.com">Jun Inamori</A>
|
|
* @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
|
|
* @version $Id: 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 color space. The cube can be split
|
|
* approximately in half to generate two cubes.
|
|
*/
|
|
private static class Cube {
|
|
int[] min = {0, 0, 0};
|
|
int[] max = {255, 255, 255};
|
|
boolean done = false;
|
|
List<Counter>[] 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<Counter>[] 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<Counter> v = colors[idx];
|
|
|
|
if (v == null) {
|
|
continue;
|
|
}
|
|
|
|
for (Counter c : v) {
|
|
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<Counter> v = colors[idx];
|
|
|
|
if (v == null) {
|
|
continue;
|
|
}
|
|
|
|
for (Counter c : v) {
|
|
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.
|
|
*/
|
|
@Deprecated
|
|
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 palette.
|
|
*
|
|
* @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);
|
|
}
|
|
else if (!(icm instanceof InverseColorMapIndexColorModel)) {
|
|
// If possible, use faster code
|
|
icm = new InverseColorMapIndexColorModel(icm);
|
|
}
|
|
|
|
return icm;
|
|
}
|
|
|
|
/**
|
|
* Creates an {@code IndexColorModel} from the given image, using an adaptive
|
|
* palette.
|
|
*
|
|
* @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.
|
|
@SuppressWarnings("unchecked")
|
|
List<Counter>[] 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<Counter> v = colors[index];
|
|
|
|
if (v == null) {
|
|
// No colors in this bin yet so create vector and
|
|
// add color.
|
|
v = new ArrayList<Counter>();
|
|
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++;
|
|
}
|
|
|
|
System.arraycopy(cubes, j, cubes, j + 1, numberOfCubes - j);
|
|
|
|
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 InverseColorMapIndexColorModel(numOfBits, r.length, r, g, b, r.length - 1);
|
|
}
|
|
else {
|
|
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
|
|
* palette (8 bit) from the color data in the image, and uses default
|
|
* dither.
|
|
* <p>
|
|
* The image returned is a new image, the input image is not modified.
|
|
* </p>
|
|
*
|
|
* @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 <EM>not</EM>
|
|
* set.
|
|
*
|
|
* @param pHints hints
|
|
* @return true if the hint {@code COLOR_SELECTION_QUALITY}
|
|
* is <EM>not</EM> 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 palette (8 bit) from the given palette image.
|
|
* Dithering, transparency and color selection is controlled with the
|
|
* {@code pHints}parameter.
|
|
* <p>
|
|
* The image returned is a new image, the input image is not modified.
|
|
* </p>
|
|
*
|
|
* @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 hints 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
|
|
* palette with the given number of colors.
|
|
* Dithering, transparency and color selection is controlled with the
|
|
* {@code pHints} parameter.
|
|
* <p>
|
|
* The image returned is a new image, the input image is not modified.
|
|
* </p>
|
|
*
|
|
* @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 hints 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 color model, 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 palette.
|
|
* Dithering, transparency and color selection is controlled with the
|
|
* {@code pHints} parameter.
|
|
* <p>
|
|
* The image returned is a new image, the input image is not modified.
|
|
* </p>
|
|
*
|
|
* @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 transparency?
|
|
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).
|
|
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
|
|
* palette with the given number of colors.
|
|
* Dithering, transparency and color selection is controlled with the
|
|
* {@code pHints}parameter.
|
|
* <p>
|
|
* The image returned is a new image, the input image is not modified.
|
|
* </p>
|
|
*
|
|
* @param pImage the BufferedImage to index
|
|
* @param pNumberOfColors the number of colors for the image
|
|
* @param pHints hints 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 palette.
|
|
* Dithering, transparency and color selection is controlled with the
|
|
* {@code pHints}parameter.
|
|
* <p>
|
|
* The image returned is a new image, the input image is not modified.
|
|
* </p>
|
|
*
|
|
* @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 palette (8 bit) from the given palette image.
|
|
* Dithering, transparency and color selection is controlled with the
|
|
* {@code pHints}parameter.
|
|
* <p>
|
|
* The image returned is a new image, the input image is not modified.
|
|
* </p>
|
|
*
|
|
* @param pImage the BufferedImage to index
|
|
* @param pPalette the Image to read color information from
|
|
* @param pHints hints 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 transparency, 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 <integer>] [--bgcolor|-b <color>] [--colors|-c <integer> | --grayscale|g | --monochrome|-m | --palette|-p <file>] [--dither|-d (default|diffusion|none)] [--quality|-q (default|high|low)] [--transparency|-t] [--outputformat|-f (gif|jpeg|png|wbmp|...)] [--overwrite|-w] <input> [<output>]");
|
|
System.err.print("Input format names: ");
|
|
String[] readers = ImageIO.getReaderFormatNames();
|
|
|
|
for (int i = 0; i < readers.length; i++) {
|
|
System.err.print(readers[i] + ((i + 1 < readers.length)
|
|
? ", "
|
|
: "\n"));
|
|
}
|
|
|
|
System.err.print("Output format names: ");
|
|
String[] writers = ImageIO.getWriterFormatNames();
|
|
|
|
for (int i = 0; i < writers.length; i++) {
|
|
System.err.print(writers[i] + ((i + 1 < writers.length)
|
|
? ", "
|
|
: "\n"));
|
|
}
|
|
System.exit(5);
|
|
}
|
|
|
|
// Read in image
|
|
File in = new File(pArgs[argIdx++]);
|
|
|
|
if (!in.exists()) {
|
|
System.err.println("File \"" + in.getAbsolutePath() + "\" does not exist!");
|
|
System.exit(5);
|
|
}
|
|
|
|
// Read palette if needed
|
|
File paletteFile = null;
|
|
|
|
if (paletteFileName != null) {
|
|
paletteFile = new File(paletteFileName);
|
|
if (!paletteFile.exists()) {
|
|
System.err.println("File \"" + in.getAbsolutePath() + "\" does not exist!");
|
|
System.exit(5);
|
|
}
|
|
}
|
|
|
|
// Make sure we can write
|
|
File out;
|
|
|
|
if (argIdx < pArgs.length) {
|
|
out = new File(pArgs[argIdx/*++*/]);
|
|
|
|
// Get format from file extension
|
|
if (format == null) {
|
|
format = FileUtil.getExtension(out);
|
|
}
|
|
}
|
|
else {
|
|
// Create new file in current dir, same name + format extension
|
|
String baseName = FileUtil.getBasename(in);
|
|
|
|
// Use png as default format
|
|
if (format == null) {
|
|
format = "png";
|
|
}
|
|
out = new File(baseName + '.' + format);
|
|
}
|
|
|
|
if (!overWrite && out.exists()) {
|
|
System.err.println("The file \"" + out.getAbsolutePath() + "\" allready exists!");
|
|
System.exit(5);
|
|
}
|
|
|
|
// Do the image processing
|
|
BufferedImage image = null;
|
|
BufferedImage paletteImg = null;
|
|
|
|
try {
|
|
image = ImageIO.read(in);
|
|
if (image == null) {
|
|
System.err.println("No reader for image: \"" + in.getAbsolutePath() + "\"!");
|
|
System.exit(5);
|
|
}
|
|
if (paletteFile != null) {
|
|
paletteImg = ImageIO.read(paletteFile);
|
|
if (paletteImg == null) {
|
|
System.err.println("No reader for image: \"" + paletteFile.getAbsolutePath() + "\"!");
|
|
System.exit(5);
|
|
}
|
|
}
|
|
}
|
|
catch (IOException ioe) {
|
|
ioe.printStackTrace(System.err);
|
|
System.exit(5);
|
|
}
|
|
|
|
// Create hints
|
|
int hints = DITHER_DEFAULT;
|
|
|
|
if ("DIFFUSION".equalsIgnoreCase(dither)) {
|
|
hints |= DITHER_DIFFUSION;
|
|
}
|
|
else if ("DIFFUSION_ALTSCANS".equalsIgnoreCase(dither)) {
|
|
hints |= DITHER_DIFFUSION_ALTSCANS;
|
|
}
|
|
else if ("NONE".equalsIgnoreCase(dither)) {
|
|
hints |= DITHER_NONE;
|
|
}
|
|
else {
|
|
|
|
// Don't care, use default
|
|
}
|
|
if ("HIGH".equalsIgnoreCase(quality)) {
|
|
hints |= COLOR_SELECTION_QUALITY;
|
|
}
|
|
else if ("LOW".equalsIgnoreCase(quality)) {
|
|
hints |= COLOR_SELECTION_FAST;
|
|
}
|
|
else {
|
|
|
|
// Don't care, use default
|
|
}
|
|
if (transparency) {
|
|
hints |= TRANSPARENCY_BITMASK;
|
|
}
|
|
|
|
//////////////////////////////
|
|
// Apply bg-color WORKAROUND!
|
|
// This needs to be done BEFORE palette creation to have desired effect..
|
|
if ((background != null) && (paletteImg == null)) {
|
|
paletteImg = createSolid(image, background);
|
|
}
|
|
|
|
///////////////////////////////
|
|
// Index
|
|
long start = 0;
|
|
|
|
if (speedTest > 0) {
|
|
// SPEED TESTING
|
|
System.out.println("Measuring speed!");
|
|
start = System.currentTimeMillis();
|
|
// END SPEED TESTING
|
|
}
|
|
|
|
BufferedImage indexed;
|
|
IndexColorModel colors;
|
|
|
|
if (monochrome) {
|
|
indexed = getIndexedImage(image, MonochromeColorModel.getInstance(), background, hints);
|
|
colors = MonochromeColorModel.getInstance();
|
|
}
|
|
else if (gray) {
|
|
//indexed = ImageUtil.toBuffered(ImageUtil.grayscale(image), BufferedImage.TYPE_BYTE_GRAY);
|
|
image = ImageUtil.toBuffered(ImageUtil.grayscale(image));
|
|
indexed = getIndexedImage(image, colors = getIndexColorModel(image, numColors, hints), background, hints);
|
|
|
|
// In casse of speedtest, this makes sense...
|
|
if (speedTest > 0) {
|
|
colors = getIndexColorModel(indexed, numColors, hints);
|
|
}
|
|
}
|
|
else if (paletteImg != null) {
|
|
// Get palette from image
|
|
indexed = getIndexedImage(ImageUtil.toBuffered(image, BufferedImage.TYPE_INT_ARGB),
|
|
colors = getIndexColorModel(paletteImg, numColors, hints), background, hints);
|
|
}
|
|
else {
|
|
image = ImageUtil.toBuffered(image, BufferedImage.TYPE_INT_ARGB);
|
|
indexed = getIndexedImage(image, colors = getIndexColorModel(image, numColors, hints), background, hints);
|
|
}
|
|
|
|
if (speedTest > 0) {
|
|
// SPEED TESTING
|
|
System.out.println("Color selection + dither: " + (System.currentTimeMillis() - start) + " ms");
|
|
// END SPEED TESTING
|
|
}
|
|
|
|
// Write output (in given format)
|
|
try {
|
|
if (!ImageIO.write(indexed, format, out)) {
|
|
System.err.println("No writer for format: \"" + format + "\"!");
|
|
}
|
|
}
|
|
catch (IOException ioe) {
|
|
ioe.printStackTrace(System.err);
|
|
}
|
|
|
|
if (speedTest > 0) {
|
|
// SPEED TESTING
|
|
System.out.println("Measuring speed!");
|
|
|
|
// Warmup!
|
|
for (int i = 0; i < 10; i++) {
|
|
getIndexedImage(image, colors, background, hints);
|
|
}
|
|
|
|
// Measure
|
|
long time = 0;
|
|
|
|
for (int i = 0; i < speedTest; i++) {
|
|
start = System.currentTimeMillis();
|
|
getIndexedImage(image, colors, background, hints);
|
|
time += (System.currentTimeMillis() - start);
|
|
System.out.print('.');
|
|
if ((i + 1) % 10 == 0) {
|
|
System.out.println("\nAverage (after " + (i + 1) + " iterations): " + (time / (i + 1)) + "ms");
|
|
}
|
|
}
|
|
|
|
System.out.println("\nDither only:");
|
|
System.out.println("Total time (" + speedTest + " invocations): " + time + "ms");
|
|
System.out.println("Average: " + time / speedTest + "ms");
|
|
// END SPEED TESTING
|
|
}
|
|
}
|
|
} |