/*
 * $RCSfile: TIFFImage.java,v $
 *
 * Copyright (c) 2005 Sun Microsystems, Inc. All rights reserved.
 *
 * Use is subject to license terms.
 *
 * $Revision: 1.6 $
 * $Date: 2006/02/17 17:59:15 $
 * $State: Exp $
 */
package com.lightcrafts.media.jai.codecimpl;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Transparency;
import java.awt.color.ColorSpace;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferShort;
import java.awt.image.DataBufferUShort;
import java.awt.image.DataBufferInt;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.awt.image.SampleModel;
import java.awt.image.IndexColorModel;
import java.awt.image.MultiPixelPackedSampleModel;
import java.awt.image.ComponentColorModel;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.text.MessageFormat;
import java.util.Locale;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;
import com.lightcrafts.media.jai.codec.ImageCodec;
import com.lightcrafts.media.jai.codec.SeekableStream;
import com.lightcrafts.media.jai.codec.TIFFDecodeParam;
import com.lightcrafts.media.jai.codec.TIFFDirectory;
import com.lightcrafts.media.jai.codec.TIFFField;
import com.lightcrafts.media.jai.codecimpl.util.DataBufferFloat;
import com.lightcrafts.media.jai.codecimpl.util.FloatDoubleColorModel;
import com.lightcrafts.media.jai.codecimpl.util.RasterFactory;
import com.sun.image.codec.jpeg.JPEGCodec;
import com.sun.image.codec.jpeg.JPEGDecodeParam;
import com.sun.image.codec.jpeg.JPEGImageDecoder;
import com.lightcrafts.media.jai.codecimpl.ImagingListenerProxy;
import com.lightcrafts.media.jai.codecimpl.util.ImagingException;
import com.lightcrafts.media.jai.util.SimpleCMYKColorSpace;

public class TIFFImage extends SimpleRenderedImage {

    // 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;
    private static final int TYPE_CMYK         = 9;

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

    SeekableStream stream;
    private boolean isTiled;
    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;

    // 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 = null;
        try {
            jpegRaster = colorConvert ?
                decoder.decodeAsBufferedImage().getWritableTile(0, 0) :
                decoder.decodeAsRaster();
        } catch (IOException ioe) {
            String message = JaiI18N.getString("TIFFImage13");
            ImagingListenerProxy.errorOccurred(message,
                                   new ImagingException(message, ioe),
                                   TIFFImage.class, false);
//            throw new RuntimeException(JaiI18N.getString("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) {
            String message = JaiI18N.getString("TIFFImage17");
            ImagingListenerProxy.errorOccurred(message,
                                   new ImagingException(message, dfe),
                                   this, false);
//            throw new RuntimeException(JaiI18N.getString("TIFFImage17")+": "+
//                                       dfe.getMessage());
        }
        inflater.reset();
    }

    /**
     * Creates a pixel-interleaved <code>SampleModel</code>. This is a hack
     * to work around a cast exception when using JAI with float data.
     */
    private final static SampleModel
        createPixelInterleavedSampleModel(int dataType,
                                          int tileWidth,
                                          int tileHeight,
                                          int pixelStride,
                                          int scanlineStride,
                                          int bandOffsets[]) {
        SampleModel sampleModel = null;

        if(dataType == DataBuffer.TYPE_FLOAT) {
            // This is a hack to make this work with JAI which in some
            // cases downcasts the DataBuffer to a type-specific class.
            // In the case of float data this current means the JAI class
            // com.lightcrafts.mediax.jai.DataBufferFloat.
            try {
                Class rfClass =
                    Class.forName("com.lightcrafts.mediax.jai.RasterFactory");
                Class[] paramTypes = new Class[] {int.class, int.class,
                                                  int.class, int.class,
                                                  int.class, int[].class};
                Method rfMthd =
                    rfClass.getMethod("createPixelInterleavedSampleModel",
                                      paramTypes);
                Object[] params =
                    new Object[] {new Integer(dataType),
                                  new Integer(tileWidth),
                                  new Integer(tileHeight),
                                  new Integer(pixelStride),
                                  new Integer(scanlineStride),
                                  bandOffsets};
                sampleModel = (SampleModel)rfMthd.invoke(null, params);
            } catch(Exception e) {
                // Deliberately ignore the Exception.
            }
        }

        // Create a SampleModel for non-float data or, in the case of
        // float data, if it is still null. This latter case should occur
        // if and only if the decoder is being used without JAI.
        if(dataType != DataBuffer.TYPE_FLOAT || sampleModel == null) {
            sampleModel =
                RasterFactory.createPixelInterleavedSampleModel(dataType,
                                                                tileWidth,
                                                                tileHeight,
                                                                pixelStride,
                                                                scanlineStride,
                                                                bandOffsets);
        }

        return sampleModel;
    }

    /**
     * Return as a long[] the value of a TIFF_LONG or TIFF_SHORT field.
     */
    private final 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;
    }

    /*
     * Check whether the specified tag exists in the specified
     * TIFFDirectory. If not, throw an error message. Otherwise
     * return the TIFFField.
     */
    private TIFFField getField(TIFFDirectory dir, int tagID, String tagName) {
        TIFFField field = dir.getField(tagID);
        if (field == null) {
            MessageFormat mf =
                new MessageFormat(JaiI18N.getString("TIFFImage5"));
            mf.setLocale(Locale.getDefault());
            throw new RuntimeException(mf.format(new Object[] {tagName}));
        } else {
	    return field;
        }
    }

    /**
     * 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);

        // Set a property "tiff_directory".
        properties.put("tiff_directory", dir);

	// 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(JaiI18N.getString("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(
					     JaiI18N.getString("TIFFImage1"));
                }
            }
        }
        sampleSize = (int)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(
					     JaiI18N.getString("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:
            dataType = sampleFormat[0] == 3 ?
                DataBuffer.TYPE_FLOAT : DataBuffer.TYPE_INT;
            isValidDataFormat = true;
            break;
        }

        if(!isValidDataFormat) {
            throw new RuntimeException(JaiI18N.getString("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 field.
        TIFFField photoInterpField =
            dir.getField(TIFFImageDecoder.TIFF_PHOTOMETRIC_INTERPRETATION);

        // Set the photometric interpretation variable.
        int photometricType;
        if(photoInterpField != null) {
            // Set the variable from the photometric interpretation field.
            photometricType = (int)photoInterpField.getAsLong(0);
        } else {
            // The photometric interpretation field is missing; attempt
            // to infer the type from other information.
            if(dir.getField(TIFFImageDecoder.TIFF_COLORMAP) != null) {
                // There is a colormap so most likely a palette color image.
                photometricType = 3; // RGB Palette
            } else if(sampleSize == 1) {
                // Bilevel image so most likely a document; switch based
                // on the compression type of the image.
                if(compression == COMP_FAX_G3_1D ||
                   compression == COMP_FAX_G3_2D ||
                   compression == COMP_FAX_G4_2D) {
                    photometricType = 0; // WhiteIsZero
                } else {
                    photometricType = 1; // BlackIsZero
                }
            } else if(samplesPerPixel == 3 || samplesPerPixel == 4) {
                // Assume 3 bands is RGB and 4 bands is RGBA.
                photometricType = 2; // RGB
            } else {
                // Default to multi-band grayscale.
                photometricType = 1; // BlackIsZero
            }
        }

        // 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 5: // Separated image, usually CMYK
	    if (sampleSize == 8 && samplesPerPixel == 4) {
		imageType = TYPE_CMYK;
	    }
        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 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(JaiI18N.getString("TIFFImage4"));
        }

	// Set basic image layout
        minX = minY = 0;
	width = (int)(getField(dir,
			       TIFFImageDecoder.TIFF_IMAGE_WIDTH,
			       "Image Width").getAsLong(0));

	height = (int)(getField(dir,
				TIFFImageDecoder.TIFF_IMAGE_LENGTH,
				"Image Length").getAsLong(0));

        // 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);

	if (dir.getField(TIFFImageDecoder.TIFF_TILE_OFFSETS) != null) {
	    // Image is in tiled format
            isTiled = true;

            tileWidth = (int)(getField(dir,
				       TIFFImageDecoder.TIFF_TILE_WIDTH,
				       "Tile Width").getAsLong(0));
	    tileHeight = (int)(getField(dir,
					TIFFImageDecoder.TIFF_TILE_LENGTH,
					"Tile Length").getAsLong(0));
	    tileOffsets =
		(getField(dir,
			 TIFFImageDecoder.TIFF_TILE_OFFSETS,
			 "Tile Offsets")).getAsLongs();

	    tileByteCounts = getFieldAsLongs(
	               getField(dir,
	                        TIFFImageDecoder.TIFF_TILE_BYTE_COUNTS,
	                        "Tile Byte Counts"));

        } else {

            // Image is in stripped format, looks like tiles to us
            isTiled = false;

            // 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) :
                width;
	    TIFFField field =
                dir.getField(TIFFImageDecoder.TIFF_ROWS_PER_STRIP);
	    if (field == null) {
		// Default is infinity (2^32 -1), basically the entire image
		// TODO: Can do a better job of tiling here
		tileHeight =
                    dir.getField(TIFFImageDecoder.TIFF_TILE_LENGTH) != null ?
                    (int)dir.getFieldAsLong(TIFFImageDecoder.TIFF_TILE_LENGTH):
                    height;
	    } else {
		long l = field.getAsLong(0);
		long infinity = 1;
		infinity = (infinity << 32) - 1;
		if (l == infinity || l > height) {
		    // 2^32 - 1 (effectively infinity, entire image is 1 strip)
                    // or RowsPerStrip > ImageLength so clamp as having a tile
                    // larger than the image is pointless.
		    tileHeight = height;
		} else {
		    tileHeight = (int)l;
		}
	    }

	    TIFFField tileOffsetsField =
		getField(dir,
			 TIFFImageDecoder.TIFF_STRIP_OFFSETS,
			 "Strip Offsets");
	    tileOffsets = getFieldAsLongs(tileOffsetsField);

	    TIFFField tileByteCountsField =
                dir.getField(TIFFImageDecoder.TIFF_STRIP_BYTE_COUNTS);
            if(tileByteCountsField == null) {
                // Attempt to infer the number of bytes in each strip.
                int totalBytes = ((sampleSize+7)/8)*numBands*width*height;
                int bytesPerStrip =
                    ((sampleSize+7)/8)*numBands*width*tileHeight;
                int cumulativeBytes = 0;
                tileByteCounts = new long[tileOffsets.length];
                for(int i = 0; i < tileOffsets.length; i++) {
                    tileByteCounts[i] =
                        Math.min(totalBytes - cumulativeBytes,
                                 bytesPerStrip);
                    cumulativeBytes += bytesPerStrip;
                }

                if(compression != COMP_NONE) {
                    // Replace the stream with one that will not throw
                    // an EOFException when it runs past the end.
                    this.stream = new NoEOFStream(stream);
                }
            } else {
                tileByteCounts = getFieldAsLongs(tileByteCountsField);
            }

            // Uncompressed image provided in a single tile: clamp to max bytes.
            int maxBytes = width*height*numBands*((sampleSize + 7)/8);
            if(tileByteCounts.length == 1 &&
               compression == COMP_NONE &&
               tileByteCounts[0] > maxBytes) {
                tileByteCounts[0] = maxBytes;
            }
	}

	// Calculate number of tiles and the tileSize in bytes
        tilesX = (width + tileWidth - 1)/tileWidth;
        tilesY = (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(JaiI18N.getString("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(JaiI18N.getString("TIFFImage8"));
                }

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

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

        case COMP_JPEG_OLD:
            throw new RuntimeException(JaiI18N.getString("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(JaiI18N.getString("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(JaiI18N.getString("TIFFImage10"));
	}

        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 {
                colorModel =
                    ImageCodec.createGrayIndexColorModel(sampleModel,
                                                         !isWhiteZero);
            }
            break;

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

            if(imageType == TYPE_GRAY || imageType == TYPE_RGB) {
                colorModel =
                    ImageCodec.createComponentColorModel(sampleModel);
            } else if (imageType == TYPE_CMYK) {
		colorModel = ImageCodec.createComponentColorModel(sampleModel,
								  SimpleCMYKColorSpace.getInstance());
	    } 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 || extraSamples == 2) { 
		    // associated (premultiplied) alpha when == 1
		    // unassociated alpha when ==2
		    // Fix bug: 4699316
                    transparency = Transparency.TRANSLUCENT;
                } 

                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 =
                createPixelInterleavedSampleModel(dataType,
                                                  tileWidth, tileHeight,
                                                  numBands, numBands * tileWidth,
                                                  bandOffsets);
            colorModel = null;
            break;

        case TYPE_PALETTE:
	    // Get the colormap
	    TIFFField cfield = getField(dir, TIFFImageDecoder.TIFF_COLORMAP,
					"Colormap");
	    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 =
		    RasterFactory.createPixelInterleavedSampleModel(dataType,
								    tileWidth,
								    tileHeight,
								    numBands);
		colorModel = ImageCodec.createComponentColorModel(sampleModel);

	    } 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 =
			RasterFactory.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 =
			RasterFactory.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");
        }
    }

    /**
     * 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);
    }

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

        // The tile to return.
        WritableRaster tile = null;

        // Synchronize the rest of the method in case other TIFFImage
        // instances using the same stream were created by the same
        // TIFFImageDecoder. This fixes 4690773.
        synchronized(this.stream) {

	// Get the data array out of the DataBuffer
	byte bdata[] = null;
	short sdata[] = null;
	int idata[] = null;
	float fdata[] = null;
	DataBuffer buffer = sampleModel.createDataBuffer();

	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();
	} else if (dataType == DataBuffer.TYPE_FLOAT) {
            if(buffer instanceof DataBufferFloat) {
                fdata = ((DataBufferFloat)buffer).getData();
            } else {
                // This is a hack to make this work with JAI which in some
                // cases downcasts the DataBuffer to a type-specific class.
                // In the case of float data this current means the JAI class
                // com.lightcrafts.mediax.jai.DataBufferFloat.
                try {
                    Method getDataMethod =
                        buffer.getClass().getMethod("getData", null);
                    fdata = (float[])getDataMethod.invoke(buffer, null);
                } catch(Exception e) {
                    String message = JaiI18N.getString("TIFFImage18");
                    ImagingListenerProxy.errorOccurred(message,
                                           new ImagingException(message, e),
                                           this, false);
//                    throw new RuntimeException(JaiI18N.getString("TIFFImage18"));
                }
            }
	}

        tile =
	    (WritableRaster)RasterFactory.createWritableRaster(sampleModel,
                                                   buffer,
                                                   new Point(tileXToX(tileX),
                                                             tileYToY(tileY)));

	// 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) {
            String message = JaiI18N.getString("TIFFImage13");
            ImagingListenerProxy.errorOccurred(message,
                                   new ImagingException(message, ioe),
                                   this, false);
//	    throw new RuntimeException(JaiI18N.getString("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. If the image is
        // tiled this may include pixels which are outside of the image bounds
        // if the image width and height are not multiples of the tile width
        // and height respectively.
	Rectangle tileRect = new Rectangle(tileXToX(tileX), tileYToY(tileY),
					   tileWidth, tileHeight);
	Rectangle newRect = isTiled ?
            tileRect : tileRect.intersection(getBounds());
        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) {
                String message = JaiI18N.getString("TIFFImage13");
                ImagingListenerProxy.errorOccurred(message,
                                       new ImagingException(message, ioe),
                                       this, false);
//		throw new RuntimeException(JaiI18N.getString("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) {
                        String message = JaiI18N.getString("TIFFImage13");
                        ImagingListenerProxy.errorOccurred(message,
                                               new ImagingException(message, ioe),
                                               this, false);
//			throw new RuntimeException(
//					JaiI18N.getString("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) {
                        String message = JaiI18N.getString("TIFFImage13");
                        ImagingListenerProxy.errorOccurred(message,
                                               new ImagingException(message, ioe),
                                               this, false);
//			throw new RuntimeException(
//					JaiI18N.getString("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) {
                        String message = JaiI18N.getString("TIFFImage13");
                        ImagingListenerProxy.errorOccurred(message,
                                               new ImagingException(message, ioe),
                                               this, false);
//		throw new RuntimeException(
//					JaiI18N.getString("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) {
                        String message = JaiI18N.getString("TIFFImage13");
                        ImagingListenerProxy.errorOccurred(message,
                                               new ImagingException(message, ioe),
                                               this, false);
//		throw new RuntimeException(
//					JaiI18N.getString("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) {
                        String message = JaiI18N.getString("TIFFImage13");
                        ImagingListenerProxy.errorOccurred(message,
                                               new ImagingException(message, ioe),
                                               this, false);
//			throw new RuntimeException(
//					JaiI18N.getString("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) {
                        String message = JaiI18N.getString("TIFFImage13");
                        ImagingListenerProxy.errorOccurred(message,
                                               new ImagingException(message, ioe),
                                               this, false);
//			throw new RuntimeException(
//					JaiI18N.getString("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) {
                String message = JaiI18N.getString("TIFFImage13");
                ImagingListenerProxy.errorOccurred(message,
                                       new ImagingException(message, ioe),
                                       this, false);
//		throw new RuntimeException(JaiI18N.getString("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);

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

			readFloats(byteCount/4, fdata);

		    } else if (compression == COMP_LZW) {

			stream.readFully(data, 0, byteCount);

			// Since unitsInThisTile is the number of floats,
			// 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);
			interpretBytesAsFloats(byteArray, fdata,
                                               unitsInThisTile);

		    } else if (compression == COMP_PACKBITS) {

			stream.readFully(data, 0, byteCount);

			// Since unitsInThisTile is the number of floats,
			// 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);
			interpretBytesAsFloats(byteArray, fdata,
                                               unitsInThisTile);
		    } else if (compression == COMP_DEFLATE) {

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

                    }
		}

		stream.seek(save_offset);

	    } catch (IOException ioe) {
                String message = JaiI18N.getString("TIFFImage13");
                ImagingListenerProxy.errorOccurred(message,
                                       new ImagingException(message, ioe),
                                       this, false);
//		throw new RuntimeException(JaiI18N.getString("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 &&
                        !(colorModel 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 = Integer.MAX_VALUE - Integer.MIN_VALUE;
                        for (int l = 0; l < idata.length; l += numBands) {
                            idata[l] = (int)(uintMax - (long)idata[l]);
                        }
                    }
                }
                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;
            }
        }

        } // synchronized(this.stream)

        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) {
            String message = JaiI18N.getString("TIFFImage13");
            ImagingListenerProxy.errorOccurred(message,
                                   new ImagingException(message, ioe),
                                   this, false);
//	   throw new RuntimeException(JaiI18N.getString("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) {
            String message = JaiI18N.getString("TIFFImage13");
            ImagingListenerProxy.errorOccurred(message,
                                   new ImagingException(message, ioe),
                                   this, false);
//	   throw new RuntimeException(JaiI18N.getString("TIFFImage13"));
	}

	interpretBytesAsInts(byteArray, intArray, intCount);
    }

    private void readFloats(int floatCount, float floatArray[]) {

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

	try {
	    stream.readFully(byteArray, 0, byteCount);
	} catch (IOException ioe) {
            String message = JaiI18N.getString("TIFFImage13");
            ImagingListenerProxy.errorOccurred(message,
                                   new ImagingException(message, ioe),
                                   this, false);
//	   throw new RuntimeException(JaiI18N.getString("TIFFImage13"));
	}

	interpretBytesAsFloats(byteArray, floatArray, floatCount);
    }

    // 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] =
                    (int)(((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] =
                    (int)((byteArray[j++] & 0xff) |
                          ((byteArray[j++] & 0xff) << 8) |
                          ((byteArray[j++] & 0xff) << 16) |
                          ((byteArray[j++] & 0xff) << 24));
	    }
	}
    }

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

	int j = 0;

	if (isBigEndian) {

	    for (int i=0; i<floatCount; i++) {
                int value = (int)(((byteArray[j++] & 0xff) << 24) |
                                  ((byteArray[j++] & 0xff) << 16) |
                                  ((byteArray[j++] & 0xff) << 8) |
                                  (byteArray[j++] & 0xff));
		floatArray[i] = Float.intBitsToFloat(value);
	    }

	} else {

	    for (int i=0; i<floatCount; i++) {
		int value = (int)((byteArray[j++] & 0xff) |
                                  ((byteArray[j++] & 0xff) << 8) |
                                  ((byteArray[j++] & 0xff) << 16) |
                                  ((byteArray[j++] & 0xff) << 24));
		floatArray[i] = Float.intBitsToFloat(value);
	    }
	}
    }

    // 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;
        int srcArraySize = data.length;
	byte repeat, b;

	try {

	    while (dstCount < arraySize && srcCount < srcArraySize) {

		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) {
            String message = JaiI18N.getString("TIFFImage14");
            ImagingListenerProxy.errorOccurred(message,
                                   new ImagingException(message, ae),
                                   this, false);
//	    throw new RuntimeException(JaiI18N.getString("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();
        }

        if(dataType == DataBuffer.TYPE_FLOAT) {
            ccm = new FloatDoubleColorModel(cs,
                                            true,
                                            isAlphaPremultiplied,
                                            transparency,
                                            dataType);
        } else { // all other types
            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;
    }
}

/**
 * Wrapper class for a <code>SeekableStream</code> but which does not throw
 * an <code>EOFException</code> from <code>readFully()</code> when the end
 * of stream is encountered.
 */
// NB This is a hack to fix bug 4823200 "Make TIFF decoder robust to (comp)
// images with no strip/tile byte counts field" but there does not seem to
// be any other way to work around this without extensive code changes.
class NoEOFStream extends SeekableStream {
    private SeekableStream stream;

    NoEOFStream(SeekableStream ss) {
        if(ss == null) {
            throw new IllegalArgumentException();
        }

        this.stream = ss;
    }

    public int read() throws IOException {
        int b = stream.read();
        return b < 0 ? 0 : b;
    }

    public int read(byte[] b, int off, int len) throws IOException {
        int count = stream.read(b, off, len);
        return count < 0 ? len : count;
    }

    public long getFilePointer() throws IOException {
        return stream.getFilePointer();
    }

    public void seek(long pos) throws IOException {
        stream.seek(pos);
    }
}
