1 /*
2 * SmartSprites Project
3 *
4 * Copyright (C) 2007-2009, Stanisław Osiński.
5 * All rights reserved.
6 *
7 * Redistribution and use in source and binary forms, with or without modification,
8 * are permitted provided that the following conditions are met:
9 *
10 * - Redistributions of source code must retain the above copyright notice, this
11 * list of conditions and the following disclaimer.
12 *
13 * - Redistributions in binary form must reproduce the above copyright notice, this
14 * list of conditions and the following disclaimer in the documentation and/or
15 * other materials provided with the distribution.
16 *
17 * - Neither the name of the SmartSprites Project nor the names of its contributors
18 * may be used to endorse or promote products derived from this software
19 * without specific prior written permission.
20 *
21 * - We kindly request that you include in the end-user documentation provided with
22 * the redistribution and/or in the software itself an acknowledgement equivalent
23 * to the following: "This product includes software developed by the SmartSprites
24 * Project."
25 *
26 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
27 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
28 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
29 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
30 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
31 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
32 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
33 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
34 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
35 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
36 */
37 package org.carrot2.util;
38
39 import java.awt.Color;
40 import java.awt.image.BufferedImage;
41 import java.awt.image.DataBuffer;
42 import java.awt.image.IndexColorModel;
43 import java.awt.image.WritableRaster;
44
45 import amd.Quantize;
46
47 /**
48 * A simple utility wrapping the {@link Quantize} class to work on {@link BufferedImage}s and handle transparency.
49 */
50 public class ColorQuantizer {
51
52 /** Maximum number of colors in an indexed image, leaving one for transparency. */
53 public static final int MAX_INDEXED_COLORS = 255;
54
55 /**
56 * Instantiates a new color quantizer.
57 */
58 private ColorQuantizer() {
59 // Prevent Instantiation
60 }
61
62 /**
63 * Quantizes the image to {@link #MAX_INDEXED_COLORS} with white matte for areas with partial transparency (full
64 * transparency will be preserved).
65 *
66 * @param source
67 * the source
68 *
69 * @return {@link BufferedImage} with type {@link BufferedImage#TYPE_BYTE_INDEXED} and quantized colors
70 */
71 public static BufferedImage quantize(BufferedImage source) {
72 return quantize(source, Color.WHITE);
73 }
74
75 /**
76 * Quantizes the image to {@link #MAX_INDEXED_COLORS} with the provided matte {@link Color} for areas with partial
77 * transparency (full transparency will be preserved).
78 *
79 * @param source
80 * the source
81 * @param matteColor
82 * the matte color
83 *
84 * @return {@link BufferedImage} with type {@link BufferedImage#TYPE_BYTE_INDEXED} and quantized colors
85 */
86 public static BufferedImage quantize(BufferedImage source, Color matteColor) {
87 return quantize(source, matteColor, MAX_INDEXED_COLORS);
88 }
89
90 /**
91 * Quantizes the image to the provided number of colors with the provided matte {@link Color} for areas with partial
92 * transparency (full transparency will be preserved).
93 *
94 * @param source
95 * the source
96 * @param matteColor
97 * the matte color
98 * @param maxColors
99 * the max colors
100 *
101 * @return {@link BufferedImage} with type {@link BufferedImage#TYPE_BYTE_INDEXED} and quantized colors
102 */
103 public static BufferedImage quantize(BufferedImage source, Color matteColor, int maxColors) {
104 final int width = source.getWidth();
105 final int height = source.getHeight();
106
107 // First put the matte color so that we have a sensible result
108 // for images with full alpha transparencies
109 final BufferedImage mattedSource = BufferedImageUtils.matte(source, matteColor);
110
111 // Get two copies of RGB data (quantization will overwrite one)
112 final int[][] bitmap = BufferedImageUtils.getRgb(mattedSource);
113
114 // Quantize colors and shift palette by one for transparency color
115 // We'll keep transparency color black for now.
116 final int[] colors = Quantize.quantizeImage(bitmap, maxColors);
117 final int[] colorsWithAlpha = new int[colors.length + 1];
118 System.arraycopy(colors, 0, colorsWithAlpha, 1, colors.length);
119 colorsWithAlpha[0] = matteColor.getRGB();
120 final IndexColorModel colorModel = new IndexColorModel(8, colorsWithAlpha.length, colorsWithAlpha, 0, false, 0,
121 DataBuffer.TYPE_BYTE);
122
123 // Write the results to an indexed image, skipping the fully transparent bits
124 final BufferedImage quantized = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, colorModel);
125 final WritableRaster raster = quantized.getRaster();
126 final int[][] rgb = BufferedImageUtils.getRgb(source);
127 for (int x = 0; x < width; x++) {
128 for (int y = 0; y < height; y++) {
129 final int value = (rgb[x][y] & 0xff000000) != 0x00000000 ? bitmap[x][y] + 1 : 0;
130 raster.setPixel(x, y, new int[] { value });
131 }
132 }
133
134 return quantized;
135 }
136
137 /**
138 * Reduces a direct color buffered image to an indexed color one without quality loss. To make sure no quality loss
139 * will occur, check the results of the {@link #getColorReductionInfo(BufferedImage)} method call.
140 *
141 * @param source
142 * the source
143 *
144 * @return the buffered image
145 *
146 * @throws IllegalArgumentException
147 * if the application of this method would result in image quality loss
148 */
149 public static BufferedImage reduce(BufferedImage source) {
150 final int width = source.getWidth();
151 final int height = source.getHeight();
152
153 if (BufferedImageUtils.hasPartialTransparency(source)) {
154 throw new IllegalArgumentException("The source image cannot contain translucent areas");
155 }
156
157 final int[] colorsWithAlpha = BufferedImageUtils.getDistinctColors(source, 1);
158 if (colorsWithAlpha.length - 1 > MAX_INDEXED_COLORS) {
159 throw new IllegalArgumentException(
160 "The source image cannot contain more than " + MAX_INDEXED_COLORS + " colors");
161 }
162
163 final IndexColorModel colorModel = new IndexColorModel(8, colorsWithAlpha.length, colorsWithAlpha, 0, false, 0,
164 DataBuffer.TYPE_BYTE);
165
166 // Write the results to an indexed image, skipping the fully transparent bits
167 final BufferedImage quantized = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, colorModel);
168 final int[][] rgb = BufferedImageUtils.getRgb(source);
169
170 for (int x = 0; x < width; x++) {
171 for (int y = 0; y < height; y++) {
172 if ((rgb[x][y] & 0xff000000) != 0x00000000) {
173 quantized.setRGB(x, y, source.getRGB(x, y));
174 }
175 }
176 }
177
178 return quantized;
179 }
180
181 /**
182 * Returns a {@link ColorReductionInfo} for the provided image.
183 *
184 * @param source
185 * the source
186 *
187 * @return the color reduction info
188 */
189 public static ColorReductionInfo getColorReductionInfo(BufferedImage source) {
190 return new ColorReductionInfo(BufferedImageUtils.hasPartialTransparency(source),
191 BufferedImageUtils.countDistinctColors(source));
192 }
193
194 /**
195 * Indicates how many distinct colors an image has, whether it has partial transparency (alpha channel).
196 */
197 public static class ColorReductionInfo {
198
199 /** Number of distinct colors in the image. */
200 public int distinctColors;
201
202 /** True if the image has partially transparent areas (alpha channel). */
203 public boolean hasPartialTransparency;
204
205 /**
206 * Instantiates a new color reduction info.
207 *
208 * @param hasPartialTransparency
209 * the has partial transparency
210 * @param distinctColors
211 * the distinct colors
212 */
213 public ColorReductionInfo(boolean hasPartialTransparency, int distinctColors) {
214 this.hasPartialTransparency = hasPartialTransparency;
215 this.distinctColors = distinctColors;
216 }
217
218 /**
219 * Returns true if the image can be saved in a 8-bit indexed color format with 1-bit transparency without
220 * quality loss.
221 *
222 * @return true, if successful
223 */
224 public boolean canReduceWithoutQualityLoss() {
225 return !hasPartialTransparency && distinctColors <= MAX_INDEXED_COLORS;
226 }
227 }
228 }