/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2001-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2009-2012, Geomatys
*
* 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.geotoolkit.image.internal;
import java.util.List;
import java.util.Arrays;
import java.awt.image.*;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.color.ColorSpace;
import javax.media.jai.JAI;
import javax.media.jai.PlanarImage;
import javax.media.jai.NullOpImage;
import javax.media.jai.ImageLayout;
import javax.media.jai.Interpolation;
import com.sun.media.jai.util.ImageUtil;
import org.geotoolkit.lang.Static;
import org.geotoolkit.resources.Errors;
import org.apache.sis.util.Classes;
import static java.awt.image.DataBuffer.*;
/**
* 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.
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @author Simone Giannecchini (Geosolutions)
* @version 3.20
*
* @since 1.2
* @module
*/
public final class ImageUtilities extends Static {
/**
* The default tile size. This default tile size can be
* overridden with a call to {@link JAI#setDefaultTileSize}.
*/
private static final Dimension DEFAULT_TILE_SIZE = new Dimension(512, 512);
/**
* The minimum tile size.
*/
public static final int 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 re-tiled. 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 issue, 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 unneeded 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.
*/
private 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}.
*/
private static final int[] INTERPOLATION_TYPES= {
Interpolation.INTERP_NEAREST,
Interpolation.INTERP_NEAREST,
Interpolation.INTERP_BILINEAR,
Interpolation.INTERP_BICUBIC,
Interpolation.INTERP_BICUBIC_2
};
/**
* Do not allow creation of instances of this class.
*/
private ImageUtilities() {
}
/**
* Returns the bounds of the given image as a new rectangle. Note that if the image is actually
* and instance of {@link PlanarImage} and the caller will not modify the rectangle values,
* then {@link PlanarImage#getBounds()} can be used instead.
*
* @param image The image for which to get the bounds.
* @return The bounds of the given image.
*
* @see Raster#getBounds()
* @see PlanarImage#getBounds()
*/
public static Rectangle getBounds(final RenderedImage image) {
return new Rectangle(image.getMinX(), image.getMinY(), image.getWidth(), image.getHeight());
}
/**
* 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}.
*
* @param image The image for which to suggest a layout.
* @return The suggested image layout.
*/
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).
*
* @param image The image for which to suggest a layout.
* @return The suggested image layout.
*/
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 = 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:
* <p>
* <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.
*
* @param image The image for which to suggest rendering hints.
* @return The suggested rendering hints for the given image.
*/
public static RenderingHints getRenderingHints(final RenderedImage image) {
final ImageLayout layout = getImageLayout(image, false);
return (layout != null) ? new RenderingHints(JAI.KEY_IMAGE_LAYOUT, layout) : null;
}
/**
* Returns {@code true} if the given image is tiled.
*
* @param image The image to test.
* @return {@code true} if the given image is tiled.
*
* @since 3.20
*/
public static boolean isTiled(final RenderedImage image) {
return (image.getTileWidth() < image.getWidth()) ||
(image.getTileHeight() < image.getHeight());
}
/**
* Suggests a tile size for the specified image size. On input, {@code size} is the image
* 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 #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}. Let {@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}, <i>etc.</i> until one of the
* following happen:
* <p>
* <ul>
* <li>A suitable tile size is found. More specifically, a size is found which is a divisor
* 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>
*
* @param size The image size.
* @return Suggested tile size for the given image size.
*/
public static Dimension toTileSize(final Dimension size) {
Dimension defaultSize = JAI.getDefaultTileSize();
if (defaultSize == null) {
defaultSize = 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 #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 #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-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 [GEOTOOLKIT_MIN_TILE_SIZE .. MAX_TIME_SIZE]. We will begin
* with a tile size equal 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 [GEOTOOLKIT_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) >= 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 [GEOTOOLKIT_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 javax.media.jai.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<? extends 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 = 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 = 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 = 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(Errors.Keys.UnknownInterpolation_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 interp The interpolation object, or {@code null} for "nearest"
* (which is an other way to say "no interpolation").
* @return The interpolation name.
*/
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);
}
/**
* Returns {@code true} if the given type is {@link DataBuffer#TYPE_FLOAT TYPE_FLOAT}
* or {@link DataBuffer#TYPE_DOUBLE TYPE_DOUBLE}.
*
* @param type The type to test.
* @return {@code true} if the given type is one of floating point types.
*/
public static boolean isFloatType(final int type) {
return (type == TYPE_FLOAT) || (type == TYPE_DOUBLE);
}
/**
* Returns a data type capable to hold the values of the two given data types.
*
* @param t1 The first data type.
* @param t2 The second data type.
* @return A data type capable to hold the values of the two given data types.
*/
public static int typeForBoth(final int t1, final int t2) {
final int min = Math.min(t1, t2);
final int max = Math.max(t1, t2);
if (min == max) {
return max;
}
if (min >= TYPE_BYTE && max <= TYPE_DOUBLE) {
return (min == TYPE_USHORT && max == TYPE_SHORT) ? TYPE_INT : max;
}
return TYPE_UNDEFINED;
}
/**
* Suggests the smallest type capable to hold the given range of values.
*
* @param minimum The minimal value to hold, inclusive.
* @param maximum The maximal value to hold, <strong>inclusive</strong>.
* @return The data type, or {@link DataBuffer#TYPE_UNDEFINED} if the given
* range is invalid, contains NaN or infinity values.
*/
public static int typeForRange(final double minimum, final double maximum) {
if (maximum >= minimum) {
for (int type=TYPE_BYTE; type<=TYPE_DOUBLE; type++) {
if (minimum >= minimum(type) && maximum <= maximum(type)) {
return type;
}
}
}
return TYPE_UNDEFINED;
}
/**
* Returns the minimum allowed value for a certain data type. This method does not returns
* negative infinity for float and double values, despite that they are valid values.
*
* {@section Note on floating point types}
* If the given type is {@link DataBuffer#TYPE_FLOAT TYPE_FLOAT} or {@link DataBuffer#TYPE_DOUBLE
* TYPE_DOUBLE} (you can use {@link #isFloatType} for checking that), then there is some chances
* that what you really want is the minimal <cite>normalized value</cite>. In such case, you
* should invoke {@link ColorSpace#getMinValue} instead. This doesn't apply to the alpha channel,
* where the minimal value is always 0 (fully transparent).
*
* @param dataType The data type to suggest a minimum value for.
* @return The minimum value for the given data type.
*/
public static double minimum(final int dataType) {
switch (dataType) {
case TYPE_BYTE: // Fall through
case TYPE_USHORT: return 0;
case TYPE_SHORT: return Short .MIN_VALUE;
case TYPE_INT: return Integer.MIN_VALUE;
case TYPE_FLOAT: return -Float .MAX_VALUE;
case TYPE_DOUBLE: return -Double .MAX_VALUE;
default: throw new IllegalArgumentException(String.valueOf(dataType));
}
}
/**
* Returns the maximum allowed value for a certain data type. This method does not returns
* positive infinity for float and double values, despite that they are valid values.
*
* {@section Note on floating point types}
* If the given type is {@link DataBuffer#TYPE_FLOAT TYPE_FLOAT} or {@link DataBuffer#TYPE_DOUBLE
* TYPE_DOUBLE} (you can use {@link #isFloatType} for checking that), then there is some chances
* that what you really want is the maximal <cite>normalized value</cite>. In such case, you
* should invoke {@link ColorSpace#getMaxValue} instead. This doesn't apply to the alpha channel,
* where the maximal value is always 1 (fully opaque).
*
* @param dataType The data type to suggest a maximum value for.
* @return The maximum value for the given data type.
*/
public static double maximum(final int dataType) {
switch (dataType) {
case TYPE_BYTE: return 0xFF;
case TYPE_USHORT: return 0xFFFF;
case TYPE_SHORT: return Short .MAX_VALUE;
case TYPE_INT: return Integer.MAX_VALUE;
case TYPE_FLOAT: return Float .MAX_VALUE;
case TYPE_DOUBLE: return Double .MAX_VALUE;
default: throw new IllegalArgumentException(String.valueOf(dataType));
}
}
/**
* 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 be 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; y++) {
int x = image.getMinTileX();
for (int nx = image.getNumXTiles(); --nx >= 0; x++) {
final WritableRaster raster = image.getWritableTile(x, y);
try {
fill(raster.getDataBuffer(), value);
} finally {
image.releaseWritableTile(x, y);
}
}
}
}
/**
* Sets every samples in the given region of the given raster to the given value.
*
* @param raster The raster where to set the sample values.
* @param region The region in the given rectangle where the values should be set.
* @param value The value to be given to every samples that are inside the given region.
*/
public static void fill(final WritableRaster raster, Rectangle region, final Number value) {
final Rectangle bounds = raster.getBounds();
if (region.contains(bounds)) {
fill(raster.getDataBuffer(), value);
} else {
region = region.intersection(bounds);
final double[] background = new double[raster.getNumBands()];
Arrays.fill(background, value.doubleValue());
ImageUtil.fillBackground(raster, region, background);
}
}
/**
* Sets the content of all banks in the given data buffer to the specified value. We do not
* allow setting of different value for individual bank because the data buffer "banks" do
* not necessarily match the image "bands".
* <p>
* We do not provide version for setting only a portion of the data buffer in order to
* avoid the complexity of considering the number of bits per pixel, the pixel stride
* and the line stride - the risk of bug would be too high, we are better to stick to
* the Java API for that.
*
* @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(Errors.Keys.UnsupportedDataType));
}
}
/**
* Replaces the color model in the given image by the given one.
* The sample values are transfered with no change. This method
* is <strong>not</strong> suitable for anything that change the
* pixel layout, the number of bands, etc.
*
* @param image The image in which to change the color model.
* @param cm The new color model.
* @return An image with the new color model.
*/
public static RenderedImage replaceColorModel(RenderedImage image, final ColorModel cm) {
if (image instanceof BufferedImage) {
final BufferedImage b = (BufferedImage) image;
image = new BufferedImage(cm, b.getRaster(), b.isAlphaPremultiplied(), null);
} else {
final ImageLayout layout = new ImageLayout();
layout.setColorModel(cm);
image = new NullOpImage(image, layout, null, NullOpImage.OP_COMPUTE_BOUND);
}
return image;
}
}