From 5cf1fb6cf2e56243eecd09396acea1a0ce00a9df Mon Sep 17 00:00:00 2001 From: Andreas Schmitz Date: Sat, 17 Jan 2015 15:52:09 +0100 Subject: [PATCH] added support for png 8bit with alpha channel --- .../r2d/context/DefaultRenderContext.java | 4 +- .../deegree/style/utils/ColorQuantizer.java | 678 ++++++++++++++++++ .../style/utils/DiffusionFilterOp.java | 286 ++++++++ .../wms/controller/WMSController.java | 2 + 4 files changed, 969 insertions(+), 1 deletion(-) create mode 100644 deegree-core/deegree-core-style/src/main/java/org/deegree/style/utils/ColorQuantizer.java create mode 100644 deegree-core/deegree-core-style/src/main/java/org/deegree/style/utils/DiffusionFilterOp.java diff --git a/deegree-core/deegree-core-rendering-2d/src/main/java/org/deegree/rendering/r2d/context/DefaultRenderContext.java b/deegree-core/deegree-core-rendering-2d/src/main/java/org/deegree/rendering/r2d/context/DefaultRenderContext.java index 6c9095a299..c0be67a66a 100644 --- a/deegree-core/deegree-core-rendering-2d/src/main/java/org/deegree/rendering/r2d/context/DefaultRenderContext.java +++ b/deegree-core/deegree-core-rendering-2d/src/main/java/org/deegree/rendering/r2d/context/DefaultRenderContext.java @@ -57,12 +57,13 @@ import java.io.IOException; import java.io.OutputStream; +import org.deegree.rendering.r2d.Java2DLabelRenderer; import org.deegree.rendering.r2d.Java2DRasterRenderer; import org.deegree.rendering.r2d.Java2DRenderer; import org.deegree.rendering.r2d.Java2DTextRenderer; -import org.deegree.rendering.r2d.Java2DLabelRenderer; import org.deegree.rendering.r2d.Java2DTileRenderer; import org.deegree.rendering.r2d.labelplacement.AutoLabelPlacement; +import org.deegree.style.utils.ColorQuantizer; import org.deegree.style.utils.ImageUtils; /** @@ -160,6 +161,7 @@ public boolean close() format = "bmp"; } if ( format.equals( "png; subtype=8bit" ) || format.equals( "png; mode=8bit" ) ) { + image = ColorQuantizer.quantizeImage( image, 256, false, false ); format = "png"; } return write( image, format, out ); diff --git a/deegree-core/deegree-core-style/src/main/java/org/deegree/style/utils/ColorQuantizer.java b/deegree-core/deegree-core-style/src/main/java/org/deegree/style/utils/ColorQuantizer.java new file mode 100644 index 0000000000..7318ff810f --- /dev/null +++ b/deegree-core/deegree-core-style/src/main/java/org/deegree/style/utils/ColorQuantizer.java @@ -0,0 +1,678 @@ +//$HeadURL$ +/*---------------------------------------------------------------------------- + This file is part of deegree, http://deegree.org/ + Copyright (C) 2001-2012 by: + - Department of Geography, University of Bonn - + and + - lat/lon GmbH - + and + - Occam Labs UG (haftungsbeschränkt) - + + This library is free software; you can redistribute it and/or modify it under + the terms of the GNU Lesser General Public License as published by the Free + Software Foundation; either version 2.1 of the License, or (at your option) + any later version. + This library is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + details. + You should have received a copy of the GNU Lesser General Public License + along with this library; if not, write to the Free Software Foundation, Inc., + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + Contact information: + + lat/lon GmbH + Aennchenstr. 19, 53177 Bonn + Germany + http://lat-lon.de/ + + Department of Geography, University of Bonn + Prof. Dr. Klaus Greve + Postfach 1147, 53001 Bonn + Germany + http://www.geographie.uni-bonn.de/deegree/ + + Occam Labs UG (haftungsbeschränkt) + Godesberger Allee 139, 53175 Bonn + Germany + + e-mail: info@deegree.org + ----------------------------------------------------------------------------*/ +package org.deegree.style.utils; + +/* + * Helma License Notice + * + * The contents of this file are subject to the Helma License + * Version 2.0 (the "License"). You may not use this file except in + * compliance with the License. A copy of the License is available at + * http://adele.helma.org/download/helma/license.txt + * + * Copyright 1998-2003 Helma Software. All Rights Reserved. + * + * $RCSfile$ + * $Author$ + * $Revision$ + * $Date$ + */ + +import java.awt.AlphaComposite; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferByte; +import java.awt.image.DataBufferInt; +import java.awt.image.IndexColorModel; + +/* + * Modifications by Juerg Lehni: + * + * - Ported to Java from C + * - Support for alpha-channels. + * - Returns a BufferedImage of TYPE_BYTE_INDEXED with a IndexColorModel. + * - Dithering of images through helma.image.DiffusionFilterOp by setting + * the dither parameter to true. + * - Support for a transparent color, which is correctly rendered by GIFEncoder. + * All pixels with alpha < 0x80 are converted to this color when the parameter + * alphaToBitmask is set to true. + */ +/* + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + % % + % % + % % + % QQQ U U AAA N N TTTTT IIIII ZZZZZ EEEEE % + % Q Q U U A A NN N T I ZZ E % + % Q Q U U AAAAA N N N T I ZZZ EEEEE % + % Q QQ U U A A N NN T I ZZ E % + % QQQQ UUU A A N N T IIIII ZZZZZ EEEEE % + % % + % % + % Methods to Reduce the Number of Unique Colors in an Image % + % % + % % + % Software Design % + % John Cristy % + % July 1992 % + % % + % % + % Copyright (C) 2003 ImageMagick Studio, a non-profit organization dedicated % + % to making software imaging solutions freely available. % + % % + % Permission is hereby granted, free of charge, to any person obtaining a % + % copy of this software and associated documentation files ("ImageMagick"), % + % to deal in ImageMagick without restriction, including without limitation % + % the rights to use, copy, modify, merge, publish, distribute, sublicense, % + % and/or sell copies of ImageMagick, and to permit persons to whom the % + % ImageMagick is furnished to do so, subject to the following conditions: % + % % + % The above copyright notice and this permission notice shall be included in % + % all copies or substantial portions of ImageMagick. % + % % + % The software is provided "as is", without warranty of any kind, express or % + % implied, including but not limited to the warranties of merchantability, % + % fitness for a particular purpose and noninfringement. In no event shall % + % ImageMagick Studio be liable for any claim, damages or other liability, % + % whether in an action of contract, tort or otherwise, arising from, out of % + % or in connection with ImageMagick or the use or other dealings in % + % ImageMagick. % + % % + % Except as contained in this notice, the name of the ImageMagick Studio % + % shall not be used in advertising or otherwise to promote the sale, use or % + % other dealings in ImageMagick without prior written authorization from the % + % ImageMagick Studio. % + % % + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + % + % Realism in computer graphics typically requires using 24 bits/pixel to + % generate an image. Yet many graphic display devices do not contain the + % amount of memory necessary to match the spatial and color resolution of + % the human eye. The Quantize methods takes a 24 bit image and reduces + % the number of colors so it can be displayed on raster device with less + % bits per pixel. In most instances, the quantized image closely + % resembles the original reference image. + % + % A reduction of colors in an image is also desirable for image + % transmission and real-time animation. + % + % QuantizeImage() takes a standard RGB or monochrome images and quantizes + % them down to some fixed number of colors. + % + % For purposes of color allocation, an image is a set of n pixels, where + % each pixel is a point in RGB space. RGB space is a 3-dimensional + % vector space, and each pixel, Pi, is defined by an ordered triple of + % red, green, and blue coordinates, (Ri, Gi, Bi). + % + % Each primary color component (red, green, or blue) represents an + % intensity which varies linearly from 0 to a maximum value, Cmax, which + % corresponds to full saturation of that color. Color allocation is + % defined over a domain consisting of the cube in RGB space with opposite + % vertices at (0,0,0) and (Cmax, Cmax, Cmax). QUANTIZE requires Cmax = + % 255. + % + % The algorithm maps this domain onto a tree in which each node + % represents a cube within that domain. In the following discussion + % these cubes are defined by the coordinate of two opposite vertices: + % The vertex nearest the origin in RGB space and the vertex farthest from + % the origin. + % + % The tree's root node represents the the entire domain, (0,0,0) through + % (Cmax,Cmax,Cmax). Each lower level in the tree is generated by + % subdividing one node's cube into eight smaller cubes of equal size. + % This corresponds to bisecting the parent cube with planes passing + % through the midpoints of each edge. + % + % The basic algorithm operates in three phases: Classification, + % Reduction, and Assignment. Classification builds a color description + % tree for the image. Reduction collapses the tree until the number it + % represents, at most, the number of colors desired in the output image. + % Assignment defines the output image's color map and sets each pixel's + % color by restorage_class in the reduced tree. Our goal is to minimize + % the numerical discrepancies between the original colors and quantized + % colors (quantization error). + % + % Classification begins by initializing a color description tree of + % sufficient depth to represent each possible input color in a leaf. + % However, it is impractical to generate a fully-formed color description + % tree in the storage_class phase for realistic values of Cmax. If + % colors components in the input image are quantized to k-bit precision, + % so that Cmax= 2k-1, the tree would need k levels below the root node to + % allow representing each possible input color in a leaf. This becomes + % prohibitive because the tree's total number of nodes is 1 + + % sum(i=1, k, 8k). + % + % A complete tree would require 19,173,961 nodes for k = 8, Cmax = 255. + % Therefore, to avoid building a fully populated tree, QUANTIZE: (1) + % Initializes data structures for nodes only as they are needed; (2) + % Chooses a maximum depth for the tree as a function of the desired + % number of colors in the output image (currently log2(colormap size)). + % + % For each pixel in the input image, storage_class scans downward from + % the root of the color description tree. At each level of the tree it + % identifies the single node which represents a cube in RGB space + % containing the pixel's color. It updates the following data for each + % such node: + % + % n1: Number of pixels whose color is contained in the RGB cube which + % this node represents; + % + % n2: Number of pixels whose color is not represented in a node at + % lower depth in the tree; initially, n2 = 0 for all nodes except + % leaves of the tree. + % + % Sr, Sg, Sb: Sums of the red, green, and blue component values for all + % pixels not classified at a lower depth. The combination of these sums + % and n2 will ultimately characterize the mean color of a set of + % pixels represented by this node. + % + % E: The distance squared in RGB space between each pixel contained + % within a node and the nodes' center. This represents the + % quantization error for a node. + % + % Reduction repeatedly prunes the tree until the number of nodes with n2 + % > 0 is less than or equal to the maximum number of colors allowed in + % the output image. On any given iteration over the tree, it selects + % those nodes whose E count is minimal for pruning and merges their color + % statistics upward. It uses a pruning threshold, Ep, to govern node + % selection as follows: + % + % Ep = 0 + % while number of nodes with (n2 > 0) > required maximum number of colors + % prune all nodes such that E <= Ep + % Set Ep to minimum E in remaining nodes + % + % This has the effect of minimizing any quantization error when merging + % two nodes together. + % + % When a node to be pruned has offspring, the pruning procedure invokes + % itself recursively in order to prune the tree from the leaves upward. + % n2, Sr, Sg, and Sb in a node being pruned are always added to the + % corresponding data in that node's parent. This retains the pruned + % node's color characteristics for later averaging. + % + % For each node, n2 pixels exist for which that node represents the + % smallest volume in RGB space containing those pixel's colors. When n2 + % > 0 the node will uniquely define a color in the output image. At the + % beginning of reduction, n2 = 0 for all nodes except a the leaves of + % the tree which represent colors present in the input image. + % + % The other pixel count, n1, indicates the total number of colors within + % the cubic volume which the node represents. This includes n1 - n2 + % pixels whose colors should be defined by nodes at a lower level in the + % tree. + % + % Assignment generates the output image from the pruned tree. The output + % image consists of two parts: (1) A color map, which is an array of + % color descriptions (RGB triples) for each color present in the output + % image; (2) A pixel array, which represents each pixel as an index + % into the color map array. + % + % First, the assignment phase makes one pass over the pruned color + % description tree to establish the image's color map. For each node + % with n2 > 0, it divides Sr, Sg, and Sb by n2 . This produces the mean + % color of all pixels that classify no lower than this node. Each of + % these colors becomes an entry in the color map. + % + % Finally, the assignment phase reclassifies each pixel in the pruned + % tree to identify the deepest node containing the pixel's color. The + % pixel's value in the pixel array becomes the index of this node's mean + % color in the color map. + % + % This method is based on a similar algorithm written by Paul Raveling. + % + % + */ + +public class ColorQuantizer { + public static final int MAX_NODES = 266817; + + public static final int MAX_TREE_DEPTH = 8; + + public static final int MAX_CHILDREN = 16; + + public static final int MAX_RGB = 255; + + static class ClosestColor { + int distance; + + int colorIndex; + } + + static class Node { + Cube cube; + + Node parent; + + Node children[]; + + int numChildren; + + int id; + + int level; + + int uniqueCount; + + int totalRed; + + int totalGreen; + + int totalBlue; + + int totalAlpha; + + long quantizeError; + + int colorIndex; + + Node( Cube cube ) { + this( cube, 0, 0, null ); + this.parent = this; + } + + Node( Cube cube, int id, int level, Node parent ) { + this.cube = cube; + this.parent = parent; + this.id = id; + this.level = level; + this.children = new Node[MAX_CHILDREN]; + this.numChildren = 0; + if ( parent != null ) { + parent.children[id] = this; + parent.numChildren++; + } + cube.numNodes++; + } + + void pruneLevel() { + // Traverse any children. + if ( this.numChildren > 0 ) + for ( int id = 0; id < MAX_CHILDREN; id++ ) + if ( this.children[id] != null ) + this.children[id].pruneLevel(); + if ( this.level == this.cube.depth ) + prune(); + } + + void pruneToCubeDepth() { + // Traverse any children. + if ( this.numChildren > 0 ) + for ( int id = 0; id < MAX_CHILDREN; id++ ) + if ( this.children[id] != null ) + this.children[id].pruneToCubeDepth(); + if ( this.level > this.cube.depth ) + prune(); + } + + void prune() { + // Traverse any children. + if ( this.numChildren > 0 ) + for ( int id = 0; id < MAX_CHILDREN; id++ ) + if ( this.children[id] != null ) + this.children[id].prune(); + // Merge color statistics into parent. + this.parent.uniqueCount += this.uniqueCount; + this.parent.totalRed += this.totalRed; + this.parent.totalGreen += this.totalGreen; + this.parent.totalBlue += this.totalBlue; + this.parent.totalAlpha += this.totalAlpha; + this.parent.children[this.id] = null; + this.parent.numChildren--; + this.cube.numNodes--; + } + + void reduce( long pruningThreshold ) { + // Traverse any children. + if ( this.numChildren > 0 ) + for ( int id = 0; id < MAX_CHILDREN; id++ ) + if ( this.children[id] != null ) + this.children[id].reduce( pruningThreshold ); + if ( this.quantizeError <= pruningThreshold ) + prune(); + else { + // Find minimum pruning threshold. + if ( this.uniqueCount > 0 ) + this.cube.numColors++; + if ( this.quantizeError < this.cube.nextThreshold ) + this.cube.nextThreshold = this.quantizeError; + } + } + + void findClosestColor( int red, int green, int blue, int alpha, ClosestColor closest ) { + // Traverse any children. + if ( this.numChildren > 0 ) + for ( int id = 0; id < MAX_CHILDREN; id++ ) + if ( this.children[id] != null ) + this.children[id].findClosestColor( red, green, blue, alpha, closest ); + if ( this.uniqueCount != 0 ) { + // Determine if this color is "closest". + int dr = ( this.cube.colorMap[0][this.colorIndex] & 0xff ) - red; + int dg = ( this.cube.colorMap[1][this.colorIndex] & 0xff ) - green; + int db = ( this.cube.colorMap[2][this.colorIndex] & 0xff ) - blue; + int da = ( this.cube.colorMap[3][this.colorIndex] & 0xff ) - alpha; + int distance = da * da + dr * dr + dg * dg + db * db; + if ( distance < closest.distance ) { + closest.distance = distance; + closest.colorIndex = this.colorIndex; + } + } + } + + int fillColorMap( byte colorMap[][], int index ) { + // Traverse any children. + if ( this.numChildren > 0 ) + for ( int id = 0; id < MAX_CHILDREN; id++ ) + if ( this.children[id] != null ) + index = this.children[id].fillColorMap( colorMap, index ); + if ( this.uniqueCount != 0 ) { + // Colormap entry is defined by the mean color in this cube. + colorMap[0][index] = (byte) ( this.totalRed / this.uniqueCount + 0.5 ); + colorMap[1][index] = (byte) ( this.totalGreen / this.uniqueCount + 0.5 ); + colorMap[2][index] = (byte) ( this.totalBlue / this.uniqueCount + 0.5 ); + colorMap[3][index] = (byte) ( this.totalAlpha / this.uniqueCount + 0.5 ); + this.colorIndex = index++; + } + return index; + } + } + + static class Cube { + Node root; + + int numColors; + + boolean addTransparency; + + // firstColor is set to 1 when when addTransparency is true! + int firstColor; + + byte colorMap[][]; + + long nextThreshold; + + int numNodes; + + int depth; + + Cube( int maxColors ) { + this.depth = getDepth( maxColors ); + this.numColors = 0; + this.root = new Node( this ); + } + + int getDepth( int numColors ) { + // Depth of color tree is: Log4(colormap size)+2. + int depth; + for ( depth = 1; numColors != 0; depth++ ) + numColors >>= 2; + if ( depth > MAX_TREE_DEPTH ) + depth = MAX_TREE_DEPTH; + if ( depth < 2 ) + depth = 2; + return depth; + } + + void classifyImageColors( BufferedImage image, boolean alphaToBitmask ) { + this.addTransparency = false; + this.firstColor = 0; + + Node node, child; + int x, px, y, index, level, id, count; + int pixel, red, green, blue, alpha; + int bisect, midRed, midGreen, midBlue, midAlpha; + + int width = image.getWidth(); + int height = image.getHeight(); + + // Classify the first 256 colors to a tree depth of MAX_TREE_DEPTH. + int levelThreshold = MAX_TREE_DEPTH; + // create a BufferedImage of only 1 pixel height for fetching the rows + // of the image in the correct format (ARGB) + // This speeds up things by more than factor 2, compared to the standard + // BufferedImage.getRGB solution + BufferedImage row = new BufferedImage( width, 1, BufferedImage.TYPE_INT_ARGB ); + Graphics2D g2d = row.createGraphics(); + int pixels[] = ( (DataBufferInt) row.getRaster().getDataBuffer() ).getData(); + // make sure alpha values do not add up for each row: + g2d.setComposite( AlphaComposite.Src ); + // calculate scanline by scanline in order to safe memory. + // It also seems to run faster like that + for ( y = 0; y < height; y++ ) { + g2d.drawImage( image, null, 0, -y ); + // now pixels contains the rgb values of the row y! + if ( this.numNodes > MAX_NODES ) { + // Prune one level if the color tree is too large. + this.root.pruneLevel(); + this.depth--; + } + for ( x = 0; x < width; ) { + pixel = pixels[x]; + red = ( pixel >> 16 ) & 0xff; + green = ( pixel >> 8 ) & 0xff; + blue = ( pixel >> 0 ) & 0xff; + alpha = ( pixel >> 24 ) & 0xff; + if ( alphaToBitmask ) + alpha = alpha < 0x80 ? 0 : 0xff; + + // skip same pixels, but count them + px = x; + for ( ++x; x < width; x++ ) + if ( pixels[x] != pixel ) + break; + count = x - px; + + // Start at the root and descend the color cube tree. + if ( alpha > 0 ) { + index = MAX_TREE_DEPTH - 1; + bisect = ( MAX_RGB + 1 ) >> 1; + midRed = bisect; + midGreen = bisect; + midBlue = bisect; + midAlpha = bisect; + node = this.root; + for ( level = 1; level <= levelThreshold; level++ ) { + id = ( ( ( red >> index ) & 0x01 ) << 3 | ( ( green >> index ) & 0x01 ) << 2 + | ( ( blue >> index ) & 0x01 ) << 1 | ( ( alpha >> index ) & 0x01 ) ); + bisect >>= 1; + midRed += ( id & 8 ) != 0 ? bisect : -bisect; + midGreen += ( id & 4 ) != 0 ? bisect : -bisect; + midBlue += ( id & 2 ) != 0 ? bisect : -bisect; + midAlpha += ( id & 1 ) != 0 ? bisect : -bisect; + child = node.children[id]; + if ( child == null ) { + // Set colors of new node to contain pixel. + child = new Node( this, id, level, node ); + if ( level == levelThreshold ) { + this.numColors++; + if ( this.numColors == 256 ) { + // More than 256 colors; classify to the + // cube_info.depth tree depth. + levelThreshold = this.depth; + this.root.pruneToCubeDepth(); + } + } + } + // Approximate the quantization error represented by + // this node. + node = child; + int r = red - midRed; + int g = green - midGreen; + int b = blue - midBlue; + int a = alpha - midAlpha; + node.quantizeError += count * ( r * r + g * g + b * b + a * a ); + this.root.quantizeError += node.quantizeError; + index--; + } + // Sum RGB for this leaf for later derivation of the mean + // cube color. + node.uniqueCount += count; + node.totalRed += count * red; + node.totalGreen += count * green; + node.totalBlue += count * blue; + node.totalAlpha += count * alpha; + } else if ( !this.addTransparency ) { + this.addTransparency = true; + this.numColors++; + this.firstColor = 1; // start at 1 as 0 will be the transparent color + } + } + } + } + + void reduceImageColors( int maxColors ) { + this.nextThreshold = 0; + while ( this.numColors > maxColors ) { + long pruningThreshold = this.nextThreshold; + this.nextThreshold = this.root.quantizeError - 1; + this.numColors = this.firstColor; + this.root.reduce( pruningThreshold ); + } + } + + BufferedImage assignImageColors( BufferedImage image, boolean dither, boolean alphaToBitmask ) { + // Allocate image colormap. + this.colorMap = new byte[4][this.numColors]; + this.root.fillColorMap( this.colorMap, this.firstColor ); + // create the right color model, depending on transparency settings: + IndexColorModel icm; + + int width = image.getWidth(); + int height = image.getHeight(); + + if ( alphaToBitmask ) { + if ( this.addTransparency ) { + icm = new IndexColorModel( this.depth, this.numColors, this.colorMap[0], this.colorMap[1], + this.colorMap[2], 0 ); + } else { + icm = new IndexColorModel( this.depth, this.numColors, this.colorMap[0], this.colorMap[1], + this.colorMap[2] ); + } + } else { + icm = new IndexColorModel( this.depth, this.numColors, this.colorMap[0], this.colorMap[1], + this.colorMap[2], this.colorMap[3] ); + } + + // create the indexed BufferedImage: + BufferedImage dest = new BufferedImage( width, height, BufferedImage.TYPE_BYTE_INDEXED, icm ); + + if ( dither ) + new DiffusionFilterOp().filter( image, dest ); + else { + ClosestColor closest = new ClosestColor(); + // convert to indexed color + byte[] dst = ( (DataBufferByte) dest.getRaster().getDataBuffer() ).getData(); + + // create a BufferedImage of only 1 pixel height for fetching + // the rows of the image in the correct format (ARGB) + // This speeds up things by more than factor 2, compared to the + // standard BufferedImage.getRGB solution + BufferedImage row = new BufferedImage( width, 1, BufferedImage.TYPE_INT_ARGB ); + Graphics2D g2d = row.createGraphics(); + int pixels[] = ( (DataBufferInt) row.getRaster().getDataBuffer() ).getData(); + // make sure alpha values do not add up for each row: + g2d.setComposite( AlphaComposite.Src ); + // calculate scanline by scanline in order to safe memory. + // It also seems to run faster like that + Node node; + int x, y, i, id; + int pixel, red, green, blue, alpha; + int pos = 0; + for ( y = 0; y < height; y++ ) { + g2d.drawImage( image, null, 0, -y ); + // now pixels contains the rgb values of the row y! + // filter this row now: + for ( x = 0; x < width; ) { + pixel = pixels[x]; + red = ( pixel >> 16 ) & 0xff; + green = ( pixel >> 8 ) & 0xff; + blue = ( pixel >> 0 ) & 0xff; + alpha = ( pixel >> 24 ) & 0xff; + + if ( alphaToBitmask ) + alpha = alpha < 128 ? 0 : 0xff; + + byte col; + if ( alpha == 0 && this.addTransparency ) { + col = 0; // transparency color is at position 0 of color map + } else { + // walk the tree to find the cube containing that + // color + node = this.root; + for ( i = MAX_TREE_DEPTH - 1; i > 0; i-- ) { + id = ( ( ( red >> i ) & 0x01 ) << 3 | ( ( green >> i ) & 0x01 ) << 2 + | ( ( blue >> i ) & 0x01 ) << 1 | ( ( alpha >> i ) & 0x01 ) ); + if ( node.children[id] == null ) + break; + node = node.children[id]; + } + + // Find the closest color. + closest.distance = Integer.MAX_VALUE; + node.parent.findClosestColor( red, green, blue, alpha, closest ); + col = (byte) closest.colorIndex; + } + + // first color + dst[pos++] = col; + + // next colors the same? + for ( ++x; x < width; x++ ) { + if ( pixels[x] != pixel ) + break; + dst[pos++] = col; + } + } + } + } + return dest; + } + } + + public static BufferedImage quantizeImage( BufferedImage image, int maxColors, boolean dither, boolean alphaToBitmask ) { + Cube cube = new Cube( maxColors ); + cube.classifyImageColors( image, alphaToBitmask ); + cube.reduceImageColors( maxColors ); + return cube.assignImageColors( image, dither, alphaToBitmask ); + } + +} diff --git a/deegree-core/deegree-core-style/src/main/java/org/deegree/style/utils/DiffusionFilterOp.java b/deegree-core/deegree-core-style/src/main/java/org/deegree/style/utils/DiffusionFilterOp.java new file mode 100644 index 0000000000..c59b23aeca --- /dev/null +++ b/deegree-core/deegree-core-style/src/main/java/org/deegree/style/utils/DiffusionFilterOp.java @@ -0,0 +1,286 @@ +//$HeadURL$ +/*---------------------------------------------------------------------------- + This file is part of deegree, http://deegree.org/ + Copyright (C) 2001-2012 by: + - Department of Geography, University of Bonn - + and + - lat/lon GmbH - + and + - Occam Labs UG (haftungsbeschränkt) - + + This library is free software; you can redistribute it and/or modify it under + the terms of the GNU Lesser General Public License as published by the Free + Software Foundation; either version 2.1 of the License, or (at your option) + any later version. + This library is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + details. + You should have received a copy of the GNU Lesser General Public License + along with this library; if not, write to the Free Software Foundation, Inc., + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + Contact information: + + lat/lon GmbH + Aennchenstr. 19, 53177 Bonn + Germany + http://lat-lon.de/ + + Department of Geography, University of Bonn + Prof. Dr. Klaus Greve + Postfach 1147, 53001 Bonn + Germany + http://www.geographie.uni-bonn.de/deegree/ + + Occam Labs UG (haftungsbeschränkt) + Godesberger Allee 139, 53175 Bonn + Germany + + e-mail: info@deegree.org + ----------------------------------------------------------------------------*/ +package org.deegree.style.utils; + +/* + * Helma License Notice + * + * The contents of this file are subject to the Helma License + * Version 2.0 (the "License"). You may not use this file except in + * compliance with the License. A copy of the License is available at + * http://adele.helma.org/download/helma/license.txt + * + * Copyright 1998-2003 Helma Software. All Rights Reserved. + * + * $RCSfile$ + * $Author$ + * $Revision$ + * $Date$ + */ + +/* + * DiffusionFilter code from com.jhlabs.image.DiffusionFilter, Java Image Processing + * Copyright (C) Jerry Huxtable 1998 + * http://www.jhlabs.com/ip/ + * + * Conversion to a BufferedImageOp inspired by: + * http://www.peter-cockerell.net:8080/java/FloydSteinberg/FloydSteinbergFilterOp.java + * + */ + +import java.awt.*; +import java.awt.geom.*; +import java.awt.image.*; + +public class DiffusionFilterOp implements BufferedImageOp { + + protected final static int[] diffusionMatrix = { 0, 0, 0, 0, 0, 7, 3, 5, 1, }; + + private int[] matrix; + + private int sum; + + private boolean serpentine = true; + + private int[] colorMap; + + /** + * Construct a DiffusionFilter + */ + public DiffusionFilterOp() { + setMatrix( diffusionMatrix ); + } + + /** + * Set whether to use a serpentine pattern for return or not. This can reduce 'avalanche' artifacts in the output. + * + * @param serpentine + * true to use serpentine pattern + */ + public void setSerpentine( boolean serpentine ) { + this.serpentine = serpentine; + } + + /** + * Return the serpentine setting + * + * @return the current setting + */ + public boolean getSerpentine() { + return this.serpentine; + } + + public void setMatrix( int[] matrix ) { + this.matrix = matrix; + this.sum = 0; + for ( int i = 0; i < matrix.length; i++ ) + this.sum += matrix[i]; + } + + public int[] getMatrix() { + return this.matrix; + } + + /** + * Do the filter operation + * + * @param src + * The source BufferedImage. Can be any type. + * @param dst + * The destination image. If not null, must be of type TYPE_BYTE_INDEXED + * @return A dithered version of src in a BufferedImage of type TYPE_BYTE_INDEXED + */ + public BufferedImage filter( BufferedImage src, BufferedImage dst ) { + + // If there's no dest. create one + if ( dst == null ) + dst = createCompatibleDestImage( src, null ); + + // Otherwise check that the provided dest is an indexed image + else if ( dst.getType() != BufferedImage.TYPE_BYTE_INDEXED ) { + throw new IllegalArgumentException( "Destination must be of TYPE_BYTE_INDEXED" ); + } + + DataBufferByte dstBuffer = (DataBufferByte) dst.getRaster().getDataBuffer(); + byte dstData[] = dstBuffer.getData(); + + // Other things to test are pixel bit strides, scanline stride and transfer type + // Same goes for the source image + + IndexColorModel icm = (IndexColorModel) dst.getColorModel(); + this.colorMap = new int[icm.getMapSize()]; + icm.getRGBs( this.colorMap ); + + int width = src.getWidth(); + int height = src.getHeight(); + + // This is the offset into the buffer of the current source pixel + int index = 0; + + // Loop through each pixel + // create a BufferedImage of only 1 pixel height for fetching the rows + // of the image in the correct format (ARGB) + // This speeds up things by more than factor 2, compared to the standard + // BufferedImage.getRGB solution + BufferedImage row = new BufferedImage( width, 1, BufferedImage.TYPE_INT_ARGB ); + Graphics2D g2d = row.createGraphics(); + int pixels[] = ( (DataBufferInt) row.getRaster().getDataBuffer() ).getData(); + // make sure alpha values do not add up for each row: + g2d.setComposite( AlphaComposite.Src ); + // calculate scanline by scanline in order to safe memory. + // It also seems to run faster like that + int rowIndex = 0; + for ( int y = 0; y < height; y++, rowIndex += width ) { + g2d.drawImage( src, null, 0, -y ); + // now pixels contains the rgb values of the row y! + boolean reverse = this.serpentine && ( y & 1 ) == 1; + int direction; + if ( reverse ) { + index = width - 1; + direction = -1; + } else { + index = 0; + direction = 1; + } + for ( int x = 0; x < width; x++ ) { + int rgb1 = pixels[index]; + int a1 = ( rgb1 >> 24 ) & 0xff; + int r1 = ( rgb1 >> 16 ) & 0xff; + int g1 = ( rgb1 >> 8 ) & 0xff; + int b1 = rgb1 & 0xff; + + int idx = findIndex( r1, g1, b1, a1 ); + dstData[rowIndex + index] = (byte) idx; + + int rgb2 = this.colorMap[idx]; + int a2 = ( rgb2 >> 24 ) & 0xff; + int r2 = ( rgb2 >> 16 ) & 0xff; + int g2 = ( rgb2 >> 8 ) & 0xff; + int b2 = rgb2 & 0xff; + + int er = r1 - r2; + int eg = g1 - g2; + int eb = b1 - b2; + int ea = a1 - a2; + + for ( int i = -1; i <= 1; i++ ) { + int iy = i + y; + if ( 0 <= iy && iy < height ) { + for ( int j = -1; j <= 1; j++ ) { + int jx = j + x; + if ( 0 <= jx && jx < width ) { + int w; + if ( reverse ) + w = this.matrix[( i + 1 ) * 3 - j + 1]; + else + w = this.matrix[( i + 1 ) * 3 + j + 1]; + if ( w != 0 ) { + int k = reverse ? index - j : index + j; + rgb1 = pixels[k]; + a1 = ( ( rgb1 >> 24 ) & 0xff ) + ea * w / this.sum; + r1 = ( ( rgb1 >> 16 ) & 0xff ) + er * w / this.sum; + g1 = ( ( rgb1 >> 8 ) & 0xff ) + eg * w / this.sum; + b1 = ( rgb1 & 0xff ) + eb * w / this.sum; + pixels[k] = ( clamp( a1 ) << 24 ) | ( clamp( r1 ) << 16 ) | ( clamp( g1 ) << 8 ) + | clamp( b1 ); + } + } + } + } + } + index += direction; + } + } + + return dst; + } + + private static int clamp( int c ) { + if ( c < 0 ) + return 0; + if ( c > 255 ) + return 255; + return c; + } + + int findIndex( int r1, int g1, int b1, int a1 ) + throws ArrayIndexOutOfBoundsException { + int idx = 0; + int dist = Integer.MAX_VALUE; + for ( int i = 0; i < this.colorMap.length; i++ ) { + int rgb2 = this.colorMap[i]; + int da = a1 - ( ( rgb2 >> 24 ) & 0xff ); + int dr = r1 - ( ( rgb2 >> 16 ) & 0xff ); + int dg = g1 - ( ( rgb2 >> 8 ) & 0xff ); + int db = b1 - ( rgb2 & 0xff ); + int newdist = da * da + dr * dr + dg * dg + db * db; + if ( newdist < dist ) { + idx = i; + dist = newdist; + } + } + return idx; + } + + // This always returns an indexed image + public BufferedImage createCompatibleDestImage( BufferedImage src, ColorModel destCM ) { + return new BufferedImage( src.getWidth(), src.getHeight(), BufferedImage.TYPE_BYTE_INDEXED ); + } + + // There are no rendering hints + public RenderingHints getRenderingHints() { + return null; + } + + // No transformation, so return the source point + public Point2D getPoint2D( Point2D srcPt, Point2D dstPt ) { + if ( dstPt == null ) + dstPt = new Point2D.Float(); + dstPt.setLocation( srcPt.getX(), srcPt.getY() ); + return dstPt; + } + + // No transformation, so return the source bounds + public Rectangle2D getBounds2D( BufferedImage src ) { + return src.getRaster().getBounds(); + } +} \ No newline at end of file diff --git a/deegree-services/deegree-services-wms/src/main/java/org/deegree/services/wms/controller/WMSController.java b/deegree-services/deegree-services-wms/src/main/java/org/deegree/services/wms/controller/WMSController.java index b9868d9e2e..b93c5907d2 100644 --- a/deegree-services/deegree-services-wms/src/main/java/org/deegree/services/wms/controller/WMSController.java +++ b/deegree-services/deegree-services-wms/src/main/java/org/deegree/services/wms/controller/WMSController.java @@ -125,6 +125,7 @@ import org.deegree.services.wms.controller.plugins.ImageSerializer; import org.deegree.services.wms.utils.GetMapLimitChecker; import org.deegree.style.StyleRef; +import org.deegree.style.utils.ColorQuantizer; import org.deegree.workspace.ResourceInitException; import org.deegree.workspace.ResourceMetadata; import org.deegree.workspace.Workspace; @@ -670,6 +671,7 @@ public void sendImage( BufferedImage img, HttpResponseBuffer response, String fo format = "bmp"; } if ( format.equals( "png; subtype=8bit" ) || format.equals( "png; mode=8bit" ) ) { + img = ColorQuantizer.quantizeImage( img, 256, false, false ); format = "png"; } LOG.debug( "Sending in format " + format );