package com.realityinteractive.imageio.tga; /* * TGAImageReader.java * Copyright (c) 2003 Reality Interactive, Inc. * See bottom of file for license and warranty information. * Created on Sep 26, 2003 */ import java.awt.Rectangle; import java.awt.color.ColorSpace; import java.awt.image.BufferedImage; import java.awt.image.DataBuffer; import java.awt.image.DataBufferInt; import java.awt.image.WritableRaster; import java.io.IOException; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.Iterator; import java.util.List; 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; /** *

The {@link javax.imageio.ImageReader} that exposes the TGA image reading. * 8, 15, 16, 24 and 32 bit true color or color mapped (RLE compressed or * uncompressed) are supported. Monochrome images are not supported.

* *

Great care should be employed with {@link javax.imageio.ImageReadParam}s. * Little to no effort has been made to correctly handle sub-sampling or * specified bands.

* *

{@link javax.imageio.ImageIO#setUseCache(boolean)} should be set to false * when using this reader. Also, {@link javax.imageio.ImageIO#read(java.io.InputStream)} * is the preferred read method if used against a buffered array (for performance * reasons).

* * @author Rob Grzywinski rgrzywinski@realityinteractive.com * @version $Id: TGAImageReader.java,v 1.1 2005/04/12 11:23:53 ornedan Exp $ * @since 1.0 */ // TODO: incorporate the x and y origins public class TGAImageReader extends ImageReader { /** *

The {@link javax.imageio.stream.ImageInputStream} from which the TGA * is read. This may be null if {@link javax.imageio.ImageReader#setInput(java.lang.Object)} * (or the other forms of setInput()) has not been called. The * stream will be set litle-endian when it is set.

*/ private ImageInputStream inputStream; /** *

The {@link com.realityinteractive.imageio.tga.TGAHeader}. If null * then the header has not been read since inputStream was * last set. This is created lazily.

*/ private TGAHeader header; // ========================================================================= /** * @see javax.imageio.ImageReader#ImageReader(javax.imageio.spi.ImageReaderSpi) */ public TGAImageReader(final ImageReaderSpi originatingProvider) { super(originatingProvider); } /** *

Store the input if it is an {@link javax.imageio.stream.ImageInputStream}. * Otherwise {@link java.lang.IllegalArgumentException} is thrown. The * stream is set to little-endian byte ordering.

* * @see javax.imageio.ImageReader#setInput(java.lang.Object, boolean, boolean) */ // NOTE: can't read the header in here as there would be no place for // exceptions to go. It must be read lazily. public void setInput(final Object input, final boolean seekForwardOnly, final boolean ignoreMetadata) { // delegate to the partent super.setInput(input, seekForwardOnly, ignoreMetadata); // if the input is null clear the inputStream and header if(input == null) { inputStream = null; header = null; } /* else -- the input is non-null */ // only ImageInputStream are allowed. If other throw IllegalArgumentException if(input instanceof ImageInputStream) { // set the inputStream inputStream = (ImageInputStream)input; // put the ImageInputStream into little-endian ("Intel byte ordering") // byte ordering inputStream.setByteOrder(ByteOrder.LITTLE_ENDIAN); } else /* input is not an instance of ImageInputStream */ { throw new IllegalArgumentException("Only ImageInputStreams are accepted."); // FIXME: localize } } /** *

Create and read the {@link com.realityinteractive.imageio.tga.TGAHeader} * only if there is not one already.

* * @return the TGAHeader (for convenience) * @throws IOException if there is an I/O error while reading the header */ private synchronized TGAHeader getHeader() throws IOException { // if there is already a header (non-null) then there is nothing to be // done if(header != null) return header; /* else -- there is no header */ // ensure that there is an ImageInputStream from which the header is // read if(inputStream == null) throw new IllegalStateException("There is no ImageInputStream from which the header can be read."); // FIXME: localize /* else -- there is an input stream */ header = new TGAHeader(inputStream); return header; } /** *

Only a single image can be read by this reader. Validate the * specified image index and if not 0 then {@link java.lang.IndexOutOfBoundsException} * is thrown.

* * @param imageIndex the index of the image to validate * @throws IndexOutOfBoundsException if the imageIndex is not * 0 */ private void checkImageIndex(final int imageIndex) { // if the imageIndex is not 0 then throw an exception if(imageIndex != 0) throw new IndexOutOfBoundsException("Image index out of bounds (" + imageIndex + " != 0)."); // FIXME: localize /* else -- the index is in bounds */ } // ========================================================================= // Required ImageReader methods /** * @see javax.imageio.ImageReader#getImageTypes(int) */ public Iterator/**/ getImageTypes(final int imageIndex) throws IOException { // validate the imageIndex (this will throw if invalid) checkImageIndex(imageIndex); // read / get the header final TGAHeader header = getHeader(); // get the ImageTypeSpecifier for the image type // FIXME: finish final ImageTypeSpecifier imageTypeSpecifier; switch(header.getImageType()) { case TGAConstants.COLOR_MAP: case TGAConstants.RLE_COLOR_MAP: case TGAConstants.TRUE_COLOR: case TGAConstants.RLE_TRUE_COLOR: { // determine if there is an alpha mask based on the number of // samples per pixel final int alphaMask; if(header.getSamplesPerPixel() == 4) alphaMask = 0xFF000000; else /* no alpha channel (less than 32 bits or 4 samples) */ alphaMask = 0; // packed RGB(A) pixel data (more specifically (A)BGR) // TODO: split on 16, 24, and 32 bit images otherwise there // will be wasted space final ColorSpace rgb = ColorSpace.getInstance(ColorSpace.CS_sRGB); imageTypeSpecifier = ImageTypeSpecifier.createPacked(rgb, 0x000000FF, 0x0000FF00, 0x00FF0000, alphaMask, DataBuffer.TYPE_INT, false /*not pre-multiplied by an alpha*/); break; } case TGAConstants.MONO: case TGAConstants.RLE_MONO: throw new IllegalArgumentException("Monochrome image type not supported."); case TGAConstants.NO_IMAGE: default: throw new IllegalArgumentException("The image type is not known."); // FIXME: localize } // create a list and add the ImageTypeSpecifier to it final List/**/ imageSpecifiers = new ArrayList/**/(); imageSpecifiers.add(imageTypeSpecifier); return imageSpecifiers.iterator(); } /** *

Only a single image is supported.

* * @see javax.imageio.ImageReader#getNumImages(boolean) */ public int getNumImages(final boolean allowSearch) throws IOException { // see javadoc // NOTE: 1 is returned regardless if a search is allowed or not return 1; } /** *

There is no stream metadata (i.e. null is returned).

* * @see javax.imageio.ImageReader#getStreamMetadata() */ public IIOMetadata getStreamMetadata() throws IOException { // see javadoc return null; } /** *

There is no image metadata (i.e. null is returned).

* * @see javax.imageio.ImageReader#getImageMetadata(int) */ public IIOMetadata getImageMetadata(final int imageIndex) throws IOException { // see javadoc return null; } /** * @see javax.imageio.ImageReader#getHeight(int) */ public int getHeight(final int imageIndex) throws IOException { // validate the imageIndex (this will throw if invalid) checkImageIndex(imageIndex); // get the header and return the height return getHeader().getHeight(); } /** * @see javax.imageio.ImageReader#getWidth(int) */ public int getWidth(final int imageIndex) throws IOException { // validate the imageIndex (this will throw if invalid) checkImageIndex(imageIndex); // get the header and return the width return getHeader().getHeight(); } /** * @see javax.imageio.ImageReader#read(int, javax.imageio.ImageReadParam) */ public BufferedImage read(final int imageIndex, final ImageReadParam param) throws IOException { // ensure that the image is of a supported type // NOTE: this will implicitly ensure that the imageIndex is valid final Iterator imageTypes = getImageTypes(imageIndex); if(!imageTypes.hasNext()) { throw new IOException("Unsupported Image Type"); } // read and get the header final TGAHeader header = getHeader(); // ensure that the ImageReadParam hasn't been set to other than the // defaults (this will throw if not acceptible) checkImageReadParam(param, header); // get the height and width from the header for convenience final int width = header.getWidth(); final int height = header.getHeight(); // read the color map data. If the image does not contain a color map // then null will be returned. final int[] colorMap = readColorMap(header); // seek to the pixel data offset // TODO: read the color map inputStream.seek(header.getPixelDataOffset()); // get the destination image and WritableRaster for the image type and // size final BufferedImage image = getDestination(param, imageTypes, width, height); final WritableRaster imageRaster = image.getRaster(); // get and validate the number of image bands final int numberOfImageBands = image.getSampleModel().getNumBands(); checkReadParamBandSettings(param, header.getSamplesPerPixel(), numberOfImageBands); // get the destination bands final int[] destinationBands; if(param != null) { // there is an ImageReadParam -- use its destination bands destinationBands = param.getDestinationBands(); } else { // there are no destination bands destinationBands = null; } // create the destination WritableRaster final WritableRaster raster = imageRaster.createWritableChild(0, 0, width, height, 0, 0, destinationBands); // set up to read the data final int[] intData = ((DataBufferInt)raster.getDataBuffer()).getData(); // CHECK: is this valid / acceptible? int index = 0; // the index in the intData array int runLength = 0; // the number of pixels in a run length boolean readPixel = true; // if true then a raw pixel is read. Used by the RLE. boolean isRaw = false; // if true then the next pixels should be read. Used by the RLE. int pixel = 0; // the current pixel data // TODO: break out the case of 32 bit non-RLE as it can be read // directly and 24 bit non-RLE as it can be read simply. If // subsampling and ROI's are implemented then selection must be // done per pixel for RLE otherwise it's possible to miss the // repetition count field. // TODO: account for TGAHeader.firstColorMapEntryIndex // loop over the rows // TODO: this should be destinationROI.height (right?) for(int y=0; y 0) { // decrement the run length and flag that a pixel should // not be read // NOTE: a pixel is only read from the input if the // packet was raw. If it was a run length packet // then the previous (current) pixel is used. runLength--; readPixel = isRaw; } else /* non-positive run length */ { // read the repetition count field runLength = inputStream.readByte() & 0xFF; // unsigned // determine which packet type: raw or runlength isRaw = ( (runLength & 0x80) == 0); // bit 7 == 0 -> raw; bit 7 == 1 -> runlength // if a run length packet then shift to get the number if(!isRaw) runLength -= 0x80; /* else -- is raw so there's no need to shift */ // the next field is always read (it's the pixel data) readPixel = true; } } // read the next pixel // NOTE: only don't read when in a run length packet if(readPixel) { // NOTE: the alpha must hav a default value since it is // not guaranteed to be present for each pixel read int red = 0, green = 0, blue = 0, alpha = 0xFF; // read based on the number of bits per pixel switch(header.getBitsPerPixel()) { // grey scale (R = G = B) case 8: default: { // read the data -- it is either the color map index // or the color for each pixel final int data = inputStream.readByte() & 0xFF; // unsigned // if the image is a color mapped image then the // resulting pixel is pulled from the color map, // otherwise each pixel gets the data if(header.hasColorMap()) { // the pixel is pulled from the color map // CHECK: do sanity bounds check? pixel = colorMap[data]; } else /* no color map */ { // each color component is set to the color red = green = blue = data; // combine each component into the result pixel = (red << 0) | (green << 8) | (blue << 16); } break; } // 5-5-5 (RGB) case 15: case 16: { // read the two bytes final int data = inputStream.readShort() & 0xFFFF; // unsigned // get each color component -- each is 5 bits red = ((data >> 10) & 0x1F) << 3; green = ((data >> 5) & 0x1F) << 3; blue = (data & 0x1F) << 3; // combine each component into the result pixel = (red << 0) | (green << 8) | (blue << 16); break; } // true color RGB(A) (8 bits per pixel) case 24: case 32: // read each color component -- the alpha is only // read if there are 32 bits per pixel blue = inputStream.readByte() & 0xFF; // unsigned green = inputStream.readByte() & 0xFF; // unsigned red = inputStream.readByte() & 0xFF; // unsigned if(header.getBitsPerPixel() == 32) alpha = (inputStream.readByte() & 0xFF); // unsigned /* else -- 24 bits per pixel (i.e. no alpha) */ // combine each component into the result pixel = (red << 0) | (green << 8) | (blue << 16) | (alpha << 24); break; } } // put the pixel in the data array intData[index] = pixel; // advance to the next pixel // TODO: the right-to-left switch index++; } } return image; } /** *

Reads and returns an array of color mapped values. If the image does * not contain a color map null will be returned

* * @param header the TGAHeader for the image * @return the array of int color map values or null * if the image does not contain a color map * @throws IOException if there is an I/O error while reading the color map */ private int[] readColorMap(final TGAHeader header) throws IOException { // determine if the image contains a color map. If not, return null if(!header.hasColorMap()) return null; /* else -- there is a color map */ // seek to the start of the color map in the input stream inputStream.seek(header.getColorMapDataOffset()); // get the number of colros in the color map and the number of bits // per color map entry final int numberOfColors = header.getColorMapLength(); final int bitsPerEntry = header.getBitsPerColorMapEntry(); // create the array that will contain the color map data // CHECK: why is tge explicit +1 needed here ?!? final int[] colorMap = new int[numberOfColors + 1]; // read each color map entry for(int i=0; i> 10) & 0x1F) << 3; green = ((data >> 5) & 0x1F) << 3; blue = (data & 0x1F) << 3; break; } // true color RGB(A) (8 bits per pixel) case 24: case 32: // read each color component // CHECK: is there an alpha?!? blue = inputStream.readByte() & 0xFF; // unsigned green = inputStream.readByte() & 0xFF; // unsigned red = inputStream.readByte() & 0xFF; // unsigned break; } // combine each component into the result colorMap[i] = (red << 0) | (green << 8) | (blue << 16); } return colorMap; } /** *

Validate that the specified {@link javax.imageio.ImageReadParam} * contains only the default values. If non-default values are present, * {@link java.io.IOException} is thrown.

* * @param param the ImageReadParam to be validated * @param head the TGAHeader that contains information about * the source image * @throws IOException if the ImageReadParam contains non-default * values */ private void checkImageReadParam(final ImageReadParam param, final TGAHeader header) throws IOException { if(param != null) { // get the image height and width from the header for convenience final int width = header.getWidth(); final int height = header.getHeight(); // ensure that the param contains only the defaults final Rectangle sourceROI = param.getSourceRegion(); if( (sourceROI != null) && ( (sourceROI.x != 0) || (sourceROI.y != 0) || (sourceROI.width != width) || (sourceROI.height != height) ) ) { throw new IOException("The source region of interest is not the default."); // FIXME: localize } /* else -- the source ROI is the default */ final Rectangle destinationROI = param.getSourceRegion(); if( (destinationROI != null) && ( (destinationROI.x != 0) || (destinationROI.y != 0) || (destinationROI.width != width) || (destinationROI.height != height) ) ) { throw new IOException("The destination region of interest is not the default."); // FIXME: localize } /* else -- the destination ROI is the default */ if( (param.getSourceXSubsampling() != 1) || (param.getSourceYSubsampling() != 1) ) { throw new IOException("Source sub-sampling is not supported."); // FIXME: localize } /* else -- sub-sampling is the default */ } /* else -- the ImageReadParam is null so the defaults *are* used */ } } // ============================================================================= /* 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; either version 2.1 of the License, or (at your option) any later version. 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. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */