com.day.image.DitherOp Maven / Gradle / Ivy
/************************************************************************* * * ADOBE CONFIDENTIAL * __________________ * * Copyright 2012 Adobe Systems Incorporated * All Rights Reserved. * * NOTICE: All information contained herein is, and remains * the property of Adobe Systems Incorporated and its suppliers, * if any. The intellectual and technical concepts contained * herein are proprietary to Adobe Systems Incorporated and its * suppliers and are protected by trade secret or copyright law. * Dissemination of this information or reproduction of this material * is strictly forbidden unless prior written permission is obtained * from Adobe Systems Incorporated. **************************************************************************/ package com.day.image; import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.image.BufferedImage; import java.awt.image.ColorModel; import java.awt.image.IndexColorModel; import java.awt.image.Raster; import java.awt.image.WritableRaster; /** * The
if no such color exists, which is highly * unlikely under normal circumstances. */ public OctTreeNode getNode(int r, int g, int b) { OctTreeNode node = root; int m = 1 << MAX_DEPTH; while (node != null) { // If the node is already a leaf node, return the index if (node.leaf) return node; // Calculate the child index for the next level m >>= 1; short c = 0; if ((r & m) != 0) c |= 4; if ((g & m) != 0) c |= 2; if ((b & m) != 0) c++; // and go to that child node = node.childs[c]; } // If we get here the node is null and we didn't find anything. // This should only occurr while dithering return null; } /** * Insert or modify the leaf node for the given color. If a leaf for * the color does not exist yet, it will be created as are any missing * parent nodes. If it is existing, the nodes color is weighted by * the color. * * @param r The red color value of the new node * @param g The green color value of the new node * @param b The blue color value of the new node * @param count The number of pixels bearing the given color */ public void insertNode(int r, int g, int b, int count) { OctTreeNode node = root; int m = 1 << MAX_DEPTH; while (node != null) { if (node.leaf) { // if we are on leaf, set color */ node.nofcolors += count; node.sr += r * count; node.sg += g * count; node.sb += b * count; // we're done now, force end of loop break; } else { // we must go further down the tree m >>= 1; int c = 0; if ((r & m) != 0) c |= 4; if ((g & m) != 0) c |= 2; if ((b & m) != 0) c |= 1; // faster than ++ // The requested child does not exist yet, create if (node.childs[c] == null) { // create a new child and increment counter node = node.childs[c] = new OctTreeNode(node); // Have we looked at the last bit ? if (m == 1) { // mark the node as a leaf node and increment counter node.leaf = true; leafCount++; // insert the new leaf node at the previous position // to the anchor node insertNode(redtable[MAX_DEPTH], node); // Increment the size of the circular list rednof[MAX_DEPTH]++; // Assert the maximum depth level reddepth = MAX_DEPTH; } } else { // Go on to the child node = node.childs[c]; } } } } /** * ReturnsDitherOp
provides the dithering capability for reducing * colors of an image to any number of colors. The real number of colors at the * end of color reduction depends on the color profile of the image under * reduction. ** Currently the following two dithering algorithm's are supported : *
*
* * @see Implementation of Image * Dithering/Color Reduction * * @version $Revision$, $Date$ * @author fmeschbe, based on CQ2's rgba2/rgbacolor.c color reduction * @since coati * @audience wad */ public class DitherOp extends AbstractBufferedImageOp { /** * Indicate the use of the simple bit width color reduction algorithm * with no further dithering effects. */ public static final DitherAlgorithm DITHER_NONE = new DitherSimple(); /** * Indicate the use of the simple bit width color reduction algorithm * plus using the simple error correction algorithm. */ public static final DitherAlgorithm DITHER_SIMPLE_ERROR_CORRECTION = new DitherErrorCorrection(); /** The maximum number of colors to accept after reduction. */ private final int nCol; /** * The Color to use for transparency. It is guaranteed, that this color is * part of the colors remaining in the image. */ private final Color transparency; /** * The Color to use for the background. It is guaranteed, that this color is * part of the colors remaining in the image. */ private final Color bgcolor; /** The algorithm used for color reduction. */ private final DitherAlgorithm algorithm; /** * Creates a new- Simple reduction by reducing the bit width of the color components *
- Reduction by reducing the bit width and doing some rudimentary error * correction on neighbouring pixels. *
DitherOp
instance to reduce colors of an * image to the given number of colors. * * @param nCol The maximum number of colors to reduce the image to. This * must be higher than 2. * @param transparency TheColor
of the color to be considered * as transparent. This will be one of the nCol colors if not *null
. * @param bgcolor TheColor
of the color to be considered * as the background. This will be one of the nCol colors if * notnull
. * @param algorithm The dithering algorithm to be used. * @param hints The RenderingHints for the filter operation. This parameter * may benull
and is not currently used. * * @throws NullPointerException if algorithm isnull
. * @throws IllegalArgumentException if nCol is less than 2. */ public DitherOp(int nCol, Color transparency, Color bgcolor, DitherAlgorithm algorithm, RenderingHints hints) { // init base class super(hints); // check algorithm if (algorithm == null) { throw new NullPointerException("algorithm"); } // Check nCol if (nCol < 2) { throw new IllegalArgumentException("nCol < 2"); } this.nCol = nCol; this.transparency = transparency; this.bgcolor = bgcolor; this.algorithm = algorithm; } /** * Converts the source image to an image with IndexColorModel and with * a maximal number of colors. If the source image already has an IndexColorModel and * the map size of the that color model instance * (src.getColorModel().getMapSize()
) is less than or equal to * the number of colors desired, the source image is returned. Else a new * image is returned. * * @param src The source image to convert to theIndexColorModel
. * @param nCol The maximum number of colors to reduce the image to. This * must be higher than 2. * @param transparency TheColor
of the color to be considered * as transparent. This will be one of the nCol colors if not *null
. * @param bgcolor TheColor
of the color to be considered * as the background. This will be one of the nCol colors if * notnull
. * @param hints The RenderingHints for the filter operation. This parameter * may benull
and is not currently used. * * @return An image with theIndexColorModel
color model. If * the source image already has theIndexColorModel
* and the number of colors are less than or equal to *nCol
the source image is returned else a new * image according to the parameters is returned. * * @throws NullPointerException if the source image isnull
. * @throws NullPointerException if algorithm isnull
. * @throws IllegalArgumentException if nCol is less than 2. */ public static BufferedImage convertToIndexed(BufferedImage src, int nCol, Color transparency, Color bgcolor, RenderingHints hints) { if (src == null) { throw new NullPointerException("src image is null"); } // Can't convolve an IndexColorModel. Need to expand it ColorModel srcCM = src.getColorModel(); if (srcCM instanceof IndexColorModel) { IndexColorModel icm = (IndexColorModel) srcCM; // if the index model has the max colors, return it if (icm.getMapSize() <= nCol) { return src; } // else convert to RGBA and continue src = icm.convertToIntDiscrete(src.getRaster(), true); } // make sure alpha is premultiplied as it is ignored later src = ImageSupport.coerceData(src, true); // special case for two-color output boolean doTransparency = transparency != null && nCol > 2; int transpOffset = doTransparency ? 1 : 0; // The instance used as a helper DitherOp dither = new DitherOp(nCol-transpOffset, transparency, bgcolor, DITHER_NONE, hints); // the rasters we work on Raster srcRas = src.getRaster(); // build the color tree for the source image dither.prepareSourceImage(src); OctTree tree = dither.buildColorTree(srcRas); // Dimensions of the layer int w = src.getWidth(); int h = src.getHeight(); // the colormap stuff dither.indexColorTree(tree, false); int size = tree.leafCount + transpOffset; byte[] cmap = tree.createColorMap(new byte[ size * 3 ]); int bits = 0; for (int ts=size; ts > 0; ts>>=1, bits++) {} // get the transparent color node int transparentIndex = size - 1; if (doTransparency) { OctTreeNode node = tree.getNode(transparency.getRed(), transparency.getGreen(), transparency.getBlue()); // modify the index of the transparent color node.nofcolors = transparentIndex; } // build the image IndexColorModel cm = new IndexColorModel(bits, size, cmap, 0, false, transparentIndex); WritableRaster dstRas = cm.createCompatibleWritableRaster(w, h); BufferedImage dst = new BufferedImage(cm, dstRas, cm.isAlphaPremultiplied(), null); // get the chunkbuffer - one image line int bands = srcRas.getNumBands(); int[] srcChunk = new int[bands * w]; int[] dstChunk = new int[w]; // dest raster assumed to have on band // convert the color values for (int y=0; y < h; y++) { srcRas.getPixels(0, y, w, 1, srcChunk); for (int x=srcChunk.length-bands, i=dstChunk.length-1; x >= 0; x-=bands, i--) { if (srcChunk[x+3] == 0 && doTransparency) { dstChunk[i] = transparentIndex; } else { OctTreeNode node = tree.getNode(srcChunk[x], srcChunk[x+1], srcChunk[x+2]); dstChunk[i] = node.nofcolors; } } dstRas.setPixels(0, y, w, 1, dstChunk); } return dst; } //---------- BufferedImageOp interface ------------------------------------- /** * Performs the operation on a BufferedImage. This implementation only cares * to make the images compatible and calls the * {@link #doFilter(BufferedImage, BufferedImage)} to do the actual * filtering operation. ** If the color models for the two images do not match, a color * conversion into the destination color model will be performed. * If the destination image is null, * a BufferedImage with an appropriate ColorModel will be created. *
* Note: The dest image might be clipped if it is not big enough to take * the complete resized image. *
* This method is overwritten to make sure the pixel data is premultiplied * with the alpha value to take the real alpha value into account. * * @param src The src image to be resized. * @param dst The dest image into which to place the resized image. This * may be
null
in which case a new image with the * correct size will be created. * * @return The newly created image (if dest wasnull
) or dest * into which the resized src image has been drawn. * * @throws IllegalArgumentException if the dest image is the same as the * src image. * @throws NullPointerException if the src image isnull
. */ public BufferedImage filter(BufferedImage src, BufferedImage dst) { // make sure alpha is premultiplied src = ImageSupport.coerceData(src, true); return super.filter(src, dst); } //---------- protected ----------------------------------------------------- /** * Analize and reduce the number of colors of the given image. If the number * of colors in the image is less than required number, no image * manipulation is done and the source image is drawn into the destination * image. * * @param srcIm TheBufferedImage
to submit to color reduction. * @param dstIm TheBufferedImage
to use for the image with * at most the number of colors defined with the constructor. * * @throws NullPointerException if either srcIm or dstIm isnull
. */ protected void doFilter(BufferedImage srcIm, BufferedImage dstIm) { // the rasters we work on Raster srcRas = srcIm.getRaster(); WritableRaster dstRas = dstIm.getRaster(); // build the color tree for the source image prepareSourceImage(srcIm); OctTree tree = buildColorTree(srcRas); // If we did not have node reduction, simply copy the image if (!tree.isReduced()) { dstIm.getGraphics().drawImage(srcIm, 0, 0, null); } indexColorTree(tree, false); // replace old colors by new colors algorithm.dither(srcRas, dstRas, tree); } //---------- internal helper ----------------------------------------------- private void prepareSourceImage(BufferedImage srcIm) { // Make all alpha values 0xff if (bgcolor != null) { Graphics2D g2 = srcIm.createGraphics(); g2.setComposite(AlphaComposite.DstOver); Color col = (bgcolor.getAlpha() != 0) ? new Color(bgcolor.getRGB()&0x00ffffff, true) // alpha=0 : bgcolor; g2.setColor(col); g2.fillRect(0, 0, srcIm.getWidth(), srcIm.getHeight()); g2.dispose(); } } private OctTree buildColorTree(Raster srcRas) { // Dimensions of the layer int w = srcRas.getWidth(); int h = srcRas.getHeight(); int size = w * h; // get the chunkbuffer - one image line int bands = srcRas.getNumBands(); int[] chunk = new int[bands * w]; // Build the reduction tree from the pixels OctTree tree = new OctTree(); for (int y=0; y < h; y++) { srcRas.getPixels(0, y, w, 1, chunk); for (int x=chunk.length-bands; x >= 0; x-=bands) { tree.insertNode(chunk[x], chunk[x+1], chunk[x+2], 1); } // Check if reduction is needed while (tree.leafCount > nCol) { // reduce the tree tree.reduceNode(); } } // if we have a transparency color, insert with high weight if (transparency != null) { tree.insertNode(transparency.getRed(), transparency.getGreen(), transparency.getBlue(), size); } // if we have a background color, insert with high weight if (bgcolor != null && !bgcolor.equals(transparency)) { tree.insertNode(bgcolor.getRed(), bgcolor.getGreen(), bgcolor.getBlue(), size); } // Check for the last time now while (tree.leafCount > nCol) { // reduce the tree tree.reduceNode(); } return tree; } private byte[] indexColorTree(OctTree tree, boolean createCMap) { // calculate color values and index tree tree.indexTree(); // if we have transparent color, set alpha of its node to 0 if (transparency != null) { tree.getNode(transparency.getRed(), transparency.getGreen(), transparency.getBlue()).a = 0; } // optionally build the color map from the tree return (createCMap) ? tree.createColorMap(null) : null; } //---------- dither algorithms --------------------------------------------- public static interface DitherAlgorithm { public void dither(Raster src, WritableRaster dst, OctTree tree); } private static class DitherSimple implements DitherAlgorithm { public void dither(Raster src, WritableRaster dst, OctTree tree) { // Dimensions of the layer int w = src.getWidth(); int h = src.getHeight(); // get the chunkbuffer - one image line int bands = src.getNumBands(); int[] chunk = new int[bands * w]; for (int y=0; y < h; y++) { src.getPixels(0, y, w, 1, chunk); for (int x=chunk.length-bands; x >= 0; x-=bands) { OctTreeNode node = tree.getNode(chunk[x], chunk[x+1], chunk[x+2]); chunk[x] = node.r; chunk[x+1] = node.g; chunk[x+2] = node.b; chunk[x+3] = node.a; } dst.setPixels(0, y, w, 1, chunk); } } } private static class DitherErrorCorrection implements DitherAlgorithm { public void dither(Raster src, WritableRaster dst, OctTree tree) { /* // Dimensions of the layer int w = src.getWidth(); int h = src.getHeight(); // get the chunkbuffer - one image line int bands = src.getNumBands(); int[] chunk = new int[bands * w]; // if we have transparency, we have first to fill the alpha channel if (transparency != Long.MAX_VALUE) { for (int i=0; i255) rr = 255; int gg = rgba.g + (eg*3/8); if (gg < 0) gg = 0; if (gg > 255) gg = 255; int bb = rgba.b + (eb*3/8); if (bb < 0) bb = 0; if (bb > 255) bb = 255; orgb[of+1] = orgb[of+1] & 0xff000000 + (rr << 16) + (gg << 8) + bb; } // pixel below if (y < h-1) { rgba.setFromColor(orgb[of+w]); int rr = rgba.r + (er*3/8); if (rr < 0) rr = 0; if (rr > 255) rr = 255; int gg = rgba.g + (eg*3/8); if (gg < 0) gg = 0; if (gg > 255) gg = 255; int bb = rgba.b + (eb*3/8); if (bb < 0) bb = 0; if (bb > 255) bb = 255; orgb[of+w] = orgb[of+w] & 0xff000000 + (rr << 16) + (gg << 8) + bb; } // pixel below, right if ((x < w-1) && (y < h-1)){ rgba.setFromColor(orgb[of+w+1]); int rr = rgba.r + (er/4); if (rr < 0) rr = 0; if (rr > 255) rr = 255; int gg = rgba.g + (eg/4); if (gg < 0) gg = 0; if (gg > 255) gg = 255; int bb = rgba.b + (eb/4); if (bb < 0) bb = 0; if (bb > 255) bb = 255; orgb[of+w+1] = orgb[of+w+1] & 0xff000000 + (rr << 16) + (gg << 8) + bb; } } } } */ } } //---------- internal classes ---------------------------------------------- /** * The OctTreeNode
class represents one node in the * OctTree. */ private static final class OctTreeNode { /** The weighted red component */ public int sr; /** The weighted green component */ public int sg; /** The weighted blue component */ public int sb; /** The unweighted red component, set by {@link #indexTree()}. */ public int r; /** The unweighted green component, set by {@link #indexTree()}. */ public int g; /** The unweighted blue component, set by {@link #indexTree()}. */ public int b; /** The alpha value, opaque by default. */ public int a = 255; /** * The list of children of this node. If this node is a leaf, there * are no children. */ public OctTreeNode[] childs = new OctTreeNode[8]; /** * The parent of this node. This is only used for tree reduction. */ public OctTreeNode parent; /** Set totrue
if this node is a leaf node. */ public boolean leaf; /** * The number of pixels bearing the color value represented by this * node, if this is a leaf node. Else this is not valid and/or set. ** When building the color map this field is used to store the index * number of the color in. */ public int nofcolors; /** The level of the insertion point of this node. */ public int depth; /** * The next node in the color reduction lists ({@link OctTree#redtable}. */ public OctTreeNode next; /** * Create a node under the given parent. The parent is linked and the * depth is set based on the depth of the parent. If the parent is null * the depth set is 0 else it is one higher than the depth of the * parent. * * @param parent The parent under which this node will be located or * null if this is a root or anchor node. */ private OctTreeNode(OctTreeNode parent) { this.depth = (parent != null) ? parent.depth + 1 : 0; this.parent = parent; this.leaf = false; // Of course we are not a leaf this.next = null; } /** * Recurse down the tree, visiting all leaf nodes and calculating their * real unweighted rgb color values. *
* This method is not intended to be used by the OctTree clients * directly but is called by the {@link OctTree#indexTree} method. */ public void indexTree() { if (leaf) { /** * We divide the accumulated color value by the number * of pixels. This may result in rounding errors, so * we introduce the roundoff value to increase the accumulated * color before division to do proper rounding up or down. */ // populate the palette, if we are a leaf. int roundoff = nofcolors >> 1; r = ((sr + roundoff) / nofcolors); g = ((sg + roundoff) / nofcolors); b = ((sb + roundoff) / nofcolors); // Note: Recursion stops here } else { // We are an inner node and have to recurse to our children for (int i=0; i<8; i++) { if (childs[i] != null) { childs[i].indexTree(); } } } } } /** * The
OctTree
class provides the data structure to build * a tree of color values which is very easily traversed and simplifies * color reduction on a bit by bit basis. ** NOTE: This class implements a specialization of the general octal tree. *
* Initially - before reduction - the tree has a fixed height of eight * where each node has 0 (only leafs) to 8 children and leafs are only * located at the bottom. *
* To reduce a node, the children, which must be leafs themselves, are * removed and the node reduced, is changed to a leaf node, effectively * reducing the height of the tree. To help identification of nodes to * reduce, each node carries a weight value, which in our case of layer * colors is equal to the number of pixels having the color identified * by the node. */ private static class OctTree { /** * Maximum depth of the tree. Each node of this depth level * is automatically created as a leaf node. */ private static final int MAX_DEPTH = 8; /** * How many leafs do we have in the tree. This is the number of * distinct colors managed in the tree. */ private int leafCount; /** * Keep a reference to the root node of the tree. This root node * is the start for all operations. */ private final OctTreeNode root = new OctTreeNode(null); /** * Binary trees of all the leaf nodes at specific levels. Initially - * before reduction - all nodes will be in the list number * {@link #MAX_DEPTH}. */ private final OctTreeNode[] redtable = new OctTreeNode[MAX_DEPTH+1]; /** Counts of nodes contained in each of the lists. */ private final int[] rednof = new int[MAX_DEPTH+1]; /** * The maximum depth level still containing leaf nodes. This is * intially - before reduction - set to {@link #MAX_DEPTH} in the * {@link #buildColorTree} method. */ private int reddepth; public OctTree() { for (int i=0; i
null true
if the color tree has been reduced */ public boolean isReduced() { /** * check whether there is a reduction table, except the MAX_DEPTH * one being not empty. */ for (int i=MAX_DEPTH-1; i>= 0; i--) { if (rednof[i] != 0) { return true; } } // no non-empty lists, if we get here return false; } /** * Identifies a node to reduce and reduce its children. Thus the * tree will be reduced and contain upto 7 distinct colors less than * before. Nodes reduced away are removed from the tree. * * @throws NullPointerException if {@link #getReducibleNode} returns * null which is considered a hard error. */ public void reduceNode() { /** * According to the notes to {@link #getReducibleNode()} we won't * getnull
or we really have a problem, in which case * it is legal to throw the NullPointerException if we access the node. */ // Find a reducible node. OctTreeNode node = getReducibleNode(); // Get the weighted color sum of the child nodes, which are // supposed to be leaf nodes. int sr = 0; // sum of red weights of children int sg = 0; // sum of green weights of children int sb = 0; // sum of blue weights of children int cc = 0; // sum of color weights of children int c = 0; // the number child nodes reduced away for (int i=0; i<8; i++) { OctTreeNode child = node.childs[i]; if (child != null) { // adjust sums c++; sr += child.sr; sg += child.sg; sb += child.sb; cc += child.nofcolors; // remove child from redlist deleteNode(redtable[child.depth], child); rednof[child.depth]--; // remove the child node node.childs[i] = null; } } // make the reducible node the new leaf and set the color values node.leaf = true; node.nofcolors = cc; node.sr = sr; node.sg = sg; node.sb = sb; // insert new leaf into the red-table int d = node.depth; insertNode(redtable[d], node); rednof[d]++; // Check whether we removed the last nodes at our children's level if (rednof[d+1] == 0) { reddepth--; } // adjust nofcolors leafCount -= c - 1; } //---------- node management ------------------------------------------- /** * Creates and fills a color map. * * @param cmap The optional byte array to fill with the map. If this is *null
or not big enough to take all entries * a new array is allocated. * * @return The color map either the input cmap or the newly allocated. * See comments on cmap above. */ public byte[] createColorMap(byte[] cmap) { final int mapSize = leafCount * 3; if (cmap == null || cmap.length < mapSize) { cmap = new byte[ mapSize ]; } for (int i=redtable.length-1, j=0; i >= 0 ; i--) { for (OctTreeNode n=redtable[i].next; n != null; n=n.next) { // cache the map index in the node n.nofcolors = j/3; // store the color values in the map cmap[j++] = (byte)(n.r & 0xff); cmap[j++] = (byte)(n.g & 0xff); cmap[j++] = (byte)(n.b & 0xff); } } return cmap; } /** * Deletes the node from the tree defined by its root. If the tree is * empty (root isnull
) or if the node is not found in the * tree, the 'tree' remains unchanged. * * @param root The root of the tree to delete the node from. * @param node The node to delete from the tree. */ private void deleteNode(OctTreeNode root, OctTreeNode node) { OctTreeNode p = root; OctTreeNode q = root.next; while (q != null && q != node) { p = q; q = q.next; } // invariant : q == p.next && (q == null || q == node) if (q == node) { p.next = node.next; // clean node references node.next = null; } } /** * Inserts the node into the binary reduction tree. If the tree is still * empty, that isroot
isnull
, the node * will become the root of the tree. * * @param root The root node of the binary tree ornull
to * build a new tree with the node as the root. * @param node The node to insert into the tree or to make the root node * of a new tree. */ private void insertNode(OctTreeNode root, OctTreeNode node) { OctTreeNode p = root; OctTreeNode q = p.next; while (q != null) { p = q; q = q.next; } p.next = node; node.next = null; // node.next = root.next; // root.next = node; } /** * Returns the parent of a reducible node. A node is reducible if the * number of occurrences of the color, the node represents is less than * or equal to the occurrences of all the colors in the same tree. * * @return The parent of the node with the least number of pixels or * null if no such nodes exist. This latter case is considered * a really hard error and MUST not occurr. * * @throws NullPointerException if the root parameter is *null
. */ private OctTreeNode getReducibleNode() { /** * We assume this list to be relatively short, so the performance * penalty of having to look in the complete list for a candidate * node is small compared to the peformance penalty, we would face * if the insertion and deletion of nodes would have to preserve * ordering - esp. since the ordering criteria (nofcolors) is not * constant. */ // Get a head start for the pixel counter int m = Integer.MAX_VALUE; // Start a the anchor node OctTreeNode n = redtable[reddepth].next; OctTreeNode bn = null; for (; n != null; n = n.next) { if (n.nofcolors < m) { m = n.nofcolors; bn = n; } } // We should get a node with less than Integer.MAX_VALUE pixels // set, else we really have a problem.... return (bn != null) ? bn.parent : null; } } }