/*

   Licensed to the Apache Software Foundation (ASF) under one or more
   contributor license agreements.  See the NOTICE file distributed with
   this work for additional information regarding copyright ownership.
   The ASF licenses this file to You under the Apache License, Version 2.0
   (the "License"); you may not use this file except in compliance with
   the License.  You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.

 */
package org.apache.batik.ext.awt.image.codec.tiff;

import java.awt.Rectangle;
import java.awt.Transparency;
import java.awt.color.ColorSpace;
import java.awt.image.ColorModel;
import java.awt.image.ComponentColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferInt;
import java.awt.image.DataBufferShort;
import java.awt.image.DataBufferUShort;
import java.awt.image.IndexColorModel;
import java.awt.image.MultiPixelPackedSampleModel;
import java.awt.image.PixelInterleavedSampleModel;
import java.awt.image.Raster;
import java.awt.image.SampleModel;
import java.awt.image.WritableRaster;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;

import org.apache.batik.ext.awt.image.codec.util.SeekableStream;
import org.apache.batik.ext.awt.image.rendered.AbstractRed;
import org.apache.batik.ext.awt.image.rendered.CachableRed;

import com.sun.image.codec.jpeg.JPEGCodec;
import com.sun.image.codec.jpeg.JPEGDecodeParam;
import com.sun.image.codec.jpeg.JPEGImageDecoder;

/**
 *
 * @version $Id: TIFFImage.java 498740 2007-01-22 18:35:57Z dvholten $
 */
public class TIFFImage extends AbstractRed {

    // Compression types
    public static final int COMP_NONE      = 1;
    public static final int COMP_FAX_G3_1D = 2;
    public static final int COMP_FAX_G3_2D = 3;
    public static final int COMP_FAX_G4_2D = 4;
    public static final int COMP_LZW       = 5;
    public static final int COMP_JPEG_OLD  = 6;
    public static final int COMP_JPEG_TTN2 = 7;
    public static final int COMP_PACKBITS  = 32773;
    public static final int COMP_DEFLATE   = 32946;

    // Image types
    private static final int TYPE_UNSUPPORTED = -1;
    private static final int TYPE_BILEVEL      = 0;
    private static final int TYPE_GRAY_4BIT    = 1;
    private static final int TYPE_GRAY         = 2;
    private static final int TYPE_GRAY_ALPHA   = 3;
    private static final int TYPE_PALETTE      = 4;
    private static final int TYPE_RGB          = 5;
    private static final int TYPE_RGB_ALPHA    = 6;
    private static final int TYPE_YCBCR_SUB    = 7;
    private static final int TYPE_GENERIC      = 8;

    // Incidental tags
    private static final int TIFF_JPEG_TABLES       = 347;
    private static final int TIFF_YCBCR_SUBSAMPLING = 530;

    SeekableStream stream;
    int tileSize;
    int tilesX, tilesY;
    long[] tileOffsets;
    long[] tileByteCounts;
    char[] colormap;
    int sampleSize;
    int compression;
    byte[] palette;
    int numBands;

    int chromaSubH;
    int chromaSubV;

    // Fax compression related variables
    long tiffT4Options;
    long tiffT6Options;
    int fillOrder;

    // LZW compression related variable
    int predictor;

    // TTN2 JPEG related variables
    JPEGDecodeParam decodeParam = null;
    boolean colorConvertJPEG = false;

    // DEFLATE variables
    Inflater inflater = null;

    // Endian-ness indicator
    boolean isBigEndian;

    int imageType;
    boolean isWhiteZero = false;
    int dataType;

    boolean decodePaletteAsShorts;
    boolean tiled;

    // Decoders
    private TIFFFaxDecoder decoder = null;
    private TIFFLZWDecoder lzwDecoder = null;

    /**
     * Decode a buffer of data into a Raster with the specified location.
     *
     * @param data buffer contain an interchange or abbreviated datastream.
     * @param decodeParam decoding parameters; may be null unless the
     *        data buffer contains an abbreviated datastream in which case
     *        it may not be null or an error will occur.
     * @param colorConvert whether to perform color conversion; in this
     *        case that would be limited to YCbCr-to-RGB.
     * @param minX the X position of the returned Raster.
     * @param minY the Y position of the returned Raster.
     */
    private static final Raster decodeJPEG(byte[] data,
                                           JPEGDecodeParam decodeParam,
                                           boolean colorConvert,
                                           int minX,
                                           int minY) {
        // Create an InputStream from the compressed data array.
        ByteArrayInputStream jpegStream = new ByteArrayInputStream(data);

        // Create a decoder.
        JPEGImageDecoder decoder = decodeParam == null ?
            JPEGCodec.createJPEGDecoder(jpegStream) :
            JPEGCodec.createJPEGDecoder(jpegStream,
                                        decodeParam);

        // Decode the compressed data into a Raster.
        Raster jpegRaster;
        try {
            jpegRaster = colorConvert ?
                decoder.decodeAsBufferedImage().getWritableTile(0, 0) :
                decoder.decodeAsRaster();
        } catch (IOException ioe) {
            throw new RuntimeException("TIFFImage13");
        }

        // Translate the decoded Raster to the specified location and return.
        return jpegRaster.createTranslatedChild(minX, minY);
    }

    /**
     * Inflates <code>deflated</code> into <code>inflated</code> using the
     * <code>Inflater</code> constructed during class instantiation.
     */
    private final void inflate(byte[] deflated, byte[] inflated) {
        inflater.setInput(deflated);
        try {
            inflater.inflate(inflated);
        } catch(DataFormatException dfe) {
            throw new RuntimeException("TIFFImage17"+": "+
                                       dfe.getMessage());
        }
        inflater.reset();
    }

    private static SampleModel createPixelInterleavedSampleModel
        (int dataType, int tileWidth, int tileHeight, int bands) {
        int [] bandOffsets = new int[bands];
        for (int i=0; i<bands; i++)
            bandOffsets[i] = i;
        return new PixelInterleavedSampleModel
            (dataType, tileWidth, tileHeight, bands,
             tileWidth*bands, bandOffsets);
    }

    /**
     * Return as a long[] the value of a TIFF_LONG or TIFF_SHORT field.
     */
    private long[] getFieldAsLongs(TIFFField field) {
        long[] value = null;

        if(field.getType() == TIFFField.TIFF_SHORT) {
            char[] charValue = field.getAsChars();
            value = new long[charValue.length];
            for(int i = 0; i < charValue.length; i++) {
                value[i] = charValue[i]  & 0xffff;
            }
        } else if(field.getType() == TIFFField.TIFF_LONG) {
            value = field.getAsLongs();
        } else {
            throw new RuntimeException();
        }

        return value;
    }

    /**
     * Constructs a TIFFImage that acquires its data from a given
     * SeekableStream and reads from a particular IFD of the stream.
     * The index of the first IFD is 0.
     *
     * @param stream the SeekableStream to read from.
     * @param param an instance of TIFFDecodeParam, or null.
     * @param directory the index of the IFD to read from.
     */
    public TIFFImage(SeekableStream stream,
                     TIFFDecodeParam param,
                     int directory)
        throws IOException {

        this.stream = stream;
        if (param == null) {
            param = new TIFFDecodeParam();
        }

        decodePaletteAsShorts = param.getDecodePaletteAsShorts();

        // Read the specified directory.
        TIFFDirectory dir = param.getIFDOffset() == null ?
            new TIFFDirectory(stream, directory) :
            new TIFFDirectory(stream, param.getIFDOffset().longValue(),
                              directory);

        // Get the number of samples per pixel
        TIFFField sfield = dir.getField(TIFFImageDecoder.TIFF_SAMPLES_PER_PIXEL);
        int samplesPerPixel = sfield == null ? 1 : (int)sfield.getAsLong(0);

        // Read the TIFF_PLANAR_CONFIGURATION field
        TIFFField planarConfigurationField =
            dir.getField(TIFFImageDecoder.TIFF_PLANAR_CONFIGURATION);
        char[] planarConfiguration = planarConfigurationField == null ?
            new char[] {1} :
            planarConfigurationField.getAsChars();

            // Support planar format (band sequential) only for 1 sample/pixel.
            if (planarConfiguration[0] != 1 && samplesPerPixel != 1) {
                throw new RuntimeException("TIFFImage0");
            }

            // Read the TIFF_BITS_PER_SAMPLE field
            TIFFField bitsField =
                dir.getField(TIFFImageDecoder.TIFF_BITS_PER_SAMPLE);
            char[] bitsPerSample = null;
            if(bitsField != null) {
                bitsPerSample = bitsField.getAsChars();
            } else {
                bitsPerSample = new char[] {1};

                // Ensure that all samples have the same bit depth.
                for (int i = 1; i < bitsPerSample.length; i++) {
                    if (bitsPerSample[i] != bitsPerSample[0]) {
                        throw new RuntimeException("TIFFImage1");
                    }
                }
            }
            sampleSize = bitsPerSample[0];

            // Read the TIFF_SAMPLE_FORMAT tag to see whether the data might be
            // signed or floating point
            TIFFField sampleFormatField =
                dir.getField(TIFFImageDecoder.TIFF_SAMPLE_FORMAT);

            char[] sampleFormat = null;
            if (sampleFormatField != null) {
                sampleFormat = sampleFormatField.getAsChars();

                // Check that all the samples have the same format
                for (int l=1; l<sampleFormat.length; l++) {
                    if (sampleFormat[l] != sampleFormat[0]) {
                        throw new RuntimeException("TIFFImage2");
                    }
                }

            } else {
                sampleFormat = new char[] {1};
            }

            // Set the data type based on the sample size and format.
            boolean isValidDataFormat = false;
            switch(sampleSize) {
            case 1:
            case 4:
            case 8:
                if(sampleFormat[0] != 3) {
                    // Ignore whether signed or unsigned: treat all as unsigned.
                    dataType = DataBuffer.TYPE_BYTE;
                    isValidDataFormat = true;
                }
                break;
            case 16:
                if(sampleFormat[0] != 3) {
                    dataType = sampleFormat[0] == 2 ?
                        DataBuffer.TYPE_SHORT : DataBuffer.TYPE_USHORT;
                    isValidDataFormat = true;
                }
                break;
            case 32:
              if (sampleFormat[0] == 3)
                isValidDataFormat = false;
              else {
                dataType = DataBuffer.TYPE_INT;
                isValidDataFormat = true;
              }
              break;
            }

            if(!isValidDataFormat) {
                throw new RuntimeException("TIFFImage3");
            }

            // Figure out what compression if any, is being used.
            TIFFField compField = dir.getField(TIFFImageDecoder.TIFF_COMPRESSION);
            compression = compField == null ? COMP_NONE : compField.getAsInt(0);

            // Get the photometric interpretation.
            int photometricType = (int)dir.getFieldAsLong(
                                                          TIFFImageDecoder.TIFF_PHOTOMETRIC_INTERPRETATION);

            // Determine which kind of image we are dealing with.
            imageType = TYPE_UNSUPPORTED;
            switch(photometricType) {
            case 0: // WhiteIsZero
                isWhiteZero = true;
            case 1: // BlackIsZero
                if(sampleSize == 1 && samplesPerPixel == 1) {
                    imageType = TYPE_BILEVEL;
                } else if(sampleSize == 4 && samplesPerPixel == 1) {
                    imageType = TYPE_GRAY_4BIT;
                } else if(sampleSize % 8 == 0) {
                    if(samplesPerPixel == 1) {
                        imageType = TYPE_GRAY;
                    } else if(samplesPerPixel == 2) {
                        imageType = TYPE_GRAY_ALPHA;
                    } else {
                        imageType = TYPE_GENERIC;
                    }
                }
                break;
            case 2: // RGB
                if(sampleSize % 8 == 0) {
                    if(samplesPerPixel == 3) {
                        imageType = TYPE_RGB;
                    } else if(samplesPerPixel == 4) {
                        imageType = TYPE_RGB_ALPHA;
                    } else {
                        imageType = TYPE_GENERIC;
                    }
                }
                break;
            case 3: // RGB Palette
                if(samplesPerPixel == 1 &&
                   (sampleSize == 4 || sampleSize == 8 || sampleSize == 16)) {
                    imageType = TYPE_PALETTE;
                }
                break;
            case 4: // Transparency mask
                if(sampleSize == 1 && samplesPerPixel == 1) {
                    imageType = TYPE_BILEVEL;
                }
                break;
            case 6: // YCbCr
                if(compression == COMP_JPEG_TTN2 &&
                   sampleSize == 8 && samplesPerPixel == 3) {
                    // Set color conversion flag.
                    colorConvertJPEG = param.getJPEGDecompressYCbCrToRGB();

                    // Set type to RGB if color converting.
                    imageType = colorConvertJPEG ? TYPE_RGB : TYPE_GENERIC;
                } else {
                    TIFFField chromaField = dir.getField(TIFF_YCBCR_SUBSAMPLING);
                    if(chromaField != null) {
                        chromaSubH = chromaField.getAsInt(0);
                        chromaSubV = chromaField.getAsInt(1);
                    } else {
                        chromaSubH = chromaSubV = 2;
                    }

                    if(chromaSubH*chromaSubV == 1) {
                        imageType = TYPE_GENERIC;
                    } else if(sampleSize == 8 && samplesPerPixel == 3) {
                        imageType = TYPE_YCBCR_SUB;
                    }
                }
                break;
            default: // Other including CMYK, CIE L*a*b*, unknown.
                if(sampleSize % 8 == 0) {
                    imageType = TYPE_GENERIC;
                }
            }

            // Bail out if not one of the supported types.
            if(imageType == TYPE_UNSUPPORTED) {
                throw new RuntimeException("TIFFImage4");
            }

            // Set basic image layout
            Rectangle bounds = new Rectangle
                (0, 0,
                 (int)dir.getFieldAsLong(TIFFImageDecoder.TIFF_IMAGE_WIDTH),
                 (int)dir.getFieldAsLong(TIFFImageDecoder.TIFF_IMAGE_LENGTH));

            // Set a preliminary band count. This may be changed later as needed.
            numBands = samplesPerPixel;

            // Figure out if any extra samples are present.
            TIFFField efield = dir.getField(TIFFImageDecoder.TIFF_EXTRA_SAMPLES);
            int extraSamples = efield == null ? 0 : (int)efield.getAsLong(0);

            int tileWidth, tileHeight;
            if (dir.getField(TIFFImageDecoder.TIFF_TILE_OFFSETS) != null) {
                tiled = true;
                // Image is in tiled format
                tileWidth =
                    (int)dir.getFieldAsLong(TIFFImageDecoder.TIFF_TILE_WIDTH);
                tileHeight =
                    (int)dir.getFieldAsLong(TIFFImageDecoder.TIFF_TILE_LENGTH);
                tileOffsets =
                    (dir.getField(TIFFImageDecoder.TIFF_TILE_OFFSETS)).getAsLongs();
                tileByteCounts =
                    getFieldAsLongs(dir.getField(TIFFImageDecoder.TIFF_TILE_BYTE_COUNTS));

            } else {
                tiled = false;

                // Image is in stripped format, looks like tiles to us
                // Note: Some legacy files may have tile width and height
                // written but use the strip offsets and byte counts fields
                // instead of the tile offsets and byte counts. Therefore
                // we default here to the tile dimensions if they are written.
                tileWidth =
                    dir.getField(TIFFImageDecoder.TIFF_TILE_WIDTH) != null ?
                    (int)dir.getFieldAsLong(TIFFImageDecoder.TIFF_TILE_WIDTH) :
                    bounds.width;
                TIFFField field =
                    dir.getField(TIFFImageDecoder.TIFF_ROWS_PER_STRIP);
                if (field == null) {
                    // Default is infinity (2^32 -1), basically the entire image

                    tileHeight =
                        dir.getField(TIFFImageDecoder.TIFF_TILE_LENGTH) != null ?
                        (int)dir.getFieldAsLong(TIFFImageDecoder.TIFF_TILE_LENGTH):
                        bounds.height;
                } else {
                    long l = field.getAsLong(0);
                    long infinity = 1;
                    infinity = (infinity << 32) - 1;
                    if (l == infinity) {
                        // 2^32 - 1 (effectively infinity, entire image is 1 strip)
                        tileHeight = bounds.height;
                    } else {
                        tileHeight = (int)l;
                    }
                }

                TIFFField tileOffsetsField =
                    dir.getField(TIFFImageDecoder.TIFF_STRIP_OFFSETS);
                if (tileOffsetsField == null) {
                    throw new RuntimeException("TIFFImage5");
                } else {
                    tileOffsets = getFieldAsLongs(tileOffsetsField);
                }

                TIFFField tileByteCountsField =
                    dir.getField(TIFFImageDecoder.TIFF_STRIP_BYTE_COUNTS);
                if (tileByteCountsField == null) {
                    throw new RuntimeException("TIFFImage6");
                } else {
                    tileByteCounts = getFieldAsLongs(tileByteCountsField);
                }
            }

            // Calculate number of tiles and the tileSize in bytes
            tilesX = (bounds.width + tileWidth - 1)/tileWidth;
            tilesY = (bounds.height + tileHeight - 1)/tileHeight;
            tileSize = tileWidth * tileHeight * numBands;

            // Check whether big endian or little endian format is used.
            isBigEndian = dir.isBigEndian();

            TIFFField fillOrderField =
                dir.getField(TIFFImageDecoder.TIFF_FILL_ORDER);
            if (fillOrderField != null) {
                fillOrder = fillOrderField.getAsInt(0);
            } else {
                // Default Fill Order
                fillOrder = 1;
            }

            switch(compression) {
            case COMP_NONE:
            case COMP_PACKBITS:
                // Do nothing.
                break;
            case COMP_DEFLATE:
                inflater = new Inflater();
                break;
            case COMP_FAX_G3_1D:
            case COMP_FAX_G3_2D:
            case COMP_FAX_G4_2D:
                if(sampleSize != 1) {
                    throw new RuntimeException("TIFFImage7");
                }

                // Fax T.4 compression options
                if (compression == 3) {
                    TIFFField t4OptionsField =
                        dir.getField(TIFFImageDecoder.TIFF_T4_OPTIONS);
                    if (t4OptionsField != null) {
                        tiffT4Options = t4OptionsField.getAsLong(0);
                    } else {
                        // Use default value
                        tiffT4Options = 0;
                    }
                }

                // Fax T.6 compression options
                if (compression == 4) {
                    TIFFField t6OptionsField =
                        dir.getField(TIFFImageDecoder.TIFF_T6_OPTIONS);
                    if (t6OptionsField != null) {
                        tiffT6Options = t6OptionsField.getAsLong(0);
                    } else {
                        // Use default value
                        tiffT6Options = 0;
                    }
                }

                // Fax encoding, need to create the Fax decoder.
                decoder = new TIFFFaxDecoder(fillOrder,
                                             tileWidth, tileHeight);
                break;

            case COMP_LZW:
                // LZW compression used, need to create the LZW decoder.
                TIFFField predictorField =
                    dir.getField(TIFFImageDecoder.TIFF_PREDICTOR);

                if (predictorField == null) {
                    predictor = 1;
                } else {
                    predictor = predictorField.getAsInt(0);

                    if (predictor != 1 && predictor != 2) {
                        throw new RuntimeException("TIFFImage8");
                    }

                    if (predictor == 2 && sampleSize != 8) {
                        throw new RuntimeException(sampleSize +
                                                   "TIFFImage9");
                    }
                }

                lzwDecoder = new TIFFLZWDecoder(tileWidth, predictor,
                                                samplesPerPixel);
                break;

            case COMP_JPEG_OLD:
                throw new RuntimeException("TIFFImage15");

            case COMP_JPEG_TTN2:
                if(!(sampleSize == 8 &&
                     ((imageType == TYPE_GRAY && samplesPerPixel == 1) ||
                      (imageType == TYPE_PALETTE && samplesPerPixel == 1) ||
                      (imageType == TYPE_RGB && samplesPerPixel == 3)))) {
                    throw new RuntimeException("TIFFImage16");
                }

                // Create decodeParam from JPEGTables field if present.
                if(dir.isTagPresent(TIFF_JPEG_TABLES)) {
                    TIFFField jpegTableField = dir.getField(TIFF_JPEG_TABLES);
                    byte[] jpegTable = jpegTableField.getAsBytes();
                    ByteArrayInputStream tableStream =
                        new ByteArrayInputStream(jpegTable);
                    JPEGImageDecoder decoder =
                        JPEGCodec.createJPEGDecoder(tableStream);
                    decoder.decodeAsRaster();
                    decodeParam = decoder.getJPEGDecodeParam();
                }

                break;
            default:
                throw new RuntimeException("TIFFImage10");
            }

            ColorModel  colorModel  = null;
            SampleModel sampleModel = null;
            switch(imageType) {
            case TYPE_BILEVEL:
            case TYPE_GRAY_4BIT:
                sampleModel =
                    new MultiPixelPackedSampleModel(dataType,
                                                    tileWidth,
                                                    tileHeight,
                                                    sampleSize);
                if(imageType == TYPE_BILEVEL) {
                    byte[] map = new byte[] {(byte)(isWhiteZero ? 255 : 0),
                                             (byte)(isWhiteZero ? 0 : 255)};
                    colorModel = new IndexColorModel(1, 2, map, map, map);
                } else {
                    byte [] map = new byte[16];
                    if (isWhiteZero) {
                        for (int i=0; i<map.length; i++)
                            map[i] = (byte)(255-(16*i));
                    } else {
                        for (int i=0; i<map.length; i++)
                            map[i] = (byte)(16*i);
                    }
                    colorModel = new IndexColorModel(4, 16, map, map, map);
                }
                break;

            case TYPE_GRAY:
            case TYPE_GRAY_ALPHA:
            case TYPE_RGB:
            case TYPE_RGB_ALPHA:
                // Create a pixel interleaved SampleModel with decreasing
                // band offsets.
                int[] reverseOffsets = new int[numBands];
                for (int i=0; i<numBands; i++) {
                    reverseOffsets[i] = numBands - 1 - i;
                }
                sampleModel = new PixelInterleavedSampleModel
                    (dataType, tileWidth, tileHeight,
                     numBands, numBands*tileWidth, reverseOffsets);

                if(imageType == TYPE_GRAY) {
                  colorModel = new ComponentColorModel
                    (ColorSpace.getInstance(ColorSpace.CS_GRAY),
                     new int[] { sampleSize }, false, false,
                     Transparency.OPAQUE, dataType);
                } else if (imageType == TYPE_RGB) {
                  colorModel = new ComponentColorModel
                    (ColorSpace.getInstance(ColorSpace.CS_sRGB),
                     new int[] { sampleSize, sampleSize, sampleSize },
                     false, false, Transparency.OPAQUE, dataType);
                } else { // hasAlpha
                    // Transparency.OPAQUE signifies image data that is
                    // completely opaque, meaning that all pixels have an alpha
                    // value of 1.0. So the extra band gets ignored, which is
                    // what we want.
                    int transparency = Transparency.OPAQUE;
                    if(extraSamples == 1) { // associated (premultiplied) alpha
                        transparency = Transparency.TRANSLUCENT;
                    } else if(extraSamples == 2) { // unassociated alpha
                        transparency = Transparency.BITMASK;
                    }

                    colorModel =
                        createAlphaComponentColorModel(dataType,
                                                       numBands,
                                                       extraSamples == 1,
                                                       transparency);
                }
                break;

            case TYPE_GENERIC:
            case TYPE_YCBCR_SUB:
                // For this case we can't display the image, so we create a
                // SampleModel with increasing bandOffsets, and keep the
                // ColorModel as null, as there is no appropriate ColorModel.

                int[] bandOffsets = new int[numBands];
                for (int i=0; i<numBands; i++) {
                    bandOffsets[i] = i;
                }

                sampleModel = new PixelInterleavedSampleModel
                    (dataType, tileWidth, tileHeight,
                     numBands, numBands * tileWidth, bandOffsets);
                colorModel = null;
                break;

            case TYPE_PALETTE:
                // Get the colormap
                TIFFField cfield = dir.getField(TIFFImageDecoder.TIFF_COLORMAP);
                if (cfield == null) {
                    throw new RuntimeException("TIFFImage11");
                } else {
                    colormap = cfield.getAsChars();
                }

                // Could be either 1 or 3 bands depending on whether we use
                // IndexColorModel or not.
                if (decodePaletteAsShorts) {
                    numBands = 3;

                    // If no SampleFormat tag was specified and if the
                    // sampleSize is less than or equal to 8, then the
                    // dataType was initially set to byte, but now we want to
                    // expand the palette as shorts, so the dataType should
                    // be ushort.
                    if (dataType == DataBuffer.TYPE_BYTE) {
                        dataType = DataBuffer.TYPE_USHORT;
                    }

                    // Data will have to be unpacked into a 3 band short image
                    // as we do not have a IndexColorModel that can deal with
                    // a colormodel whose entries are of short data type.
                    sampleModel = createPixelInterleavedSampleModel
                        (dataType, tileWidth, tileHeight, numBands);

                  colorModel = new ComponentColorModel
                    (ColorSpace.getInstance(ColorSpace.CS_sRGB),
                     new int[] { 16, 16, 16 }, false, false,
                     Transparency.OPAQUE, dataType);

                } else {

                    numBands = 1;

                    if (sampleSize == 4) {
                        // Pixel data will not be unpacked, will use
                        // MPPSM to store packed data and
                        // IndexColorModel to do the unpacking.
                        sampleModel = new MultiPixelPackedSampleModel
                            (DataBuffer.TYPE_BYTE, tileWidth, tileHeight,
                             sampleSize);
                    } else if (sampleSize == 8) {

                        sampleModel = createPixelInterleavedSampleModel
                            (DataBuffer.TYPE_BYTE, tileWidth, tileHeight,
                             numBands);
                    } else if (sampleSize == 16) {

                        // Here datatype has to be unsigned since we
                        // are storing indices into the
                        // IndexColorModel palette. Ofcourse the
                        // actual palette entries are allowed to be
                        // negative.
                        dataType = DataBuffer.TYPE_USHORT;
                        sampleModel = createPixelInterleavedSampleModel
                            (DataBuffer.TYPE_USHORT, tileWidth, tileHeight,
                             numBands);
                    }

                    int bandLength = colormap.length/3;
                    byte[] r = new byte[bandLength];
                    byte[] g = new byte[bandLength];
                    byte[] b = new byte[bandLength];

                    int gIndex = bandLength;
                    int bIndex = bandLength * 2;

                    if (dataType == DataBuffer.TYPE_SHORT) {

                        for (int i=0; i<bandLength; i++) {
                            r[i] = param.decodeSigned16BitsTo8Bits
                                ((short)colormap[i]);
                            g[i] = param.decodeSigned16BitsTo8Bits
                                ((short)colormap[gIndex+i]);
                            b[i] = param.decodeSigned16BitsTo8Bits
                                ((short)colormap[bIndex+i]);
                        }

                    } else {

                        for (int i=0; i<bandLength; i++) {
                            r[i] = param.decode16BitsTo8Bits
                                (colormap[i] & 0xffff);
                            g[i] = param.decode16BitsTo8Bits
                                (colormap[gIndex+i] & 0xffff);
                            b[i] = param.decode16BitsTo8Bits
                                (colormap[bIndex+i] & 0xffff);
                        }

                    }

                    colorModel = new IndexColorModel(sampleSize,
                                                     bandLength, r, g, b);
                }
                break;

            default:
                throw new RuntimeException("TIFFImage4");
            }

        Map properties = new HashMap();
        // Set a property "tiff_directory".
        properties.put("tiff_directory", dir);

        // System.out.println("Constructed TIFF");

        init((CachableRed)null, bounds, colorModel, sampleModel,
             0, 0, properties);
    }

