/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2006-2016, 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.geom.NoninvertibleTransformException; import java.awt.geom.Rectangle2D; 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.Vector; 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.BorderExtender; import javax.media.jai.ColorCube; import javax.media.jai.Histogram; import javax.media.jai.IHSColorSpace; import javax.media.jai.ImageFunction; 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.OperationDescriptor; import javax.media.jai.OperationRegistry; import javax.media.jai.ParameterBlockJAI; import javax.media.jai.ParameterListDescriptor; import javax.media.jai.PlanarImage; import javax.media.jai.PropertyGenerator; import javax.media.jai.ROI; import javax.media.jai.ROIShape; 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.AddDescriptor; import javax.media.jai.operator.ConstantDescriptor; import javax.media.jai.operator.ExtremaDescriptor; import javax.media.jai.operator.HistogramDescriptor; import javax.media.jai.operator.InvertDescriptor; import javax.media.jai.operator.MeanDescriptor; import javax.media.jai.operator.MosaicDescriptor; import javax.media.jai.operator.MosaicType; import javax.media.jai.operator.MultiplyConstDescriptor; import javax.media.jai.operator.SubtractDescriptor; import javax.media.jai.operator.XorConstDescriptor; import javax.media.jai.registry.RenderedRegistryMode; import org.geotools.factory.Hints; 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 it.geosolutions.jaiext.utilities.ImageLayout2; import it.geosolutions.jaiext.vectorbin.ROIGeometry; import org.opengis.coverage.processing.OperationNotFoundException; 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; import it.geosolutions.jaiext.JAIExt; import it.geosolutions.jaiext.algebra.AlgebraDescriptor; import it.geosolutions.jaiext.algebra.AlgebraDescriptor.Operator; import it.geosolutions.jaiext.classifier.ColorMapTransform; import it.geosolutions.jaiext.colorconvert.IHSColorSpaceJAIExt; import it.geosolutions.jaiext.colorindexer.ColorIndexer; import it.geosolutions.jaiext.lookup.LookupTable; import it.geosolutions.jaiext.lookup.LookupTableFactory; import it.geosolutions.jaiext.piecewise.PiecewiseTransform1D; import it.geosolutions.jaiext.range.NoDataContainer; import it.geosolutions.jaiext.range.Range; import it.geosolutions.jaiext.range.RangeFactory; import it.geosolutions.jaiext.stats.HistogramWrapper; import it.geosolutions.jaiext.stats.Statistics; import it.geosolutions.jaiext.stats.Statistics.StatsType; /** * 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 { private static final double[] ROI_BACKGROUND = new double[] {0}; private static final double[][] ROI_THRESHOLDS = new double[][] {{1.0}}; private static final String OPERATION_CONST_OP_NAME = "operationConst"; private static final String ALGEBRIC_OP_NAME = "algebric"; public final static String JAIEXT_ENABLED_KEY = "org.geotools.coverage.jaiext.enabled"; public final static boolean JAIEXT_ENABLED; public static boolean isJaiExtEnabled() { return JAIEXT_ENABLED; } /** * 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(); /** Registration of the JAI-EXT operations */ static { JAIEXT_ENABLED = Boolean.getBoolean(JAIEXT_ENABLED_KEY); JAIExt.initJAIEXT(JAIEXT_ENABLED); } /** 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; } protected static OperationDescriptor getOperationDescriptor(final String name) throws OperationNotFoundException { final OperationRegistry registry = JAI.getDefaultInstance().getOperationRegistry(); OperationDescriptor operation = (OperationDescriptor) registry.getDescriptor(RenderedRegistryMode.MODE_NAME, name); if (operation != null) { return operation; } throw new OperationNotFoundException(Errors.format(ErrorKeys.OPERATION_NOT_FOUND_$1, name)); } /** * 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"; /** * The image property name generated by {@link HistogramDescriptor}. */ private static final String HISTOGRAM = "histogram"; /** * The image property name generated by {@link MeanDescriptor}. */ private static final String MEAN = "mean"; /** * Register manually the GTCrop operation, in web containers JAI registration may fails */ static { if (WARP_REDUCTION_ENABLED) { GTWarpPropertyGenerator.register(false); } LOGGER.log(Level.INFO, "Warp/affine reduction enabled: " + WARP_REDUCTION_ENABLED); GTAffinePropertyGenerator.register(false); } /** * 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 NoData range to check nodata, or {@code null} if none. */ private Range nodata; /** * Array of values used for indicating the background values */ private double[] background; /** * 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} or * a {@linkplain #mosaic mosaic operation} * * @see #load(String, int, boolean) * @see #mosaic(RenderedImage[], MosaicType, PlanarImage[], ROI[], double[][], Range[]) */ public ImageWorker() { inheritanceStopPoint = this.image = null; } /** * Creates a new uninitialized worker with RenderingHints for a {@linkplain #mosaic mosaic operation} * * * @see #mosaic(RenderedImage[], MosaicType, PlanarImage[], ROI[], double[][], Range[]) */ public ImageWorker(RenderingHints hints) { this(); setRenderingHints(hints); } /** * 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) { setImage(image); } private Range extractNoDataProperty(final RenderedImage image) { Object property = image.getProperty(NoDataContainer.GC_NODATA); if(property != null){ if(property instanceof NoDataContainer){ return ((NoDataContainer)property).getAsRange(); }else if(property instanceof Double){ return RangeFactory.create((Double)property, (Double)property); } } return null; } /** * 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; setNoData(extractNoDataProperty(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; } // Creating a parameter block ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); // Executing the operation return JAI.create("Null", pb, getRenderingHints()); } /** * Returns the {@linkplain #getRenderedImage rendered image} after null operation. * This operation may be used for setting new ImageProperties or for applying new RenderingHints. * * @return The rendered operation. * * @see #getRenderedImage * @see #getPlanarImage * @see #getImageAsROI */ public ImageWorker nullOp() { // Creating a parameter block ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); // Executing the operation image = JAI.create("Null", pb, getRenderingHints()); return this; } /** * 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; } /** * Returns the <cite>NoData Range</cite> currently set, or {@code null} if none. The default value is {@code null}. * * @return The current NoData Range. */ public final Range getNoData() { return nodata; } /** * Returns the <cite>NoData Range</cite> currently set, or {@code null} if none. The default value is {@code null}. * * @return The current NoData Range. */ public final double[] getDestinationNoData() { return background; } /** * Returns true if destination NoData values must be set and they must be used in computation */ public boolean isNoDataNeeded() { return roi != null || nodata != null; } /** * 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; // If ROI == null remove it also from the image properties PlanarImage pl = getPlanarImage(); if (roi == null) { pl.removeProperty("ROI"); } else { pl.setProperty("ROI", roi); } image = pl; invalidateStatistics(); return this; } /** * Set the <cite>NoData Range</cite> for checking NoData during computation. * * @param nodata The new NoData Range. * @return This ImageWorker * */ public final ImageWorker setNoData(final Range nodata) { this.nodata = nodata; if(nodata != null && image != null){ PlanarImage img = getPlanarImage(); img.setProperty(NoDataContainer.GC_NODATA, new NoDataContainer(nodata)); image = img; } else if(image != null){ PlanarImage img = getPlanarImage(); Object property = img.getProperty(NoDataContainer.GC_NODATA); if(property != null && property != Image.UndefinedProperty){ img.removeProperty(NoDataContainer.GC_NODATA); image = img; } } invalidateStatistics(); return this; } /** * Set the image background value * * @param background The image background. * @return This ImageWorker * */ public final ImageWorker setBackground(final double[] background) { this.background = background; 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(RenderingHint) */ public final ImageWorker setRenderingHints(final RenderingHints hints) { if (commonHints == null) { commonHints = new RenderingHints(null); } if (hints != null) { commonHints.add(hints); } return this; } public final ImageWorker removeRenderingHints() { if (commonHints != null) { commonHints = null; } 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; // Create the parameterBlock ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); if (JAIExt.isJAIExtOperation("Stats")) { StatsType[] stats = new StatsType[] { StatsType.EXTREMA }; // Band definition int numBands = getNumBands(); int[] bands = new int[numBands]; for (int i = 0; i < numBands; i++) { bands[i] = i; } // Image parameters pb.set(ONE, 0); // xPeriod pb.set(ONE, 1); // yPeriod pb.set(roi, 2); // ROI pb.set(nodata, 3); // NoData pb.set(bands, 5); // band indexes pb.set(stats, 6); // statistic operation image = JAI.create("Stats", pb, getRenderingHints()); // Retrieving the statistics Statistics[][] results = (Statistics[][]) getComputedProperty(Statistics.STATS_PROPERTY); double[][] ext = new double[2][numBands]; for (int i = 0; i < numBands; i++) { double[] extBand = (double[]) results[i][0].getResult(); ext[0][i] = extBand[0]; ext[1][i] = extBand[1]; } // Setting the property if (image instanceof PlanarImage) { ((PlanarImage) image).setProperty(EXTREMA, ext); } else { PlanarImage p = getPlanarImage(); p.setProperty(EXTREMA, ext); image = p; } } else { pb.set(roi, 0); // The region of the image to scan. Default to all. pb.set(ONE, 1); // The horizontal sampling rate. Default to 1. pb.set(ONE, 2); // The vertical sampling rate. Default to 1. pb.set(ONE, 4); // Maximum number of run length codes to store. Default to 1. image = JAI.create("Extrema", pb, getRenderingHints()); } extrema = getComputedProperty(EXTREMA); } return (double[][]) extrema; } /** * Returns the histogram of the image. */ public Histogram getHistogram(int[] numBins, double[] lowValues, double[] highValues) { Object histogram = getComputedProperty(HISTOGRAM); if (!(histogram instanceof Histogram)) { final Integer ONE = 1; // Create the parameterBlock ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); if (JAIExt.isJAIExtOperation("Stats")) { StatsType[] stats = new StatsType[] { StatsType.HISTOGRAM }; // Band definition int numBands = getNumBands(); int[] bands = new int[numBands]; for (int i = 0; i < numBands; i++) { bands[i] = i; } // Image parameters pb.set(ONE, 0); // xPeriod pb.set(ONE, 1); // yPeriod pb.set(roi, 2); // ROI pb.set(nodata, 3); // NoData pb.set(bands, 5); // band indexes pb.set(stats, 6); // statistic operation pb.set(numBins, 9); // Bin number. pb.set(lowValues, 7); // Lower values per band. pb.set(highValues, 8); // Higher values per band. image = JAI.create("Stats", pb, getRenderingHints()); // Retrieving the statistics Statistics[][] results = (Statistics[][]) getComputedProperty(Statistics.STATS_PROPERTY); int[][] bins = new int[numBands][]; // Cycle on the bands for(int i = 0; i < results.length; i++){ Statistics stat = results[i][0]; double[] binsDouble = (double[]) stat.getResult(); bins[i] = new int[binsDouble.length]; for(int j = 0; j < binsDouble.length; j++){ bins[i][j] = (int) binsDouble[j]; } } ParameterBlock parameterBlock = getRenderedOperation().getParameterBlock(); if(numBins == null){ numBins = (int[]) parameterBlock.getObjectParameter(9); } if(lowValues == null){ lowValues = (double[]) parameterBlock.getObjectParameter(7); } if(highValues == null){ highValues = (double[]) parameterBlock.getObjectParameter(8); } HistogramWrapper wrapper = new HistogramWrapper(numBins, lowValues, highValues, bins); // Setting the property if (image instanceof PlanarImage) { ((PlanarImage) image).setProperty(HISTOGRAM, wrapper); } else { PlanarImage p = getPlanarImage(); p.setProperty(HISTOGRAM, wrapper); image = p; } } else { pb.set(roi, 0); // The region of the image to scan. Default to all. pb.set(ONE, 1); // The horizontal sampling rate. Default to 1. pb.set(ONE, 2); // The vertical sampling rate. Default to 1. pb.set(numBins, 3); // Bin number. pb.set(lowValues, 4); // Lower values per band. pb.set(highValues, 5); // Higher values per band. image = JAI.create("Histogram", pb, getRenderingHints()); } histogram = getComputedProperty(HISTOGRAM); } return (Histogram) histogram; } /** * Returns the minimums and maximums values found in the image. Those extremas are returned as an array of the form {@code double[2][#bands]}. */ public double[] getMean() { Object mean = getComputedProperty(MEAN); if (!(mean instanceof double[])) { final Integer ONE = 1; // Create the parameterBlock ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); if (JAIExt.isJAIExtOperation("Stats")) { StatsType[] stats = new StatsType[] { StatsType.MEAN }; // Band definition int numBands = getNumBands(); int[] bands = new int[numBands]; for (int i = 0; i < numBands; i++) { bands[i] = i; } // Image parameters pb.set(ONE, 0); // xPeriod pb.set(ONE, 1); // yPeriod pb.set(roi, 2); // ROI pb.set(nodata, 3); // NoData pb.set(bands, 5); // band indexes pb.set(stats, 6); // statistic operation image = JAI.create("Stats", pb, getRenderingHints()); // Retrieving the statistics Statistics[][] results = (Statistics[][]) getComputedProperty(Statistics.STATS_PROPERTY); double[] meanBands = new double[numBands]; for (int i = 0; i < numBands; i++) { meanBands[i] = (double) results[i][0].getResult(); } // Setting the property if (image instanceof PlanarImage) { ((PlanarImage) image).setProperty(MEAN, meanBands); } else { PlanarImage p = getPlanarImage(); p.setProperty(MEAN, meanBands); image = p; } } else { pb.set(roi, 0); // The region of the image to scan. Default to all. pb.set(ONE, 1); // The horizontal sampling rate. Default to 1. pb.set(ONE, 2); // The vertical sampling rate. Default to 1. image = JAI.create("Mean", pb, getRenderingHints()); } mean = getComputedProperty(MEAN); } return (double[]) mean; } /** * 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 || cm.getColorSpace() instanceof IHSColorSpaceJAIExt; } /** * 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]; final double destNodata = (background != null && background.length > 0) ? background[0] : ((nodata != null && !nodata.contains(0)) ? 0d: Double.NaN); 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) { ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); // The source image. pb.set(scale, 0); // The per-band constants to multiply by. pb.set(offset, 1); // The per-band offsets to be added. pb.set(roi, 2); // ROI pb.set(nodata, 3); // NoData range if (isNoDataNeeded() && !Double.isNaN(destNodata)) { pb.set(destNodata, 5); } image = JAI.create("Rescale", pb, hints); } else { ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); // The source image. pb.set(DataBuffer.TYPE_BYTE, 0); // The destination image data type (BYTE) image = JAI.create("Format", pb, hints); setNoData(RangeFactory.convert(nodata, DataBuffer.TYPE_BYTE)); } 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) { // error diffusion final KernelJAI ditherMask = KernelJAI.ERROR_FILTER_FLOYD_STEINBERG; final LookupTableJAI colorMap = ColorCube.BYTE_496; // Creation of the ParameterBlock ParameterBlock pb = new ParameterBlock(); // Setting source pb.setSource(image, 0); // Setting parameters pb.set(colorMap, 0); pb.set(ditherMask, 1); pb.set(roi, 2); pb.set(nodata, 3); if (isNoDataNeeded()) { if (background != null && background.length > 0) { int dest = (int) background[0]; pb.set(dest, 4); } } image = JAI.create("ErrorDiffusion", pb, hints); } else { // ordered dither final KernelJAI[] ditherMask = KernelJAI.DITHER_MASK_443; final ColorCube colorMap = ColorCube.BYTE_496; // Creation of the ParameterBlock ParameterBlock pb = new ParameterBlock(); // Setting source pb.setSource(image, 0); // Setting parameters pb.set(colorMap, 0); pb.set(ditherMask, 1); pb.set(roi, 2); pb.set(nodata, 3); if (isNoDataNeeded()) { if (background != null && background.length > 0) { int dest = (int) background[0]; pb.set(dest, 4); } } image = JAI.create("OrderedDither", pb, 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 LookupTable 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 = LookupTableFactory .create(table, image.getSampleModel().getDataType()); } else { final short[] table = new short[mapSize]; for (int i = 0; i < mapSize; i++) { table[i] = (short) ((oldCM.getAlpha(i) == 0) ? suggestedTransparent : i); } lookupTable = LookupTableFactory.create(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); // ParameterBlock definition ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); pb.set(lookupTable, 0); pb.set(roi, 2); pb.set(nodata, 3); if (isNoDataNeeded()) { if (background != null && background.length > 0) { pb.set(background[0], 1); } } image = JAI.create("Lookup", pb, hints); // New parameterblock for format operation pb = new ParameterBlock(); pb.setSource(image, 0); int dataType = image.getSampleModel().getDataType(); pb.set(dataType, 0); image = JAI.create("Format", pb, hints); // Converting NoData Range setNoData(RangeFactory.convert(nodata, dataType)); } 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(); // ParameterBlock creation ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); pb.set(new int[] { --numBands }, 0); final RenderedOp alphaChannel = JAI.create("BandSelect", pb, 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); LookupTable lut = null; switch (datatype) { case DataBuffer.TYPE_BYTE: { final byte[][] data = new byte[numDestinationBands][256]; 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]); } if(icm.getMapSize() < 256) { Color bgColor = getBackgroundColor(); if(bgColor == null) { bgColor = Color.BLACK; } byte r = (byte) (bgColor.getRed() & 0xFF); byte g = (byte) (bgColor.getRed() & 0xFF); byte b = (byte) (bgColor.getBlue() & 0xFF); byte a = (byte) (bgColor.getAlpha() & 0xFF); for (int i = icm.getMapSize(); i < 256; i++) { data[0][i] = r; if (numDestinationBands >= 2) { // remember to optimize for grayscale images if (!gray) { data[1][i] = g; } else { data[1][i] = a; } } if (numDestinationBands >= 3) { data[2][i] = b; } if (numDestinationBands == 4) { data[3][i] = a; } } } lut = LookupTableFactory.create(data, datatype); } 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 = LookupTableFactory.create(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 = 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); // ParameterBlock definition ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); pb.set(lut, 0); pb.set(roi, 2); pb.set(nodata, 3); if (isNoDataNeeded()) { if (background != null && background.length > 0) { pb.set(background[0], 1); } } image = JAI.create("Lookup", pb, 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); ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); // The source image. pb.set(type, 0); image = JAI.create("Format", pb, hints); setNoData(RangeFactory.convert(nodata, type)); } 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 ColorSpace ihs = isJaiExtEnabled() ? IHSColorSpaceJAIExt.getInstance() : 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); // Setting the parameter blocks ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); pb.set(cm, 0); pb.set(roi, 1); pb.set(nodata, 2); if (isNoDataNeeded()) { if (background != null && background.length > 0) { // Elaborating the final NoData value if (background.length != cm.getNumColorComponents()) { throw new IllegalArgumentException("Wrong DestinationNoData value defined"); } pb.set(background, 3); } } image = JAI.create("ColorConvert", pb, 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; pb.addSource(sourceImage); for (int i = 0; i < length; i++) { pb.addSource(firstBand); } pb.set(new Range[] { nodata }, 0); if (isNoDataNeeded()) { if (background != null && background.length > 0) { // Elaborating the final NoData value pb.set(background[0], 1); } } pb.set(roi, 3); sourceImage = JAI.create("bandmerge", pb); image = 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) { return addBand(image, before, false, null); } /** * 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. * @param addAlpha <code>true</code> if we want to set the last image as alpha, <code>false</code> otherwise. * * @return this {@link ImageWorker}. * */ public final ImageWorker addBand(RenderedImage image, boolean before, boolean addAlpha, Range nodata2) { ParameterBlock pb = new ParameterBlock(); if (before) { pb.setSource(image, 0); pb.setSource(this.image, 1); } else { pb.setSource(this.image, 0); pb.setSource(image, 1); } pb.set(new Range[] { nodata, nodata2 }, 0); if (isNoDataNeeded() || nodata2 != null) { if (background != null && background.length > 0) { double dest = background[0]; pb.set(dest, 1); } } pb.set(roi, 3); pb.set(addAlpha, 4); this.image = JAI.create("BandMerge", pb, this.getRenderingHints()); invalidateStatistics(); 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. * @param addAlpha <code>true</code> if we want to set the last image as alpha, <code>false</code> otherwise. * * @return this {@link ImageWorker}. * */ public final ImageWorker addBands(RenderedImage[] bands, boolean addAlpha, Range[] nodata2) { return addBands(bands, addAlpha, nodata2, null); } /** * 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. * @param addAlpha <code>true</code> if we want to set the last image as alpha, <code>false</code> otherwise. * @param transformationList List of AffineTransformation that can be applied to the input rasters in order to repoject * them to the same CRS. * * @return this {@link ImageWorker}. * */ public final ImageWorker addBands(RenderedImage[] bands, boolean addAlpha, Range[] nodata2, List<AffineTransform> transformationList) { ParameterBlock pb = new ParameterBlock(); for(RenderedImage band : bands){ pb.addSource(band); } Range[] newRange = new Range[bands.length + 1]; newRange[0] = nodata; if(nodata2 != null){ System.arraycopy(nodata2, 0, newRange, 1, nodata2.length); } pb.set(newRange, 0); if (isNoDataNeeded()) { if (background != null && background.length > 0) { double dest = background[0]; pb.set(dest, 1); } } pb.set(transformationList, 3); pb.set(roi, 3); pb.set(addAlpha, 4); this.image = JAI.create("BandMerge", pb, 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); forceColorModel(cm); 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); // ParameterBlock definition ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); pb.set(coeff, 0); pb.set(roi, 1); pb.set(nodata, 2); if (isNoDataNeeded()) { if (background != null && background.length > 0) { pb.set(background[0], 3); } } image = JAI.create("BandCombine", pb, 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; } // ParameterBlock creation ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); pb.set(bands, 0); image = JAI.create("BandSelect", pb, 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) { // ParameterBlock creation ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); pb.set(bands, 0); image = JAI.create("BandSelect", pb, 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) { ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); // The source image. pb.set(dataType, 0); image = JAI.create("Format", pb, getRenderingHints()); setNoData(RangeFactory.convert(nodata, dataType)); // 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(); // ParameterBlock definition ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); pb.set(threshold, 0); pb.set(roi, 1); pb.set(nodata, 2); image = JAI.create("Binarize", pb, hints); setNoData(null); 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 LookupTable table; final int min = Math.min(value0, value1); if (min >= 0) { final int max = Math.max(value0, value1); if (max < 256) { table = LookupTableFactory.create(new byte[] { (byte) value0, (byte) value1 }, DataBuffer.TYPE_BYTE); } else if (max < 65536) { table = LookupTableFactory.create(new short[] { (short) value0, (short) value1 }, true); } else { table = LookupTableFactory.create(new int[] { value0, value1 }); } } else { table = LookupTableFactory.create(new int[] { value0, value1 }, DataBuffer.TYPE_BYTE); } // ParameterBlock definition ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); pb.set(table, 0); pb.set(roi, 2); pb.set(nodata, 3); if (isNoDataNeeded()) { if (background != null && background.length > 0) { pb.set(background[0], 1); } } image = JAI.create("Lookup", pb, 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)); // ParameterBlock definition ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); // The source image. pb.set(image.getSampleModel().getDataType(), 0); image = JAI.create("Format", pb, hints); setNoData(RangeFactory.convert(nodata, image.getSampleModel().getDataType())); 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; } // ParameterBlock creation ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); pb.set(opaqueBands, 0); image = JAI.create("BandSelect", pb, 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. LookupTable table = LookupTableFactory.create(tableData, image.getSampleModel() .getDataType()); // Do the lookup operation. // we should not transform on color map here hints.put(JAI.KEY_TRANSFORM_ON_COLORMAP, Boolean.FALSE); // ParameterBlock definition ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); pb.set(table, 0); pb.set(roi, 2); pb.set(nodata, 3); if(isNoDataNeeded()){ if (background != null && background.length > 0) { pb.set(background[0], 1); } } PlanarImage luImage = JAI.create("Lookup", pb, hints); // 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); // ParameterBlock definition pb = new ParameterBlock(); pb.setSource(luImage, 0); pb.set(matrix, 0); pb.set(roi, 1); pb.set(nodata, 2); if (background != null && background.length > 0) { pb.set(background[0], 3); } luImage = JAI.create("BandCombine", pb, getRenderingHints()); // luImage = BandCombineDescriptor.create(luImage, matrix, hints); } pb = new ParameterBlock(); pb.setSource(image, 0); pb.setSource(luImage, 1); pb.set(new Range[] { nodata }, 0); if (isNoDataNeeded()) { if (background != null && background.length > 0) { double dest = background[0]; pb.set(dest, 1); } } pb.set(roi, 3); pb.set(true, 4); image = JAI.create("BandMerge", pb, hints); // image = BandMergeDescriptor.create(image, luImage, hints); invalidateStatistics(); return this; } /** * Inverts the pixel values of the {@linkplain #image}. * * @see InvertDescriptor */ public final ImageWorker invert() { // ParameterBlock creation ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); if (JAIExt.isJAIExtOperation(ALGEBRIC_OP_NAME)) { pb.set(AlgebraDescriptor.Operator.INVERT, 0); pb.set(roi, 1); pb.set(nodata, 2); if (isNoDataNeeded()) { if (background != null && background.length > 0) { double dest = background[0]; pb.set(dest, 3); } } image = JAI.create(ALGEBRIC_OP_NAME, pb, getRenderingHints()); } else { image = JAI.create("Invert", pb, 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 LookupTable lut = LookupTableFactory.create(lutData, mask.getSampleModel() .getDataType()); // ParameterBlock definition ParameterBlock pb = new ParameterBlock(); pb.setSource(mask, 0); pb.set(lut, 0); if (background != null && background.length > 0) { pb.set(background[0], 1); } pb.set(roi, 2); mask = JAI.create("Lookup", pb, hints); // mask = LookupDescriptor.create(mask, lut, hints); /* * Adding to the other image exploiting the implict clamping */ pb = new ParameterBlock(); pb.setSource(image, 0); pb.setSource(mask, 1); if (JAIExt.isJAIExtOperation(ALGEBRIC_OP_NAME)) { prepareAlgebricOperation(Operator.SUM, pb, roi, nodata, true); image = JAI.create(ALGEBRIC_OP_NAME, pb, getRenderingHints()); } else { image = JAI.create("Add", pb, getRenderingHints()); } // image = AddDescriptor.create(image, mask, getRenderingHints()); tileCacheEnabled(true); invalidateStatistics(); return this; } else { // general case // it has to be binary if (!isBinary()) binarize(); // Split between JAI and JAI-EXT operations boolean algebricJAIExt = JAIExt.isJAIExtOperation(ALGEBRIC_OP_NAME); boolean opConstJAIExt = JAIExt.isJAIExtOperation(OPERATION_CONST_OP_NAME); ParameterBlock pb; // now if we mask with 1 we have to invert the mask RenderingHints renderingHints = new RenderingHints(JAI.KEY_REPLACE_INDEX_COLOR_MODEL, Boolean.FALSE); if (maskValue) { pb = new ParameterBlock(); pb.setSource(mask, 0); if (algebricJAIExt) { prepareAlgebricOperation(Operator.NOT, pb, roi, null, false); mask = JAI.create(ALGEBRIC_OP_NAME, pb, renderingHints); } else { mask = JAI.create("Not", pb, renderingHints); } } // and with the image to zero the interested pixels tileCacheEnabled(false); pb = new ParameterBlock(); pb.setSource(mask, 0); pb.setSource(image, 1); if (algebricJAIExt) { prepareAlgebricOperation(Operator.AND, pb, roi, nodata, true); image = JAI.create(ALGEBRIC_OP_NAME, pb, getRenderingHints()); } else { image = JAI.create("And", pb, getRenderingHints()); } // image = AndDescriptor.create(mask, image, getRenderingHints()); // add the new value to the mask pb = new ParameterBlock(); pb.setSource(mask, 0); if (opConstJAIExt) { prepareOpConstOperation(Operator.SUM, new double[] { newValue }, pb, roi, null, false); image = JAI.create(OPERATION_CONST_OP_NAME, pb, renderingHints); } else { image = JAI.create("AddConst", pb, renderingHints); } // mask = AddConstDescriptor.create(mask, new double[] { newValue }, renderingHints); // add the mask to the image to mask with the new value pb = new ParameterBlock(); pb.setSource(mask, 0); pb.setSource(image, 1); if (algebricJAIExt) { prepareAlgebricOperation(Operator.SUM, pb, roi, nodata, true); image = JAI.create(ALGEBRIC_OP_NAME, pb, getRenderingHints()); } else { image = JAI.create("Add", pb, getRenderingHints()); } // image = AddDescriptor.create(mask, image, getRenderingHints()); tileCacheEnabled(true); invalidateStatistics(); return this; } } private void prepareAlgebricOperation(Operator op, ParameterBlock pb, ROI roi, Range nodata, boolean setDestNoData) { pb.set(op, 0); pb.set(roi, 1); pb.set(nodata, 2); if (background != null && background.length > 0) { pb.set(background[0], 3); // We must set the new NoData value if (setDestNoData && roi != null && nodata != null) { setNoData(RangeFactory.create(background[0], background[0])); } } } private void prepareOpConstOperation(Operator op, double[] values, ParameterBlock pb, ROI roi, Range nodata, boolean setDestNoData) { pb.set(op, 1); pb.set(values, 0); pb.set(roi, 2); pb.set(nodata, 3); if (background != null && background.length > 0) { pb.set(background[0], 4); // We must set the new NoData value if (setDestNoData && roi != null && nodata != null) { setNoData(RangeFactory.create(background[0], background[0])); } } } /** * 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) { ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); pb.setSource(renderedImage, 1); if (JAIExt.isJAIExtOperation(ALGEBRIC_OP_NAME)) { prepareAlgebricOperation(Operator.SUM, pb, roi, nodata, true); image = JAI.create(ALGEBRIC_OP_NAME, pb, getRenderingHints()); } else { image = JAI.create("Add", pb, getRenderingHints()); } // 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) { ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); if (JAIExt.isJAIExtOperation(OPERATION_CONST_OP_NAME)) { prepareOpConstOperation(Operator.MULTIPLY, inValues, pb, roi, nodata, true); image = JAI.create(OPERATION_CONST_OP_NAME, pb, getRenderingHints()); } else { image = JAI.create("MultiplyConst", pb, getRenderingHints()); } // image = MultiplyConstDescriptor.create(image, inValues, getRenderingHints()); invalidateStatistics(); return this; } /** * Takes two rendered or renderable source images, and myltiply form each pixel the related value of the second image, each one from each source * image of the corresponding position and band. See JAI {@link MultiplyDescriptor} for details. * * @param renderedImage the {@link RenderedImage} to be multiplied to this {@link ImageWorker}. * @return this {@link ImageWorker}. * * @see MultiplyDescriptor */ public final ImageWorker multiply(RenderedImage renderedImage) { ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); pb.setSource(renderedImage, 1); if (JAIExt.isJAIExtOperation(ALGEBRIC_OP_NAME)) { prepareAlgebricOperation(Operator.MULTIPLY, pb, roi, nodata, true); image = JAI.create(ALGEBRIC_OP_NAME, pb, getRenderingHints()); } else { image = JAI.create("Multiply", pb, 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) { ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); if (JAIExt.isJAIExtOperation(OPERATION_CONST_OP_NAME)) { double[] valuesD = new double[values.length]; for (int i = 0; i < values.length; i++) { valuesD[i] = values[i]; } prepareOpConstOperation(Operator.XOR, valuesD, pb, roi, nodata, true); image = JAI.create(OPERATION_CONST_OP_NAME, pb, getRenderingHints()); } else { image = JAI.create("XorConst", pb, getRenderingHints()); } // image = XorConstDescriptor.create(image, values, getRenderingHints()); invalidateStatistics(); return this; } /** * Takes two rendered or renderable source images, and subtract form each pixel the related value of the second image, each one from each source * image of the corresponding position and band. See JAI {@link AddDescriptor} for details. * * @param renderedImage the {@link RenderedImage} to be subtracted to this {@link ImageWorker}. * @return this {@link ImageWorker}. * * @see SubtractDescriptor */ public final ImageWorker subtract(final RenderedImage renderedImage) { ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); pb.setSource(renderedImage, 1); if (JAIExt.isJAIExtOperation(ALGEBRIC_OP_NAME)) { prepareAlgebricOperation(Operator.SUBTRACT, pb, roi, nodata, true); image = JAI.create(ALGEBRIC_OP_NAME, pb, getRenderingHints()); } else { image = JAI.create("Subtract", pb, getRenderingHints()); } invalidateStatistics(); return this; } /** * Takes two rendered or renderable source images, and do an OR for each pixel images, each one from each source * image of the corresponding position and band. See JAI {@link AddDescriptor} for details. * * @param renderedImage the {@link RenderedImage} to be subtracted to this {@link ImageWorker}. * @return this {@link ImageWorker}. * * @see SubtractDescriptor */ public final ImageWorker or(final RenderedImage renderedImage) { ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); pb.setSource(renderedImage, 1); if (JAIExt.isJAIExtOperation(ALGEBRIC_OP_NAME)) { prepareAlgebricOperation(Operator.OR, pb, roi, nodata, true); image = JAI.create(ALGEBRIC_OP_NAME, pb, getRenderingHints()); } else { image = JAI.create("Or", pb, getRenderingHints()); } invalidateStatistics(); return this; } public final ImageWorker artifactsFilter(int threshold, int filterSize) { ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); pb.set(roi, 0); pb.set(background, 1); pb.set(threshold, 2); pb.set(filterSize, 3); pb.set(nodata, 4); 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(); // ParameterBlock definition ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); // The source image. pb.set(type, 0); image = JAI.create("Format", pb, hints); setNoData(RangeFactory.convert(nodata, type)); } 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); LookupTable table = buildOpacityLookupTable(0, 1, -1, image.getSampleModel() .getDataType()); ImageLayout layout = new ImageLayout(image); layout.setColorModel(newColorModel); RenderingHints hints = new RenderingHints(JAI.KEY_IMAGE_LAYOUT, layout); // ParameterBlock definition ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); pb.set(table, 0); pb.set(roi, 2); pb.set(nodata, 3); if (isNoDataNeeded()) { if (background != null && background.length > 0) { pb.set(background[0], 1); } } result = JAI.create("Lookup", pb, hints); // 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)); ParameterBlock pb = new ParameterBlock(); pb.setSource(expanded, 0); pb.setSource(alphaBand, 1); pb.set(new Range[] { nodata, nodata == null ? null : RangeFactory.create(alpha - 1, alpha - 1) }, 0); if (isNoDataNeeded()) { if (background != null && background.length > 0) { double dest = background[0]; pb.set(dest, 1); } } pb.set(roi, 3); pb.set(true, 4); result = JAI.create("BandMerge", pb, null); // 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; // ParameterBlock definition ParameterBlock pb = new ParameterBlock(); pb.setSource(expanded, 0); LookupTable table = buildOpacityLookupTable(opacity, bands, alphaBand, expanded .getSampleModel().getDataType()); pb.set(table, 0); pb.set(roi, 2); pb.set(nodata, 3); if (isNoDataNeeded()) { if (background != null && background.length > 0) { pb.set(background[0], 1); } } result = JAI.create("Lookup", pb, null); // 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 */ LookupTable buildOpacityLookupTable(float opacity, final int bands, int alphaBand, int dataType) { 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; } } } LookupTable table = LookupTableFactory.create(matrix, dataType); 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 = getOperationDescriptor("affine").getParameterListDescriptor(RenderedRegistryMode.MODE_NAME); if (interpolation == null) { interpolation = (Interpolation) pld.getParamDefaultValue("interpolation"); } if (bgValues == null) { if (background == null || background.length <= 0) { bgValues = (double[]) pld.getParamDefaultValue("backgroundValues"); } else { bgValues = background; } } // Setting the new backgroung values background = bgValues; // 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 final ParameterBlock sourceParamBlock = op.getParameterBlock(); 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 ParameterBlock pb = new ParameterBlock(); pb.setSource(source, 0); pb.set(tx, 0); pb.set(interpolation, 1); pb.set(bgValues, 2); pb.set(roi, 3); pb.set(true, 5); pb.set(nodata, 6); RenderedOp at = JAI.create("Affine", pb, getRenderingHints()); // commonHints); Rectangle targetBB = at.getBounds(); int tileWidth = at.getTileWidth(); int tileHeight = at.getTileHeight(); ImageUtilities.disposeSinglePlanarImage(at); 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) sourceParamBlock.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"); // Boolean indicating if optional ROI may be reprojected back to the initial image boolean canProcessROI = true; // Boolean indicating if NoData are the same as for the source operation or are not present Range oldNoData = (Range) (sourceParamBlock.getNumParameters() > 3 ? sourceParamBlock.getObjectParameter(4) : null); boolean hasSameNodata = (oldNoData == null && nodata == null) || (oldNoData != null && nodata != null && oldNoData.equals(nodata)); if (((property == null) || property.equals(java.awt.Image.UndefinedProperty) || !(property instanceof ROI))) { paramBlk.add(warp).add(interpolation).add(bgValues); if (oldNoData != null) { paramBlk.set(oldNoData, 4); } // Try to reproject ROI after Warp ROI newROI = null; if (roi != null) { ROI reprojectedROI = roi; try { MathTransform inverse = originalTransform.inverse(); if (inverse instanceof AffineTransform) { AffineTransform inv = (AffineTransform) inverse; newROI = reprojectedROI.transform(inv); } } catch (Exception e) { if (LOGGER.isLoggable(Level.WARNING)) { LOGGER.log( Level.WARNING, "Unable to compute the inverse of the new ROI provided", e); } // Skip Warp Affine reduction canProcessROI = false; } } if (newROI != null) { setROI(newROI); paramBlk.set(newROI, 3); } } else { // Intersect ROIs ROI newROI = null; if (roi != null) { // Try to reproject ROI after Warp ROI reprojectedROI = roi; try { MathTransform inverse = originalTransform.inverse(); if (inverse instanceof AffineTransform) { AffineTransform inv = (AffineTransform) inverse; reprojectedROI = reprojectedROI.transform(inv); newROI = reprojectedROI.intersect((ROI) property); } } catch (Exception e) { if (LOGGER.isLoggable(Level.WARNING)) { LOGGER.log( Level.WARNING, "Unable to compute the inverse of the new ROI provided", e); } // Skip Warp Affine reduction canProcessROI = false; } } else { newROI = (ROI) property; } setROI(newROI); paramBlk.add(warp).add(interpolation).add(newROI); if (oldNoData != null) { paramBlk.set(oldNoData, 4); } } // handle background values if (bgValues == null && sourceParamBlock.getNumParameters() > 2) { bgValues = (double[]) sourceParamBlock.getObjectParameter(2); } if (bgValues != null) { paramBlk.set(bgValues, 2); } // Checks if ROI can be processed if (canProcessROI && hasSameNodata){ // force in the image layout, this way we get exactly the same // as the affine we're eliminating Hints localHints = new Hints(getRenderingHints()); 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); il.setTileHeight(tileWidth); il.setTileWidth(tileHeight); il.setTileGridXOffset(0); il.setTileGridYOffset(0); localHints.put(JAI.KEY_IMAGE_LAYOUT, il); RenderedOp result = JAI.create("Warp", paramBlk, localHints); result.setProperty("MathTransform", chained); image = result; // getting the new ROI property Object prop = result.getProperty("roi"); if (prop != null && prop instanceof ROI){ setROI((ROI) prop); } else { setROI(null); } 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 = sourceParamBlock; RenderedImage sSource = paramBlock.getRenderedSource(0); AffineTransform sTx = (AffineTransform) paramBlock.getObjectParameter(0); Interpolation sInterp = (Interpolation) paramBlock.getObjectParameter(1); double[] sBgValues = (double[]) paramBlock.getObjectParameter(2); Range nodata = null; ROI r = null; boolean similarROI = true; boolean hasSameNodata = true; // Minor checks on ROI and NoData if(paramBlock.getNumParameters() > 3){ nodata = (Range) paramBlock.getObjectParameter(6); r = (ROI)paramBlock.getObjectParameter(3); if(r != null){ try { AffineTransform inverse = sTx.createInverse(); ROI newROI = this.roi != null ? this.roi.transform(inverse) : null; similarROI = newROI != null && newROI.intersects(r.getBounds()); } catch (NoninvertibleTransformException e) { LOGGER.log(Level.SEVERE, e.getMessage(), e); } hasSameNodata = nodata == null || (sBgValues != null && this.nodata != null && sBgValues.length > 0 && sBgValues[0] == this.nodata .getMin().doubleValue()); } } if ((sInterp == interpolation && Arrays.equals(sBgValues, bgValues)) && ((nodata == null || hasSameNodata) && (r == null || similarROI))) { // we can replace it AffineTransform concat = new AffineTransform(tx); concat.concatenate(sTx); tx = concat; source = sSource; if(similarROI && r != null){ try { AffineTransform inverse = sTx.createInverse(); ROI newROI = this.roi != null ? this.roi.transform(inverse) : null; this.roi = newROI.intersect(r); } catch (NoninvertibleTransformException e) { LOGGER.log(Level.SEVERE, e.getMessage(), e); } } if (hasSameNodata && nodata != null) { setNoData(nodata); } } } else if ("Scale".equals(opName)) { ParameterBlock paramBlock = sourceParamBlock; 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); Range nodata = null; ROI r = null; boolean similarROI = true; boolean hasSameNodata =true; // Minor checks on ROI and NoData final int numParameters = paramBlock.getNumParameters(); if (numParameters > 5){ r = (ROI) paramBlock.getObjectParameter(5); nodata = numParameters > 7 ? (Range) paramBlock.getObjectParameter(7) : null; // The background may haven't been set double[] sBgValues = numParameters > 8 ? (double[]) paramBlock.getObjectParameter(8) : null; if (r != null) { try { AffineTransform sTx = AffineTransform.getScaleInstance(xScale, yScale); sTx.concatenate(AffineTransform.getTranslateInstance(xTrans, yTrans)); AffineTransform inverse = sTx.createInverse(); ROI newROI = this.roi != null ? this.roi.transform(inverse) : null; similarROI = newROI != null && newROI.intersects(r.getBounds()); } catch (NoninvertibleTransformException e) { LOGGER.log(Level.SEVERE, e.getMessage(), e); } } hasSameNodata = nodata == null || (sBgValues != null && this.nodata != null && sBgValues.length > 0 && sBgValues[0] == this.nodata .getMin().doubleValue()); } if (sInterp == interpolation && ((nodata == null || hasSameNodata) && (r == null || similarROI))) { // we can replace it AffineTransform concat = new AffineTransform(tx); concat.concatenate(new AffineTransform(xScale, 0, 0, yScale, xTrans, yTrans)); tx = concat; source = sSource; if(similarROI && r != null){ try { AffineTransform sTx = AffineTransform.getScaleInstance(xScale, yScale); sTx.concatenate(AffineTransform.getTranslateInstance(xTrans, yTrans)); AffineTransform inverse = sTx.createInverse(); ROI newROI = this.roi != null ? this.roi.transform(inverse) : null; this.roi = newROI.intersect(r); } catch (NoninvertibleTransformException e) { LOGGER.log(Level.SEVERE, e.getMessage(), e); } } if (hasSameNodata && nodata != null) { setNoData(nodata); } } } } // 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; boolean nonNegativeScaleX = tx.getScaleX() >= 0; boolean nonNegativeScaleY = tx.getScaleY() >= 0; // did it become a identity after the combination? if (!hasScaleX && !hasScaleY && !hasShearX && !hasShearY && !hasTranslateX && !hasTranslateY) { this.image = source; return this; } ParameterBlock pb = new ParameterBlock(); pb.setSource(source, 0); if (!hasShearX && !hasShearY && nonNegativeScaleX && nonNegativeScaleY) { 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(getRenderingHints()); localHints.remove(JAI.KEY_IMAGE_LAYOUT); pb.set(1.0f, 0); pb.set(1.0f, 1); pb.set((float) Math.round(tx.getTranslateX()), 2); pb.set((float) Math.round(tx.getTranslateY()), 3); pb.set(interpolation, 4); pb.set(roi, 5); pb.set(nodata, 7); if (isNoDataNeeded()) { if (background != null && background.length > 0) { pb.set(background, 8); } } image = JAI.create("Scale", pb, localHints); // getting the new ROI property if (roi != null) { PropertyGenerator gen = getOperationDescriptor("Scale") .getPropertyGenerators(RenderedRegistryMode.MODE_NAME)[0]; Object prop = gen.getProperty("roi", image); if (prop != null && prop instanceof ROI) { setROI((ROI) prop); } else { setROI(null); } } } else { // generic scale pb.set((float) tx.getScaleX(), 0); pb.set((float) tx.getScaleY(), 1); pb.set((float) tx.getTranslateX(), 2); pb.set((float) tx.getTranslateY(), 3); pb.set(interpolation, 4); pb.set(roi, 5); pb.set(nodata, 7); if (isNoDataNeeded()) { if (background != null && background.length > 0) { pb.set(background, 8); } } image = JAI.create("Scale", pb, getRenderingHints()); if (roi != null) { PropertyGenerator gen = getOperationDescriptor("Scale") .getPropertyGenerators(RenderedRegistryMode.MODE_NAME)[0]; Object prop = gen.getProperty("roi", image); if (prop != null && prop instanceof ROI) { setROI((ROI) prop); } else { setROI(null); } } } } else { pb.set(tx, 0); pb.set(interpolation, 1); pb.set(bgValues, 2); pb.set(roi, 3); pb.set(true, 5); pb.set(nodata, 6); image = JAI.create("Affine", pb, getRenderingHints()); if (roi != null) { PropertyGenerator gen = getOperationDescriptor("Affine") .getPropertyGenerators(RenderedRegistryMode.MODE_NAME)[0]; Object prop = gen.getProperty("roi", image); if (prop != null && prop instanceof ROI) { setROI((ROI) prop); } else { setROI(null); } } } 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 = paramBlock.getFloatParameter(0); float sy = paramBlock.getFloatParameter(1); float sWidth = paramBlock.getFloatParameter(2); float sHeight = paramBlock.getFloatParameter(3); Rectangle2D.Float sourceBounds = new Rectangle2D.Float(sx, sy, sWidth, sHeight); Rectangle2D.Float bounds = new Rectangle.Float(x, y, width, height); Rectangle2D intersection = bounds.createIntersection(sourceBounds); x = (float) intersection.getMinX(); y = (float) intersection.getMinY(); width = (float) intersection.getWidth(); height = (float) intersection.getHeight(); } } ParameterBlock pb = new ParameterBlock(); pb.setSource(source, 0); pb.set(x, 0); pb.set(y, 1); pb.set(width, 2); pb.set(height, 3); pb.set(roi, 4); pb.set(nodata, 5); if (isNoDataNeeded()) { double destinationNoData = nodata != null? nodata.getMin().doubleValue() : (background!= null && background.length > 0)? background[0] : Double.NaN; if (!Double.isNaN(destinationNoData)){ pb.set(new double[]{destinationNoData}, 6); } } image = JAI.create("Crop", pb, commonHints); invalidateStatistics(); return this; } public ImageWorker function(ImageFunction function, int w, int h, float xScale, float yScale, float xTrans, float yTrans) { if(image != null){ if (LOGGER.isLoggable(Level.FINE)) { LOGGER.log(Level.FINE, "Input image already present but will be replaced by ImageFunction"); } } // Create a new parameter block ParameterBlock pb = new ParameterBlock(); pb.add(function).add(w).add(h).add(xScale).add(yScale).add(xTrans).add(yTrans).add(roi) .add(nodata); if (isNoDataNeeded()) { if (background != null && background.length > 0) { pb.add((float)background[0]); } } RenderedImage result = JAI.create("ImageFunction", pb, getRenderingHints()); setImage(result); return this; } /** * Returns the background colors as a value, if at all possible (3 or 4 values in the right range) * @return */ private Color getBackgroundColor() { if(background == null || background.length < 3 || background.length > 4) { return null; } for (int i = 0; i < background.length; i++) { double component = background[i]; if(component < 0 || component > 255) { return null; } } if(background.length == 3) { return new Color((int) background[0], (int) background[1], (int) background[2]); } else if(background.length == 4) { return new Color((int) background[0], (int) background[1], (int) background[2], (int) background[3]); } else { return null; } } public ImageWorker mosaic(RenderedImage[] images, MosaicType type, PlanarImage[] alphas, ROI[] rois, double[][] thresholds, Range[] nodata) { // check if we might be applying a background value that's not in palettes, still assuming // the input images have uniform palettes double[] background = this.background; if(images != null && images.length > 0) { ColorModel cmref = images[0].getColorModel(); Color backgroundColor = getBackgroundColor(); if(cmref instanceof IndexColorModel && (cmref.getTransparency() != IndexColorModel.OPAQUE || images[0].getProperty("ROI") instanceof ROI) && backgroundColor != null) { IndexColorModel icm = (IndexColorModel) cmref; int index = ColorUtilities.getColorIndex(icm, backgroundColor, -1); Color color; if(icm.hasAlpha()) { color = new Color(icm.getRed(index), icm.getGreen(index), icm.getBlue(index)); } else { color = new Color(icm.getRed(index), icm.getGreen(index), icm.getBlue(index), icm.getAlpha(index)); } if(color.equals(backgroundColor)) { background = new double[] {index}; } else { // we have to expand to RGB to apply that value for (int i = 0; i < images.length; i++) { images[i] = new ImageWorker(images[i]).forceComponentColorModel().getRenderedImage(); } } } } // ParameterBlock creation ParameterBlock pb = new ParameterBlock(); int srcNum = 0; //pb.addSource(image); if(images != null && images.length > 0){ for(int i = 0; i < images.length; i++){ if(images[i] != null){ pb.addSource(images[i]); srcNum++; } } } // Setting ROIs ROI[] roisNew = null; if(rois != null && srcNum > 0){ roisNew = new ROI[srcNum]; System.arraycopy(rois, 0, roisNew, 0, rois.length); } // Setting Alphas PlanarImage[] alphasNew = null; if(alphas != null && srcNum > 0){ alphasNew = new PlanarImage[srcNum]; System.arraycopy(alphas, 0, alphasNew, 0, alphas.length); } // Setting NoData Range[] nodataNew = null; boolean noInternalNoData = true; if(nodata != null && srcNum > 0){ nodataNew = new Range[srcNum]; System.arraycopy(nodata, 0, nodataNew, 0, nodata.length); } else { nodataNew = new Range[srcNum]; for(int i = 0; i < srcNum; i++){ RenderedImage img = pb.getRenderedSource(i); Range nodProp = extractNoDataProperty(img); noInternalNoData &= (nodProp == null); nodataNew[i] = nodProp; } } if(noInternalNoData && thresholds != null){ nodataNew = handleMosaicThresholds(thresholds, srcNum); } // Setting the parameters pb.add(type); pb.add(alphasNew); pb.add(roisNew); pb.add(thresholds); pb.add(background); pb.add(nodataNew); image = JAI.create("Mosaic", pb, getRenderingHints()); // Setting the final ROI as union of the older ROIs, assuming // we did not apply a background color, in that case, there is no more ROI to // care for if(background == null) { if(roisNew != null ) { ROI finalROI = mosaicROIs(pb.getSources(), roisNew); setROI(finalROI); } } else { setROI(null); } return this; } private ROI mosaicROIs(Vector sources, ROI... roiArray) { if(roiArray == null) { return null; } // collect all ROIs List<ROI> rois = new ArrayList<>(Arrays.asList(roiArray)); int numSources = sources.size(); if(roiArray.length < numSources){ for(int i = roiArray.length; i < numSources; i++){ RenderedImage img = (RenderedImage) sources.get(i); ROI r = new ROIShape(new Rectangle(img.getMinX(), img.getMinY(), img.getWidth(), img.getHeight())); rois.add(r); } } // bail out for the simple case without creating new objects if(rois.size() == 1) { return rois.get(0); } // prepare the vector union, take aside a ROIGeometry if possible // as it can add both ROIShape and ROIGeometry List<ROI> rasterROIs = new ArrayList<>(); List<ROI> vectorROIs = new ArrayList<>(); ROI vectorReference = null; for (ROI roi : rois) { if(roi instanceof ROIShape || roi instanceof ROIGeometry) { if(vectorReference == null && roi instanceof ROIGeometry) { vectorReference = (ROIGeometry) roi; } else { vectorROIs.add(roi); } } else { rasterROIs.add(roi); } } if(vectorReference == null && vectorROIs.size() > 0) { vectorReference = vectorROIs.remove(0); } // accumulate the vector ROIs, if any for (ROI roi : vectorROIs) { vectorReference = vectorReference.add(roi); } // optimization in case we end up with just one ROI, no need to mosaic if(rasterROIs.size() == 0) { return vectorReference; } else if(rasterROIs.size() == 1 && vectorReference == null) { return rasterROIs.get(0); } // ok, rasterize the vector one if any and mosaic ParameterBlock pb = new ParameterBlock(); if(vectorReference != null) { pb.addSource(vectorReference.getAsImage()); } for (ROI rasterROI : rasterROIs) { pb.addSource(rasterROI.getAsImage()); } pb.add(javax.media.jai.operator.MosaicDescriptor.MOSAIC_TYPE_OVERLAY); pb.add(null); // alphas pb.add(null); // ROI (null to avoid double bit -> byte expansion of the ROI data pb.add(ROI_THRESHOLDS); pb.add(ROI_BACKGROUND); pb.add(handleMosaicThresholds(ROI_THRESHOLDS, rasterROIs.size() + (vectorReference != null ? 1 : 0))); RenderedImage roiMosaic = JAI.create("Mosaic", pb, getRenderingHints()); return new ROI(roiMosaic); } private Range[] handleMosaicThresholds(double[][] thresholds, int srcNum) { Range[] nodata = new Range[srcNum]; int minSrcNum = Math.min(srcNum, thresholds.length); for(int i = 0; i < minSrcNum; i++){ double maxValue = Double.NEGATIVE_INFINITY; int numBands = thresholds[i].length; for(int b = 0; b < numBands; b++){ double bandValue = thresholds[i][b]; if(bandValue > maxValue){ maxValue = bandValue; } } nodata[i] = RangeFactory.create(Double.NEGATIVE_INFINITY, true, maxValue, false); } if(minSrcNum < srcNum){ for(int i = minSrcNum; i < srcNum; i++){ nodata[i] = nodata[0]; } } return nodata; } public ImageWorker border(int leftPad, int rightPad, int topPad, int bottomPad, BorderExtender ext){ ParameterBlock pb = new ParameterBlock(); pb.addSource(image); pb.add(leftPad); pb.add(rightPad); pb.add(topPad); pb.add(bottomPad); pb.add(ext); pb.add(nodata); if (isNoDataNeeded()) { if (background != null && background.length > 0) { pb.add(background); } } image = JAI.create("Border", pb, getRenderingHints()); return this; } public ImageWorker translate(float xTrans, float yTrans, Interpolation interp){ ParameterBlock pb = new ParameterBlock(); pb.addSource(image); pb.add(xTrans); pb.add(yTrans); pb.add(interp); // do not use getRenderingHints() with translate, as it cannot deal with layout hints image = JAI.create("Translate", pb, commonHints); return this; } /** * Warps the underlying raster using the provided Warp object. */ public ImageWorker warp(Warp warp, Interpolation interp) { ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); // The source image. pb.set(warp, 0); pb.set(interp, 1); pb.set(roi, 3); pb.set(nodata, 4); pb.set(background, 2); if (isNoDataNeeded()) { if (background != null && background.length > 0) { // // We must set the new NoData value // setNoData(RangeFactory.create(background[0], background[0])); // invalidateStatistics(); } } image = JAI.create("Warp", pb, getRenderingHints()); Object prop = image.getProperty("roi"); if(prop != null && prop instanceof ROI){ setROI((ROI) prop); } else { setROI(null); } return this; } /** * Scales the underlying raster using the provided parameters. */ public ImageWorker scale(float xScale, float yScale, float xTrans, float yTrans, Interpolation interp) { ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); // The source image. pb.set(xScale, 0); pb.set(yScale, 1); pb.set(xTrans, 2); pb.set(yTrans, 3); pb.set(interp, 4); pb.set(roi, 5); pb.set(false, 6); pb.set(nodata, 7); if (isNoDataNeeded()) { if (background != null && background.length > 0) { pb.set(background, 8); } } image = JAI.create("Scale", pb, getRenderingHints()); // getting the new ROI property PropertyGenerator gen = getOperationDescriptor("Scale").getPropertyGenerators(RenderedRegistryMode.MODE_NAME)[0]; Object prop = gen.getProperty("roi", image); if(prop != null && prop instanceof ROI){ setROI((ROI) prop); } else { setROI(null); } return this; } /** * Warps the underlying raster using the provided Warp object. */ public ImageWorker lookup(LookupTable table) { // ParameterBlock definition ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); pb.set(table, 0); pb.set(roi, 2); // Convert the NoData if(nodata != null){ nodata = RangeFactory.convert(nodata, image.getSampleModel().getDataType()); } pb.set(nodata, 3); if (isNoDataNeeded()) { if (background != null && background.length > 0) { pb.set(background[0], 1); } } image = JAI.create("Lookup", pb, getRenderingHints()); return this; } /** * Warps the underlying using the provided Warp object. */ public ImageWorker colorIndex(ColorIndexer indexer) { ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); // The source image. pb.set(indexer, 0); pb.set(roi, 1); pb.set(nodata, 2); if (isNoDataNeeded()) { if (background != null && background.length > 0) { pb.set(background, 3); } } image = JAI.create("ColorIndexer", pb, getRenderingHints()); return this; } /** * Apply a Raster classification on the underlying image. */ public ImageWorker classify(ColorMapTransform domain1D, Integer bandIndex) { ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); // The source image. pb.set(domain1D, 0); pb.set(bandIndex, 1); pb.set(roi, 2); pb.set(nodata, 3); if (isNoDataNeeded()) { if (domain1D.hasGaps()) { // We must set the new NoData value setNoData(RangeFactory.create(domain1D.getDefaultValue(), domain1D.getDefaultValue())); } } image = JAI.create("RasterClassifier", pb, getRenderingHints()); return this; } /** * Apply a Generic Piecewise operation on the underlying image. */ public ImageWorker piecewise(PiecewiseTransform1D transform, Integer bandIndex) { ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); // The source image. pb.set(transform, 0); pb.set(bandIndex, 1); pb.set(roi, 2); pb.set(nodata, 3); if (isNoDataNeeded()) { if (transform.hasGaps()) { // We must set the new NoData value setNoData(RangeFactory.create(transform.getDefaultValue(), transform.getDefaultValue())); } } image = JAI.create("GenericPiecewise", pb, getRenderingHints()); return this; } /** * Apply a rescale operation on the underlying image. */ public ImageWorker rescale(double [] scale, double [] offset) { ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); // The source image. pb.set(scale, 0); // The per-band constants to multiply by. pb.set(offset, 1); // The per-band offsets to be added. pb.set(roi, 2); // ROI pb.set(nodata, 3); // NoData range if (isNoDataNeeded()) { if (background != null && background.length > 0) { pb.set(background[0], 5); // destination No Data value } } image = JAI.create("Rescale", pb, getRenderingHints()); return this; } /** * Apply a rescale operation on the underlying image. */ public ImageWorker bandCombine(double[][] coeff) { // ParameterBlock definition ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); pb.set(coeff, 0); pb.set(roi, 1); pb.set(nodata, 2); if (isNoDataNeeded()) { if (background != null && background.length > 0) { pb.set(background[0], 3); } } image = JAI.create("BandCombine", pb, getRenderingHints()); return this; } /** * Apply a rangeLookup operation on the underlying image. */ public ImageWorker rangeLookup(Object rangeLookup) { // ParameterBlock definition ParameterBlock pb = new ParameterBlock(); pb.setSource(image, 0); pb.set(rangeLookup, 0); pb.set(roi, 2); if (roi != null) { if (background != null && background.length > 0) { pb.set(background[0], 1); // We must set the new NoData value setNoData(RangeFactory.create(background[0], background[0])); } } if(JAIExt.isJAIExtOperation("RLookup")){ image = JAI.create("RLookup", pb, getRenderingHints()); }else{ image = JAI.create("RangeLookup", pb, getRenderingHints()); } return this; } /** * Forces all NODATA pixels, as well as those outside of the ROI, to be transparent (expanding * the color model as needed in order to make it so). In case the image has no ROI or no nodata, * the method won't perform any change */ public ImageWorker prepareForRendering() { // anything to do? ROI roi = getROI(); if (roi == null) { Object roiCandidate = image.getProperty("roi"); if(roiCandidate instanceof ROI) { roi = (ROI) roiCandidate; } } if (nodata == null && roi == null) { return this; } RenderedImage image = getRenderedImage(); // figure out the suitable background value ColorModel cm = image.getColorModel(); double[] bgValues = null; PlanarImage[] alphaChannels = null; final int transparencyType = cm.getTransparency(); // in case of index color model we try to preserve it, so that output // formats that can work with it can enjoy its extra compactness if (cm instanceof IndexColorModel) { IndexColorModel icm = (IndexColorModel) cm; // try to find the index that matches the requested background color final int bgColorIndex; bgColorIndex = icm.getTransparentPixel(); // we did not find the background color, well we have to expand to RGB and then tell Mosaic to use the RGB(A) color as the // background if (bgColorIndex == -1) { // we need to expand the image to RGB forceComponentColorModel(); addAlphaChannel(); bgValues = new double[] { 0, 0, 0, 0}; } else { // we found the background color in the original image palette therefore we set its index as the bkg value. // The final Mosaic will use the IndexColorModel of this image anywa, therefore all we need to do is to force // the background to point to the right color in the palette bgValues = new double[] { bgColorIndex }; } // collect alpha channels if we have them in order to reuse them later on for mosaic operation if (cm.hasAlpha() && bgColorIndex == -1) { forceComponentColorModel(); final RenderedImage alpha = new ImageWorker(getRenderedImage()).retainLastBand().getRenderedImage(); alphaChannels = new PlanarImage[] { PlanarImage.wrapRenderedImage(alpha) }; } } else if(cm instanceof ComponentColorModel) { // convert to RGB if necessary ComponentColorModel ccm = (ComponentColorModel) cm; boolean hasAlpha = cm.hasAlpha(); // if we have a grayscale image see if we have to expand to RGB if (ccm.getNumColorComponents() == 1) { if ((ccm.getTransferType() == DataBuffer.TYPE_DOUBLE || ccm.getTransferType() == DataBuffer.TYPE_FLOAT || ccm.getTransferType() == DataBuffer.TYPE_UNDEFINED || !hasAlpha)) { // expand to RGB, this is not a case we can optimize final ImageWorker iw = new ImageWorker(image); if (hasAlpha) { final RenderedImage alpha = iw.retainLastBand().getRenderedImage(); // get first band final RenderedImage gray = new ImageWorker(image).retainFirstBand() .getRenderedImage(); image = new ImageWorker(gray).bandMerge(3).addBand(alpha, false) .forceComponentColorModel().forceColorSpaceRGB().getRenderedImage(); } else { image = iw.bandMerge(3).forceComponentColorModel().forceColorSpaceRGB() .getRenderedImage(); } } else { // has alpha channel, extract it final ImageWorker iw = new ImageWorker(image); final RenderedImage alpha = iw.retainLastBand().getRenderedImage(); alphaChannels = new PlanarImage[] { PlanarImage.wrapRenderedImage(alpha) }; bgValues = new double[] { 0, 0, 0, 0 }; } // get back the ColorModel cm = image.getColorModel(); ccm = (ComponentColorModel) cm; hasAlpha = cm.hasAlpha(); } if (bgValues == null) { if (hasAlpha) { // get alpha final ImageWorker iw = new ImageWorker(image); final RenderedImage alpha = iw.retainLastBand().getRenderedImage(); alphaChannels = new PlanarImage[] { PlanarImage.wrapRenderedImage(alpha) }; bgValues = new double[] { 0, 0, 0, 0 }; } else { image = new ImageWorker(image).addAlphaChannel().getRenderedImage(); // this will work fine for all situation where the color components are <= 3 // e.g., one band rasters with no colormap will have only one usually bgValues = new double[] { 0, 0, 0, 0 }; } } } // // If we need to add a collar use mosaic or if we need to blend/apply a bkg color ImageWorker iw = new ImageWorker(image); ROI[] rois = new ROI[] {roi}; // build the transparency thresholds double[][] thresholds = new double[][] { { ColorUtilities.getThreshold(image .getSampleModel().getDataType()) } }; // apply the mosaic iw.setBackground(bgValues); iw.mosaic(new RenderedImage[] { image }, alphaChannels != null && transparencyType==Transparency.TRANSLUCENT ? MosaicDescriptor.MOSAIC_TYPE_BLEND: MosaicDescriptor.MOSAIC_TYPE_OVERLAY, alphaChannels, rois, thresholds, null); this.image = iw.getRenderedImage(); return this; } /** * Adds an extra channel to the image, with a value of 255 (not public yet because it won't work with * all image types) * * @return */ private ImageWorker addAlphaChannel() { final ImageLayout tempLayout= new ImageLayout(image); tempLayout.unsetValid(ImageLayout.COLOR_MODEL_MASK).unsetValid(ImageLayout.SAMPLE_MODEL_MASK); RenderedImage alpha = ConstantDescriptor.create( Float.valueOf( image.getWidth()), Float.valueOf(image.getHeight()), new Byte[] { Byte.valueOf((byte) 255) }, new RenderingHints(JAI.KEY_IMAGE_LAYOUT,tempLayout)); addBand(alpha, false, true, null); 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); } } }