/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2005-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.palette;
import java.awt.Color;
import java.awt.color.ColorSpace;
import java.awt.image.*;
import java.io.IOException;
import java.io.FileNotFoundException;
import javax.imageio.IIOException;
import javax.imageio.ImageTypeSpecifier;
import org.geotoolkit.resources.Errors;
import org.geotoolkit.image.color.ColorUtilities;
/**
* A set of RGB colors created by a {@linkplain PaletteFactory palette factory} from a name.
* A palette can creates an {@linkplain IndexColorModel index color model} or an {@linkplain
* ImageTypeSpecifier image type specifier} from the RGB colors.
*
* @author Antoine Hnawia (IRD)
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 3.11
*
* @since 3.11 (derived from 2.4)
* @module
*/
public final class IndexedPalette extends Palette {
/**
* The maximal allowed value, corresponding to the maximum value for unsigned 16 bits integer.
* DO NOT EDIT: this value <strong>MUST</strong> be {@code 0xFFFF}.
*/
public static final int MAX_UNSIGNED = 0xFFFF;
/**
* Index of the first valid element (inclusive) in the {@linkplain IndexColorModel
* index color model} to be created. Pixels in the range 0 inclusive to {@code lower}
* exclusive will be reserved for "no data" values.
* <p>
* Strictly speaking, this index should be non-negative because {@link IndexColorModel}
* does not support negative index. However this {@code Palette} implementation accepts
* negative values provided that {@link #upper} is not greater than {@value Short#MAX_VALUE}.
* If this condition holds, then {@code Palette} will transpose negative values as positive
* values in the range {@code 0x80000} to {@code 0xFFFF} inclusive. Be aware that such
* approach consume the maximal amount of memory, i.e. 256 kilobytes for each color model.
*
* @see SpatialImageReader#getRawDataType(int)
*/
protected final int lower;
/**
* Index of the last valid element (exclusive) in the {@linkplain IndexColorModel
* index color model} to be created. Pixels in the range {@code upper} inclusive
* to {@link #size} exclusive will be reserved for "no data" values. This value
* is always greater than {@link #lower} (note that it may be negative).
*/
protected final int upper;
/**
* The size of the {@linkplain IndexColorModel index color model} to be created.
* This is the value to be returned by {@link IndexColorModel#getMapSize()}. This
* value is always positive.
*/
protected final int size;
/**
* Creates a palette with the specified name and size. The RGB colors will be distributed
* in the range {@code lower} inclusive to {@code upper} exclusive. Remaining pixel values
* (if any) will be left to a black or transparent color by default.
*
* @param factory The originating factory.
* @param name The palette name.
* @param lower Index of the first valid element (inclusive) in the
* {@linkplain IndexColorModel index color model} to be created.
* @param upper Index of the last valid element (exclusive) in the
* {@linkplain IndexColorModel index color model} to be created.
* @param size The size of the {@linkplain IndexColorModel index color model} to be created.
* This is the value to be returned by {@link IndexColorModel#getMapSize()}.
* @param numBands The number of bands (usually 1).
* @param visibleBand The band to use for color computations (usually 0).
*/
protected IndexedPalette(final PaletteFactory factory, final String name, final int lower,
final int upper, int size, final int numBands, final int visibleBand)
{
super(factory, name, numBands, visibleBand);
final int minAllowed, maxAllowed; // inclusives
if (lower < 0) {
minAllowed = Short.MIN_VALUE;
maxAllowed = Short.MAX_VALUE;
size = (size <= 0x100) ? 0x100 : (MAX_UNSIGNED + 1);
// 'size-1' must be FF or FFFF in order to rool negative values.
} else {
minAllowed = 0;
maxAllowed = MAX_UNSIGNED;
}
ensureInsideBounds(lower, minAllowed, maxAllowed);
ensureInsideBounds(upper, minAllowed, maxAllowed + 1);
ensureInsideBounds(size, upper, MAX_UNSIGNED + 1);
if (lower >= upper) {
throw new IllegalArgumentException(factory.getErrorResources().getString(
Errors.Keys.IllegalRange_2, lower, upper));
}
this.lower = lower;
this.upper = upper;
this.size = size;
}
/**
* Returns the scale from <cite>normalized values</cite> (values in the range [0..1])
* to values in the range of this palette.
*/
@Override
final double getScale() {
return upper - lower;
}
/**
* Returns the offset from <cite>normalized values</cite> (values in the range [0..1])
* to values in the range of this palette.
*/
@Override
final double getOffset() {
return lower;
}
/**
* Creates and returns ARGB values for the {@linkplain IndexColorModel index color model} to be
* created. This method is invoked automatically the first time the color model is required, or
* when it need to be rebuilt.
*
* @throws FileNotFoundException If the RGB values need to be read from a file and this file
* (typically inferred from {@link #name}) is not found.
* @throws IOException If an other find of I/O error occurred.
* @throws IIOException If an other kind of error prevent this method to complete.
*/
private int[] createARGB() throws IOException {
final Color[] colors = factory.getColors(name);
if (colors == null) {
throw new FileNotFoundException(factory.getErrorResources().getString(
Errors.Keys.FileDoesNotExist_1, name));
}
final int[] ARGB = new int[size];
if (lower >= 0) {
ColorUtilities.expand(colors, ARGB, lower, upper);
} else {
ColorUtilities.expand(colors, ARGB, 0, upper - lower);
final int negativeStart = size + lower;
final int negativeCount = -lower;
final int[] negatives = new int[negativeCount];
System.arraycopy(ARGB, 0, negatives, 0, negativeCount);
System.arraycopy(ARGB, negativeCount, ARGB, 0, negativeStart);
System.arraycopy(negatives, 0, ARGB, negativeStart, negativeCount);
}
return ARGB;
}
/**
* Tells if the given ARGB array contains only opaque gray colors, with values
* matching the index value.
*
* @param ARGB The colors to be inspected.
* @return {@code true} if the palette is grayscale, {@code false} otherwise.
*/
public static boolean isGrayPalette(final int[] ARGB) {
final boolean shift = (ARGB.length >= 0x100);
for (int i=0; i<ARGB.length; i++) {
int code = ARGB[i];
if ((code & 0xFF000000) != 0xFF000000) {
return false; // Non-opaque color.
}
int expected = i;
if (shift) {
expected >>>= 8;
}
if (((code ) & 0xFF) != expected ||
((code >>> 8) & 0xFF) != expected ||
((code >>> 16) & 0xFF) != expected)
{
return false;
}
}
return true;
}
/**
* Creates the image type specifier for this palette. This method tries to reuse existing
* color model if possible, since it may consume a significant amount of memory.
*
* @throws IOException If the RGB values can not be read from the file.
*/
@Override
protected ImageTypeSpecifier createImageTypeSpecifier() throws IOException {
final int[] ARGB = createARGB();
final int bits = ColorUtilities.getBitCount(ARGB.length);
final int type = (bits <= 8) ? DataBuffer.TYPE_BYTE : DataBuffer.TYPE_USHORT;
final boolean packed = (bits==1 || bits==2 || bits==4);
final boolean dense = (packed || bits==8 || bits==16);
final boolean isGray = isGrayPalette(ARGB);
if (!isGray && dense && (1 << bits) == ARGB.length && numBands == 1) {
final byte[] A = new byte[ARGB.length];
final byte[] R = new byte[ARGB.length];
final byte[] G = new byte[ARGB.length];
final byte[] B = new byte[ARGB.length];
for (int i=0; i<ARGB.length; i++) {
int code = ARGB[i];
B[i] = (byte) ((code ) & 0xFF);
G[i] = (byte) ((code >>>= 8) & 0xFF);
R[i] = (byte) ((code >>>= 8) & 0xFF);
A[i] = (byte) ((code >>>= 8) & 0xFF);
}
return ImageTypeSpecifier.createIndexed(R,G,B,A, bits, type);
}
/*
* The "ImageTypeSpecifier.createIndexed(...)" method is too strict. The IndexColorModel
* constructor is more flexible. This block mimic the "ImageTypeSpecifier.createIndexed"
* work without the constraints imposed by "createIndexed". Being more flexible consume
* less memory for the color palette, since we don't force it to be 64 kb in the USHORT
* data type case.
*/
final ColorModel cm;
final SampleModel sm;
if (isGray) {
final ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_GRAY);
cm = new ComponentColorModel(cs, new int[] {bits}, false, true, ColorModel.OPAQUE, type);
sm = cm.createCompatibleSampleModel(1, 1);
} else {
cm = ColorUtilities.getIndexColorModel(ARGB, numBands, visibleBand, -1);
if (packed && numBands == 1) {
sm = new MultiPixelPackedSampleModel(type, 1, 1, bits);
} else {
final int[] bandOffsets = new int[numBands];
for (int i=1; i<bandOffsets.length; i++) {
bandOffsets[i] = i;
}
sm = new PixelInterleavedSampleModel(type, 1, 1, numBands, numBands, bandOffsets);
}
}
return new ImageTypeSpecifier(cm, sm);
}
/**
* Returns a hash value for this palette.
*/
@Override
public int hashCode() {
return super.hashCode() + 31*(lower + 31*(upper + 31*size));
}
/**
* Compares this palette with the specified object for equality.
*/
@Override
public boolean equals(final Object object) {
if (object == this) {
return true;
}
if (super.equals(object)) {
final IndexedPalette that = (IndexedPalette) object;
return this.lower == that.lower &&
this.upper == that.upper &&
this.size == that.size;
}
return false;
}
/**
* Returns a string representation of this palette. Used for debugging purpose only.
*/
@Override
public String toString() {
return name + " [" + lower + " \u2026 " + (upper-1) + "] size=" + size;
}
}