    /**
     * Reads a private IFD from a given offset in the stream.  This
     * method may be used to obtain IFDs that are referenced
     * only by private tag values.
     */
    public TIFFDirectory getPrivateIFD(long offset) throws IOException {
        return new TIFFDirectory(stream, offset, 0);
    }


    public WritableRaster copyData(WritableRaster wr) {
        copyToRaster(wr);
        return wr;
    }


    /**
     * Returns tile (tileX, tileY) as a Raster.
     */
    public synchronized Raster getTile(int tileX, int tileY) {
        if ((tileX < 0) || (tileX >= tilesX) ||
            (tileY < 0) || (tileY >= tilesY)) {
            throw new IllegalArgumentException("TIFFImage12");
        }

        // System.out.println("Called TIFF getTile:" + tileX + "," + tileY);


        // Get the data array out of the DataBuffer
        byte[] bdata = null;
        short[] sdata = null;
        int[] idata = null;

        SampleModel sampleModel = getSampleModel();
        WritableRaster tile = makeTile(tileX,tileY);

        DataBuffer buffer = tile.getDataBuffer();

        int dataType = sampleModel.getDataType();
        if (dataType == DataBuffer.TYPE_BYTE) {
            bdata = ((DataBufferByte)buffer).getData();
        } else if (dataType == DataBuffer.TYPE_USHORT) {
            sdata = ((DataBufferUShort)buffer).getData();
        } else if (dataType == DataBuffer.TYPE_SHORT) {
            sdata = ((DataBufferShort)buffer).getData();
        } else if (dataType == DataBuffer.TYPE_INT) {
            idata = ((DataBufferInt)buffer).getData();
        }

        // Variables used for swapping when converting from RGB to BGR
        byte bswap;
        short sswap;
        int iswap;

        // Save original file pointer position and seek to tile data location.
        long save_offset = 0;
        try {
            save_offset = stream.getFilePointer();
            stream.seek(tileOffsets[tileY*tilesX + tileX]);
        } catch (IOException ioe) {
            throw new RuntimeException("TIFFImage13");
        }

        // Number of bytes in this tile (strip) after compression.
        int byteCount = (int)tileByteCounts[tileY*tilesX + tileX];

        // Find out the number of bytes in the current tile
        Rectangle newRect;
        if (!tiled)
            newRect = tile.getBounds();
        else
            newRect = new Rectangle(tile.getMinX(), tile.getMinY(),
                                    tileWidth, tileHeight);

        int unitsInThisTile = newRect.width * newRect.height * numBands;

        // Allocate read buffer if needed.
        byte[] data = compression != COMP_NONE || imageType == TYPE_PALETTE ?
            new byte[byteCount] : null;

        // Read the data, uncompressing as needed. There are four cases:
        // bilevel, palette-RGB, 4-bit grayscale, and everything else.
        if(imageType == TYPE_BILEVEL) { // bilevel
            try {
                if (compression == COMP_PACKBITS) {
                    stream.readFully(data, 0, byteCount);

                    // Since the decompressed data will still be packed
                    // 8 pixels into 1 byte, calculate bytesInThisTile
                    int bytesInThisTile;
                    if ((newRect.width % 8) == 0) {
                        bytesInThisTile = (newRect.width/8) * newRect.height;
                    } else {
                        bytesInThisTile =
                            (newRect.width/8 + 1) * newRect.height;
                    }
                    decodePackbits(data, bytesInThisTile, bdata);
                } else if (compression == COMP_LZW) {
                    stream.readFully(data, 0, byteCount);
                    lzwDecoder.decode(data, bdata, newRect.height);
                } else if (compression == COMP_FAX_G3_1D) {
                    stream.readFully(data, 0, byteCount);
                    decoder.decode1D(bdata, data, 0, newRect.height);
                } else if (compression == COMP_FAX_G3_2D) {
                    stream.readFully(data, 0, byteCount);
                    decoder.decode2D(bdata, data, 0, newRect.height,
                                     tiffT4Options);
                } else if (compression == COMP_FAX_G4_2D) {
                    stream.readFully(data, 0, byteCount);
                    decoder.decodeT6(bdata, data, 0, newRect.height,
                                     tiffT6Options);
                } else if (compression == COMP_DEFLATE) {
                    stream.readFully(data, 0, byteCount);
                    inflate(data, bdata);
                } else if (compression == COMP_NONE) {
                    stream.readFully(bdata, 0, byteCount);
                }

                stream.seek(save_offset);
            } catch (IOException ioe) {
                throw new RuntimeException("TIFFImage13");
            }
        } else if(imageType == TYPE_PALETTE) { // palette-RGB
            if (sampleSize == 16) {

                if (decodePaletteAsShorts) {

                    short[] tempData= null;

                    // At this point the data is 1 banded and will
                    // become 3 banded only after we've done the palette
                    // lookup, since unitsInThisTile was calculated with
                    // 3 bands, we need to divide this by 3.
                    int unitsBeforeLookup = unitsInThisTile / 3;

                    // Since unitsBeforeLookup is the number of shorts,
                    // but we do our decompression in terms of bytes, we
                    // need to multiply it by 2 in order to figure out
                    // how many bytes we'll get after decompression.
                    int entries = unitsBeforeLookup * 2;

                    // Read the data, if compressed, decode it, reset the pointer
                    try {

                        if (compression == COMP_PACKBITS) {

                            stream.readFully(data, 0, byteCount);

                            byte[] byteArray = new byte[entries];
                            decodePackbits(data, entries, byteArray);
                            tempData = new short[unitsBeforeLookup];
                            interpretBytesAsShorts(byteArray, tempData,
                                                   unitsBeforeLookup);

                        }  else if (compression == COMP_LZW) {

                            // Read in all the compressed data for this tile
                            stream.readFully(data, 0, byteCount);

                            byte[] byteArray = new byte[entries];
                            lzwDecoder.decode(data, byteArray, newRect.height);
                            tempData = new short[unitsBeforeLookup];
                            interpretBytesAsShorts(byteArray, tempData,
                                                   unitsBeforeLookup);

                        }  else if (compression == COMP_DEFLATE) {

                            stream.readFully(data, 0, byteCount);
                            byte[] byteArray = new byte[entries];
                            inflate(data, byteArray);
                            tempData = new short[unitsBeforeLookup];
                            interpretBytesAsShorts(byteArray, tempData,
                                                   unitsBeforeLookup);

                        } else if (compression == COMP_NONE) {

                            // byteCount tells us how many bytes are there
                            // in this tile, but we need to read in shorts,
                            // which will take half the space, so while
                            // allocating we divide byteCount by 2.
                            tempData = new short[byteCount/2];
                            readShorts(byteCount/2, tempData);
                        }

                        stream.seek(save_offset);

                    } catch (IOException ioe) {
                        throw new RuntimeException("TIFFImage13");
                    }

                    if (dataType == DataBuffer.TYPE_USHORT) {

                        // Expand the palette image into an rgb image with ushort
                        // data type.
                        int cmapValue;
                        int count = 0, lookup, len = colormap.length/3;
                        int len2 = len * 2;
                        for (int i=0; i<unitsBeforeLookup; i++) {
                            // Get the index into the colormap
                            lookup = tempData[i] & 0xffff;
                            // Get the blue value
                            cmapValue = colormap[lookup+len2];
                            sdata[count++] = (short)(cmapValue & 0xffff);
                            // Get the green value
                            cmapValue = colormap[lookup+len];
                            sdata[count++] = (short)(cmapValue & 0xffff);
                            // Get the red value
                            cmapValue = colormap[lookup];
                            sdata[count++] = (short)(cmapValue & 0xffff);
                        }

                    } else if (dataType == DataBuffer.TYPE_SHORT) {

                        // Expand the palette image into an rgb image with
                        // short data type.
                        int cmapValue;
                        int count = 0, lookup, len = colormap.length/3;
                        int len2 = len * 2;
                        for (int i=0; i<unitsBeforeLookup; i++) {
                            // Get the index into the colormap
                            lookup = tempData[i] & 0xffff;
                            // Get the blue value
                            cmapValue = colormap[lookup+len2];
                            sdata[count++] = (short)cmapValue;
                            // Get the green value
                            cmapValue = colormap[lookup+len];
                            sdata[count++] = (short)cmapValue;
                            // Get the red value
                            cmapValue = colormap[lookup];
                            sdata[count++] = (short)cmapValue;
                        }
                    }

                } else {

                    // No lookup being done here, when RGB values are needed,
                    // the associated IndexColorModel can be used to get them.

                    try {

                        if (compression == COMP_PACKBITS) {

                            stream.readFully(data, 0, byteCount);

                            // Since unitsInThisTile is the number of shorts,
                            // but we do our decompression in terms of bytes, we
                            // need to multiply unitsInThisTile by 2 in order to
                            // figure out how many bytes we'll get after
                            // decompression.
                            int bytesInThisTile = unitsInThisTile * 2;

                            byte[] byteArray = new byte[bytesInThisTile];
                            decodePackbits(data, bytesInThisTile, byteArray);
                            interpretBytesAsShorts(byteArray, sdata,
                                                   unitsInThisTile);

                        } else if (compression == COMP_LZW) {

                            stream.readFully(data, 0, byteCount);

                            // Since unitsInThisTile is the number of shorts,
                            // but we do our decompression in terms of bytes, we
                            // need to multiply unitsInThisTile by 2 in order to
                            // figure out how many bytes we'll get after
                            // decompression.
                            byte[] byteArray = new byte[unitsInThisTile * 2];
                            lzwDecoder.decode(data, byteArray, newRect.height);
                            interpretBytesAsShorts(byteArray, sdata,
                                                   unitsInThisTile);

                        }  else if (compression == COMP_DEFLATE) {

                            stream.readFully(data, 0, byteCount);
                            byte[] byteArray = new byte[unitsInThisTile * 2];
                            inflate(data, byteArray);
                            interpretBytesAsShorts(byteArray, sdata,
                                                   unitsInThisTile);

                        } else if (compression == COMP_NONE) {

                            readShorts(byteCount/2, sdata);
                        }

                        stream.seek(save_offset);

                    } catch (IOException ioe) {
                        throw new RuntimeException("TIFFImage13");
                    }
                }

            } else if (sampleSize == 8) {

                if (decodePaletteAsShorts) {

                    byte[] tempData= null;

                    // At this point the data is 1 banded and will
                    // become 3 banded only after we've done the palette
                    // lookup, since unitsInThisTile was calculated with
                    // 3 bands, we need to divide this by 3.
                    int unitsBeforeLookup = unitsInThisTile / 3;

                    // Read the data, if compressed, decode it, reset the pointer
                    try {

                        if (compression == COMP_PACKBITS) {

                            stream.readFully(data, 0, byteCount);
                            tempData = new byte[unitsBeforeLookup];
                            decodePackbits(data, unitsBeforeLookup, tempData);

                        }  else if (compression == COMP_LZW) {

                            stream.readFully(data, 0, byteCount);
                            tempData = new byte[unitsBeforeLookup];
                            lzwDecoder.decode(data, tempData, newRect.height);

                        } else if (compression == COMP_JPEG_TTN2) {

                            stream.readFully(data, 0, byteCount);
                            Raster tempTile = decodeJPEG(data,
                                                         decodeParam,
                                                         colorConvertJPEG,
                                                         tile.getMinX(),
                                                         tile.getMinY());
                            int[] tempPixels = new int[unitsBeforeLookup];
                            tempTile.getPixels(tile.getMinX(),
                                               tile.getMinY(),
                                               tile.getWidth(),
                                               tile.getHeight(),
                                               tempPixels);
                            tempData = new byte[unitsBeforeLookup];
                            for(int i = 0; i < unitsBeforeLookup; i++) {
                                tempData[i] = (byte)tempPixels[i];
                            }

                        }  else if (compression == COMP_DEFLATE) {

                            stream.readFully(data, 0, byteCount);
                            tempData = new byte[unitsBeforeLookup];
                            inflate(data, tempData);

                        } else if (compression == COMP_NONE) {

                            tempData = new byte[byteCount];
                            stream.readFully(tempData, 0, byteCount);
                        }

                        stream.seek(save_offset);

                    } catch (IOException ioe) {
                        throw new RuntimeException("TIFFImage13");
                    }

                    // Expand the palette image into an rgb image with ushort
                    // data type.
                    int cmapValue;
                    int count = 0, lookup, len = colormap.length/3;
                    int len2 = len * 2;
                    for (int i=0; i<unitsBeforeLookup; i++) {
                        // Get the index into the colormap
                        lookup = tempData[i] & 0xff;
                        // Get the blue value
                        cmapValue = colormap[lookup+len2];
                        sdata[count++] = (short)(cmapValue & 0xffff);
                        // Get the green value
                        cmapValue = colormap[lookup+len];
                        sdata[count++] = (short)(cmapValue & 0xffff);
                        // Get the red value
                        cmapValue = colormap[lookup];
                        sdata[count++] = (short)(cmapValue & 0xffff);
                    }
                } else {

                    // No lookup being done here, when RGB values are needed,
                    // the associated IndexColorModel can be used to get them.

                    try {

                        if (compression == COMP_PACKBITS) {

                            stream.readFully(data, 0, byteCount);
                            decodePackbits(data, unitsInThisTile, bdata);

                        } else if (compression == COMP_LZW) {

                            stream.readFully(data, 0, byteCount);
                            lzwDecoder.decode(data, bdata, newRect.height);

                        } else if (compression == COMP_JPEG_TTN2) {

                            stream.readFully(data, 0, byteCount);
                            tile.setRect(decodeJPEG(data,
                                                    decodeParam,
                                                    colorConvertJPEG,
                                                    tile.getMinX(),
                                                    tile.getMinY()));

                        }  else if (compression == COMP_DEFLATE) {

                            stream.readFully(data, 0, byteCount);
                            inflate(data, bdata);

                        } else if (compression == COMP_NONE) {

                            stream.readFully(bdata, 0, byteCount);
                        }

                        stream.seek(save_offset);

                    } catch (IOException ioe) {
                        throw new RuntimeException("TIFFImage13");
                    }
                }

            } else if (sampleSize == 4) {

                int padding = (newRect.width % 2 == 0) ? 0 : 1;
                int bytesPostDecoding = ((newRect.width/2 + padding) *
                                         newRect.height);

                // Output short images
                if (decodePaletteAsShorts) {

                    byte[] tempData = null;

                    try {
                        stream.readFully(data, 0, byteCount);
                        stream.seek(save_offset);
                    } catch (IOException ioe) {
                        throw new RuntimeException("TIFFImage13");
                    }

                    // If compressed, decode the data.
                    if (compression == COMP_PACKBITS) {

                        tempData = new byte[bytesPostDecoding];
                        decodePackbits(data, bytesPostDecoding, tempData);

                    }  else if (compression == COMP_LZW) {

                        tempData = new byte[bytesPostDecoding];
                        lzwDecoder.decode(data, tempData, newRect.height);

                    }  else if (compression == COMP_DEFLATE) {

                        tempData = new byte[bytesPostDecoding];
                        inflate(data, tempData);

                    } else if (compression == COMP_NONE) {

                        tempData = data;
                    }

                    int bytes = unitsInThisTile / 3;

                    // Unpack the 2 pixels packed into each byte.
                    data = new byte[bytes];

                    int srcCount = 0, dstCount = 0;
                    for (int j=0; j<newRect.height; j++) {
                        for (int i=0; i<newRect.width/2; i++) {
                            data[dstCount++] =
                                (byte)((tempData[srcCount] & 0xf0) >> 4);
                            data[dstCount++] =
                                (byte)(tempData[srcCount++] & 0x0f);
                        }

                        if (padding == 1) {
                            data[dstCount++] =
                                (byte)((tempData[srcCount++] & 0xf0) >> 4);
                        }
                    }

                    int len = colormap.length/3;
                    int len2 = len*2;
                    int cmapValue, lookup;
                    int count = 0;
                    for (int i=0; i<bytes; i++) {
                        lookup = data[i] & 0xff;
                        cmapValue = colormap[lookup+len2];
                        sdata[count++] = (short)(cmapValue & 0xffff);
                        cmapValue = colormap[lookup+len];
                        sdata[count++] = (short)(cmapValue & 0xffff);
                        cmapValue = colormap[lookup];
                        sdata[count++] = (short)(cmapValue & 0xffff);
                    }
                } else {

                    // Output byte values, use IndexColorModel for unpacking
                    try {

                        // If compressed, decode the data.
                        if (compression == COMP_PACKBITS) {

                            stream.readFully(data, 0, byteCount);
                            decodePackbits(data, bytesPostDecoding, bdata);

                        }  else if (compression == COMP_LZW) {

                            stream.readFully(data, 0, byteCount);
                            lzwDecoder.decode(data, bdata, newRect.height);

                        }  else if (compression == COMP_DEFLATE) {

                            stream.readFully(data, 0, byteCount);
                            inflate(data, bdata);

                        } else if (compression == COMP_NONE) {

                            stream.readFully(bdata, 0, byteCount);
                        }

                        stream.seek(save_offset);

                    } catch (IOException ioe) {
                        throw new RuntimeException("TIFFImage13");
                    }
                }
            }
        } else if(imageType == TYPE_GRAY_4BIT) { // 4-bit gray
            try {
                if (compression == COMP_PACKBITS) {

                    stream.readFully(data, 0, byteCount);

                    // Since the decompressed data will still be packed
                    // 2 pixels into 1 byte, calculate bytesInThisTile
                    int bytesInThisTile;
                    if ((newRect.width % 8) == 0) {
                        bytesInThisTile = (newRect.width/2) * newRect.height;
                    } else {
                        bytesInThisTile = (newRect.width/2 + 1) *
                            newRect.height;
                    }

                    decodePackbits(data, bytesInThisTile, bdata);

                } else if (compression == COMP_LZW) {

                    stream.readFully(data, 0, byteCount);
                    lzwDecoder.decode(data, bdata, newRect.height);

                }  else if (compression == COMP_DEFLATE) {

                    stream.readFully(data, 0, byteCount);
                    inflate(data, bdata);

                } else {

                    stream.readFully(bdata, 0, byteCount);
                }

                stream.seek(save_offset);
            } catch (IOException ioe) {
                throw new RuntimeException("TIFFImage13");
            }
        } else { // everything else
            try {

                if (sampleSize == 8) {

                    if (compression == COMP_NONE) {
                        stream.readFully(bdata, 0, byteCount);

                    } else if (compression == COMP_LZW) {

                        stream.readFully(data, 0, byteCount);
                        lzwDecoder.decode(data, bdata, newRect.height);

                    } else if (compression == COMP_PACKBITS) {

                        stream.readFully(data, 0, byteCount);
                        decodePackbits(data, unitsInThisTile, bdata);

                    } else if (compression == COMP_JPEG_TTN2) {

                        stream.readFully(data, 0, byteCount);
                        tile.setRect(decodeJPEG(data,
                                                decodeParam,
                                                colorConvertJPEG,
                                                tile.getMinX(),
                                                tile.getMinY()));
                    } else if (compression == COMP_DEFLATE) {

                        stream.readFully(data, 0, byteCount);
                        inflate(data, bdata);
                    }

                } else if (sampleSize == 16) {

                    if (compression == COMP_NONE) {

                        readShorts(byteCount/2, sdata);

                    } else if (compression == COMP_LZW) {

                        stream.readFully(data, 0, byteCount);

                        // Since unitsInThisTile is the number of shorts,
                        // but we do our decompression in terms of bytes, we
                        // need to multiply unitsInThisTile by 2 in order to
                        // figure out how many bytes we'll get after
                        // decompression.
                        byte[] byteArray = new byte[unitsInThisTile * 2];
                        lzwDecoder.decode(data, byteArray, newRect.height);
                        interpretBytesAsShorts(byteArray, sdata,
                                               unitsInThisTile);

                    } else if (compression == COMP_PACKBITS) {

                        stream.readFully(data, 0, byteCount);

                        // Since unitsInThisTile is the number of shorts,
                        // but we do our decompression in terms of bytes, we
                        // need to multiply unitsInThisTile by 2 in order to
                        // figure out how many bytes we'll get after
                        // decompression.
                        int bytesInThisTile = unitsInThisTile * 2;

                        byte[] byteArray = new byte[bytesInThisTile];
                        decodePackbits(data, bytesInThisTile, byteArray);
                        interpretBytesAsShorts(byteArray, sdata,
                                               unitsInThisTile);
                    } else if (compression == COMP_DEFLATE) {

                        stream.readFully(data, 0, byteCount);
                        byte[] byteArray = new byte[unitsInThisTile * 2];
                        inflate(data, byteArray);
                        interpretBytesAsShorts(byteArray, sdata,
                                               unitsInThisTile);

                    }
                } else if (sampleSize == 32 &&
                           dataType == DataBuffer.TYPE_INT) { // redundant
                    if (compression == COMP_NONE) {

                        readInts(byteCount/4, idata);

                    } else if (compression == COMP_LZW) {

                        stream.readFully(data, 0, byteCount);

                        // Since unitsInThisTile is the number of ints,
                        // but we do our decompression in terms of bytes, we
                        // need to multiply unitsInThisTile by 4 in order to
                        // figure out how many bytes we'll get after
                        // decompression.
                        byte[] byteArray = new byte[unitsInThisTile * 4];
                        lzwDecoder.decode(data, byteArray, newRect.height);
                        interpretBytesAsInts(byteArray, idata,
                                             unitsInThisTile);

                    } else if (compression == COMP_PACKBITS) {

                        stream.readFully(data, 0, byteCount);

                        // Since unitsInThisTile is the number of ints,
                        // but we do our decompression in terms of bytes, we
                        // need to multiply unitsInThisTile by 4 in order to
                        // figure out how many bytes we'll get after
                        // decompression.
                        int bytesInThisTile = unitsInThisTile * 4;

                        byte[] byteArray = new byte[bytesInThisTile];
                        decodePackbits(data, bytesInThisTile, byteArray);
                        interpretBytesAsInts(byteArray, idata,
                                             unitsInThisTile);
                    } else if (compression == COMP_DEFLATE) {

                        stream.readFully(data, 0, byteCount);
                        byte[] byteArray = new byte[unitsInThisTile * 4];
                        inflate(data, byteArray);
                        interpretBytesAsInts(byteArray, idata,
                                             unitsInThisTile);

                    }
                }

                stream.seek(save_offset);

            } catch (IOException ioe) {
                throw new RuntimeException("TIFFImage13");
            }

            // Modify the data for certain special cases.
            switch(imageType) {
            case TYPE_GRAY:
            case TYPE_GRAY_ALPHA:
                if(isWhiteZero) {
                    // Since we are using a ComponentColorModel with this
                    // image, we need to change the WhiteIsZero data to
                    // BlackIsZero data so it will display properly.
                    if (dataType == DataBuffer.TYPE_BYTE &&
                        !(getColorModel() instanceof IndexColorModel)) {

                        for (int l = 0; l < bdata.length; l += numBands) {
                            bdata[l] = (byte)(255 - bdata[l]);
                        }
                    } else if (dataType == DataBuffer.TYPE_USHORT) {

                        int ushortMax = Short.MAX_VALUE - Short.MIN_VALUE;
                        for (int l = 0; l < sdata.length; l += numBands) {
                            sdata[l] = (short)(ushortMax - sdata[l]);
                        }

                    } else if (dataType == DataBuffer.TYPE_SHORT) {

                        for (int l = 0; l < sdata.length; l += numBands) {
                            sdata[l] = (short)(~sdata[l]);
                        }
                    } else if (dataType == DataBuffer.TYPE_INT) {

                        long uintMax = ((long)Integer.MAX_VALUE -
                                        (long)Integer.MIN_VALUE);
                        for (int l = 0; l < idata.length; l += numBands) {
                            idata[l] = (int)(uintMax - idata[l]);
                        }
                    }
                }
                break;
            case TYPE_RGB:
                // Change RGB to BGR order, as Java2D displays that faster.
                // Unnecessary for JPEG-in-TIFF as the decoder handles it.
                if (sampleSize == 8 && compression != COMP_JPEG_TTN2) {
                    for (int i=0; i<unitsInThisTile; i+=3) {
                        bswap = bdata[i];
                        bdata[i] = bdata[i+2];
                        bdata[i+2] = bswap;
                    }
                } else if (sampleSize == 16) {
                    for (int i=0; i<unitsInThisTile; i+=3) {
                        sswap = sdata[i];
                        sdata[i] = sdata[i+2];
                        sdata[i+2] = sswap;
                    }
                } else if (sampleSize == 32) {
                    if(dataType == DataBuffer.TYPE_INT) {
                        for (int i=0; i<unitsInThisTile; i+=3) {
                            iswap = idata[i];
                            idata[i] = idata[i+2];
                            idata[i+2] = iswap;
                        }
                    }
                }
                break;
            case TYPE_RGB_ALPHA:
                // Convert from RGBA to ABGR for Java2D
                if (sampleSize == 8) {
                    for (int i=0; i<unitsInThisTile; i+=4) {
                        // Swap R and A
                        bswap = bdata[i];
                        bdata[i] = bdata[i+3];
                        bdata[i+3] = bswap;

                        // Swap G and B
                        bswap = bdata[i+1];
                        bdata[i+1] = bdata[i+2];
                        bdata[i+2] = bswap;
                    }
                } else if (sampleSize == 16) {
                    for (int i=0; i<unitsInThisTile; i+=4) {
                        // Swap R and A
                        sswap = sdata[i];
                        sdata[i] = sdata[i+3];
                        sdata[i+3] = sswap;

                        // Swap G and B
                        sswap = sdata[i+1];
                        sdata[i+1] = sdata[i+2];
                        sdata[i+2] = sswap;
                    }
                } else if (sampleSize == 32) {
                    if(dataType == DataBuffer.TYPE_INT) {
                        for (int i=0; i<unitsInThisTile; i+=4) {
                            // Swap R and A
                            iswap = idata[i];
                            idata[i] = idata[i+3];
                            idata[i+3] = iswap;

                            // Swap G and B
                            iswap = idata[i+1];
                            idata[i+1] = idata[i+2];
                            idata[i+2] = iswap;
                        }
                    }
                }
                break;
            case TYPE_YCBCR_SUB:
                // Post-processing for YCbCr with subsampled chrominance:
                // simply replicate the chroma channels for displayability.
                int pixelsPerDataUnit = chromaSubH*chromaSubV;

                int numH = newRect.width/chromaSubH;
                int numV = newRect.height/chromaSubV;

                byte[] tempData = new byte[numH*numV*(pixelsPerDataUnit + 2)];
                System.arraycopy(bdata, 0, tempData, 0, tempData.length);

                int samplesPerDataUnit = pixelsPerDataUnit*3;
                int[] pixels = new int[samplesPerDataUnit];

                int bOffset = 0;
                int offsetCb = pixelsPerDataUnit;
                int offsetCr = offsetCb + 1;

                int y = newRect.y;
                for(int j = 0; j < numV; j++) {
                    int x = newRect.x;
                    for(int i = 0; i < numH; i++) {
                        int Cb = tempData[bOffset + offsetCb];
                        int Cr = tempData[bOffset + offsetCr];
                        int k = 0;
                        while(k < samplesPerDataUnit) {
                            pixels[k++] = tempData[bOffset++];
                            pixels[k++] = Cb;
                            pixels[k++] = Cr;
                        }
                        bOffset += 2;
                        tile.setPixels(x, y, chromaSubH, chromaSubV, pixels);
                        x += chromaSubH;
                    }
                    y += chromaSubV;
                }

                break;
            }
        }

        return tile;
    }

