/* Copyright (C) 2001, 2008 United States Government as represented by
the Administrator of the National Aeronautics and Space Administration.
All Rights Reserved.
*/
package gov.nasa.worldwind.formats.tiff;
import java.awt.Point;
import java.awt.Transparency;
import java.awt.color.ColorSpace;
import java.awt.image.BandedSampleModel;
import java.awt.image.BufferedImage;
import java.awt.image.ComponentColorModel;
import java.awt.image.ComponentSampleModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.PixelInterleavedSampleModel;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.io.IOException;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Iterator;
import javax.imageio.IIOException;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
/**
*
* @author brownrigg
* @version $Id$
*/
public class GeotiffImageReader extends ImageReader {
public GeotiffImageReader(ImageReaderSpi provider) {
super(provider);
}
@Override
public int getNumImages(boolean allowSearch) throws IOException {
// TODO: This should allow for multiple images that may be present. For now, we'll ignore all but first.
return 1;
}
@Override
public int getWidth(int imageIndex) throws IOException {
if (imageIndex < 0 || imageIndex >= getNumImages(true))
throw new IllegalArgumentException(this.getClass().getName() + ".getWidth(): illegal imageIndex: " + imageIndex);
TiffIFDEntry widthEntry = getByTag(ifds.get(imageIndex), TiffTags.IMAGE_WIDTH);
return (int)widthEntry.asLong();
}
@Override
public int getHeight(int imageIndex) throws IOException {
if (imageIndex < 0 || imageIndex >= getNumImages(true))
throw new IllegalArgumentException(this.getClass().getName() + ".getHeight(): illegal imageIndex: " + imageIndex);
TiffIFDEntry heightEntry = getByTag(ifds.get(imageIndex), TiffTags.IMAGE_LENGTH);
return (int)heightEntry.asLong();
}
@Override
public Iterator<ImageTypeSpecifier> getImageTypes(int imageIndex) throws IOException {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public IIOMetadata getStreamMetadata() throws IOException {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public IIOMetadata getImageMetadata(int imageIndex) throws IOException {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public BufferedImage read(int imageIndex, ImageReadParam param) throws IOException {
// TODO: For this first implementation, we are completly ignoring the ImageReadParam given to us.
// Our target functionality is not the entire ImageIO, but only that needed to support the static
// read method ImageIO.read("myImage.tif").
// TODO: more generally, the following test should reflect that more than one image is possible in a Tiff.
if (imageIndex != 0)
throw new IllegalArgumentException(this.getClass().getName() + ".read(): illegal imageIndex: " + imageIndex);
readIFDs();
// Extract the various IFD tags we need to read this image...
TiffIFDEntry widthEntry = null;
TiffIFDEntry lengthEntry = null;
TiffIFDEntry bitsPerSampleEntry = null;
TiffIFDEntry samplesPerPixelEntry = null;
TiffIFDEntry photoInterpEntry = null;
TiffIFDEntry stripOffsetsEntry = null;
TiffIFDEntry stripCountsEntry = null;
TiffIFDEntry rowsPerStripEntry = null;
TiffIFDEntry planarConfigEntry = null;
TiffIFDEntry[] ifd = ifds.get(imageIndex);
for (int i=0; i<ifd.length; i++) {
TiffIFDEntry entry = ifd[i];
switch (entry.tag) {
case TiffTags.IMAGE_WIDTH:
widthEntry = entry; break;
case TiffTags.IMAGE_LENGTH:
lengthEntry = entry; break;
case TiffTags.BITS_PER_SAMPLE:
bitsPerSampleEntry = entry; break;
case TiffTags.SAMPLES_PER_PIXEL:
samplesPerPixelEntry = entry; break;
case TiffTags.PHOTO_INTERPRETATION:
photoInterpEntry = entry; break;
case TiffTags.STRIP_OFFSETS:
stripOffsetsEntry = entry; break;
case TiffTags.STRIP_BYTE_COUNTS:
stripCountsEntry = entry; break;
case TiffTags.ROWS_PER_STRIP:
rowsPerStripEntry = entry; break;
case TiffTags.PLANAR_CONFIGURATION:
planarConfigEntry = entry; break;
}
}
if (widthEntry == null || lengthEntry == null || samplesPerPixelEntry == null || photoInterpEntry == null ||
stripOffsetsEntry == null || stripCountsEntry == null || rowsPerStripEntry == null || planarConfigEntry == null)
// note that bitsPerSample is an optional entry, so not checked above; its default is "1".
throw new IIOException(this.getClass().getName() + ".read(): unable to decipher image organization");
int width = (int) widthEntry.asLong();
int height = (int) lengthEntry.asLong();
int samplesPerPixel = (int) samplesPerPixelEntry.asLong();
long photoInterp = photoInterpEntry.asLong();
long rowsPerStrip = rowsPerStripEntry.asLong();
long planarConfig = planarConfigEntry.asLong();
int[] bitsPerSample = getBitsPerSample(bitsPerSampleEntry);
long[] stripOffsets = getStripsArray(stripOffsetsEntry);
long[] stripCounts = getStripsArray(stripCountsEntry);
// TODO: deal with samples-sizes other than byte (?)
// make sure a DataBufferByte is going to do the trick
for (int i = 0; i < bitsPerSample.length; i++) {
if (bitsPerSample[i] != 8)
throw new IIOException(this.getClass().getName() + ".read(): only expecting 8 bits/sample; found " +
bitsPerSample[i]);
}
ColorSpace colorSpace = (samplesPerPixel > 1) ?
ColorSpace.getInstance(ColorSpace.CS_sRGB) :
ColorSpace.getInstance(ColorSpace.CS_GRAY);
int transparency = Transparency.OPAQUE;
boolean hasAlpha = false;
if (samplesPerPixel == 4) {
transparency = Transparency.TRANSLUCENT;
hasAlpha = true;
}
int[] bankOffsets = new int[samplesPerPixel];
for (int i=0; i<samplesPerPixel; i++) bankOffsets[i] = i;
int[] offsets = new int[(planarConfig == TiffConstants.PLANARCONFIG_CHUNKY) ? 1 : samplesPerPixel];
for (int i=0; i<offsets.length; i++) offsets[i] = 0;
ComponentColorModel colorModel = new ComponentColorModel(colorSpace, bitsPerSample, hasAlpha, false,
transparency, DataBuffer.TYPE_BYTE);
ComponentSampleModel sampleModel;
if (samplesPerPixel == 1)
sampleModel = new ComponentSampleModel(DataBuffer.TYPE_BYTE, width, height, 1, width, bankOffsets);
else
sampleModel = (planarConfig == TiffConstants.PLANARCONFIG_CHUNKY) ?
new PixelInterleavedSampleModel(DataBuffer.TYPE_BYTE, width, height, samplesPerPixel,
width*samplesPerPixel, bankOffsets) :
new BandedSampleModel(DataBuffer.TYPE_BYTE, width, height, width, bankOffsets, offsets);
byte[][] imageData;
if (planarConfig == TiffConstants.PLANARCONFIG_CHUNKY)
imageData = readPixelInterleaved(width, height, samplesPerPixel, stripOffsets, stripCounts);
else
imageData = readPlanar(width, height, samplesPerPixel, stripOffsets, stripCounts, rowsPerStrip);
DataBufferByte dataBuff = new DataBufferByte(imageData, width*height, offsets);
WritableRaster raster = Raster.createWritableRaster(sampleModel, dataBuff, new Point(0,0));
return new BufferedImage(colorModel, raster, false, null);
}
/*
* Coordinates reading all the ImageFileDirectories in a Tiff file (there's typically only one).
*
*/
private void readIFDs() throws IOException {
if (this.theStream != null)
return;
if (super.input == null || !(super.input instanceof ImageInputStream)) {
throw new IIOException(this.getClass().getName() + ": null/invalid ImageInputStream");
}
this.theStream = (ImageInputStream) super.input;
// determine byte ordering...
byte[] ifh = new byte[2]; // Tiff image-file header
try {
theStream.readFully(ifh);
if (ifh[0] == 0x4D && ifh[1] == 0x4D) {
theStream.setByteOrder(ByteOrder.BIG_ENDIAN);
} else if (ifh[0] == 0x49 && ifh[1] == 0x49) {
theStream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
} else {
throw new IOException();
}
} catch (IOException ex) {
throw new IIOException(this.getClass().getName() + ": error reading signature");
}
// skip the magic number and get offset to first (and likely only) ImageFileDirectory...
theStream.readFully(ifh);
long ifdOffset = theStream.readUnsignedInt();
readIFD(ifdOffset);
}
/*
* Reads an ImageFileDirectory and places it in our list. Calls itself recursively if additional
* IFDs are indicated.
*
*/
private void readIFD(long offset) throws IIOException {
try {
theStream.seek(offset);
int numEntries = theStream.readUnsignedShort();
TiffIFDEntry[] ifd = new TiffIFDEntry[numEntries];
byte[] valoffset = new byte[4];
for (int i=0; i<numEntries; i++) {
int tag = theStream.readUnsignedShort();
int type = theStream.readUnsignedShort();
long count = theStream.readUnsignedInt();
theStream.readFully(valoffset);
ifd[i] = new TiffIFDEntry(tag, type, count, valoffset);
}
ifds.add(ifd);
/****** TODO: UNCOMMENT; IN GENERAL, THERE CAN BE MORE THAN ONE IFD IN A TIFF FILE
long nextIFDOffset = theStream.readUnsignedInt();
if (nextIFDOffset > 0)
readIFD(nextIFDOffset);
*/
} catch (Exception ex) {
throw new IIOException("Error reading Tiff IFD: " + ex.getMessage());
}
}
/*
* Reads image data organized as a singular image plane (and pixel interleaved, in the case of color images).
*
*/
private byte[][] readPixelInterleaved(int width, int height, int samplesPerPixel,
long[] stripOffsets, long[] stripCounts) throws IOException
{
byte[][] data = new byte[1][width * height * samplesPerPixel];
int offset = 0;
for (int i = 0; i < stripOffsets.length; i++) {
this.theStream.seek(stripOffsets[i]);
int len = (int) stripCounts[i];
if ((offset + len) >= data[0].length)
len = data[0].length - offset;
this.theStream.readFully(data[0], offset, len);
offset += stripCounts[i];
}
return data;
}
/*
* Reads image data organized as separate image planes.
*
*/
private byte[][] readPlanar(int width, int height, int samplesPerPixel,
long[] stripOffsets, long[] stripCounts, long rowsPerStrip) throws IOException
{
byte[][] data = new byte[samplesPerPixel][width * height];
int band = 0;
int offset = 0;
int numRows = 0;
for (int i = 0; i < stripOffsets.length; i++) {
this.theStream.seek(stripOffsets[i]);
int len = (int) stripCounts[i];
if ((offset+len) >= data[band].length) len = data[band].length - offset;
this.theStream. readFully(data[band], offset, len);
offset += stripCounts[i];
numRows += rowsPerStrip;
if (numRows >= height) {
++band;
numRows = 0;
offset = 0;
}
}
return data;
}
/*
* Returns the (first!) IFD-Entry with the given tag, or null if not found.
*
*/
private TiffIFDEntry getByTag(TiffIFDEntry[] ifd, int tag) {
for (int i = 0; i < ifd.length; i++) {
if (ifd[i].tag == tag) {
return ifd[i];
}
}
return null;
}
/*
* Utility method intended to read the array of StripOffsets or StripByteCounts.
*/
private long[] getStripsArray(TiffIFDEntry stripsEntry) throws IOException {
long[] offsets = new long[(int)stripsEntry.count];
long fileOffset = stripsEntry.asLong();
this.theStream.seek(fileOffset);
if (stripsEntry.type == TiffTypes.SHORT)
for (int i=0; i<stripsEntry.count; i++)
offsets[i] = this.theStream.readUnsignedShort();
else
for (int i=0; i<stripsEntry.count; i++)
offsets[i] = this.theStream.readUnsignedInt();
return offsets;
}
/*
* Utility to extract bitsPerSample info (if present). This is a bit tricky, because if the samples/pixel == 1,
* the bitsPerSample will fit in the offset/value field of the ImageFileDirectory element. In contrast, when
* samples/pixel == 3, the 3 shorts that make up bitsPerSample don't fit in the offset/value field, so we have
* to go track them down elsewhere in the file. Finally, as bitsPerSample is optional for bilevel images,
* we'll return something sane if this tag is absent.
*/
private int[] getBitsPerSample(TiffIFDEntry entry) throws IOException {
if (entry == null) {
return new int[]{1};
} // the default according to the Tiff6.0 spec.
if (entry.count == 1) {
return new int[]{(int)entry.asLong()};
}
long[] tmp = getStripsArray(entry);
int[] bits = new int[tmp.length];
for (int i=0; i<tmp.length; i++) {
bits[i] = (int)tmp[i];
}
return bits;
}
private ImageInputStream theStream = null;
private ArrayList<TiffIFDEntry[]> ifds = new ArrayList<TiffIFDEntry[]>(1);
}