/*
* Copyright (c) 2008, Harald Kuhr
* 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 copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* 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 HOLDER 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.
*/
/*
******************************************************************************
*
* ============================================================================
* The Apache Software License, Version 1.1
* ============================================================================
*
* Copyright (C) 2000 The Apache Software Foundation. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modifica-
* tion, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. 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.
*
* 3. The end-user documentation included with the redistribution, if any, must
* include the following acknowledgment: "This product includes software
* developed by the Apache Software Foundation (http://www.apache.org/)."
* Alternately, this acknowledgment may appear in the software itself, if
* and wherever such third-party acknowledgments normally appear.
*
* 4. The names "Batik" and "Apache Software Foundation" must not be used to
* endorse or promote products derived from this software without prior
* written permission. For written permission, please contact
* apache@apache.org.
*
* 5. Products derived from this software may not be called "Apache", nor may
* "Apache" appear in their name, without prior written permission of the
* Apache Software Foundation.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED 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
* APACHE SOFTWARE FOUNDATION OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLU-
* DING, 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.
*
* This software consists of voluntary contributions made by many individuals
* on behalf of the Apache Software Foundation. For more information on the
* Apache Software Foundation, please see
* Support for the default JVM (ordered/pattern) dither, Floyd-Steinberg like * error-diffusion and no dither, controlled by the hints * {@link #DITHER_DIFFUSION}, * {@link #DITHER_NONE} and * {@link #DITHER_DEFAULT}. *
** Color selection speed/accuracy can be controlled using the hints * {@link #COLOR_SELECTION_FAST}, * {@link #COLOR_SELECTION_QUALITY} and * {@link #COLOR_SELECTION_DEFAULT}. *
** Transparency support can be controlled using the hints * {@link #TRANSPARENCY_OPAQUE}, * {@link #TRANSPARENCY_BITMASK} and * {@link #TRANSPARENCY_TRANSLUCENT}. *
**
* This product includes software developed by the Apache Software Foundation. * * This software consists of voluntary contributions made by many individuals * on behalf of the Apache Software Foundation. For more information on the * Apache Software Foundation, please see http://www.apache.org/ ** * * @author Thomas DeWeese * @author Jun Inamori * @author Harald Kuhr * @version $Id: IndexImage.java#1 $ * @see DiffusionDither */ class IndexImage { /** * Dither mask */ protected final static int DITHER_MASK = 0xFF; /** * Java default dither */ public final static int DITHER_DEFAULT = 0x00; /** * No dither */ public final static int DITHER_NONE = 0x01; /** * Error diffusion dither */ public final static int DITHER_DIFFUSION = 0x02; /** * Error diffusion dither with alternating scans */ public final static int DITHER_DIFFUSION_ALTSCANS = 0x03; /** * Color Selection mask */ protected final static int COLOR_SELECTION_MASK = 0xFF00; /** * Default color selection */ public final static int COLOR_SELECTION_DEFAULT = 0x0000; /** * Prioritize speed */ public final static int COLOR_SELECTION_FAST = 0x0100; /** * Prioritize quality */ public final static int COLOR_SELECTION_QUALITY = 0x0200; /** * Transparency mask */ protected final static int TRANSPARENCY_MASK = 0xFF0000; /** * Default transparency (none) */ public final static int TRANSPARENCY_DEFAULT = 0x000000; /** * Discard any alpha information */ public final static int TRANSPARENCY_OPAQUE = 0x010000; /** * Convert alpha to bitmask */ public final static int TRANSPARENCY_BITMASK = 0x020000; /** * Keep original alpha (not supported yet) */ protected final static int TRANSPARENCY_TRANSLUCENT = 0x030000; /** * Used to track a color and the number of pixels of that colors */ private static class Counter { /** * Field val */ public int val; /** * Field count */ public int count = 1; /** * Constructor Counter * * @param val the initial value */ public Counter(int val) { this.val = val; } /** * Method add * * @param val the new value * @return {@code true} if the value was added, otherwise {@code false} */ public boolean add(int val) { // See if the value matches us... if (this.val != val) { return false; } count++; return true; } } /** * Used to define a cube of the color space. The cube can be split * approximately in half to generate two cubes. */ private static class Cube { int[] min = {0, 0, 0}; int[] max = {255, 255, 255}; boolean done = false; List
* The image returned is a new image, the input image is not modified. *
* * @param pImage the BufferedImage to index and get color information from. * @return the indexed BufferedImage. The image will be of type * {@code BufferedImage.TYPE_BYTE_INDEXED}, and use an * {@code IndexColorModel}. * @see BufferedImage#TYPE_BYTE_INDEXED * @see IndexColorModel */ public static BufferedImage getIndexedImage(BufferedImage pImage) { return getIndexedImage(pImage, 256, DITHER_DEFAULT); } /** * Tests if the hint {@code COLOR_SELECTION_QUALITY} is not * set. * * @param pHints hints * @return true if the hint {@code COLOR_SELECTION_QUALITY} * is not set. */ private static boolean isFast(int pHints) { return (pHints & COLOR_SELECTION_MASK) != COLOR_SELECTION_QUALITY; } /** * Tests if the hint {@code TRANSPARENCY_BITMASK} or * {@code TRANSPARENCY_TRANSLUCENT} is set. * * @param pHints hints * @return true if the hint {@code TRANSPARENCY_BITMASK} or * {@code TRANSPARENCY_TRANSLUCENT} is set. */ static boolean isTransparent(int pHints) { return (pHints & TRANSPARENCY_BITMASK) != 0 || (pHints & TRANSPARENCY_TRANSLUCENT) != 0; } /** * Converts the input image (must be {@code TYPE_INT_RGB} or * {@code TYPE_INT_ARGB}) to an indexed image. If the palette image * uses an {@code IndexColorModel}, this will be used. Otherwise, generating an * adaptive palette (8 bit) from the given palette image. * Dithering, transparency and color selection is controlled with the * {@code pHints}parameter. ** The image returned is a new image, the input image is not modified. *
* * @param pImage the BufferedImage to index * @param pPalette the Image to read color information from * @param pMatte the background color, used where the original image was * transparent * @param pHints hints that control output quality and speed. * @return the indexed BufferedImage. The image will be of type * {@code BufferedImage.TYPE_BYTE_INDEXED} or * {@code BufferedImage.TYPE_BYTE_BINARY}, and use an * {@code IndexColorModel}. * @throws ImageConversionException if an exception occurred during color * model extraction. * @see #DITHER_DIFFUSION * @see #DITHER_NONE * @see #COLOR_SELECTION_FAST * @see #COLOR_SELECTION_QUALITY * @see #TRANSPARENCY_OPAQUE * @see #TRANSPARENCY_BITMASK * @see BufferedImage#TYPE_BYTE_INDEXED * @see BufferedImage#TYPE_BYTE_BINARY * @see IndexColorModel */ public static BufferedImage getIndexedImage(BufferedImage pImage, Image pPalette, Color pMatte, int pHints) throws ImageConversionException { return getIndexedImage(pImage, getIndexColorModel(pPalette, 256, pHints), pMatte, pHints); } /** * Converts the input image (must be {@code TYPE_INT_RGB} or * {@code TYPE_INT_ARGB}) to an indexed image. Generating an adaptive * palette with the given number of colors. * Dithering, transparency and color selection is controlled with the * {@code pHints} parameter. ** The image returned is a new image, the input image is not modified. *
* * @param pImage the BufferedImage to index * @param pNumberOfColors the number of colors for the image * @param pMatte the background color, used where the original image was * transparent * @param pHints hints that control output quality and speed. * @return the indexed BufferedImage. The image will be of type * {@code BufferedImage.TYPE_BYTE_INDEXED} or * {@code BufferedImage.TYPE_BYTE_BINARY}, and use an * {@code IndexColorModel}. * @see #DITHER_DIFFUSION * @see #DITHER_NONE * @see #COLOR_SELECTION_FAST * @see #COLOR_SELECTION_QUALITY * @see #TRANSPARENCY_OPAQUE * @see #TRANSPARENCY_BITMASK * @see BufferedImage#TYPE_BYTE_INDEXED * @see BufferedImage#TYPE_BYTE_BINARY * @see IndexColorModel */ public static BufferedImage getIndexedImage(BufferedImage pImage, int pNumberOfColors, Color pMatte, int pHints) { // NOTE: We need to apply matte before creating color model, otherwise we // won't have colors for potential faded transitions IndexColorModel icm; if (pMatte != null) { icm = getIndexColorModel(createSolid(pImage, pMatte), pNumberOfColors, pHints); } else { icm = getIndexColorModel(pImage, pNumberOfColors, pHints); } // If we found less colors, then no need to dither if ((pHints & DITHER_MASK) != DITHER_NONE && (icm.getMapSize() < pNumberOfColors)) { pHints = (pHints & ~DITHER_MASK) | DITHER_NONE; } return getIndexedImage(pImage, icm, pMatte, pHints); } /** * Converts the input image (must be {@code TYPE_INT_RGB} or * {@code TYPE_INT_ARGB}) to an indexed image. Using the supplied * {@code IndexColorModel}'s palette. * Dithering, transparency and color selection is controlled with the * {@code pHints} parameter. ** The image returned is a new image, the input image is not modified. *
* * @param pImage the BufferedImage to index * @param pColors an {@code IndexColorModel} containing the color information * @param pMatte the background color, used where the original image was * transparent. Also note that any transparent antialias will be * rendered against this color. * @param pHints RenderingHints that control output quality and speed. * @return the indexed BufferedImage. The image will be of type * {@code BufferedImage.TYPE_BYTE_INDEXED} or * {@code BufferedImage.TYPE_BYTE_BINARY}, and use an * {@code IndexColorModel}. * @see #DITHER_DIFFUSION * @see #DITHER_NONE * @see #COLOR_SELECTION_FAST * @see #COLOR_SELECTION_QUALITY * @see #TRANSPARENCY_OPAQUE * @see #TRANSPARENCY_BITMASK * @see BufferedImage#TYPE_BYTE_INDEXED * @see BufferedImage#TYPE_BYTE_BINARY * @see IndexColorModel */ public static BufferedImage getIndexedImage(BufferedImage pImage, IndexColorModel pColors, Color pMatte, int pHints) { // TODO: Consider: /* if (pImage.getType() == BufferedImage.TYPE_BYTE_INDEXED || pImage.getType() == BufferedImage.TYPE_BYTE_BINARY) { pImage = ImageUtil.toBufferedImage(pImage, BufferedImage.TYPE_INT_ARGB); } */ // Get dimensions final int width = pImage.getWidth(); final int height = pImage.getHeight(); // Support transparency? boolean transparency = isTransparent(pHints) && (pImage.getColorModel().getTransparency() != Transparency.OPAQUE) && (pColors.getTransparency() != Transparency.OPAQUE); // Create image with solid background BufferedImage solid = pImage; if (pMatte != null) { // transparency doesn't really matter solid = createSolid(pImage, pMatte); } BufferedImage indexed; // Support TYPE_BYTE_BINARY, but only for 2 bit images, as the default // dither does not work with TYPE_BYTE_BINARY it seems... if (pColors.getMapSize() > 2) { indexed = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, pColors); } else { indexed = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_BINARY, pColors); } // Apply dither if requested switch (pHints & DITHER_MASK) { case DITHER_DIFFUSION: case DITHER_DIFFUSION_ALTSCANS: // Create a DiffusionDither to apply dither to indexed DiffusionDither dither = new DiffusionDither(pColors); if ((pHints & DITHER_MASK) == DITHER_DIFFUSION_ALTSCANS) { dither.setAlternateScans(true); } dither.filter(solid, indexed); break; case DITHER_NONE: // Just copy pixels, without dither // NOTE: This seems to be slower than the method below, using // Graphics2D.drawImage, and VALUE_DITHER_DISABLE, // however you possibly end up getting a dithered image anyway, // therefore, do it slower and produce correct result. :-) CopyDither copy = new CopyDither(pColors); copy.filter(solid, indexed); break; case DITHER_DEFAULT: // This is the default default: // Render image data onto indexed image, using default // (probably we get dither, but it depends on the GFX engine). Graphics2D g2d = indexed.createGraphics(); try { RenderingHints hints = new RenderingHints(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE); g2d.setRenderingHints(hints); g2d.drawImage(solid, 0, 0, null); } finally { g2d.dispose(); } break; } // Transparency support, this approach seems lame, but it's the only // solution I've found until now (that actually works). if (transparency) { // Re-apply the alpha-channel of the original image applyAlpha(indexed, pImage); } // Return the indexed BufferedImage return indexed; } /** * Converts the input image (must be {@code TYPE_INT_RGB} or * {@code TYPE_INT_ARGB}) to an indexed image. Generating an adaptive * palette with the given number of colors. * Dithering, transparency and color selection is controlled with the * {@code pHints}parameter. ** The image returned is a new image, the input image is not modified. *
* * @param pImage the BufferedImage to index * @param pNumberOfColors the number of colors for the image * @param pHints hints that control output quality and speed. * @return the indexed BufferedImage. The image will be of type * {@code BufferedImage.TYPE_BYTE_INDEXED} or * {@code BufferedImage.TYPE_BYTE_BINARY}, and use an * {@code IndexColorModel}. * @see #DITHER_DIFFUSION * @see #DITHER_NONE * @see #COLOR_SELECTION_FAST * @see #COLOR_SELECTION_QUALITY * @see #TRANSPARENCY_OPAQUE * @see #TRANSPARENCY_BITMASK * @see BufferedImage#TYPE_BYTE_INDEXED * @see BufferedImage#TYPE_BYTE_BINARY * @see IndexColorModel */ public static BufferedImage getIndexedImage(BufferedImage pImage, int pNumberOfColors, int pHints) { return getIndexedImage(pImage, pNumberOfColors, null, pHints); } /** * Converts the input image (must be {@code TYPE_INT_RGB} or * {@code TYPE_INT_ARGB}) to an indexed image. Using the supplied * {@code IndexColorModel}'s palette. * Dithering, transparency and color selection is controlled with the * {@code pHints}parameter. ** The image returned is a new image, the input image is not modified. *
* * @param pImage the BufferedImage to index * @param pColors an {@code IndexColorModel} containing the color information * @param pHints RenderingHints that control output quality and speed. * @return the indexed BufferedImage. The image will be of type * {@code BufferedImage.TYPE_BYTE_INDEXED} or * {@code BufferedImage.TYPE_BYTE_BINARY}, and use an * {@code IndexColorModel}. * @see #DITHER_DIFFUSION * @see #DITHER_NONE * @see #COLOR_SELECTION_FAST * @see #COLOR_SELECTION_QUALITY * @see #TRANSPARENCY_OPAQUE * @see #TRANSPARENCY_BITMASK * @see BufferedImage#TYPE_BYTE_INDEXED * @see BufferedImage#TYPE_BYTE_BINARY * @see IndexColorModel */ public static BufferedImage getIndexedImage(BufferedImage pImage, IndexColorModel pColors, int pHints) { return getIndexedImage(pImage, pColors, null, pHints); } /** * Converts the input image (must be {@code TYPE_INT_RGB} or * {@code TYPE_INT_ARGB}) to an indexed image. If the palette image * uses an {@code IndexColorModel}, this will be used. Otherwise, generating an * adaptive palette (8 bit) from the given palette image. * Dithering, transparency and color selection is controlled with the * {@code pHints}parameter. ** The image returned is a new image, the input image is not modified. *
* * @param pImage the BufferedImage to index * @param pPalette the Image to read color information from * @param pHints hints that control output quality and speed. * @return the indexed BufferedImage. The image will be of type * {@code BufferedImage.TYPE_BYTE_INDEXED} or * {@code BufferedImage.TYPE_BYTE_BINARY}, and use an * {@code IndexColorModel}. * @see #DITHER_DIFFUSION * @see #DITHER_NONE * @see #COLOR_SELECTION_FAST * @see #COLOR_SELECTION_QUALITY * @see #TRANSPARENCY_OPAQUE * @see #TRANSPARENCY_BITMASK * @see BufferedImage#TYPE_BYTE_INDEXED * @see BufferedImage#TYPE_BYTE_BINARY * @see IndexColorModel */ public static BufferedImage getIndexedImage(BufferedImage pImage, Image pPalette, int pHints) { return getIndexedImage(pImage, pPalette, null, pHints); } /** * Creates a copy of the given image, with a solid background * * @param pOriginal the original image * @param pBackground the background color * @return a new {@code BufferedImage} */ private static BufferedImage createSolid(BufferedImage pOriginal, Color pBackground) { // Create a temporary image of same dimension and type BufferedImage solid = new BufferedImage(pOriginal.getColorModel(), pOriginal.copyData(null), pOriginal.isAlphaPremultiplied(), null); Graphics2D g = solid.createGraphics(); try { // Clear in background color g.setColor(pBackground); g.setComposite(AlphaComposite.DstOver);// Paint "underneath" g.fillRect(0, 0, pOriginal.getWidth(), pOriginal.getHeight()); } finally { g.dispose(); } return solid; } /** * Applies the alpha-component of the alpha image to the given image. * The given image is modified in place. * * @param pImage the image to apply alpha to * @param pAlpha the image containing the alpha */ private static void applyAlpha(BufferedImage pImage, BufferedImage pAlpha) { // Apply alpha as transparency, using threshold of 25% for (int y = 0; y < pAlpha.getHeight(); y++) { for (int x = 0; x < pAlpha.getWidth(); x++) { // Get alpha component of pixel, if less than 25% opaque // (0x40 = 64 => 25% of 256), the pixel will be transparent if (((pAlpha.getRGB(x, y) >> 24) & 0xFF) < 0x40) { pImage.setRGB(x, y, 0x00FFFFFF); // 100% transparent } } } } /* * This class is also a command-line utility. */ public static void main(String pArgs[]) { // Defaults int argIdx = 0; int speedTest = -1; boolean overWrite = false; boolean monochrome = false; boolean gray = false; int numColors = 256; String dither = null; String quality = null; String format = null; Color background = null; boolean transparency = false; String paletteFileName = null; boolean errArgs = false; // Parse args while ((argIdx < pArgs.length) && (pArgs[argIdx].charAt(0) == '-') && (pArgs[argIdx].length() >= 2)) { if ((pArgs[argIdx].charAt(1) == 's') || pArgs[argIdx].equals("--speedtest")) { argIdx++; // Get number of iterations if ((pArgs.length > argIdx) && (pArgs[argIdx].charAt(0) != '-')) { try { speedTest = Integer.parseInt(pArgs[argIdx++]); } catch (NumberFormatException nfe) { errArgs = true; break; } } else { // Default to 10 iterations speedTest = 10; } } else if ((pArgs[argIdx].charAt(1) == 'w') || pArgs[argIdx].equals("--overwrite")) { overWrite = true; argIdx++; } else if ((pArgs[argIdx].charAt(1) == 'c') || pArgs[argIdx].equals("--colors")) { argIdx++; try { numColors = Integer.parseInt(pArgs[argIdx++]); } catch (NumberFormatException nfe) { errArgs = true; break; } } else if ((pArgs[argIdx].charAt(1) == 'g') || pArgs[argIdx].equals("--grayscale")) { argIdx++; gray = true; } else if ((pArgs[argIdx].charAt(1) == 'm') || pArgs[argIdx].equals("--monochrome")) { argIdx++; numColors = 2; monochrome = true; } else if ((pArgs[argIdx].charAt(1) == 'd') || pArgs[argIdx].equals("--dither")) { argIdx++; dither = pArgs[argIdx++]; } else if ((pArgs[argIdx].charAt(1) == 'p') || pArgs[argIdx].equals("--palette")) { argIdx++; paletteFileName = pArgs[argIdx++]; } else if ((pArgs[argIdx].charAt(1) == 'q') || pArgs[argIdx].equals("--quality")) { argIdx++; quality = pArgs[argIdx++]; } else if ((pArgs[argIdx].charAt(1) == 'b') || pArgs[argIdx].equals("--bgcolor")) { argIdx++; try { background = StringUtil.toColor(pArgs[argIdx++]); } catch (Exception e) { errArgs = true; break; } } else if ((pArgs[argIdx].charAt(1) == 't') || pArgs[argIdx].equals("--transparency")) { argIdx++; transparency = true; } else if ((pArgs[argIdx].charAt(1) == 'f') || pArgs[argIdx].equals("--outputformat")) { argIdx++; format = StringUtil.toLowerCase(pArgs[argIdx++]); } else if ((pArgs[argIdx].charAt(1) == 'h') || pArgs[argIdx].equals("--help")) { argIdx++; // Setting errArgs to true, to print usage errArgs = true; } else { System.err.println("Unknown option \"" + pArgs[argIdx++] + "\""); } } if (errArgs || (pArgs.length < (argIdx + 1))) { System.err.println("Usage: IndexImage [--help|-h] [--speedtest|-s