View Javadoc
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 }