ColorQuantizer.java
/*
* SmartSprites Project
*
* Copyright (C) 2007-2009, Stanisław Osiński.
* 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 SmartSprites Project nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* - We kindly request that you include in the end-user documentation provided with
* the redistribution and/or in the software itself an acknowledgement equivalent
* to the following: "This product includes software developed by the SmartSprites
* Project."
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.carrot2.util;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.IndexColorModel;
import java.awt.image.WritableRaster;
import amd.Quantize;
/**
* A simple utility wrapping the {@link Quantize} class to work on {@link BufferedImage}s and handle transparency.
*/
public class ColorQuantizer {
/** Maximum number of colors in an indexed image, leaving one for transparency. */
public static final int MAX_INDEXED_COLORS = 255;
/**
* Instantiates a new color quantizer.
*/
private ColorQuantizer() {
// Prevent Instantiation
}
/**
* Quantizes the image to {@link #MAX_INDEXED_COLORS} with white matte for areas with partial transparency (full
* transparency will be preserved).
*
* @param source
* the source
*
* @return {@link BufferedImage} with type {@link BufferedImage#TYPE_BYTE_INDEXED} and quantized colors
*/
public static BufferedImage quantize(BufferedImage source) {
return quantize(source, Color.WHITE);
}
/**
* Quantizes the image to {@link #MAX_INDEXED_COLORS} with the provided matte {@link Color} for areas with partial
* transparency (full transparency will be preserved).
*
* @param source
* the source
* @param matteColor
* the matte color
*
* @return {@link BufferedImage} with type {@link BufferedImage#TYPE_BYTE_INDEXED} and quantized colors
*/
public static BufferedImage quantize(BufferedImage source, Color matteColor) {
return quantize(source, matteColor, MAX_INDEXED_COLORS);
}
/**
* Quantizes the image to the provided number of colors with the provided matte {@link Color} for areas with partial
* transparency (full transparency will be preserved).
*
* @param source
* the source
* @param matteColor
* the matte color
* @param maxColors
* the max colors
*
* @return {@link BufferedImage} with type {@link BufferedImage#TYPE_BYTE_INDEXED} and quantized colors
*/
public static BufferedImage quantize(BufferedImage source, Color matteColor, int maxColors) {
final int width = source.getWidth();
final int height = source.getHeight();
// First put the matte color so that we have a sensible result
// for images with full alpha transparencies
final BufferedImage mattedSource = BufferedImageUtils.matte(source, matteColor);
// Get two copies of RGB data (quantization will overwrite one)
final int[][] bitmap = BufferedImageUtils.getRgb(mattedSource);
// Quantize colors and shift palette by one for transparency color
// We'll keep transparency color black for now.
final int[] colors = Quantize.quantizeImage(bitmap, maxColors);
final int[] colorsWithAlpha = new int[colors.length + 1];
System.arraycopy(colors, 0, colorsWithAlpha, 1, colors.length);
colorsWithAlpha[0] = matteColor.getRGB();
final IndexColorModel colorModel = new IndexColorModel(8, colorsWithAlpha.length, colorsWithAlpha, 0, false, 0,
DataBuffer.TYPE_BYTE);
// Write the results to an indexed image, skipping the fully transparent bits
final BufferedImage quantized = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, colorModel);
final WritableRaster raster = quantized.getRaster();
final int[][] rgb = BufferedImageUtils.getRgb(source);
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
final int value = (rgb[x][y] & 0xff000000) != 0x00000000 ? bitmap[x][y] + 1 : 0;
raster.setPixel(x, y, new int[] { value });
}
}
return quantized;
}
/**
* Reduces a direct color buffered image to an indexed color one without quality loss. To make sure no quality loss
* will occur, check the results of the {@link #getColorReductionInfo(BufferedImage)} method call.
*
* @param source
* the source
*
* @return the buffered image
*
* @throws IllegalArgumentException
* if the application of this method would result in image quality loss
*/
public static BufferedImage reduce(BufferedImage source) {
final int width = source.getWidth();
final int height = source.getHeight();
if (BufferedImageUtils.hasPartialTransparency(source)) {
throw new IllegalArgumentException("The source image cannot contain translucent areas");
}
final int[] colorsWithAlpha = BufferedImageUtils.getDistinctColors(source, 1);
if (colorsWithAlpha.length - 1 > MAX_INDEXED_COLORS) {
throw new IllegalArgumentException(
"The source image cannot contain more than " + MAX_INDEXED_COLORS + " colors");
}
final IndexColorModel colorModel = new IndexColorModel(8, colorsWithAlpha.length, colorsWithAlpha, 0, false, 0,
DataBuffer.TYPE_BYTE);
// Write the results to an indexed image, skipping the fully transparent bits
final BufferedImage quantized = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, colorModel);
final int[][] rgb = BufferedImageUtils.getRgb(source);
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
if ((rgb[x][y] & 0xff000000) != 0x00000000) {
quantized.setRGB(x, y, source.getRGB(x, y));
}
}
}
return quantized;
}
/**
* Returns a {@link ColorReductionInfo} for the provided image.
*
* @param source
* the source
*
* @return the color reduction info
*/
public static ColorReductionInfo getColorReductionInfo(BufferedImage source) {
return new ColorReductionInfo(BufferedImageUtils.hasPartialTransparency(source),
BufferedImageUtils.countDistinctColors(source));
}
/**
* Indicates how many distinct colors an image has, whether it has partial transparency (alpha channel).
*/
public static class ColorReductionInfo {
/** Number of distinct colors in the image. */
public int distinctColors;
/** True if the image has partially transparent areas (alpha channel). */
public boolean hasPartialTransparency;
/**
* Instantiates a new color reduction info.
*
* @param hasPartialTransparency
* the has partial transparency
* @param distinctColors
* the distinct colors
*/
public ColorReductionInfo(boolean hasPartialTransparency, int distinctColors) {
this.hasPartialTransparency = hasPartialTransparency;
this.distinctColors = distinctColors;
}
/**
* Returns true if the image can be saved in a 8-bit indexed color format with 1-bit transparency without
* quality loss.
*
* @return true, if successful
*/
public boolean canReduceWithoutQualityLoss() {
return !hasPartialTransparency && distinctColors <= MAX_INDEXED_COLORS;
}
}
}