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.Transparency;
050import java.awt.image.BufferedImage;
051import java.awt.image.RenderedImage;
052import java.awt.image.ColorModel;
053import java.awt.image.IndexColorModel;
054import java.awt.image.Raster;
055import java.awt.image.WritableRaster;
056import java.awt.Color;
057import javax.imageio.ImageTypeSpecifier;
058
059
060/**
061 * This class implements the octree quantization method 
062 *  as it is described in the "Graphics Gems"
063 *  (ISBN 0-12-286166-3, Chapter 4, pages 297-293)
064 */
065public class PaletteBuilder {
066
067    /**
068     * maximum of tree depth
069     */
070    protected static final int MAXLEVEL = 8;
071
072    protected RenderedImage src;
073    protected ColorModel srcColorModel;
074    protected Raster srcRaster;
075
076    protected int requiredSize;
077
078    protected ColorNode root;
079
080    protected int numNodes;
081    protected int maxNodes;
082    protected int currLevel;
083    protected int currSize;
084
085    protected ColorNode[] reduceList;
086    protected ColorNode[] palette;
087
088    protected int transparency;
089    protected ColorNode transColor;
090
091
092    /**
093     * Creates an image representing given image
094     * <code>src</code> using <code>IndexColorModel<code>.
095     * 
096     * Lossless conversion is not always possible (e.g. if number
097     * of colors in the  given image exceeds maximum palette size).
098     * Result image then is an approximation constructed by octree 
099     * quantization method.
100     *
101     * @exception IllegalArgumentException if <code>src</code> is
102     * <code>null</code>.
103     *
104     * @exception UnsupportedOperationException if implemented method
105     * is unable to create approximation of <code>src</code>
106     * and <code>canCreatePalette</code> returns <code>false</code>.
107     *
108     * @see createIndexColorModel
109     *
110     * @see canCreatePalette
111     *
112     */
113    public static RenderedImage createIndexedImage(RenderedImage src) {
114        PaletteBuilder pb = new PaletteBuilder(src);
115        pb.buildPalette();
116        return pb.getIndexedImage();
117    }
118
119    /**
120     * Creates an palette representing colors from given image
121     * <code>img</code>. If number of colors in the given image exceeds 
122     * maximum palette size closest colors would be merged.
123     *
124     * @exception IllegalArgumentException if <code>img</code> is
125     * <code>null</code>.
126     *
127     * @exception UnsupportedOperationException if implemented method
128     * is unable to create approximation of <code>img</code>
129     * and <code>canCreatePalette</code> returns <code>false</code>.
130     *
131     * @see createIndexedImage
132     *
133     * @see canCreatePalette
134     *
135     */
136    public static IndexColorModel createIndexColorModel(RenderedImage img) {
137        PaletteBuilder pb = new PaletteBuilder(img);
138        pb.buildPalette();
139        return pb.getIndexColorModel();
140    }
141
142    /**
143     * Returns <code>true</code> if PaletteBuilder is able to create
144     * palette for given image type.
145     *
146     * @param type an instance of <code>ImageTypeSpecifier</code> to be
147     * indexed.
148     *
149     * @return <code>true</code> if the <code>PaletteBuilder</code>
150     * is likely to be able to create palette for this image type.
151     *
152     * @exception IllegalArgumentException if <code>type</code>
153     * is <code>null</code>.
154     */
155    public static boolean canCreatePalette(ImageTypeSpecifier type) {
156        if (type == null) {
157            throw new IllegalArgumentException("type == null");
158        }
159        return true;
160    }
161
162    /**
163     * Returns <code>true</code> if PaletteBuilder is able to create
164     * palette for given rendered image.
165     *
166     * @param image an instance of <code>RenderedImage</code> to be
167     * indexed.
168     *
169     * @return <code>true</code> if the <code>PaletteBuilder</code>
170     * is likely to be able to create palette for this image type.
171     *
172     * @exception IllegalArgumentException if <code>image</code>
173     * is <code>null</code>.
174     */
175    public static boolean canCreatePalette(RenderedImage image) {
176        if (image == null) {
177            throw new IllegalArgumentException("image == null");
178        }
179        ImageTypeSpecifier type = new ImageTypeSpecifier(image);
180        return canCreatePalette(type);
181    }
182
183    protected RenderedImage getIndexedImage() {
184        IndexColorModel icm = getIndexColorModel();
185
186        BufferedImage dst =
187            new BufferedImage(src.getWidth(), src.getHeight(),
188                              BufferedImage.TYPE_BYTE_INDEXED, icm);
189
190        WritableRaster wr = dst.getRaster();
191        int minX = src.getMinX();
192        int minY = src.getMinY();
193        for (int y =0; y < dst.getHeight(); y++) {
194            for (int x = 0; x < dst.getWidth(); x++) {
195                Color aColor = getSrcColor(x + minX, y + minY);
196                wr.setSample(x, y, 0, findColorIndex(root, aColor));
197            }
198        }
199        
200        return dst;
201    }
202
203
204    protected PaletteBuilder(RenderedImage src) {
205        this(src, 256);
206    }
207
208    protected PaletteBuilder(RenderedImage src, int size) {
209        this.src = src;
210        this.srcColorModel = src.getColorModel();
211        this.srcRaster = src.getData();
212
213        this.transparency =
214            srcColorModel.getTransparency();
215
216        if (transparency != Transparency.OPAQUE) {
217            this.requiredSize = size - 1;
218            transColor = new ColorNode();
219            transColor.isLeaf = true;
220        } else {
221            this.requiredSize = size;
222        }
223    }
224
225    private Color getSrcColor(int x, int y) {
226        int argb = srcColorModel.getRGB(srcRaster.getDataElements(x, y, null));
227        return new Color(argb, transparency != Transparency.OPAQUE);
228    }
229
230    protected int findColorIndex(ColorNode aNode, Color aColor) {
231        if (transparency != Transparency.OPAQUE &&
232            aColor.getAlpha() != 0xff)
233        {
234            return 0; // default transparnt pixel
235        }
236
237        if (aNode.isLeaf) {
238            return aNode.paletteIndex;
239        } else {
240            int childIndex = getBranchIndex(aColor, aNode.level);
241        
242            return findColorIndex(aNode.children[childIndex], aColor);
243        }
244    }
245
246    protected void buildPalette() {
247        reduceList = new ColorNode[MAXLEVEL + 1];
248        for (int i = 0; i < reduceList.length; i++) {
249            reduceList[i] = null;
250        }
251        
252        numNodes = 0;
253        maxNodes = 0;
254        root = null;
255        currSize = 0;
256        currLevel = MAXLEVEL;
257
258        /*
259          from the book
260
261        */
262        
263        int w = src.getWidth();
264        int h = src.getHeight();
265        int minX = src.getMinX();
266        int minY = src.getMinY();
267        for (int y = 0; y < h; y++) {
268            for (int x = 0; x < w; x++) {
269
270                Color aColor = getSrcColor(w - x + minX - 1, h - y + minY - 1);
271                /*
272                 * If transparency of given image is not opaque we assume all 
273                 * colors with alpha less than 1.0 as fully transparent.
274                 */
275                if (transparency != Transparency.OPAQUE &&
276                    aColor.getAlpha() != 0xff)
277                {
278                    transColor = insertNode(transColor, aColor, 0);
279                } else {
280                    root = insertNode(root, aColor, 0);
281                }
282                if (currSize > requiredSize) {
283                    reduceTree();
284                }
285            }
286        }
287    }
288
289    protected ColorNode insertNode(ColorNode aNode, Color aColor, int aLevel) {
290
291        if (aNode == null) {
292            aNode = new ColorNode();
293            numNodes++;
294            if (numNodes > maxNodes) {
295                maxNodes = numNodes;
296            }
297            aNode.level = aLevel;
298            aNode.isLeaf = (aLevel > MAXLEVEL);
299            if (aNode.isLeaf) {
300                currSize++;
301            }
302        }
303        aNode.colorCount++;
304        aNode.red   += aColor.getRed();
305        aNode.green += aColor.getGreen();
306        aNode.blue  += aColor.getBlue();
307        
308        if (!aNode.isLeaf) {
309            int branchIndex = getBranchIndex(aColor, aLevel);
310            if (aNode.children[branchIndex] == null) {
311                aNode.childCount++;
312                if (aNode.childCount == 2) {
313                    aNode.nextReducible = reduceList[aLevel];
314                    reduceList[aLevel] = aNode;
315                }
316            }
317            aNode.children[branchIndex] =
318                insertNode(aNode.children[branchIndex], aColor, aLevel + 1);
319        }
320        return aNode;
321    }
322
323    protected IndexColorModel getIndexColorModel() {
324        int size = currSize;
325        if (transparency != Transparency.OPAQUE) {
326            size ++; // we need place for transparent color;
327        }
328
329        byte[] red = new byte[size];
330        byte[] green = new byte[size];
331        byte[] blue = new byte[size];
332
333        int index = 0;
334        palette = new ColorNode[size];
335        if (transparency != Transparency.OPAQUE) {
336            index ++;
337        }
338
339        int lastIndex = findPaletteEntry(root, index, red, green, blue);
340
341        IndexColorModel icm = null;
342        if (transparency != Transparency.OPAQUE) {
343            icm = new IndexColorModel(8, size, red, green, blue, 0);
344        } else {
345            icm = new IndexColorModel(8, currSize, red, green, blue);
346        }
347        return icm;
348    }
349
350    protected int findPaletteEntry(ColorNode aNode, int index,
351                                   byte[] red, byte[] green, byte[] blue)
352        {
353            if (aNode.isLeaf) {
354                red[index]   = (byte)(aNode.red/aNode.colorCount);
355                green[index] = (byte)(aNode.green/aNode.colorCount);
356                blue[index]  = (byte)(aNode.blue/aNode.colorCount);
357                aNode.paletteIndex = index;
358
359                palette[index] = aNode;
360
361                index++;
362            } else {
363                for (int i = 0; i < 8; i++) {
364                    if (aNode.children[i] != null) {
365                        index = findPaletteEntry(aNode.children[i], index,
366                                                 red, green, blue);
367                    }
368                }
369            }
370            return index;
371        }
372
373    protected int getBranchIndex(Color aColor, int aLevel) {
374        if (aLevel > MAXLEVEL || aLevel < 0) {
375            throw new IllegalArgumentException("Invalid octree node depth: " +
376                                               aLevel);
377        }
378
379        int shift = MAXLEVEL - aLevel;
380        int red_index = 0x1 & ((0xff & aColor.getRed()) >> shift);
381        int green_index = 0x1 & ((0xff & aColor.getGreen()) >> shift);
382        int blue_index = 0x1 & ((0xff & aColor.getBlue()) >> shift);
383        int index = (red_index << 2) | (green_index << 1) | blue_index;
384        return index;
385    }
386
387    protected void reduceTree() {
388        int level = reduceList.length - 1;
389        while (reduceList[level] == null && level >= 0) {
390            level--;
391        }
392
393        ColorNode thisNode = reduceList[level];
394        if (thisNode == null) {
395            // nothing to reduce
396            return;
397        }
398
399        // look for element with lower color count
400        ColorNode pList = thisNode;
401        int minColorCount = pList.colorCount;
402
403        int cnt = 1;
404        while (pList.nextReducible != null) {
405            if (minColorCount > pList.nextReducible.colorCount) {
406                thisNode = pList;
407                minColorCount = pList.colorCount;
408            }
409            pList = pList.nextReducible;
410            cnt++;
411        }
412
413        // save pointer to first reducible node
414        // NB: current color count for node could be changed in future
415        if (thisNode == reduceList[level]) {
416            reduceList[level] = thisNode.nextReducible;
417        } else {
418            pList = thisNode.nextReducible; // we need to process it
419            thisNode.nextReducible = pList.nextReducible;
420            thisNode = pList;
421        }
422        
423        if (thisNode.isLeaf) {
424            return;
425        }
426
427        // reduce node
428        int leafChildCount = thisNode.getLeafChildCount();
429        thisNode.isLeaf = true;
430        currSize -= (leafChildCount - 1);
431        int aDepth = thisNode.level;
432        for (int i = 0; i < 8; i++) {
433            thisNode.children[i] = freeTree(thisNode.children[i]);
434        }
435        thisNode.childCount = 0;
436    }
437
438    protected ColorNode freeTree(ColorNode aNode) {
439        if (aNode == null) {
440            return null;
441        }
442        for (int i = 0; i < 8; i++) {
443            aNode.children[i] = freeTree(aNode.children[i]);
444        }
445
446        numNodes--;
447        return null;
448    }
449
450    /**
451     * The node of color tree.
452     */
453    protected class ColorNode {
454        public boolean isLeaf;
455        public int childCount;
456        ColorNode[] children;
457
458        public int colorCount;
459        public long red;
460        public long blue;
461        public long green;
462
463        public int paletteIndex;
464
465        public int level;
466        ColorNode nextReducible;
467
468        public ColorNode() {
469            isLeaf = false;
470            level = 0;
471            childCount = 0;
472            children = new ColorNode[8];
473            for (int i = 0; i < 8; i++) {
474                children[i] = null;
475            }
476
477            colorCount = 0;
478            red = green = blue = 0;
479
480            paletteIndex = 0;
481        }
482
483        public int getLeafChildCount() {
484            if (isLeaf) {
485                return 0;
486            }
487            int cnt = 0;
488            for (int i = 0; i < children.length; i++) {
489                if (children[i] != null) {
490                    if (children[i].isLeaf) {
491                        cnt ++;
492                    } else {
493                        cnt += children[i].getLeafChildCount();
494                    }
495                }
496            }
497            return cnt;
498        }
499
500        public int getRGB() {
501            int r = (int)red/colorCount;
502            int g = (int)green/colorCount;
503            int b = (int)blue/colorCount;
504
505            int c = 0xff << 24 | (0xff&r) << 16 | (0xff&g) << 8 | (0xff&b);
506            return c;
507        }
508    }
509}