/*
 * @(#)JPEGImageReader.java	1.35 01/03/23
 *
 * Copyright 2000 by Sun Microsystems, Inc.,
 * 901 San Antonio Road, Palo Alto, California, 94303, U.S.A.
 * All rights reserved.
 *
 * This software is the confidential and proprietary information
 * of Sun Microsystems, Inc. ("Confidential Information").  You
 * shall not disclose such Confidential Information and shall use
 * it only in accordance with the terms of the license agreement
 * you entered into with Sun.
 */

package com.sun.imageio.plugins.jpeg;

import javax.imageio.IIOException;
import javax.imageio.ImageReader;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageReaderSpi;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.plugins.jpeg.JPEGImageReadParam;
import javax.imageio.plugins.jpeg.JPEGQTable;
import javax.imageio.plugins.jpeg.JPEGHuffmanTable;

import java.awt.Point;
import java.awt.Rectangle;
import java.awt.color.ColorSpace;
import java.awt.image.BufferedImage;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.ColorModel;
import java.awt.image.IndexColorModel;
import java.awt.image.ColorConvertOp;
import java.io.IOException;
import java.util.List;
import java.util.Iterator;
import java.util.ArrayList;

public class JPEGImageReader extends ImageReader {

    private boolean debug = false;

    private ImageInputStream iis = null;

    /**
     * List of stream positions for images, reinitialized every time
     * a new input source is set.  
     */
    private List imagePositions = null;

    /**
     * The number of images in the stream, or 0.
     */
    private int numImages = 0;

    static {
        java.security.AccessController.doPrivileged(
            new sun.security.action.LoadLibraryAction("jpeg"));
        initReaderIDs(ImageInputStream.class);
    }

    private String [] warnings = {
        "Truncated File - Missing EOI marker",  // 0
        "JFIF markers not allowed in JFIF JPEG thumbnail; ignored"
    };

    /**
     * Warning code to be passed to warningOccurred to indicate
     * that the EOI marker is missing from the end of the stream.
     * This usually signals that the stream is corrupted, but
     * everything up to the last MCU should be usable.
     */
    protected static final int WARNING_NO_EOI = 0;

    /**
     * Warning code to be passed to warningOccurred to indicate
     * that a JFIF segment was encountered inside a JFXX JPEG
     * thumbnail and is being ignored.
     */
    protected static final int WARNING_NO_JFIF_IN_THUMB = 1;

    /**
     * Image index of image for which header information 
     * is available.
     */
    private int currentImage = -1;
    
    // The following is copied out from C after reading the header.
    // Unlike metadata, which may never be retrieved, we need this
    // if we are to read an image at all.

    /** Set by native code */
    private int width;
    /** Set by native code */
    private int height;
    /** Set by native code.  A modified IJG+NIFTY colorspace code. */
    private int colorSpaceCode;
    /** Set by native code.  A modified IJG+NIFTY colorspace code. */
    private int outColorSpaceCode;
    /** Set by native code */
    private int numComponents;

    /** If we need to post-convert in Java, convert with this op */
    private ColorConvertOp convert = null;

    /** The image we are going to fill */
    private BufferedImage image = null;

    /** An intermediate Raster to hold decoded data */
    private WritableRaster raster = null;

    /** A view of our target Raster that we can setRect to */
    private WritableRaster target = null;

    /** The databuffer for the above Raster */
    private DataBufferByte buffer = null;

    /** The region in the destination where we will write pixels */
    private Rectangle destROI = null;

    /** The list of destination bands, if any */
    private int [] destinationBands = null;

    /** Stream metadata, cached, even when the stream is changed. */
    private JPEGMetadata streamMetadata = null;

    /** 
     * Set to true every time we seek in the stream; used to 
     * invalidate the native buffer contents in C.
     */
    private boolean haveSeeked = false;

    private JPEGQTable [] abbrevQTables = null;
    private JPEGHuffmanTable[] abbrevDCHuffmanTables = null;
    private JPEGHuffmanTable[] abbrevACHuffmanTables = null;

    private int minProgressivePass = 0;
    private int maxProgressivePass = Integer.MAX_VALUE;

    /**
     * Set to true once stream has been checked for stream metadata
     */
    private boolean tablesOnlyChecked = false;

    private static final ImageTypeSpecifier [] defaultTypes = 
        new ImageTypeSpecifier [JPEG.NUM_JCS_CODES];