    private void readShorts(int shortCount, short[] shortArray) {

        // Since each short consists of 2 bytes, we need a
        // byte array of double size
        int byteCount = 2 * shortCount;
        byte[] byteArray = new byte[byteCount];

        try {
            stream.readFully(byteArray, 0, byteCount);
        } catch (IOException ioe) {
            throw new RuntimeException("TIFFImage13");
        }

        interpretBytesAsShorts(byteArray, shortArray, shortCount);
    }

    private void readInts(int intCount, int[] intArray) {

        // Since each int consists of 4 bytes, we need a
        // byte array of quadruple size
        int byteCount = 4 * intCount;
        byte[] byteArray = new byte[byteCount];

        try {
            stream.readFully(byteArray, 0, byteCount);
        } catch (IOException ioe) {
            throw new RuntimeException("TIFFImage13");
        }

        interpretBytesAsInts(byteArray, intArray, intCount);
    }

    // Method to interpret a byte array to a short array, depending on
    // whether the bytes are stored in a big endian or little endian format.
    private void interpretBytesAsShorts(byte[] byteArray,
                                        short[] shortArray,
                                        int shortCount) {

        int j = 0;
        int firstByte, secondByte;

        if (isBigEndian) {

            for (int i=0; i<shortCount; i++) {
                firstByte = byteArray[j++] & 0xff;
                secondByte = byteArray[j++] & 0xff;
                shortArray[i] = (short)((firstByte << 8) + secondByte);
            }

        } else {

            for (int i=0; i<shortCount; i++) {
                firstByte = byteArray[j++] & 0xff;
                secondByte = byteArray[j++] & 0xff;
                shortArray[i] = (short)((secondByte << 8) + firstByte);
            }
        }
    }

    // Method to interpret a byte array to a int array, depending on
    // whether the bytes are stored in a big endian or little endian format.
    private void interpretBytesAsInts(byte[] byteArray,
                                      int[] intArray,
                                      int intCount) {

        int j = 0;

        if (isBigEndian) {

            for (int i=0; i<intCount; i++) {
                intArray[i] = (((byteArray[j++] & 0xff) << 24) |
                               ((byteArray[j++] & 0xff) << 16) |
                               ((byteArray[j++] & 0xff) << 8) |
                               ( byteArray[j++] & 0xff));
            }

        } else {

            for (int i=0; i<intCount; i++) {
                intArray[i] = ((byteArray[j++] & 0xff) |
                              ((byteArray[j++] & 0xff) << 8) |
                              ((byteArray[j++] & 0xff) << 16) |
                              ((byteArray[j++] & 0xff) << 24));
            }
        }
    }

    // Uncompress packbits compressed image data.
    private byte[] decodePackbits(byte[] data, int arraySize, byte[] dst) {

        if (dst == null) {
            dst = new byte[arraySize];
        }

        int srcCount = 0, dstCount = 0;
        byte repeat, b;

        try {

            while (dstCount < arraySize) {

                b = data[srcCount++];

                if (b >= 0 && b <= 127) {

                    // literal run packet
                    for (int i=0; i<(b + 1); i++) {
                        dst[dstCount++] = data[srcCount++];
                    }

                } else if (b <= -1 && b >= -127) {

                    // 2 byte encoded run packet
                    repeat = data[srcCount++];
                    for (int i=0; i<(-b + 1); i++) {
                        dst[dstCount++] = repeat;
                    }

                } else {
                    // no-op packet. Do nothing
                    srcCount++;
                }
            }
        } catch (java.lang.ArrayIndexOutOfBoundsException ae) {
            throw new RuntimeException("TIFFImage14");
        }

        return dst;
    }

