/* (c) 2014 - 2016 Open Source Geospatial Foundation - all rights reserved
* (c) 2001 - 2013 OpenPlans
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.wms.map;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Transparency;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.IndexColorModel;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.awt.image.VolatileImage;
import java.awt.image.WritableRaster;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.media.jai.PlanarImage;
import javax.media.jai.TiledImage;
import org.geoserver.platform.GeoServerExtensions;
import org.geoserver.platform.ServiceException;
import org.geotools.image.ImageWorker;
import org.geotools.image.palette.CustomPaletteBuilder;
import org.geotools.image.palette.InverseColorMapOp;
import org.geotools.renderer.lite.gridcoverage2d.GridCoverageRenderer;
/**
* Provides utility methods for the shared handling of images by the raster map
* and legend producers.
*
* @author Gabriel Roldan
* @author Simone Giannecchini, GeoSolutions S.A.S.
* @version $Id$
*/
public class ImageUtils {
private static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger("org.vfny.geoserver.responses.wms.map");
/**
* This variable is use for testing purposes in order to force this
* {@link GridCoverageRenderer} to dump images at various steps on the disk.
*/
private static boolean DEBUG = Boolean.valueOf(GeoServerExtensions.getProperty("org.geoserver.wms.map.ImageUtils.debug"));
private static String DEBUG_DIR;
static {
if (DEBUG) {
final File tempDir = new File(GeoServerExtensions.getProperty("user.home"),".geoserver");
if (!tempDir.exists() ) {
if(!tempDir.mkdir())
LOGGER.severe("Unable to create debug dir, exiting application!!!");
DEBUG=false;
DEBUG_DIR = null;
} else
{
DEBUG_DIR = tempDir.getAbsolutePath();
LOGGER.fine("MetatileMapOutputFormat debug dir "+DEBUG_DIR);
}
}
}
/**
* Forces the use of the class as a pure utility methods one by declaring a
* private default constructor.
*/
private ImageUtils() {
// do nothing
}
/**
* Sets up a {@link BufferedImage#TYPE_4BYTE_ABGR} if the paletteInverter is
* not provided, or a indexed image otherwise. Subclasses may override this
* method should they need a special kind of image
*
* @param width
* the width of the image to create.
* @param height
* the height of the image to create.
* @param paletteInverter
* an {@link IndexColorModel} if the image is to be indexed, or
* <code>null</code> otherwise.
* @return an image of size <code>width x height</code> appropriate for
* the given color model, if any, and to be used as a transparent
* image or not depending on the <code>transparent</code>
* parameter.
*/
public static BufferedImage createImage(final int width, final int height,
final IndexColorModel palette, final boolean transparent) {
// WARNING: whenever this method is changed, change getDrawingSurfaceMemoryUse
// accordingly
if (palette != null) {
// unfortunately we can't use packed rasters because line rendering
// gets completely
// broken, see GEOS-1312 (https://osgeo-org.atlassian.net/browse/GEOS-1312)
// final WritableRaster raster =
// palette.createCompatibleWritableRaster(width, height);
final WritableRaster raster = Raster.createInterleavedRaster(palette.getTransferType(),
width, height, 1, null);
return new BufferedImage(palette, raster, false, null);
}
if (transparent) {
return new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR);
}
// don't use alpha channel if the image is not transparent (load testing shows this
// image setup is the fastest to draw and encode on
return new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
}
/**
* Computes the memory usage of the buffered image used as the drawing
* surface.
* @param width
* @param height
* @param palette
* @param transparent
*
*/
public static long getDrawingSurfaceMemoryUse(final int width, final int height,
final IndexColorModel palette, final boolean transparent) {
long memory = width * height;
if (palette != null) {
return memory;
}
if (transparent) {
return memory * 4;
}
return memory * 3;
}
/**
* Sets up and returns a {@link Graphics2D} for the given
* <code>preparedImage</code>, which is already prepared with a
* transparent background or the given background color.
*
* @param transparent
* whether the graphics is transparent or not.
* @param bgColor
* the background color to fill the graphics with if its not
* transparent.
* @param preparedImage
* the image for which to create the graphics.
* @param extraHints
* an optional map of extra rendering hints to apply to the
* {@link Graphics2D}, other than
* {@link RenderingHints#KEY_ANTIALIASING}.
* @return a {@link Graphics2D} for <code>preparedImage</code> with
* transparent background if <code>transparent == true</code> or
* with the background painted with <code>bgColor</code>
* otherwise.
*/
public static Graphics2D prepareTransparency(
final boolean transparent,
final Color bgColor,
final RenderedImage preparedImage,
final Map<RenderingHints.Key, Object> extraHints) {
final Graphics2D graphic;
if (preparedImage instanceof BufferedImage) {
graphic = ((BufferedImage) preparedImage).createGraphics();
} else if (preparedImage instanceof TiledImage) {
graphic = ((TiledImage) preparedImage).createGraphics();
} else if (preparedImage instanceof VolatileImage) {
graphic = ((VolatileImage) preparedImage).createGraphics();
} else {
throw new ServiceException("Unrecognized back-end image type");
}
// fill the background with no antialiasing
Map<RenderingHints.Key, Object> hintsMap;
if (extraHints == null) {
hintsMap = new HashMap<RenderingHints.Key, Object>();
} else {
hintsMap = new HashMap<RenderingHints.Key, Object>(extraHints);
}
hintsMap.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
graphic.setRenderingHints(hintsMap);
if (transparent) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("setting to transparent");
}
int type = AlphaComposite.SRC;
graphic.setComposite(AlphaComposite.getInstance(type));
Color c = new Color(bgColor.getRed(), bgColor.getGreen(), bgColor.getBlue(), 0);
graphic.setBackground(bgColor);
graphic.setColor(c);
graphic.fillRect(0, 0, preparedImage.getWidth(), preparedImage.getHeight());
type = AlphaComposite.SRC_OVER;
graphic.setComposite(AlphaComposite.getInstance(type));
} else {
graphic.setColor(bgColor);
graphic.fillRect(0, 0, preparedImage.getWidth(), preparedImage.getHeight());
}
return graphic;
}
/**
*
* @param originalImage
* @param invColorMap may be {@code null}
*
*/
public static RenderedImage forceIndexed8Bitmask(RenderedImage originalImage, final InverseColorMapOp invColorMap) {
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer("Method forceIndexed8Bitmask called ");
LOGGER.finer("invColorMap is null? "+(invColorMap==null));
// check image type
String type = "RI";
if (originalImage instanceof PlanarImage) {
type = "PI";
} else if (originalImage instanceof BufferedImage) {
type = "BI";
}
if(LOGGER.isLoggable(Level.FINER)){
LOGGER.finer("OriginalImage type " + type);
LOGGER.finer("OriginalImage info: " + originalImage.toString());
}
}
// /////////////////////////////////////////////////////////////////
//
// Check what we need to do depending on the color model of the image we
// are working on.
//
// /////////////////////////////////////////////////////////////////
final ColorModel cm = originalImage.getColorModel();
final boolean dataTypeByte = originalImage.getSampleModel().getDataType() == DataBuffer.TYPE_BYTE;
RenderedImage image;
// /////////////////////////////////////////////////////////////////
//
// IndexColorModel and DataBuffer.TYPE_BYTE
//
// ///
//
// If we got an image whose color model is already indexed on 8 bits
// we have to check if it is bitmask or not.
//
// /////////////////////////////////////////////////////////////////
if ((cm instanceof IndexColorModel) && dataTypeByte) {
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer("Image has IndexColorModel and type byte!");
}
final IndexColorModel icm = (IndexColorModel) cm;
if (icm.getTransparency() != Transparency.TRANSLUCENT) {
// //
//
// The image is indexed on 8 bits and the color model is either
// opaque or bitmask. WE do not have to do anything.
//
// //
image = originalImage;
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer("Image has Transparency != TRANSLUCENT, do nothing");
}
} else {
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer("Image has Transparency TRANSLUCENT, forceBitmaskIndexColorModel");
}
// //
//
// The image is indexed on 8 bits and the color model is
// Translucent, we have to perform some color operations in
// order to convert it to bitmask.
//
// //
image = new ImageWorker(originalImage).forceBitmaskIndexColorModel().getRenderedImage();
if(DEBUG){
writeRenderedImage(image, "indexed8translucent");
}
}
} else {
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer("Image has generic color model and/or type");
}
// /////////////////////////////////////////////////////////////////
//
// NOT IndexColorModel and DataBuffer.TYPE_BYTE
//
// ///
//
// We got an image that needs to be converted.
//
// /////////////////////////////////////////////////////////////////
image = new ImageWorker(originalImage).rescaleToBytes().getRenderedImage();
if (invColorMap != null) {
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer("We have an invColorMap");
}
// make me parametric which means make me work with other image
// types
image = invColorMap.filterRenderedImage(image);
if(DEBUG){
writeRenderedImage(image, "invColorMap");
}
} else {
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer("We do not have an invColorMap");
}
// //
//
// We do not have a paletteInverter, let's create a palette that
// is as good as possible.
//
// //
// make sure we start from a componentcolormodel.
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer("Making sure we start from a componentcolormodel");
}
image = new ImageWorker(image).forceComponentColorModel().getRenderedImage();
if(DEBUG){
writeRenderedImage(image, "forceComponentColorModel");
}
// //
//
// Build the CustomPaletteBuilder doing some good subsampling.
//
// //
int subsx = 1 + (int) (Math.log(image.getWidth()) / Math.log(32));
int subsy = 1 + (int) (Math.log(image.getHeight()) / Math.log(32));
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer("CustomPaletteBuilder[subsx="+subsx+",subsy="+subsy+"]");
LOGGER.finer("InputImage is:"+image.toString());
}
CustomPaletteBuilder cpb=new CustomPaletteBuilder(image, 256, subsx, subsy, 1).buildPalette();
image = cpb.getIndexedImage();
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer("Computed Palette:"+paletteRepresentation(cpb.getIndexColorModel()));
}
if(DEBUG){
writeRenderedImage(image, "buildPalette");
}
}
}
return image;
}
private static String paletteRepresentation(IndexColorModel indexColorModel) {
final StringBuilder builder = new StringBuilder();
final int mapSize= indexColorModel.getMapSize();
builder.append("PaletteSize:").append(mapSize).append("\n");
builder.append("Transparency:").append(indexColorModel.getTransparency()).append("\n");
builder.append("TransparentPixel:").append(indexColorModel.getTransparentPixel()).append("\n");
for(int i=0;i<mapSize;i++){
builder.append("[r=").append(indexColorModel.getRed(i)).append(",");
builder.append("[g=").append(indexColorModel.getGreen(i)).append(",");
builder.append("[b=").append(indexColorModel.getBlue(i)).append("]\n");
}
return builder.toString();
}
/**
* Write the provided {@link RenderedImage} in the debug directory with the provided file name.
*
* @param raster
* the {@link RenderedImage} that we have to write.
* @param fileName
* a {@link String} indicating where we should write it.
*/
static void writeRenderedImage(final RenderedImage raster, final String fileName) {
if (DEBUG_DIR == null)
throw new NullPointerException(
"Unable to write the provided coverage in the debug directory");
if (DEBUG == false)
throw new IllegalStateException(
"Unable to write the provided coverage since we are not in debug mode");
try {
ImageIO.write(raster, "tiff", new File(DEBUG_DIR, fileName + ".tiff"));
} catch (IOException e) {
LOGGER.log(Level.SEVERE, e.getLocalizedMessage(), e);
}
}
}