/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2006-2008, Open Source Geospatial Foundation (OSGeo)
*
* 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;
* version 2.1 of the License.
*
* 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.
*/
package org.geotools.image;
import java.awt.Color;
import java.awt.HeadlessException;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Transparency;
import java.awt.color.ColorSpace;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.ComponentColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DirectColorModel;
import java.awt.image.IndexColorModel;
import java.awt.image.PackedColorModel;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel;
import java.awt.image.WritableRaster;
import java.awt.image.renderable.ParameterBlock;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.IIOException;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.plugins.jpeg.JPEGImageWriteParam;
import javax.imageio.spi.IIORegistry;
import javax.imageio.spi.ImageOutputStreamSpi;
import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.stream.ImageOutputStream;
import javax.media.jai.ColorCube;
import javax.media.jai.IHSColorSpace;
import javax.media.jai.ImageLayout;
import javax.media.jai.Interpolation;
import javax.media.jai.JAI;
import javax.media.jai.KernelJAI;
import javax.media.jai.LookupTableJAI;
import javax.media.jai.ParameterBlockJAI;
import javax.media.jai.ParameterListDescriptor;
import javax.media.jai.PlanarImage;
import javax.media.jai.ROI;
import javax.media.jai.RenderedOp;
import javax.media.jai.TileCache;
import javax.media.jai.Warp;
import javax.media.jai.WarpAffine;
import javax.media.jai.WarpGrid;
import javax.media.jai.operator.AddConstDescriptor;
import javax.media.jai.operator.AddDescriptor;
import javax.media.jai.operator.AffineDescriptor;
import javax.media.jai.operator.AndDescriptor;
import javax.media.jai.operator.BandCombineDescriptor;
import javax.media.jai.operator.BandMergeDescriptor;
import javax.media.jai.operator.BandSelectDescriptor;
import javax.media.jai.operator.BinarizeDescriptor;
import javax.media.jai.operator.ColorConvertDescriptor;
import javax.media.jai.operator.ConstantDescriptor;
import javax.media.jai.operator.ErrorDiffusionDescriptor;
import javax.media.jai.operator.ExtremaDescriptor;
import javax.media.jai.operator.FormatDescriptor;
import javax.media.jai.operator.InvertDescriptor;
import javax.media.jai.operator.LookupDescriptor;
import javax.media.jai.operator.MultiplyConstDescriptor;
import javax.media.jai.operator.NotDescriptor;
import javax.media.jai.operator.NullDescriptor;
import javax.media.jai.operator.OrderedDitherDescriptor;
import javax.media.jai.operator.RescaleDescriptor;
import javax.media.jai.operator.ScaleDescriptor;
import javax.media.jai.operator.XorConstDescriptor;
import javax.media.jai.registry.RenderedRegistryMode;
import org.geotools.factory.Hints;
import org.geotools.image.crop.GTCropDescriptor;
import org.geotools.image.io.ImageIOExt;
import org.geotools.referencing.ReferencingFactoryFinder;
import org.geotools.referencing.operation.transform.WarpBuilder;
import org.geotools.resources.Arguments;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.i18n.Errors;
import org.geotools.resources.image.ColorUtilities;
import org.geotools.resources.image.ImageUtilities;
import org.geotools.util.logging.Logging;
import org.jaitools.imageutils.ImageLayout2;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.MathTransform2D;
import org.opengis.referencing.operation.MathTransformFactory;
import com.sun.imageio.plugins.png.PNGImageWriter;
import com.sun.media.imageioimpl.common.BogusColorSpace;
import com.sun.media.imageioimpl.common.PackageUtil;
import com.sun.media.imageioimpl.plugins.gif.GIFImageWriter;
import com.sun.media.jai.util.ImageUtil;
/**
* Helper methods for applying JAI operations on an image. The image is specified at {@linkplain #ImageWorker(RenderedImage) creation time}. Sucessive
* operations can be applied by invoking the methods defined in this class, and the final image can be obtained by invoking {@link #getRenderedImage}
* at the end of the process.
* <p>
* If an exception is thrown during a method invocation, then this {@code ImageWorker} is left in an undetermined state and should not be used
* anymore.
*
* @since 2.3
*
*
* @source $URL$
* @version $Id$
* @author Simone Giannecchini
* @author Bryce Nordgren
* @author Martin Desruisseaux
*/
public class ImageWorker {
/**
* The logger to use for this class.
*/
private final static Logger LOGGER = Logging.getLogger("org.geotools.image");
/** CODEC_LIB_AVAILABLE */
private static final boolean CODEC_LIB_AVAILABLE = PackageUtil.isCodecLibAvailable();
/** JDK_JPEG_IMAGE_WRITER_SPI */
private static final ImageWriterSpi JDK_JPEG_IMAGE_WRITER_SPI;
static {
ImageWriterSpi temp = null;
try {
Class<?> clazz = Class.forName("com.sun.imageio.plugins.jpeg.JPEGImageWriterSpi");
if (clazz != null) {
temp = (ImageWriterSpi) clazz.newInstance();
} else {
temp = null;
}
} catch (Exception e) {
LOGGER.log(Level.FINER, e.getMessage(), e);
temp = null;
}
// assign
JDK_JPEG_IMAGE_WRITER_SPI = temp;
}
/** IMAGEIO_GIF_IMAGE_WRITER_SPI */
private static final ImageWriterSpi IMAGEIO_GIF_IMAGE_WRITER_SPI;
static {
ImageWriterSpi temp = null;
try {
Class<?> clazz = Class
.forName("com.sun.media.imageioimpl.plugins.gif.GIFImageWriterSpi");
if (clazz != null) {
temp = (ImageWriterSpi) clazz.newInstance();
} else {
temp = null;
}
} catch (Exception e) {
LOGGER.log(Level.FINER, e.getMessage(), e);
temp = null;
}
// assign
IMAGEIO_GIF_IMAGE_WRITER_SPI = temp;
}
/** IMAGEIO_JPEG_IMAGE_WRITER_SPI */
private static final ImageWriterSpi IMAGEIO_JPEG_IMAGE_WRITER_SPI;
static {
ImageWriterSpi temp = null;
try {
Class<?> clazz = Class
.forName("com.sun.media.imageioimpl.plugins.jpeg.CLibJPEGImageWriterSpi");
if (clazz != null && PackageUtil.isCodecLibAvailable()) {
temp = (ImageWriterSpi) clazz.newInstance();
} else {
temp = null;
}
} catch (Exception e) {
LOGGER.log(Level.FINER, e.getMessage(), e);
temp = null;
}
// assign
IMAGEIO_JPEG_IMAGE_WRITER_SPI = temp;
}
/** IMAGEIO_EXT_TIFF_IMAGE_WRITER_SPI */
private static final ImageWriterSpi IMAGEIO_EXT_TIFF_IMAGE_WRITER_SPI;
static {
ImageWriterSpi temp = null;
try {
Class<?> clazz = Class
.forName("it.geosolutions.imageioimpl.plugins.tiff.TIFFImageWriterSpi");
if (clazz != null) {
temp = (ImageWriterSpi) clazz.newInstance();
} else {
temp = null;
}
} catch (Exception e) {
LOGGER.log(Level.FINER, e.getMessage(), e);
temp = null;
}
// assign
IMAGEIO_EXT_TIFF_IMAGE_WRITER_SPI = temp;
}
/** IMAGEIO_PNG_IMAGE_WRITER_SPI */
private static final ImageWriterSpi CLIB_PNG_IMAGE_WRITER_SPI;
static {
ImageWriterSpi temp = null;
try {
Class<?> clazz = Class
.forName("com.sun.media.imageioimpl.plugins.png.CLibPNGImageWriterSpi");
if (clazz != null && PackageUtil.isCodecLibAvailable()) {
temp = (ImageWriterSpi) clazz.newInstance();
} else {
temp = null;
}
} catch (Exception e) {
LOGGER.log(Level.FINER, e.getMessage(), e);
temp = null;
}
// assign
CLIB_PNG_IMAGE_WRITER_SPI = temp;
}
/**
* Raster space epsilon
*/
static final float RS_EPS = 1E-02f;
/**
* Controls the warp-affine reduction
*/
public static final String WARP_REDUCTION_ENABLED_KEY = "org.geotools.image.reduceWarpAffine";
static boolean WARP_REDUCTION_ENABLED = Boolean.parseBoolean(System.getProperty(
WARP_REDUCTION_ENABLED_KEY, "TRUE"));
/**
* Workaround class for compressing PNG using the default PNGImageEncoder shipped with the JDK.
* <p>
* {@link PNGImageWriter} does not support {@link ImageWriteParam#setCompressionMode(int)} set to {@link ImageWriteParam#MODE_EXPLICIT}, it only
* allows {@link ImageWriteParam#MODE_DEFAULT}.
*
* @author Simone Giannecchini
*
* @todo Consider moving to {@link org.geotools.image.io} package.
*/
public final static class PNGImageWriteParam extends ImageWriteParam {
/**
* Default constructor.
*/
public PNGImageWriteParam() {
super();
this.canWriteProgressive = true;
this.canWriteCompressed = true;
this.locale = Locale.getDefault();
}
}
/** CS_PYCC */
static final ColorSpace CS_PYCC;
static {
ColorSpace cs = null;
try {
cs = ColorSpace.getInstance(ColorSpace.CS_PYCC);
} catch (Throwable t) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, t.getLocalizedMessage(), t);
}
}
// assign, either null or the real CS
CS_PYCC = cs;
}
/**
* If {@link Boolean#FALSE FALSE}, image operators are not allowed to produce tiled images. The default is {@link Boolean#TRUE TRUE}. The
* {@code FALSE} value is sometime useful for exporting images to some formats that doesn't support tiling (e.g. GIF).
*
* @see #setRenderingHint
*/
public static final Hints.Key TILING_ALLOWED = new Hints.Key(Boolean.class);
/**
* The image property name generated by {@link ExtremaDescriptor}.
*/
private static final String EXTREMA = "extrema";
/**
* Register manually the GTCrop operation, in web containers JAI registration may fails
*/
static {
GTCropDescriptor.register();
if (WARP_REDUCTION_ENABLED) {
GTWarpPropertyGenerator.register(false);
}
LOGGER.log(Level.INFO, "Warp/affine reduction enabled: " + WARP_REDUCTION_ENABLED);
}
/**
* The image specified by the user at construction time, or last time {@link #invalidateStatistics} were invoked. The {@link #getComputedProperty}
* method will not search a property pass this point.
*/
private RenderedImage inheritanceStopPoint;
/**
* The image being built.
*/
protected RenderedImage image;
/**
* The region of interest, or {@code null} if none.
*/
private ROI roi;
/**
* The rendering hints to provides to all image operators. Additional hints may be set (in a separated {@link RenderingHints} object) for
* particular images.
*/
private RenderingHints commonHints;
/**
* 0 if tile cache is enabled, any other value otherwise. This counter is incremented everytime {@code tileCacheEnabled(false)} is invoked, and
* decremented every time {@code tileCacheEnabled(true)} is invoked.
*/
private int tileCacheDisabled = 0;
/**
* Creates a new uninitialized builder for an {@linkplain #load image read}.
*
* @see #load
*/
public ImageWorker() {
inheritanceStopPoint = this.image = null;
}
/**
* Creates a new builder for an image read from the specified file.
*
* @param input The file to read.
* @throws IOException if the file can't be read.
*/
public ImageWorker(final File input) throws IOException {
this(ImageIO.read(input));
}
/**
* Creates a new builder for the specified image. The images to be computed (if any) will save their tiles in the default {@linkplain TileCache
* tile cache}.
*
* @param image The source image.
*/
public ImageWorker(final RenderedImage image) {
inheritanceStopPoint = this.image = image;
}
/**
* Prepare this builder for the specified image. The images to be computed (if any) will save their tiles in the default {@linkplain TileCache
* tile cache}.
*
* @param image The source image.
*/
public final ImageWorker setImage(final RenderedImage image) {
inheritanceStopPoint = this.image = image;
return this;
}
/**
* Creates a new image worker with the same hints but a different image.
*/
private ImageWorker fork(final RenderedImage image) {
final ImageWorker worker = new ImageWorker(image);
if (commonHints != null && !commonHints.isEmpty()) {
RenderingHints hints = new RenderingHints(null);
hints.add(worker.commonHints);
worker.commonHints = hints;
}
return worker;
}
/**
* Loads an image using the provided file name and the {@linkplain #getRenderingHints current hints}, which are used to control caching and
* layout.
*
* @param source Filename of the source image to read.
* @param imageChoice Image index in multipage images.
* @param readMatadata If {@code true}, metadata will be read.
*/
public final void load(final String source, final int imageChoice, final boolean readMetadata) {
final ParameterBlockJAI pbj = new ParameterBlockJAI("ImageRead");
pbj.setParameter("Input", source).setParameter("ImageChoice", Integer.valueOf(imageChoice))
.setParameter("ReadMetadata", Boolean.valueOf(readMetadata))
.setParameter("VerifyInput", Boolean.TRUE);
image = JAI.create("ImageRead", pbj, getRenderingHints());
}
// /////////////////////////////////////////////////////////////////////////////////////
// ////// ////////
// ////// IMAGE, PROPERTIES AND RENDERING HINTS ACCESSORS ////////
// ////// ////////
// /////////////////////////////////////////////////////////////////////////////////////
/**
* Returns the current image.
*
* @return The rendered image.
*
* @see #getBufferedImage
* @see #getPlanarImage
* @see #getRenderedOperation
* @see #getImageAsROI
*/
public final RenderedImage getRenderedImage() {
return image;
}
/**
* Returns the current image as a buffered image.
*
* @return The buffered image.
*
* @see #getRenderedImage
* @see #getPlanarImage
* @see #getRenderedOperation
* @see #getImageAsROI
*
* @since 2.5
*/
public final BufferedImage getBufferedImage() {
if (image instanceof BufferedImage) {
return (BufferedImage) image;
} else {
return getPlanarImage().getAsBufferedImage();
}
}
/**
* Returns the {@linkplain #getRenderedImage rendered image} as a planar image.
*
* @return The planar image.
*
* @see #getRenderedImage
* @see #getRenderedOperation
* @see #getImageAsROI
*/
public final PlanarImage getPlanarImage() {
return PlanarImage.wrapRenderedImage(getRenderedImage());
}
/**
* Returns the {@linkplain #getRenderedImage rendered image} as a rendered operation.
*
* @return The rendered operation.
*
* @see #getRenderedImage
* @see #getPlanarImage
* @see #getImageAsROI
*/
public final RenderedOp getRenderedOperation() {
final RenderedImage image = getRenderedImage();
if (image instanceof RenderedOp) {
return (RenderedOp) image;
}
return NullDescriptor.create(image, getRenderingHints());
}
/**
* Returns a {@linkplain ROI Region Of Interest} built from the current {@linkplain #getRenderedImage image}. If the image is multi-bands, then
* this method first computes an estimation of its {@linkplain #intensity intensity}. Next, this method {@linkplain #binarize() binarize} the
* image and constructs a {@link ROI} from the result.
*
* @return The image as a region of interest.
*
* @see #getRenderedImage
* @see #getPlanarImage
* @see #getRenderedOperation
*/
public final ROI getImageAsROI() {
binarize();
return new ROI(getRenderedImage());
}
/**
* Returns the <cite>region of interest</cite> currently set, or {@code null} if none. The default value is {@code null}.
*
* @return The current region of interest.
*
* @see #getMinimums
* @see #getMaximums
*/
public final ROI getROI() {
return roi;
}
/**
* Set the <cite>region of interest</cite> (ROI). A {@code null} set the ROI to the whole {@linkplain #image}. The ROI is used by statistical
* methods like {@link #getMinimums} and {@link #getMaximums}.
*
* @param roi The new region of interest.
* @return This ImageWorker
*
* @see #getMinimums
* @see #getMaximums
*/
public final ImageWorker setROI(final ROI roi) {
this.roi = roi;
invalidateStatistics();
return this;
}
/**
* Returns the rendering hint for the specified key, or {@code null} if none.
*/
public final Object getRenderingHint(final RenderingHints.Key key) {
return (commonHints != null) ? commonHints.get(key) : null;
}
/**
* Sets a rendering hint tile to use for all images to be computed by this class. This method applies only to the next images to be computed;
* images already computed before this method call (if any) will not be affected.
* <p>
* Some common examples:
* <p>
* <ul>
* <li><code>setRenderingHint({@linkplain JAI#KEY_TILE_CACHE}, null)</code> disables completly the tile cache.</li>
* <li><code>setRenderingHint({@linkplain #TILING_ALLOWED}, Boolean.FALSE)</code> forces all operators to produce untiled images.</li>
* </ul>
*
* @return This ImageWorker
*/
public final ImageWorker setRenderingHint(final RenderingHints.Key key, final Object value) {
if (commonHints == null) {
commonHints = new RenderingHints(null);
}
commonHints.add(new RenderingHints(key, value));
return this;
}
/**
* Set a map of rendering hints to use for all images to be computed by this class. This method applies only to the next images to be computed;
* images already computed before this method call (if any) will not be affected.
*
* <p>
* If <code>hints</code> is null we won't modify this list.
*
* @return This ImageWorker
* @see #setRenderingHint(RenderingHints)
*/
public final ImageWorker setRenderingHints(final RenderingHints hints) {
if (commonHints == null) {
commonHints = new RenderingHints(null);
}
if (hints != null)
commonHints.add(hints);
return this;
}
/**
* Removes a rendering hint. Note that invoking this method is <strong>not</strong> the same than invoking
* <code>{@linkplain #setRenderingHint setRenderingHint}(key, null)</code>. This is especially true for the {@linkplain javax.media.jai.TileCache
* tile cache} hint:
* <p>
* <ul>
* <li><code>{@linkplain #setRenderingHint setRenderingHint}({@linkplain JAI#KEY_TILE_CACHE},
* null)</code> disables the use of any tile cache. In other words, this method call do request a tile cache, which happen to be the "null"
* cache.</li>
*
* <li><code>removeRenderingHint({@linkplain JAI#KEY_TILE_CACHE})</code> unsets any tile cache specified by a previous rendering hint. All images
* to be computed after this method call will save their tiles in the {@linkplain JAI#getTileCache JAI default tile cache}.</li>
* </ul>
*
* @return This ImageWorker
*/
public final ImageWorker removeRenderingHint(final RenderingHints.Key key) {
if (commonHints != null) {
commonHints.remove(key);
}
return this;
}
/**
* Returns the rendering hints for an image to be computed by this class. The default implementation returns the following hints:
* <p>
* <ul>
* <li>An {@linkplain ImageLayout image layout} with tiles size computed automatically from the current {@linkplain #image} size.</li>
* <li>Any additional hints specified through the {@link #setRenderingHint} method. If the user provided explicitly a {@link JAI#KEY_IMAGE_LAYOUT}
* , then the user layout has precedence over the automatic layout computed in previous step.</li>
* </ul>
*
* @return The rendering hints to use for image computation (never {@code null}).
*/
public final RenderingHints getRenderingHints() {
RenderingHints hints = ImageUtilities.getRenderingHints(image);
if (hints == null) {
hints = new RenderingHints(null);
if (commonHints != null) {
hints.add(commonHints);
}
} else if (commonHints != null) {
hints.putAll(commonHints);
}
if (Boolean.FALSE.equals(hints.get(TILING_ALLOWED))) {
final ImageLayout layout = getImageLayout(hints);
if (commonHints == null || layout != commonHints.get(JAI.KEY_IMAGE_LAYOUT)) {
// Set the layout only if it is not a user-supplied object.
layout.setTileWidth(image.getWidth());
layout.setTileHeight(image.getHeight());
layout.setTileGridXOffset(image.getMinX());
layout.setTileGridYOffset(image.getMinY());
hints.put(JAI.KEY_IMAGE_LAYOUT, layout);
}
}
if (tileCacheDisabled != 0
&& (commonHints != null && !commonHints.containsKey(JAI.KEY_TILE_CACHE))) {
hints.add(new RenderingHints(JAI.KEY_TILE_CACHE, null));
}
return hints;
}
/**
* Returns the {@linkplain #getRenderingHints rendering hints}, but with a {@linkplain ComponentColorModel component color model} of the specified
* data type. The data type is changed only if no color model was explicitly specified by the user through {@link #getRenderingHints()}.
*
* @param type The data type (typically {@link DataBuffer#TYPE_BYTE}).
*/
private final RenderingHints getRenderingHints(final int type) {
/*
* Gets the default hints, which usually contains only informations about tiling. If the user overridden the rendering hints with an explict
* color model, keep the user's choice.
*/
final RenderingHints hints = getRenderingHints();
final ImageLayout layout = getImageLayout(hints);
if (layout.isValid(ImageLayout.COLOR_MODEL_MASK)) {
return hints;
}
/*
* Creates the new color model.
*/
final ColorModel oldCm = image.getColorModel();
if (oldCm != null) {
final ColorModel newCm = new ComponentColorModel(oldCm.getColorSpace(),
oldCm.hasAlpha(), // If true, supports transparency.
oldCm.isAlphaPremultiplied(), // If true, alpha is premultiplied.
oldCm.getTransparency(), // What alpha values can be represented.
type); // Type of primitive array used to represent pixel.
/*
* Creating the final image layout which should allow us to change color model.
*/
layout.setColorModel(newCm);
layout.setSampleModel(newCm.createCompatibleSampleModel(image.getWidth(),
image.getHeight()));
} else {
final int numBands = image.getSampleModel().getNumBands();
final ColorModel newCm = new ComponentColorModel(new BogusColorSpace(numBands), false, // If true, supports transparency.
false, // If true, alpha is premultiplied.
Transparency.OPAQUE, // What alpha values can be represented.
type); // Type of primitive array used to represent pixel.
/*
* Creating the final image layout which should allow us to change color model.
*/
layout.setColorModel(newCm);
layout.setSampleModel(newCm.createCompatibleSampleModel(image.getWidth(),
image.getHeight()));
}
hints.put(JAI.KEY_IMAGE_LAYOUT, layout);
return hints;
}
/**
* Gets the image layout from the specified rendering hints, creating a new one if needed. This method do not modify the specified hints. If the
* caller modifies the image layout, it should invoke {@code hints.put(JAI.KEY_IMAGE_LAYOUT, layout)} explicitly.
*/
private static ImageLayout getImageLayout(final RenderingHints hints) {
final Object candidate = hints.get(JAI.KEY_IMAGE_LAYOUT);
if (candidate instanceof ImageLayout) {
return (ImageLayout) candidate;
}
return new ImageLayout();
}
/**
* If {@code false}, disables the tile cache. Invoking this method with value {@code true} cancel the last invocation with value {@code false}. If
* this method was invoking many time with value {@code false}, then this method must be invoked the same amount of time with the value
* {@code true} for reenabling the cache.
* <p>
* <strong>Note:</strong> This method name doesn't contain the usual {@code set} prefix because it doesn't really set a flag. Instead it
* increments or decrements a counter.
*
* @return This ImageWorker
*/
public final ImageWorker tileCacheEnabled(final boolean status) {
if (status) {
if (tileCacheDisabled != 0) {
tileCacheDisabled--;
} else {
throw new IllegalStateException();
}
} else {
tileCacheDisabled++;
}
return this;
}
/**
* Returns the number of bands in the {@linkplain #image}.
*
* @see #retainBands
* @see #retainFirstBand
* @see SampleModel#getNumBands
*/
public final int getNumBands() {
return image.getSampleModel().getNumBands();
}
/**
* Returns the transparent pixel value, or -1 if none.
*/
public final int getTransparentPixel() {
final ColorModel cm = image.getColorModel();
return (cm instanceof IndexColorModel) ? ((IndexColorModel) cm).getTransparentPixel() : -1;
}
/**
* Gets a property from the property set of the {@linkplain #image}. If the property name is not recognized, then {@link Image#UndefinedProperty}
* will be returned. This method do <strong>not</strong> inherits properties from the image specified at {@linkplain #ImageWorker(RenderedImage)
* construction time} - only properties generated by this class are returned.
*/
private Object getComputedProperty(final String name) {
final Object value = image.getProperty(name);
return (value == inheritanceStopPoint.getProperty(name)) ? Image.UndefinedProperty : value;
}
/**
* Returns the minimums and maximums values found in the image. Those extremas are returned as an array of the form {@code double[2][#bands]}.
*/
private double[][] getExtremas() {
Object extrema = getComputedProperty(EXTREMA);
if (!(extrema instanceof double[][])) {
final Integer ONE = 1;
image = ExtremaDescriptor.create(image, // The source image.
roi, // The region of the image to scan. Default to all.
ONE, // The horizontal sampling rate. Default to 1.
ONE, // The vertical sampling rate. Default to 1.
null, // Whether to store extrema locations. Default to false.
ONE, // Maximum number of run length codes to store. Default to 1.
getRenderingHints());
extrema = getComputedProperty(EXTREMA);
}
return (double[][]) extrema;
}
/**
* Tells this builder that all statistics on pixel values (e.g. the "extrema" property in the {@linkplain #image}) should not be inherited from
* the source images (if any). This method should be invoked every time an operation changed the pixel values.
*
* @return This ImageWorker
*/
private ImageWorker invalidateStatistics() {
inheritanceStopPoint = image;
return this;
}
/**
* Returns the minimal values found in every {@linkplain #image} bands. If a {@linkplain #getROI region of interest} is defined, then the
* statistics will be computed only over that region.
*
* @see #getMaximums
* @see #setROI
*/
public final double[] getMinimums() {
return getExtremas()[0];
}
/**
* Returns the maximal values found in every {@linkplain #image} bands. If a {@linkplain #getROI region of interest} is defined, then the
* statistics will be computed only over that region.
*
* @see #getMinimums
* @see #setROI
*/
public final double[] getMaximums() {
return getExtremas()[1];
}
// /////////////////////////////////////////////////////////////////////////////////////
// ////// ////////
// ////// KIND OF IMAGE (BYTES, BINARY, INDEXED, RGB...) ////////
// ////// ////////
// /////////////////////////////////////////////////////////////////////////////////////
/**
* Returns {@code true} if the {@linkplain #image} stores its pixel values in 8 bits.
*
* @see #rescaleToBytes
*/
public final boolean isBytes() {
final SampleModel sm = image.getSampleModel();
final int[] sampleSize = sm.getSampleSize();
for (int i = 0; i < sampleSize.length; i++)
if (sampleSize[i] != 8)
return false;
return true;
}
/**
* Returns {@code true} if the {@linkplain #image} is binary. Such image usually contains only two values: 0 and 1.
*
* @see #binarize()
* @see #binarize(double)
* @see #binarize(int,int)
*/
public final boolean isBinary() {
return ImageUtil.isBinary(image.getSampleModel());
}
/**
* Returns {@code true} if the {@linkplain #image} uses an {@linkplain IndexColorModel index color model}.
*
* @see #forceIndexColorModel
* @see #forceBitmaskIndexColorModel
* @see #forceIndexColorModelForGIF
*/
public final boolean isIndexed() {
return image.getColorModel() instanceof IndexColorModel;
}
/**
* Returns {@code true} if the {@linkplain #image} uses a RGB {@linkplain ColorSpace color space}. Note that a RGB color space doesn't mean that
* pixel values are directly stored as RGB components. The image may be {@linkplain #isIndexed indexed} as well.
*
* @see #forceColorSpaceRGB
*/
public final boolean isColorSpaceRGB() {
final ColorModel cm = image.getColorModel();
if (cm == null) {
return false;
}
return cm.getColorSpace().getType() == ColorSpace.TYPE_RGB;
}
/**
* Returns {@code true} if the {@linkplain #image} uses a YCbCr {@linkplain ColorSpace color space}.
*
* @see #forceColorSpaceYCbCr()
*/
public final boolean isColorSpaceYCbCr() {
// check the presence of the PYCC.pf file that contains the profile for the YCbCr color space
if (ImageWorker.CS_PYCC == null) {
throw new IllegalStateException(
"Unable to create an YCbCr profile most like since we are unable to locate the YCbCr color profile. Check the Java installation.");
}
final ColorModel cm = image.getColorModel();
if (cm == null) {
return false;
}
return cm.getColorSpace().getType() == ColorSpace.TYPE_YCbCr
|| cm.getColorSpace().equals(CS_PYCC);
}
/**
* Returns {@code true} if the {@linkplain #image} uses a IHA {@linkplain ColorSpace color space}.
*
* @see #forceColorSpaceIHS()
*/
public final boolean isColorSpaceIHS() {
final ColorModel cm = image.getColorModel();
if (cm == null) {
return false;
}
return cm.getColorSpace() instanceof IHSColorSpace;
}
/**
* Returns {@code true} if the {@linkplain #image} uses a GrayScale {@linkplain ColorSpace color space}. Note that a GrayScale color space doesn't
* mean that pixel values are directly stored as GrayScale component. The image may be {@linkplain #isIndexed indexed} as well.
*
* @see #forceColorSpaceGRAYScale
*/
public final boolean isColorSpaceGRAYScale() {
final ColorModel cm = image.getColorModel();
if (cm == null)
return false;
return cm.getColorSpace().getType() == ColorSpace.TYPE_GRAY;
}
/**
* Returns {@code true} if the {@linkplain #image} is {@linkplain Transparency#TRANSLUCENT translucent}.
*
* @see #forceBitmaskIndexColorModel
*/
public final boolean isTranslucent() {
return image.getColorModel().getTransparency() == Transparency.TRANSLUCENT;
}
// /////////////////////////////////////////////////////////////////////////////////////
// ////// ////////
// ////// IMAGE OPERATORS ////////
// ////// ////////
// /////////////////////////////////////////////////////////////////////////////////////
/**
* Rescales the {@linkplain #image} such that it uses 8 bits. If the image already uses 8 bits, then this method does nothing. Otherwise this
* method computes the minimum and maximum values for each band, {@linkplain RescaleDescriptor rescale} them in the range {@code [0 .. 255]} and
* force the resulting image to {@link DataBuffer#TYPE_BYTE TYPE_BYTE}.
*
* @return This ImageWorker
*
* @see #isBytes
* @see RescaleDescriptor
*/
public final ImageWorker rescaleToBytes() {
if (isBytes()) {
// Already using bytes - nothing to do.
return this;
}
// this is to support 16 bits IndexColorModel
forceComponentColorModel(true, true);
final double[][] extrema = getExtremas();
final int length = extrema[0].length;
final double[] scale = new double[length];
final double[] offset = new double[length];
boolean computeRescale = false;
for (int i = 0; i < length; i++) {
final double delta = extrema[1][i] - extrema[0][i];
if (Math.abs(delta) > 1E-6 // maximum and minimum does not coincide
&& ((extrema[1][i] - 255 > 1E-6) // the maximum is greater than 255
|| (extrema[0][i] < -1E-6))) // the minimum is smaller than 0
{
// we need to rescale
computeRescale = true;
// rescale factors
scale[i] = 255 / delta;
offset[i] = -scale[i] * extrema[0][i];
} else {
// we do not rescale explicitly bu in case we have to, we relay on the clamping capabilities of the format operator
scale[i] = 1;
offset[i] = 0;
}
}
final RenderingHints hints = getRenderingHints(DataBuffer.TYPE_BYTE);
if (computeRescale)
image = RescaleDescriptor.create(image, // The source image.
scale, // The per-band constants to multiply by.
offset, // The per-band offsets to be added.
hints); // The rendering hints.
else
image = FormatDescriptor.create(image, // The source image.
DataBuffer.TYPE_BYTE, // The destination image data type (BYTE)
hints); // The rendering hints.
invalidateStatistics(); // Extremas are no longer valid.
// All post conditions for this method contract.
assert isBytes();
return this;
}
/**
* Reduces the color model to {@linkplain IndexColorModel index color model}. If the current {@linkplain #image} already uses an
* {@linkplain IndexColorModel index color model}, then this method do nothing. Otherwise, the current implementation performs a ditering on the
* original color model. Note that this operation loose the alpha channel.
* <p>
* This for the moment should work only with opaque images, with non opaque images we just remove the alpha band in order to build an
* {@link IndexColorModel}. This is one because in general it could be very difficult to decide the final transparency for each pixel given the
* complexity if the algorithms for obtaining an {@link IndexColorModel}.
* <p>
* If an {@link IndexColorModel} with a single transparency index is enough for you, we advise you to take a look at
* {@link #forceIndexColorModelForGIF(boolean)} methdo.
*
* @see #isIndexed
* @see #forceBitmaskIndexColorModel
* @see #forceIndexColorModelForGIF
* @see OrderedDitherDescriptor
*/
public final ImageWorker forceIndexColorModel(final boolean error) {
final ColorModel cm = image.getColorModel();
if (cm instanceof IndexColorModel) {
// Already an index color model - nothing to do.
return this;
}
tileCacheEnabled(false);
if (getNumBands() % 2 == 0)
retainBands(getNumBands() - 1);
forceColorSpaceRGB();
final RenderingHints hints = getRenderingHints();
if (error) {
// color quantization
// final RenderedOp temp = ColorQuantizerDescriptor.create(image,
// ColorQuantizerDescriptor.MEDIANCUT, new Integer(254),
// new Integer(200), null, new Integer(1), new Integer(1),
// getRenderingHints());
// final ImageLayout layout= new ImageLayout();
// layout.setColorModel(temp.getColorModel());
// hints.add(new RenderingHints(JAI.KEY_IMAGE_LAYOUT,layout));
// error diffusion
final KernelJAI ditherMask = KernelJAI.ERROR_FILTER_FLOYD_STEINBERG;
final LookupTableJAI colorMap = ColorCube.BYTE_496;
// (LookupTableJAI) temp.getProperty("JAI.LookupTable");
image = ErrorDiffusionDescriptor.create(image, colorMap, ditherMask, hints);
} else {
// ordered dither
final KernelJAI[] ditherMask = KernelJAI.DITHER_MASK_443;
final ColorCube colorMap = ColorCube.BYTE_496;
image = OrderedDitherDescriptor.create(image, colorMap, ditherMask, hints);
}
tileCacheEnabled(true);
invalidateStatistics();
// All post conditions for this method contract.
assert isIndexed();
return this;
}
/**
* Reduces the color model to {@linkplain IndexColorModel index color model} with {@linkplain Transparency#OPAQUE opaque} or
* {@linkplain Transparency#BITMASK bitmask} transparency. If the current {@linkplain #image} already uses a suitable color model, then this
* method do nothing.
*
* @return this {@link ImageWorker}.
*
* @see #isIndexed
* @see #isTranslucent
* @see #forceIndexColorModel
* @see #forceIndexColorModelForGIF
*/
public final ImageWorker forceBitmaskIndexColorModel() {
forceBitmaskIndexColorModel(getTransparentPixel(), true);
return this;
}
/**
* Reduces the color model to {@linkplain IndexColorModel index color model} with {@linkplain Transparency#OPAQUE opaque} or
* {@linkplain Transparency#BITMASK bitmask} transparency. If the current {@linkplain #image} already uses a suitable color model, then this
* method do nothing.
*
* @param suggestedTransparent A suggested pixel index to define as the transparent pixel. *
* @param errorDiffusion Tells if I should use {@link ErrorDiffusionDescriptor} or {@link OrderedDitherDescriptor} JAi operations. errorDiffusion
* @return this {@link ImageWorker}.
*
* @see #isIndexed
* @see #isTranslucent
* @see #forceIndexColorModel
* @see #forceIndexColorModelForGIF
*/
public final ImageWorker forceBitmaskIndexColorModel(int suggestedTransparent,
final boolean errorDiffusion) {
final ColorModel cm = image.getColorModel();
if (cm instanceof IndexColorModel) {
final IndexColorModel oldCM = (IndexColorModel) cm;
switch (oldCM.getTransparency()) {
case Transparency.OPAQUE: {
// Suitable color model. There is nothing to do.
return this;
}
case Transparency.BITMASK: {
if (oldCM.getTransparentPixel() == suggestedTransparent) {
// Suitable color model. There is nothing to do.
return this;
}
break;
}
default: {
break;
}
}
// check if we already have a pixel fully transparent
final int transparentPixel = ColorUtilities.getTransparentPixel(oldCM);
/*
* The index color model need to be replaced. Creates a lookup table mapping from the old pixel values to new pixels values, with
* transparent colors mapped to the new transparent pixel value. The lookup table uses TYPE_BYTE or TYPE_USHORT, which are the two only
* types supported by IndexColorModel.
*/
final int mapSize = oldCM.getMapSize();
if (transparentPixel < 0)
suggestedTransparent = suggestedTransparent <= mapSize ? mapSize + 1
: suggestedTransparent;
else
suggestedTransparent = transparentPixel;
final int newSize = Math.max(mapSize, suggestedTransparent);
final int newPixelSize = ColorUtilities.getBitCount(newSize);
if (newPixelSize > 16)
throw new IllegalArgumentException(
"Unable to create index color model with more than 65536 elements");
final LookupTableJAI lookupTable;
if (newPixelSize <= 8) {
final byte[] table = new byte[mapSize];
for (int i = 0; i < mapSize; i++) {
table[i] = (byte) ((oldCM.getAlpha(i) == 0) ? suggestedTransparent : i);
}
lookupTable = new LookupTableJAI(table);
} else {
final short[] table = new short[mapSize];
for (int i = 0; i < mapSize; i++) {
table[i] = (short) ((oldCM.getAlpha(i) == 0) ? suggestedTransparent : i);
}
lookupTable = new LookupTableJAI(table, true);
}
/*
* Now we need to perform the look up transformation. First of all we create the new color model with a bitmask transparency using the
* transparency index specified to this method. Then we perform the lookup operation in order to prepare for the gif image.
*/
final byte[][] rgb = new byte[3][newSize];
oldCM.getReds(rgb[0]);
oldCM.getGreens(rgb[1]);
oldCM.getBlues(rgb[2]);
final IndexColorModel newCM = new IndexColorModel(newPixelSize, newSize, rgb[0],
rgb[1], rgb[2], suggestedTransparent);
final RenderingHints hints = getRenderingHints();
final ImageLayout layout = getImageLayout(hints);
layout.setColorModel(newCM);
// we should not transform on color map here
hints.put(JAI.KEY_TRANSFORM_ON_COLORMAP, Boolean.FALSE);
hints.put(JAI.KEY_IMAGE_LAYOUT, layout);
image = LookupDescriptor.create(image, lookupTable, hints);
// workaround bug in Lookup since it looks like it is switching 255 and 254
image = FormatDescriptor.create(image, image.getSampleModel().getDataType(), hints);
} else {
// force component color model first
forceComponentColorModel(true);
/*
* The image is not indexed.
*/
if (cm.hasAlpha()) {
// Getting the alpha channel.
tileCacheEnabled(false);
int numBands = getNumBands();
final RenderingHints hints = getRenderingHints();
final RenderedOp alphaChannel = BandSelectDescriptor.create(image,
new int[] { --numBands }, hints);
retainBands(numBands);
forceIndexColorModel(errorDiffusion);
tileCacheEnabled(true);
/*
* Adding transparency if needed, which means using the alpha channel to build a new color model. The method call below implies
* 'forceColorSpaceRGB()' and 'forceIndexColorModel()' method calls.
*/
addTransparencyToIndexColorModel(alphaChannel, false, suggestedTransparent,
errorDiffusion);
} else
forceIndexColorModel(errorDiffusion);
}
// All post conditions for this method contract.
assert isIndexed();
assert !isTranslucent();
return this;
}
/**
* Converts the image to a GIF-compliant image. This method has been created in order to convert the input image to a form that is compatible with
* the GIF model. It first remove the information about transparency since the error diffusion and the error dither operations are unable to
* process images with more than 3 bands. Afterwards the image is processed with an error diffusion operator in order to reduce the number of
* bands from 3 to 1 and the number of color to 216. A suitable layout is used for the final image via the {@linkplain #getRenderingHints
* rendering hints} in order to take into account the different layout model for the final image.
* <p>
* <strong>Tip:</strong> For optimizing writing GIF, we need to create the image untiled. This can be done by invoking
* <code>{@linkplain #setRenderingHint setRenderingHint}({@linkplain
* #TILING_ALLOWED}, Boolean.FALSE)</code> first.
*
* @param errorDiffusion Tells if I should use {@link ErrorDiffusionDescriptor} or {@link OrderedDitherDescriptor} JAi operations.
*
* @return this {@link ImageWorker}.
*
* @see #isIndexed
* @see #forceIndexColorModel
* @see #forceBitmaskIndexColorModel
*/
public final ImageWorker forceIndexColorModelForGIF(final boolean errorDiffusion) {
/*
* Checking the color model to see if we need to convert it back to color model. We might also need to reformat the image in order to get it
* to 8 bits samples.
*/
ColorModel cm = image.getColorModel();
if (cm instanceof PackedColorModel) {
forceComponentColorModel();
cm = image.getColorModel();
}
if (!(cm instanceof IndexColorModel) || cm.getPixelSize() > 8)
rescaleToBytes();
/*
* Getting the alpha channel and separating from the others bands. If the initial image had no alpha channel (more specifically, if it is
* either opaque or a bitmask) we proceed without doing anything since it seems that GIF encoder in such a case works fine. If we need to
* create a bitmask, we will use the last index value allowed (255) as the transparent pixel value.
*/
if (isTranslucent()) {
forceBitmaskIndexColorModel(255, errorDiffusion);
} else {
forceIndexColorModel(errorDiffusion);
}
// All post conditions for this method contract.
// assert isBytes(); // could be less, like binary, 4 bits
assert isIndexed();
assert !isTranslucent();
return this;
}
/**
* Reformats the {@linkplain ColorModel color model} to a {@linkplain ComponentColorModel component color model} preserving transparency. This is
* used especially in order to go from {@link PackedColorModel} to {@link ComponentColorModel}, which seems to be well accepted from
* {@code PNGEncoder} and {@code TIFFEncoder}.
* <p>
* This code is adapted from jai-interests mailing list archive.
*
* @return this {@link ImageWorker}.
*
* @see FormatDescriptor
*/
public final ImageWorker forceComponentColorModel() {
return forceComponentColorModel(false);
}
/**
* Reformats the {@linkplain ColorModel color model} to a {@linkplain ComponentColorModel component color model} preserving transparency. This is
* used especially in order to go from {@link PackedColorModel} to {@link ComponentColorModel}, which seems to be well accepted from
* {@code PNGEncoder} and {@code TIFFEncoder}.
* <p>
* This code is adapted from jai-interests mailing list archive.
*
* @param checkTransparent
* @param optimizeGray
*
* @return this {@link ImageWorker}.
*
* @see FormatDescriptor
*/
public final ImageWorker forceComponentColorModel(boolean checkTransparent, boolean optimizeGray) {
final ColorModel cm = image.getColorModel();
if (cm instanceof ComponentColorModel) {
// Already an component color model - nothing to do.
return this;
}
// shortcut for index color model
if (cm instanceof IndexColorModel) {
final IndexColorModel icm = (IndexColorModel) cm;
final SampleModel sm = this.image.getSampleModel();
final int datatype = sm.getDataType();
final boolean gray = ColorUtilities.isGrayPalette(icm, checkTransparent) & optimizeGray;
final boolean alpha = icm.hasAlpha();
/*
* If the image is grayscale, retain only the needed bands.
*/
final int numDestinationBands = gray ? (alpha ? 2 : 1) : (alpha ? 4 : 3);
LookupTableJAI lut = null;
switch (datatype) {
case DataBuffer.TYPE_BYTE: {
final byte data[][] = new byte[numDestinationBands][icm.getMapSize()];
icm.getReds(data[0]);
if (numDestinationBands >= 2)
// remember to optimize for grayscale images
if (!gray)
icm.getGreens(data[1]);
else
icm.getAlphas(data[1]);
if (numDestinationBands >= 3)
icm.getBlues(data[2]);
if (numDestinationBands == 4) {
icm.getAlphas(data[3]);
}
lut = new LookupTableJAI(data);
}
break;
case DataBuffer.TYPE_USHORT: {
final int mapSize = icm.getMapSize();
final short data[][] = new short[numDestinationBands][mapSize];
for (int i = 0; i < mapSize; i++) {
data[0][i] = (short) icm.getRed(i);
if (numDestinationBands >= 2)
// remember to optimize for grayscale images
if (!gray)
data[1][i] = (short) icm.getGreen(i);
else
data[1][i] = (short) icm.getAlpha(i);
if (numDestinationBands >= 3)
data[2][i] = (short) icm.getBlue(i);
if (numDestinationBands == 4) {
data[3][i] = (short) icm.getAlpha(i);
}
}
lut = new LookupTableJAI(data, datatype == DataBuffer.TYPE_USHORT);
}
break;
default:
throw new IllegalArgumentException(Errors.format(ErrorKeys.ILLEGAL_ARGUMENT_$2,
"datatype", datatype));
}
// did we initialized the LUT?
if (lut == null)
throw new IllegalStateException(Errors.format(ErrorKeys.NULL_ARGUMENT_$1, "lut"));
/*
* Get the default hints, which usually contains only informations about tiling. If the user override the rendering hints with an explicit
* color model, keep the user's choice.
*/
final RenderingHints hints = (RenderingHints) getRenderingHints();
final ImageLayout layout;
final Object candidate = hints.get(JAI.KEY_IMAGE_LAYOUT);
if (candidate instanceof ImageLayout) {
layout = (ImageLayout) candidate;
} else {
layout = new ImageLayout(image);
hints.add(new RenderingHints(JAI.KEY_IMAGE_LAYOUT, layout));
}
int[] bits = new int[numDestinationBands];
// bits per component
for (int i = 0; i < numDestinationBands; i++)
bits[i] = sm.getSampleSize(i);
final ComponentColorModel destinationColorModel = new ComponentColorModel(
numDestinationBands >= 3 ? ColorSpace.getInstance(ColorSpace.CS_sRGB)
: ColorSpace.getInstance(ColorSpace.CS_GRAY), bits, alpha,
cm.isAlphaPremultiplied(), alpha ? Transparency.TRANSLUCENT
: Transparency.OPAQUE, datatype);
final SampleModel destinationSampleModel = destinationColorModel
.createCompatibleSampleModel(image.getWidth(), image.getHeight());
layout.setColorModel(destinationColorModel);
layout.setSampleModel(destinationSampleModel);
image = LookupDescriptor.create(image, lut, hints);
} else {
// Most of the code adapted from jai-interests is in 'getRenderingHints(int)'.
final int type = (cm instanceof DirectColorModel) ? DataBuffer.TYPE_BYTE : image
.getSampleModel().getTransferType();
final RenderingHints hints = getRenderingHints(type);
// image=ColorConvertDescriptor.create(image, RIFUtil.getImageLayoutHint(hints).getColorModel(null), hints);
image = FormatDescriptor.create(image, type, hints);
;
}
invalidateStatistics();
// All post conditions for this method contract.
assert image.getColorModel() instanceof ComponentColorModel;
return this;
}
/**
* Reformats the {@linkplain ColorModel color model} to a {@linkplain ComponentColorModel component color model} preserving transparency. This is
* used especially in order to go from {@link PackedColorModel} to {@link ComponentColorModel}, which seems to be well accepted from
* {@code PNGEncoder} and {@code TIFFEncoder}.
* <p>
* This code is adapted from jai-interests mailing list archive.
*
* @param checkTransparent tells this method to not consider fully transparent pixels when optimizing grayscale palettes.
*
* @return this {@link ImageWorker}.
*
* @see FormatDescriptor
*/
public final ImageWorker forceComponentColorModel(boolean checkTransparent) {
return forceComponentColorModel(checkTransparent, true);
}
/**
* Forces the {@linkplain #image} color model to the {@linkplain ColorSpace#CS_sRGB RGB color space}. If the current color space is already of
* {@linkplain ColorSpace#TYPE_RGB RGB type}, then this method does nothing. This operation may loose the alpha channel.
*
* @return this {@link ImageWorker}.
*
* @see #isColorSpaceRGB
* @see ColorConvertDescriptor
*/
public final ImageWorker forceColorSpaceRGB() {
if (!isColorSpaceRGB()) {
final ColorModel cm = new ComponentColorModel(
ColorSpace.getInstance(ColorSpace.CS_sRGB), false, false, Transparency.OPAQUE,
image.getSampleModel().getDataType());
// force computation of the new colormodel
forceColorModel(cm);
}
// All post conditions for this method contract.
assert isColorSpaceRGB();
return this;
}
/**
* Forces the {@linkplain #image} color model to the {@linkplain ColorSpace#CS_PYCC YCbCr color space}. If the current color space is already of
* {@linkplain ColorSpace#CS_PYCC YCbCr}, then this method does nothing.
*
* @return this {@link ImageWorker}.
*
* @see #isColorSpaceRGB
* @see ColorConvertDescriptor
*/
public final ImageWorker forceColorSpaceYCbCr() {
if (!isColorSpaceYCbCr()) {
// go to component model
forceComponentColorModel();
// Create a ColorModel to convert the image to YCbCr.
final ColorModel cm = new ComponentColorModel(CS_PYCC, false, false,
Transparency.OPAQUE, this.image.getSampleModel().getDataType());
// force computation of the new colormodel
forceColorModel(cm);
}
// All post conditions for this method contract.
assert isColorSpaceYCbCr();
return this;
}
/**
* Forces the {@linkplain #image} color model to the IHS color space. If the current color space is already of IHS type, then this method does
* nothing. This operation may loose the alpha channel.
*
* @return this {@link ImageWorker}.
*
* @see ColorConvertDescriptor
*/
public final ImageWorker forceColorSpaceIHS() {
if (!isColorSpaceIHS()) {
forceComponentColorModel();
// Create a ColorModel to convert the image to IHS.
final IHSColorSpace ihs = IHSColorSpace.getInstance();
final int numBits = image.getColorModel().getComponentSize(0);
final ColorModel ihsColorModel = new ComponentColorModel(ihs, new int[] { numBits,
numBits, numBits }, false, false, Transparency.OPAQUE, image.getSampleModel()
.getDataType());
// compute
forceColorModel(ihsColorModel);
}
// All post conditions for this method contract.
assert isColorSpaceIHS();
return this;
}
/** Forces the provided {@link ColorModel} via the JAI ColorConvert operation. */
private void forceColorModel(final ColorModel cm) {
final ImageLayout2 il = new ImageLayout2(image);
il.setColorModel(cm);
il.setSampleModel(cm.createCompatibleSampleModel(image.getWidth(), image.getHeight()));
final RenderingHints oldRi = this.getRenderingHints();
final RenderingHints newRi = (RenderingHints) oldRi.clone();
newRi.add(new RenderingHints(JAI.KEY_IMAGE_LAYOUT, il));
setRenderingHints(newRi);
image = ColorConvertDescriptor.create(image, cm, getRenderingHints());
// restore RI
this.setRenderingHints(oldRi);
// invalidate stats
invalidateStatistics();
}
/**
* Add the bands to the Component Color Model
*
* @param writeband number of bands after the bandmerge.
*
* @return this {@link ImageWorker}.
*
*/
public final ImageWorker bandMerge(int writeband) {
ParameterBlock pb = new ParameterBlock();
PlanarImage sourceImage = PlanarImage.wrapRenderedImage(getRenderedImage());
int numBands = sourceImage.getSampleModel().getNumBands();
// getting first band
final RenderedImage firstBand = JAI.create("bandSelect", sourceImage, new int[] { 0 });
// adding to the image
final int length = writeband - numBands;
for (int i = 0; i < length; i++) {
pb.removeParameters();
pb.removeSources();
pb.addSource(sourceImage);
pb.addSource(firstBand);
sourceImage = JAI.create("bandmerge", pb);
pb.removeParameters();
pb.removeSources();
}
image = (RenderedImage) sourceImage;
invalidateStatistics();
// All post conditions for this method contract.
assert image.getSampleModel().getNumBands() == writeband;
return this;
}
/**
* Perform a BandMerge operation between the underlying image and the provided one.
*
* @param image to merge with the underlying one.
* @param before <code>true</code> if we want to use first the provided image, <code>false</code> otherwise.
*
* @return this {@link ImageWorker}.
*
*/
public final ImageWorker addBand(RenderedImage image, boolean before) {
this.image = before ? BandMergeDescriptor.create(image, this.image,
this.getRenderingHints()) : BandMergeDescriptor.create(this.image, image,
this.getRenderingHints());
invalidateStatistics();
return this;
}
/**
* Forces the {@linkplain #image} color model to the {@linkplain ColorSpace#CS_GRAY GRAYScale color space}. If the current color space is already
* of {@linkplain ColorSpace#TYPE_GRAY type}, then this method does nothing.
*
* @return this {@link ImageWorker}.
*
* @see #isColorSpaceGRAYScale
* @see ColorConvertDescriptor
*/
public final ImageWorker forceColorSpaceGRAYScale() {
if (!isColorSpaceRGB()) {
final ColorModel cm = new ComponentColorModel(
ColorSpace.getInstance(ColorSpace.CS_GRAY), false, false, Transparency.OPAQUE,
DataBuffer.TYPE_BYTE);
image = ColorConvertDescriptor.create(image, cm, getRenderingHints());
invalidateStatistics();
}
// All post conditions for this method contract.
assert isColorSpaceGRAYScale();
return this;
}
/**
* Creates an image which represents approximatively the intensity of {@linkplain #image}. The result is always a single-banded image. If the
* image uses an {@linkplain IHSColorSpace IHS color space}, then this method just {@linkplain #retainFirstBand retain the first band} without any
* further processing. Otherwise, this method performs a simple {@linkplain BandCombineDescriptor band combine} operation on the
* {@linkplain #image} in order to come up with a simple estimation of the intensity of the image based on the average value of the color
* components. It is worthwhile to note that the alpha band is stripped from the image.
*
* @return this {@link ImageWorker}.
*
* @see BandCombineDescriptor
*/
public final ImageWorker intensity() {
/*
* If the color model already uses a IHS color space or a Gray color space, keep only the intensity band. Otherwise, we need a component color
* model to be sure to understand what we are doing.
*/
ColorModel cm = image.getColorModel();
final ColorSpace cs = cm.getColorSpace();
if (cs.getType() == ColorSpace.TYPE_GRAY || cs instanceof IHSColorSpace) {
retainFirstBand();
return this;
}
if (cm instanceof IndexColorModel) {
forceComponentColorModel();
cm = image.getColorModel();
}
// Number of color componenents
final int numBands = cm.getNumComponents();
final int numColorBands = cm.getNumColorComponents();
final boolean hasAlpha = cm.hasAlpha();
// One band, nothing to combine.
if (numBands == 1) {
return this;
}
// One band plus alpha, let's remove alpha.
if (numColorBands == 1 && hasAlpha) {
retainFirstBand();
return this;
}
// remove the alpha band
if (numColorBands != numBands) {
this.retainBands(numBands);
}
/*
* We have more than one band. Note that there is no need to remove the alpha band before to apply the "bandCombine" operation - it is
* suffisient to let the coefficient for the alpha band to the 0 value.
*/
final double[][] coeff = new double[1][numBands + 1];
Arrays.fill(coeff[0], 0, numColorBands, 1.0 / numColorBands);
image = BandCombineDescriptor.create(image, coeff, getRenderingHints());
invalidateStatistics();
// All post conditions for this method contract.
assert getNumBands() == 1;
return this;
}
/**
* Retains inconditionnaly the first band of {@linkplain #image}. All other bands (if any) are discarted without any further processing.
*
* @return this {@link ImageWorker}.
*
* @see #getNumBands
* @see #retainBands
* @see BandSelectDescriptor
*/
public final ImageWorker retainFirstBand() {
retainBands(1);
// All post conditions for this method contract.
assert getNumBands() == 1;
return this;
}
/**
* Retains unconditionally the last band of {@linkplain #image}. All other bands (if any) are discarded without any further processing.
*
* <p>
* It is worth to point out that we use the true number of bands rather than the number of color components. This means that if we apply this
* method on a colormapped image we get back the image itself untouched since it originally contains 1 band although the color components are 3 or
* 4 as per the attached colormap.
*
* @return this {@link ImageWorker}.
*
* @see #getNumBands
* @see #retainBands
* @see BandSelectDescriptor
*/
public final ImageWorker retainLastBand() {
final int band = getNumBands() - 1;
if (band != 0) {
retainBands(new int[] { band });
}
// All post conditions for this method contract.
assert getNumBands() == 1;
return this;
}
/**
* Retains inconditionnaly the first {@code numBands} of {@linkplain #image}. All other bands (if any) are discarted without any further
* processing. This method does nothing if the current {@linkplain #image} does not have a greater amount of bands than {@code numBands}.
*
* @param numBands the number of bands to retain.
* @return this {@link ImageWorker}.
*
* @see #getNumBands
* @see #retainFirstBand
* @see BandSelectDescriptor
*/
public final ImageWorker retainBands(final int numBands) {
if (numBands <= 0) {
throw new IndexOutOfBoundsException(Errors.format(ErrorKeys.ILLEGAL_ARGUMENT_$2,
"numBands", numBands));
}
if (getNumBands() > numBands) {
final int[] bands = new int[numBands];
for (int i = 0; i < bands.length; i++) {
bands[i] = i;
}
image = BandSelectDescriptor.create(image, bands, getRenderingHints());
}
// All post conditions for this method contract.
assert getNumBands() <= numBands;
return this;
}
/**
* Retains inconditionnaly certain bands of {@linkplain #image}. All other bands (if any) are discarded without any further processing.
*
* @param bands the bands to retain.
* @return this {@link ImageWorker}.
*
* @see #getNumBands
* @see #retainFirstBand
* @see BandSelectDescriptor
*/
public final ImageWorker retainBands(final int[] bands) {
image = BandSelectDescriptor.create(image, bands, getRenderingHints());
return this;
}
/**
* Formats the underlying image to the provided data type.
*
* @param dataType to be used for this {@link FormatDescriptor} operation.
* @return this {@link ImageWorker}
*/
public final ImageWorker format(final int dataType) {
image = FormatDescriptor.create(image, dataType, getRenderingHints());
// All post conditions for this method contract.
assert image.getSampleModel().getDataType() == dataType;
return this;
}
/**
* Binarizes the {@linkplain #image}. If the image is multi-bands, then this method first computes an estimation of its {@linkplain #intensity
* intensity}. Then, the threshold value is set halfway between the minimal and maximal values found in the image.
*
* @return this {@link ImageWorker}.
*
* @see #isBinary
* @see #binarize(double)
* @see #binarize(int,int)
* @see BinarizeDescriptor
*/
public final ImageWorker binarize() {
binarize(Double.NaN);
// All post conditions for this method contract.
assert isBinary();
return this;
}
/**
* Binarizes the {@linkplain #image}. If the image is already binarized, then this method does nothing.
*
* @param threshold The threshold value.
* @return this {@link ImageWorker}.
*
* @see #isBinary
* @see #binarize()
* @see #binarize(int,int)
* @see BinarizeDescriptor
*/
public final ImageWorker binarize(double threshold) {
// If the image is already binary and the threshold is >=1 then there is no work to do.
if (!isBinary()) {
if (Double.isNaN(threshold)) {
if (getNumBands() != 1) {
tileCacheEnabled(false);
intensity();
tileCacheEnabled(true);
}
final double[][] extremas = getExtremas();
threshold = 0.5 * (extremas[0][0] + extremas[1][0]);
}
final RenderingHints hints = getRenderingHints();
image = BinarizeDescriptor.create(image, threshold, hints);
invalidateStatistics();
}
// All post conditions for this method contract.
assert isBinary();
return this;
}
/**
* Binarizes the {@linkplain #image} (if not already done) and replace all 0 values by {@code value0} and all 1 values by {@code value1}. If the
* image should be binarized using a custom threshold value (instead of the automatic one), invoke {@link #binarize(double)} explicitly before
* this method.
*
* @return this {@link ImageWorker}.
* @see #isBinary
* @see #binarize()
* @see #binarize(double)
* @see BinarizeDescriptor
* @see LookupDescriptor
*/
public final ImageWorker binarize(final int value0, final int value1) {
tileCacheEnabled(false);
binarize();
tileCacheEnabled(true);
final LookupTableJAI table;
final int min = Math.min(value0, value1);
if (min >= 0) {
final int max = Math.max(value0, value1);
if (max < 256) {
table = new LookupTableJAI(new byte[] { (byte) value0, (byte) value1 });
} else if (max < 65536) {
table = new LookupTableJAI(new short[] { (short) value0, (short) value1 }, true);
} else {
table = new LookupTableJAI(new int[] { value0, value1 });
}
} else {
table = new LookupTableJAI(new int[] { value0, value1 });
}
image = LookupDescriptor.create(image, table, getRenderingHints());
invalidateStatistics();
return this;
}
/**
* Replaces all occurences of the given color (usually opaque) by a fully transparent color. Currents implementation supports image backed by any
* {@link IndexColorModel}, or by {@link ComponentColorModel} with {@link DataBuffer#TYPE_BYTE TYPE_BYTE}. More types may be added in future
* GeoTools versions.
*
* @param transparentColor The color to make transparent.
* @return this image worker.
*
* @throws IllegalStateException if the current {@linkplain #image} has an unsupported color model.
*/
public final ImageWorker makeColorTransparent(final Color transparentColor)
throws IllegalStateException {
if (transparentColor == null) {
throw new IllegalArgumentException(Errors.format(ErrorKeys.NULL_ARGUMENT_$1,
"transparentColor"));
}
final ColorModel cm = image.getColorModel();
if (cm instanceof IndexColorModel) {
return maskIndexColorModel(transparentColor);
} else if (cm instanceof ComponentColorModel) {
switch (image.getSampleModel().getDataType()) {
case DataBuffer.TYPE_BYTE: {
return maskComponentColorModelByte(transparentColor);
}
// Add other types here if we support them...
}
}
throw new IllegalStateException(Errors.format(ErrorKeys.UNSUPPORTED_DATA_TYPE));
}
/**
* For an image backed by an {@link IndexColorModel}, replaces all occurences of the given color (usually opaque) by a fully transparent color.
*
* @param transparentColor The color to make transparent.
* @return this image worker.
*
*/
private final ImageWorker maskIndexColorModel(final Color transparentColor) {
assert image.getColorModel() instanceof IndexColorModel;
// Gets informations about the provided images.
IndexColorModel cm = (IndexColorModel) image.getColorModel();
final int numComponents = cm.getNumComponents();
int transparency = cm.getTransparency();
int transparencyIndex = cm.getTransparentPixel();
final int mapSize = cm.getMapSize();
final int transparentRGB = transparentColor.getRGB() & 0x00FFFFFF;
/*
* Optimization in case of Transparency.BITMASK. If the color we want to use as the fully transparent one is the same that is actually used as
* the transparent color, we leave doing nothing.
*/
if (transparency == Transparency.BITMASK && transparencyIndex != -1) {
int transpColor = cm.getRGB(transparencyIndex) & 0x00FFFFFF;
if (transpColor == transparentRGB) {
return this;
}
}
/*
* Find the index of the specified color. Most of the time, the color should appears only once, which will leads us to a BITMASK image.
* However we allows more occurences, which will leads us to a TRANSLUCENT image.
*/
final List<Integer> transparentPixelsIndexes = new ArrayList<Integer>();
for (int i = 0; i < mapSize; i++) {
// Gets the color for this pixel removing the alpha information.
final int color = cm.getRGB(i) & 0xFFFFFF;
if (transparentRGB == color) {
transparentPixelsIndexes.add(i);
if (Transparency.BITMASK == transparency) {
break;
}
}
}
final int found = transparentPixelsIndexes.size();
if (found == 1) {
// Transparent color found.
transparencyIndex = transparentPixelsIndexes.get(0);
transparency = Transparency.BITMASK;
} else if (found == 0) {
return this;
} else {
transparencyIndex = -1;
transparency = Transparency.TRANSLUCENT;
}
// Prepare the new ColorModel.
// Get the old map and update it as needed.
final byte rgb[][] = new byte[4][mapSize];
cm.getReds(rgb[0]);
cm.getGreens(rgb[1]);
cm.getBlues(rgb[2]);
if (numComponents == 4) {
cm.getAlphas(rgb[3]);
} else {
Arrays.fill(rgb[3], (byte) 255);
}
if (transparency != Transparency.TRANSLUCENT) {
cm = new IndexColorModel(cm.getPixelSize(), mapSize, rgb[0], rgb[1], rgb[2],
transparencyIndex);
} else {
for (int k = 0; k < found; k++) {
rgb[3][transparentPixelsIndexes.get(k)] = (byte) 0;
}
cm = new IndexColorModel(cm.getPixelSize(), mapSize, rgb[0], rgb[1], rgb[2], rgb[3]);
}
// Format the input image.
final ImageLayout layout = new ImageLayout(image);
layout.setColorModel(cm);
final RenderingHints hints = getRenderingHints();
hints.add(new RenderingHints(JAI.KEY_IMAGE_LAYOUT, layout));
hints.add(new RenderingHints(JAI.KEY_REPLACE_INDEX_COLOR_MODEL, Boolean.FALSE));
image = FormatDescriptor.create(image, image.getSampleModel().getDataType(), hints);
invalidateStatistics();
return this;
}
/**
* For an image backed by an {@link ComponentColorModel}, replaces all occurences of the given color (usually opaque) by a fully transparent
* color.
*
* @param transparentColor The color to make transparent.
* @return this image worker.
*
*
* Current implementation invokes a lot of JAI operations:
*
* "BandSelect" --> "Lookup" --> "BandCombine" --> "Extrema" --> "Binarize" --> "Format" --> "BandSelect" (one more time) --> "Multiply"
* --> "BandMerge".
*
* I would expect more speed and memory efficiency by writing our own JAI operation (PointOp subclass) doing that in one step. It would
* also be more deterministic (our "binarize" method depends on statistics on pixel values) and avoid unwanted side-effect like turning
* black color (RGB = 0,0,0) to transparent one. It would also be easier to maintain I believe.
*/
private final ImageWorker maskComponentColorModelByte(final Color transparentColor) {
assert image.getColorModel() instanceof ComponentColorModel;
assert image.getSampleModel().getDataType() == DataBuffer.TYPE_BYTE;
/*
* Prepares the look up table for the source image. Remember what follows which is taken from the JAI programming guide.
*
* "The lookup operation performs a general table lookup on a rendered or renderable image. The destination image is obtained by passing the
* source image through the lookup table. The source image may be single- or multi-banded of data types byte, ushort, short, or int. The
* lookup table may be single- or multi-banded of any JAI- supported data types.
*
* The destination image must have the same data type as the lookup table, and its number of bands is determined based on the number of bands
* of the source and the table. If the source is single-banded, the destination has the same number of bands as the lookup table; otherwise,
* the destination has the same number of bands as the source.
*
* If either the source or the table is single-banded and the other one is multibanded, the single band is applied to every band of the
* multi-banded object. If both are multi-banded, their corresponding bands are matched up."
*
* A final annotation, if we have an input image with transparency we just DROP it since we want to re-add it using the supplied color as the
* mask for transparency.
*/
/*
* In case of a gray color model we can do everything in one step by expanding the color model to get one more band directly which is the
* alpha band itself.
*
* For a multiband image the lookup is applied to each band separately. This means that we cannot control directly the image as a whole but we
* need first to interact with the single bands then to combine the result into a single band that will provide us with the alpha band.
*/
int numBands = image.getSampleModel().getNumBands();
final int numColorBands = image.getColorModel().getNumColorComponents();
final RenderingHints hints = getRenderingHints();
if (numColorBands != numBands) {
// Typically, numColorBands will be equals to numBands-1.
final int[] opaqueBands = new int[numColorBands];
for (int i = 0; i < opaqueBands.length; i++) {
opaqueBands[i] = i;
}
image = BandSelectDescriptor.create(image, opaqueBands, hints);
numBands = numColorBands;
}
// now prepare the lookups
final byte[][] tableData = new byte[numColorBands][256];
final boolean singleStep = (numColorBands == 1);
if (singleStep) {
final byte[] data = tableData[0];
Arrays.fill(data, (byte) 255);
data[transparentColor.getRed()] = 0;
} else {
switch (numColorBands) {
case 3:
Arrays.fill(tableData[2], (byte) 255);
tableData[2][transparentColor.getBlue()] = 0; // fall through
case 2:
Arrays.fill(tableData[1], (byte) 255);
tableData[1][transparentColor.getGreen()] = 0; // fall through
case 1:
Arrays.fill(tableData[0], (byte) 255);
tableData[0][transparentColor.getRed()] = 0; // fall through
case 0:
break;
}
}
// Create a LookupTableJAI object to be used with the "lookup" operator.
LookupTableJAI table = new LookupTableJAI(tableData);
// Do the lookup operation.
// we should not transform on color map here
hints.put(JAI.KEY_TRANSFORM_ON_COLORMAP, Boolean.FALSE);
PlanarImage luImage = LookupDescriptor.create(image, table, hints);
/*
* Now that we have performed the lookup operation we have to remember what we stated here above.
*
* If the input image is multiband we will get a multiband image as the output of the lookup operation hence we need to perform some form of
* band combination to get the alpha band out of the lookup image.
*
* The way we wanted things to be done is by exploiting the clamping behavior that kicks in when we do sums and the like on pixels and we
* overcome the maximum value allowed by the DataBufer DataType.
*/
if (!singleStep) {
// We simply add the three generated bands together in order to get the right.
final double[][] matrix = new double[1][4];
// Values at index 0,1,2 are set to 1.0, value at index 3 is left to 0.
Arrays.fill(matrix[0], 0, 3, 1.0);
luImage = BandCombineDescriptor.create(luImage, matrix, hints);
}
image = BandMergeDescriptor.create(image, luImage, hints);
invalidateStatistics();
return this;
}
/**
* Inverts the pixel values of the {@linkplain #image}.
*
* @see InvertDescriptor
*/
public final ImageWorker invert() {
image = InvertDescriptor.create(image, getRenderingHints());
invalidateStatistics();
return this;
}
/**
* Applies the specified mask over the current {@linkplain #image}. The mask should be {@linkplain #binarize() binarized} - if it is not, this
* method will do it itself. Then, for every pixels in the mask with value equals to {@code maskValue}, the corresponding pixel in the
* {@linkplain #image} will be set to the specified {@code newValue}.
* <p>
* <strong>Note:</strong> current implementation force the color model to an {@linkplain IndexColorModel indexed} one. Future versions may avoid
* this change.
*
* @param mask The mask to apply, as a {@linkplain #binarize() binarized} image.
* @param maskValue The mask value to search for ({@code false} for 0 or {@code true} for 1).
* @param newValue The new value for every pixels in {@linkplain #image} corresponding to {@code maskValue} in the mask.
*
* @return this {@link ImageWorker}.
*
* @todo This now should work only if {@code newValue} is 255 and {@code maskValue} is {@code false}.
*/
public final ImageWorker mask(RenderedImage mask, final boolean maskValue, int newValue) {
/*
* Make sure that the underlying image is indexed.
*/
tileCacheEnabled(false);
forceIndexColorModel(true);
final RenderingHints hints = new RenderingHints(JAI.KEY_TILE_CACHE, null);
/*
* special case for newValue == 255 && !maskValue.
*/
if (newValue == 255 && !maskValue) {
/*
* Build a lookup table in order to make the transparent pixels equal to 255 and all the others equal to 0.
*/
final byte[] lutData = new byte[256];
// mapping all the non-transparent pixels to opaque
Arrays.fill(lutData, (byte) 0);
// for transparent pixels
lutData[0] = (byte) 255;
final LookupTableJAI lut = new LookupTableJAI(lutData);
mask = LookupDescriptor.create(mask, lut, hints);
/*
* Adding to the other image exploiting the implict clamping
*/
image = AddDescriptor.create(image, mask, getRenderingHints());
tileCacheEnabled(true);
invalidateStatistics();
return this;
} else {
// general case
// it has to be binary
if (!isBinary())
binarize();
// now if we mask with 1 we have to invert the mask
if (maskValue)
mask = NotDescriptor.create(mask, new RenderingHints(
JAI.KEY_REPLACE_INDEX_COLOR_MODEL, Boolean.FALSE));
// and with the image to zero the interested pixels
tileCacheEnabled(false);
image = AndDescriptor.create(mask, image, getRenderingHints());
// add the new value to the mask
mask = AddConstDescriptor.create(mask, new double[] { newValue }, new RenderingHints(
JAI.KEY_REPLACE_INDEX_COLOR_MODEL, Boolean.FALSE));
// add the mask to the image to mask with the new value
image = AddDescriptor.create(mask, image, getRenderingHints());
tileCacheEnabled(true);
invalidateStatistics();
return this;
}
}
/**
* Takes two rendered or renderable source images, and adds every pair of pixels, one from each source image of the corresponding position and
* band. See JAI {@link AddDescriptor} for details.
*
* @param renderedImage the {@link RenderedImage} to be added to this {@link ImageWorker}.
* @return this {@link ImageWorker}.
*
* @see AddDescriptor
*/
public final ImageWorker addImage(final RenderedImage renderedImage) {
image = AddDescriptor.create(image, renderedImage, getRenderingHints());
invalidateStatistics();
return this;
}
/**
* Takes one rendered or renderable image and an array of double constants, and multiplies every pixel of the same band of the source by the
* constant from the corresponding array entry. See JAI {@link MultiplyConstDescriptor} for details.
*
* @param inValues The constants to be multiplied.
* @return this {@link ImageWorker}.
*
* @see MultiplyConstDescriptor
*/
public final ImageWorker multiplyConst(double[] inValues) {
image = MultiplyConstDescriptor.create(image, inValues, getRenderingHints());
invalidateStatistics();
return this;
}
/**
* Takes one rendered or renderable image and an array of integer constants, and performs a bit-wise logical "xor" between every pixel in the same
* band of the source and the constant from the corresponding array entry. See JAI {@link XorConstDescriptor} for details.
*
* @see XorConstDescriptor
*/
public final ImageWorker xorConst(int[] values) {
image = XorConstDescriptor.create(image, values, getRenderingHints());
invalidateStatistics();
return this;
}
/**
* Adds transparency to a preexisting image whose color model is {@linkplain IndexColorModel index color model}. For all pixels with the value
* {@code false} in the specified transparency mask, the corresponding pixel in the {@linkplain #image} is set to the transparent pixel value. All
* other pixels are left unchanged.
*
* @param alphaChannel The mask to apply as a {@linkplain #binarize() binarized} image.
* @param errorDiffusion Tells if I should use {@link ErrorDiffusionDescriptor} or {@link OrderedDitherDescriptor} JAi operations.
* @return this {@link ImageWorker}.
*
* @see #isTranslucent
* @see #forceBitmaskIndexColorModel
*/
public ImageWorker addTransparencyToIndexColorModel(final RenderedImage alphaChannel,
final boolean errorDiffusion) {
addTransparencyToIndexColorModel(alphaChannel, true, getTransparentPixel(), errorDiffusion);
return this;
}
/**
* Adds transparency to a preexisting image whose color model is {@linkplain IndexColorModel index color model}. First, this method creates a new
* index color model with the specified {@code transparent} pixel, if needed (this method may skip this step if the specified pixel is already
* transparent. Then for all pixels with the value {@code false} in the specified transparency mask, the corresponding pixel in the
* {@linkplain #image} is set to that transparent value. All other pixels are left unchanged.
*
* @param alphaChannel The mask to apply as a {@linkplain #binarize() binarized} image.
* @param translucent {@code true} if {@linkplain Transparency#TRANSLUCENT translucent} images are allowed, or {@code false} if the resulting
* images must be a {@linkplain Transparency#BITMASK bitmask}.
* @param transparent The value for transparent pixels, to be given to every pixels in the {@linkplain #image} corresponding to {@code false} in
* the mask. The special value {@code -1} maps to the last pixel value allowed for the {@linkplain IndexedColorModel indexed color model}.
* @param errorDiffusion Tells if I should use {@link ErrorDiffusionDescriptor} or {@link OrderedDitherDescriptor} JAi operations.
*
* @return this {@link ImageWorker}.
*/
public final ImageWorker addTransparencyToIndexColorModel(final RenderedImage alphaChannel,
final boolean translucent, int transparent, final boolean errorDiffusion) {
tileCacheEnabled(false);
forceIndexColorModel(errorDiffusion);
tileCacheEnabled(true);
/*
* Prepares hints and layout to use for mask operations. A color model hint will be set only if the block below is executed.
*/
final ImageWorker worker = fork(image);
final RenderingHints hints = worker.getRenderingHints();
/*
* Gets the index color model. If the specified 'transparent' value is not fully transparent, replaces the color model by a new one with the
* transparent pixel defined. NOTE: the "transparent &= (1 << pixelSize) - 1" instruction below is a safety for making sure that the
* transparent index value can hold in the amount of bits allowed for this color model (the mapSize value may not use all bits). It works as
* expected with the -1 special value. It also make sure that "transparent + 1" do not exeed the maximum map size allowed.
*/
final boolean forceBitmask;
final IndexColorModel oldCM = (IndexColorModel) image.getColorModel();
final int pixelSize = oldCM.getPixelSize();
transparent &= (1 << pixelSize) - 1;
forceBitmask = !translucent && oldCM.getTransparency() == Transparency.TRANSLUCENT;
if (forceBitmask || oldCM.getTransparentPixel() != transparent) {
final int mapSize = Math.max(oldCM.getMapSize(), transparent + 1);
final byte[][] RGBA = new byte[translucent ? 4 : 3][mapSize];
// Note: we might use less that 256 values.
oldCM.getReds(RGBA[0]);
oldCM.getGreens(RGBA[1]);
oldCM.getBlues(RGBA[2]);
final IndexColorModel newCM;
if (translucent) {
oldCM.getAlphas(RGBA[3]);
RGBA[3][transparent] = 0;
newCM = new IndexColorModel(pixelSize, mapSize, RGBA[0], RGBA[1], RGBA[2], RGBA[3]);
} else {
newCM = new IndexColorModel(pixelSize, mapSize, RGBA[0], RGBA[1], RGBA[2],
transparent);
}
/*
* Set the color model hint.
*/
final ImageLayout layout = getImageLayout(hints);
layout.setColorModel(newCM);
worker.setRenderingHint(JAI.KEY_IMAGE_LAYOUT, layout);
}
/*
* Applies the mask, maybe with a color model change.
*/
worker.setRenderingHint(JAI.KEY_REPLACE_INDEX_COLOR_MODEL, Boolean.FALSE);
worker.mask(alphaChannel, false, transparent);
image = worker.image;
invalidateStatistics();
// All post conditions for this method contract.
assert isIndexed();
assert translucent || !isTranslucent() : translucent;
assert ((IndexColorModel) image.getColorModel()).getAlpha(transparent) == 0;
return this;
}
/**
* If the was not already tiled, tile it. Note that no tiling will be done if 'getRenderingHints()' failed to suggest a tile size. This method is
* for internal use by {@link #write} methods only.
*
* @return this {@link ImageWorker}.
*/
public final ImageWorker tile() {
final RenderingHints hints = getRenderingHints();
final ImageLayout layout = getImageLayout(hints);
if (layout.isValid(ImageLayout.TILE_WIDTH_MASK)
|| layout.isValid(ImageLayout.TILE_HEIGHT_MASK)) {
final int type = image.getSampleModel().getDataType();
image = FormatDescriptor.create(image, type, hints);
}
return this;
}
/**
* Applies the specified opacity to the image by either adding an alpha band, or modifying the existing one by multiplication
*
* @param opacity The opacity to be applied, between 0 and 1
*
* @return this {@link ImageWorker}.
*/
public ImageWorker applyOpacity(float opacity) {
RenderedImage result;
ColorModel colorModel = image.getColorModel();
// if it's an index color model we can just recompute the palette
// and replace it
if (colorModel instanceof IndexColorModel) {
// grab the original palette
IndexColorModel index = (IndexColorModel) colorModel;
byte[] reds = new byte[index.getMapSize()];
byte[] greens = new byte[index.getMapSize()];
byte[] blues = new byte[index.getMapSize()];
byte[] alphas = new byte[index.getMapSize()];
index.getReds(reds);
index.getGreens(greens);
index.getBlues(blues);
index.getAlphas(alphas);
// multiply the alphas by opacity
final int transparentPixel = index.getTransparentPixel();
for (int i = 0; i < alphas.length; i++) {
alphas[i] = (byte) Math.round((0xFF & alphas[i]) * opacity);
if (i == transparentPixel) {
alphas[i] = 0;
}
}
// build a new palette
IndexColorModel newColorModel = new IndexColorModel(index.getPixelSize(),
index.getMapSize(), reds, greens, blues, alphas);
LookupTableJAI table = buildOpacityLookupTable(0, 1, -1);
ImageLayout layout = new ImageLayout(image);
layout.setColorModel(newColorModel);
RenderingHints hints = new RenderingHints(JAI.KEY_IMAGE_LAYOUT, layout);
result = LookupDescriptor.create(image, table, hints);
} else {
// not indexed, then make sure it's some sort of component color model or turn it into one
RenderedImage expanded;
if (!(colorModel instanceof ComponentColorModel)) {
expanded = new ImageWorker(image).forceComponentColorModel().getRenderedImage();
} else {
expanded = image;
}
// do we have to add the alpha band or it's there and we need to change it?
if (!expanded.getColorModel().hasAlpha()) {
// we just need to add it, so first build a constant image with the same structure
// as the original image
byte alpha = (byte) Math.round(255 * opacity);
ImageLayout layout = new ImageLayout(image.getMinX(), image.getMinY(),
image.getWidth(), image.getHeight());
RenderedOp alphaBand = ConstantDescriptor.create((float) image.getWidth(),
(float) image.getHeight(), new Byte[] { new Byte(alpha) },
new RenderingHints(JAI.KEY_IMAGE_LAYOUT, layout));
result = BandMergeDescriptor.create(expanded, alphaBand, null);
} else {
// we need to transform the existing, we'll use a lookup
final int bands = expanded.getSampleModel().getNumBands();
int alphaBand = bands - 1;
LookupTableJAI table = buildOpacityLookupTable(opacity, bands, alphaBand);
result = LookupDescriptor.create(expanded, table, null);
}
}
image = result;
return this;
}
/**
* Builds a lookup table that is the identity on all bands but the alpha one, where the opacity is applied
*
* @param opacity
* @param bands
* @param alphaBand
* @return
*/
LookupTableJAI buildOpacityLookupTable(float opacity, final int bands, int alphaBand) {
byte[][] matrix = new byte[bands][256];
for (int band = 0; band < matrix.length; band++) {
if (band == alphaBand) {
for (int i = 0; i < 256; i++) {
matrix[band][i] = (byte) Math.round(i * opacity);
}
} else {
for (int i = 0; i < 256; i++) {
matrix[band][i] = (byte) i;
}
}
}
LookupTableJAI table = new LookupTableJAI(matrix);
return table;
}
/**
* Writes the {@linkplain #image} to the specified file. This method differs from {@link ImageIO#write(String,File)} in a number of ways:
* <p>
* <ul>
* <li>The {@linkplain ImageWriter image writer} to use is inferred from the file extension.</li>
* <li>If the image writer accepts {@link File} objects as input, then the {@code file} argument is given directly without creating an
* {@link ImageOutputStream} object. This is important for some formats like HDF, which work <em>only</em> with files.</li>
* <li>If the {@linkplain #image} is not tiled, then it is tiled prior to be written.</li>
* <li>If some special processing is needed for a given format, then the corresponding method is invoked. Example:
* {@link #forceIndexColorModelForGIF}.</li>
* </ul>
*
* @return this {@link ImageWorker}.
*/
public final ImageWorker write(final File output) throws IOException {
final String filename = output.getName();
final int dot = filename.lastIndexOf('.');
if (dot < 0) {
throw new IIOException(Errors.format(ErrorKeys.NO_IMAGE_WRITER));
}
final String extension = filename.substring(dot + 1).trim();
write(output, ImageIO.getImageWritersBySuffix(extension));
return this;
}
/**
* Writes outs the image contained into this {@link ImageWorker} as a PNG using the provided destination, compression and compression rate.
* <p>
* The destination object can be anything providing that we have an {@link ImageOutputStreamSpi} that recognizes it.
*
* @param destination where to write the internal {@link #image} as a PNG.
* @param compression algorithm.
* @param compressionRate percentage of compression.
* @param nativeAcc should we use native acceleration.
* @param paletted should we write the png as 8 bits?
* @return this {@link ImageWorker}.
* @throws IOException In case an error occurs during the search for an {@link ImageOutputStream} or during the eoncding process.
*
* @todo Current code doesn't check if the writer already accepts the provided destination. It wraps it in a {@link ImageOutputStream}
* inconditionnaly.
*/
public final void writePNG(final Object destination, final String compression,
final float compressionRate, final boolean nativeAcc, final boolean paletted)
throws IOException {
// Reformatting this image for PNG.
final boolean hasPalette = image.getColorModel() instanceof IndexColorModel;
final boolean hasColorModel = hasPalette ? false
: image.getColorModel() instanceof ComponentColorModel;
if (paletted && !hasPalette) {
// we have to reduce colors
forceIndexColorModelForGIF(true);
} else {
if (!hasColorModel && !hasPalette) {
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.fine("Forcing input image to be compatible with PNG: No palette, no component color model");
}
// png supports gray, rgb, rgba and paletted 8 bit, but not, for example, double and float values, or 16 bits palettes
forceComponentColorModel();
}
}
// PNG does not support all kinds of index color models
if (hasPalette) {
IndexColorModel icm = (IndexColorModel) image.getColorModel();
// PNG supports palettes with up to 256 colors, beyond that we have to expand to RGB
if (icm.getMapSize() > 256) {
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.fine("Forcing input image to be compatible with PNG: Palette with > 256 color is not supported.");
}
rescaleToBytes();
if (paletted) {
forceIndexColorModelForGIF(true);
}
}
}
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Encoded input image for png writer");
}
// Getting a writer.
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Getting a writer");
}
ImageWriter writer = null;
ImageWriterSpi originatingProvider = null;
// ImageIO
if (nativeAcc) {
if (CLIB_PNG_IMAGE_WRITER_SPI != null) {
// let me check if the native writer can encode this image
if (CLIB_PNG_IMAGE_WRITER_SPI.canEncodeImage(new ImageTypeSpecifier(image))) {
writer = CLIB_PNG_IMAGE_WRITER_SPI.createWriterInstance();
originatingProvider = CLIB_PNG_IMAGE_WRITER_SPI;
} else {
LOGGER.fine("The ImageIO PNG native encode cannot encode this image!");
writer = null;
originatingProvider = null;
}
} else {
LOGGER.fine("Unable to use Native ImageIO PNG writer.");
}
}
// move on with the writer quest
if (!nativeAcc || writer == null) {
final Iterator<ImageWriter> it = ImageIO.getImageWriters(new ImageTypeSpecifier(image),
"PNG");
if (!it.hasNext()) {
throw new IllegalStateException(Errors.format(ErrorKeys.NO_IMAGE_WRITER));
}
while (it.hasNext()) {
writer = it.next();
originatingProvider = writer.getOriginatingProvider();
// check that this is not the native one
if (CLIB_PNG_IMAGE_WRITER_SPI != null
&& originatingProvider.getClass().equals(
CLIB_PNG_IMAGE_WRITER_SPI.getClass())) {
if (it.hasNext()) {
writer = it.next();
originatingProvider = writer.getOriginatingProvider();
} else {
LOGGER.fine("Unable to use PNG writer different than ImageIO CLib one");
}
}
// let me check if the native writer can encode this image (paranoiac checks this was already performed by the ImageIO search
if (originatingProvider.canEncodeImage(new ImageTypeSpecifier(image))) {
break; // leave loop
}
// clean
writer = null;
originatingProvider = null;
}
}
// ok, last resort use the JDK one and reformat the image
if (writer == null) {
List providers = com.sun.media.imageioimpl.common.ImageUtil.getJDKImageReaderWriterSPI(
IIORegistry.getDefaultInstance(), "PNG", false);
if (providers == null || providers.isEmpty()) {
throw new IllegalStateException("Unable to find JDK Png encoder!");
}
originatingProvider = (ImageWriterSpi) providers.get(0);
writer = originatingProvider.createWriterInstance();
// kk, last resort reformat the image
forceComponentColorModel(true, true);
rescaleToBytes();
if (!originatingProvider.canEncodeImage(image)) {
throw new IllegalArgumentException(
"Unable to find a valid PNG Encoder! And believe me, we tried hard!");
}
}
LOGGER.fine("Using ImageIO Writer with SPI: "
+ originatingProvider.getClass().getCanonicalName());
// Getting a stream.
LOGGER.fine("Setting write parameters for this writer");
ImageWriteParam iwp = null;
final ImageOutputStream memOutStream = ImageIOExt.createImageOutputStream(image,
destination);
if (memOutStream == null) {
throw new IIOException(Errors.format(ErrorKeys.NULL_ARGUMENT_$1, "stream"));
}
if (CLIB_PNG_IMAGE_WRITER_SPI != null
&& originatingProvider.getClass().equals(CLIB_PNG_IMAGE_WRITER_SPI.getClass())) {
// Compressing with native.
LOGGER.fine("Writer is native");
iwp = writer.getDefaultWriteParam();
// Define compression mode
iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
// best compression
iwp.setCompressionType(compression);
// we can control quality here
iwp.setCompressionQuality(compressionRate);
// destination image type
iwp.setDestinationType(new ImageTypeSpecifier(image.getColorModel(), image
.getSampleModel()));
} else {
// Compressing with pure Java.
LOGGER.fine("Writer is NOT native");
// Instantiating PNGImageWriteParam
iwp = new PNGImageWriteParam();
// Define compression mode
iwp.setCompressionMode(ImageWriteParam.MODE_DEFAULT);
}
LOGGER.fine("About to write png image");
try {
writer.setOutput(memOutStream);
writer.write(null, new IIOImage(image, null, null), iwp);
} finally {
try {
writer.dispose();
} catch (Throwable e) {
if (LOGGER.isLoggable(Level.FINEST))
LOGGER.log(Level.FINEST, e.getLocalizedMessage(), e);
}
try {
memOutStream.close();
} catch (Throwable e) {
if (LOGGER.isLoggable(Level.FINEST))
LOGGER.log(Level.FINEST, e.getLocalizedMessage(), e);
}
}
}
/**
* Writes outs the image contained into this {@link ImageWorker} as a GIF using the provided destination, compression and compression rate.
* <p>
* It is worth to point out that the only compressions algorithm availaible with the jdk {@link GIFImageWriter} is "LZW" while the compression
* rates have to be confined between 0 and 1. AN acceptable values is usally 0.75f.
* <p>
* The destination object can be anything providing that we have an {@link ImageOutputStreamSpi} that recognizes it.
*
* @param destination where to write the internal {@link #image} as a gif.
* @param compression The name of compression algorithm.
* @param compressionRate percentage of compression, as a number between 0 and 1.
* @return this {@link ImageWorker}.
* @throws IOException In case an error occurs during the search for an {@link ImageOutputStream} or during the eoncding process.
*
* @see #forceIndexColorModelForGIF(boolean)
*/
public final ImageWorker writeGIF(final Object destination, final String compression,
final float compressionRate) throws IOException {
forceIndexColorModelForGIF(true);
if (IMAGEIO_GIF_IMAGE_WRITER_SPI == null) {
throw new IIOException(Errors.format(ErrorKeys.NO_IMAGE_WRITER));
}
final ImageOutputStream stream = ImageIOExt.createImageOutputStream(image, destination);
if (stream == null)
throw new IIOException(Errors.format(ErrorKeys.NULL_ARGUMENT_$1, "stream"));
final ImageWriter writer = IMAGEIO_GIF_IMAGE_WRITER_SPI.createWriterInstance();
final ImageWriteParam param = writer.getDefaultWriteParam();
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
param.setCompressionType(compression);
param.setCompressionQuality(compressionRate);
try {
writer.setOutput(stream);
writer.write(null, new IIOImage(image, null, null), param);
} finally {
try {
stream.close();
} catch (Throwable e) {
if (LOGGER.isLoggable(Level.FINEST))
LOGGER.log(Level.FINEST, e.getLocalizedMessage(), e);
}
try {
writer.dispose();
} catch (Throwable e) {
if (LOGGER.isLoggable(Level.FINEST))
LOGGER.log(Level.FINEST, e.getLocalizedMessage(), e);
}
}
return this;
}
/**
* Writes outs the image contained into this {@link ImageWorker} as a JPEG using the provided destination , compression and compression rate.
* <p>
* The destination object can be anything providing that we have an {@link ImageOutputStreamSpi} that recognizes it.
*
* @param destination where to write the internal {@link #image} as a JPEG.
* @param compression algorithm.
* @param compressionRate percentage of compression.
* @param nativeAcc should we use native acceleration.
* @return this {@link ImageWorker}.
* @throws IOException In case an error occurs during the search for an {@link ImageOutputStream} or during the eoncding process.
*/
public final void writeJPEG(final Object destination, final String compression,
final float compressionRate, final boolean nativeAcc) throws IOException {
// Reformatting this image for jpeg.
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Encoding input image to write out as JPEG.");
}
// go to component color model if needed
ColorModel cm = image.getColorModel();
final boolean hasAlpha = cm.hasAlpha();
forceComponentColorModel();
cm = image.getColorModel();
// rescale to 8 bit
rescaleToBytes();
cm = image.getColorModel();
// remove transparent band
final int numBands = image.getSampleModel().getNumBands();
if (hasAlpha) {
retainBands(numBands - 1);
}
// Getting a writer.
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Getting a JPEG writer and configuring it.");
}
ImageWriter writer = null;
if (nativeAcc && CODEC_LIB_AVAILABLE && IMAGEIO_JPEG_IMAGE_WRITER_SPI != null) {
try {
writer = IMAGEIO_JPEG_IMAGE_WRITER_SPI.createWriterInstance();
} catch (Exception e) {
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.log(Level.INFO, "Unable to instantiate CLIB JPEG ImageWriter", e);
}
writer = null;
}
}
// in case we want the JDK one or in case the native one is not at hand we use the JDK one
if (writer == null) {
if (JDK_JPEG_IMAGE_WRITER_SPI == null) {
throw new IllegalStateException(Errors.format(ErrorKeys.ILLEGAL_CLASS_$2,
"Unable to find JDK JPEG Writer"));
}
writer = JDK_JPEG_IMAGE_WRITER_SPI.createWriterInstance();
}
// Compression is available on both lib
final ImageWriteParam iwp = writer.getDefaultWriteParam();
final ImageOutputStream outStream = ImageIOExt.createImageOutputStream(image, destination);
if (outStream == null) {
throw new IIOException(Errors.format(ErrorKeys.NULL_ARGUMENT_$1, "stream"));
}
iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
iwp.setCompressionType(compression); // Lossy compression.
iwp.setCompressionQuality(compressionRate); // We can control quality here.
if (iwp instanceof JPEGImageWriteParam) {
final JPEGImageWriteParam param = (JPEGImageWriteParam) iwp;
param.setOptimizeHuffmanTables(true);
try {
param.setProgressiveMode(JPEGImageWriteParam.MODE_DEFAULT);
} catch (UnsupportedOperationException e) {
throw new IOException(e);
}
}
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Writing out...");
}
try {
writer.setOutput(outStream);
// the JDK writer has problems with images that do not start at minx==miny==0
// while the clib writer has issues with tiled images
if ((!nativeAcc && (image.getMinX() != 0 || image.getMinY() != 0))
|| (nativeAcc && (image.getNumXTiles() > 1 || image.getNumYTiles() > 1))) {
final BufferedImage finalImage = new BufferedImage(image.getColorModel(),
((WritableRaster) image.getData()).createWritableTranslatedChild(0, 0),
image.getColorModel().isAlphaPremultiplied(), null);
writer.write(null, new IIOImage(finalImage, null, null), iwp);
} else {
writer.write(null, new IIOImage(image, null, null), iwp);
}
} finally {
try {
writer.dispose();
} catch (Throwable e) {
if (LOGGER.isLoggable(Level.FINEST))
LOGGER.log(Level.FINEST, e.getLocalizedMessage(), e);
}
try {
outStream.close();
} catch (Throwable e) {
if (LOGGER.isLoggable(Level.FINEST))
LOGGER.log(Level.FINEST, e.getLocalizedMessage(), e);
}
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Writing out... Done!");
}
}
}
/**
* Writes outs the image contained into this {@link ImageWorker} as a TIFF using the provided destination, compression and compression rate and
* basic tiling information
* <p>
* The destination object can be anything providing that we have an {@link ImageOutputStreamSpi} that recognizes it.
*
* @param destination where to write the internal {@link #image} as a TIFF.
* @param compression algorithm.
* @param compressionRate percentage of compression.
* @param nativeAcc should we use native acceleration.
* @param tileSizeX tile size x direction (or -1 if tiling is not desired)
* @param tileSizeY tile size y direction (or -1 if tiling is not desired)
* @return this {@link ImageWorker}.
* @throws IOException In case an error occurs during the search for an {@link ImageOutputStream} or during the eoncding process.
*/
public final void writeTIFF(final Object destination, final String compression,
final float compressionRate, final int tileSizeX, final int tileSizeY)
throws IOException {
// Reformatting this image for jpeg.
if (LOGGER.isLoggable(Level.FINER))
LOGGER.finer("Encoding input image to write out as TIFF.");
// Getting a writer.
if (LOGGER.isLoggable(Level.FINER))
LOGGER.finer("Getting a TIFF writer and configuring it.");
ImageWriter writer = null;
if (IMAGEIO_EXT_TIFF_IMAGE_WRITER_SPI == null) {
// our own is not there, strange... this should not happen
LOGGER.finer("Unable to find ImageIO-Ext Tiff Writer, looking for another one");
final Iterator<ImageWriter> it = ImageIO.getImageWritersByFormatName("TIFF");
if (!it.hasNext()) {
throw new IllegalStateException(Errors.format(ErrorKeys.NO_IMAGE_WRITER));
}
writer = it.next();
} else {
writer = IMAGEIO_EXT_TIFF_IMAGE_WRITER_SPI.createWriterInstance();
}
// checks
if (writer == null) {
throw new IllegalStateException("Unable to find Tiff ImageWriter!");
}
final ImageWriteParam iwp = writer.getDefaultWriteParam();
final ImageOutputStream outStream = ImageIOExt.createImageOutputStream(image, destination);
if (outStream == null) {
throw new IIOException(Errors.format(ErrorKeys.NULL_ARGUMENT_$1, "stream"));
}
if (compression != null) {
iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
iwp.setCompressionType(compression);
iwp.setCompressionQuality(compressionRate); // We can control quality here.
} else {
iwp.setCompressionMode(ImageWriteParam.MODE_DEFAULT);
}
if (tileSizeX > 0 && tileSizeY > 0) {
iwp.setTilingMode(ImageWriteParam.MODE_EXPLICIT);
iwp.setTiling(tileSizeX, tileSizeY, 0, 0);
}
if (LOGGER.isLoggable(Level.FINER)) {
LOGGER.finer("Writing out...");
}
try {
writer.setOutput(outStream);
writer.write(null, new IIOImage(image, null, null), iwp);
} finally {
try {
writer.dispose();
} catch (Throwable e) {
if (LOGGER.isLoggable(Level.FINEST))
LOGGER.log(Level.FINEST, e.getLocalizedMessage(), e);
}
try {
outStream.close();
} catch (Throwable e) {
if (LOGGER.isLoggable(Level.FINEST))
LOGGER.log(Level.FINEST, e.getLocalizedMessage(), e);
}
}
}
/**
* Performs an affine transform on the image, applying optimization such as affine removal in case the affine is an identity, affine merging if
* the affine is applied on top of another affine, and using optimized operations for integer translates
*
* @param tx
* @param interpolation
* @param bgValues
* @return
*/
public ImageWorker affine(AffineTransform tx, Interpolation interpolation, double[] bgValues) {
// identity elimination -> check the tx params against the image size to see if
// any if likely to actually move the image by at least one pixel
int size = Math.max(image.getWidth(), image.getHeight());
boolean hasScaleX = Math.abs(tx.getScaleX() - 1) * size > RS_EPS;
boolean hasScaleY = Math.abs(tx.getScaleY() - 1) * size > RS_EPS;
boolean hasShearX = Math.abs(tx.getShearX()) * size > RS_EPS;
boolean hasShearY = Math.abs(tx.getShearY()) * size > RS_EPS;
boolean hasTranslateX = Math.abs(tx.getTranslateX()) > RS_EPS;
boolean hasTranslateY = Math.abs(tx.getTranslateY()) > RS_EPS;
if (!hasScaleX && !hasScaleY && !hasShearX && !hasShearY && !hasTranslateX
&& !hasTranslateY) {
return this;
}
// apply defaults to allow for comparisong
ParameterListDescriptor pld = new AffineDescriptor()
.getParameterListDescriptor(RenderedRegistryMode.MODE_NAME);
if (interpolation == null) {
interpolation = (Interpolation) pld.getParamDefaultValue("interpolation");
}
if (bgValues == null) {
bgValues = (double[]) pld.getParamDefaultValue("backgroundValues");
}
// affine over affine/scale?
RenderedImage source = image;
if (image instanceof RenderedOp) {
RenderedOp op = (RenderedOp) image;
Object mtProperty = op.getProperty("MathTransform");
Object sourceBoundsProperty = op.getProperty("SourceBoundingBox");
String opName = op.getOperationName();
// check if we can do a warp-affine reduction
if (WARP_REDUCTION_ENABLED && "Warp".equals(opName)
&& mtProperty instanceof MathTransform2D
&& sourceBoundsProperty instanceof Rectangle) {
try {
// we can merge the affine into the warp
MathTransform2D originalTransform = (MathTransform2D) mtProperty;
MathTransformFactory factory = ReferencingFactoryFinder
.getMathTransformFactory(null);
MathTransform affineMT = factory
.createAffineTransform(new org.geotools.referencing.operation.matrix.AffineTransform2D(
tx));
MathTransform2D chained = (MathTransform2D) factory
.createConcatenatedTransform(affineMT.inverse(), originalTransform);
// setup the warp builder
Double tolerance = (Double) getRenderingHint(Hints.RESAMPLE_TOLERANCE);
if (tolerance == null) {
tolerance = (Double) Hints.getSystemDefault(Hints.RESAMPLE_TOLERANCE);
}
if (tolerance == null) {
tolerance = 0.333;
}
// setup a warp builder that is not gong to use too much memory
WarpBuilder wb = new WarpBuilder(tolerance);
wb.setMaxPositions(4 * 1024 * 1024);
// compute the target bbox the same way the affine would have to have a 1-1 match
RenderedOp at = AffineDescriptor.create(source, tx, interpolation, bgValues,
commonHints);
Rectangle targetBB = at.getBounds();
at.dispose();
Rectangle sourceBB = (Rectangle) sourceBoundsProperty;
// warp
Rectangle mappingBB;
if (source.getProperty("ROI") instanceof ROI) {
// Due to a limitation in JAI we need to make sure the
// mapping bounding box covers both source and target bounding box
// otherwise the warped roi image layout won't be computed properly
mappingBB = sourceBB.union(targetBB);
} else {
mappingBB = targetBB;
}
Warp warp = wb.buildWarp(chained, mappingBB);
// do the switch only if we get a warp that is as fast as the original one
Warp sourceWarp = (Warp) op.getParameterBlock().getObjectParameter(0);
if (warp instanceof WarpGrid
|| warp instanceof WarpAffine
|| !(sourceWarp instanceof WarpGrid || sourceWarp instanceof WarpAffine)) {
// and then the JAI Operation
PlanarImage sourceImage = op.getSourceImage(0);
final ParameterBlock paramBlk = new ParameterBlock().addSource(sourceImage);
Object property = sourceImage.getProperty("ROI");
if ((property == null) || property.equals(java.awt.Image.UndefinedProperty)
|| !(property instanceof ROI)) {
paramBlk.add(warp).add(interpolation).add(bgValues);
} else {
paramBlk.add(warp).add(interpolation).add(bgValues).add((ROI) property);
}
// force in the image layout, this way we get exactly the same
// as the affine we're eliminating
Hints localHints = new Hints(commonHints);
localHints.remove(JAI.KEY_IMAGE_LAYOUT);
ImageLayout il = new ImageLayout();
il.setMinX(targetBB.x);
il.setMinY(targetBB.y);
il.setWidth(targetBB.width);
il.setHeight(targetBB.height);
localHints.put(JAI.KEY_IMAGE_LAYOUT, il);
RenderedOp result = JAI.create("Warp", paramBlk, localHints);
result.setProperty("MathTransform", chained);
image = result;
return this;
}
} catch (Exception e) {
LOGGER.log(
Level.WARNING,
"Failed to squash warp and affine into a single operation, chaining them instead",
e);
// move on
}
}
// see if we can merge affine with other affine types then
if ("Affine".equals(opName)) {
ParameterBlock paramBlock = op.getParameterBlock();
RenderedImage sSource = paramBlock.getRenderedSource(0);
AffineTransform sTx = (AffineTransform) paramBlock.getObjectParameter(0);
Interpolation sInterp = (Interpolation) paramBlock.getObjectParameter(1);
double[] sBgValues = (double[]) paramBlock.getObjectParameter(2);
if ((sInterp == interpolation && Arrays.equals(sBgValues, bgValues))) {
// we can replace it
AffineTransform concat = new AffineTransform(tx);
concat.concatenate(sTx);
tx = concat;
source = sSource;
}
} else if ("Scale".equals(opName)) {
ParameterBlock paramBlock = op.getParameterBlock();
RenderedImage sSource = paramBlock.getRenderedSource(0);
float xScale = paramBlock.getFloatParameter(0);
float yScale = paramBlock.getFloatParameter(1);
float xTrans = paramBlock.getFloatParameter(2);
float yTrans = paramBlock.getFloatParameter(3);
Interpolation sInterp = (Interpolation) paramBlock.getObjectParameter(4);
if (sInterp == interpolation) {
// we can replace it
AffineTransform concat = new AffineTransform(tx);
concat.concatenate(new AffineTransform(xScale, 0, 0, yScale, xTrans, yTrans));
tx = concat;
source = sSource;
}
}
}
// check again params, we might have combined two transformations sets
hasScaleX = Math.abs(tx.getScaleX() - 1) * size > RS_EPS;
hasScaleY = Math.abs(tx.getScaleY() - 1) * size > RS_EPS;
hasShearX = Math.abs(tx.getShearX()) * size > RS_EPS;
hasShearY = Math.abs(tx.getShearY()) * size > RS_EPS;
hasTranslateX = Math.abs(tx.getTranslateX()) > RS_EPS;
hasTranslateY = Math.abs(tx.getTranslateY()) > RS_EPS;
boolean intTranslateX = Math.abs((tx.getTranslateX() - Math.round(tx.getTranslateX()))) < RS_EPS;
boolean intTranslateY = Math.abs((tx.getTranslateY() - Math.round(tx.getTranslateY()))) < RS_EPS;
// did it become a identity after the combination?
if (!hasScaleX && !hasScaleY && !hasShearX && !hasShearY && !hasTranslateX
&& !hasTranslateY) {
this.image = source;
return this;
}
if (!hasShearX && !hasShearY) {
if (!hasScaleX && !hasScaleY && intTranslateX && intTranslateY) {
// this will do an integer translate, but to get there we need to remove the image layout
Hints localHints = new Hints(commonHints);
localHints.remove(JAI.KEY_IMAGE_LAYOUT);
image = ScaleDescriptor.create(source, 1.0f, 1.0f,
(float) Math.round(tx.getTranslateX()),
(float) Math.round(tx.getTranslateY()), interpolation, localHints);
} else {
// generic scale
image = ScaleDescriptor.create(source, (float) tx.getScaleX(),
(float) tx.getScaleY(), (float) tx.getTranslateX(),
(float) tx.getTranslateY(), interpolation, commonHints);
}
} else {
image = AffineDescriptor.create(source, tx, interpolation, bgValues, commonHints);
}
return this;
}
/**
* Crops the image to the specified bounds. Will use an internal operation that ensures the tile cache and tile scheduler hints are used, and will
* perform operation elimination in case the crop is doing nothing, or in case the crop is performed over another crop
*
* @param x
* @param y
* @param width
* @param height
* @return
*/
public ImageWorker crop(float x, float y, float width, float height) {
// no op elimination
if (image.getMinX() == x && image.getMinY() == y && image.getWidth() == width
&& image.getHeight() == height) {
return this;
}
// crop over crop
RenderedImage source = image;
if (image instanceof RenderedOp) {
RenderedOp op = (RenderedOp) image;
if ("Crop".equals(op.getOperationName()) || "GTCrop".equals(op.getOperationName())) {
ParameterBlock paramBlock = op.getParameterBlock();
source = paramBlock.getRenderedSource(0);
float sx = (float) paramBlock.getFloatParameter(0);
float sy = (float) paramBlock.getFloatParameter(1);
float sWidth = (float) paramBlock.getFloatParameter(2);
float sHeight = (float) paramBlock.getFloatParameter(3);
// merge the two (just need to sum the two origins)
if (sx > 0) {
x = sx + x;
}
if (sy > 0) {
y = sy + y;
}
}
}
image = GTCropDescriptor.create(source, x, y, width, height, commonHints);
return this;
}
/**
* Writes the {@linkplain #image} to the specified output, trying all encoders in the specified iterator in the iteration order.
*
* @return this {@link ImageWorker}.
*/
private ImageWorker write(final Object output, final Iterator<? extends ImageWriter> encoders)
throws IOException {
if (encoders != null) {
while (encoders.hasNext()) {
final ImageWriter writer = encoders.next();
final ImageWriterSpi spi = writer.getOriginatingProvider();
final Class<?>[] outputTypes;
if (spi == null) {
outputTypes = ImageWriterSpi.STANDARD_OUTPUT_TYPE;
} else {
/*
* If the encoder is for some format handled in a special way (e.g. GIF), apply the required operation. Note that invoking the
* same method many time (e.g. "forceIndexColorModelForGIF", which could occurs if there is more than one GIF encoder registered)
* should not hurt - all method invocation after the first one should be no-op.
*/
final String[] formats = spi.getFormatNames();
if (containsFormatName(formats, "gif")) {
forceIndexColorModelForGIF(true);
} else {
tile();
}
if (!spi.canEncodeImage(image)) {
continue;
}
outputTypes = spi.getOutputTypes();
}
/*
* Now try to set the output directly (if possible), or as an ImageOutputStream if the encoder doesn't accept directly the specified
* output. Note that some formats like HDF may not support ImageOutputStream.
*/
final ImageOutputStream stream;
if (acceptInputType(outputTypes, output.getClass())) {
writer.setOutput(output);
stream = null;
} else if (acceptInputType(outputTypes, ImageOutputStream.class)) {
stream = ImageIOExt.createImageOutputStream(image, output);
writer.setOutput(stream);
} else {
continue;
}
/*
* Now saves the image.
*/
writer.write(image);
writer.dispose();
if (stream != null) {
stream.close();
}
return this;
}
}
throw new IIOException(Errors.format(ErrorKeys.NO_IMAGE_WRITER));
}
/**
* Returns {@code true} if the specified array contains the specified type.
*/
private static boolean acceptInputType(final Class<?>[] types, final Class<?> searchFor) {
for (int i = types.length; --i >= 0;) {
if (searchFor.isAssignableFrom(types[i])) {
return true;
}
}
return false;
}
/**
* Returns {@code true} if the specified array contains the specified string.
*/
private static boolean containsFormatName(final String[] formats, final String searchFor) {
for (int i = formats.length; --i >= 0;) {
if (searchFor.equalsIgnoreCase(formats[i])) {
return true;
}
}
return false;
}
// /////////////////////////////////////////////////////////////////////////////////////
// ////// ////////
// ////// DEBUGING HELP ////////
// ////// ////////
// /////////////////////////////////////////////////////////////////////////////////////
/**
* Shows the current {@linkplain #image} in a window together with the operation chain as a {@linkplain javax.swing.JTree tree}. This method is
* provided mostly for debugging purpose. This method requires the {@code gt2-widgets-swing.jar} file in the classpath.
*
* @throws HeadlessException if {@code gt2-widgets-swing.jar} is not on the classpath, or if AWT can't create the window components.
* @return this {@link ImageWorker}.
*
* @see org.geotools.gui.swing.image.OperationTreeBrowser#show(RenderedImage)
*/
public final ImageWorker show() throws HeadlessException {
/*
* Uses reflection because the "gt2-widgets-swing.jar" dependency is optional and may not be available in the classpath. All the complicated
* stuff below is simply doing this call:
*
* OperationTreeBrowser.show(image);
*
* Tip: The @see tag in the above javadoc can be used as a check for the existence of class and method referenced below. Check for the javadoc
* warnings.
*/
final Class<?> c;
try {
c = Class.forName("org.geotools.gui.swing.image.OperationTreeBrowser");
} catch (ClassNotFoundException cause) {
final HeadlessException e;
e = new HeadlessException("The \"gt2-widgets-swing.jar\" file is required.");
e.initCause(cause);
throw e;
}
try {
c.getMethod("show", new Class[] { RenderedImage.class }).invoke(null,
new Object[] { image });
} catch (InvocationTargetException e) {
final Throwable cause = e.getCause();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
}
if (cause instanceof Error) {
throw (Error) cause;
}
throw new AssertionError(e);
} catch (Exception e) {
/*
* ClassNotFoundException may be expected, but all other kinds of checked exceptions (and they are numerous...) are errors.
*/
throw new AssertionError(e);
}
return this;
}
/**
* Provides a hint that this {@link ImageWorker} will no longer be accessed from a reference in user space. The results are equivalent to those
* that occur when the program loses its last reference to this image, the garbage collector discovers this, and finalize is called. This can be
* used as a hint in situations where waiting for garbage collection would be overly conservative.
* <p>
* Mind, this also results in disposing the JAI Image chain attached to the image the worker is applied to, so don't call this method on image
* changes (full/partial) that you want to use.
* <p>
* {@link ImageWorker} defines this method to remove the image being disposed from the list of sinks in all of its source images. The results of
* referencing an {@link ImageWorker} after a call to dispose() are undefined.
*/
public final void dispose() {
if (commonHints != null) {
this.commonHints.clear();
}
this.commonHints = null;
this.roi = null;
if (this.image instanceof PlanarImage) {
ImageUtilities.disposePlanarImageChain(PlanarImage.wrapRenderedImage(image));
} else if (this.image instanceof BufferedImage) {
((BufferedImage) this.image).flush();
this.image = null;
}
}
/**
* Loads the image from the specified file, and {@linkplain #show display} it in a window. This method is mostly as a convenient way to test
* operation chains. This method can be invoked from the command line. If an optional {@code -operation} argument is provided, the Java method
* (one of the image operations provided in this class) immediately following it is executed. Example:
*
* <blockquote>
*
* <pre>
* java org.geotools.image.ImageWorker -operation binarize <var><filename></var>
* </pre>
*
* </blockquote>
*/
public static void main(String[] args) {
final Arguments arguments = new Arguments(args);
final String operation = arguments.getOptionalString("-operation");
args = arguments.getRemainingArguments(1);
if (args.length != 0)
try {
final ImageWorker worker = new ImageWorker(new File(args[0]));
// Force usage of tile cache for every operations, including intermediate steps.
worker.setRenderingHint(JAI.KEY_TILE_CACHE, JAI.getDefaultInstance().getTileCache());
if (operation != null) {
worker.getClass().getMethod(operation, (Class[]) null)
.invoke(worker, (Object[]) null);
}
/*
* TIP: Tests operations here (before the call to 'show()'), if wanted.
*/
worker.show();
} catch (FileNotFoundException e) {
arguments.printSummary(e);
} catch (NoSuchMethodException e) {
arguments.printSummary(e);
} catch (Exception e) {
e.printStackTrace(arguments.err);
}
}
}