/*
 * Copyright (c) 2000, 2007, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

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.color.ICC_Profile;
import java.awt.color.ICC_ColorSpace;
import java.awt.color.CMMException;
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;
import java.util.NoSuchElementException;

import sun.java2d.Disposer;
import sun.java2d.DisposerRecord;

public class JPEGImageReader extends ImageReader {

    private boolean debug = false;

    /**
     * The following variable contains a pointer to the IJG library
     * structure for this reader.  It is assigned in the constructor
     * and then is passed in to every native call.  It is set to 0
     * by dispose to avoid disposing twice.
     */
    private long structPointer = 0;

    /** The input stream we read from */
    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,
                      JPEGQTable.class,
                      JPEGHuffmanTable.class);
    }

    // The following warnings are converted to strings when used
    // as keys to get localized resources from JPEGImageReaderResources
    // and its children.

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

    /**
     * Warning code to be passed to warningOccurred to indicate
     * that embedded ICC profile is invalid and will be ignored.
     */
    protected static final int WARNING_IGNORE_INVALID_ICC = 2;

    private static final int MAX_WARNING = WARNING_IGNORE_INVALID_ICC;

    /**
     * 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 setImageData native code callback */
    private int width;
    /** Set by setImageData native code callback */
    private int height;
    /**
     * Set by setImageData native code callback.  A modified
     * IJG+NIFTY colorspace code.
     */
    private int colorSpaceCode;
    /**
     * Set by setImageData native code callback.  A modified
     * IJG+NIFTY colorspace code.
     */
    private int outColorSpaceCode;
    /** Set by setImageData native code callback */
    private int numComponents;
    /** Set by setImageData native code callback */
    private ColorSpace iccCS = null;


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

    /** Image metadata, valid for the imageMetadataIndex only. */
    private JPEGMetadata imageMetadata = null;
    private int imageMetadataIndex = -1;

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

    /**
     * Tables that have been read from a tables-only image at the
     * beginning of a stream.
     */
    private JPEGQTable [] abbrevQTables = null;
    private JPEGHuffmanTable[] abbrevDCHuffmanTables = null;
    private JPEGHuffmanTable[] abbrevACHuffmanTables = null;

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

    /**
     * Variables used by progress monitoring.
     */
    private static final int UNKNOWN = -1;  // Number of passes
    private static final int MIN_ESTIMATED_PASSES = 10; // IJG default
    private int knownPassCount = UNKNOWN;
    private int pass = 0;
    private float percentToDate = 0.0F;
    private float previousPassPercentage = 0.0F;
    private int progInterval = 0;

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

    /** The referent to be registered with the Disposer. */
    private Object disposerReferent = new Object();

    /** The DisposerRecord that handles the actual disposal of this reader. */
    private DisposerRecord disposerRecord;

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

    public JPEGImageReader(ImageReaderSpi originator) {
        super(originator);
        structPointer = initJPEGImageReader();
        disposerRecord = new JPEGReaderDisposerRecord(structPointer);
        Disposer.addRecord(disposerReferent, disposerRecord);
    }

    /** Sets up per-reader C structure and returns a pointer to it. */
    private native long initJPEGImageReader();

    /**
     * Called by the native code or other classes to signal a warning.
     * The code is used to lookup a localized message to be used when
     * sending warnings to listeners.
     */
    protected void warningOccurred(int code) {
        if ((code < 0) || (code > MAX_WARNING)){
            throw new InternalError("Invalid warning index");
        }
        processWarningOccurred
            ("com.sun.imageio.plugins.jpeg.JPEGImageReaderResources",
             Integer.toString(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 truly complete localization of all warning messages,
     * the sun_jpeg_output_message routine in the native code should
     * send only the codes and parameters to a method here in Java,
     * which will then format and send the warnings, using localized
     * strings.  This method will have to deal with all the parameters
     * and formats (%u with possibly large numbers, %02d, %02x, etc.)
     * that actually occur in the JPEG library.  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)
    {
        setThreadLock();
        try {
            super.setInput(input, seekForwardOnly, ignoreMetadata);
            this.ignoreMetadata = ignoreMetadata;
            resetInternalState();
            iis = (ImageInputStream) input; // Always works
            setSource(structPointer, iis);
        } finally {
            clearThreadLock();
        }
    }

    private native void setSource(long structPointer,
                                  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);
            System.out.println("length is " + iis.length());
        }
        // 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 {
        setThreadLock();
        try { // locked thread
            return getNumImagesOnThread(allowSearch);
        } finally {
            clearThreadLock();
        }
    }

    private int getNumImagesOnThread(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);

            JPEGBuffer buffer = new JPEGBuffer(iis);
            buffer.loadBuf(0);

            boolean done = false;
            while (!done) {
                done = buffer.scanForFF(this);
                switch (buffer.buf[buffer.bufPtr] & 0xff) {
                case JPEG.SOI:
                    numImages++;
                    // FALL THROUGH to decrement buffer vars
                    // This first set doesn't have a length
                case 0: // not a marker, just a data 0xff
                case JPEG.RST0:
                case JPEG.RST1:
                case JPEG.RST2:
                case JPEG.RST3:
                case JPEG.RST4:
                case JPEG.RST5:
                case JPEG.RST6:
                case JPEG.RST7:
                case JPEG.EOI:
                    buffer.bufAvail--;
                    buffer.bufPtr++;
                    break;
                    // All the others have a length
                default:
                    buffer.bufAvail--;
                    buffer.bufPtr++;
                    buffer.loadBuf(2);
                    int length = ((buffer.buf[buffer.bufPtr++] & 0xff) << 8) |
                        (buffer.buf[buffer.bufPtr++] & 0xff);
                    buffer.bufAvail -= 2;
                    length -= 2; // length includes itself
                    buffer.skipData(length);
                }
            }


            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;
        retval = readImageHeader(structPointer, 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(long structPointer,
                                           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,
                              byte [] iccData) {
        this.width = width;
        this.height = height;
        this.colorSpaceCode = colorSpaceCode;
        this.outColorSpaceCode = outColorSpaceCode;
        this.numComponents = numComponents;

        if (iccData == null) {
            iccCS = null;
            return;
        }

        ICC_Profile newProfile = null;
        try {
            newProfile = ICC_Profile.getInstance(iccData);
        } catch (IllegalArgumentException e) {
            /*
             * Color profile data seems to be invalid.
             * Ignore this profile.
             */
            iccCS = null;
            warningOccurred(WARNING_IGNORE_INVALID_ICC);

            return;
        }
        byte[] newData = newProfile.getData();

        ICC_Profile oldProfile = null;
        if (iccCS instanceof ICC_ColorSpace) {
            oldProfile = ((ICC_ColorSpace)iccCS).getProfile();
        }
        byte[] oldData = null;
        if (oldProfile != null) {
            oldData = oldProfile.getData();
        }

        /*
         * At the moment we can't rely on the ColorSpace.equals()
         * and ICC_Profile.equals() because they do not detect
         * the case when two profiles are created from same data.
         *
         * So, we have to do data comparison in order to avoid
         * creation of different ColorSpace instances for the same
         * embedded data.
         */
        if (oldData == null ||
            !java.util.Arrays.equals(oldData, newData))
        {
            iccCS = new ICC_ColorSpace(newProfile);
            // verify new color space
            try {
                float[] colors = iccCS.fromRGB(new float[] {1f, 0f, 0f});
            } catch (CMMException e) {
                /*
                 * Embedded profile seems to be corrupted.
                 * Ignore this profile.
                 */
                iccCS = null;
                warningOccurred(WARNING_IGNORE_INVALID_ICC);
            }
        }
    }

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

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

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

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

        if ((code > 0) && (code < JPEG.NUM_JCS_CODES)) {
            ret = ImageTypeProducer.getTypeProducer(code);
        }
        return ret;
    }

    public ImageTypeSpecifier getRawImageType(int imageIndex)
        throws IOException {
        setThreadLock();
        try {
            if (currentImage != imageIndex) {
                readHeader(imageIndex, true);
            }

            // Returns null if it can't be represented
            return getImageType(colorSpaceCode).getType();
        } finally {
            clearThreadLock();
        }
    }

    public Iterator getImageTypes(int imageIndex)
        throws IOException {
        setThreadLock();
        try {
            return getImageTypesOnThread(imageIndex);
        } finally {
            clearThreadLock();
        }
    }

    private Iterator getImageTypesOnThread(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.
        ImageTypeProducer raw = getImageType(colorSpaceCode);

        // Given the encoded colorspace, build a list of ITS's
        // representing outputs you could handle starting
        // with the default.

        ArrayList<ImageTypeProducer> list = new ArrayList<ImageTypeProducer>(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));
            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.

            // due to 4705399, use RGB as default in order to avoid
            // slowing down of drawing operations with result image.
            list.add(getImageType(JPEG.JCS_RGB));

            if (iccCS != null) {
                list.add(new ImageTypeProducer() {
                    protected ImageTypeSpecifier produce() {
                        return ImageTypeSpecifier.createInterleaved
                         (iccCS,
                          JPEG.bOffsRGB,  // Assume it's for RGB
                          DataBuffer.TYPE_BYTE,
                          false,
                          false);
                    }
                });

            }

            list.add(getImageType(JPEG.JCS_GRAYSCALE));
            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 new ImageTypeIterator(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
                setOutColorSpace(structPointer, JPEG.JCS_RGB);
                // Update java state according to changes
                // in the native part of decoder.
                outColorSpaceCode = JPEG.JCS_RGB;
                numComponents = 3;
            } 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
                    setOutColorSpace(structPointer, JPEG.JCS_GRAYSCALE);
                    // Update java state according to changes
                    // in the native part of decoder.
                    outColorSpaceCode = JPEG.JCS_GRAYSCALE;
                    numComponents = 1;
                }
            } else if ((iccCS != null) &&
                       (cm.getNumComponents() == numComponents) &&
                       (cs != iccCS)) {
                // We have an ICC profile but it isn't used in the dest
                // image.  So convert from the profile cs to the target cs
                convert = new ColorConvertOp(iccCS, cs, null);
                // Leave IJG conversion in place; we still need it
            } else if ((iccCS == null) &&
                       (!cs.isCS_sRGB()) &&
                       (cm.getNumComponents() == numComponents)) {
                // Target isn't sRGB, so convert from sRGB to the target
                convert = new ColorConvertOp(JPEG.JCS.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:
            {
                ColorSpace YCC = JPEG.JCS.getYCC();
                if (YCC == null) { // We can't do YCC at all
                    throw new IIOException("Incompatible color conversion");
                }
                if ((cs != YCC) &&
                    (cm.getNumComponents() == numComponents)) {
                    convert = new ColorConvertOp(YCC, cs, null);
                }
            }
            break;
        case JPEG.JCS_YCCA:
            {
                ColorSpace YCC = JPEG.JCS.getYCC();
                // No conversions available; image must be YCCA
                if ((YCC == null) || // We can't do YCC at all
                    (cs != YCC) ||
                    (cm.getNumComponents() != numComponents)) {
                    throw new IIOException("Incompatible color conversion");
                }
            }
            break;
        default:
            // Anything else we can't handle at all
            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(long structPointer, int id);

    /////// End of Color Conversion & Image Types

    public ImageReadParam getDefaultReadParam() {
        return new JPEGImageReadParam();
    }

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

    public IIOMetadata getImageMetadata(int imageIndex)
        throws IOException {
        setThreadLock();
        try {
            // imageMetadataIndex will always be either a valid index or
            // -1, in which case imageMetadata will not be null.
            // So we can leave checking imageIndex for gotoImage.
            if ((imageMetadataIndex == imageIndex)
                && (imageMetadata != null)) {
                return imageMetadata;
            }

            gotoImage(imageIndex);

            imageMetadata = new JPEGMetadata(false, false, iis, this);

            imageMetadataIndex = imageIndex;

            return imageMetadata;
        } finally {
            clearThreadLock();
        }
    }

    public BufferedImage read(int imageIndex, ImageReadParam param)
        throws IOException {
        setThreadLock();
        try {
            try {
                readInternal(imageIndex, param, false);
            } catch (RuntimeException e) {
                resetLibraryState(structPointer);
                throw e;
            } catch (IOException e) {
                resetLibraryState(structPointer);
                throw e;
            }

            BufferedImage ret = image;
            image = null;  // don't keep a reference here
            return ret;
        } finally {
            clearThreadLock();
        }
    }

    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 {
            // Set the output color space equal to the input colorspace
            // This disables all conversions
            setOutColorSpace(structPointer, 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);
        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;
        }
        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 update listeners (don't call back if none)
        boolean callbackUpdates = ((updateListeners != null)
                                   || (progressListeners != null));

        // Set up progression data
        initProgressData();
        // if we have a metadata object, we can count the scans
        // and set knownPassCount
        if (imageIndex == imageMetadataIndex) { // We have metadata
            knownPassCount = 0;
            for (Iterator iter = imageMetadata.markerSequence.iterator();
                 iter.hasNext();) {
                if (iter.next() instanceof SOSMarkerSegment) {
                    knownPassCount++;
                }
            }
        }
        progInterval = Math.max((target.getHeight()-1) / 20, 1);
        if (knownPassCount > 0) {
            progInterval *= knownPassCount;
        } else if (maxProgressivePass != Integer.MAX_VALUE) {
            progInterval *= (maxProgressivePass - minProgressivePass + 1);
        }

        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("callbackUpdates is " + callbackUpdates);
        }

        // Finally, we are ready to read

        processImageStarted(currentImage);

        boolean aborted = false;

        // Note that getData disables acceleration on buffer, but it is
        // just a 1-line intermediate data transfer buffer that will not
        // affect the acceleration of the resulting image.
        aborted = readImage(structPointer,
                            buffer.getData(),
                            numRasterBands,
                            srcBands,
                            bandSizes,
                            srcROI.x, srcROI.y,
                            srcROI.width, srcROI.height,
                            periodX, periodY,
                            abbrevQTables,
                            abbrevDCHuffmanTables,
                            abbrevACHuffmanTables,
                            minProgressivePass, maxProgressivePass,
                            callbackUpdates);

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

        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, boolean progressive) {
        if (convert != null) {
            convert.filter(raster, raster);
        }
        target.setRect(destROI.x, destROI.y + y, raster);

        processImageUpdate(image,
                           destROI.x, destROI.y+y,
                           raster.getWidth(), 1,
                           1, 1,
                           destinationBands);
        if ((y > 0) && (y%progInterval == 0)) {
            int height = target.getHeight()-1;
            float percentOfPass = ((float)y)/height;
            if (progressive) {
                if (knownPassCount != UNKNOWN) {
                    processImageProgress((pass + percentOfPass)*100.0F
                                         / knownPassCount);
                } else if (maxProgressivePass != Integer.MAX_VALUE) {
                    // Use the range of allowed progressive passes
                    processImageProgress((pass + percentOfPass)*100.0F
                        / (maxProgressivePass - minProgressivePass + 1));
                } else {
                    // Assume there are a minimum of MIN_ESTIMATED_PASSES
                    // and that there is always one more pass
                    // Compute the percentage as the percentage at the end
                    // of the previous pass, plus the percentage of this
                    // pass scaled to be the percentage of the total remaining,
                    // assuming a minimum of MIN_ESTIMATED_PASSES passes and
                    // that there is always one more pass.  This is monotonic
                    // and asymptotic to 1.0, which is what we need.
                    int remainingPasses = // including this one
                        Math.max(2, MIN_ESTIMATED_PASSES-pass);
                    int totalPasses = pass + remainingPasses-1;
                    progInterval = Math.max(height/20*totalPasses,
                                            totalPasses);
                    if (y%progInterval == 0) {
                        percentToDate = previousPassPercentage +
                            (1.0F - previousPassPercentage)
                            * (percentOfPass)/remainingPasses;
                        if (debug) {
                            System.out.print("pass= " + pass);
                            System.out.print(", y= " + y);
                            System.out.print(", progInt= " + progInterval);
                            System.out.print(", % of pass: " + percentOfPass);
                            System.out.print(", rem. passes: "
                                             + remainingPasses);
                            System.out.print(", prev%: "
                                             + previousPassPercentage);
                            System.out.print(", %ToDate: " + percentToDate);
                            System.out.print(" ");
                        }
                        processImageProgress(percentToDate*100.0F);
                    }
                }
            } else {
                processImageProgress(percentOfPass * 100.0F);
            }
        }
    }

    private void initProgressData() {
        knownPassCount = UNKNOWN;
        pass = 0;
        percentToDate = 0.0F;
        previousPassPercentage = 0.0F;
        progInterval = 0;
    }

    private void passStarted (int pass) {
        this.pass = pass;
        previousPassPercentage = percentToDate;
        processPassStarted(image,
                           pass,
                           minProgressivePass,
                           maxProgressivePass,
                           0, 0,
                           1,1,
                           destinationBands);
    }

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

    void thumbnailStarted(int thumbnailIndex) {
        processThumbnailStarted(currentImage, thumbnailIndex);
    }

    // Provide access to protected superclass method
    void thumbnailProgress(float percentageDone) {
        processThumbnailProgress(percentageDone);
    }

    // Provide access to protected superclass method
    void thumbnailComplete() {
        processThumbnailComplete();
    }

    /**
     * Returns <code>true</code> if the read was aborted.
     */
    private native boolean readImage(long structPointer,
                                     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 wantUpdates);

    public void abort() {
        setThreadLock();
        try {
            super.abort();
            abortRead(structPointer);
        } finally {
            clearThreadLock();
        }
    }

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

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

    public boolean canReadRaster() {
        return true;
    }

    public Raster readRaster(int imageIndex, ImageReadParam param)
        throws IOException {
        setThreadLock();
        Raster retval = null;
        try {
            /*
             * This could be further optimized by not resetting the dest.
             * offset and creating a translated raster in readInternal()
             * (see bug 4994702 for more info).
             */

            // 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 (param != null) {
                saveDestOffset = param.getDestinationOffset();
                param.setDestinationOffset(new Point(0, 0));
            }
            retval = readInternal(imageIndex, param, true);
            // Apply the destination offset, if any, as a logical offset
            if (saveDestOffset != null) {
                target = target.createWritableTranslatedChild(saveDestOffset.x,
                                                              saveDestOffset.y);
            }
        } catch (RuntimeException e) {
            resetLibraryState(structPointer);
            throw e;
        } catch (IOException e) {
            resetLibraryState(structPointer);
            throw e;
        } finally {
            clearThreadLock();
        }
        return retval;
    }

    public boolean readerSupportsThumbnails() {
        return true;
    }

    public int getNumThumbnails(int imageIndex) throws IOException {
        setThreadLock();
        try {
            getImageMetadata(imageIndex);  // checks iis state for us
            // Now check the jfif segments
            JFIFMarkerSegment jfif =
                (JFIFMarkerSegment) imageMetadata.findMarkerSegment
                (JFIFMarkerSegment.class, true);
            int retval = 0;
            if (jfif != null) {
                retval = (jfif.thumb == null) ? 0 : 1;
                retval += jfif.extSegments.size();
            }
            return retval;
        } finally {
            clearThreadLock();
        }
    }

    public int getThumbnailWidth(int imageIndex, int thumbnailIndex)
        throws IOException {
        setThreadLock();
        try {
            if ((thumbnailIndex < 0)
                || (thumbnailIndex >= getNumThumbnails(imageIndex))) {
                throw new IndexOutOfBoundsException("No such thumbnail");
            }
            // Now we know that there is a jfif segment
            JFIFMarkerSegment jfif =
                (JFIFMarkerSegment) imageMetadata.findMarkerSegment
                (JFIFMarkerSegment.class, true);
            return  jfif.getThumbnailWidth(thumbnailIndex);
        } finally {
            clearThreadLock();
        }
    }

    public int getThumbnailHeight(int imageIndex, int thumbnailIndex)
        throws IOException {
        setThreadLock();
        try {
            if ((thumbnailIndex < 0)
                || (thumbnailIndex >= getNumThumbnails(imageIndex))) {
                throw new IndexOutOfBoundsException("No such thumbnail");
            }
            // Now we know that there is a jfif segment
            JFIFMarkerSegment jfif =
                (JFIFMarkerSegment) imageMetadata.findMarkerSegment
                (JFIFMarkerSegment.class, true);
            return  jfif.getThumbnailHeight(thumbnailIndex);
        } finally {
            clearThreadLock();
        }
    }

    public BufferedImage readThumbnail(int imageIndex,
                                       int thumbnailIndex)
        throws IOException {
        setThreadLock();
        try {
            if ((thumbnailIndex < 0)
                || (thumbnailIndex >= getNumThumbnails(imageIndex))) {
                throw new IndexOutOfBoundsException("No such thumbnail");
            }
            // Now we know that there is a jfif segment and that iis is good
            JFIFMarkerSegment jfif =
                (JFIFMarkerSegment) imageMetadata.findMarkerSegment
                (JFIFMarkerSegment.class, true);
            return  jfif.getThumbnail(iis, thumbnailIndex, this);
        } finally {
            clearThreadLock();
        }
    }

    private void resetInternalState() {
        // reset C structures
        resetReader(structPointer);

        // reset local Java structures
        numImages = 0;
        imagePositions = new ArrayList();
        currentImage = -1;
        image = null;
        raster = null;
        target = null;
        buffer = null;
        destROI = null;
        destinationBands = null;
        streamMetadata = null;
        imageMetadata = null;
        imageMetadataIndex = -1;
        haveSeeked = false;
        tablesOnlyChecked = false;
        iccCS = null;
        initProgressData();
    }

    public void reset() {
        setThreadLock();
        try {
            super.reset();
        } finally {
            clearThreadLock();
        }
    }

    private native void resetReader(long structPointer);

    public void dispose() {
        setThreadLock();
        try {
            if (structPointer != 0) {
                disposerRecord.dispose();
                structPointer = 0;
            }
        } finally {
            clearThreadLock();
        }
    }

    private static native void disposeReader(long structPointer);

    private static class JPEGReaderDisposerRecord implements DisposerRecord {
        private long pData;

        public JPEGReaderDisposerRecord(long pData) {
            this.pData = pData;
        }

        public synchronized void dispose() {
            if (pData != 0) {
                disposeReader(pData);
                pData = 0;
            }
        }
    }

    private Thread theThread = null;
    private int theLockCount = 0;

    private synchronized void setThreadLock() {
        Thread currThread = Thread.currentThread();
        if (theThread != null) {
            if (theThread != currThread) {
                // it looks like that this reader instance is used
                // by multiple threads.
                throw new IllegalStateException("Attempt to use instance of " +
                                                this + " locked on thread " +
                                                theThread + " from thread " +
                                                currThread);
            } else {
                theLockCount ++;
            }
        } else {
            theThread = currThread;
            theLockCount = 1;
        }
    }

    private synchronized void clearThreadLock() {
        Thread currThread = Thread.currentThread();
        if (theThread == null || theThread != currThread) {
            throw new IllegalStateException("Attempt to clear thread lock " +
                                            " form wrong thread." +
                                            " Locked thread: " + theThread +
                                            "; current thread: " + currThread);
        }
        theLockCount --;
        if (theLockCount == 0) {
            theThread = null;
        }
    }
}

