001/*
002 * $RCSfile: PaletteBuilder.java,v $
003 *
004 * Copyright (c) 2005 Sun Microsystems, Inc. All rights reserved.
005 *
006 * Redistribution and use in source and binary forms, with or without
007 * modification, are permitted provided that the following conditions
008 * are met: 
009 * 
010 * - Redistribution of source code must retain the above copyright 
011 *   notice, this  list of conditions and the following disclaimer.
012 * 
013 * - Redistribution in binary form must reproduce the above copyright
014 *   notice, this list of conditions and the following disclaimer in 
015 *   the documentation and/or other materials provided with the
016 *   distribution.
017 * 
018 * Neither the name of Sun Microsystems, Inc. or the names of 
019 * contributors may be used to endorse or promote products derived 
020 * from this software without specific prior written permission.
021 * 
022 * This software is provided "AS IS," without a warranty of any 
023 * kind. ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND 
024 * WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, 
025 * FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY
026 * EXCLUDED. SUN MIDROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL 
027 * NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF 
028 * USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS
029 * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR 
030 * ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL,
031 * CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND
032 * REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF OR
033 * INABILITY TO USE THIS SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE
034 * POSSIBILITY OF SUCH DAMAGES. 
035 * 
036 * You acknowledge that this software is not designed or intended for 
037 * use in the design, construction, operation or maintenance of any 
038 * nuclear facility. 
039 *
040 * $Revision: 1.3 $
041 * $Date: 2007/08/31 00:06:00 $
042 * $State: Exp $
043 */
044
045
046
047package com.github.jaiimageio.impl.common;
048
049import java.awt.Color;
050import java.awt.Transparency;
051import java.awt.image.BufferedImage;
052import java.awt.image.ColorModel;
053import java.awt.image.IndexColorModel;
054import java.awt.image.Raster;
055import java.awt.image.RenderedImage;
056import java.awt.image.WritableRaster;
057
058import javax.imageio.ImageTypeSpecifier;
059
060
061/**
062 * This class implements the octree quantization method 
063 *  as it is described in the "Graphics Gems"
064 *  (ISBN 0-12-286166-3, Chapter 4, pages 297-293)
065 */
066public class PaletteBuilder {
067
068    /**
069     * maximum of tree depth
070     */
071    protected static final int MAXLEVEL = 8;
072
073    protected RenderedImage src;
074    protected ColorModel srcColorModel;
075    protected Raster srcRaster;
076
077    protected int requiredSize;
078
079    protected ColorNode root;
080
081    protected int numNodes;
082    protected int maxNodes;
083    protected int currLevel;
084    protected int currSize;
085
086    protected ColorNode[] reduceList;
087    protected ColorNode[] palette;
088
089    protected int transparency;
090    protected ColorNode transColor;
091
092
093    /**
094     * Creates an image representing given image
095     * <code>src</code> using <code>IndexColorModel<code>.
096     * 
097     * Lossless conversion is not always possible (e.g. if number
098     * of colors in the  given image exceeds maximum palette size).
099     * Result image then is an approximation constructed by octree 
100     * quantization method.
101     *
102     * @exception IllegalArgumentException if <code>src</code> is
103     * <code>null</code>.
104     *
105     * @exception UnsupportedOperationException if implemented method
106     * is unable to create approximation of <code>src</code>
107     * and <code>canCreatePalette</code> returns <code>false</code>.
108     *
109     * @see #createIndexColorModel(RenderedImage)
110     *
111     * @see #canCreatePalette(RenderedImage)
112     *
113     */
114    public static RenderedImage createIndexedImage(RenderedImage src) {
115        PaletteBuilder pb = new PaletteBuilder(src);
116        pb.buildPalette();
117        return pb.getIndexedImage();
118    }
119
120    /**
121     * Creates an palette representing colors from given image
122     * <code>img</code>. If number of colors in the given image exceeds 
123     * maximum palette size closest colors would be merged.
124     *
125     * @exception IllegalArgumentException if <code>img</code> is
126     * <code>null</code>.
127     *
128     * @exception UnsupportedOperationException if implemented method
129     * is unable to create approximation of <code>img</code>
130     * and <code>canCreatePalette</code> returns <code>false</code>.
131     *
132     * @see #createIndexedImage(RenderedImage)
133     *
134     * @see #canCreatePalette(RenderedImage)
135     *
136     */
137    public static IndexColorModel createIndexColorModel(RenderedImage img) {
138        PaletteBuilder pb = new PaletteBuilder(img);
139        pb.buildPalette();
140        return pb.getIndexColorModel();
141    }
142
143    /**
144     * Returns <code>true</code> if PaletteBuilder is able to create
145     * palette for given image type.
146     *
147     * @param type an instance of <code>ImageTypeSpecifier</code> to be
148     * indexed.
149     *
150     * @return <code>true</code> if the <code>PaletteBuilder</code>
151     * is likely to be able to create palette for this image type.
152     *
153     * @exception IllegalArgumentException if <code>type</code>
154     * is <code>null</code>.
155     */
156    public static boolean canCreatePalette(ImageTypeSpecifier type) {
157        if (type == null) {
158            throw new IllegalArgumentException("type == null");
159        }
160        return true;
161    }
162
163    /**
164     * Returns <code>true</code> if PaletteBuilder is able to create
165     * palette for given rendered image.
166     *
167     * @param image an instance of <code>RenderedImage</code> to be
168     * indexed.
169     *
170     * @return <code>true</code> if the <code>PaletteBuilder</code>
171     * is likely to be able to create palette for this image type.
172     *
173     * @exception IllegalArgumentException if <code>image</code>
174     * is <code>null</code>.
175     */
176    public static boolean canCreatePalette(RenderedImage image) {
177        if (image == null) {
178            throw new IllegalArgumentException("image == null");
179        }
180        ImageTypeSpecifier type = new ImageTypeSpecifier(image);
181        return canCreatePalette(type);
182    }
183
184    protected RenderedImage getIndexedImage() {
185        IndexColorModel icm = getIndexColorModel();
186
187        BufferedImage dst =
188            new BufferedImage(src.getWidth(), src.getHeight(),
189                              BufferedImage.TYPE_BYTE_INDEXED, icm);
190
191        WritableRaster wr = dst.getRaster();
192        int minX = src.getMinX();
193        int minY = src.getMinY();
194        for (int y =0; y < dst.getHeight(); y++) {
195            for (int x = 0; x < dst.getWidth(); x++) {
196                Color aColor = getSrcColor(x + minX, y + minY);
197                wr.setSample(x, y, 0, findColorIndex(root, aColor));
198            }
199        }
200        
201        return dst;
202    }
203
204
205    protected PaletteBuilder(RenderedImage src) {
206        this(src, 256);
207    }
208
209    protected PaletteBuilder(RenderedImage src, int size) {
210        this.src = src;
211        this.srcColorModel = src.getColorModel();
212        this.srcRaster = src.getData();
213
214        this.transparency =
215            srcColorModel.getTransparency();
216
217        if (transparency != Transparency.OPAQUE) {
218            this.requiredSize = size - 1;
219            transColor = new ColorNode();
220            transColor.isLeaf = true;
221        } else {
222            this.requiredSize = size;
223        }
224    }
225
226    private Color getSrcColor(int x, int y) {
227        int argb = srcColorModel.getRGB(srcRaster.getDataElements(x, y, null));
228        return new Color(argb, transparency != Transparency.OPAQUE);
229    }
230
231    protected int findColorIndex(ColorNode aNode, Color aColor) {
232        if (transparency != Transparency.OPAQUE &&
233            aColor.getAlpha() != 0xff)
234        {
235            return 0; // default transparnt pixel
236        }
237
238        if (aNode.isLeaf) {
239            return aNode.paletteIndex;
240        } else {
241            int childIndex = getBranchIndex(aColor, aNode.level);
242        
243            return findColorIndex(aNode.children[childIndex], aColor);
244        }
245    }
246
247    protected void buildPalette() {
248        reduceList = new ColorNode[MAXLEVEL + 1];
249        for (int i = 0; i < reduceList.length; i++) {
250            reduceList[i] = null;
251        }
252        
253        numNodes = 0;
254        maxNodes = 0;
255        root = null;
256        currSize = 0;
257        currLevel = MAXLEVEL;
258
259        /*
260          from the book
261
262        */
263        
264        int w = src.getWidth();
265        int h = src.getHeight();
266        int minX = src.getMinX();
267        int minY = src.getMinY();
268        for (int y = 0; y < h; y++) {
269            for (int x = 0; x < w; x++) {
270
271                Color aColor = getSrcColor(w - x + minX - 1, h - y + minY - 1);
272                /*
273                 * If transparency of given image is not opaque we assume all 
274                 * colors with alpha less than 1.0 as fully transparent.
275                 */
276                if (transparency != Transparency.OPAQUE &&
277                    aColor.getAlpha() != 0xff)
278                {
279                    transColor = insertNode(transColor, aColor, 0);
280                } else {
281                    root = insertNode(root, aColor, 0);
282                }
283                if (currSize > requiredSize) {
284                    reduceTree();
285                }
286            }
287        }
288    }
289
290    protected ColorNode insertNode(ColorNode aNode, Color aColor, int aLevel) {
291
292        if (aNode == null) {
293            aNode = new ColorNode();
294            numNodes++;
295            if (numNodes > maxNodes) {
296                maxNodes = numNodes;
297            }
298            aNode.level = aLevel;
299            aNode.isLeaf = (aLevel > MAXLEVEL);
300            if (aNode.isLeaf) {
301                currSize++;
302            }
303        }
304        aNode.colorCount++;
305        aNode.red   += aColor.getRed();
306        aNode.green += aColor.getGreen();
307        aNode.blue  += aColor.getBlue();
308        
309        if (!aNode.isLeaf) {
310            int branchIndex = getBranchIndex(aColor, aLevel);
311            if (aNode.children[branchIndex] == null) {
312                aNode.childCount++;
313                if (aNode.childCount == 2) {
314                    aNode.nextReducible = reduceList[aLevel];
315                    reduceList[aLevel] = aNode;
316                }
317            }
318            aNode.children[branchIndex] =
319                insertNode(aNode.children[branchIndex], aColor, aLevel + 1);
320        }
321        return aNode;
322    }
323
324    protected IndexColorModel getIndexColorModel() {
325        int size = currSize;
326        if (transparency != Transparency.OPAQUE) {
327            size ++; // we need place for transparent color;
328        }
329
330        byte[] red = new byte[size];
331        byte[] green = new byte[size];
332        byte[] blue = new byte[size];
333
334        int index = 0;
335        palette = new ColorNode[size];
336        if (transparency != Transparency.OPAQUE) {
337            index ++;
338        }
339
340        int lastIndex = findPaletteEntry(root, index, red, green, blue);
341
342        IndexColorModel icm = null;
343        if (transparency != Transparency.OPAQUE) {
344            icm = new IndexColorModel(8, size, red, green, blue, 0);
345        } else {
346            icm = new IndexColorModel(8, currSize, red, green, blue);
347        }
348        return icm;
349    }
350
351    protected int findPaletteEntry(ColorNode aNode, int index,
352                                   byte[] red, byte[] green, byte[] blue)
353        {
354            if (aNode.isLeaf) {
355                red[index]   = (byte)(aNode.red/aNode.colorCount);
356                green[index] = (byte)(aNode.green/aNode.colorCount);
357                blue[index]  = (byte)(aNode.blue/aNode.colorCount);
358                aNode.paletteIndex = index;
359
360                palette[index] = aNode;
361
362                index++;
363            } else {
364                for (int i = 0; i < 8; i++) {
365                    if (aNode.children[i] != null) {
366                        index = findPaletteEntry(aNode.children[i], index,
367                                                 red, green, blue);
368                    }
369                }
370            }
371            return index;
372        }
373
374    protected int getBranchIndex(Color aColor, int aLevel) {
375        if (aLevel > MAXLEVEL || aLevel < 0) {
376            throw new IllegalArgumentException("Invalid octree node depth: " +
377                                               aLevel);
378        }
379
380        int shift = MAXLEVEL - aLevel;
381        int red_index = 0x1 & ((0xff & aColor.getRed()) >> shift);
382        int green_index = 0x1 & ((0xff & aColor.getGreen()) >> shift);
383        int blue_index = 0x1 & ((0xff & aColor.getBlue()) >> shift);
384        int index = (red_index << 2) | (green_index << 1) | blue_index;
385        return index;
386    }
387
388    protected void reduceTree() {
389        int level = reduceList.length - 1;
390        while (reduceList[level] == null && level >= 0) {
391            level--;
392        }
393
394        ColorNode thisNode = reduceList[level];
395        if (thisNode == null) {
396            // nothing to reduce
397            return;
398        }
399
400        // look for element with lower color count
401        ColorNode pList = thisNode;
402        int minColorCount = pList.colorCount;
403
404        int cnt = 1;
405        while (pList.nextReducible != null) {
406            if (minColorCount > pList.nextReducible.colorCount) {
407                thisNode = pList;
408                minColorCount = pList.colorCount;
409            }
410            pList = pList.nextReducible;
411            cnt++;
412        }
413
414        // save pointer to first reducible node
415        // NB: current color count for node could be changed in future
416        if (thisNode == reduceList[level]) {
417            reduceList[level] = thisNode.nextReducible;
418        } else {
419            pList = thisNode.nextReducible; // we need to process it
420            thisNode.nextReducible = pList.nextReducible;
421            thisNode = pList;
422        }
423        
424        if (thisNode.isLeaf) {
425            return;
426        }
427
428        // reduce node
429        int leafChildCount = thisNode.getLeafChildCount();
430        thisNode.isLeaf = true;
431        currSize -= (leafChildCount - 1);
432        int aDepth = thisNode.level;
433        for (int i = 0; i < 8; i++) {
434            thisNode.children[i] = freeTree(thisNode.children[i]);
435        }
436        thisNode.childCount = 0;
437    }
438
439    protected ColorNode freeTree(ColorNode aNode) {
440        if (aNode == null) {
441            return null;
442        }
443        for (int i = 0; i < 8; i++) {
444            aNode.children[i] = freeTree(aNode.children[i]);
445        }
446
447        numNodes--;
448        return null;
449    }
450
451    /**
452     * The node of color tree.
453     */
454    protected class ColorNode {
455        public boolean isLeaf;
456        public int childCount;
457        ColorNode[] children;
458
459        public int colorCount;
460        public long red;
461        public long blue;
462        public long green;
463
464        public int paletteIndex;
465
466        public int level;
467        ColorNode nextReducible;
468
469        public ColorNode() {
470            isLeaf = false;
471            level = 0;
472            childCount = 0;
473            children = new ColorNode[8];
474            for (int i = 0; i < 8; i++) {
475                children[i] = null;
476            }
477
478            colorCount = 0;
479            red = green = blue = 0;
480
481            paletteIndex = 0;
482        }
483
484        public int getLeafChildCount() {
485            if (isLeaf) {
486                return 0;
487            }
488            int cnt = 0;
489            for (int i = 0; i < children.length; i++) {
490                if (children[i] != null) {
491                    if (children[i].isLeaf) {
492                        cnt ++;
493                    } else {
494                        cnt += children[i].getLeafChildCount();
495                    }
496                }
497            }
498            return cnt;
499        }
500
501        public int getRGB() {
502            int r = (int)red/colorCount;
503            int g = (int)green/colorCount;
504            int b = (int)blue/colorCount;
505
506            int c = 0xff << 24 | (0xff&r) << 16 | (0xff&g) << 8 | (0xff&b);
507            return c;
508        }
509    }
510}