/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2001-2008, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.resources.image;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferDouble;
import java.awt.image.DataBufferFloat;
import java.awt.image.DataBufferInt;
import java.awt.image.DataBufferShort;
import java.awt.image.DataBufferUShort;
import java.awt.image.IndexColorModel;
import java.awt.image.MultiPixelPackedSampleModel;
import java.awt.image.RenderedImage;
import java.awt.image.WritableRaster;
import java.awt.image.WritableRenderedImage;
import java.io.IOException;
import java.lang.reflect.Method;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.spi.ImageInputStreamSpi;
import javax.imageio.spi.ImageReaderWriterSpi;
import javax.imageio.stream.ImageInputStream;
import javax.media.jai.BorderExtender;
import javax.media.jai.BorderExtenderCopy;
import javax.media.jai.BorderExtenderReflect;
import javax.media.jai.ImageLayout;
import javax.media.jai.Interpolation;
import javax.media.jai.JAI;
import javax.media.jai.OpImage;
import javax.media.jai.ParameterBlockJAI;
import javax.media.jai.PlanarImage;
import javax.media.jai.RenderedOp;
import org.geotools.geometry.GeneralEnvelope;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.image.ImageWorker;
import org.geotools.image.io.ImageIOExt;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import org.geotools.resources.Classes;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.i18n.Errors;
import org.geotools.util.Utilities;
import org.opengis.geometry.BoundingBox;
import org.opengis.metadata.extent.GeographicBoundingBox;
import com.sun.media.imageioimpl.common.PackageUtil;
import com.sun.media.jai.operator.ImageReadDescriptor;
import com.sun.media.jai.util.Rational;
/**
* A set of static methods working on images. Some of those methods are useful, but not
* really rigorous. This is why they do not appear in any "official" package, but instead
* in this private one.
*
* <strong>Do not rely on this API!</strong>
*
* It may change in incompatible way in any future version.
*
* @since 2.0
*
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux (IRD)
* @author Simone Giannecchini, GeoSolutions
*/
@SuppressWarnings("unchecked")
public final class ImageUtilities {
/**
* {@code true} if JAI media lib is available.
*/
private static final boolean mediaLibAvailable;
static {
// do we wrappers at hand?
boolean mediaLib = false;
Class mediaLibImage = null;
try {
mediaLibImage = Class.forName("com.sun.medialib.mlib.Image");
} catch (ClassNotFoundException e) {
}
mediaLib = (mediaLibImage != null);
// npw check if we either wanted to disable explicitly and if we installed the native libs
if(mediaLib){
try {
// explicit disable
mediaLib =
!Boolean.getBoolean("com.sun.media.jai.disableMediaLib");
//native libs installed
if(mediaLib)
{
final Class mImage=mediaLibImage;
mediaLib=AccessController.doPrivileged(new PrivilegedAction<Boolean>() {
public Boolean run() {
try {
//get the method
final Class params[] = {};
Method method= mImage.getDeclaredMethod("isAvailable", params);
//invoke
final Object paramsObj[] = {};
final Object o=mImage.newInstance();
return (Boolean) method.invoke(o, paramsObj);
} catch (Throwable e) {
return false;
}
}
});
}
} catch (Throwable e) {
// Because the property com.sun.media.jai.disableMediaLib isn't
// defined as public, the users shouldn't know it. In most of
// the cases, it isn't defined, and thus no access permission
// is granted to it in the policy file. When JAI is utilized in
// a security environment, AccessControlException will be thrown.
// In this case, we suppose that the users would like to use
// medialib accelaration. So, the medialib won't be disabled.
// The fix of 4531501
mediaLib=false;
}
}
mediaLibAvailable=mediaLib;
}
/**
* {@link RenderingHints} used to prevent {@link JAI} operations from expanding
* {@link IndexColorModel}s.
*/
public final static RenderingHints DONT_REPLACE_INDEX_COLOR_MODEL = new RenderingHints(
JAI.KEY_REPLACE_INDEX_COLOR_MODEL, Boolean.FALSE);
/**
* {@link RenderingHints} used to force {@link JAI} operations to expand
* {@link IndexColorModel}s.
*/
public final static RenderingHints REPLACE_INDEX_COLOR_MODEL = new RenderingHints(
JAI.KEY_REPLACE_INDEX_COLOR_MODEL, Boolean.TRUE);
/**
* {@link RenderingHints} for requesting Nearest Neighbor intepolation.
*/
public static final RenderingHints NN_INTERPOLATION_HINT = new RenderingHints(
JAI.KEY_INTERPOLATION, Interpolation.getInstance(Interpolation.INTERP_NEAREST));
/**
* {@link RenderingHints} for avoiding caching of {@link JAI} {@link RenderedOp}s.
*/
public static final RenderingHints NOCACHE_HINT = new RenderingHints(
JAI.KEY_TILE_CACHE, null);
/**
* Cached instance of a {@link RenderingHints} for controlling border extension on
* {@link JAI} operations. It contains an instance of a {@link BorderExtenderCopy}.
*/
public final static RenderingHints EXTEND_BORDER_BY_COPYING = new RenderingHints(
JAI.KEY_BORDER_EXTENDER, BorderExtender
.createInstance(BorderExtender.BORDER_COPY));
/**
* Cached instance of a {@link RenderingHints} for controlling border extension on
* {@link JAI} operations. It contains an instance of a {@link BorderExtenderReflect}.
*/
public final static RenderingHints EXTEND_BORDER_BY_REFLECT = new RenderingHints(
JAI.KEY_BORDER_EXTENDER, BorderExtender
.createInstance(BorderExtender.BORDER_REFLECT));
/**
* The default tile size. This default tile size can be
* overridden with a call to {@link JAI#setDefaultTileSize}.
*/
private static final Dimension GEOTOOLS_DEFAULT_TILE_SIZE = new Dimension(512,512);
/**
* The minimum tile size.
*/
private static final int GEOTOOLS_MIN_TILE_SIZE = 256;
/**
* Maximum tile width or height before to consider a tile as a stripe. It tile width or height
* are smaller or equals than this size, then the image will be retiled. That is done because
* there are many formats that use stripes as an alternative to tiles, an example is tiff. A
* stripe can be a performance black hole, users can have stripes as large as 20000 columns x 8
* rows. If we just want to see a chunk of 512x512, this is a lot of uneeded data to load.
*/
private static final int STRIPE_SIZE = 64;
/**
* List of valid names. Note: the "Optimal" type is not
* implemented because currently not provided by JAI.
*/
public static final String[] INTERPOLATION_NAMES = {
"Nearest", // JAI name
"NearestNeighbor", // OpenGIS name
"Bilinear",
"Bicubic",
"Bicubic2" // Not in OpenGIS specification.
};
/**
* Interpolation types (provided by Java Advanced Imaging) for {@link #INTERPOLATION_NAMES}.
*/
public static final int[] INTERPOLATION_TYPES= {
Interpolation.INTERP_NEAREST,
Interpolation.INTERP_NEAREST,
Interpolation.INTERP_BILINEAR,
Interpolation.INTERP_BICUBIC,
Interpolation.INTERP_BICUBIC_2
};
public final static BorderExtender DEFAULT_BORDER_EXTENDER = BorderExtender.createInstance(BorderExtender.BORDER_COPY);
public final static RenderingHints BORDER_EXTENDER_HINTS = new RenderingHints(JAI.KEY_BORDER_EXTENDER, DEFAULT_BORDER_EXTENDER);
public static final String DIRECT_KAKADU_PLUGIN = "it.geosolutions.imageio.plugins.jp2k.JP2KKakaduImageReader";
// FORMULAE FOR FORWARD MAP are derived as follows
// Nearest
// Minimum:
// srcMin = floor ((dstMin + 0.5 - trans) / scale)
// srcMin <= (dstMin + 0.5 - trans) / scale < srcMin + 1
// srcMin*scale <= dstMin + 0.5 - trans < (srcMin + 1)*scale
// srcMin*scale - 0.5 + trans
// <= dstMin < (srcMin + 1)*scale - 0.5 + trans
// Let A = srcMin*scale - 0.5 + trans,
// Let B = (srcMin + 1)*scale - 0.5 + trans
//
// dstMin = ceil(A)
//
// Maximum:
// Note that srcMax is defined to be srcMin + dimension - 1
// srcMax = floor ((dstMax + 0.5 - trans) / scale)
// srcMax <= (dstMax + 0.5 - trans) / scale < srcMax + 1
// srcMax*scale <= dstMax + 0.5 - trans < (srcMax + 1)*scale
// srcMax*scale - 0.5 + trans
// <= dstMax < (srcMax+1) * scale - 0.5 + trans
// Let float A = (srcMax + 1) * scale - 0.5 + trans
//
// dstMax = floor(A), if floor(A) < A, else
// dstMax = floor(A) - 1
// OR dstMax = ceil(A - 1)
//
// Other interpolations
//
// First the source should be shrunk by the padding that is
// required for the particular interpolation. Then the
// shrunk source should be forward mapped as follows:
//
// Minimum:
// srcMin = floor (((dstMin + 0.5 - trans)/scale) - 0.5)
// srcMin <= ((dstMin + 0.5 - trans)/scale) - 0.5 < srcMin+1
// (srcMin+0.5)*scale <= dstMin+0.5-trans <
// (srcMin+1.5)*scale
// (srcMin+0.5)*scale - 0.5 + trans
// <= dstMin < (srcMin+1.5)*scale - 0.5 + trans
// Let A = (srcMin+0.5)*scale - 0.5 + trans,
// Let B = (srcMin+1.5)*scale - 0.5 + trans
//
// dstMin = ceil(A)
//
// Maximum:
// srcMax is defined as srcMin + dimension - 1
// srcMax = floor (((dstMax + 0.5 - trans) / scale) - 0.5)
// srcMax <= ((dstMax + 0.5 - trans)/scale) - 0.5 < srcMax+1
// (srcMax+0.5)*scale <= dstMax + 0.5 - trans <
// (srcMax+1.5)*scale
// (srcMax+0.5)*scale - 0.5 + trans
// <= dstMax < (srcMax+1.5)*scale - 0.5 + trans
// Let float A = (srcMax+1.5)*scale - 0.5 + trans
//
// dstMax = floor(A), if floor(A) < A, else
// dstMax = floor(A) - 1
// OR dstMax = ceil(A - 1)
//
public static final float RATIONAL_TOLERANCE = 0.000001F;
/**
* Do not allow creation of instances of this class.
*/
private ImageUtilities() {
}
/**
* Suggests an {@link ImageLayout} for the specified image. All parameters are initially set
* equal to those of the given {@link RenderedImage}, and then the {@linkplain #toTileSize
* tile size is updated according the image size}. This method never returns {@code null}.
*/
public static ImageLayout getImageLayout(final RenderedImage image) {
return getImageLayout(image, true);
}
/**
* Returns an {@link ImageLayout} for the specified image. If {@code initToImage} is
* {@code true}, then all parameters are initially set equal to those of the given
* {@link RenderedImage} and the returned layout is never {@code null} (except if
* the image is null).
*/
private static ImageLayout getImageLayout(final RenderedImage image, final boolean initToImage) {
if (image == null) {
return null;
}
ImageLayout layout = initToImage ? new ImageLayout(image) : null;
if ((image.getNumXTiles()==1 || image.getTileWidth () <= STRIPE_SIZE) &&
(image.getNumYTiles()==1 || image.getTileHeight() <= STRIPE_SIZE))
{
// If the image was already tiled, reuse the same tile size.
// Otherwise, compute default tile size. If a default tile
// size can't be computed, it will be left unset.
if (layout != null) {
layout = layout.unsetTileLayout();
}
Dimension defaultSize = JAI.getDefaultTileSize();
if (defaultSize == null) {
defaultSize = GEOTOOLS_DEFAULT_TILE_SIZE;
}
int s;
if ((s=toTileSize(image.getWidth(), defaultSize.width)) != 0) {
if (layout == null) {
layout = new ImageLayout();
}
layout = layout.setTileWidth(s);
layout.setTileGridXOffset(image.getMinX());
}
if ((s=toTileSize(image.getHeight(), defaultSize.height)) != 0) {
if (layout == null) {
layout = new ImageLayout();
}
layout = layout.setTileHeight(s);
layout.setTileGridYOffset(image.getMinY());
}
}
return layout;
}
/**
* Suggests a set of {@link RenderingHints} for the specified image.
* The rendering hints may include the following parameters:
*
* <ul>
* <li>{@link JAI#KEY_IMAGE_LAYOUT} with a proposed tile size.</li>
* </ul>
*
* This method may returns {@code null} if no rendering hints is proposed.
*/
public static RenderingHints getRenderingHints(final RenderedImage image) {
final ImageLayout layout = getImageLayout(image, false);
return (layout != null) ? new RenderingHints(JAI.KEY_IMAGE_LAYOUT, layout) : null;
}
/**
* Suggests a tile size for the specified image size. On input, {@code size} is the image's
* size. On output, it is the tile size. This method write the result directly in the supplied
* object and returns {@code size} for convenience.
* <p>
* This method it aimed to computing a tile size such that the tile grid would have overlapped
* the image bound in order to avoid having tiles crossing the image bounds and being therefore
* partially empty. This method will never returns a tile size smaller than
* {@value ImageUtilities#GEOTOOLS_MIN_TILE_SIZE}. If this method can't suggest a size,
* then it left the corresponding {@code size} field ({@link Dimension#width width} or
* {@link Dimension#height height}) unchanged.
* <p>
* The {@link Dimension#width width} and {@link Dimension#height height} fields are processed
* independently in the same way. The following discussion use the {@code width} field as an
* example.
* <p>
* This method inspects different tile sizes close to the {@linkplain JAI#getDefaultTileSize()
* default tile size}. Lets {@code width} be the default tile width. Values are tried in the
* following order: {@code width}, {@code width+1}, {@code width-1}, {@code width+2},
* {@code width-2}, {@code width+3}, {@code width-3}, <cite>etc.</cite> until one of the
* following happen:
* <p>
* <ul>
* <li>A suitable tile size is found. More specifically, a size is found which is a dividor
* of the specified image size, and is the closest one of the default tile size. The
* {@link Dimension} field ({@code width} or {@code height}) is set to this value.</li>
*
* <li>An arbitrary limit (both a minimum and a maximum tile size) is reached. In this case,
* this method <strong>may</strong> set the {@link Dimension} field to a value that
* maximize the remainder of <var>image size</var> / <var>tile size</var> (in other
* words, the size that left as few empty pixels as possible).</li>
* </ul>
*/
public static Dimension toTileSize(final Dimension size) {
Dimension defaultSize = JAI.getDefaultTileSize();
if (defaultSize == null) {
defaultSize = GEOTOOLS_DEFAULT_TILE_SIZE;
}
int s;
if ((s=toTileSize(size.width, defaultSize.width )) != 0) size.width = s;
if ((s=toTileSize(size.height, defaultSize.height)) != 0) size.height = s;
return size;
}
/**
* Suggests a tile size close to {@code tileSize} for the specified {@code imageSize}.
* This method it aimed to computing a tile size such that the tile grid would have
* overlapped the image bound in order to avoid having tiles crossing the image bounds
* and being therefore partially empty. This method will never returns a tile size smaller
* than {@value #GEOTOOLS_MIN_TILE_SIZE}. If this method can't suggest a size, then it returns 0.
*
* @param imageSize The image size.
* @param tileSize The preferred tile size, which is often {@value #GEOTOOLS_DEFAULT_TILE_SIZE}.
*/
private static int toTileSize(final int imageSize, final int tileSize) {
final int MAX_TILE_SIZE = Math.min(tileSize*2, imageSize);
final int stop = Math.max(tileSize-GEOTOOLS_MIN_TILE_SIZE, MAX_TILE_SIZE-tileSize);
int sopt = 0; // An "optimal" tile size, to be used if no exact dividor is found.
int rmax = 0; // The remainder of 'imageSize / sopt'. We will try to maximize this value.
/*
* Inspects all tile sizes in the range [GEOTOOLS_MIN_TILE_SIZE .. MAX_TIME_SIZE]. We will begin
* with a tile size equals to the specified 'tileSize'. Next we will try tile sizes of
* 'tileSize+1', 'tileSize-1', 'tileSize+2', 'tileSize-2', 'tileSize+3', 'tileSize-3',
* etc. until a tile size if found suitable.
*
* More generally, the loop below tests the 'tileSize+i' and 'tileSize-i' values. The
* 'stop' constant was computed assuming that MIN_TIME_SIZE < tileSize < MAX_TILE_SIZE.
* If a tile size is found which is a dividor of the image size, than that tile size (the
* closest one to 'tileSize') is returned. Otherwise, the loop continue until all values
* in the range [GEOTOOLS_MIN_TILE_SIZE .. MAX_TIME_SIZE] were tested. In this process, we remind
* the tile size that gave the greatest reminder (rmax). In other words, this is the tile
* size with the smallest amount of empty pixels.
*/
for (int i=0; i<=stop; i++) {
int s;
if ((s = tileSize+i) <= MAX_TILE_SIZE) {
final int r = imageSize % s;
if (r == 0) {
// Found a size >= to 'tileSize' which is a dividor of image size.
return s;
}
if (r > rmax) {
rmax = r;
sopt = s;
}
}
if ((s = tileSize-i) >= GEOTOOLS_MIN_TILE_SIZE) {
final int r = imageSize % s;
if (r == 0) {
// Found a size <= to 'tileSize' which is a dividor of image size.
return s;
}
if (r > rmax) {
rmax = r;
sopt = s;
}
}
}
/*
* No dividor were found in the range [GEOTOOLS_MIN_TILE_SIZE .. MAX_TIME_SIZE]. At this point
* 'sopt' is an "optimal" tile size (the one that left as few empty pixel as possible),
* and 'rmax' is the amount of non-empty pixels using this tile size. We will use this
* "optimal" tile size only if it fill at least 75% of the tile. Otherwise, we arbitrarily
* consider that it doesn't worth to use a "non-standard" tile size. The purpose of this
* arbitrary test is again to avoid too many small tiles (assuming that
*/
return (rmax >= tileSize - tileSize/4) ? sopt : 0;
}
/**
* Computes a new {@link ImageLayout} which is the intersection of the specified
* {@code ImageLayout} and all {@code RenderedImage}s in the supplied list. If the
* {@link ImageLayout#getMinX minX}, {@link ImageLayout#getMinY minY},
* {@link ImageLayout#getWidth width} and {@link ImageLayout#getHeight height}
* properties are not defined in the {@code layout}, then they will be inherited
* from the <strong>first</strong> source for consistency with {@link OpImage} constructor.
*
* @param layout The original layout. This object will not be modified.
* @param sources The list of sources {@link RenderedImage}.
* @return A new {@code ImageLayout}, or the original {@code layout} if no change was needed.
*/
public static ImageLayout createIntersection(final ImageLayout layout, final List<RenderedImage> sources) {
ImageLayout result = layout;
if (result == null) {
result = new ImageLayout();
}
final int n = sources.size();
if (n != 0) {
// If layout is not set, OpImage uses the layout of the *first*
// source image according OpImage constructor javadoc.
RenderedImage source = (RenderedImage) sources.get(0);
int minXL = result.getMinX (source);
int minYL = result.getMinY (source);
int maxXL = result.getWidth (source) + minXL;
int maxYL = result.getHeight(source) + minYL;
for (int i=0; i<n; i++) {
source = (RenderedImage) sources.get(i);
final int minX = source.getMinX ();
final int minY = source.getMinY ();
final int maxX = source.getWidth () + minX;
final int maxY = source.getHeight() + minY;
int mask = 0;
if (minXL < minX) mask |= (1|4); // set minX and width
if (minYL < minY) mask |= (2|8); // set minY and height
if (maxXL > maxX) mask |= (4); // Set width
if (maxYL > maxY) mask |= (8); // Set height
if (mask != 0) {
if (layout == result) {
result = (ImageLayout) layout.clone();
}
if ((mask & 1) != 0) result.setMinX (minXL=minX);
if ((mask & 2) != 0) result.setMinY (minYL=minY);
if ((mask & 4) != 0) result.setWidth ((maxXL=maxX) - minXL);
if ((mask & 8) != 0) result.setHeight((maxYL=maxY) - minYL);
}
}
// If the bounds changed, adjust the tile size.
if (result != layout) {
source = (RenderedImage) sources.get(0);
if (result.isValid(ImageLayout.TILE_WIDTH_MASK)) {
final int oldSize = result.getTileWidth(source);
final int newSize = toTileSize(result.getWidth(source), oldSize);
if (oldSize != newSize) {
result.setTileWidth(newSize);
}
}
if (result.isValid(ImageLayout.TILE_HEIGHT_MASK)) {
final int oldSize = result.getTileHeight(source);
final int newSize = toTileSize(result.getHeight(source), oldSize);
if (oldSize != newSize) {
result.setTileHeight(newSize);
}
}
}
}
return result;
}
/**
* Casts the specified object to an {@link Interpolation object}.
*
* @param type The interpolation type as an {@link Interpolation} or a {@link CharSequence}
* object.
* @return The interpolation object for the specified type.
* @throws IllegalArgumentException if the specified interpolation type is not a know one.
*/
public static Interpolation toInterpolation(final Object type) throws IllegalArgumentException {
if (type instanceof Interpolation) {
return (Interpolation) type;
} else if (type instanceof CharSequence) {
final String name = type.toString();
final int length= INTERPOLATION_NAMES.length;
for (int i=0; i<length; i++) {
if (INTERPOLATION_NAMES[i].equalsIgnoreCase(name)) {
return Interpolation.getInstance(INTERPOLATION_TYPES[i]);
}
}
}
throw new IllegalArgumentException(Errors.format(ErrorKeys.UNKNOW_INTERPOLATION_$1, type));
}
/**
* Returns the interpolation name for the specified interpolation object.
* This method tries to infer the name from the object's class name.
*
* @param Interpolation The interpolation object, or {@code null} for "nearest"
* (which is an other way to say "no interpolation").
*/
public static String getInterpolationName(Interpolation interp) {
if (interp == null) {
interp = Interpolation.getInstance(Interpolation.INTERP_NEAREST);
}
final String prefix = "Interpolation";
for (Class<?> classe = interp.getClass(); classe!=null; classe=classe.getSuperclass()) {
String name = Classes.getShortName(classe);
int index = name.lastIndexOf(prefix);
if (index >= 0) {
return name.substring(index + prefix.length());
}
}
return Classes.getShortClassName(interp);
}
/**
* Allows or disallows native acceleration for the specified image format. By default, the
* image I/O extension for JAI provides native acceleration for PNG and JPEG. Unfortunatly,
* those native codec has bug in their 1.0 version. Invoking this method will force the use
* of standard codec provided in J2SE 1.4.
* <p>
* <strong>Implementation note:</strong> the current implementation assume that JAI codec
* class name start with "CLib". It work for Sun's 1.0 implementation, but may change in
* future versions. If this method doesn't recognize the class name, it does nothing.
*
* @param format The format name (e.g. "png").
* @param category {@code ImageReaderSpi.class} to set the reader, or
* {@code ImageWriterSpi.class} to set the writer.
* @param allowed {@code false} to disallow native acceleration.
* @deprecated Use {@link ImageIOExt#allowNativeCodec(String, Class, boolean)} instead
*/
public static synchronized <T extends ImageReaderWriterSpi> void allowNativeCodec(
final String format, final Class<T> category, final boolean allowed)
{
ImageIOExt.allowNativeCodec(format, category, allowed);
}
/**
* Tiles the specified image.
*
* @todo Usually, the tiling doesn't need to be performed as a separated operation. The
* {@link ImageLayout} hint with tile information can be provided to most JAI operators.
* The {@link #getRenderingHints} method provides such tiling information only if the
* image was not already tiled, so it should not be a cause of tile size mismatch in an
* operation chain. The mean usage for a separated "tile" operation is to tile an image
* before to save it on disk in some format supporting tiling.
*
* @throws IOException If an I/O operation were required (in order to check if the image
* were tiled on disk) and failed.
*
* @since 2.3
*/
public static RenderedOp tileImage(final RenderedOp image) throws IOException {
// /////////////////////////////////////////////////////////////////////
//
// initialization
//
// /////////////////////////////////////////////////////////////////////
final int width = image.getWidth();
final int height = image.getHeight();
final int tileHeight = image.getTileHeight();
final int tileWidth = image.getTileWidth();
boolean needToTile = false;
// /////////////////////////////////////////////////////////////////////
//
// checking if the image comes directly from an image read operation
//
// /////////////////////////////////////////////////////////////////////
// getting the reader
final Object o = image.getProperty(ImageReadDescriptor.PROPERTY_NAME_IMAGE_READER);
if (o instanceof ImageReader) {
final ImageReader reader = (ImageReader) o;
if (!reader.isImageTiled(0)) {
needToTile = true;
}
}
// /////////////////////////////////////////////////////////////////////
//
// If the original image has tileW==W &&tileH==H it is untiled.
//
// /////////////////////////////////////////////////////////////////////
if (!needToTile && tileWidth == width && tileHeight <= height) {
needToTile = true;
}
// /////////////////////////////////////////////////////////////////////
//
// tiling central.
//
// /////////////////////////////////////////////////////////////////////
if (needToTile) {
// tiling the original image by providing a suitable layout
final ImageLayout layout = getImageLayout(image);
layout.unsetValid(ImageLayout.COLOR_MODEL_MASK | ImageLayout.SAMPLE_MODEL_MASK);
// changing parameters related to the tiling
final RenderingHints hints = new RenderingHints(JAI.KEY_IMAGE_LAYOUT, layout);
// reading the image
final ParameterBlockJAI pbjFormat = new ParameterBlockJAI("Format");
pbjFormat.addSource(image);
pbjFormat.setParameter("dataType", image.getSampleModel().getDataType());
return JAI.create("Format", pbjFormat, hints);
}
return image;
}
/**
* Sets every samples in the given image to the given value. This method is typically used
* for clearing an image content.
*
* @param image The image to fill.
* @param value The value to to given to every samples.
*/
public static void fill(final WritableRenderedImage image, final Number value) {
int y = image.getMinTileY();
for (int ny = image.getNumYTiles(); --ny >= 0;) {
int x = image.getMinTileX();
for (int nx = image.getNumXTiles(); --nx >= 0;) {
final WritableRaster raster = image.getWritableTile(x, y);
try {
fill(raster.getDataBuffer(), value);
} finally {
image.releaseWritableTile(x, y);
}
}
}
}
/**
* Sets the content of all banks in the given data buffer to the specified value. We do not
* allow setting of different value for invidivual bank because the data buffer "banks" do
* not necessarly match the image "bands".
*
* @param buffer The data buffer to fill.
* @param value The values to be given to every elements in the data buffer.
*/
private static void fill(final DataBuffer buffer, final Number value) {
final int[] offsets = buffer.getOffsets();
final int size = buffer.getSize();
if (buffer instanceof DataBufferByte) {
final DataBufferByte data = (DataBufferByte) buffer;
final byte n = value.byteValue();
for (int i=0; i<offsets.length; i++) {
final int offset = offsets[i];
Arrays.fill(data.getData(i), offset, offset + size, n);
}
} else if (buffer instanceof DataBufferShort) {
final DataBufferShort data = (DataBufferShort) buffer;
final short n = value.shortValue();
for (int i=0; i<offsets.length; i++) {
final int offset = offsets[i];
Arrays.fill(data.getData(i), offset, offset + size, n);
}
} else if (buffer instanceof DataBufferUShort) {
final DataBufferUShort data = (DataBufferUShort) buffer;
final short n = value.shortValue();
for (int i=0; i<offsets.length; i++) {
final int offset = offsets[i];
Arrays.fill(data.getData(i), offset, offset + size, n);
}
} else if (buffer instanceof DataBufferInt) {
final DataBufferInt data = (DataBufferInt) buffer;
final int n = value.intValue();
for (int i=0; i<offsets.length; i++) {
final int offset = offsets[i];
Arrays.fill(data.getData(i), offset, offset + size, n);
}
} else if (buffer instanceof DataBufferFloat) {
final DataBufferFloat data = (DataBufferFloat) buffer;
final float n = value.floatValue();
for (int i=0; i<offsets.length; i++) {
final int offset = offsets[i];
Arrays.fill(data.getData(i), offset, offset + size, n);
}
} else if (buffer instanceof DataBufferDouble) {
final DataBufferDouble data = (DataBufferDouble) buffer;
final double n = value.doubleValue();
for (int i=0; i<offsets.length; i++) {
final int offset = offsets[i];
Arrays.fill(data.getData(i), offset, offset + size, n);
}
} else {
throw new IllegalArgumentException(Errors.format(ErrorKeys.UNSUPPORTED_DATA_TYPE));
}
}
/**
* Tells me whether or not the native libraries for JAI are active or not.
*
* @return <code>false</code> in case the JAI native libs are not in the path, <code>true</code> otherwise.
*/
public static boolean isMediaLibAvailable() {
return mediaLibAvailable;
}
/**
* Tells me whether or not the native libraries for JAI/ImageIO are active or not.
*
* @return <code>false</code> in case the JAI/ImageIO native libs are not in the path, <code>true</code> otherwise.
* @deprecated Use {@link ImageIOExt#isCLibAvailable()} instead
*/
public static boolean isCLibAvailable() {
return PackageUtil.isCodecLibAvailable();
}
/**
* Get a proper {@link ImageInputStreamSpi} instance for the provided {@link Object} input without
* trying to create an {@link ImageInputStream}.
*
* @see #getImageInputStreamSPI(Object, boolean)
* @deprecated Use {@link ImageIOExt#getImageInputStreamSPI(Object, boolean)} instead
*/
public final static ImageInputStreamSpi getImageInputStreamSPI(final Object input) {
return ImageIOExt.getImageInputStreamSPI(input);
}
/**
* Get a proper {@link ImageInputStreamSpi} instance for the provided {@link Object} input.
*
* @param input the input object for which we need to find a proper {@link ImageInputStreamSpi} instance
* @param streamCreationCheck if <code>true</code>, when a proper {@link ImageInputStreamSpi} have been found
* for the provided input, use it to try creating an {@link ImageInputStream} on top of the input.
*
* @return an {@link ImageInputStreamSpi} instance.
* @deprecated Use {@link ImageIOExt#getImageInputStreamSPI(Object, boolean)} instead
*/
public final static ImageInputStreamSpi getImageInputStreamSPI(final Object input, final boolean streamCreationCheck) {
return ImageIOExt.getImageInputStreamSPI(input, streamCreationCheck);
}
/**
* Dispose an image with all its ancestors.
*
* @param pi the {@link PlanarImage} to dispose.
*/
public static void disposePlanarImageChain(PlanarImage pi) {
Utilities.ensureNonNull("PlanarImage", pi);
disposePlanarImageChain(pi, new HashSet<PlanarImage>());
}
private static void disposePlanarImageChain(PlanarImage pi, HashSet<PlanarImage> visited) {
Vector sinks = pi.getSinks();
// check all the sinks (the image might be in the middle of a chain)
if(sinks != null) {
for (Object sink: sinks) {
if(sink instanceof PlanarImage && !visited.contains(sink))
disposePlanarImageChain((PlanarImage) sink, visited);
else if(sink instanceof BufferedImage) {
((BufferedImage) sink).flush();
}
}
}
// dispose the image itself
pi.dispose();
visited.add(pi);
// check the image sources
Vector sources = pi.getSources();
if(sources != null) {
for (Object child : sources) {
if(child instanceof PlanarImage && !visited.contains(child))
disposePlanarImageChain((PlanarImage) child, visited);
else if(child instanceof BufferedImage) {
((BufferedImage) child).flush();
}
}
}
// ImageRead might also hold onto a image input stream that we have to close
if(pi instanceof RenderedOp) {
RenderedOp op = (RenderedOp) pi;
for(Object param : op.getParameterBlock().getParameters()) {
if(param instanceof ImageInputStream) {
ImageInputStream iis = (ImageInputStream) param;
try {
iis.close();
} catch(IOException e) {
// fine, we tried
}
}
}
}
}
/**
* Relies on the {@link ImageWorker} to mask a certain color from an image.
*
* @param transparentColor the {@link Color} to make transparent
* @param image the {@link RenderedImage} to work on
* @return a new {@link RenderedImage} where the provided {@link Color} has turned into transparent.
*
* @throws IllegalStateException
*/
public static RenderedImage maskColor(final Color transparentColor,
final RenderedImage image) throws IllegalStateException {
Utilities.ensureNonNull("image", image);
if(transparentColor==null){
return image;
}
final ImageWorker w = new ImageWorker(image);
if (image.getSampleModel() instanceof MultiPixelPackedSampleModel){
w.forceComponentColorModel();
}
return w.makeColorTransparent(transparentColor).getRenderedImage();
}
static public ImageReadParam cloneImageReadParam(ImageReadParam param) {
// The ImageReadParam passed in is non-null. As the
// ImageReadParam class is not Cloneable, if the param
// class is simply ImageReadParam, then create a new
// ImageReadParam instance and set all its fields
// which were set in param. This will eliminate problems
// with concurrent modification of param for the cases
// in which there is not a special ImageReadparam used.
// Create a new ImageReadParam instance.
ImageReadParam newParam = new ImageReadParam();
// Set all fields which need to be set.
// IIOParamController field.
if (param.hasController()) {
newParam.setController(param.getController());
}
// Destination fields.
newParam.setDestination(param.getDestination());
if (param.getDestinationType() != null) {
// Set the destination type only if non-null as the
// setDestinationType() clears the destination field.
newParam.setDestinationType(param.getDestinationType());
}
newParam.setDestinationBands(param.getDestinationBands());
newParam.setDestinationOffset(param.getDestinationOffset());
// Source fields.
newParam.setSourceBands(param.getSourceBands());
newParam.setSourceRegion(param.getSourceRegion());
if (param.getSourceMaxProgressivePass() != Integer.MAX_VALUE) {
newParam.setSourceProgressivePasses(param
.getSourceMinProgressivePass(), param
.getSourceNumProgressivePasses());
}
if (param.canSetSourceRenderSize()) {
newParam.setSourceRenderSize(param.getSourceRenderSize());
}
newParam.setSourceSubsampling(param.getSourceXSubsampling(), param
.getSourceYSubsampling(), param.getSubsamplingXOffset(), param
.getSubsamplingYOffset());
// Replace the local variable with the new ImageReadParam.
return newParam;
}
public static Rectangle2D layoutHelper(RenderedImage source,
float scaleX,
float scaleY,
float transX,
float transY,
Interpolation interp) {
// Represent the scale factors as Rational numbers.
// Since a value of 1.2 is represented as 1.200001 which
// throws the forward/backward mapping in certain situations.
// Convert the scale and translation factors to Rational numbers
Rational scaleXRational = Rational.approximate(scaleX,RATIONAL_TOLERANCE);
Rational scaleYRational = Rational.approximate(scaleY,RATIONAL_TOLERANCE);
long scaleXRationalNum = (long) scaleXRational.num;
long scaleXRationalDenom = (long) scaleXRational.denom;
long scaleYRationalNum = (long) scaleYRational.num;
long scaleYRationalDenom = (long) scaleYRational.denom;
Rational transXRational = Rational.approximate(transX,RATIONAL_TOLERANCE);
Rational transYRational = Rational.approximate(transY,RATIONAL_TOLERANCE);
long transXRationalNum = (long) transXRational.num;
long transXRationalDenom = (long) transXRational.denom;
long transYRationalNum = (long) transYRational.num;
long transYRationalDenom = (long) transYRational.denom;
int x0 = source.getMinX();
int y0 = source.getMinY();
int w = source.getWidth();
int h = source.getHeight();
// Variables to store the calculated destination upper left coordinate
long dx0Num, dx0Denom, dy0Num, dy0Denom;
// Variables to store the calculated destination bottom right
// coordinate
long dx1Num, dx1Denom, dy1Num, dy1Denom;
// Start calculations for destination
dx0Num = x0;
dx0Denom = 1;
dy0Num = y0;
dy0Denom = 1;
// Formula requires srcMaxX + 1 = (x0 + w - 1) + 1 = x0 + w
dx1Num = x0 + w;
dx1Denom = 1;
// Formula requires srcMaxY + 1 = (y0 + h - 1) + 1 = y0 + h
dy1Num = y0 + h;
dy1Denom = 1;
dx0Num *= scaleXRationalNum;
dx0Denom *= scaleXRationalDenom;
dy0Num *= scaleYRationalNum;
dy0Denom *= scaleYRationalDenom;
dx1Num *= scaleXRationalNum;
dx1Denom *= scaleXRationalDenom;
dy1Num *= scaleYRationalNum;
dy1Denom *= scaleYRationalDenom;
// Equivalent to subtracting 0.5
dx0Num = 2 * dx0Num - dx0Denom;
dx0Denom *= 2;
dy0Num = 2 * dy0Num - dy0Denom;
dy0Denom *= 2;
// Equivalent to subtracting 1.5
dx1Num = 2 * dx1Num - 3 * dx1Denom;
dx1Denom *= 2;
dy1Num = 2 * dy1Num - 3 * dy1Denom;
dy1Denom *= 2;
// Adding translation factors
// Equivalent to float dx0 += transX
dx0Num = dx0Num * transXRationalDenom + transXRationalNum * dx0Denom;
dx0Denom *= transXRationalDenom;
// Equivalent to float dy0 += transY
dy0Num = dy0Num * transYRationalDenom + transYRationalNum * dy0Denom;
dy0Denom *= transYRationalDenom;
// Equivalent to float dx1 += transX
dx1Num = dx1Num * transXRationalDenom + transXRationalNum * dx1Denom;
dx1Denom *= transXRationalDenom;
// Equivalent to float dy1 += transY
dy1Num = dy1Num * transYRationalDenom + transYRationalNum * dy1Denom;
dy1Denom *= transYRationalDenom;
// Get the integral coordinates
int l_x0, l_y0, l_x1, l_y1;
l_x0 = Rational.ceil(dx0Num, dx0Denom);
l_y0 = Rational.ceil(dy0Num, dy0Denom);
l_x1 = Rational.ceil(dx1Num, dx1Denom);
l_y1 = Rational.ceil(dy1Num, dy1Denom);
// Set the top left coordinate of the destination
final Rectangle2D retValue= new Rectangle2D.Double();
retValue.setFrame(l_x0, l_y0, l_x1 - l_x0 + 1, l_y1 - l_y0 + 1);
return retValue;
}
/**
* Look for an {@link ImageReader} instance that is able to read the
* provided {@link ImageInputStream}, which must be non null.
*
* <p>
* In case no reader is found, <code>null</code> is returned.
*
* @param inStream
* an instance of {@link ImageInputStream} for which we need to
* find a suitable {@link ImageReader}.
* @return a suitable instance of {@link ImageReader} or <code>null</code>
* if one cannot be found.
* @deprecated Use {@link ImageIOExt#getImageioReader(ImageInputStream)} instead
*/
static public ImageReader getImageioReader(final ImageInputStream inStream) {
return ImageIOExt.getImageioReader(inStream);
}
/**
* Builds a {@link ReferencedEnvelope} from a {@link GeographicBoundingBox}.
* This is useful in order to have an implementation of {@link BoundingBox}
* from a {@link GeographicBoundingBox} which strangely does implement
* {@link GeographicBoundingBox}.
*
* @param geographicBBox
* the {@link GeographicBoundingBox} to convert.
* @return an instance of {@link ReferencedEnvelope}.
*/
static public ReferencedEnvelope getReferencedEnvelopeFromGeographicBoundingBox(
final GeographicBoundingBox geographicBBox) {
Utilities.ensureNonNull("GeographicBoundingBox", geographicBBox);
return new ReferencedEnvelope(geographicBBox.getEastBoundLongitude(),
geographicBBox.getWestBoundLongitude(), geographicBBox
.getSouthBoundLatitude(), geographicBBox
.getNorthBoundLatitude(), DefaultGeographicCRS.WGS84);
}
/**
* Builds a {@link ReferencedEnvelope} in WGS84 from a {@link GeneralEnvelope}.
*
* @param coverageEnvelope
* the {@link GeneralEnvelope} to convert.
* @return an instance of {@link ReferencedEnvelope} in WGS84 or <code>null</code> in case a problem during the conversion occurs.
*/
static public ReferencedEnvelope getWGS84ReferencedEnvelope(
final GeneralEnvelope coverageEnvelope) {
Utilities.ensureNonNull("coverageEnvelope", coverageEnvelope);
final ReferencedEnvelope refEnv= new ReferencedEnvelope(coverageEnvelope);
try{
return refEnv.transform(DefaultGeographicCRS.WGS84, true);
}catch (Exception e) {
return null;
}
}
/**
* Retrieves the dimensions of the {@link RenderedImage} at index
* <code>imageIndex</code> for the provided {@link ImageReader} and
* {@link ImageInputStream}.
*
* <p>
* Notice that none of the input parameters can be <code>null</code> or a
* {@link NullPointerException} will be thrown. Morevoer the
* <code>imageIndex</code> cannot be negative or an
* {@link IllegalArgumentException} will be thrown.
*
* @param imageIndex
* the index of the image to get the dimensions for.
* @param inStream
* the {@link ImageInputStream} to use as an input
* @param reader
* the {@link ImageReader} to decode the image dimensions.
* @return a {@link Rectangle} that contains the dimensions for the image at
* index <code>imageIndex</code>
* @throws IOException
* in case the {@link ImageReader} or the
* {@link ImageInputStream} fail.
*/
public static Rectangle getDimension(final int imageIndex,
final ImageInputStream inStream, final ImageReader reader)
throws IOException {
Utilities.ensureNonNull("inStream", inStream);
Utilities.ensureNonNull("reader", reader);
if (imageIndex < 0)
throw new IllegalArgumentException(Errors.format(
ErrorKeys.INDEX_OUT_OF_BOUNDS_$1, imageIndex));
inStream.reset();
reader.setInput(inStream);
return new Rectangle(0, 0, reader.getWidth(imageIndex), reader
.getHeight(imageIndex));
}
/**
* Checks that the provided <code>dimensions</code> when intersected with
* the source region used by the provided {@link ImageReadParam} instance
* does not result in an empty {@link Rectangle}.
*
* <p>
* Input parameters cannot be null.
*
* @param readParameters
* an instance of {@link ImageReadParam} for which we want to
* check the source region element.
* @param dimensions
* an instance of {@link Rectangle} to use for the check.
* @return <code>true</code> if the intersection is not empty,
* <code>false</code> otherwise.
*/
public final static boolean checkEmptySourceRegion(final ImageReadParam readParameters,
final Rectangle dimensions) {
Utilities.ensureNonNull("readDimension", dimensions);
Utilities.ensureNonNull("readP", readParameters);
final Rectangle sourceRegion = readParameters.getSourceRegion();
Rectangle.intersect(sourceRegion, dimensions, sourceRegion);
if (sourceRegion.isEmpty())
return true;
readParameters.setSourceRegion(sourceRegion);
return false;
}
}