    // Need a createColorModel().
    // Create ComponentColorModel for TYPE_RGB images
    private ComponentColorModel createAlphaComponentColorModel
    (int dataType, int numBands,
     boolean isAlphaPremultiplied, int transparency) {

        ComponentColorModel ccm = null;
        int[] RGBBits = null;
        ColorSpace cs = null;
        switch(numBands) {
            case 2: // gray+alpha
                cs = ColorSpace.getInstance(ColorSpace.CS_GRAY);
                break;
            case 4: // RGB+alpha
                cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
                break;
            default:
                throw new IllegalArgumentException();
        }

        int componentSize = 0;
        switch(dataType) {
            case DataBuffer.TYPE_BYTE:
                componentSize = 8;
                break;
            case DataBuffer.TYPE_USHORT:
            case DataBuffer.TYPE_SHORT:
                componentSize = 16;
                break;
            case DataBuffer.TYPE_INT:
                componentSize = 32;
                break;
            default:
                throw new IllegalArgumentException();
        }

        RGBBits = new int[numBands];
        for(int i = 0; i < numBands; i++) {
            RGBBits[i] = componentSize;
        }

        ccm = new ComponentColorModel(cs,
                                      RGBBits,
                                      true,
                                      isAlphaPremultiplied,
                                      transparency,
                                      dataType);


        return ccm;
    }

}
