/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2010-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2010-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.plugin;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.Locale;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.EOFException;
import java.nio.Buffer;
import java.nio.ByteOrder;
import java.nio.ByteBuffer;
import java.nio.ShortBuffer;
import java.nio.IntBuffer;
import java.nio.FloatBuffer;
import java.nio.DoubleBuffer;
import java.nio.channels.FileChannel;
import java.awt.Rectangle;
import java.awt.Transparency;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster;
import java.awt.image.SampleModel;
import java.awt.image.ColorModel;
import java.awt.image.ComponentColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferShort;
import java.awt.image.DataBufferUShort;
import java.awt.image.DataBufferInt;
import java.awt.image.DataBufferFloat;
import java.awt.image.DataBufferDouble;
import javax.imageio.IIOException;
import javax.imageio.ImageReader;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.spi.ServiceRegistry;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.logging.Logging;
import org.geotoolkit.image.SampleModels;
import org.geotoolkit.image.io.SpatialImageReader;
import org.geotoolkit.image.io.UnsupportedImageFormatException;
import org.geotoolkit.lang.SystemOverride;
import org.geotoolkit.resources.Errors;
/**
* An image reader for uncompressed TIFF files or RGB images. This image reader duplicates the works
* performed by the reader provided in <cite>Image I/O extension for Java Advanced Imaging</cite>,
* but is specialized to the specific case of uncompressed files. For such case, this
* {@code RawTiffImageReader} is faster.
* <p>
* {@code RawTiffImageReader} has the following restrictions:
* <p>
* <ul>
* <li>Can read only from files as a {@link File} or {@link String} input objects
* (the standard {@link javax.imageio.stream.ImageInputStream} is not supported).</li>
* <li>Can read only uncompressed (RAW) images
* (the JPEG and LZW compressions are not supported).</li>
* <li>Can read only tiled images (this restriction may be removed in a future version).</li>
* <li>Color model must be RGB (this restriction may be removed in a future version).</li>
* <li>Components are stored in "chunky" format, not planar (this restriction may be removed
* in a future version).</li>
* <li>The source and target bands can not be modified (this restriction may be removed
* in a future version).</li>
* <li>Metadata are ignored (this restriction may be removed in a future version).</li>
* </ul>
* <p>
* Because of the above-cited restrictions, this reader registers itself only after the JAI
* readers (unless otherwise specified). Users wanting this reader should request for it
* explicitly, for example as below:
*
* {@preformat java
* ImageReaderSpi spi = IIORegistry.getDefaultInstance().getServiceProviderByClass(RawTiffImageReader.class);
* ImageReader reader = spi.createReaderInstance();
* }
*
* This image reader can also process <cite>Big TIFF</cite> images.
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.16
*
* @since 3.16
* @module
*/
public class RawTiffImageReader extends SpatialImageReader {
/**
* Typical size of a <cite>Image File Directory</cite> (IFD). This is only a hint;
* FIDs can safely be larger than that. The only purpose of this value is to reduce
* the amount of unneeded data to read from the disk.
*/
private static final int IFD_SIZE = 1024;
/**
* Size of data structures in standard TIFF files ({@code SIZE_*}) and in big TIFF files
* ({@code SIZE_BIG_*}). In standard TIFF, the size of structures for counting the number
* of records or the file offsets vary, while in big TIFF everything is 64 bits.
*/
private static final int
SIZE_ENTRY = 12, SIZE_BIG_ENTRY = 20,
SIZE_SHORT = Short.SIZE/Byte.SIZE, SIZE_BIG_SHORT = Long.SIZE/Byte.SIZE,
SIZE_INT = Integer.SIZE/Byte.SIZE, SIZE_BIG_INT = Long.SIZE/Byte.SIZE;
/**
* Types supported by this reader. The type is the short at offset 2 in the directory entry.
*/
private static final short
TYPE_BYTE = 6, TYPE_UBYTE = 1,
TYPE_SHORT = 8, TYPE_USHORT = 3,
TYPE_INT = 9, TYPE_UINT = 4, TYPE_IFD = 13, // IFD is like UINT.
TYPE_LONG = 17, TYPE_ULONG = 16, TYPE_IFD8 = 18, // IFD is like ULONG.
TYPE_FLOAT = 11, TYPE_DOUBLE = 12;
/**
* The size of each type in bytes, or 0 if unknown.
*/
private static final int[] TYPE_SIZE = new int[19];
static {
final int[] size = TYPE_SIZE;
size[TYPE_BYTE] = size[TYPE_UBYTE] = Byte.SIZE / Byte.SIZE;
size[TYPE_SHORT] = size[TYPE_USHORT] = Short.SIZE / Byte.SIZE;
size[TYPE_INT] = size[TYPE_UINT] = size[TYPE_IFD] = Integer.SIZE / Byte.SIZE;
size[TYPE_LONG] = size[TYPE_ULONG] = size[TYPE_IFD8] = Long.SIZE / Byte.SIZE;
size[TYPE_FLOAT] = Float.SIZE / Byte.SIZE;
size[TYPE_DOUBLE] = Double.SIZE / Byte.SIZE;
}
/**
* The channel to the TIFF file. Will be created from the {@linkplain #input} when first needed.
*/
private FileChannel channel;
/**
* The buffer for reading blocks of data.
*/
private final ByteBuffer buffer;
/**
* Position in the {@linkplain #channel} of the first byte in the {@linkplain #buffer}.
*/
private long positionBuffer;
/**
* Current position of the file channel. Stored for avoiding multiple calls to
* {@link FileChannel#position(long)} while reading consecutive block of data.
*/
private long filePosition;
/**
* Positions of each <cite>Image File Directory</cite> (IFD) in this file. The positions are
* fetched when first needed. The number of valid positions is either {@link #countIFD}, or
* the length of the {@code positionIFD} array if {@code countIFD} is negative.
*/
private long[] positionIFD;
/**
* Number of valid elements in {@link #positionIFD}, or -1 if the {@code positionIFD}
* array is now completed. In the later case, the length of {@code positionIFD} is the
* number of images.
*/
private int countIFD;
/**
* {@code true} if the file uses the BigTIFF format, or (@code false} for standard TIFF.
*/
private boolean isBigTIFF;
/**
* Index of the current image, or -1 if none.
*/
private int currentImage;
/**
* Information from the <cite>Directory Entries</cite> for the
* {@linkplain #currentImage current image}.
*/
private int imageWidth, imageHeight, tileWidth, tileHeight, samplesPerPixel;
/**
* Number of bits per samples. This is (8,8,8) for RGB images.
*/
private long[] bitsPerSample;
/**
* The offsets of each tiles in the current image.
*/
private long[] tileOffsets;
/**
* The cached return value of {@link #getRawDataType(int)} for the current image.
*/
private ImageTypeSpecifier rawImageType;
/**
* Creates a new reader.
*
* @param provider The provider, or {@code null} if none.
*/
public RawTiffImageReader(final Spi provider) {
super((provider != null) ? provider : new Spi());
buffer = ByteBuffer.allocateDirect(8196);
positionIFD = new long[4];
currentImage = -1;
}
/**
* Returns {@code true} since this image format places no inherent impediment on random access
* to pixels. Actually, having easy random access is the whole point of uncompressed TIFF files
* in many GIS infrastructures.
*
* @param imageIndex The image index (ignored by this implementation).
* @return Always {@code true} in this implementation.
* @throws IOException If an I/O access was necessary and failed.
*/
@Override
public boolean isRandomAccessEasy(final int imageIndex) throws IOException {
return true;
}
/**
* Ensures that the channel is open. If the channel is already open, then this method
* does nothing.
*
* @throws IllegalStateException if the input is not set.
* @throws IOException If an error occurred while opening the channel.
*/
private void open() throws IllegalStateException, IOException {
if (channel == null) {
if (input == null) {
throw new IllegalStateException(error(Errors.Keys.NoImageInput));
}
final FileInputStream in;
if (input instanceof String) {
in = new FileInputStream((String) input);
} else {
in = new FileInputStream((File) input);
}
channel = in.getChannel();
// Closing the channel will close the input stream.
buffer.clear();
readFully(16, 1024); // Header size of Big TIFF (the standard header size is 8 bytes).
final byte c = buffer.get();
if (c != buffer.get()) {
throw invalidFile("ByteOrder");
}
final ByteOrder order;
if (c == 'M') {
order = ByteOrder.BIG_ENDIAN;
} else if (c == 'I') {
order = ByteOrder.LITTLE_ENDIAN;
} else {
throw invalidFile("ByteOrder");
}
final short version = buffer.order(order).getShort();
if (isBigTIFF = (version == 0x002B)) {
if (buffer.getShort() != 8 || buffer.getShort() != 0) {
throw invalidFile("OffsetSize");
}
} else if (version != 0x002A) {
throw invalidFile("MagicNumber");
}
countIFD = 0;
nextImageFileDirectory();
currentImage = -1;
}
}
/**
* Sets the new buffer position and ensures that the buffer contains at least the given number
* of bytes. Bytes will be read from the channel if needed. If there is not enough remaining
* bytes in the channel, a {@link EOFException} is thrown.
*
* @param position The channel position of the first byte to be read (if needed).
* @param min Minimal amount of bytes that the buffer shall contain.
* @param max Suggested maximal number of bytes. This is only a hint for reducing unneeded read.
* @throws IOException If the given amount of bytes can not be read.
*/
private void ensureBufferContains(long position, final int min, final int max) throws IOException {
final long offset = position - positionBuffer;
if (offset >= 0 && offset < buffer.limit()) {
if (buffer.position((int) offset).remaining() < min) {
final int valid = buffer.compact().position();
if ((position += valid) != filePosition) {
channel.position(position);
}
readFully(min - valid, Math.max(min, max) - valid);
}
} else {
buffer.clear();
if (position != filePosition) {
channel.position(position);
}
readFully(min, Math.max(min, max));
}
}
/**
* Reads at least the given minimal number of bytes (more bytes may be read), but no more
* bytes than the given maximum. The {@linkplain #buffer} position is set to zero and its
* limit is set to the number valid bytes.
*
* @param min Minimal amount of bytes that the buffer shall contain.
* @param max Suggested maximal number of bytes. This is only a hint for reducing unneeded read.
* @throws IOException If the given amount of bytes can not be read.
*/
private void readFully(int min, int max) throws IOException {
max += buffer.position();
if (max < buffer.limit()) {
buffer.limit(max);
}
while (min > 0) {
assert buffer.hasRemaining();
final int n = channel.read(buffer);
if (n < 0) {
throw new EOFException();
}
min -= n;
}
positionBuffer = (filePosition = channel.position()) - buffer.position();
buffer.rewind();
}
/**
* Reads the next bytes in the {@linkplain #buffer}, which must be the 32 or 64 bits
* offset to the next <cite>Image File Directory</cite> (IFD). The offset is then stored
* in the next free slot of {@link #positionIFD}.
*
* @return {@code true} if we found a new IFD, or {@code false} if there is no more IFD.
*/
private boolean nextImageFileDirectory() {
assert countIFD >= 0;
final long position = readInt();
if (position != 0) {
if (countIFD == positionIFD.length) {
positionIFD = Arrays.copyOf(positionIFD, Math.max(4, countIFD*2));
}
positionIFD[countIFD++] = position;
return true;
} else {
positionIFD = ArraysExt.resize(positionIFD, countIFD);
countIFD = -1;
return false;
}
}
/**
* Reads the {@code int} or {@code long} value (depending if the file is
* standard of big TIFF) at the current {@linkplain #buffer} position.
*
* @return The next integer.
*/
private long readInt() {
return isBigTIFF ? buffer.getLong() : buffer.getInt() & 0xFFFFFFFFL;
}
/**
* Reads the {@code short} or {@code long} value (depending if the file is
* standard of big TIFF) at the current {@linkplain #buffer} position.
*
* @return The next short.
*/
private long readShort() {
return isBigTIFF ? buffer.getLong() : buffer.getShort() & 0xFFFFL;
}
/**
* Returns the number of images available from the current input file. This method
* will scan the file the first time it is invoked with a {@code true} argument value.
*/
@Override
public int getNumImages(boolean allowSearch) throws IOException {
open(); // Does nothing if already open.
if (countIFD >= 0) { // Should never be 0 actually.
if (!allowSearch) {
return -1;
}
final int entrySize, shortSize, intSize;
if (isBigTIFF) {
entrySize = SIZE_BIG_ENTRY;
shortSize = SIZE_BIG_SHORT;
intSize = SIZE_BIG_INT;
} else {
entrySize = SIZE_ENTRY;
shortSize = SIZE_SHORT;
intSize = SIZE_INT;
}
do {
long position = positionIFD[countIFD - 1];
ensureBufferContains(position, shortSize, IFD_SIZE);
final long n = readShort();
position += shortSize;
ensureBufferContains(position + n * entrySize, intSize, IFD_SIZE);
} while (nextImageFileDirectory());
}
return positionIFD.length;
}
/**
* Selects the image at the given index.
*
* @param imageIndex The index of the image to make the current one.
* @throws IOException If an error occurred while reading the file.
* @throws IndexOutOfBoundsException If the given image index is out of bounds.
*/
private void selectImage(final int imageIndex) throws IOException, IndexOutOfBoundsException {
if (imageIndex != currentImage) {
open(); // Does nothing if already open.
if (imageIndex >= minIndex) {
final int entrySize, shortSize, intSize;
if (isBigTIFF) {
entrySize = SIZE_BIG_ENTRY;
shortSize = SIZE_BIG_SHORT;
intSize = SIZE_BIG_INT;
} else {
entrySize = SIZE_ENTRY;
shortSize = SIZE_SHORT;
intSize = SIZE_INT;
}
/*
* If the requested image if after the last image read,
* scan the file until we find the IFD location.
*/
if (countIFD >= 0) { // Should never be 0 actually.
int imageAhead = imageIndex - countIFD;
while (imageAhead >= 0) {
long position = positionIFD[countIFD - 1];
ensureBufferContains(position, shortSize, IFD_SIZE);
final long n = readShort();
position += shortSize;
ensureBufferContains(position + n * entrySize, intSize, IFD_SIZE);
if (!nextImageFileDirectory()) {
throw new IndexOutOfBoundsException(error(
Errors.Keys.IndexOutOfBounds_1, imageIndex));
}
imageAhead--;
}
}
/*
* Read the Image File Directory (IFD) content.
*/
if (imageIndex < positionIFD.length) {
imageWidth = -1;
imageHeight = -1;
tileWidth = -1;
tileHeight = -1;
samplesPerPixel = 0;
bitsPerSample = null;
tileOffsets = null;
rawImageType = null;
final Collection<long[]> deferred = new ArrayList<>(4);
long position = positionIFD[imageIndex];
ensureBufferContains(position, shortSize + intSize, IFD_SIZE);
final long n = readShort();
position += shortSize;
for (int i=0; i<n; i++) {
ensureBufferContains(position, entrySize + intSize, IFD_SIZE);
parseDirectoryEntries(deferred);
position += entrySize;
}
/*
* Complete the arrays that needs further processing.
*/
readDeferredArrays(deferred.toArray(new long[deferred.size()][]));
/*
* Declare the image as valid only if the mandatory information are present.
*/
ensureDefined(imageWidth, "imageWidth");
ensureDefined(imageHeight, "imageHeight");
ensureDefined(samplesPerPixel, "samplesPerPixel");
if (true) {
ensureDefined(tileWidth, "tileWidth");
ensureDefined(tileHeight, "tileHeight");
ensureDefined(tileOffsets, "tileOffsets");
}
currentImage = imageIndex;
return;
}
}
throw new IndexOutOfBoundsException(error(Errors.Keys.IndexOutOfBounds_1, imageIndex));
}
}
/**
* Ensures that the given value is positive.
*
* @param value The value which must be positive.
* @param name The name for the parameter value, to be used in case of error.
* @param locale The locale to use for formatting the error message.
* @throws IIOException If the given value is considered undefined.
*/
private void ensureDefined(final int value, final String name) throws IIOException {
if (value < 0) {
throw new IIOException(error(Errors.Keys.NoSuchElementName_1, name));
}
}
/**
* Ensures that the given array is non-null.
*
* @param value The value which must be non-null.
* @param name The name for the parameter value, to be used in case of error.
* @param locale The locale to use for formatting the error message.
* @throws IIOException If the given value is considered undefined.
*/
private void ensureDefined(final long[] value, final String name) throws IIOException {
if (value == null) {
throw new IIOException(error(Errors.Keys.NoSuchElementName_1, name));
}
}
/**
* Parses the content of the directory entry at the current {@linkplain #buffer} position.
* The {@linkplain #buffer} is assumed to have all the required bytes for one entry. The
* buffer position is undetermined after this method call.
*
* @throws IIOException If an error occurred while parsing an entry.
*/
private void parseDirectoryEntries(final Collection<long[]> deferred) throws IIOException {
final short id = buffer.getShort();
switch (id) {
case 0x0100: imageWidth = entryValue ("imageWidth"); break;
case 0x0101: imageHeight = entryValue ("imageHeight"); break;
case 0x0102: bitsPerSample = entryValues("bitsPerSample", deferred); break;
case 0x0115: samplesPerPixel = entryValue ("samplesPerPixel"); break;
case 0x0142: tileWidth = entryValue ("tileWidth"); break;
case 0x0143: tileHeight = entryValue ("tileHeight"); break;
case 0x0144: tileOffsets = entryValues("tileOffsets", deferred); break;
case 0x011C: { // PlanarConfiguration.
final int planarConfiguration = entryValue("PlanarConfiguration");
if (planarConfiguration != 1) { // '1' stands for "chunky", 2 for "planar".
throw new UnsupportedImageFormatException(error(Errors.Keys.IllegalParameterValue_2,
"planarConfiguration", planarConfiguration));
}
break;
}
case 0x0106: { // PhotometricInterpretation.
final int photometricInterpretation = entryValue("photometricInterpretation");
if (photometricInterpretation != 2) { // '2' stands for RGB.
throw new UnsupportedImageFormatException(error(Errors.Keys.IllegalParameterValue_2,
"photometricInterpretation", photometricInterpretation));
}
break;
}
case 0x0103: { // Compression.
final int compression = entryValue("compression");
if (compression != 1) { // '1' stands for "uncompressed".
final Object name;
switch (compression) {
case 6: name = "JPEG"; break;
case 7: name = "LZW"; break;
default: name = compression; break;
}
throw new UnsupportedImageFormatException(error(Errors.Keys.IllegalParameterValue_2,
"compression", name));
}
break;
}
}
}
/**
* Reads one value of the given type from the given buffer.
* This method assumes that the type is valid.
*
* @param type The data type.
* @return The value.
*/
private long read(final short type) throws IIOException {
switch (type) {
case TYPE_BYTE: return buffer.get();
case TYPE_UBYTE: return buffer.get() & 0xFFL;
case TYPE_SHORT: return buffer.getShort();
case TYPE_USHORT: return buffer.getShort() & 0xFFFFL;
case TYPE_INT: return buffer.getInt();
case TYPE_IFD:
case TYPE_UINT: return buffer.getInt() & 0xFFFFFFFFL;
case TYPE_LONG: return buffer.getLong();
case TYPE_IFD8:
case TYPE_ULONG: {
final long value = buffer.getLong();
if (value < 0) {
throw new UnsupportedImageFormatException(error(Errors.Keys.UnsupportedDataType));
}
return value;
}
default: throw new AssertionError(type);
}
}
/**
* Reads a value from the directory entry. This method can be invoked right after the entry ID.
* The {@linkplain #buffer} is assumed to have all the required bytes for one entry. The buffer
* position is undetermined after this method call.
*
* @param name The name of the entry being parsed.
* @return The entry value as an integer.
* @throws IIOException If the entry can not be read as an integer.
*/
private int entryValue(final String name) throws IIOException {
final short type = buffer.getShort();
if (readInt() != 1) {
throw new IIOException(error(Errors.Keys.DuplicatedValue_1, name));
}
switch (type) {
case TYPE_BYTE: return buffer.get();
case TYPE_UBYTE: return buffer.get() & 0xFF;
case TYPE_SHORT: return buffer.getShort();
case TYPE_USHORT: return buffer.getShort() & 0xFFFF;
case TYPE_INT:
case TYPE_UINT: return buffer.getInt();
default: throw new IIOException(error(Errors.Keys.IllegalParameterType_2, name, type));
}
}
/**
* Reads many values from the director entry. This method can be invoked right after the entry
* ID. The {@linkplain #buffer} is assumed to have all the required bytes for one entry. The
* buffer position is undetermined after this method call.
* <p>
* Note that the returned array may need further processing after this method call, in which
* case it is added to the {@code deferred} collection.
*
* @param name The name of the entry being parsed.
* @param deferred A collection where to add arrays for which the reading has been deferred.
* @return The array of values (need further processing if added in the {@code deferred}) collection).
* @throws IIOException If the entry can not be read as an integer.
*/
private long[] entryValues(final String name, final Collection<long[]> deferred) throws IIOException {
final short type = buffer.getShort();
final long count = readInt();
if (count > Integer.MAX_VALUE) {
throw new IIOException(error(Errors.Keys.FileHasTooManyData));
}
final int dataSize, intSize = isBigTIFF ? SIZE_BIG_INT : SIZE_INT;
if (type < 0 || type == TYPE_FLOAT || type == TYPE_DOUBLE || (dataSize = TYPE_SIZE[type]) == 0) {
throw new IIOException(error(Errors.Keys.IllegalParameterType_2, name, type));
}
final long[] values = new long[(int) count];
if (values.length * dataSize <= intSize) {
for (int i=0; i<values.length; i++) {
values[i] = read(type);
}
} else {
values[0] = readInt();
values[1] = type;
deferred.add(values);
}
return values;
}
/**
* To be invoked after {@link #entryValues(String, Collection)} in order to process all
* the deferred arrays. This method tries to read the arrays in sequential order as much
* as possible.
*
* @param deferred The deferred arrays.
* @throws IOException If an I/O operation was required and failed.
*/
private void readDeferredArrays(final long[][] deferred) throws IOException {
Arrays.sort(deferred, OFFSET_COMPARATOR);
for (final long[] array : deferred) {
final short type = (short) array[1];
assert type == array[1]; // Ensure that the value is in the range of the short type.
final int dataSize = TYPE_SIZE[type];
long position = array[0];
for (int i=0; i<array.length;) {
int n = array.length - i;
final int length = n * dataSize;
ensureBufferContains(position, Math.min(length, buffer.capacity()), Math.max(length, 1024));
n = Math.min(n, buffer.remaining() / dataSize);
position += n * dataSize;
while (--n >= 0) {
array[i++] = read(type);
}
}
}
}
/**
* Comparator used for sorting the array to be processed by {@link #readDeferredArrays(long[][])},
* in order to read the data sequentially from the disk.
*/
private static final Comparator<long[]> OFFSET_COMPARATOR = new Comparator<long[]>() {
@Override public int compare(final long[] o1, final long[] o2) {
return Long.signum(o1[0] - o2[0]);
}
};
/**
* Returns the number of bands available for the specified image.
*
* @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.
*/
@Override
public int getNumBands(final int imageIndex) throws IOException {
selectImage(imageIndex);
return samplesPerPixel;
}
/**
* Returns the width of the image at the given index.
*
* @param imageIndex the index of the image to be queried.
* @return The width of the given image.
* @throws IOException If an error occurred while reading the file.
*/
@Override
public int getWidth(final int imageIndex) throws IOException {
selectImage(imageIndex);
return imageWidth;
}
/**
* Returns the height of the image at the given index.
*
* @param imageIndex the index of the image to be queried.
* @return The height of the given image.
* @throws IOException If an error occurred while reading the file.
*/
@Override
public int getHeight(int imageIndex) throws IOException {
selectImage(imageIndex);
return imageHeight;
}
/**
* Returns the width of the tiles in the given image.
*
* @param imageIndex the index of the image to be queried.
* @return The width of the tile in the given image.
* @throws IOException If an error occurred while reading the file.
*/
@Override
public int getTileWidth(final int imageIndex) throws IOException {
selectImage(imageIndex);
return (tileWidth >= 0) ? tileWidth : imageWidth;
}
/**
* Returns the height of the tiles in the given image.
*
* @param imageIndex the index of the image to be queried.
* @return The height of the tile in the given image.
* @throws IOException If an error occurred while reading the file.
*/
@Override
public int getTileHeight(int imageIndex) throws IOException {
selectImage(imageIndex);
return (tileHeight >= 0) ? tileHeight : imageHeight;
}
/**
* Returns {@code true} if the image is organized into tiles.
*
* @param imageIndex the index of the image to be queried.
* @return {@code true} if the image is organized into tiles.
* @throws IOException If an error occurred while reading the file.
*/
@Override
public boolean isImageTiled(final int imageIndex) throws IOException {
selectImage(imageIndex);
return (tileWidth >= 0) && (tileHeight >= 0);
}
/**
* Information about a tile. The inherited {@link Rectangle} contains the coordinate of
* the region to write in the target image, <em>relative to the upper-left pixel to be
* written (i.e. the upper-left pixel in the set of tiles returned by {@link #getTiles}
* is located at (0,0) by definition).
* <p>
* Tiles can be sorted in ascending order of file positions, for sequential access.
* Note: the {@code compareTo} method is inconsistent with the {@code equals} method.
*/
@SuppressWarnings("serial")
private static final class Tile extends Rectangle implements Comparable<Tile> {
/** Tile position in file. */ final long position;
/** Creates a new instance for the given position in file and target region. */
Tile(final int x, final int y, final int width, final int height, final long position) {
super(x, y, width, height);
this.position = position;
assert !isEmpty() && x >= 0 && y >= 0: this;
}
/** Compares this tile with the specified tile for order of file position. */
@Override public int compareTo(final Tile other) {
return Long.signum(position - other.position);
}
}
/**
* Returns the tiles in the given source region. The tiles are sorted by increasing file
* position, in order to perform sequential file access as much as possible.
*
* @param r The source region requested by the user.
* @param pixelStride Number of bytes in each sample value in the source file.
* @param scanlineStride Number of bytes in each row of the tile in the source file.
* @return The tiles that intersect the given region.
*/
private Tile[] getTiles(final Rectangle r, final int xSubsampling, final int ySubsampling,
final int pixelStride, final int scanlineStride)
{
final int minTileX = r.x / tileWidth; // Inclusive
final int minTileY = r.y / tileHeight; // Inclusive
final int maxTileX = (r.x + r.width + tileWidth - 2) / tileWidth; // Exclusive
final int maxTileY = (r.y + r.height + tileHeight - 2) / tileHeight; // Exclusive
final int rowLength = (imageWidth + tileWidth - 1) / tileWidth;
final Tile[] tiles = new Tile[(maxTileX - minTileX) * (maxTileY - minTileY)];
int count = 0;
for (int tileY=minTileY; tileY<maxTileY; tileY++) {
final int ySource = tileY * tileHeight - r.y;
final int y = (Math.max(0, ySource) + ySubsampling - 1) / ySubsampling;
final int height = (Math.min(r.height, ySource + tileHeight) - 1) / ySubsampling + 1 - y;
final int rowBase = (y * ySubsampling - ySource) * scanlineStride;
for (int tileX=minTileX; tileX<maxTileX; tileX++) {
final int xSource = tileX * tileWidth - r.x;
final int x = (Math.max(0, xSource) + xSubsampling - 1) / xSubsampling;
final int width = (Math.min(r.width, xSource + tileWidth) - 1) / xSubsampling + 1 - x;
final int offset = (x * xSubsampling - xSource) * pixelStride + rowBase;
final int tileIndex = tileY * rowLength + tileX;
tiles[count++] = new Tile(x, y, width, height, tileOffsets[tileIndex] + offset);
}
}
assert count == tiles.length;
Arrays.sort(tiles);
return tiles;
}
/**
* Returns {@code true} since TIFF images have color palette.
*/
@Override
public boolean hasColors(final int imageIndex) throws IOException {
selectImage(imageIndex);
return true;
}
/**
* Returns the data type which most closely represents the "raw" internal data of the image.
* The default implementation is as below:
*
* {@preformat java
* return getRawImageType(imageIndex).getSampleModel().getDataType();
* }
*
* @param imageIndex The index of the image to be queried.
* @return The data type (typically {@link DataBuffer#TYPE_BYTE}).
* @throws IOException If an error occurs reading the format information from the input source.
*/
@Override
protected int getRawDataType(final int imageIndex) throws IOException {
return getRawImageType(imageIndex).getSampleModel().getDataType();
}
/**
* Returns the {@link SampleModel} and {@link ColorModel} which most closely represents the
* internal format of the image.
*
* @param imageIndex The index of the image to be queried.
* @return The internal format of the image.
* @throws IOException If an error occurs reading the format information from the input source.
*/
@Override
public ImageTypeSpecifier getRawImageType(final int imageIndex) throws IOException {
selectImage(imageIndex);
if (rawImageType == null) {
final ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
final int type;
final int[] bits;
final long[] bitsPerSample = this.bitsPerSample;
if (bitsPerSample != null) {
int size = 0;
bits = new int[bitsPerSample.length];
for (int i=0; i<bits.length; i++) {
final long b = bitsPerSample[i];
if ((bits[i] = (int) b) != b) {
// Verify that 'bitPerSample' values are inside 'int' range (paranoiac check).
throw new UnsupportedImageFormatException(error(
Errors.Keys.IllegalParameterValue_2, "bitsPerSample", b));
}
if (i != 0 && b != size) {
// Current implementation requires all sample values to be of the same size.
throw new UnsupportedImageFormatException(error(Errors.Keys.InconsistentValue));
}
size = (int) b;
}
/*
* We require exact value, because the reading process read all sample values
* in one contiguous read operation.
*/
switch (size) {
case Byte .SIZE: type = DataBuffer.TYPE_BYTE; break;
case Short .SIZE: type = DataBuffer.TYPE_USHORT; break;
case Integer.SIZE: type = DataBuffer.TYPE_INT; break;
default: {
throw new UnsupportedImageFormatException(error(
Errors.Keys.IllegalParameterValue_2, "bitsPerSample", size));
}
}
} else {
/*
* If the bitsPerSample field were not specified, assume bytes.
*/
type = DataBuffer.TYPE_BYTE;
bits = new int[(samplesPerPixel != 0) ? samplesPerPixel : cs.getNumComponents()];
Arrays.fill(bits, 8);
}
final boolean hasAlpha = bits.length > cs.getNumComponents();
final ColorModel cm = new ComponentColorModel(cs, bits, hasAlpha, false,
hasAlpha ? Transparency.TRANSLUCENT : Transparency.OPAQUE, type);
rawImageType = new ImageTypeSpecifier(cm, cm.createCompatibleSampleModel(imageWidth, imageHeight));
}
return rawImageType;
}
/**
* 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
* only the {@linkplain #getRawImageType(int) raw image type}.
*
* @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 {
return Collections.singleton(getRawImageType(imageIndex)).iterator();
}
/**
* Reads the image at the given index.
*
* @param imageIndex The index of the image to read.
* @param param Parameters used to control the reading process, or {@code null}.
* @return The image.
* @throws IOException If an error occurred while reading the image.
*/
@Override
public BufferedImage read(final int imageIndex, final ImageReadParam param) throws IOException {
selectImage(imageIndex);
final BufferedImage image = getDestination(param, getImageTypes(imageIndex), imageWidth, imageHeight);
final Rectangle srcRegion = new Rectangle();
final Rectangle dstRegion = new Rectangle();
computeRegions(param, imageWidth, imageHeight, image, srcRegion, dstRegion);
read(image.getRaster(), param, srcRegion, dstRegion);
return image;
}
/**
* Processes to the image reading, and stores the pixels in the given raster.
*
* @param raster The raster where to store the pixel values.
* @param param Parameters used to control the reading process, or {@code null}.
* @param srcRegion The region to read in source image.
* @param dstRegion The region to write in the given raster.
* @throws IOException If an error occurred while reading the image.
*/
private void read(final WritableRaster raster, final ImageReadParam param,
final Rectangle srcRegion, final Rectangle dstRegion) throws IOException
{
clearAbortRequest();
final int numBands = raster.getNumBands();
checkReadParamBandSettings(param, samplesPerPixel, numBands);
final int[] sourceBands;
final int[] destinationBands;
final int sourceXSubsampling;
final int sourceYSubsampling;
if (param != null) {
sourceBands = param.getSourceBands();
destinationBands = param.getDestinationBands();
sourceXSubsampling = param.getSourceXSubsampling();
sourceYSubsampling = param.getSourceYSubsampling();
} else {
sourceBands = null;
destinationBands = null;
sourceXSubsampling = 1;
sourceYSubsampling = 1;
}
if (sourceBands != null || destinationBands != null) {
throw new IIOException("Source and target bands not yet supported.");
}
final DataBuffer dataBuffer = raster.getDataBuffer();
final int[] bankOffsets = dataBuffer.getOffsets();
final int dataType = dataBuffer.getDataType();
final int sampleSize = DataBuffer.getDataTypeSize(dataType) / Byte.SIZE;
final int sourcePixelStride = sampleSize * samplesPerPixel;
final int targetPixelStride = sampleSize * numBands;
final int sourceScanlineStride = sourcePixelStride * tileWidth;
final int targetScanlineStride = SampleModels.getScanlineStride(raster.getSampleModel());
/*
* Get a view of the ByteBuffer as a NIO Buffer of the appropriate type.
* The buffer is cleared first because the 'sourceBuffer' capacity will
* be set to this buffer limit.
*/
final Buffer sourceBuffer;
if (true) { // For keep position and limit variables locale.
final int position = buffer.position();
final int limit = buffer.limit();
buffer.clear();
switch (dataType) {
case DataBuffer.TYPE_BYTE: sourceBuffer = buffer.duplicate(); break;
case DataBuffer.TYPE_USHORT:
case DataBuffer.TYPE_SHORT: sourceBuffer = buffer.asShortBuffer(); break;
case DataBuffer.TYPE_INT: sourceBuffer = buffer.asIntBuffer(); break;
case DataBuffer.TYPE_FLOAT: sourceBuffer = buffer.asFloatBuffer(); break;
case DataBuffer.TYPE_DOUBLE: sourceBuffer = buffer.asDoubleBuffer(); break;
default: throw new IIOException(error(Errors.Keys.UnsupportedDataType, dataBuffer.getClass()));
}
buffer.limit(limit).position(position);
}
/*
* In current implementation, we support only one bank (in TIFF terminology: "chunky format").
* However we loop over all banks as a matter of principle, in anticipation of a future version
* that may support the "planar format".
*/
final int sourceXSubsamplingStride = sourceXSubsampling * sourcePixelStride;
for (int bank=0; bank<bankOffsets.length; bank++) {
/*
* Get the underlying array of the image DataBuffer in which to write the data.
*/
final Object targetArray;
switch (dataType) {
case DataBuffer.TYPE_BYTE: targetArray = ((DataBufferByte) dataBuffer).getData(bank); break;
case DataBuffer.TYPE_USHORT: targetArray = ((DataBufferUShort) dataBuffer).getData(bank); break;
case DataBuffer.TYPE_SHORT: targetArray = ((DataBufferShort) dataBuffer).getData(bank); break;
case DataBuffer.TYPE_INT: targetArray = ((DataBufferInt) dataBuffer).getData(bank); break;
case DataBuffer.TYPE_FLOAT: targetArray = ((DataBufferFloat) dataBuffer).getData(bank); break;
case DataBuffer.TYPE_DOUBLE: targetArray = ((DataBufferDouble) dataBuffer).getData(bank); break;
default: throw new AssertionError(dataType);
}
/*
* Iterate over the tiles to read, in sequential file access order (which is not
* necessarily the same than tile indices order). The rectangles inherited from
* the tiles are the coordinates where to write in the target image.
*/
final int targetImageStart = bankOffsets[bank] + targetScanlineStride * dstRegion.y + targetPixelStride * dstRegion.x;
for (final Tile tile : getTiles(srcRegion, sourceXSubsampling, sourceYSubsampling, sourcePixelStride, sourceScanlineStride)) {
/*
* Constants used for the iterations.
*/
final int targetTileStart = targetImageStart + targetScanlineStride * tile.y + targetPixelStride * tile.x;
final int numSourceBytesToRead =
(tile.height-1) * sourceScanlineStride * sourceYSubsampling +
(tile.width -1) * sourcePixelStride * sourceXSubsampling + sourcePixelStride;
int numTargetPixelsPerRow = tile.width;
/*
* If the data to read and the data to write are contiguous, read and write the
* pixels in one single pass. We will do that by pretending that all the data to
* read is like a single line.
*/
if (tile.width * targetPixelStride == targetScanlineStride &&
tile.width * sourcePixelStride == sourceScanlineStride &&
sourceYSubsampling == 1)
{
numTargetPixelsPerRow *= tile.height;
}
/*
* Count the number of pixels that remain to be read for a particular row.
* This values will be decremented during the iteration until it reach 0,
* then reinitialized for a new row.
*/
int remainingRowPixels = numTargetPixelsPerRow;
/*
* Initialize the position in the file from which to read data, and position in
* the 'targetArray' where to write the first sample value for the current tile.
*/
int row = 0;
int sourcePosition = 0;
int targetPosition = targetTileStart;
while (sourcePosition < numSourceBytesToRead) {
/*
* Following assertion fails if we did not computed correctly the 'n' value in
* this loop (see further below). Should never fail since 'n' was computed as
* Math.min(remainingRowPixels, ...).
*/
assert (remainingRowPixels >= 0) : remainingRowPixels;
if (remainingRowPixels == 0) {
/*
* If we finished reading all sample values for the current row, move
* the position to the begining of the next row and ensure that the
* source buffer contains at least one pixel.
*/
remainingRowPixels = numTargetPixelsPerRow;
sourcePosition = ++row * sourceScanlineStride * sourceYSubsampling;
targetPosition = row * targetScanlineStride + targetTileStart;
ensureBufferContains(tile.position + sourcePosition,
sourcePixelStride, numSourceBytesToRead - sourcePosition);
} else {
/*
* If the buffer is empty (see the comment at the end of this loop for an
* explanation why the buffer is empty), read sample values from the source
* file. We try to fill the buffer completly if possible for efficiency, but
* the algorithm is tolerant to partial filling.
*/
final int remainingSourceBytesCount = numSourceBytesToRead - sourcePosition;
ensureBufferContains(tile.position + sourcePosition,
Math.min(remainingSourceBytesCount, buffer.capacity()),
remainingSourceBytesCount);
}
/*
* Compute the position in the view buffer. The byte buffer position
* must be aligned on a sample value boundary.
*/
assert (buffer.position() % sampleSize) == 0 : buffer;
int bufferPosition = buffer.position() / sampleSize;
/*
* Compute the number of pixels that we can copy for the current row.
* The second argument of the Math.min(...) method call is a compact
* form (only one division) of the following steps:
*
* numSourcePixels = (buffer.remaining() / sourcePixelStride) rounded toward 0.
* numSubsampled = (numSourcePixels / sourceXSubsampling) rounded toward upper.
*/
int n = Math.min(remainingRowPixels, (buffer.remaining() +
sourceXSubsamplingStride - sourcePixelStride) / sourceXSubsamplingStride);
/*
* Update the positions now, as if the copy operations were already completed.
*/
remainingRowPixels -= n;
sourcePosition += (n * sourceXSubsamplingStride);
/*
* We will copy the pixel values in the target array using a fast bulk method
* if possible, or a slow loop if we need to apply a subsampling on the fly.
*/
final int sourceStep, targetStep;
if (sourceXSubsampling == 1) {
sourceStep = targetStep = n * numBands;
n = 1;
} else {
sourceStep = numBands * sourceXSubsampling;
targetStep = numBands;
}
do {
sourceBuffer.position(bufferPosition);
switch (dataType) {
case DataBuffer.TYPE_BYTE: ((ByteBuffer) sourceBuffer).get((byte[]) targetArray, targetPosition, targetStep); break;
case DataBuffer.TYPE_USHORT:
case DataBuffer.TYPE_SHORT: ((ShortBuffer) sourceBuffer).get((short[]) targetArray, targetPosition, targetStep); break;
case DataBuffer.TYPE_INT: ((IntBuffer) sourceBuffer).get((int[]) targetArray, targetPosition, targetStep); break;
case DataBuffer.TYPE_FLOAT: ((FloatBuffer) sourceBuffer).get((float[]) targetArray, targetPosition, targetStep); break;
case DataBuffer.TYPE_DOUBLE: ((DoubleBuffer) sourceBuffer).get((double[]) targetArray, targetPosition, targetStep); break;
default: throw new AssertionError(dataType);
}
bufferPosition += sourceStep;
targetPosition += targetStep;
} while (--n != 0);
/*
* At this point, either we have read a full row (in which case the buffer position
* will be set by the first call to 'ensureBufferContains' in this loop), or either
* there is not enough remaining data, in which case the buffer position will be set
* by the second call to 'ensureBufferContains' in this loop. Consequently there is
* no need to set the buffer position explicitly here.
*/
}
}
}
}
/**
* Formats an error message for an invalid TIFF file.
*
* @todo Localize.
*/
private IIOException invalidFile(final String cause) {
return new IIOException("Invalid value for record " + cause);
}
/**
* Formats an error message with no argument.
*/
private String error(final short key) {
return Errors.getResources(getLocale()).getString(key);
}
/**
* Formats an error message with one argument.
*/
private String error(final short key, final Object arg0) {
return Errors.getResources(getLocale()).getString(key, arg0);
}
/**
* Formats an error message with two argument.
*/
private String error(final short key, final Object arg0, final Object arg1) {
return Errors.getResources(getLocale()).getString(key, arg0, arg1);
}
/**
* Closes the file channel. If the channel is already closed, then this method does nothing.
*
* @throws IOException If an error occurred while closing the channel.
*/
@Override
protected void close() throws IOException {
super.close();
positionBuffer = 0;
filePosition = 0;
countIFD = 0;
currentImage = -1;
bitsPerSample = null;
tileOffsets = null;
rawImageType = null;
if (channel != null) {
channel.close();
channel = null;
// Keep the buffer, since we may reuse it for the next image.
}
}
/**
* Service provider interface (SPI) for {@code RawTiffImageReader}s. This SPI provides
* necessary implementation for creating default {@link RawTiffImageReader} instances.
* <p>
* The default constructor initializes the fields to the values listed below.
* Users wanting different values should create a subclass of {@code Spi} and
* set the desired values in their constructor.
* <p>
* <table border="1" cellspacing="0">
* <tr bgcolor="lightblue"><th>Field</th><th>Value</th></tr>
* <tr><td> {@link #names} </td><td> {@code "tiff"} </td></tr>
* <tr><td> {@link #MIMETypes} </td><td> {@code "image/tiff"} </td></tr>
* <tr><td> {@link #pluginClassName} </td><td> {@code "org.geotoolkit.image.io.plugin.RawTiffImageReader"} </td></tr>
* <tr><td> {@link #vendorName} </td><td> {@code "Geotoolkit.org"} </td></tr>
* <tr><td> {@link #version} </td><td> Value of {@link org.geotoolkit.util.Version#GEOTOOLKIT} </td></tr>
* </table>
* <p>
* By default, this provider register itself <em>after</em> the provider supplied by the
* <cite>Image I/O extension for JAI</cite>, because the later supports a wider range of
* formats. See {@link #onRegistration onRegistration} for more information.
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.16
*
* @since 3.16
* @module
*/
public static class Spi extends SpatialImageReader.Spi implements SystemOverride {
/**
* Default list of file extensions.
*/
private static final String[] SUFFIXES = new String[] {"tiff", "tif"};
/**
* The mime types for the {@link RawTiffImageReader}.
*/
private static final String[] MIME_TYPES = {"image/tiff"};
/**
* The list of valid input types.
*/
private static final Class<?>[] INPUT_TYPES = new Class<?>[] {
File.class, String.class
};
/**
* Constructs a default {@code RawTiffImageReader.Spi}. The fields are initialized as
* documented in the <a href="#skip-navbar_top">class javadoc</a>. Subclasses can
* modify those values if desired.
* <p>
* For efficiency reasons, the fields are initialized to shared arrays.
* Subclasses can assign new arrays, but should not modify the default array content.
*/
public Spi() {
names = SUFFIXES;
suffixes = SUFFIXES;
inputTypes = INPUT_TYPES;
MIMETypes = MIME_TYPES;
pluginClassName = "org.geotoolkit.image.io.plugin.RawTiffImageReader";
// Current implementation does not support metadata.
nativeStreamMetadataFormatName = null;
nativeImageMetadataFormatName = null;
}
/**
* Returns a brief, human-readable description of this service provider
* and its associated implementation. The resulting string should be
* localized for the supplied locale, if possible.
*
* @param locale A Locale for which the return value should be localized.
* @return A String containing a description of this service provider.
*/
@Override
public String getDescription(final Locale locale) {
return "TIFF image reader";
}
/**
* Current implementation returns {@code false} in every case. Future implementation
* may perform a better check if the {@link RawTiffImageReader} become less restrictive.
*
* @param source The input source to be decoded.
* @return {@code true} if the given source can be used by {@link RawTiffImageReader}.
* @throws IOException if an I/O error occurs while reading the stream.
*/
@Override
public boolean canDecodeInput(final Object source) throws IOException {
return false;
}
/**
* Returns an instance of the {@code ImageReader} implementation associated
* with this service provider.
*
* @param extension An optional extension object, which may be null.
* @return An image reader instance.
* @throws IOException if the attempt to instantiate the reader fails.
*/
@Override
public ImageReader createReaderInstance(final Object extension) throws IOException {
return new RawTiffImageReader(this);
}
/**
* Invoked when this Service Provider is registered. By default, this method
* {@linkplain ServiceRegistry#setOrdering(Class, Object, Object) sets the ordering}
* of this {@code RawTiffImageReader.Spi} after the one provided in <cite>Image I/O
* extension for JAI</cite>. This behavior can be changed by setting the
* <code>{@value org.geotoolkit.lang.SystemOverride#KEY_ALLOW_OVERRIDE}</code>
* system property explicitly to {@code true}.
* <p>
* Note that the Geotk TIFF image reader will be selected only if the source given to the
* {@link #canDecodeInput(Object)} method is compliant with the restrictions documented
* in {@link RawTiffImageReader} javadoc, otherwise the standard TIFF image reader will
* be selected instead.
*
* @param registry The registry where is service is registered.
* @param category The category for which this service is registered.
*/
@Override
public void onRegistration(final ServiceRegistry registry, final Class<?> category) {
super.onRegistration(registry, category);
if (category.equals(ImageReaderSpi.class)) {
for (Iterator<ImageReaderSpi> it = registry.getServiceProviders(ImageReaderSpi.class, false); it.hasNext();) {
ImageReaderSpi other = it.next();
if (other != this && ArraysExt.contains(other.getFormatNames(), "tiff")) {
ImageReaderSpi last = this;
try {
if (Boolean.getBoolean(KEY_ALLOW_OVERRIDE)) {
last = other;
other = this;
}
} catch (SecurityException e) {
Logging.recoverableException(null, Spi.class, "onRegistration", e);
}
registry.setOrdering(ImageReaderSpi.class, other, last);
}
}
}
}
}
}