    static {
        defaultTypes[JPEG.JCS_GRAYSCALE] = 
            ImageTypeSpecifier.createFromBufferedImageType
            (BufferedImage.TYPE_BYTE_GRAY);
        defaultTypes[JPEG.JCS_RGB] =
            ImageTypeSpecifier.createInterleaved
            (JPEG.sRGB,
             JPEG.bOffsRGB,
             DataBuffer.TYPE_BYTE,
             false,
             false);
        defaultTypes[JPEG.JCS_RGBA] =
            ImageTypeSpecifier.createPacked
            (JPEG.sRGB,
             0xff000000,
             0x00ff0000,
             0x0000ff00,
             0x000000ff,
             DataBuffer.TYPE_INT,
             false);
        if (JPEG.YCC != null) {
            defaultTypes[JPEG.JCS_YCC] =
                ImageTypeSpecifier.createInterleaved
                (JPEG.YCC,
                 JPEG.bandOffsets[2],
                 DataBuffer.TYPE_BYTE,
                 false,
                 false);
            defaultTypes[JPEG.JCS_YCCA] =
                ImageTypeSpecifier.createInterleaved
                (JPEG.YCC,
                 JPEG.bandOffsets[3],
                 DataBuffer.TYPE_BYTE,
                 true,
                 false);
        }
    }

    /** Sets up static C structures. */
    private static native void initReaderIDs(Class iisClass);

    public JPEGImageReader(ImageReaderSpi originator) {
        super(originator);
        // Synchronize to protect the native linked list
        synchronized (JPEGImageReader.class) {
            initJPEGImageReader();
        }
    }

    /** Sets up per-reader C structures. */
    private native void initJPEGImageReader();

    /**
     * Called by the native code, or other classes to signal a warning.  
     * The argument must be a valid index into the warning strings array
     * above, or a fatal error is thrown.  The code is used to lookup the
     * message to be used when sending warnings to listeners.  All
     * localization must be done here in Java rather than in the native code.
     * The current implementation is not localized at all.
     */
    protected void warningOccurred(int code) {
        if ((code < 0) || (code >= warnings.length)){
            throw new InternalError("Invalid warning index");
        }
        processWarningOccurred(warnings[code]);
    }

    /**
     * The library has it's own error facility that emits warning messages.
     * This routine is called by the native code when it has already 
     * formatted a string for output.  
     * XXX  For proper localization of all warning messages,
     * the sun_jpeg_output_message routine in the native code should
     * send only the codes and parameters to warningOccurred, above,
     * which will then format and send the warnings, using localized
     * strings.  For now, this prevents library warnings from being
     * printed to stderr.
     */
    protected void warningWithMessage(String msg) {
        processWarningOccurred(msg);
    }

    public void setInput(Object input,
                         boolean seekForwardOnly,
                         boolean ignoreMetadata) {
        super.setInput(input, seekForwardOnly, ignoreMetadata);
        this.ignoreMetadata = ignoreMetadata;
        iis = (ImageInputStream) input; // Always works
        numImages = 0;
        imagePositions = new ArrayList();
        // Synchronize to protect the native linked list
        synchronized (JPEGImageReader.class) {
            setSource(iis);
        }
        tablesOnlyChecked = false;
        haveSeeked = false;
    }

    private native void setSource(ImageInputStream source);

    private void checkTablesOnly() throws IOException {
        if (debug) {
            System.out.println("Checking for tables-only image");
        }
        long savePos = iis.getStreamPosition();
        if (debug) {
            System.out.println("saved pos is " + savePos);
        }
        // Read the first header
        boolean tablesOnly = readNativeHeader(true);
        if (tablesOnly) {
            if (debug) {
                System.out.println("tables-only image found");
                long pos = iis.getStreamPosition();
                System.out.println("pos after return from native is " + pos);
            }
            // This reads the tables-only image twice, once from C
            // and once from Java, but only if ignoreMetadata is false
            if (ignoreMetadata == false) {
                iis.seek(savePos);
                haveSeeked = true;
                streamMetadata = new JPEGMetadata(true, false,
                                                  iis, this);
                long pos = iis.getStreamPosition();
                if (debug) {
                    System.out.println
                        ("pos after constructing stream metadata is " + pos);
                }
            }
            // Now we are at the first image if there are any, so add it 
            // to the list
            if (hasNextImage()) {
                imagePositions.add(new Long(iis.getStreamPosition()));
            }
        } else { // Not tables only, so add original pos to the list
            imagePositions.add(new Long(savePos));
            // And set current image since we've read it now
            currentImage = 0;
        }
        if (seekForwardOnly) {
            Long pos = (Long) imagePositions.get(imagePositions.size()-1);
            iis.flushBefore(pos.longValue());
        }
        tablesOnlyChecked = true;
    }

    public int getNumImages(boolean allowSearch) throws IOException {
        if (numImages != 0) {
            return numImages;
        }
        if (iis == null) {
            throw new IllegalStateException("Input not set");
        }
        if (allowSearch == true) {
            if (seekForwardOnly) {
                throw new IllegalStateException(
                    "seekForwardOnly and allowSearch can't both be true!");
            }
            // Otherwise we have to read the entire stream

            if (!tablesOnlyChecked) {
                checkTablesOnly();
            }
            
            iis.mark();

            gotoImage(0);
            
            // Scan for SOI markers, incrementing count
            boolean foundFF = false;
            for (int byteval = iis.read(); 
                 byteval != -1;
                 byteval = iis.read()) {
                
                if (foundFF == true) {
                    switch (byteval) {
                    case JPEG.SOI:
                        numImages++;
                        break;
                        // This first set doesn't have a length
                    case 0: // not a marker, just a data 0xff
                    case JPEG.EOI:
                    case JPEG.RST0:
                    case JPEG.RST1:
                    case JPEG.RST2:
                    case JPEG.RST3:
                    case JPEG.RST4:
                    case JPEG.RST5:
                    case JPEG.RST6:
                    case JPEG.RST7:
                        break;
                        // All the others have a length
                    default:
                        int length = (iis.read() << 8) | (iis.read());
                        length -= 2; // length includes itself
                        iis.skipBytes(length);
                    }
                }
                
                foundFF = (byteval == 0xff) ? true : false;
                    
            }
            
            iis.reset();

            return numImages;
        }

        return -1;  // Search is necessary for JPEG
    }

    /**
     * Sets the input stream to the start of the requested image.
     * <pre>
     * @exception IllegalStateException if the input source has not been
     * set.
     * @exception IndexOutOfBoundsException if the supplied index is
     * out of bounds.
     * </pre>
     */
    private void gotoImage(int imageIndex) throws IOException { 
        if (iis == null) {
            throw new IllegalStateException("Input not set");
        }
        if (imageIndex < minIndex) {
            throw new IndexOutOfBoundsException();
        }
        if (!tablesOnlyChecked) {
            checkTablesOnly();
        }
        if (imageIndex < imagePositions.size()) {
            iis.seek(((Long)(imagePositions.get(imageIndex))).longValue());
        } else {
            // read to start of image, saving positions
            // First seek to the last position we already have, and skip the
            // entire image
            Long pos = (Long) imagePositions.get(imagePositions.size()-1);
            iis.seek(pos.longValue());
            skipImage();
            // Now add all intervening positions, skipping images
            for (int index = imagePositions.size(); 
                 index <= imageIndex;
                 index++) {
                // Is there an image?
                if (!hasNextImage()) {
                    throw new IndexOutOfBoundsException();
                }
                pos = new Long(iis.getStreamPosition());
                imagePositions.add(pos);
                if (seekForwardOnly) {
                    iis.flushBefore(pos.longValue());
                }
                if (index < imageIndex) { 
                    skipImage();
                }  // Otherwise we are where we want to be
            }
        }

        if (seekForwardOnly) {
            minIndex = imageIndex;
        }

        haveSeeked = true;  // No way is native buffer still valid
    }

    /**
     * Skip over a complete image in the stream, leaving the stream
     * positioned such that the next byte to be read is the first 
     * byte of the next image.  For JPEG, this means that we read
     * until we encounter an EOI marker or until the end of the stream.
     * If the stream ends before an EOI marker is encountered, an
     * IndexOutOfBoundsException is thrown.
     */
    private void skipImage() throws IOException {
        if (debug) {
            System.out.println("skipImage called");
        }
        boolean foundFF = false;
        for (int byteval = iis.read(); 
             byteval != -1;
             byteval = iis.read()) {

            if (foundFF == true) {
                if (byteval == JPEG.EOI) {
                    return;
                }
            }
            foundFF = (byteval == 0xff) ? true : false;
        }
        throw new IndexOutOfBoundsException();
    }

    /**
     * Returns <code>true</code> if there is an image beyond
     * the current stream position.  Does not disturb the 
     * stream position.
     */
    private boolean hasNextImage() throws IOException {
        if (debug) {
            System.out.print("hasNextImage called; returning ");
        }
        iis.mark();
        boolean foundFF = false;
        for (int byteval = iis.read(); 
             byteval != -1;
             byteval = iis.read()) {

            if (foundFF == true) {
                if (byteval == JPEG.SOI) {
                    iis.reset();
                    if (debug) {
                        System.out.println("true");
                    }
                    return true;
                }
            }
            foundFF = (byteval == 0xff) ? true : false;
        }
        // We hit the end of the stream before we hit an SOI, so no image
        iis.reset();
        if (debug) {
            System.out.println("false");
        }
        return false;
    }

    /**
     * Push back the given number of bytes to the input stream.
     * Called by the native code at the end of each image so
     * that the next one can be identified from Java.
     */
    private void pushBack(int num) throws IOException {
        if (debug) {
            System.out.println("pushing back " + num + " bytes");
        }
        iis.seek(iis.getStreamPosition()-num);
        // The buffer is clear after this, so no need to set haveSeeked.
    }

    /**
     * Reads header information for the given image, if possible.
     */
    private void readHeader(int imageIndex, boolean reset) 
        throws IOException { 
        gotoImage(imageIndex);
        readNativeHeader(reset); // Ignore return
        currentImage = imageIndex;
    }

    private boolean readNativeHeader(boolean reset) throws IOException {
        boolean retval = false;
        // Synchronize to protect the native linked list
        synchronized (JPEGImageReader.class) {
            retval = readImageHeader(haveSeeked, reset);
        }
        haveSeeked = false;
        return retval;
    }

    /**
     * Read in the header information starting from the current
     * stream position, returning <code>true</code> if the
     * header was a tables-only image.  After this call, the
     * native IJG decompression struct will contain the image
     * information required by most query calls below
     * (e.g. getWidth, getHeight, etc.), if the header was not
     * a tables-only image.
     * If reset is <code>true</code>, the state of the IJG
     * object is reset so that it can read a header again.
     * This happens automatically if the header was a tables-only
     * image.
     */
    private native boolean readImageHeader(boolean clearBuffer,
                                           boolean reset)
        throws IOException;        

    /*
     * Called by the native code whenever an image header has been 
     * read.  Whether we read metadata or not, we always need this
     * information, so it is passed back independently of
     * metadata, which may never be read.
     */
    private void setImageData(int width, 
                              int height, 
                              int colorSpaceCode,
                              int outColorSpaceCode,
                              int numComponents) {
        this.width = width;
        this.height = height;
        this.colorSpaceCode = colorSpaceCode;
        this.outColorSpaceCode = outColorSpaceCode;
        this.numComponents = numComponents;
    }

    public int getWidth(int imageIndex) throws IOException {
        if (currentImage != imageIndex) {
            readHeader(imageIndex, true);
        }
        return width;
    }

    public int getHeight(int imageIndex) throws IOException {
        if (currentImage != imageIndex) {
            readHeader(imageIndex, true);
        }
        return height;
    }

    /////////// Color Conversion and Image Types

    /** 
     * Return an ImageTypeSpecifier corresponding to the given
     * color space code, or null if the color space is unsupported.
     */
    private ImageTypeSpecifier getImageType(int code) {
        ImageTypeSpecifier ret = null;

        if ((code > 0) && (code < JPEG.NUM_JCS_CODES)) {
            ret = defaultTypes[code];
        }
        return ret;
    }

    public ImageTypeSpecifier getRawImageType(int imageIndex)
        throws IOException {
        if (currentImage != imageIndex) {
            readHeader(imageIndex, true);
        }
        // Returns null if it can't be represented
        return getImageType(colorSpaceCode);
    }

    public Iterator getImageTypes(int imageIndex)
        throws IOException {
        if (currentImage != imageIndex) {
            readHeader(imageIndex, true);
        }

        // We return an iterator containing the default, any
        // conversions that the library provides, and
        // all the other default types with the same number
        // of components, as we can do these as a post-process.
        // As we convert Rasters rather than images, images
        // with alpha cannot be converted in a post-process.

        // If this image can't be interpreted, this method
        // returns an empty Iterator.

        // Get the raw ITS, if there is one.  Note that this
        // won't always be the same as the default.
        ImageTypeSpecifier raw = getImageType(colorSpaceCode);
        
        // Given the encoded colorspace, build a list of ITS's 
        // representing outputs you could handle starting
        // with the default.
        
        ArrayList list = new ArrayList(1);

        switch (colorSpaceCode) {
        case JPEG.JCS_GRAYSCALE:
            list.add(raw);
            list.add(getImageType(JPEG.JCS_RGB));
            break;
        case JPEG.JCS_RGB:
            list.add(raw);
            list.add(getImageType(JPEG.JCS_GRAYSCALE));
            if (JPEG.YCC != null) {
                list.add(getImageType(JPEG.JCS_YCC));
            }
            break;
        case JPEG.JCS_RGBA:
            list.add(raw);
            break;
        case JPEG.JCS_YCC:
            if (raw != null) {  // Might be null if PYCC.pf not installed
                list.add(raw);
                list.add(getImageType(JPEG.JCS_RGB));
            }
            break;
        case JPEG.JCS_YCCA:
            if (raw != null) {  // Might be null if PYCC.pf not installed
                list.add(raw);
            }
            break;
        case JPEG.JCS_YCbCr:
            // As there is no YCbCr ColorSpace, we can't support
            // the raw type.
            // FIXME - If there is an ICC Profile, use that as the default
            list.add(getImageType(JPEG.JCS_RGB));
            list.add(getImageType(JPEG.JCS_GRAYSCALE));
            if (JPEG.YCC != null) { // Might be null if PYCC.pf not installed
                list.add(getImageType(JPEG.JCS_YCC));
            }
            break;
        case JPEG.JCS_YCbCrA:  // Default is to convert to RGBA
            // As there is no YCbCr ColorSpace, we can't support
            // the raw type.
            list.add(getImageType(JPEG.JCS_RGBA));
            break;
        }
        
        return list.iterator();
    }

    /**
     * Checks the implied color conversion between the stream and
     * the target image, altering the IJG output color space if necessary. 
     * If a java color conversion is required, then this sets up 
     * <code>convert</code>.
     * If bands are being rearranged at all (either source or destination
     * bands are specified in the param), then the default color 
     * conversions are assumed to be correct.
     * Throws an IIOException if there is no conversion available.
     */ 
    private void checkColorConversion(BufferedImage image, 
                                      ImageReadParam param) 
        throws IIOException {

        // If we are rearranging channels at all, the default
        // conversions remain in place.  If the user wants 
        // raw channels then he should do this while reading
        // a Raster.
        if (param != null) {
            if ((param.getSourceBands() != null) || 
                (param.getDestinationBands() != null)) {
                // Accept default conversions out of decoder, silently
                return;
            }
        }

        // XXX - We do not currently support any indexed color models,
        // though we could, as IJG will quantize for us.
        // This is a performance and memory-use issue, as
        // users can read RGB and then convert to indexed in Java.

        ColorModel cm = image.getColorModel();

        if (cm instanceof IndexColorModel) {
            throw new IIOException("IndexColorModel not supported");
        }
        
        // Now check the ColorSpace type against outColorSpaceCode
        // We may want to tweak the default
        ColorSpace cs = cm.getColorSpace();
        int csType = cs.getType();
        convert = null;
        switch (outColorSpaceCode) {
        case JPEG.JCS_GRAYSCALE:  // Its gray in the file
            if  (csType == ColorSpace.TYPE_RGB) { // We want RGB
                // IJG can do this for us more efficiently
                // Synchronize to protect the native linked list
                synchronized (JPEGImageReader.class) {
                    setOutColorSpace(JPEG.JCS_RGB);
                }
            } else if (csType != ColorSpace.TYPE_GRAY) {
                throw new IIOException("Incompatible color conversion");
            }
            break;
        case JPEG.JCS_RGB:  // IJG wants to go to RGB
            if (csType ==  ColorSpace.TYPE_GRAY) {  // We want gray
                if (colorSpaceCode == JPEG.JCS_YCbCr) {
                    // If the jpeg space is YCbCr, IJG can do it
                    // Synchronize to protect the native linked list
                    synchronized (JPEGImageReader.class) {
                        setOutColorSpace(JPEG.JCS_GRAYSCALE);
                    }
                }
            } else if ((csType != ColorSpace.TYPE_RGB) &&
                       (cm.getNumComponents() == numComponents)) {
                convert = new ColorConvertOp(JPEG.sRGB, cs, null);
            } else if (csType != ColorSpace.TYPE_RGB) {
                throw new IIOException("Incompatible color conversion");
            }
            break;
        case JPEG.JCS_RGBA:
            // No conversions available; image must be RGBA
            if ((csType != ColorSpace.TYPE_RGB) ||
                (cm.getNumComponents() != numComponents)) {
                throw new IIOException("Incompatible color conversion");
            }
            break;
        case JPEG.JCS_YCC:
            if (JPEG.YCC == null) { // We can't do YCC at all
                throw new IIOException("Incompatible color conversion");
            }
            if ((cs != JPEG.YCC) &&
                (cm.getNumComponents() == numComponents)) {
                convert = new ColorConvertOp(JPEG.YCC, cs, null);
            }
            break;
        case JPEG.JCS_YCCA:
            // No conversions available; image must be YCCA
            if ((JPEG.YCC == null) || // We can't do YCC at all
                (cs != JPEG.YCC) ||
                (cm.getNumComponents() != numComponents)) {
                throw new IIOException("Incompatible color conversion");
            }
            break;
        default:
            // Anything else we can't handle at all
            // FIXME - unless we have an ICC Profile?
            throw new IIOException("Incompatible color conversion");
        }
    }

    /** 
     * Set the IJG output space to the given value.  The library will
     * perform the appropriate colorspace conversions.
     */
    private native void setOutColorSpace(int id);

    /////// End of Color Conversion & Image Types
    
    public ImageReadParam getDefaultReadParam() {
        return new JPEGImageReadParam();
    }

    public IIOMetadata getStreamMetadata() throws IOException {
        if (!tablesOnlyChecked) {
            checkTablesOnly();
        }
        return streamMetadata;
    }

    public IIOMetadata getImageMetadata(int imageIndex)
        throws IOException {

        gotoImage(imageIndex);

        return (IIOMetadata) new JPEGMetadata(false, false, iis, this);
        
    }

    public BufferedImage read(int imageIndex, ImageReadParam param)
        throws IOException {
        try {
            readInternal(imageIndex, param, false);
        } catch (RuntimeException e) {
            resetLibraryState();
            throw e;
        } catch (IOException e) {
            resetLibraryState();
            throw e;
        }
        BufferedImage ret = image;
        image = null;  // don't keep a reference here
        return ret;
    }

    private Raster readInternal(int imageIndex, 
                                ImageReadParam param,
                                boolean wantRaster) throws IOException {
        readHeader(imageIndex, false);
        
        WritableRaster imRas = null;
        int numImageBands = 0;

        if (!wantRaster){
            // Can we read this image?
            Iterator imageTypes = getImageTypes(imageIndex);
            if (imageTypes.hasNext() == false) {
                throw new IIOException("Unsupported Image Type");
            }

            image = getDestination(param, imageTypes, width, height);
            imRas = image.getRaster();

            // The destination may still be incompatible.

            numImageBands = image.getSampleModel().getNumBands();

            // Check whether we can handle any implied color conversion

            // Throws IIOException if the stream and the image are
            // incompatible, and sets convert if a java conversion
            // is necessary
            checkColorConversion(image, param);

            // Check the source and destination bands in the param
            checkReadParamBandSettings(param, numComponents, numImageBands);
        } else {
            // Synchronize to protect the native linked list
            synchronized (JPEGImageReader.class) {
                // Set the output color space equal to the input colorspace
                // This disables all conversions
                setOutColorSpace(colorSpaceCode);
            }
            image = null;
        }

        // Create an intermediate 1-line Raster that will hold the decoded,
        // subsampled, clipped, band-selected image data in a single
        // byte-interleaved buffer.  The above transformations
        // will occur in C for performance.  Every time this Raster
        // is filled we will call back to acceptPixels below to copy
        // this to whatever kind of buffer our image has.

        int [] srcBands = JPEG.bandOffsets[numComponents-1];
        int numRasterBands = (wantRaster ? numComponents : numImageBands);
        destinationBands = null;

        Rectangle srcROI = new Rectangle(0, 0, 0, 0);
        destROI = new Rectangle(0, 0, 0, 0);
        // For Rasters, destination offset is logical, not physical, so
        // set it to 0 before calling computeRegions, so that the destination
        // region is not clipped.
        Point saveDestOffset = null;
        if (wantRaster) {
            if (param != null) {
                saveDestOffset = param.getDestinationOffset();
                param.setDestinationOffset(new Point(0, 0));
            }
        }
        computeRegions(param, width, height, image, srcROI, destROI);

        int periodX = 1;
        int periodY = 1;

        minProgressivePass = 0;
        maxProgressivePass = Integer.MAX_VALUE;

        if (param != null) {
            periodX = param.getSourceXSubsampling();
            periodY = param.getSourceYSubsampling();

            int[] sBands = param.getSourceBands();
            if (sBands != null) {
                srcBands = sBands;
                numRasterBands = srcBands.length;
            }
            if (!wantRaster) {  // ignore dest bands for Raster
                destinationBands = param.getDestinationBands();
            }

            minProgressivePass = param.getSourceMinProgressivePass();
            maxProgressivePass = param.getSourceMaxProgressivePass();

            if (param instanceof JPEGImageReadParam) {
                JPEGImageReadParam jparam = (JPEGImageReadParam) param;
                if (jparam.areTablesSet()) {
                    abbrevQTables = jparam.getQTables();
                    abbrevDCHuffmanTables = jparam.getDCHuffmanTables();
                    abbrevACHuffmanTables = jparam.getACHuffmanTables();
                }
            }
        }

        int lineSize = destROI.width*numRasterBands;
        
        buffer = new DataBufferByte(lineSize);

        int [] bandOffs = JPEG.bandOffsets[numRasterBands-1];
        
        raster = Raster.createInterleavedRaster(buffer, 
                                                destROI.width, 1, 
                                                lineSize,
                                                numRasterBands,
                                                bandOffs,
                                                null);
        
        // Now that we have the Raster we'll decode to, get a view of the
        // target Raster that will permit a simple setRect for each scanline
        if (wantRaster) {
            target =  Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, 
                                                     destROI.width,
                                                     destROI.height,
                                                     lineSize,
                                                     numRasterBands,
                                                     bandOffs,
                                                     null);
        } else {
            target = imRas.createWritableChild(destROI.x, 
                                               destROI.y,
                                               destROI.width,
                                               destROI.height,
                                               0, 0,
                                               destinationBands);
        }
        int [] bandSizes = target.getSampleModel().getSampleSize();

        /*
         * If the process is sequential, and we have restart markers, 
         * we could skip to the correct restart marker, if the library
         * lets us.  That's an optimization to investigate later.
         */

        // Check for progess, update listeners (don't call back if none)
        boolean callbackProgress = progressListeners != null;
        boolean callbackUpdates = updateListeners != null;

        if (debug) {
            System.out.println("**** Read Data *****");
            System.out.println("numRasterBands is " + numRasterBands);
            System.out.print("srcBands:");
            for (int i = 0; i<srcBands.length;i++)
                System.out.print(" " + srcBands[i]);
            System.out.println();
            System.out.println("destination bands is " + destinationBands);
            if (destinationBands != null) {
                for (int i = 0; i < destinationBands.length; i++) {
                    System.out.print(" " + destinationBands[i]);
                }
                System.out.println();
            }
            System.out.println("sourceROI is " + srcROI);
            System.out.println("destROI is " + destROI);
            System.out.println("periodX is " + periodX);
            System.out.println("periodY is " + periodY);
            System.out.println("minProgressivePass is " + minProgressivePass);
            System.out.println("maxProgressivePass is " + maxProgressivePass);
            System.out.println("callbackProgress is " + callbackProgress);
            System.out.println("callbackUpdates is " + callbackUpdates);
        }

        // Finally, we are ready to read

        processImageStarted(currentImage);

        boolean aborted = false;

        // Synchronize to protect the native linked list
        synchronized (JPEGImageReader.class) {
            aborted = readImage(buffer.getData(),
                                numRasterBands,
                                srcBands,
                                bandSizes,
                                srcROI.x, srcROI.y,
                                srcROI.width, srcROI.height,
                                periodX, periodY,
                                abbrevQTables,
                                abbrevDCHuffmanTables,
                                abbrevACHuffmanTables,
                                minProgressivePass, maxProgressivePass,
                                callbackProgress, callbackUpdates);
        }

