1 /*
   2  * Copyright (c) 2005, 2017, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.sun.imageio.plugins.common;
  27 
  28 import java.awt.Transparency;
  29 import java.awt.image.BufferedImage;
  30 import java.awt.image.RenderedImage;
  31 import java.awt.image.ColorModel;
  32 import java.awt.image.IndexColorModel;
  33 import java.awt.image.Raster;
  34 import java.awt.image.WritableRaster;
  35 import java.awt.Color;
  36 import javax.imageio.ImageTypeSpecifier;
  37 
  38 
  39 /**
  40  * This class implements the octree quantization method
  41  *  as it is described in the "Graphics Gems"
  42  *  (ISBN 0-12-286166-3, Chapter 4, pages 297-293)
  43  */
  44 public class PaletteBuilder {
  45 
  46     /**
  47      * maximum of tree depth
  48      */
  49     protected static final int MAXLEVEL = 8;
  50 
  51     protected RenderedImage src;
  52     protected ColorModel srcColorModel;
  53     protected Raster srcRaster;
  54 
  55     protected int requiredSize;
  56 
  57     protected ColorNode root;
  58 
  59     protected int numNodes;
  60     protected int maxNodes;
  61     protected int currLevel;
  62     protected int currSize;
  63 
  64     protected ColorNode[] reduceList;
  65     protected ColorNode[] palette;
  66 
  67     protected int transparency;
  68     protected ColorNode transColor;
  69 
  70 
  71     /**
  72      * Creates an image representing given image
  73      * {@code src} using {@code IndexColorModel}.
  74      *
  75      * Lossless conversion is not always possible (e.g. if number
  76      * of colors in the  given image exceeds maximum palette size).
  77      * Result image then is an approximation constructed by octree
  78      * quantization method.
  79      *
  80      * @exception IllegalArgumentException if {@code src} is
  81      * {@code null}.
  82      *
  83      * @exception UnsupportedOperationException if implemented method
  84      * is unable to create approximation of {@code src}
  85      * and {@code canCreatePalette} returns {@code false}.
  86      *
  87      * @see #createIndexColorModel
  88      * @see #canCreatePalette
  89      */
  90     public static RenderedImage createIndexedImage(RenderedImage src) {
  91         PaletteBuilder pb = new PaletteBuilder(src);
  92         pb.buildPalette();
  93         return pb.getIndexedImage();
  94     }
  95 
  96     /**
  97      * Creates an palette representing colors from given image
  98      * {@code img}. If number of colors in the given image exceeds
  99      * maximum palette size closest colors would be merged.
 100      *
 101      * @exception IllegalArgumentException if {@code img} is
 102      * {@code null}.
 103      *
 104      * @exception UnsupportedOperationException if implemented method
 105      * is unable to create approximation of {@code img}
 106      * and {@code canCreatePalette} returns {@code false}.
 107      *
 108      * @see #createIndexedImage
 109      * @see #canCreatePalette
 110      */
 111     public static IndexColorModel createIndexColorModel(RenderedImage img) {
 112         PaletteBuilder pb = new PaletteBuilder(img);
 113         pb.buildPalette();
 114         return pb.getIndexColorModel();
 115     }
 116 
 117     /**
 118      * Returns {@code true} if PaletteBuilder is able to create
 119      * palette for given image type.
 120      *
 121      * @param type an instance of {@code ImageTypeSpecifier} to be
 122      * indexed.
 123      *
 124      * @return {@code true} if the {@code PaletteBuilder}
 125      * is likely to be able to create palette for this image type.
 126      *
 127      * @exception IllegalArgumentException if {@code type}
 128      * is {@code null}.
 129      */
 130     public static boolean canCreatePalette(ImageTypeSpecifier type) {
 131         if (type == null) {
 132             throw new IllegalArgumentException("type == null");
 133         }
 134         return true;
 135     }
 136 
 137     /**
 138      * Returns {@code true} if PaletteBuilder is able to create
 139      * palette for given rendered image.
 140      *
 141      * @param image an instance of {@code RenderedImage} to be
 142      * indexed.
 143      *
 144      * @return {@code true} if the {@code PaletteBuilder}
 145      * is likely to be able to create palette for this image type.
 146      *
 147      * @exception IllegalArgumentException if {@code image}
 148      * is {@code null}.
 149      */
 150     public static boolean canCreatePalette(RenderedImage image) {
 151         if (image == null) {
 152             throw new IllegalArgumentException("image == null");
 153         }
 154         ImageTypeSpecifier type = new ImageTypeSpecifier(image);
 155         return canCreatePalette(type);
 156     }
 157 
 158     protected RenderedImage getIndexedImage() {
 159         IndexColorModel icm = getIndexColorModel();
 160 
 161         BufferedImage dst =
 162             new BufferedImage(src.getWidth(), src.getHeight(),
 163                               BufferedImage.TYPE_BYTE_INDEXED, icm);
 164 
 165         WritableRaster wr = dst.getRaster();
 166         for (int y =0; y < dst.getHeight(); y++) {
 167             for (int x = 0; x < dst.getWidth(); x++) {
 168                 Color aColor = getSrcColor(x,y);
 169                 wr.setSample(x, y, 0, findColorIndex(root, aColor));
 170             }
 171         }
 172 
 173         return dst;
 174     }
 175 
 176 
 177     protected PaletteBuilder(RenderedImage src) {
 178         this(src, 256);
 179     }
 180 
 181     protected PaletteBuilder(RenderedImage src, int size) {
 182         this.src = src;
 183         this.srcColorModel = src.getColorModel();
 184         this.srcRaster = src.getData();
 185 
 186         this.transparency =
 187             srcColorModel.getTransparency();
 188 
 189         this.requiredSize = size;
 190     }
 191 
 192     private Color getSrcColor(int x, int y) {
 193         int argb = srcColorModel.getRGB(srcRaster.getDataElements(x, y, null));
 194         return new Color(argb, transparency != Transparency.OPAQUE);
 195     }
 196 
 197     protected int findColorIndex(ColorNode aNode, Color aColor) {
 198         if (transparency != Transparency.OPAQUE &&
 199             aColor.getAlpha() != 0xff)
 200         {
 201             return 0; // default transparnt pixel
 202         }
 203 
 204         if (aNode.isLeaf) {
 205             return aNode.paletteIndex;
 206         } else {
 207             int childIndex = getBranchIndex(aColor, aNode.level);
 208 
 209             return findColorIndex(aNode.children[childIndex], aColor);
 210         }
 211     }
 212 
 213     protected void buildPalette() {
 214         reduceList = new ColorNode[MAXLEVEL + 1];
 215         for (int i = 0; i < reduceList.length; i++) {
 216             reduceList[i] = null;
 217         }
 218 
 219         numNodes = 0;
 220         maxNodes = 0;
 221         root = null;
 222         currSize = 0;
 223         currLevel = MAXLEVEL;
 224 
 225         /*
 226           from the book
 227 
 228         */
 229 
 230         int w = src.getWidth();
 231         int h = src.getHeight();
 232         for (int y = 0; y < h; y++) {
 233             for (int x = 0; x < w; x++) {
 234 
 235                 Color aColor = getSrcColor(w - x - 1, h - y - 1);
 236                 /*
 237                  * If transparency of given image is not opaque we assume all
 238                  * colors with alpha less than 1.0 as fully transparent.
 239                  */
 240                 if (transparency != Transparency.OPAQUE &&
 241                     aColor.getAlpha() != 0xff)
 242                 {
 243                     if (transColor == null) {
 244                         this.requiredSize --; // one slot for transparent color
 245 
 246                         transColor = new ColorNode();
 247                         transColor.isLeaf = true;
 248                     }
 249                     transColor = insertNode(transColor, aColor, 0);
 250                 } else {
 251                     root = insertNode(root, aColor, 0);
 252                 }
 253                 if (currSize > requiredSize) {
 254                     reduceTree();
 255                 }
 256             }
 257         }
 258     }
 259 
 260     protected ColorNode insertNode(ColorNode aNode, Color aColor, int aLevel) {
 261 
 262         if (aNode == null) {
 263             aNode = new ColorNode();
 264             numNodes++;
 265             if (numNodes > maxNodes) {
 266                 maxNodes = numNodes;
 267             }
 268             aNode.level = aLevel;
 269             aNode.isLeaf = (aLevel > MAXLEVEL);
 270             if (aNode.isLeaf) {
 271                 currSize++;
 272             }
 273         }
 274         aNode.colorCount++;
 275         aNode.red   += aColor.getRed();
 276         aNode.green += aColor.getGreen();
 277         aNode.blue  += aColor.getBlue();
 278 
 279         if (!aNode.isLeaf) {
 280             int branchIndex = getBranchIndex(aColor, aLevel);
 281             if (aNode.children[branchIndex] == null) {
 282                 aNode.childCount++;
 283                 if (aNode.childCount == 2) {
 284                     aNode.nextReducible = reduceList[aLevel];
 285                     reduceList[aLevel] = aNode;
 286                 }
 287             }
 288             aNode.children[branchIndex] =
 289                 insertNode(aNode.children[branchIndex], aColor, aLevel + 1);
 290         }
 291         return aNode;
 292     }
 293 
 294     protected IndexColorModel getIndexColorModel() {
 295         int size = currSize;
 296         if (transColor != null) {
 297             size ++; // we need place for transparent color;
 298         }
 299 
 300         byte[] red = new byte[size];
 301         byte[] green = new byte[size];
 302         byte[] blue = new byte[size];
 303 
 304         int index = 0;
 305         palette = new ColorNode[size];
 306         if (transColor != null) {
 307             index ++;
 308         }
 309 
 310         if (root != null) {
 311             findPaletteEntry(root, index, red, green, blue);
 312         }
 313 
 314         IndexColorModel icm = null;
 315         if (transColor  != null) {
 316             icm = new IndexColorModel(8, size, red, green, blue, 0);
 317         } else {
 318             icm = new IndexColorModel(8, currSize, red, green, blue);
 319         }
 320         return icm;
 321     }
 322 
 323     protected int findPaletteEntry(ColorNode aNode, int index,
 324                                    byte[] red, byte[] green, byte[] blue)
 325         {
 326             if (aNode.isLeaf) {
 327                 red[index]   = (byte)(aNode.red/aNode.colorCount);
 328                 green[index] = (byte)(aNode.green/aNode.colorCount);
 329                 blue[index]  = (byte)(aNode.blue/aNode.colorCount);
 330                 aNode.paletteIndex = index;
 331 
 332                 palette[index] = aNode;
 333 
 334                 index++;
 335             } else {
 336                 for (int i = 0; i < 8; i++) {
 337                     if (aNode.children[i] != null) {
 338                         index = findPaletteEntry(aNode.children[i], index,
 339                                                  red, green, blue);
 340                     }
 341                 }
 342             }
 343             return index;
 344         }
 345 
 346     protected int getBranchIndex(Color aColor, int aLevel) {
 347         if (aLevel > MAXLEVEL || aLevel < 0) {
 348             throw new IllegalArgumentException("Invalid octree node depth: " +
 349                                                aLevel);
 350         }
 351 
 352         int shift = MAXLEVEL - aLevel;
 353         int red_index = 0x1 & ((0xff & aColor.getRed()) >> shift);
 354         int green_index = 0x1 & ((0xff & aColor.getGreen()) >> shift);
 355         int blue_index = 0x1 & ((0xff & aColor.getBlue()) >> shift);
 356         int index = (red_index << 2) | (green_index << 1) | blue_index;
 357         return index;
 358     }
 359 
 360     protected void reduceTree() {
 361         int level = reduceList.length - 1;
 362         while (reduceList[level] == null && level >= 0) {
 363             level--;
 364         }
 365 
 366         ColorNode thisNode = reduceList[level];
 367         if (thisNode == null) {
 368             // nothing to reduce
 369             return;
 370         }
 371 
 372         // look for element with lower color count
 373         ColorNode pList = thisNode;
 374         int minColorCount = pList.colorCount;
 375 
 376         int cnt = 1;
 377         while (pList.nextReducible != null) {
 378             if (minColorCount > pList.nextReducible.colorCount) {
 379                 thisNode = pList;
 380                 minColorCount = pList.colorCount;
 381             }
 382             pList = pList.nextReducible;
 383             cnt++;
 384         }
 385 
 386         // save pointer to first reducible node
 387         // NB: current color count for node could be changed in future
 388         if (thisNode == reduceList[level]) {
 389             reduceList[level] = thisNode.nextReducible;
 390         } else {
 391             pList = thisNode.nextReducible; // we need to process it
 392             thisNode.nextReducible = pList.nextReducible;
 393             thisNode = pList;
 394         }
 395 
 396         if (thisNode.isLeaf) {
 397             return;
 398         }
 399 
 400         // reduce node
 401         int leafChildCount = thisNode.getLeafChildCount();
 402         thisNode.isLeaf = true;
 403         currSize -= (leafChildCount - 1);
 404         int aDepth = thisNode.level;
 405         for (int i = 0; i < 8; i++) {
 406             thisNode.children[i] = freeTree(thisNode.children[i]);
 407         }
 408         thisNode.childCount = 0;
 409     }
 410 
 411     protected ColorNode freeTree(ColorNode aNode) {
 412         if (aNode == null) {
 413             return null;
 414         }
 415         for (int i = 0; i < 8; i++) {
 416             aNode.children[i] = freeTree(aNode.children[i]);
 417         }
 418 
 419         numNodes--;
 420         return null;
 421     }
 422 
 423     /**
 424      * The node of color tree.
 425      */
 426     protected class ColorNode {
 427         public boolean isLeaf;
 428         public int childCount;
 429         ColorNode[] children;
 430 
 431         public int colorCount;
 432         public long red;
 433         public long blue;
 434         public long green;
 435 
 436         public int paletteIndex;
 437 
 438         public int level;
 439         ColorNode nextReducible;
 440 
 441         public ColorNode() {
 442             isLeaf = false;
 443             level = 0;
 444             childCount = 0;
 445             children = new ColorNode[8];
 446             for (int i = 0; i < 8; i++) {
 447                 children[i] = null;
 448             }
 449 
 450             colorCount = 0;
 451             red = green = blue = 0;
 452 
 453             paletteIndex = 0;
 454         }
 455 
 456         public int getLeafChildCount() {
 457             if (isLeaf) {
 458                 return 0;
 459             }
 460             int cnt = 0;
 461             for (int i = 0; i < children.length; i++) {
 462                 if (children[i] != null) {
 463                     if (children[i].isLeaf) {
 464                         cnt ++;
 465                     } else {
 466                         cnt += children[i].getLeafChildCount();
 467                     }
 468                 }
 469             }
 470             return cnt;
 471         }
 472 
 473         public int getRGB() {
 474             int r = (int)red/colorCount;
 475             int g = (int)green/colorCount;
 476             int b = (int)blue/colorCount;
 477 
 478             int c = 0xff << 24 | (0xff&r) << 16 | (0xff&g) << 8 | (0xff&b);
 479             return c;
 480         }
 481     }
 482 }