All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.twelvemonkeys.image.IndexImage Maven / Gradle / Ivy

There is a newer version: 1.2.2.1-jre17
Show newest version
/*
 * 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
*    [email protected].
*
* 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 .
*
******************************************************************************
*
*/

package com.twelvemonkeys.image;

import com.twelvemonkeys.io.FileUtil;
import com.twelvemonkeys.lang.StringUtil;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.IndexColorModel;
import java.awt.image.RenderedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * This class implements an adaptive palette generator to reduce images
 * to a variable number of colors.
 * It can also render images into fixed color pallettes.
 * 

* 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[] colors = null; int count = 0; static final int RED = 0; static final int GRN = 1; static final int BLU = 2; /** * Define a new cube. * * @param colors contains the 3D color histogram to be subdivided * @param count the total number of pixels in the 3D histogram. */ public Cube(List[] colors, int count) { this.colors = colors; this.count = count; } /** * If this returns true then the cube can not be subdivided any * further * * @return true if cube can not be subdivided any further */ public boolean isDone() { return done; } /** * Splits the cube into two parts. This cube is * changed to be one half and the returned cube is the other half. * This tries to pick the right channel to split on. * * @return the {@code Cube} containing the other half */ public Cube split() { int dr = max[0] - min[0] + 1; int dg = max[1] - min[1] + 1; int db = max[2] - min[2] + 1; int c0, c1, splitChannel; // Figure out which axis is the longest and split along // that axis (this tries to keep cubes square-ish). if (dr >= dg) { c0 = GRN; if (dr >= db) { splitChannel = RED; c1 = BLU; } else { splitChannel = BLU; c1 = RED; } } else if (dg >= db) { splitChannel = GRN; c0 = RED; c1 = BLU; } else { splitChannel = BLU; c0 = RED; c1 = GRN; } Cube ret; ret = splitChannel(splitChannel, c0, c1); if (ret != null) { return ret; } ret = splitChannel(c0, splitChannel, c1); if (ret != null) { return ret; } ret = splitChannel(c1, splitChannel, c0); if (ret != null) { return ret; } done = true; return null; } /** * Splits the image according to the parameters. It tries * to find a location where half the pixels are on one side * and half the pixels are on the other. * * @param splitChannel split channel * @param c0 channel 0 * @param c1 channel 1 * @return the {@code Cube} containing the other half */ public Cube splitChannel(int splitChannel, int c0, int c1) { if (min[splitChannel] == max[splitChannel]) { return null; } int splitSh4 = (2 - splitChannel) * 4; int c0Sh4 = (2 - c0) * 4; int c1Sh4 = (2 - c1) * 4; // int splitSh8 = (2-splitChannel)*8; // int c0Sh8 = (2-c0)*8; // int c1Sh8 = (2-c1)*8; // int half = count / 2; // Each entry is the number of pixels that have that value // in the split channel within the cube (so pixels // that have that value in the split channel aren't counted // if they are outside the cube in the other color channels. int counts[] = new int[256]; int tcount = 0; // System.out.println("Cube: [" + // min[0] + "-" + max[0] + "] [" + // min[1] + "-" + max[1] + "] [" + // min[2] + "-" + max[2] + "]"); int[] minIdx = {min[0] >> 4, min[1] >> 4, min[2] >> 4}; int[] maxIdx = {max[0] >> 4, max[1] >> 4, max[2] >> 4}; int minR = min[0], minG = min[1], minB = min[2]; int maxR = max[0], maxG = max[1], maxB = max[2]; int val; int[] vals = {0, 0, 0}; for (int i = minIdx[splitChannel]; i <= maxIdx[splitChannel]; i++) { int idx1 = i << splitSh4; for (int j = minIdx[c0]; j <= maxIdx[c0]; j++) { int idx2 = idx1 | (j << c0Sh4); for (int k = minIdx[c1]; k <= maxIdx[c1]; k++) { int idx = idx2 | (k << c1Sh4); List v = colors[idx]; if (v == null) { continue; } for (Counter c : v) { val = c.val; vals[0] = (val & 0xFF0000) >> 16; vals[1] = (val & 0xFF00) >> 8; vals[2] = (val & 0xFF); if (((vals[0] >= minR) && (vals[0] <= maxR)) && ((vals[1] >= minG) && (vals[1] <= maxG)) && ((vals[2] >= minB) && (vals[2] <= maxB))) { // The val lies within this cube so count it. counts[vals[splitChannel]] += c.count; tcount += c.count; } } } } // We've found the half way point. Note that the // rest of counts is not filled out. if (tcount >= half) { break; } } tcount = 0; int lastAdd = -1; // These indicate what the top value for the low cube and // the low value of the high cube should be in the split channel // (they may not be one off if there are 'dead' spots in the // counts array.) int splitLo = min[splitChannel], splitHi = max[splitChannel]; for (int i = min[splitChannel]; i <= max[splitChannel]; i++) { int c = counts[i]; if (c == 0) { // No counts below this so move up bottom of cube. if ((tcount == 0) && (i < max[splitChannel])) { this.min[splitChannel] = i + 1; } continue; } if (tcount + c < half) { lastAdd = i; tcount += c; continue; } if ((half - tcount) <= ((tcount + c) - half)) { // Then lastAdd is a better top idx for this then i. if (lastAdd == -1) { // No lower place to break. if (c == this.count) { // All pixels are at this value so make min/max // reflect that. this.max[splitChannel] = i; return null;// no split to make. } else { // There are values about this one so // split above. splitLo = i; splitHi = i + 1; break; } } splitLo = lastAdd; splitHi = i; } else { if (i == this.max[splitChannel]) { if (c == this.count) { // would move min up but that should // have happened already. return null;// no split to make. } else { // Would like to break between i and i+1 // but no i+1 so use lastAdd and i; splitLo = lastAdd; splitHi = i; break; } } // Include c in counts tcount += c; splitLo = i; splitHi = i + 1; } break; } // System.out.println("Split: " + splitChannel + "@" // + splitLo + "-"+splitHi + // " Count: " + tcount + " of " + count + // " LA: " + lastAdd); // Create the new cube and update everyone's bounds & counts. Cube ret = new Cube(colors, tcount); this.count = this.count - tcount; ret.min[splitChannel] = this.min[splitChannel]; ret.max[splitChannel] = splitLo; this.min[splitChannel] = splitHi; ret.min[c0] = this.min[c0]; ret.max[c0] = this.max[c0]; ret.min[c1] = this.min[c1]; ret.max[c1] = this.max[c1]; return ret; } /** * Returns the average color for this cube * * @return the average */ public int averageColor() { if (this.count == 0) { return 0; } float red = 0, grn = 0, blu = 0; int minR = min[0], minG = min[1], minB = min[2]; int maxR = max[0], maxG = max[1], maxB = max[2]; int[] minIdx = {minR >> 4, minG >> 4, minB >> 4}; int[] maxIdx = {maxR >> 4, maxG >> 4, maxB >> 4}; int val, ired, igrn, iblu; float weight; for (int i = minIdx[0]; i <= maxIdx[0]; i++) { int idx1 = i << 8; for (int j = minIdx[1]; j <= maxIdx[1]; j++) { int idx2 = idx1 | (j << 4); for (int k = minIdx[2]; k <= maxIdx[2]; k++) { int idx = idx2 | k; List v = colors[idx]; if (v == null) { continue; } for (Counter c : v) { val = c.val; ired = (val & 0xFF0000) >> 16; igrn = (val & 0x00FF00) >> 8; iblu = (val & 0x0000FF); if (((ired >= minR) && (ired <= maxR)) && ((igrn >= minG) && (igrn <= maxG)) && ((iblu >= minB) && (iblu <= maxB))) { weight = (c.count / (float) this.count); red += ((float) ired) * weight; grn += ((float) igrn) * weight; blu += ((float) iblu) * weight; } } } } } // System.out.println("RGB: [" + red + ", " + // grn + ", " + blu + "]"); return (((int) (red + 0.5f)) << 16 | ((int) (grn + 0.5f)) << 8 | ((int) (blu + 0.5f))); } }// end Cube /** * You cannot create this */ private IndexImage() { } /** * @param pImage the image to get {@code IndexColorModel} from * @param pNumberOfColors the number of colors for the {@code IndexColorModel} * @param pFast {@code true} if fast * @return an {@code IndexColorModel} * @see #getIndexColorModel(Image,int,int) * * @deprecated Use {@link #getIndexColorModel(Image,int,int)} instead! * This version will be removed in a later version of the API. */ public static IndexColorModel getIndexColorModel(Image pImage, int pNumberOfColors, boolean pFast) { return getIndexColorModel(pImage, pNumberOfColors, pFast ? COLOR_SELECTION_FAST : COLOR_SELECTION_QUALITY); } /** * Gets an {@code IndexColorModel} from the given image. If the image has an * {@code IndexColorModel}, this will be returned. Otherwise, an {@code IndexColorModel} * is created, using an adaptive palette. * * @param pImage the image to get {@code IndexColorModel} from * @param pNumberOfColors the number of colors for the {@code IndexColorModel} * @param pHints one of {@link #COLOR_SELECTION_FAST}, * {@link #COLOR_SELECTION_QUALITY} or * {@link #COLOR_SELECTION_DEFAULT}. * @return The {@code IndexColorModel} from the given image, or a newly created * {@code IndexColorModel} using an adaptive palette. * @throws ImageConversionException if an exception occurred during color * model extraction. */ public static IndexColorModel getIndexColorModel(Image pImage, int pNumberOfColors, int pHints) throws ImageConversionException { IndexColorModel icm = null; RenderedImage image = null; if (pImage instanceof RenderedImage) { image = (RenderedImage) pImage; ColorModel cm = image.getColorModel(); if (cm instanceof IndexColorModel) { // Test if we have right number of colors if (((IndexColorModel) cm).getMapSize() <= pNumberOfColors) { //System.out.println("IndexColorModel from BufferedImage"); icm = (IndexColorModel) cm;// Done } } // Else create from buffered image, hard way, see below } else { // Create from image using BufferedImageFactory BufferedImageFactory factory = new BufferedImageFactory(pImage); ColorModel cm = factory.getColorModel(); if ((cm instanceof IndexColorModel) && ((IndexColorModel) cm).getMapSize() <= pNumberOfColors) { //System.out.println("IndexColorModel from Image"); icm = (IndexColorModel) cm;// Done } else { // Else create from (buffered) image, hard way image = factory.getBufferedImage(); } } // We now have at least a buffered image, create model from it if (icm == null) { icm = createIndexColorModel(ImageUtil.toBuffered(image), pNumberOfColors, pHints); } else if (!(icm instanceof InverseColorMapIndexColorModel)) { // If possible, use faster code icm = new InverseColorMapIndexColorModel(icm); } return icm; } /** * Creates an {@code IndexColorModel} from the given image, using an adaptive * palette. * * @param pImage the image to get {@code IndexColorModel} from * @param pNumberOfColors the number of colors for the {@code IndexColorModel} * @param pHints use fast mode if possible (might give slightly lower * quality) * @return a new {@code IndexColorModel} created from the given image */ private static IndexColorModel createIndexColorModel(BufferedImage pImage, int pNumberOfColors, int pHints) { // TODO: Use ImageUtil.hasTransparentPixels(pImage, true) || // -- haraldK, 20021024, experimental, try to use one transparent pixel boolean useTransparency = isTransparent(pHints); if (useTransparency) { pNumberOfColors--; } //System.out.println("Transp: " + useTransparency + " colors: " + pNumberOfColors); int width = pImage.getWidth(); int height = pImage.getHeight(); // Using 4 bits from R, G & B. @SuppressWarnings("unchecked") List[] colors = new List[1 << 12];// [4096] // Speedup, doesn't decrease image quality much int step = 1; if (isFast(pHints)) { step += (width * height / 16384);// 128x128px } int sampleCount = 0; int rgb; //for (int x = 0; x < width; x++) { //for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { for (int y = x % step; y < height; y += step) { // Count the number of color samples sampleCount++; // Get ARGB pixel from image rgb = (pImage.getRGB(x, y) & 0xFFFFFF); // Get index from high four bits of each component. int index = (((rgb & 0xF00000) >>> 12) | ((rgb & 0x00F000) >>> 8) | ((rgb & 0x0000F0) >>> 4)); // Get the 'hash vector' for that key. List v = colors[index]; if (v == null) { // No colors in this bin yet so create vector and // add color. v = new ArrayList(); v.add(new Counter(rgb)); colors[index] = v; } else { // Find our color in the bin or create a counter for it. Iterator i = v.iterator(); while (true) { if (i.hasNext()) { // try adding our color to each counter... if (((Counter) i.next()).add(rgb)) { break; } } else { v.add(new Counter(rgb)); break; } } } } } // All colours found, reduce to pNumberOfColors int numberOfCubes = 1; int fCube = 0; Cube[] cubes = new Cube[pNumberOfColors]; cubes[0] = new Cube(colors, sampleCount); //cubes[0] = new Cube(colors, width * height); while (numberOfCubes < pNumberOfColors) { while (cubes[fCube].isDone()) { fCube++; if (fCube == numberOfCubes) { break; } } if (fCube == numberOfCubes) { break; } Cube cube = cubes[fCube]; Cube newCube = cube.split(); if (newCube != null) { if (newCube.count > cube.count) { Cube tmp = cube; cube = newCube; newCube = tmp; } int j = fCube; int count = cube.count; for (int i = fCube + 1; i < numberOfCubes; i++) { if (cubes[i].count < count) { break; } cubes[j++] = cubes[i]; } cubes[j++] = cube; count = newCube.count; while (j < numberOfCubes) { if (cubes[j].count < count) { break; } j++; } System.arraycopy(cubes, j, cubes, j + 1, numberOfCubes - j); cubes[j/*++*/] = newCube; numberOfCubes++; } } // Create RGB arrays with correct number of colors // If we have transparency, the last color will be the transparent one byte[] r = new byte[useTransparency ? numberOfCubes + 1 : numberOfCubes]; byte[] g = new byte[useTransparency ? numberOfCubes + 1 : numberOfCubes]; byte[] b = new byte[useTransparency ? numberOfCubes + 1 : numberOfCubes]; for (int i = 0; i < numberOfCubes; i++) { int val = cubes[i].averageColor(); r[i] = (byte) ((val >> 16) & 0xFF); g[i] = (byte) ((val >> 8) & 0xFF); b[i] = (byte) ((val) & 0xFF); //System.out.println("Color [" + i + "]: #" + // (((val>>16)<16)?"0":"") + // Integer.toHexString(val)); } // For some reason using less than 8 bits causes a bug in the dither // - transparency added to all totally black colors? int numOfBits = 8; // -- haraldK, 20021024, as suggested by Thomas E. Deweese // plus adding a transparent pixel IndexColorModel icm; if (useTransparency) { icm = new InverseColorMapIndexColorModel(numOfBits, r.length, r, g, b, r.length - 1); } else { icm = new InverseColorMapIndexColorModel(numOfBits, r.length, r, g, b); } return icm; } /** * Converts the input image (must be {@code TYPE_INT_RGB} or * {@code TYPE_INT_ARGB}) to an indexed image. Generating an adaptive * palette (8 bit) from the color data in the image, and uses default * dither. *

* 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 ] [--bgcolor|-b ] [--colors|-c | --grayscale|g | --monochrome|-m | --palette|-p ] [--dither|-d (default|diffusion|none)] [--quality|-q (default|high|low)] [--transparency|-t] [--outputformat|-f (gif|jpeg|png|wbmp|...)] [--overwrite|-w] []"); System.err.print("Input format names: "); String[] readers = ImageIO.getReaderFormatNames(); for (int i = 0; i < readers.length; i++) { System.err.print(readers[i] + ((i + 1 < readers.length) ? ", " : "\n")); } System.err.print("Output format names: "); String[] writers = ImageIO.getWriterFormatNames(); for (int i = 0; i < writers.length; i++) { System.err.print(writers[i] + ((i + 1 < writers.length) ? ", " : "\n")); } System.exit(5); } // Read in image File in = new File(pArgs[argIdx++]); if (!in.exists()) { System.err.println("File \"" + in.getAbsolutePath() + "\" does not exist!"); System.exit(5); } // Read palette if needed File paletteFile = null; if (paletteFileName != null) { paletteFile = new File(paletteFileName); if (!paletteFile.exists()) { System.err.println("File \"" + in.getAbsolutePath() + "\" does not exist!"); System.exit(5); } } // Make sure we can write File out; if (argIdx < pArgs.length) { out = new File(pArgs[argIdx/*++*/]); // Get format from file extension if (format == null) { format = FileUtil.getExtension(out); } } else { // Create new file in current dir, same name + format extension String baseName = FileUtil.getBasename(in); // Use png as default format if (format == null) { format = "png"; } out = new File(baseName + '.' + format); } if (!overWrite && out.exists()) { System.err.println("The file \"" + out.getAbsolutePath() + "\" allready exists!"); System.exit(5); } // Do the image processing BufferedImage image = null; BufferedImage paletteImg = null; try { image = ImageIO.read(in); if (image == null) { System.err.println("No reader for image: \"" + in.getAbsolutePath() + "\"!"); System.exit(5); } if (paletteFile != null) { paletteImg = ImageIO.read(paletteFile); if (paletteImg == null) { System.err.println("No reader for image: \"" + paletteFile.getAbsolutePath() + "\"!"); System.exit(5); } } } catch (IOException ioe) { ioe.printStackTrace(System.err); System.exit(5); } // Create hints int hints = DITHER_DEFAULT; if ("DIFFUSION".equalsIgnoreCase(dither)) { hints |= DITHER_DIFFUSION; } else if ("DIFFUSION_ALTSCANS".equalsIgnoreCase(dither)) { hints |= DITHER_DIFFUSION_ALTSCANS; } else if ("NONE".equalsIgnoreCase(dither)) { hints |= DITHER_NONE; } else { // Don't care, use default } if ("HIGH".equalsIgnoreCase(quality)) { hints |= COLOR_SELECTION_QUALITY; } else if ("LOW".equalsIgnoreCase(quality)) { hints |= COLOR_SELECTION_FAST; } else { // Don't care, use default } if (transparency) { hints |= TRANSPARENCY_BITMASK; } ////////////////////////////// // Apply bg-color WORKAROUND! // This needs to be done BEFORE palette creation to have desired effect.. if ((background != null) && (paletteImg == null)) { paletteImg = createSolid(image, background); } /////////////////////////////// // Index long start = 0; if (speedTest > 0) { // SPEED TESTING System.out.println("Measuring speed!"); start = System.currentTimeMillis(); // END SPEED TESTING } BufferedImage indexed; IndexColorModel colors; if (monochrome) { indexed = getIndexedImage(image, MonochromeColorModel.getInstance(), background, hints); colors = MonochromeColorModel.getInstance(); } else if (gray) { //indexed = ImageUtil.toBuffered(ImageUtil.grayscale(image), BufferedImage.TYPE_BYTE_GRAY); image = ImageUtil.toBuffered(ImageUtil.grayscale(image)); indexed = getIndexedImage(image, colors = getIndexColorModel(image, numColors, hints), background, hints); // In casse of speedtest, this makes sense... if (speedTest > 0) { colors = getIndexColorModel(indexed, numColors, hints); } } else if (paletteImg != null) { // Get palette from image indexed = getIndexedImage(ImageUtil.toBuffered(image, BufferedImage.TYPE_INT_ARGB), colors = getIndexColorModel(paletteImg, numColors, hints), background, hints); } else { image = ImageUtil.toBuffered(image, BufferedImage.TYPE_INT_ARGB); indexed = getIndexedImage(image, colors = getIndexColorModel(image, numColors, hints), background, hints); } if (speedTest > 0) { // SPEED TESTING System.out.println("Color selection + dither: " + (System.currentTimeMillis() - start) + " ms"); // END SPEED TESTING } // Write output (in given format) try { if (!ImageIO.write(indexed, format, out)) { System.err.println("No writer for format: \"" + format + "\"!"); } } catch (IOException ioe) { ioe.printStackTrace(System.err); } if (speedTest > 0) { // SPEED TESTING System.out.println("Measuring speed!"); // Warmup! for (int i = 0; i < 10; i++) { getIndexedImage(image, colors, background, hints); } // Measure long time = 0; for (int i = 0; i < speedTest; i++) { start = System.currentTimeMillis(); getIndexedImage(image, colors, background, hints); time += (System.currentTimeMillis() - start); System.out.print('.'); if ((i + 1) % 10 == 0) { System.out.println("\nAverage (after " + (i + 1) + " iterations): " + (time / (i + 1)) + "ms"); } } System.out.println("\nDither only:"); System.out.println("Total time (" + speedTest + " invocations): " + time + "ms"); System.out.println("Average: " + time / speedTest + "ms"); // END SPEED TESTING } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy