/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2001-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.resources.image;
import java.awt.Color;
import java.awt.color.ColorSpace;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.IndexColorModel;
import java.util.Arrays;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.i18n.Errors;
/**
* A set of static methods for handling of colors informations. Some of those methods
* are useful, but not really rigorous. This is why they do not appear in any "official"
* package, but instead in this private one.
*
* <strong>Do not rely on this API!</strong>
*
* It may change in incompatible way in any future version.
*
* @since 2.0
*
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux (IRD)
* @author Simone Giannecchini
*/
public final class ColorUtilities {
/**
* Small number for rounding errors.
*/
private static final double EPS = 1E-6;
/**
* Do not allow creation of instances of this class.
*/
private ColorUtilities() {
}
/**
* Creates an sRGB color with the specified red, green, blue, and alpha
* values in the range (0 - 255).
*
* @param r the red component
* @param g the green component
* @param b the blue component
* @param a the alpha component
* @throws IllegalArgumentException if {@coder}, {@code g}, {@code b}
* or {@code a} are outside of the range 0 to 255, inclusive.
*/
public static int getIntFromColor(int r, int g, int b, int a) {
return ((a & 0xFF) << 24) |
((r & 0xFF) << 16) |
((g & 0xFF) << 8) |
((b & 0xFF) << 0);
}
/**
* Returns a subarray of the specified color array. The {@code lower} and
* {@code upper} index will be clamb into the {@code palette} range.
* If they are completly out of range, or if they would result in an empty array,
* then {@code null} is returned.
*
* This method is used by {@link org.geotools.cv.SampleDimension} as an heuristic
* approach for distributing palette colors into a list of categories.
*
* @param palette The color array (may be {@code null}).
* @param lower The lower index, inclusive.
* @param upper The upper index, inclusive.
* @return The subarray (may be {@code palette} if the original array already fit),
* or {@code null} if the {@code lower} and {@code upper} index
* are out of {@code palette} bounds.
*/
public static Color[] subarray(final Color[] palette, int lower, int upper) {
if (palette != null) {
lower = Math.max(lower, 0);
upper = Math.min(upper, palette.length);
if (lower >= upper) {
return null;
}
if (lower!=0 || upper!=palette.length) {
final Color[] sub = new Color[upper-lower];
System.arraycopy(palette, lower, sub, 0, sub.length);
return sub;
}
}
return palette;
}
/**
* Copies {@code colors} into array {@code ARGB} from index {@code lower}
* inclusive to index {@code upper} exclusive. If {@code upper-lower} is not
* equals to the length of {@code colors} array, then colors will be interpolated.
* <p>
* <b>Note:</b> Profiling shows that this method is a "hot spot". It needs to be fast,
* which is why the implementation is not as straight-forward as it could.
*
* @param colors Colors to copy into the {@code ARGB} array.
* @param ARGB Array of integer to write ARGB values to.
* @param lower Index (inclusive) of the first element of {@code ARGB} to change.
* @param upper Index (exclusive) of the last element of {@code ARGB} to change.
*/
@SuppressWarnings("fallthrough")
public static void expand(final Color[] colors, final int[] ARGB,
final int lower, final int upper)
{
/*
* Trivial cases.
*/
switch (colors.length) {
case 1: Arrays.fill(ARGB, lower, upper, colors[0].getRGB()); // fall through
case 0: return; // Note: getRGB() is really getARGB()
}
switch (upper - lower) {
case 1: ARGB[lower] = colors[0].getRGB(); // fall through
case 0: return; // Note: getRGB() is really getARGB()
}
/*
* Prepares the coefficients for the iteration.
* The non-final ones will be updated inside the loop.
*/
final double scale = (double)(colors.length - 1) / (double)(upper - 1 - lower);
final int maxBase = colors.length - 2;
double index = 0;
int base = 0;
for (int i=lower;;) {
final int C0 = colors[base + 0].getRGB();
final int C1 = colors[base + 1].getRGB();
final int A0 = (C0 >>> 24) & 0xFF, A1 = ((C1 >>> 24) & 0xFF) - A0;
final int R0 = (C0 >>> 16) & 0xFF, R1 = ((C1 >>> 16) & 0xFF) - R0;
final int G0 = (C0 >>> 8) & 0xFF, G1 = ((C1 >>> 8) & 0xFF) - G0;
final int B0 = (C0 ) & 0xFF, B1 = ((C1 ) & 0xFF) - B0;
final int oldBase = base;
do {
final double delta = index - base;
ARGB[i] = (roundByte(A0 + delta*A1) << 24) |
(roundByte(R0 + delta*R1) << 16) |
(roundByte(G0 + delta*G1) << 8) |
(roundByte(B0 + delta*B1));
if (++i == upper) {
return;
}
index = (i - lower) * scale;
base = Math.min(maxBase, (int)(index + EPS)); // Really want rounding toward 0.
} while (base == oldBase);
}
}
/**
* Rounds a float value and clamp the result between 0 and 255 inclusive.
*/
public static int roundByte(final double value) {
return (int) Math.min(Math.max(Math.round(value), 0), 255);
}
/**
* Returns an index color model for specified ARGB codes. If the specified
* array has not transparent color (i.e. all alpha values are 255), then the
* returned color model will be opaque. Otherwise, if the specified array has
* one and only one color with alpha value of 0, the returned color model will
* have only this transparent color. Otherwise, the returned color model will
* be translucent.
*
* @param ARGB An array of ARGB values.
* @return An index color model for the specified array.
*/
public static IndexColorModel getIndexColorModel(final int[] ARGB) {
return getIndexColorModel(ARGB, 1, 0);
}
/**
* Returns a tolerant index color model for the specified ARGB code. This color model accept
* image with the specified number of bands.
*
* @param ARGB An array of ARGB values.
* @param numBands The number of bands.
* @param visibleBand The band to display.
* @return An index color model for the specified array.
*
* @todo Considerer caching previously created instances using weak references. Index color
* model may be big (up to 256 kb), so it may be worth to cache big instances. NOTE:
* IndexColorModel inherits a equals(Object) implementation from ColorModel, but do
* not override it, so the definition is incomplete.
*/
public static IndexColorModel getIndexColorModel(final int[] ARGB,
final int numBands,
final int visibleBand)
{
boolean hasAlpha = false;
int transparent = -1;
final int length = ARGB.length;
for (int i=0; i<length; i++) {
final int alpha = (ARGB[i] & 0xFF000000);
if (alpha != 0xFF000000) {
if (alpha == 0x00000000 && transparent < 0) {
transparent = i;
continue;
}
hasAlpha = true;
break;
}
}
final int bits = getBitCount(length);
final int type = getTransferType(length);
if (numBands == 1) {
return new IndexColorModel(bits, length, ARGB, 0, hasAlpha, transparent, type);
} else {
return new MultiBandsIndexColorModel(bits, length, ARGB, 0, hasAlpha, transparent,
type, numBands, visibleBand);
}
}
/**
* Returns a bit count for an {@link IndexColorModel} mapping {@code mapSize} colors.
* It is guaranteed that the following relation is hold:
*
* <center><pre>(1 << getBitCount(mapSize)) >= mapSize</pre></center>
*/
public static int getBitCount(final int mapSize) {
int max = mapSize - 1;
if (max <= 1) {
return 1;
}
int count = 0;
do {
count++;
max >>= 1;
} while (max != 0);
assert (1 << count) >= mapSize : mapSize;
assert (1 << (count-1)) < mapSize : mapSize;
return count;
}
/**
* Returns a suggered type for an {@link IndexColorModel}
* of {@code mapSize} colors. This method returns
* {@link DataBuffer#TYPE_BYTE} or {@link DataBuffer#TYPE_USHORT}.
*/
public static int getTransferType(final int mapSize) {
return (mapSize <= 256) ? DataBuffer.TYPE_BYTE : DataBuffer.TYPE_USHORT;
}
/**
* Transforms a color from XYZ color space to LAB. The color are transformed
* in place. This method returns {@code color} for convenience.
* Reference: http://www.brucelindbloom.com/index.html?ColorDifferenceCalc.html
*/
public static float[] XYZtoLAB(final float[] color) {
color[0] /= 0.9642; // Other refeference: 0.95047;
color[1] /= 1.0000; // 1.00000;
color[2] /= 0.8249; // 1.08883;
for (int i=0; i<3; i++) {
final float c = color[i];
color[i] = (float)((c > 216/24389f) ? Math.pow(c, 1.0/3) : ((24389/27.0)*c + 16)/116);
}
final float L = 116 * color[1] - 16;
final float a = 500 * (color[0] - color[1]);
final float b = 200 * (color[1] - color[2]);
assert !Float.isNaN(L) && !Float.isNaN(a) && !Float.isNaN(b);
color[0] = L;
color[1] = a;
color[2] = b;
return color;
}
/**
* Computes the distance E (CIE 1994) between two colors in LAB color space.
* Reference: http://www.brucelindbloom.com/index.html?ColorDifferenceCalc.html
*/
public static float colorDistance(final float[] lab1, final float[] lab2) {
// if (false) {
// // Compute distance using CIE94 formula.
// // NOTE: this formula sometime fails because of negative
// // value in the first Math.sqrt(...) expression.
// final double dL = (double)lab1[0] - lab2[0];
// final double da = (double)lab1[1] - lab2[1];
// final double db = (double)lab1[2] - lab2[2];
// final double C1 = Math.hypot(lab1[1], lab1[2]);
// final double C2 = Math.hypot(lab2[1], lab2[2]);
// final double dC = C1 - C2;
// final double dH = Math.sqrt(da*da + db*db - dC*dC);
// final double sL = dL / 2;
// final double sC = dC / (1 + 0.048*C1);
// final double sH = dH / (1 + 0.014*C1);
// return (float)Math.sqrt(sL*sL + sC*sC + sH*sH);
// } else {
// Compute distance using delta E formula.
double sum = 0;
for (int i=Math.min(lab1.length, lab2.length); --i>=0;) {
final double delta = lab1[i] - lab2[i];
sum += delta*delta;
}
return (float)Math.sqrt(sum);
// }
}
/**
* Returns the most transparent pixel in the specified color model. If many colors has
* the same alpha value, than the darkest one is returned. This method never returns
* a negative value (0 is returned if the color model has no colors).
*
* @param colors The color model in which to look for a transparent color.
* @return The index of a transparent color, or 0.
*/
public static int getTransparentPixel(final IndexColorModel colors) {
int index = colors.getTransparentPixel();
if (index < 0) {
index = 0;
int alpha = Integer.MAX_VALUE;
float delta = Float.POSITIVE_INFINITY;
final ColorSpace space = colors.getColorSpace();
final float[] RGB = new float[3];
final float[] BLACK = XYZtoLAB(space.toCIEXYZ(RGB)); // Black in Lab color space.
assert BLACK != RGB;
for (int i=colors.getMapSize(); --i>=0;) {
final int a = colors.getAlpha(i);
if (a <= alpha) {
RGB[0] = colors.getRed (i)/255f;
RGB[1] = colors.getGreen(i)/255f;
RGB[2] = colors.getBlue (i)/255f;
final float d = colorDistance(XYZtoLAB(space.toCIEXYZ(RGB)), BLACK);
assert d >= 0 : i; // Check mostly for NaN value
if (a<alpha || d<delta) {
alpha = a;
delta = d;
index = i;
}
}
}
}
return index;
}
/**
* Returns the index of the specified color, excluding the specified one. If the color
* is not explicitly found, a close color is returned. This method never returns a negative
* value (0 is returned if the color model has no colors).
*
* @param colors The color model in which to look for a color index.
* @param color The color to search for.
* @param exclude An index to exclude from the search (usually the background or the
* {@linkplain #getTransparentPixel transparent} pixel), or -1 if none.
* @return The index of the color, or 0.
*/
public static int getColorIndex(final IndexColorModel colors,
final Color color,
final int exclude)
{
final ColorSpace space = colors.getColorSpace();
final float[] RGB = {
color.getRed() /255f,
color.getGreen()/255f,
color.getBlue() /255f
};
final float[] REF = XYZtoLAB(space.toCIEXYZ(RGB));
float delta = Float.POSITIVE_INFINITY;
int index = 0;
assert REF != RGB;
for (int i=colors.getMapSize(); --i>=0;) {
if (i != exclude) {
RGB[0] = colors.getRed (i)/255f;
RGB[1] = colors.getGreen(i)/255f;
RGB[2] = colors.getBlue (i)/255f;
final float d = colorDistance(XYZtoLAB(space.toCIEXYZ(RGB)), REF);
assert d >= 0 : i; // Check mostly for NaN value
if (d <= delta) {
delta = d;
index = i;
}
}
}
return index;
}
/**
* Tries to guess the number of bands from the specified color model. The recommanded approach
* is to invoke {@link java.awt.image.SampleModel#getNumBands}. This method should be used only
* as a fallback when the sample model is not available. This method uses some heuristic rules
* for guessing the number of bands, so the return value may not be exact in all cases.
*/
public static int getNumBands(final ColorModel model) {
if (model instanceof IndexColorModel) {
if (model instanceof MultiBandsIndexColorModel) {
return ((MultiBandsIndexColorModel) model).numBands;
}
return 1;
}
return model.getNumComponents();
}
/**
* Tells us if a specific {@link IndexColorModel} contains only gray color
* or not, ignoring alpha information.
*
* @param icm {@link IndexColorModel} to be inspected.
* @param ignoreTransparents {@code true} if the RGB values of fully transparent pixels
* (the ones with an {@linkplain IndexColorModel#getAlpha(int) alpha} value of 0)
* should not be taken in account during the check for gray color.
* @return {@code true} if the palette is grayscale, {@code false} otherwise.
*/
public static boolean isGrayPalette(final IndexColorModel icm, boolean ignoreTransparents) {
if (!icm.hasAlpha()) {
// We will not check transparent pixels if there is none in the color model.
ignoreTransparents = false;
}
final int mapSize = icm.getMapSize();
for (int i=0; i<mapSize; i++) {
if (ignoreTransparents) {
// If this entry is transparent and we were asked
// to check transparents pixels, let's leave.
if (icm.getAlpha(i) == 0) {
continue;
}
}
// Get the color for this pixel only if it is requested.
// If gray, all components are the same.
final int green = icm.getGreen(i);
if (green != icm.getRed(i) || green != icm.getBlue(i)) {
return false;
}
}
return true;
}
/**
* Provide the minimum allowe value for a certain data type.
* @param dataType the data type to suggest a maximum value for.
* @return the data type maximum value for.
*/
public static double getMinimum(int dataType) {
switch (dataType) {
case DataBuffer.TYPE_BYTE:case DataBuffer.TYPE_USHORT:
return 0;
case DataBuffer.TYPE_SHORT:
return Short.MIN_VALUE;
case DataBuffer.TYPE_INT:
return Integer.MIN_VALUE;
case DataBuffer.TYPE_DOUBLE:
return Long.MIN_VALUE;
case DataBuffer.TYPE_FLOAT:
return -Float.MAX_VALUE;
default:
throw new IllegalArgumentException(Errors.format(ErrorKeys.ILLEGAL_ARGUMENT_$2,"DataType unknown:",dataType));
}
}
/**
* Returns a suitable threshold depending on the {@link DataBuffer} type.
*
* <p>
* Remember that the threshold works with >=.
*
* @param dataType
* to create a low threshold for.
* @return a minimum threshold value suitable for this data type.
*/
public static double getThreshold(int dataType) {
switch (dataType) {
case DataBuffer.TYPE_BYTE:
case DataBuffer.TYPE_USHORT:
// this may cause problems and truncations when the native mosaic
// operations is enabled
return 0.0;
case DataBuffer.TYPE_INT:
return Integer.MIN_VALUE;
case DataBuffer.TYPE_SHORT:
return Short.MIN_VALUE;
case DataBuffer.TYPE_DOUBLE:
return -Double.MAX_VALUE;
case DataBuffer.TYPE_FLOAT:
return -Float.MAX_VALUE;
}
return 0;
}
/**
* Looks for the specified color in the color model
* @param bgColor The color to be searched
* @param icm The color model to be searched into
* @return The index of the color in the color model, or -1 if not found
*/
public static int findColorIndex(Color bgColor, IndexColorModel icm) {
if(bgColor== null)
throw new NullPointerException((Errors.format(ErrorKeys.NULL_ARGUMENT_$1,"bgColor")));
if(icm== null)
throw new NullPointerException((Errors.format(ErrorKeys.NULL_ARGUMENT_$1,"icm")));
final int r = bgColor.getRed();
final int g = bgColor.getGreen();
final int b = bgColor.getBlue();
final int a = bgColor.getAlpha();
final int size = icm.getMapSize();
for(int i = 0; i < size; i++) {
if(r == icm.getRed(i) &&
g == icm.getGreen(i) &&
b == icm.getBlue(i) &&
(a == icm.getAlpha(i) || !icm.hasAlpha()))
return i;
}
return -1;
}
/**
* Provide the maximum allowe value for a certain data type.
* @param dataType the data type to suggest a maximum value for.
* @return the data type maximum value for.
*/
public static double getMaximum(int dataType) {
switch (dataType) {
case DataBuffer.TYPE_BYTE:
return 255;
case DataBuffer.TYPE_SHORT:
return Short.MAX_VALUE;
case DataBuffer.TYPE_USHORT:
return 65535;
case DataBuffer.TYPE_INT:
return Integer.MAX_VALUE;
case DataBuffer.TYPE_DOUBLE:
return Long.MAX_VALUE;
case DataBuffer.TYPE_FLOAT:
return Float.MAX_VALUE;
default:
throw new IllegalArgumentException(Errors.format(ErrorKeys.ILLEGAL_ARGUMENT_$2,"DataType unknown:",dataType));
}
}
}