/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2001-2012, Open Source Geospatial Foundation (OSGeo) * (C) 2009-2012, Geomatys * * 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.geotoolkit.image.color; import java.util.Arrays; import java.util.Locale; import java.awt.Color; import java.awt.color.ColorSpace; import java.awt.image.DataBuffer; import java.awt.image.ColorModel; import java.awt.image.IndexColorModel; import static java.awt.image.DataBuffer.*; import org.geotoolkit.lang.Static; /** * 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. * * @author Martin Desruisseaux (IRD, Geomatys) * @author Simone Giannecchini (Geosolutions) * @version 3.14 * * @since 1.2 * @module */ public final class ColorUtilities extends Static { /** * Small number for rounding errors. */ private static final double EPS = 1E-6; /** * An index color model having only two colors, black and white. */ public static final IndexColorModel BINARY_COLOR_MODEL = new IndexColorModel( 1, 2, new int[] {0, -1}, 0, false, -1, TYPE_BYTE); /** * 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. * @return The RGB color from the given components. */ public static int getIntFromColor(int r, int g, int b, int a) { return ((a & 0xFF) << 24) | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF); } /** * Returns the color components in an array of {@code double} values. * This is mostly for usage with JAI operators which require sample values * as {@code double} no matter the actual image data type. * * @param color The color for which to extract the component values. * @param numBands The length of the array to be returned. * If greater than 4, then the extra values will be initialized to 0. * @return The color components in an array of the given length. */ @SuppressWarnings("fallthrough") public static double[] toDoubleValues(final Color color, final int numBands) { final double[] values = new double[numBands]; switch (numBands) { default: values[3] = color.getAlpha(); case 3: values[2] = color.getBlue(); case 2: values[1] = color.getGreen(); case 1: values[0] = color.getRed(); case 0: break; } return values; } /** * Returns a string representation of the given color. * This method omits the alpha component if the color is opaque. * * @param color The color. * @return A string representation of the given color. * * @since 3.14 */ public static String toString(final Color color) { int ARGB = color.getRGB(); final boolean isOpaque = (ARGB & 0xFF000000) == 0xFF000000; int size; if (isOpaque) { ARGB &= 0xFFFFFF; size = 6; } else { size = 8; } final String code = Integer.toHexString(ARGB).toUpperCase(Locale.US); size -= code.length(); final StringBuilder buffer = new StringBuilder(); buffer.append('#'); while (--size >= 0) { buffer.append('0'); } return buffer.append(code).toString(); } /** * 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 completely out of range, or if * they would result in an empty array, then {@code null} is returned. * <p> * This method is used by {@link org.geotoolkit.coverage.GridSampleDimension} 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) { return Arrays.copyOfRange(palette, lower, upper); } } 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 * equal to the length of {@code colors} array, then colors will be interpolated. * * {@note 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. * * @param value The value to round. * @return The rounded and clamped value. */ 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, -1); } /** * Returns a tolerant index color model for the specified ARGB code. This color model accept * image with the specified number of bands. * <p> * This methods caches previously created instances using weak references, because index * color model may be big (up to 256 kb). * * @param ARGB An array of ARGB values. * @param numBands The number of bands. * @param visibleBand The band to display. * @param transparent The transparent pixel, or -1 for auto-detection. * @return An index color model for the specified array. */ public static IndexColorModel getIndexColorModel(final int[] ARGB, final int numBands, final int visibleBand, int transparent) { // No needs to scan the ARGB values in search of a transparent pixel; // the IndexColorModel constructor does that for us. final int length = ARGB.length; final int bits = getBitCount(length); final int type = getTransferType(length); final IndexColorModel cm; if (numBands == 1) { cm = new IndexColorModel(bits, length, ARGB, 0, true, transparent, type); } else { cm = new MultiBandsIndexColorModel(bits, length, ARGB, 0, true, transparent, type, numBands, visibleBand); } return ColorModels.unique(cm); } /** * Returns a bit count for an {@link IndexColorModel} mapping {@code mapSize} colors. * It is guaranteed that the following relation is hold: * * {@preformat java * (1 << getBitCount(mapSize)) >= mapSize * } * * @param mapSize The number of colors in the map. * @return The number of bits to use. */ public static int getBitCount(final int mapSize) { final int count = Math.max(1, 32 - Integer.numberOfLeadingZeros(mapSize - 1)); 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}. * * @param mapSize The number of colors in the map. * @return The suggested transfer type. */ public static int getTransferType(final int mapSize) { return (mapSize <= 256) ? TYPE_BYTE : TYPE_USHORT; } /** * Transforms a color from XYZ color space to LAB. The color are transformed * in place. This method returns {@code color} for convenience. * <p> * Reference: http://www.brucelindbloom.com/index.html?ColorDifferenceCalc.html * * @param color The XYZ color to convert. * @return The LAB color. */ 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. * <p> * Reference: http://www.brucelindbloom.com/index.html?ColorDifferenceCalc.html * * @param lab1 The first LAB color. * @param lab2 The second LAB color. * @return The CIE94 distance between the two supplied colors. */ public static float colorDistance(final float[] lab1, final float[] lab2) { double sum; if (false) { // Computes 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); sum = sL*sL + sC*sC + sH*sH; } else { // Computes distance using delta E formula. 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 recommended 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. * * @param model The color model for which to guess the number of bands. * @return The number of bands in the given color model. */ 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 if a specific {@link IndexColorModel} contains only gray color, * ignoring alpha information. * * @param model index color model 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 model, boolean ignoreTransparents) { if (!model.hasAlpha()) { // We will not check transparent pixels if there is none in the color model. ignoreTransparents = false; } final int mapSize = model.getMapSize(); for (int i=0; i<mapSize; i++) { if (ignoreTransparents) { // If this entry is transparent and we were asked // to ignore fully transparents pixels, let's leave. if (model.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 = model.getGreen(i); if (green != model.getRed(i) || green != model.getBlue(i)) { return false; } } return true; } }