IndexImage.java

/*
 * 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 <http://www.apache.org/>.
*
******************************************************************************
*
*/

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.
 * <p>
 * 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}.
 * </p>
 * <p>
 * Color selection speed/accuracy can be controlled using the hints
 * {@link #COLOR_SELECTION_FAST},
 * {@link #COLOR_SELECTION_QUALITY} and
 * {@link #COLOR_SELECTION_DEFAULT}.
 * </p>
 * <p>
 * Transparency support can be controlled using the hints
 * {@link #TRANSPARENCY_OPAQUE},
 * {@link #TRANSPARENCY_BITMASK} and
 * {@link #TRANSPARENCY_TRANSLUCENT}.
 * </p>
 * <hr>
 * <p>
 * <pre>
 * 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 <A href="http://www.apache.org/">http://www.apache.org/</A>
 * </pre>
 * </p>
 *
 * @author <A href="mailto:deweese@apache.org">Thomas DeWeese</A>
 * @author <A href="mailto:jun@oop-reserch.com">Jun Inamori</A>
 * @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
 * @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<Counter>[] 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<Counter>[] 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<Counter> 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<Counter> 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.
     */
    @Deprecated
    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<Counter>[] 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<Counter> v = colors[index];

                if (v == null) {
                    // No colors in this bin yet so create vector and
                    // add color.
                    v = new ArrayList<Counter>();
                    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.
     * <p>
     * The image returned is a new image, the input image is not modified.
     * </p>
     *
     * @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 <EM>not</EM>
     * set.
     *
     * @param pHints hints
     * @return true if the hint {@code COLOR_SELECTION_QUALITY}
     *         is <EM>not</EM> 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.
     * <p>
     * The image returned is a new image, the input image is not modified.
     * </p>
     *
     * @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.
     * <p>
     * The image returned is a new image, the input image is not modified.
     * </p>
     *
     * @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.
     * <p>
     * The image returned is a new image, the input image is not modified.
     * </p>
     *
     * @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.
     * <p>
     * The image returned is a new image, the input image is not modified.
     * </p>
     *
     * @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.
     * <p>
     * The image returned is a new image, the input image is not modified.
     * </p>
     *
     * @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.
     * <p>
     * The image returned is a new image, the input image is not modified.
     * </p>
     *
     * @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 <integer>] [--bgcolor|-b <color>] [--colors|-c <integer> | --grayscale|g | --monochrome|-m | --palette|-p <file>] [--dither|-d (default|diffusion|none)] [--quality|-q (default|high|low)] [--transparency|-t] [--outputformat|-f (gif|jpeg|png|wbmp|...)] [--overwrite|-w] <input> [<output>]");
            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
        }
    }
}