        if (aborted) {
            processReadAborted();
        } else {
            processImageComplete();
        }

        // Apply the destination offset, if any, as a logical offset
        // This applies only to Rasters
        if (saveDestOffset != null) {
            target = target.createWritableTranslatedChild(saveDestOffset.x,
                                                          saveDestOffset.y);
        }

        return target;

    }

    /** 
     * This method is called back from C when the intermediate Raster
     * is full.  The parameter indicates the scanline in the target
     * Raster to which the intermediate Raster should be copied.
     * After the copy, we notify update listeners.
     */
    private void acceptPixels(int y) {
        if (convert != null) {
            convert.filter(raster, raster);
        }
        target.setRect(0, y, raster);
        
        processImageUpdate(image,
                           destROI.x, destROI.y+y,
                           raster.getWidth(), 1,
                           1, 1,
                           destinationBands);
        if ((y > 0) && (y%8 == 0)) {
            processImageProgress((float)y/(float)target.getHeight()*100.0F);
        }
    }

    private void passStarted (int pass) {
        processPassStarted(image, 
                           pass,
                           minProgressivePass,
                           maxProgressivePass,
                           0, 0,
                           1,1,
                           // XXX? Color conversion makes bands weird,
                           // so just use the destination bands
                           destinationBands);
    }

    private void passComplete () {
        processPassComplete(image);
    }

    /**
     * Returns <code>true</code> if the read was aborted.
     */
    private native boolean readImage(byte [] buffer,
                                     int numRasterBands,
                                     int [] srcBands,
                                     int [] bandSizes,
                                     int sourceXOffset, int sourceYOffset,
                                     int sourceWidth, int sourceHeight,
                                     int periodX, int periodY,
                                     JPEGQTable [] abbrevQTables,
                                     JPEGHuffmanTable [] abbrevDCHuffmanTables,
                                     JPEGHuffmanTable [] abbrevACHuffmanTables,
                                     int minProgressivePass,
                                     int maxProgressivePass,
                                     boolean wantProgress,
                                     boolean wantUpdates);
    
    public void abort() {
        super.abort();
        // Synchronize to protect the native linked list
        synchronized (JPEGImageReader.class) {
            abortRead();
        }
    }

    /** Set the C level abort flag. Keep it atomic for thread safety. */
    private native void abortRead();

    /** Resets library state when an exception occurred during a read. */
    private native void resetLibraryState();

    public boolean canReadRaster() {
        return true;
    }

    public Raster readRaster(int imageIndex, ImageReadParam param)
        throws IOException {
        Raster retval = null;
        try {
            retval = readInternal(imageIndex, param, true);
        } catch (RuntimeException e) {            
            resetLibraryState();
            throw e;
        } catch (IOException e) {
            resetLibraryState();
            throw e;
        }
        return retval;
    }

    /*
    public boolean readerSupportsThumbnails() {
        return false;  // Until implemented
    }

    public int getNumThumbnails(int imageIndex) throws IOException {
        // FIXME implement
        return 0;
    }

    public int getThumbnailWidth(int imageIndex, int thumbnailIndex)
        throws IOException {
        // FIXME implement
        return 0;
    }

    public int getThumbnailHeight(int imageIndex, int thumbnailIndex)
        throws IOException {
        // FIXME implement
        return 0;
    }

    public BufferedImage readThumbnail(int imageIndex,
                                       int thumbnailIndex)
        throws IOException {
        // FIXME implement
        return null;
    }
    */

    public void reset() {
        // reset C structures
        // Synchronize to protect the native linked list
        synchronized (JPEGImageReader.class) {
            resetReader();
        }
        // reset local Java structures
        super.reset();
        iis = null;
        numImages = 0;
        imagePositions = new ArrayList();
        currentImage = -1;
        image = null;
        raster = null;
        target = null;
        buffer = null;
        destROI = null;
        destinationBands = null;
        streamMetadata = null;
        haveSeeked = false;
        tablesOnlyChecked = false;
        System.gc();
    }

    private native void resetReader();

    public void dispose() {
        // Synchronize to protect the native linked list
        synchronized (JPEGImageReader.class) {
            disposeReader();
        }
    }

    private native void disposeReader();

    public void finalize() {
        dispose();
    }

}