/**
 * An internal helper class that wraps producer's iterator
 * and extracts specifier instances on demand.
 */
class ImageTypeIterator implements Iterator<ImageTypeSpecifier> {
     private Iterator<ImageTypeProducer> producers;
     private ImageTypeSpecifier theNext = null;

     public ImageTypeIterator(Iterator<ImageTypeProducer> producers) {
         this.producers = producers;
     }

     public boolean hasNext() {
         if (theNext != null) {
             return true;
         }
         if (!producers.hasNext()) {
             return false;
         }
         do {
             theNext = producers.next().getType();
         } while (theNext == null && producers.hasNext());

         return (theNext != null);
     }

     public ImageTypeSpecifier next() {
         if (theNext != null || hasNext()) {
             ImageTypeSpecifier t = theNext;
             theNext = null;
             return t;
         } else {
             throw new NoSuchElementException();
         }
     }

     public void remove() {
         producers.remove();
     }
}

/**
 * An internal helper class that provides means for deferred creation
 * of ImageTypeSpecifier instance required to describe available
 * destination types.
 *
 * This implementation only supports standard
 * jpeg color spaces (defined by corresponding JCS color space code).
 *
 * To support other color spaces one can override produce() method to
 * return custom instance of ImageTypeSpecifier.
 */
class ImageTypeProducer {

