/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2007-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.io;
import java.util.Set;
import java.util.List;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Collections;
import java.util.logging.LogRecord;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.SampleModel;
import java.awt.image.BufferedImage;
import java.awt.image.IndexColorModel;
import java.io.IOException;
import javax.imageio.ImageReader;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.event.IIOReadWarningListener;
import javax.imageio.metadata.IIOMetadataFormat;
import org.opengis.coverage.grid.GridEnvelope;
import org.apache.sis.util.ArraysExt;
import org.geotoolkit.util.Utilities;
import org.apache.sis.util.Disposable;
import org.apache.sis.measure.NumberRange;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.util.Locales;
import org.geotoolkit.resources.Errors;
import org.geotoolkit.resources.Loggings;
import org.apache.sis.util.resources.IndexedResourceBundle;
import org.geotoolkit.coverage.grid.GeneralGridEnvelope;
import org.geotoolkit.image.io.metadata.SpatialMetadataFormat;
import org.geotoolkit.image.io.metadata.SpatialMetadata;
import org.geotoolkit.image.io.metadata.SampleDimension;
import org.geotoolkit.image.io.metadata.MetadataHelper;
import org.geotoolkit.image.io.metadata.SampleDomain;
import org.geotoolkit.internal.image.io.Warnings;
import static org.geotoolkit.image.io.SampleConversionType.*;
import static org.geotoolkit.image.io.MultidimensionalImageStore.*;
import static org.apache.sis.util.collection.Containers.isNullOrEmpty;
import static org.geotoolkit.image.io.metadata.SpatialMetadataFormat.ISO_FORMAT_NAME;
import static org.geotoolkit.image.io.metadata.SpatialMetadataFormat.GEOTK_FORMAT_NAME;
import org.geotoolkit.image.palette.IndexedPalette;
import org.geotoolkit.image.palette.Palette;
import org.geotoolkit.image.palette.PaletteFactory;
/**
* Base class for readers of spatial (usually geographic) data. This class extends the standard
* {@link ImageReader} class in order to improve the support of file formats having the following
* characteristics:
* <p>
* <ul>
* <li>Images may have metadata information that can be represented as ISO 19115-2 objects.</li>
* <li>Images may have no color information (e.g. RAW, ASCII or NetCDF files), in which case a
* colors palette can be specified in {@linkplain SpatialImageReadParam parameters}.</li>
* <li>Pixel values may be signed integers, in which case an offset needs to be applied
* since {@link java.awt.image.IndexColorModel} does not support negative values.</li>
* <li>Pixel values may be floating point values, in which case a non-standard color space
* is required.</li>
* </ul>
*
* {@section New API}
* This class provides the following API, which are new compared to the standard {@link ImageReader}
* class:
* <p>
* <ul>
* <li>The return type of {@link #getStreamMetadata()} and {@link #getImageMetadata(int)} is
* restricted to {@link SpatialMetadata}.</li>
* <li>The return type of {@link #getDefaultReadParam()} is restricted to
* {@link SpatialImageReadParam}.</li>
* <li>A new method, {@link #getNumBands(int)}, returns the number of bands in the specified
* image. Note that the bands may not contain color components in scientific dataset.</li>
* <li>A new method, {@link #getDimension(int)}, returns the dimension of the given image.
* In some formats like NetCDF, an "image" is actually a dataset which may have more than
* 2 dimensions.</li>
* </ul>
*
* {@section Services for implementors}
* This class provides the following conveniences for implementors. Note that the default behavior
* described below assumes the simplest file format: one image made of one band of floating point
* values using a grayscale color palette scaled to fit the range of sample values. This behavior
* can be changed by overriding the methods listed below.
*
* <ul>
* <li><p>Provides default {@link #getNumImages(boolean)} and {@link #getNumBands(int)}
* implementations, which return 1. This default behavior matches simple image formats
* like {@linkplain org.geotoolkit.image.io.plugin.RawImageReader RAW} or
* {@linkplain org.geotoolkit.image.io.plugin.AsciiGridReader ASCII} files.
* Those methods need to be overridden for more complex image formats.</p></li>
*
* <li><p>Provides {@link #checkImageIndex(int)} and {@link #checkBandIndex(int,int)} convenience
* methods. Those methods are invoked by most implementation of public methods. They perform
* their checks based on the informations provided by the above-cited {@link #getNumImages(boolean)}
* and {@link #getNumBands(int)} methods.</p></li>
*
* <li><p>Provides default implementations of {@link #getImageTypes(int)} and
* {@link #getRawImageType(int)} methods, which assume that only one image type is offered.
* The offered type is described by a default {@linkplain ImageTypeSpecifier image type
* specifier} created from the informations provided by {@link #getRawDataType(int)} and
* {@link #getImageMetadata(int)}.</p></li>
*
* <li><p>Provides {@link #getStreamMetadata()} and {@link #getImageMetadata(int)} default
* implementations, which return {@code null} as authorized by the specification.</p></li>
* </ul>
* <p>
* See the {@link #getDestination(int, ImageReadParam, int, int, SampleConverter[])} method for an
* example of code using some of the services provided by this class.
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 3.19
*
* @see SpatialImageWriter
*
* @since 3.06 (derived from 1.2)
* @module
*/
public abstract class SpatialImageReader extends ImageReader implements WarningProducer, Disposable {
/**
* Stream and image metadata for each images, or {@code null} if not yet created.
* The element at index 0 is the stream metadata, and next elements are image metadata
* for the image at {@code index}-1.
*/
private SpatialMetadata[] metadata;
/**
* Constructs a new image reader.
*
* @param provider The {@link ImageReaderSpi} that is constructing this object, or {@code null}.
*/
protected SpatialImageReader(final Spi provider) {
super(provider);
availableLocales = Locales.SIS.getAvailableLocales();
}
/**
* Sets the input source to use. This method invokes {@link #close()}
* before to set the new input.
*
* @param input The input object to use for future decoding.
* @param seekForwardOnly If {@code true}, images and metadata may only be read
* in ascending order from this input source.
* @param ignoreMetadata If {@code true}, metadata may be ignored during reads.
*/
@Override
public void setInput(Object input, boolean seekForwardOnly, boolean ignoreMetadata) {
closeSilently();
super.setInput(input, seekForwardOnly, ignoreMetadata);
}
/**
* Returns the resources for formatting error messages.
*/
final IndexedResourceBundle getErrorResources() {
return Errors.getResources(getLocale());
}
/**
* Ensures that the specified image index is inside the expected range.
* The expected range is {@link #minIndex minIndex} inclusive (initially 0)
* to <code>{@link #getNumImages getNumImages}(false)</code> exclusive.
* If {@code getNumImages(false)} returned -1, then this method does not
* check the upper bound.
*
* @param imageIndex Index to check for validity.
* @throws IndexOutOfBoundsException if the specified index is outside the expected range.
* @throws IOException If the operation failed because of an I/O error.
*/
protected void checkImageIndex(final int imageIndex) throws IOException, IndexOutOfBoundsException {
final int numImages = getNumImages(false);
final int minIndex = getMinIndex();
if (imageIndex < minIndex || (imageIndex >= numImages && numImages >= 0)) {
throw new IndexOutOfBoundsException(indexOutOfBounds(imageIndex, minIndex, numImages));
}
}
/**
* Ensures that the specified band index is inside the expected range. The expected
* range is 0 inclusive to <code>{@link #getNumBands getNumBands}(imageIndex)</code>
* exclusive.
*
* @param imageIndex The image index.
* @param bandIndex Index to check for validity.
* @throws IndexOutOfBoundsException if the specified index is outside the expected range.
* @throws IOException If the operation failed because of an I/O error.
*/
protected void checkBandIndex(final int imageIndex, final int bandIndex)
throws IOException, IndexOutOfBoundsException
{
// Call 'getNumBands' first in order to call 'checkImageIndex'.
final int numBands = getNumBands(imageIndex);
if (bandIndex >= numBands || bandIndex < 0) {
throw new IndexOutOfBoundsException(indexOutOfBounds(bandIndex, 0, numBands));
}
}
/**
* Formats an error message for an index out of bounds.
*
* @param index The index out of bounds.
* @param lower The lower legal value, inclusive.
* @param upper The upper legal value, exclusive.
*/
private String indexOutOfBounds(final int index, final int lower, final int upper) {
return getErrorResources().getString(Errors.Keys.ValueOutOfBounds_3,
index, (lower < upper) ? lower : "EOF", upper-1);
}
/**
* Returns the number of images available from the current input source.
* The default implementation returns 1.
*
* @param allowSearch If {@code true}, the number of images will be returned
* even if a search is required.
* @return The number of images, or -1 if {@code allowSearch}
* is {@code false} and a search would be required.
*
* @throws IllegalStateException if the {@linkplain #input input} source has not been set.
* @throws IOException if an error occurs reading the information from the input source.
*/
@Override
public int getNumImages(final boolean allowSearch) throws IllegalStateException, IOException {
if (input != null) {
return 1;
}
throw new IllegalStateException(getErrorResources().getString(Errors.Keys.NoImageInput));
}
/**
* Returns the number of bands available for the specified image.
* The default implementation returns 1.
*
* @param imageIndex The image index.
* @return The number of bands available for the specified image.
* @throws IOException if an error occurs reading the information from the input source.
*/
public int getNumBands(final int imageIndex) throws IOException {
checkImageIndex(imageIndex);
return 1;
}
/**
* Returns the number of dimension of the image at the given index.
* The default implementation returns 2.
* {@link MultidimensionalImageStore} implementations provide a different value.
*
* @param imageIndex The image index.
* @return The number of dimension for the image at the given index.
* @throws IOException if an error occurs reading the information from the input source.
*
* @see MultidimensionalImageStore
*
* @since 2.5
*/
public int getDimension(final int imageIndex) throws IOException {
checkImageIndex(imageIndex);
return 2; // max(X_DIMENSION, Y_DIMENSION) + 1
}
/**
* Returns the grid envelope of the image at the given index. The default implementation
* creates a grid envelope from the information provided by {@link #getDimension(int)},
* {@link #getWidth(int)} and {@link #getHeight(int)} methods.
* {@link MultidimensionalImageStore} implementations provide a different value.
*
* @param imageIndex The image index.
* @return The grid envelope for the image at the given index.
* @throws IOException if an error occurs reading the information from the input source.
*
* @see MultidimensionalImageStore
*
* @since 3.19
*/
@SuppressWarnings("fallthrough")
public GridEnvelope getGridEnvelope(final int imageIndex) throws IOException {
final int dimension = getDimension(imageIndex);
final int[] lower = new int[dimension];
final int[] upper = new int[dimension];
switch (dimension) {
default: Arrays.fill(upper, 1); // Fall through in all cases.
case Y_DIMENSION+1: upper[Y_DIMENSION] = getHeight(imageIndex);
case X_DIMENSION+1: upper[X_DIMENSION] = getWidth (imageIndex);
case 0: break;
}
return new GeneralGridEnvelope(lower, upper, false);
}
/**
* Returns metadata associated with the input source as a whole.
* The default implementation performs the following choice:
* <p>
* <ul>
* <li>If the metadata from a previous call were cached, return those metadata.</li>
* <li>Otherwise if {@link #isIgnoringMetadata()} is {@code true}, return {@code null}.</li>
* <li>Otherwise invoke <code>{@linkplain #createMetadata(int) createMetadata}(-1)</code>
* and cache the result.</li>
* </ul>
*
* @return The metadata, or {@code null} if none.
* @throws IOException if an error occurs during reading.
*/
@Override
public SpatialMetadata getStreamMetadata() throws IOException {
return getSpatialMetadata(-1);
}
/**
* Returns metadata associated with the given image.
* The default implementation performs the following choice:
* <p>
* <ul>
* <li>Invoke <code>{@linkplain #checkImageIndex(int) checkImageIndex}(imageIndex)</code>.</li>
* <li>If the metadata from a previous call were cached for the given index, return those metadata.</li>
* <li>Otherwise if {@link #isIgnoringMetadata()} is {@code true}, return {@code null}.</li>
* <li>Otherwise invoke <code>{@linkplain #createMetadata(int) createMetadata}(imageIndex)</code>
* and cache the result.</li>
* </ul>
*
* @param imageIndex The image index.
* @return The metadata, or {@code null} if none.
* @throws IOException if an error occurs during reading.
*/
@Override
public SpatialMetadata getImageMetadata(int imageIndex) throws IOException {
checkImageIndex(imageIndex);
return getSpatialMetadata(imageIndex);
}
/**
* Returns spatial metadata associated with the given image.
* This method performs the following choice:
* <p>
* <ul>
* <li>If the metadata from a previous call were cached for the given index, return those metadata.</li>
* <li>Otherwise if {@link #isIgnoringMetadata()} is {@code true}, return {@code null}.</li>
* <li>Otherwise invoke <code>{@linkplain #createMetadata(int) createMetadata}(imageIndex)</code>
* and cache the result.</li>
* </ul>
*
* @param imageIndex The image index, or -1 for stream metadata.
* @return The spatial metadata, or {@code null} if none.
* @throws IOException if an error occurs during reading.
*/
private SpatialMetadata getSpatialMetadata(final int imageIndex) throws IOException {
/*
* Checks if a cached instance is available.
*/
final int cacheIndex = imageIndex + 1;
if (metadata != null && cacheIndex >= 0 && cacheIndex < metadata.length) {
final SpatialMetadata candidate = metadata[cacheIndex];
if (candidate != null) {
return (candidate != SpatialMetadata.EMPTY) ? candidate : null;
}
}
if (isIgnoringMetadata()) {
return null;
}
/*
* Creates a new instance and cache it.
*/
SpatialMetadata candidate = createMetadata(imageIndex);
if (candidate != null) {
candidate.setReadOnly(true);
}
if (metadata == null) {
metadata = new SpatialMetadata[Math.max(cacheIndex+1, 4)];
}
if (cacheIndex >= metadata.length) {
metadata = Arrays.copyOf(metadata, Math.max(cacheIndex+1, metadata.length*2));
}
metadata[cacheIndex] = (candidate != null) ? candidate : SpatialMetadata.EMPTY;
return candidate;
}
/**
* Creates a new stream or image metadata. This method is invoked by the default implementation
* of {@link #getStreamMetadata()} and {@link #getImageMetadata(int)} when the requested metadata
* were not cached by a previous call, and {@link #isIgnoringMetadata()} returns {@code false}.
* <p>
* The default implementation returns {@code null} if every cases. Subclasses should override
* this method if they can provide metadata.
*
* @param imageIndex -1 for stream metadata, or the image index for image metadata.
* @return The requested metadata, or {@code null} if none.
* @throws IOException If an error occurred while reading metadata.
*/
protected SpatialMetadata createMetadata(final int imageIndex) throws IOException {
return null;
}
/**
* Returns {@code true} if the image at the given index has a color palette. Some formats like
* {@linkplain org.geotoolkit.image.io.plugin.RawImageReader RAW},
* {@linkplain org.geotoolkit.image.io.plugin.AsciiGridReader ASCII Grid} or
* {@linkplain org.geotoolkit.image.io.plugin.NetcdfImageReader NetCDF} don't store any color
* information with the pixel values, while other formats like PNG or JPEG (optionally wrapped
* in a {@linkplain org.geotoolkit.image.io.plugin.WorldFileImageReader World File reader})
* provide such color palette.
* <p>
* If this method returns {@code false}, no color information is included in the stream
* to be read and users can provide their own color palette with a call to the
* {@link SpatialImageReadParam#setPaletteName(String)} method. If this mehod returns
* {@code true}, then the image to be read already have its own color information and
* any call to the above-mentioned {@code setPaletteName} method are likely to be ignored.
* <p>
* The default implementation returns {@code false} in every cases. Subclasses shall override
* this method if the implemented image format may have a color palette.
*
* @param imageIndex The index of the image to be queried.
* @return {@code true} if the image at the given index has a color palette.
* @throws IOException If an error occurs reading the information from the input source.
*
* @see SpatialImageReadParam#setPaletteName(String)
*
* @since 3.11
*/
public boolean hasColors(final int imageIndex) throws IOException {
checkImageIndex(imageIndex);
return false;
}
/**
* Returns a collection of {@link ImageTypeSpecifier} containing possible image types to which
* the given image may be decoded. The default implementation returns a singleton containing
* <code>{@link #getRawImageType(int) getRawImageType}(imageIndex)</code>.
*
* @param imageIndex The index of the image to be queried.
* @return A set of suggested image types for decoding the current given image.
* @throws IOException If an error occurs reading the format information from the input source.
*/
@Override
public Iterator<ImageTypeSpecifier> getImageTypes(final int imageIndex) throws IOException {
final ImageTypeSpecifier type = getRawImageType(imageIndex);
final Set<ImageTypeSpecifier> types;
if (type != null) {
types = Collections.singleton(type);
} else {
// Should never occur in non-broken ImageReader, but experience
// suggests that we are better to be assume that this case happen.
types = Collections.emptySet();
}
return types.iterator();
}
/**
* Returns an image type specifier indicating the {@link SampleModel} and {@link ColorModel}
* which most closely represents the "raw" internal format of the image. The default
* implementation delegates to the following:
*
* {@preformat java
* return getImageType(imageIndex, getDefaultReadParam(), null);
* }
*
* If this method needs to be overridden, consider overriding the later instead.
*
* @param imageIndex The index of the image to be queried.
* @return The image type (never {@code null}).
* @throws IOException If an error occurs reading the format information from the input source.
*
* @see #getImageType(int, ImageReadParam, SampleConverter[])
* @see #getDefaultReadParam()
*/
@Override
public ImageTypeSpecifier getRawImageType(final int imageIndex) throws IOException {
return getImageType(imageIndex, getDefaultReadParam(), null);
}
/**
* Returns an image type specifier indicating the {@link SampleModel} and {@link ColorModel}
* to use for reading the image. In addition, this method also detects if some conversions
* (represented by {@link SampleConverter} instances) are required in order to store the
* sample values using the selected models. The conversions (if any) are keept as small as
* possible, but are sometime impossible to avoid for example because {@link IndexColorModel}
* does not allow negative sample values.
* <p>
* The default implementation applies the following steps:
*
* <ol>
* <li><p>The {@linkplain SampleDimension#getValidSampleValues() range of expected values}
* and the {@linkplain SampleDimension#getFillSampleValues() fill values} are extracted
* from the {@linkplain #getImageMetadata(int) image metadata}, if any.</p></li>
*
* <li><p>If the given {@code parameters} argument is an instance of {@link SpatialImageReadParam},
* then the user-supplied {@linkplain SpatialImageReadParam#getPaletteName palette name}
* is fetched. Otherwise or if no palette name was explicitly set, then this method default
* to {@value org.geotoolkit.image.io.SpatialImageReadParam#DEFAULT_PALETTE_NAME}. The
* palette name will be used in order to {@linkplain PaletteFactory#getColors(String)
* read a predefined set of colors} (as [A]RGB values) to be given to the
* {@linkplain IndexColorModel index color model}.</p></li>
*
* <li><p>If the {@linkplain #getRawDataType raw data type} is {@link DataBuffer#TYPE_FLOAT
* TYPE_FLOAT} or {@link DataBuffer#TYPE_DOUBLE TYPE_DOUBLE}, then this method builds
* a {@linkplain PaletteFactory#getContinuousPalette continuous palette} suitable for
* the range fetched at step 1. The data are assumed <cite>geophysics</cite> values
* rather than some packed values. Consequently, the {@linkplain SampleConverter sample
* converters} will replace no-data values by {@linkplain Float#NaN NaN}, but no other
* changes will be applied.</p></li>
*
* <li><p>Otherwise, if the {@linkplain #getRawDataType raw data type} is a unsigned integer type
* like {@link DataBuffer#TYPE_BYTE TYPE_BYTE} or {@link DataBuffer#TYPE_USHORT TYPE_USHORT},
* then this method builds an {@linkplain PaletteFactory#getPalette indexed palette} (i.e. a
* palette backed by an {@linkplain IndexColorModel index color model}) with just the minimal
* {@linkplain IndexColorModel#getMapSize size} needed for containing fully the range and the
* no-data values fetched at step 1. The data are assumed <cite>packed</cite> values rather
* than geophysics values. Consequently, the {@linkplain SampleConverter sample converters}
* will be the {@linkplain SampleConverter#IDENTITY identity converter} except in the
* following cases:
* <ul>
* <li>The {@linkplain SampleDimension#getValidSampleValues() range of valid values} is
* outside the range allowed by the {@linkplain #getRawDataType raw data type} (e.g.
* the range of valid values contains negative integers). In this case, the sample
* converter will shift the values to a strictly positive range and replace fill
* values by 0.</li>
* <li>At least one {@linkplain SampleDimension#getFillSampleValues() fill value} is
* outside the range of values allowed by the {@linkplain #getRawDataType raw data
* type}. In this case, this method will try to only replace the fill values by 0,
* without shifting the valid values if this shift can be avoided.</li>
* <li>At least one {@linkplain SampleDimension#getFillSampleValues() fill value} is
* far away from the {@linkplain SampleDimension#getValidSampleValues() range of
* valid values} (for example 9999 while the range of valid values is [0…255]).
* The meaning of "far away" is determined by the {@link #collapseNoDataValues
* collapseNoDataValues} method.</li>
* </ul>
* </p></li>
*
* <li><p>Otherwise, if the {@linkplain #getRawDataType raw data type} is a signed integer
* type like {@link DataBuffer#TYPE_SHORT TYPE_SHORT}, then this method builds an
* {@linkplain PaletteFactory#getPalette indexed palette} with the maximal {@linkplain
* IndexColorModel#getMapSize size} supported by the raw data type (note that this is
* memory expensive - typically 256 kilobytes). Negative values will be stored in their
* two's complement binary form in order to fit in the range of positive integers
* supported by the {@linkplain IndexColorModel index color model}.</p></li>
* </ol>
*
* {@section Using the Sample Converters}
* If the {@code converters} argument is non-null, then this method will store the
* {@link SampleConverter} instances in the supplied array. The array length shall be equals
* to the number of {@linkplain ImageReadParam#getSourceBands() source} and
* {@linkplain ImageReadParam#getDestinationBands() destination bands}.
* <p>
* The converters shall be used by {@link #read(int,ImageReadParam) read} method
* implementations for converting the values read in the datafile to values acceptable
* by the {@linkplain ColorModel color model}. See the
* {@link #getDestination(int, ImageReadParam, int, int, SampleConverter[]) getDestination}
* method for code example.
*
* {@section Overriding this method}
* Subclasses can override this method for example if the color {@linkplain Palette palette}
* and range of values should be computed in a different way. The example below creates an
* image type using hard-coded objects:
*
* {@preformat java
* int minimum = -2000; // Minimal expected value
* int maximum = +2300; // Maximal expected value
* int fillValue = -9999; // Value for missing data
* String colors = "rainbow"; // Named set of RGB colors
* converters[0] = SampleConverter.createOffset(1 - minimum, fillValue);
* Palette palette = PaletteFactory.getDefault().getPalettePadValueFirst(colors, maximum - minimum);
* return palette.getImageTypeSpecifier();
* }
*
* @param imageIndex
* The index of the image to be queried.
* @param parameters
* The user-supplied parameters, or {@code null}. Note: we recommend to supply
* {@link #getDefaultReadParam} instead of {@code null} since subclasses may
* override the later with default values suitable to a particular format.
* @param converters
* If non-null, an array where to store the converters created by this method.
* The length of this array shall be equals to the number of target bands.
* @return
* The image type (never {@code null}).
* @throws IOException
* If an error occurs while reading the format information from the input source.
*
* @see #getRawDataType
* @see #collapseNoDataValues
* @see #getDestination(int, ImageReadParam, int, int, SampleConverter[])
*/
@SuppressWarnings("fallthrough")
protected ImageTypeSpecifier getImageType(final int imageIndex,
final ImageReadParam parameters,
final SampleConverter[] converters)
throws IOException
{
/*
* Extracts all informations we will need from the user-supplied parameters, if any.
* Note: the number of bands in the target image (as requested by the caller)
* may be different than the number of bands in the source image (on disk).
*/
final ImageTypeSpecifier userType;
final String paletteName;
final int[] sourceBands;
final int[] targetBands;
final int visibleBand;
final int numBands;
if (parameters != null) {
sourceBands = parameters.getSourceBands();
targetBands = parameters.getDestinationBands();
userType = parameters.getDestinationType();
} else {
sourceBands = null;
targetBands = null;
userType = null;
}
if (sourceBands != null) {
numBands = sourceBands.length; // == targetBands.length (assuming valid ImageReadParam).
} else if (targetBands != null) {
numBands = targetBands.length;
} else {
numBands = getNumBands(imageIndex);
}
List<? extends SampleDomain> bands = null;
if (parameters instanceof SpatialImageReadParam) {
final SpatialImageReadParam geoparam = (SpatialImageReadParam) parameters;
paletteName = geoparam.getNonNullPaletteName();
visibleBand = geoparam.getVisibleBand();
bands = geoparam.getSampleDomains();
} else {
paletteName = SpatialImageReadParam.DEFAULT_PALETTE_NAME;
visibleBand = 0;
}
/*
* Gets the band metadata. If the user specified explicitly a SampleDomain in the
* parameters, this is all the information we need - so we can avoid the cost of
* querying IIOMetadata. Otherwise we will need to extract the image IIOMetadata.
*/
boolean convertBandIndices = false;
if (bands == null) {
final SpatialMetadata metadata;
final boolean oldIgnore = ignoreMetadata;
try {
ignoreMetadata = false;
metadata = getImageMetadata(imageIndex);
} finally {
ignoreMetadata = oldIgnore;
}
if (metadata != null) {
final List<SampleDimension> sd = metadata.getListForType(SampleDimension.class);
if (!isNullOrEmpty(sd)) {
convertBandIndices = (sourceBands != null);
bands = sd;
}
}
}
/*
* Gets the data type, and check if we should replace it by an other type. Type
* replacements are allowed only if the appropriate SampleConversionType enum is set.
*/
boolean replaceFillValues = false;
int dataType = (userType != null) ? userType.getSampleModel().getDataType() : getRawDataType(imageIndex);
if (userType == null && parameters instanceof SpatialImageReadParam) {
final SpatialImageReadParam geoparam = (SpatialImageReadParam) parameters;
switch (dataType) {
case DataBuffer.TYPE_SHORT: {
if (geoparam.isSampleConversionAllowed(SHIFT_SIGNED_INTEGERS)) {
dataType = DataBuffer.TYPE_USHORT;
}
// Fall through
}
case DataBuffer.TYPE_USHORT:
case DataBuffer.TYPE_INT:
case DataBuffer.TYPE_BYTE: {
if (bands == null || !geoparam.isSampleConversionAllowed(STORE_AS_FLOATS)) {
break;
}
boolean hasFillValues = false;
for (final SampleDomain domain : bands) {
final double[] fillValues = domain.getFillSampleValues();
if (fillValues != null && fillValues.length != 0) {
hasFillValues = true;
break;
}
}
if (!hasFillValues) {
break;
}
dataType = DataBuffer.TYPE_FLOAT;
// Fall through
}
case DataBuffer.TYPE_FLOAT:
case DataBuffer.TYPE_DOUBLE: {
replaceFillValues = geoparam.isSampleConversionAllowed(REPLACE_FILL_VALUES);
}
}
}
/*
* Gets the minimal and maximal values allowed for the target image type.
* Note that this is meanless for floating point types, so the values in
* that case are arbitrary.
*
* The only integer types that are signed are SHORT (not to be confused with
* USHORT) and INT. Other types like BYTE and USHORT are treated as unsigned.
*/
final boolean isFloat;
final long floor, ceil;
switch (dataType) {
case DataBuffer.TYPE_UNDEFINED: // Actually we don't really know what to do for this case...
case DataBuffer.TYPE_DOUBLE: // Fall through since we can treat this case as float.
case DataBuffer.TYPE_FLOAT: {
isFloat = true;
floor = Long.MIN_VALUE;
ceil = Long.MAX_VALUE;
break;
}
case DataBuffer.TYPE_INT: {
isFloat = false;
floor = Integer.MIN_VALUE;
ceil = Integer.MAX_VALUE;
break;
}
case DataBuffer.TYPE_SHORT: {
isFloat = false;
floor = Short.MIN_VALUE;
ceil = Short.MAX_VALUE;
break;
}
default: {
isFloat = false;
floor = 0;
ceil = (1L << DataBuffer.getDataTypeSize(dataType)) - 1;
break;
}
}
/*
* Computes a range of values for all bands, as the union in order to make sure that
* we can stores every sample values. Also creates SampleConverters in the process.
* The later is an opportunist action since we gather most of the needed information
* during the loop.
*/
NumberRange<?> allRanges = null;
NumberRange<?> visibleRange = null;
SampleConverter visibleConverter = SampleConverter.IDENTITY;
double maximumFillValue = 0; // Only in the visible band, and must be positive.
if (bands != null) {
MetadataHelper helper = null; // To be created only if needed.
final int numMetadataBands = bands.size(); // Never 0 - check was performed above.
for (int i=0; i<numBands; i++) {
int bandIndex = convertBandIndices ? sourceBands[i] : i;
if (bandIndex < 0 || bandIndex >= numMetadataBands) {
if (numMetadataBands != 1) {
// If there is exactly one metadata band, don't log any warning since
// we will assume that the metadata band apply to all data bands.
Warnings.log(this, null, SpatialImageReader.class, "getImageType",
indexOutOfBounds(bandIndex, 0, numMetadataBands));
}
bandIndex = numMetadataBands - 1;
}
/*
* Before to get the range, get the fill values with maximal precision.
* Some values may need to be casted from 'double' to 'float' in order
* to match the sample values in the raster. This cast to various types
* will be performed internally by the SampleConverter implementations.
*/
final SampleDomain band = bands.get(bandIndex);
final double[] fillValues = band.getFillSampleValues();
final NumberRange<?> range;
if (band instanceof SampleDimension) {
if (helper == null) {
helper = new MetadataHelper(this);
}
range = helper.getValidSampleValues(bandIndex, (SampleDimension) band, fillValues);
} else {
range = band.getValidSampleValues();
}
double minimum, maximum;
if (range != null) {
minimum = range.getMinDouble();
maximum = range.getMaxDouble();
if (!isFloat) {
// If the metadata do not contain any information about the range,
// treat as if we use the maximal range allowed by the data type.
if (minimum == Double.NEGATIVE_INFINITY) minimum = floor;
if (maximum == Double.POSITIVE_INFINITY) maximum = ceil;
}
final double extent = maximum - minimum;
if (extent >= 0 && (isFloat || extent <= (ceil - floor))) {
allRanges = (allRanges != null) ? allRanges.unionAny(range) : range;
} else {
// Use range.getMin/MaxValue() because they may be integers rather than doubles.
Warnings.log(this, null, SpatialImageReader.class, "getImageType",
Errors.Keys.IllegalRange_2, range.getMinValue(), range.getMaxValue());
continue;
}
} else {
minimum = Double.NaN;
maximum = Double.NaN;
}
final int targetBand = (targetBands != null) ? targetBands[i] : i;
/*
* For floating point types, replaces no-data values by NaN because the floating
* point numbers are typically used for geophysics data, so the raster is likely
* to be a "geophysics" view for GridCoverage2D. All other values are stored "as
* is" without any offset.
*
* For integer types, if the range of values from the source data file fits into
* the range of values allowed by the destination raster, we will use an identity
* converter. If the only required conversion is a shift from negative to positive
* values, creates an offset converter with no-data values collapsed to 0.
*/
final SampleConverter converter;
if (isFloat) {
// If the sample values are float values, we need to replace 99.99 fill value
// (for example) by 99.99f, which is 99.98999786376953 in double precision,
// otherwise the SampleConverter may not find them (denpending which method
// is invoked). This cast is done by the PadValueMask constructor.
converter = replaceFillValues ?
SampleConverter.createPadValuesMask(fillValues) : SampleConverter.IDENTITY;
} else {
final boolean isZeroValid = (minimum <= 0 && maximum >= 0);
boolean collapsePadValues = false;
if (fillValues != null && fillValues.length != 0) {
final double[] sorted = fillValues.clone();
Arrays.sort(sorted);
double minFill = sorted[0];
double maxFill = minFill;
int indexMax = sorted.length;
while (--indexMax!=0 && Double.isNaN(maxFill = sorted[indexMax]));
assert minFill <= maxFill || Double.isNaN(minFill) : maxFill;
if (targetBand == visibleBand && maxFill > maximumFillValue) {
maximumFillValue = maxFill;
}
if (minFill < floor || maxFill > ceil) {
// At least one fill value is outside the range of acceptable values.
collapsePadValues = true;
} else if (minimum >= 0) {
/*
* Arbitrary optimization of memory usage: if there is a "large" empty
* space between the range of valid values and a no-data value, then we
* may (at subclass implementors choice) collapse the no-data values to
* zero in order to avoid wasting the empty space. Note that we do not
* perform this collapse if the valid range contains negative values
* because it would not save any memory. We do not check the no-data
* values between 0 and 'minimum' for the same reason.
*/
int k = Arrays.binarySearch(sorted, maximum);
if (k >= 0) k++; // We want the first element greater than maximum.
else k = ~k; // Really ~ operator, not -
if (k <= indexMax) {
double unusedSpace = Math.max(sorted[k] - maximum - 1, 0);
while (++k <= indexMax) {
final double delta = sorted[k] - sorted[k-1] - 1;
if (delta > 0) {
unusedSpace += delta;
}
}
final int unused = (int) Math.min(Math.round(unusedSpace), Integer.MAX_VALUE);
collapsePadValues = collapseNoDataValues(isZeroValid, sorted, unused);
// We invoked 'collapseNoDataValues' unconditionally even if
// 'unused' is zero because the user may decide on the basis
// of other criterions, like 'isZeroValid'.
}
}
}
if (minimum < floor || maximum > ceil) {
// The range of valid values is outside the range allowed by raw data type.
converter = SampleConverter.createOffset(Math.ceil(1 - minimum), fillValues);
} else if (collapsePadValues) {
if (isZeroValid) {
// We need to collapse the no-data values to 0, but it causes a clash
// with the range of valid values. So we also shift the later.
converter = SampleConverter.createOffset(Math.ceil(1 - minimum), fillValues);
} else {
// We need to collapse the no-data values and there is no clash.
converter = SampleConverter.createPadValuesMask(fillValues);
}
} else {
/*
* Do NOT take 'fillValues' in account if there is no need to collapse
* them. This is not the converter's job to transform "packed" values to
* "geophysics" values. We just want them to fit in the IndexColorModel,
* and they already fit. So the identity converter is appropriate even
* in presence of pad values.
*/
converter = SampleConverter.IDENTITY;
}
}
if (converters != null && i < converters.length) {
converters[i] = converter;
}
if (targetBand == visibleBand) {
visibleConverter = converter;
visibleRange = range;
}
}
}
/*
* Ensure that all converters are defined. We typically have no converter if there
* is no "ImageDescription/Dimensions" metadata. If the user specified explicitly
* the image type, then we are done.
*/
if (converters != null) {
for (int i=Math.min(converters.length, numBands); --i>=0;) {
if (converters[i] == null) {
converters[i] = visibleConverter;
}
}
}
if (userType != null) {
return userType;
}
/*
* Creates a color palette suitable for the range of values in the visible band.
* The case for floating points is the simplest: we should not have any offset,
* at most a replacement of no-data values. In the case of integer values, we
* must make sure that the indexed color map is large enough for containing both
* the highest data value and the highest no-data value.
*/
if (visibleRange == null) {
visibleRange = (allRanges != null) ? allRanges : NumberRange.create(floor, true, ceil, true);
}
PaletteFactory factory = null;
if (parameters instanceof SpatialImageReadParam) {
factory = ((SpatialImageReadParam) parameters).getPaletteFactory();
}
if (factory == null) {
factory = PaletteFactory.getDefault();
}
factory.setWarningLocale(locale);
final double minimum = visibleRange.getMinDouble();
final double maximum = visibleRange.getMaxDouble();
final Palette palette;
if (isFloat) {
assert visibleConverter.getOffset() == 0 : visibleConverter;
palette = factory.getContinuousPalette(paletteName, (float) minimum,
(float) maximum, dataType, numBands, visibleBand);
} else {
final double offset = visibleConverter.getOffset();
long lower, upper;
if (minimum == Double.NEGATIVE_INFINITY) {
lower = floor;
} else {
lower = Math.round(minimum + offset);
if (!visibleRange.isMinIncluded()) {
lower++; // Must be inclusive
}
}
if (maximum == Double.POSITIVE_INFINITY) {
upper = ceil;
} else {
upper = Math.round(maximum + offset);
if (visibleRange.isMaxIncluded()) {
upper++; // Must be exclusive
}
}
long size = Math.max(upper, Math.round(maximumFillValue) + 1);
if (lower < 0) {
size -= lower;
}
/*
* The target lower, upper and size parameters are usually in the range of SHORT
* or USHORT data type. The Palette class will perform the necessary checks and
* throw an exception if those variables are out of range. However we may have
* values out of this range for TYPE_INT, in which case we will use the same slow
* color model than the one for floating point values.
*/
if (lower >= Short.MIN_VALUE && (lower + size) <= (lower >= 0 ? IndexedPalette.MAX_UNSIGNED+1 : Short.MAX_VALUE+1)) {
palette = factory.getPalette(paletteName, (int) lower, (int) upper, (int) size, numBands, visibleBand);
} else {
palette = factory.getContinuousPalette(paletteName, lower, upper, dataType, numBands, visibleBand);
}
}
return palette.getImageTypeSpecifier();
}
/**
* Returns the data type which most closely represents the "raw" internal data of the image.
* It should be one of {@link DataBuffer} {@code TYPE_*} constants. This information is used
* by {@link #getImageType(int, ImageReadParam, SampleConverter[]) getImageType(...)} in order
* to create a default {@link ImageTypeSpecifier}.
* <p>
* The default {@code SpatialImageReader} implementation works better with
*
* {@link DataBuffer#TYPE_BYTE TYPE_BYTE},
* {@link DataBuffer#TYPE_SHORT TYPE_SHORT},
* {@link DataBuffer#TYPE_USHORT TYPE_USHORT} and
* {@link DataBuffer#TYPE_FLOAT TYPE_FLOAT}.
*
* Other types may work, but developers are advised to override the {@code getImageType(...)}
* method as well.
* <p>
* The default implementation returns {@link DataBuffer#TYPE_FLOAT TYPE_FLOAT} in every cases.
*
* {@section Special case for negative integer values (<code>TYPE_SHORT</code>)}
* If the sample values are integers but the {@linkplain SampleDimension#getValidSampleValues()
* range of valid values} contains negative values, then strictly speaking this method should
* return a signed type ({@link DataBuffer#TYPE_SHORT TYPE_SHORT} or {@link DataBuffer#TYPE_INT
* TYPE_INT}). If nevertheless this method return a unsigned type
* ({@link DataBuffer#TYPE_BYTE TYPE_BYTE} or {@link DataBuffer#TYPE_USHORT TYPE_USHORT}), then
* the default {@link #getImageType(int, ImageReadParam, SampleConverter[]) getImageType(...)}
* implementation will add an offset in order to fit all sample values in the range of strictly
* positive values.
* <p>
* <table border="1" cellspacing="0" cellpadding="9"><tr><td>
* <b>Example:</b> if the range of sample values is [-23000 … +23000], then there
* is a choice:
*
* <ol>
* <li><p><b>Signed integers storage:</b> If this method returns {@link DataBuffer#TYPE_SHORT
* TYPE_SHORT}, then the data will be stored "as is" without transformation. However the
* {@linkplain IndexColorModel#getMapSize() size of the Index Color Model} will be the maximal
* length allowed by 16 bits integers, which result in a Color Model consuming 256 kilobytes
* of memory no matter how large is the range of values actually used.</p>
*
* <p>Positive values are stored in the [0 … {@value java.lang.Short#MAX_VALUE}] range
* directly, while the negative values are converted in their two's complement binary form
* before to be stored in the [32768 … 65535] range. The space not used by the
* [-23000 … +23000] range is lost.</p></li>
*
* <li><p><b>Unsigned integers storage:</b> If this method returns {@link DataBuffer#TYPE_USHORT
* TYPE_USHORT}, then the data will be translated to the smallest strictly positive range that
* can holds the data ([1 … 46001] for the above example). The 0 value is reserved for
* missing data. The result is a smaller {@linkplain IndexColorModel Index Color Model} than
* the one used by untranslated data.</p></li>
* </ol>
* </td></tr></table>
* <p>
* Beware that signed integers (case 1 in the above example) used with {@link IndexColorModel}
* require explicit casts to the {@code short} type as in the example below. Using directly the
* {@link java.awt.image.Raster#getSample(int,int,int)} return value is not sufficient because
* the returned value would be unsigned no matter what this {@code getRawDataType(int)} method
* returned.
*
* {@preformat java
* int value = (short) myRaster.getSample(x, y, b); // Intentional casts int → short → int.
* }
*
* Given this gotcha and the fact that signed integers require large color palette, users are
* advised to prefer unsigned types if they can afford the offset applied on sample values.
*
* @param imageIndex The index of the image to be queried.
* @return The data type ({@link DataBuffer#TYPE_FLOAT} by default).
* @throws IOException If an error occurs reading the format information from the input source.
*
* @see #getImageType(int, ImageReadParam, SampleConverter[])
* @see SampleConversionType#SHIFT_SIGNED_INTEGERS
*/
protected int getRawDataType(final int imageIndex) throws IOException {
checkImageIndex(imageIndex);
return DataBuffer.TYPE_FLOAT;
}
/**
* Returns {@code true} if the no-data values should be collapsed to 0 in order to save memory.
* This method is invoked automatically by the {@link #getImageType(int, ImageReadParam,
* SampleConverter[]) getImageType(...)} method when it detected some unused space between the
* {@linkplain SampleDimension#getValidSampleValues range of valid values} and at least one
* {@linkplain SampleDimension#getFillSampleValues no-data value}.
* <p>
* The default implementation returns {@code false} in all cases, thus avoiding arbitrary
* choice. Subclasses can override this method with some arbitrary threashold, as in the
* example below:
*
* {@preformat java
* return unusedSpace >= 1024;
* }
*
* @param isZeroValid
* {@code true} if 0 is a valid value. If this method returns {@code true} while
* {@code isZeroValid} is {@code true}, then the {@linkplain SampleConverter sample
* converter} to be returned by {@link #getImageType(int, ImageReadParam,
* SampleConverter[]) getImageType(...)} will offset all valid values by 1.
* @param nodataValues
* The {@linkplain Arrays#sort(double[]) sorted}
* {@linkplain SampleDimension#getFillSampleValues no-data values} (never null and never empty).
* @param unusedSpace
* The largest amount of unused space outside the range of valid values.
* @return {@code true} if the no-data values should be collapsed to 0 in order to save memory.
*/
protected boolean collapseNoDataValues(boolean isZeroValid, double[] nodataValues, int unusedSpace) {
return false;
}
/**
* Returns the buffered image to which decoded pixel data should be written. The image
* is determined by inspecting the supplied parameters if it is non-null, as described
* in the {@linkplain #getDestination(ImageReadParam,Iterator,int,int) super-class method}.
* In addition, this method also detects if the sample values need to be converted before
* to be stored in the {@link BufferedImage}, for example because {@link IndexColorModel}
* does not support negative integers. The conversions (if any) are represented by
* {@link SampleConverter} objects, which are computed as documented in the
* {@link #getImageType getImageType} method.
*
* {@section Using the Sample Converters}
* If the {@code converters} argument is non-null, then this method will store the
* {@link SampleConverter} instances in the supplied array. The array length shall be equals
* to the number of {@linkplain ImageReadParam#getSourceBands() source} and
* {@linkplain ImageReadParam#getDestinationBands() destination bands}.
* <p>
* The converters shall be used by {@link #read(int,ImageReadParam) read} method
* implementations for converting the values read in the datafile to values acceptable
* by the {@linkplain ColorModel color model}.
* Example (omitting the {@linkplain ImageReadParam#setSourceSubsampling subsamplings}
* handling for simplicity):
*
* {@preformat java
* public BufferedImage read(int imageIndex, ImageReadParam param) throws IOException {
* final int[] srcBands, dstBands;
* if (param != null) {
* srcBands = param.getSourceBands();
* dstBands = param.getDestinationBands();
* } else {
* srcBands = null;
* dstBands = null;
* }
* final int numSrcBands = (srcBands != null) ? srcBands.length : ...; // Image-dependant
* final int numDstBands = (dstBands != null) ? dstBands.length : numSrcBands;
* checkReadParamBandSettings(param, numSrcBands, numDstBands);
*
* final int width = ...; // Image-dependant
* final int height = ...; // Image-dependant
* final SampleConverter[] converters = new SampleConverter[numDstBands];
* final BufferedImage image = getDestination(imageIndex, param, width, height, converters);
* final WritableRaster raster = image.getRaster();
* final Rectangle srcRegion = new Rectangle();
* final Rectangle destRegion = new Rectangle();
* computeRegions(param, width, height, image, srcRegion, destRegion);
* final int xmin = destRegion.x;
* final int ymin = destRegion.y;
* final int xmax = destRegion.width + xmin;
* final int ymax = destRegion.height + ymin;
* for (int band=0; band < numDstBands; band++) {
* final int srcBand = (srcBands == null) ? band : srcBands[band];
* final int dstBand = (dstBands == null) ? band : dstBands[band];
* final SampleConverter converter = converters[band];
* for (int y=ymin; y<ymax; y++) {
* for (int x=xmin; x<xmax; x++) {
* float value = ...; // Image-dependant
* value = converter.convert(value);
* raster.setSample(x, y, dstBand, value);
* }
* }
* }
* }
* }
*
* @param imageIndex The index of the image to be retrieved.
* @param parameters The parameter given to the {@code read} method.
* @param width The true width of the image or tile begin decoded.
* @param height The true width of the image or tile being decoded.
* @param converters If non-null, an array where to store the converters required
* for converting decoded pixel data into stored pixel data.
* @return The buffered image to which decoded pixel data should be written.
* @throws IOException If an error occurs reading the format information from the input source.
*
* @see #getImageType(int, ImageReadParam, SampleConverter[])
*/
protected BufferedImage getDestination(final int imageIndex, final ImageReadParam parameters,
final int width, final int height, final SampleConverter[] converters)
throws IOException
{
final ImageTypeSpecifier type = getImageType(imageIndex, parameters, converters);
final Set<ImageTypeSpecifier> spi = Collections.singleton(type);
return getDestination(parameters, spi.iterator(), width, height);
}
/**
* Returns a default parameter object appropriate for this format. The default
* implementation constructs and returns a new {@link SpatialImageReadParam}.
*
* @return An {@code ImageReadParam} object which may be used.
*/
@Override
public SpatialImageReadParam getDefaultReadParam() {
return new SpatialImageReadParam(this);
}
/**
* Invoked when a warning occurred. The default implementation makes the following choice:
* <p>
* <ul>
* <li>If at least one {@linkplain IIOReadWarningListener warning listener}
* has been {@linkplain #addIIOReadWarningListener specified}, then the
* {@link IIOReadWarningListener#warningOccurred warningOccurred} method is
* invoked for each of them and the log record is <strong>not</strong> logged.</li>
*
* <li>Otherwise, the log record is sent to the {@code "org.geotoolkit.image.io"} logger.</li>
* </ul>
* <p>
* Subclasses may override this method if more processing is wanted, or for
* throwing exception if some warnings should be considered as fatal errors.
*
* @param record The warning to log.
* @return {@code true} if the message has been sent to at least one warning listener,
* or {@code false} if it has been sent to the logging system as a fallback.
*
* @see org.geotoolkit.image.io.metadata.MetadataNodeParser#warningOccurred(LogRecord)
*/
@Override
public boolean warningOccurred(final LogRecord record) {
if (warningListeners == null) {
record.setLoggerName(LOGGER.getName());
LOGGER.log(record);
return false;
} else {
processWarningOccurred(Loggings.format(record));
return true;
}
}
/**
* Invokes {@link #close} and logs the exception if any. This method is invoked from
* methods that do not allow {@link IOException} to be thrown. Since we will not use
* the stream anymore after closing it, it should not be a big deal if an error occurred.
*/
final void closeSilently() {
try {
close();
} catch (IOException exception) {
Logging.unexpectedException(LOGGER, getClass(), "close", exception);
}
}
/**
* Invoked when a new input is set or when the reader is disposed. The default implementation
* clears the internal cache. Sub-classes can override this method if they have more resources
* to dispose, but should always invoke {@code super.close()}.
*
* @throws IOException If an error occurred while closing a stream.
*
* @since 3.16
*/
protected void close() throws IOException {
metadata = null;
}
/*
* There is no need to override reset(), because the default ImageReader.reset()
* implementation invokes setInput(null, false, false), which in turn invokes our
* closeSilently() method.
*/
/**
* Allows any resources held by this reader to be released. If an input stream were
* created by {@link StreamImageReader} or {@link ImageReaderAdapter}, it will be
* {@linkplain StreamImageReader#close() closed} before to dispose this reader.
*/
@Override
public void dispose() {
closeSilently();
super.dispose();
}
/**
* Service provider interfaces (SPI) for {@link SpatialImageReader}s.
* This base class initializes fields to the values listed below:
* <p>
* <table border="1">
* <tr bgcolor="lightblue">
* <th>Field</th>
* <th>Value</th>
* </tr><tr>
* <td> {@link #nativeStreamMetadataFormatName} </td>
* <td> {@value org.geotoolkit.image.io.metadata.SpatialMetadataFormat#GEOTK_FORMAT_NAME} </td>
* </tr><tr>
* <td> {@link #nativeImageMetadataFormatName} </td>
* <td> {@value org.geotoolkit.image.io.metadata.SpatialMetadataFormat#GEOTK_FORMAT_NAME} </td>
* </tr>
* </table>
* <p>
* All other fields are left to their default values ({@code null} or {@code false}).
* Subclasses are responsible for initializing those fields.
* Some subclasses may also restore the {@link #nativeStreamMetadataFormatName} to
* {@code null} if they do not support stream metadata.
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.07
*
* @see SpatialImageWriter.Spi
*
* @since 3.07
* @module
*/
protected abstract static class Spi extends ImageReaderSpi {
/**
* The {@value org.geotoolkit.image.io.metadata.SpatialMetadataFormat#GEOTK_FORMAT_NAME} value
* in an array, for optional assignment to {@code extra[Stream|Image]MetadataFormatNames} fields.
*/
private static final String[] GEOTK = {
GEOTK_FORMAT_NAME
};
/**
* An array containing GEOTK and ISO metadata.
*/
private static final String[] GEOTK_ISO = {
SpatialMetadataFormat.GEOTK_FORMAT_NAME, SpatialMetadataFormat.ISO_FORMAT_NAME
};
/**
* Initializes a default provider for {@link SpatialImageReader}s.
* <p>
* For efficiency reasons, the fields are initialized to a shared array.
* Subclasses can assign new arrays, but should not modify the default array content.
*/
protected Spi() {
nativeStreamMetadataFormatName = GEOTK_FORMAT_NAME;
nativeImageMetadataFormatName = GEOTK_FORMAT_NAME;
if (getClass().getName().startsWith("org.geotoolkit.")) {
vendorName = "Geotoolkit.org";
version = Utilities.VERSION.toString();
}
}
/**
* Adds the given format to the list of extra stream or metadata format names,
* if not already present. This method does nothing if the format is already
* listed as the native or an extra format.
*
* @param formatName
* @param stream {@code true} for adding to the list of {@linkplain #extraStreamMetadataFormatNames extra stream formats}.
* @param image {@code true} for adding to the list of {@linkplain #extraImageMetadataFormatNames extra image formats}.
*
* @since 3.20
*/
protected void addExtraMetadataFormat(final String formatName, final boolean stream, final boolean image) {
if (stream) extraStreamMetadataFormatNames = addExtraMetadataFormat(formatName, nativeStreamMetadataFormatName, extraStreamMetadataFormatNames);
if (image) extraImageMetadataFormatNames = addExtraMetadataFormat(formatName, nativeImageMetadataFormatName, extraImageMetadataFormatNames);
}
/**
* Adds the {@value SpatialMetadataFormat#GEOTK_FORMAT_NAME} to the given array, if
* not already presents. This method returns a shared array for some common cases.
*/
static String[] addExtraMetadataFormat(final String formatName, final String nativeName, final String[] formatNames) {
ArgumentChecks.ensureNonNull("formatName", formatName);
if (formatName.equals(nativeName) || ArraysExt.contains(formatNames, formatName)) {
return formatNames;
}
if (formatNames == null || formatNames.length == 0) {
return formatName.equals(GEOTK_FORMAT_NAME) ? GEOTK : new String[] {formatName};
}
if ((GEOTK_FORMAT_NAME.equals(formatNames[0]) && formatName.equals(ISO_FORMAT_NAME))) {
return GEOTK_ISO;
}
return ArraysExt.append(formatNames, formatName);
}
/**
* Returns a code indicating which kind of metadata to returns. The codes are:
* <p>
* <ul>
* <li>0: return {@code null}</li>
* <li>1: delegate to {@link SpatialMetadataFormat} static methods.</li>
* <li>2: delegate to default {@link ImageReadWriteSpi} methods.</li>
* <li>3: the given format name is unsupported.</li>
* </ul>
* <p>
* We can not delegates to {@link ImageReadWriteSpi} directly, because its default
* implementation is not null-safe (note that the Image I/O specification allows to
* return {@code null} formats).
*/
static int getMetadataFormatCode(final String formatName,
final String nativeName, final String nativeClassName,
final String[] extraNames, final String[] extraClassNames)
{
ArgumentChecks.ensureNonNull("formatName", formatName);
if (formatName.equals(nativeName)) {
return isSpatialFormat(formatName) ? 1 :
(nativeClassName != null) ? 2 : 0;
}
if (extraNames != null) {
for (int i=0; i<extraNames.length; i++) {
if (formatName.equals(extraNames[i])) {
return isSpatialFormat(formatName) ? 1 :
(extraClassNames != null && extraClassNames[i] != null) ? 2 : 0;
}
}
}
return 3;
}
/**
* Returns {@code true} if the given format name is one of the Geotk hard-coded ones.
*/
private static boolean isSpatialFormat(final String formatName) {
return formatName.equals(GEOTK_FORMAT_NAME) || formatName.equals(ISO_FORMAT_NAME);
}
/**
* Returns a description of the stream metadata of the given name.
* If no description is available, then this method returns {@code null}.
*
* @param formatName The desired stream metadata format.
* @return The stream metadata format of the given name.
*/
@Override
public IIOMetadataFormat getStreamMetadataFormat(final String formatName) {
switch (getMetadataFormatCode(formatName,
nativeStreamMetadataFormatName,
nativeStreamMetadataFormatClassName,
extraStreamMetadataFormatNames,
extraStreamMetadataFormatClassNames))
{
case 0: return null;
case 1: return SpatialMetadataFormat.getStreamInstance(formatName);
default: return super.getStreamMetadataFormat(formatName);
}
}
/**
* Returns a description of the image metadata of the given name.
* If no description is available, then this method returns {@code null}.
*
* @param formatName The desired image metadata format.
* @return The image metadata format of the given name.
*/
@Override
public IIOMetadataFormat getImageMetadataFormat(final String formatName) {
switch (getMetadataFormatCode(formatName,
nativeImageMetadataFormatName,
nativeImageMetadataFormatClassName,
extraImageMetadataFormatNames,
extraImageMetadataFormatClassNames))
{
case 0: return null;
case 1: return SpatialMetadataFormat.getImageInstance(formatName);
default: return super.getImageMetadataFormat(formatName);
}
}
}
}