    private ImageTypeSpecifier type = null;
    boolean failed = false;
    private int csCode;

    public ImageTypeProducer(int csCode) {
        this.csCode = csCode;
    }

    public ImageTypeProducer() {
        csCode = -1; // undefined
    }

    public synchronized ImageTypeSpecifier getType() {
        if (!failed && type == null) {
            try {
                type = produce();
            } catch (Throwable e) {
                failed = true;
            }
        }
        return type;
    }

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

    public synchronized static ImageTypeProducer getTypeProducer(int csCode) {
        if (csCode < 0 || csCode >= JPEG.NUM_JCS_CODES) {
            return null;
        }
        if (defaultTypes[csCode] == null) {
            defaultTypes[csCode] = new ImageTypeProducer(csCode);
        }
        return defaultTypes[csCode];
    }

    protected ImageTypeSpecifier produce() {
        switch (csCode) {
            case JPEG.JCS_GRAYSCALE:
                return ImageTypeSpecifier.createFromBufferedImageType
                        (BufferedImage.TYPE_BYTE_GRAY);
            case JPEG.JCS_RGB:
                return ImageTypeSpecifier.createInterleaved(JPEG.JCS.sRGB,
                        JPEG.bOffsRGB,
                        DataBuffer.TYPE_BYTE,
                        false,
                        false);
            case JPEG.JCS_RGBA:
                return ImageTypeSpecifier.createPacked(JPEG.JCS.sRGB,
                        0xff000000,
                        0x00ff0000,
                        0x0000ff00,
                        0x000000ff,
                        DataBuffer.TYPE_INT,
                        false);
            case JPEG.JCS_YCC:
                if (JPEG.JCS.getYCC() != null) {
                    return ImageTypeSpecifier.createInterleaved(
                            JPEG.JCS.getYCC(),
                        JPEG.bandOffsets[2],
                        DataBuffer.TYPE_BYTE,
                        false,
                        false);
                } else {
                    return null;
                }
            case JPEG.JCS_YCCA:
                if (JPEG.JCS.getYCC() != null) {
                    return ImageTypeSpecifier.createInterleaved(
                            JPEG.JCS.getYCC(),
                        JPEG.bandOffsets[3],
                        DataBuffer.TYPE_BYTE,
                        true,
                        false);
                } else {
                    return null;
                }
            default:
                return null;
        }
    }
}
