/*
 * Copyright (c) 2001, 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.IIOImage;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageReader;
import javax.imageio.metadata.IIOInvalidTreeException;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;
import javax.imageio.stream.MemoryCacheImageOutputStream;
import javax.imageio.event.IIOReadProgressListener;

import java.awt.Graphics;
import java.awt.color.ICC_Profile;
import java.awt.color.ICC_ColorSpace;
import java.awt.color.ColorSpace;
import java.awt.image.ColorModel;
import java.awt.image.SampleModel;
import java.awt.image.IndexColorModel;
import java.awt.image.ComponentColorModel;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.io.IOException;
import java.io.ByteArrayOutputStream;
import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;

import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.NamedNodeMap;

/**
 * A JFIF (JPEG File Interchange Format) APP0 (Application-Specific)
 * marker segment.  Inner classes are included for JFXX extension
 * marker segments, for different varieties of thumbnails, and for
 * ICC Profile APP2 marker segments.  Any of these secondary types
 * that occur are kept as members of a single JFIFMarkerSegment object.
 */
class JFIFMarkerSegment extends MarkerSegment {
    int majorVersion;
    int minorVersion;
    int resUnits;
    int Xdensity;
    int Ydensity;
    int thumbWidth;
    int thumbHeight;
    JFIFThumbRGB thumb = null;  // If present
    ArrayList extSegments = new ArrayList();
    ICCMarkerSegment iccSegment = null; // optional ICC
    private static final int THUMB_JPEG = 0x10;
    private static final int THUMB_PALETTE = 0x11;
    private static final int THUMB_UNASSIGNED = 0x12;
    private static final int THUMB_RGB = 0x13;
    private static final int DATA_SIZE = 14;
    private static final int ID_SIZE = 5;
    private final int MAX_THUMB_WIDTH = 255;
    private final int MAX_THUMB_HEIGHT = 255;

    private final boolean debug = false;

    /**
     * Set to <code>true</code> when reading the chunks of an
     * ICC profile.  All chunks are consolidated to create a single
     * "segment" containing all the chunks.  This flag is a state
     * variable identifying whether to construct a new segment or
     * append to an old one.
     */
    private boolean inICC = false;

    /**
     * A placeholder for an ICC profile marker segment under
     * construction.  The segment is not added to the list
     * until all chunks have been read.
     */
    private ICCMarkerSegment tempICCSegment = null;


    /**
     * Default constructor.  Used to create a default JFIF header
     */
    JFIFMarkerSegment() {
        super(JPEG.APP0);
        majorVersion = 1;
        minorVersion = 2;
        resUnits = JPEG.DENSITY_UNIT_ASPECT_RATIO;
        Xdensity = 1;
        Ydensity = 1;
        thumbWidth = 0;
        thumbHeight = 0;
    }

    /**
     * Constructs a JFIF header by reading from a stream wrapped
     * in a JPEGBuffer.
     */
    JFIFMarkerSegment(JPEGBuffer buffer) throws IOException {
        super(buffer);
        buffer.bufPtr += ID_SIZE;  // skip the id, we already checked it

        majorVersion = buffer.buf[buffer.bufPtr++];
        minorVersion = buffer.buf[buffer.bufPtr++];
        resUnits = buffer.buf[buffer.bufPtr++];
        Xdensity = (buffer.buf[buffer.bufPtr++] & 0xff) << 8;
        Xdensity |= buffer.buf[buffer.bufPtr++] & 0xff;
        Ydensity = (buffer.buf[buffer.bufPtr++] & 0xff) << 8;
        Ydensity |= buffer.buf[buffer.bufPtr++] & 0xff;
        thumbWidth = buffer.buf[buffer.bufPtr++] & 0xff;
        thumbHeight = buffer.buf[buffer.bufPtr++] & 0xff;
        buffer.bufAvail -= DATA_SIZE;
        if (thumbWidth > 0) {
            thumb = new JFIFThumbRGB(buffer, thumbWidth, thumbHeight);
        }
    }

    /**
     * Constructs a JFIF header from a DOM Node.
     */
    JFIFMarkerSegment(Node node) throws IIOInvalidTreeException {
        this();
        updateFromNativeNode(node, true);
    }

    /**
     * Returns a deep-copy clone of this object.
     */
    protected Object clone() {
        JFIFMarkerSegment newGuy = (JFIFMarkerSegment) super.clone();
        if (!extSegments.isEmpty()) { // Clone the list with a deep copy
            newGuy.extSegments = new ArrayList();
            for (Iterator iter = extSegments.iterator(); iter.hasNext();) {
                JFIFExtensionMarkerSegment jfxx =
                    (JFIFExtensionMarkerSegment) iter.next();
                newGuy.extSegments.add(jfxx.clone());
            }
        }
        if (iccSegment != null) {
            newGuy.iccSegment = (ICCMarkerSegment) iccSegment.clone();
        }
        return newGuy;
    }

    /**
     * Add an JFXX extension marker segment from the stream wrapped
     * in the JPEGBuffer to the list of extension segments.
     */
    void addJFXX(JPEGBuffer buffer, JPEGImageReader reader)
        throws IOException {
        extSegments.add(new JFIFExtensionMarkerSegment(buffer, reader));
    }

    /**
     * Adds an ICC Profile APP2 segment from the stream wrapped
     * in the JPEGBuffer.
     */
    void addICC(JPEGBuffer buffer) throws IOException {
        if (inICC == false) {
            if (iccSegment != null) {
                throw new IIOException
                    ("> 1 ICC APP2 Marker Segment not supported");
            }
            tempICCSegment = new ICCMarkerSegment(buffer);
            if (inICC == false) { // Just one chunk
                iccSegment = tempICCSegment;
                tempICCSegment = null;
            }
        } else {
            if (tempICCSegment.addData(buffer) == true) {
                iccSegment = tempICCSegment;
                tempICCSegment = null;
            }
        }
    }

    /**
     * Add an ICC Profile APP2 segment by constructing it from
     * the given ICC_ColorSpace object.
     */
    void addICC(ICC_ColorSpace cs) throws IOException {
        if (iccSegment != null) {
            throw new IIOException
                ("> 1 ICC APP2 Marker Segment not supported");
        }
        iccSegment = new ICCMarkerSegment(cs);
    }

    /**
     * Returns a tree of DOM nodes representing this object and any
     * subordinate JFXX extension or ICC Profile segments.
     */
    IIOMetadataNode getNativeNode() {
        IIOMetadataNode node = new IIOMetadataNode("app0JFIF");
        node.setAttribute("majorVersion", Integer.toString(majorVersion));
        node.setAttribute("minorVersion", Integer.toString(minorVersion));
        node.setAttribute("resUnits", Integer.toString(resUnits));
        node.setAttribute("Xdensity", Integer.toString(Xdensity));
        node.setAttribute("Ydensity", Integer.toString(Ydensity));
        node.setAttribute("thumbWidth", Integer.toString(thumbWidth));
        node.setAttribute("thumbHeight", Integer.toString(thumbHeight));
        if (!extSegments.isEmpty()) {
            IIOMetadataNode JFXXnode = new IIOMetadataNode("JFXX");
            node.appendChild(JFXXnode);
            for (Iterator iter = extSegments.iterator(); iter.hasNext();) {
                JFIFExtensionMarkerSegment seg =
                    (JFIFExtensionMarkerSegment) iter.next();
                JFXXnode.appendChild(seg.getNativeNode());
            }
        }
        if (iccSegment != null) {
            node.appendChild(iccSegment.getNativeNode());
        }

        return node;
    }

    /**
     * Updates the data in this object from the given DOM Node tree.
     * If fromScratch is true, this object is being constructed.
     * Otherwise an existing object is being modified.
     * Throws an IIOInvalidTreeException if the tree is invalid in
     * any way.
     */
    void updateFromNativeNode(Node node, boolean fromScratch)
        throws IIOInvalidTreeException {
        // none of the attributes are required
        NamedNodeMap attrs = node.getAttributes();
        if (attrs.getLength() > 0) {
            int value = getAttributeValue(node, attrs, "majorVersion",
                                          0, 255, false);
            majorVersion = (value != -1) ? value : majorVersion;
            value = getAttributeValue(node, attrs, "minorVersion",
                                      0, 255, false);
            minorVersion = (value != -1) ? value : minorVersion;
            value = getAttributeValue(node, attrs, "resUnits", 0, 2, false);
            resUnits = (value != -1) ? value : resUnits;
            value = getAttributeValue(node, attrs, "Xdensity", 1, 65535, false);
            Xdensity = (value != -1) ? value : Xdensity;
            value = getAttributeValue(node, attrs, "Ydensity", 1, 65535, false);
            Ydensity = (value != -1) ? value : Ydensity;
            value = getAttributeValue(node, attrs, "thumbWidth", 0, 255, false);
            thumbWidth = (value != -1) ? value : thumbWidth;
            value = getAttributeValue(node, attrs, "thumbHeight", 0, 255, false);
            thumbHeight = (value != -1) ? value : thumbHeight;
        }
        if (node.hasChildNodes()) {
            NodeList children = node.getChildNodes();
            int count = children.getLength();
            if (count > 2) {
                throw new IIOInvalidTreeException
                    ("app0JFIF node cannot have > 2 children", node);
            }
            for (int i = 0; i < count; i++) {
                Node child = children.item(i);
                String name = child.getNodeName();
                if (name.equals("JFXX")) {
                    if ((!extSegments.isEmpty()) && fromScratch) {
                        throw new IIOInvalidTreeException
                            ("app0JFIF node cannot have > 1 JFXX node", node);
                    }
                    NodeList exts = child.getChildNodes();
                    int extCount = exts.getLength();
                    for (int j = 0; j < extCount; j++) {
                        Node ext = exts.item(j);
                        extSegments.add(new JFIFExtensionMarkerSegment(ext));
                    }
                }
                if (name.equals("app2ICC")) {
                    if ((iccSegment != null) && fromScratch) {
                        throw new IIOInvalidTreeException
                            ("> 1 ICC APP2 Marker Segment not supported", node);
                    }
                    iccSegment = new ICCMarkerSegment(child);
                }
            }
        }
    }

    int getThumbnailWidth(int index) {
        if (thumb != null) {
            if (index == 0) {
                return thumb.getWidth();
            }
            index--;
        }
        JFIFExtensionMarkerSegment jfxx =
            (JFIFExtensionMarkerSegment) extSegments.get(index);
        return jfxx.thumb.getWidth();
    }

    int getThumbnailHeight(int index) {
        if (thumb != null) {
            if (index == 0) {
                return thumb.getHeight();
            }
            index--;
        }
        JFIFExtensionMarkerSegment jfxx =
            (JFIFExtensionMarkerSegment) extSegments.get(index);
        return jfxx.thumb.getHeight();
    }

    BufferedImage getThumbnail(ImageInputStream iis,
                               int index,
                               JPEGImageReader reader) throws IOException {
        reader.thumbnailStarted(index);
        BufferedImage ret = null;
        if ((thumb != null) && (index == 0)) {
                ret = thumb.getThumbnail(iis, reader);
        } else {
            if (thumb != null) {
                index--;
            }
            JFIFExtensionMarkerSegment jfxx =
                (JFIFExtensionMarkerSegment) extSegments.get(index);
            ret = jfxx.thumb.getThumbnail(iis, reader);
        }
        reader.thumbnailComplete();
        return ret;
    }


    /**
     * Writes the data for this segment to the stream in
     * valid JPEG format.  Assumes that there will be no thumbnail.
     */
    void write(ImageOutputStream ios,
               JPEGImageWriter writer) throws IOException {
        // No thumbnail
        write(ios, null, writer);
    }

    /**
     * Writes the data for this segment to the stream in
     * valid JPEG format.  The length written takes the thumbnail
     * width and height into account.  If necessary, the thumbnail
     * is clipped to 255 x 255 and a warning is sent to the writer
     * argument.  Progress updates are sent to the writer argument.
     */
    void write(ImageOutputStream ios,
               BufferedImage thumb,
               JPEGImageWriter writer) throws IOException {
        int thumbWidth = 0;
        int thumbHeight = 0;
        int thumbLength = 0;
        int [] thumbData = null;
        if (thumb != null) {
            // Clip if necessary and get the data in thumbData
            thumbWidth = thumb.getWidth();
            thumbHeight = thumb.getHeight();
            if ((thumbWidth > MAX_THUMB_WIDTH)
                || (thumbHeight > MAX_THUMB_HEIGHT)) {
                writer.warningOccurred(JPEGImageWriter.WARNING_THUMB_CLIPPED);
            }
            thumbWidth = Math.min(thumbWidth, MAX_THUMB_WIDTH);
            thumbHeight = Math.min(thumbHeight, MAX_THUMB_HEIGHT);
            thumbData = thumb.getRaster().getPixels(0, 0,
                                                    thumbWidth, thumbHeight,
                                                    (int []) null);
            thumbLength = thumbData.length;
        }
        length = DATA_SIZE + LENGTH_SIZE + thumbLength;
        writeTag(ios);
        byte [] id = {0x4A, 0x46, 0x49, 0x46, 0x00};
        ios.write(id);
        ios.write(majorVersion);
        ios.write(minorVersion);
        ios.write(resUnits);
        write2bytes(ios, Xdensity);
        write2bytes(ios, Ydensity);
        ios.write(thumbWidth);
        ios.write(thumbHeight);
        if (thumbData != null) {
            writer.thumbnailStarted(0);
            writeThumbnailData(ios, thumbData, writer);
            writer.thumbnailComplete();
        }
    }

    /*
     * Write out the values in the integer array as a sequence of bytes,
     * reporting progress to the writer argument.
     */
    void writeThumbnailData(ImageOutputStream ios,
                            int [] thumbData,
                            JPEGImageWriter writer) throws IOException {
        int progInterval = thumbData.length / 20;  // approx. every 5%
        if (progInterval == 0) {
            progInterval = 1;
        }
        for (int i = 0; i < thumbData.length; i++) {
            ios.write(thumbData[i]);
            if ((i > progInterval) && (i % progInterval == 0)) {
                writer.thumbnailProgress
                    (((float) i * 100) / ((float) thumbData.length));
            }
        }
    }

    /**
     * Write out this JFIF Marker Segment, including a thumbnail or
     * appending a series of JFXX Marker Segments, as appropriate.
     * Warnings and progress reports are sent to the writer argument.
     * The list of thumbnails is matched to the list of JFXX extension
     * segments, if any, in order to determine how to encode the
     * thumbnails.  If there are more thumbnails than metadata segments,
     * default encoding is used for the extra thumbnails.
     */
    void writeWithThumbs(ImageOutputStream ios,
                         List thumbnails,
                         JPEGImageWriter writer) throws IOException {
        if (thumbnails != null) {
            JFIFExtensionMarkerSegment jfxx = null;
            if (thumbnails.size() == 1) {
                if (!extSegments.isEmpty()) {
                    jfxx = (JFIFExtensionMarkerSegment) extSegments.get(0);
                }
                writeThumb(ios,
                           (BufferedImage) thumbnails.get(0),
                           jfxx,
                           0,
                           true,
                           writer);
            } else {
                // All others write as separate JFXX segments
                write(ios, writer);  // Just the header without any thumbnail
                for (int i = 0; i < thumbnails.size(); i++) {
                    jfxx = null;
                    if (i < extSegments.size()) {
                        jfxx = (JFIFExtensionMarkerSegment) extSegments.get(i);
                    }
                    writeThumb(ios,
                               (BufferedImage) thumbnails.get(i),
                               jfxx,
                               i,
                               false,
                               writer);
                }
            }
        } else {  // No thumbnails
            write(ios, writer);
        }

    }

    private void writeThumb(ImageOutputStream ios,
                            BufferedImage thumb,
                            JFIFExtensionMarkerSegment jfxx,
                            int index,
                            boolean onlyOne,
                            JPEGImageWriter writer) throws IOException {
        ColorModel cm = thumb.getColorModel();
        ColorSpace cs = cm.getColorSpace();

        if (cm instanceof IndexColorModel) {
            // We never write a palette image into the header
            // So if it's the only one, we need to write the header first
            if (onlyOne) {
                write(ios, writer);
            }
            if ((jfxx == null)
                || (jfxx.code == THUMB_PALETTE)) {
                writeJFXXSegment(index, thumb, ios, writer); // default
            } else {
                // Expand to RGB
                BufferedImage thumbRGB =
                    ((IndexColorModel) cm).convertToIntDiscrete
                    (thumb.getRaster(), false);
                jfxx.setThumbnail(thumbRGB);
                writer.thumbnailStarted(index);
                jfxx.write(ios, writer);  // Handles clipping if needed
                writer.thumbnailComplete();
            }
        } else if (cs.getType() == ColorSpace.TYPE_RGB) {
            if (jfxx == null) {
                if (onlyOne) {
                    write(ios, thumb, writer); // As part of the header
                } else {
                    writeJFXXSegment(index, thumb, ios, writer); // default
                }
            } else {
                // If this is the only one, write the header first
                if (onlyOne) {
                    write(ios, writer);
                }
                if (jfxx.code == THUMB_PALETTE) {
                    writeJFXXSegment(index, thumb, ios, writer); // default
                    writer.warningOccurred
                        (JPEGImageWriter.WARNING_NO_RGB_THUMB_AS_INDEXED);
                } else {
                    jfxx.setThumbnail(thumb);
                    writer.thumbnailStarted(index);
                    jfxx.write(ios, writer);  // Handles clipping if needed
                    writer.thumbnailComplete();
                }
            }
        } else if (cs.getType() == ColorSpace.TYPE_GRAY) {
            if (jfxx == null) {
                if (onlyOne) {
                    BufferedImage thumbRGB = expandGrayThumb(thumb);
                    write(ios, thumbRGB, writer); // As part of the header
                } else {
                    writeJFXXSegment(index, thumb, ios, writer); // default
                }
            } else {
                // If this is the only one, write the header first
                if (onlyOne) {
                    write(ios, writer);
                }
                if (jfxx.code == THUMB_RGB) {
                    BufferedImage thumbRGB = expandGrayThumb(thumb);
                    writeJFXXSegment(index, thumbRGB, ios, writer);
                } else if (jfxx.code == THUMB_JPEG) {
                    jfxx.setThumbnail(thumb);
                    writer.thumbnailStarted(index);
                    jfxx.write(ios, writer);  // Handles clipping if needed
                    writer.thumbnailComplete();
                } else if (jfxx.code == THUMB_PALETTE) {
                    writeJFXXSegment(index, thumb, ios, writer); // default
                    writer.warningOccurred
                        (JPEGImageWriter.WARNING_NO_GRAY_THUMB_AS_INDEXED);
                }
            }
        } else {
            writer.warningOccurred
                (JPEGImageWriter.WARNING_ILLEGAL_THUMBNAIL);
        }
    }

    // Could put reason codes in here to be parsed in writeJFXXSegment
    // in order to provide more meaningful warnings.
    private class IllegalThumbException extends Exception {}

    /**
     * Writes out a new JFXX extension segment, without saving it.
     */
    private void writeJFXXSegment(int index,
                                  BufferedImage thumbnail,
                                  ImageOutputStream ios,
                                  JPEGImageWriter writer) throws IOException {
        JFIFExtensionMarkerSegment jfxx = null;
        try {
             jfxx = new JFIFExtensionMarkerSegment(thumbnail);
        } catch (IllegalThumbException e) {
            writer.warningOccurred
                (JPEGImageWriter.WARNING_ILLEGAL_THUMBNAIL);
            return;
        }
        writer.thumbnailStarted(index);
        jfxx.write(ios, writer);
        writer.thumbnailComplete();
    }


    /**
     * Return an RGB image that is the expansion of the given grayscale
     * image.
     */
    private static BufferedImage expandGrayThumb(BufferedImage thumb) {
        BufferedImage ret = new BufferedImage(thumb.getWidth(),
                                              thumb.getHeight(),
                                              BufferedImage.TYPE_INT_RGB);
        Graphics g = ret.getGraphics();
        g.drawImage(thumb, 0, 0, null);
        return ret;
    }

    /**
     * Writes out a default JFIF marker segment to the given
     * output stream.  If <code>thumbnails</code> is not <code>null</code>,
     * writes out the set of thumbnail images as JFXX marker segments, or
     * incorporated into the JFIF segment if appropriate.
     * If <code>iccProfile</code> is not <code>null</code>,
     * writes out the profile after the JFIF segment using as many APP2
     * marker segments as necessary.
     */
    static void writeDefaultJFIF(ImageOutputStream ios,
                                 List thumbnails,
                                 ICC_Profile iccProfile,
                                 JPEGImageWriter writer)
        throws IOException {

        JFIFMarkerSegment jfif = new JFIFMarkerSegment();
        jfif.writeWithThumbs(ios, thumbnails, writer);
        if (iccProfile != null) {
            writeICC(iccProfile, ios);
        }
    }

    /**
     * Prints out the contents of this object to System.out for debugging.
     */
    void print() {
        printTag("JFIF");
        System.out.print("Version ");
        System.out.print(majorVersion);
        System.out.println(".0"
                           + Integer.toString(minorVersion));
        System.out.print("Resolution units: ");
        System.out.println(resUnits);
        System.out.print("X density: ");
        System.out.println(Xdensity);
        System.out.print("Y density: ");
        System.out.println(Ydensity);
        System.out.print("Thumbnail Width: ");
        System.out.println(thumbWidth);
        System.out.print("Thumbnail Height: ");
        System.out.println(thumbHeight);
        if (!extSegments.isEmpty()) {
            for (Iterator iter = extSegments.iterator(); iter.hasNext();) {
                JFIFExtensionMarkerSegment extSegment =
                    (JFIFExtensionMarkerSegment) iter.next();
                extSegment.print();
            }
        }
        if (iccSegment != null) {
            iccSegment.print();
        }
    }

    /**
     * A JFIF extension APP0 marker segment.
     */
    class JFIFExtensionMarkerSegment extends MarkerSegment {
        int code;
        JFIFThumb thumb;
        private static final int DATA_SIZE = 6;
        private static final int ID_SIZE = 5;

        JFIFExtensionMarkerSegment(JPEGBuffer buffer, JPEGImageReader reader)
            throws IOException {

            super(buffer);
            buffer.bufPtr += ID_SIZE;  // skip the id, we already checked it

            code = buffer.buf[buffer.bufPtr++] & 0xff;
            buffer.bufAvail -= DATA_SIZE;
            if (code == THUMB_JPEG) {
                thumb = new JFIFThumbJPEG(buffer, length, reader);
            } else {
                buffer.loadBuf(2);
                int thumbX = buffer.buf[buffer.bufPtr++] & 0xff;
                int thumbY = buffer.buf[buffer.bufPtr++] & 0xff;
                buffer.bufAvail -= 2;
                // following constructors handle bufAvail
                if (code == THUMB_PALETTE) {
                    thumb = new JFIFThumbPalette(buffer, thumbX, thumbY);
                } else {
                    thumb = new JFIFThumbRGB(buffer, thumbX, thumbY);
                }
            }
        }

        JFIFExtensionMarkerSegment(Node node) throws IIOInvalidTreeException {
            super(JPEG.APP0);
            NamedNodeMap attrs = node.getAttributes();
            if (attrs.getLength() > 0) {
                code = getAttributeValue(node,
                                         attrs,
                                         "extensionCode",
                                         THUMB_JPEG,
                                         THUMB_RGB,
                                         false);
                if (code == THUMB_UNASSIGNED) {
                throw new IIOInvalidTreeException
                    ("invalid extensionCode attribute value", node);
                }
            } else {
                code = THUMB_UNASSIGNED;
            }
            // Now the child
            if (node.getChildNodes().getLength() != 1) {
                throw new IIOInvalidTreeException
                    ("app0JFXX node must have exactly 1 child", node);
            }
            Node child = node.getFirstChild();
            String name = child.getNodeName();
            if (name.equals("JFIFthumbJPEG")) {
                if (code == THUMB_UNASSIGNED) {
                    code = THUMB_JPEG;
                }
                thumb = new JFIFThumbJPEG(child);
            } else if (name.equals("JFIFthumbPalette")) {
                if (code == THUMB_UNASSIGNED) {
                    code = THUMB_PALETTE;
                }
                thumb = new JFIFThumbPalette(child);
            } else if (name.equals("JFIFthumbRGB")) {
                if (code == THUMB_UNASSIGNED) {
                    code = THUMB_RGB;
                }
                thumb = new JFIFThumbRGB(child);
            } else {
                throw new IIOInvalidTreeException
                    ("unrecognized app0JFXX child node", node);
            }
        }

        JFIFExtensionMarkerSegment(BufferedImage thumbnail)
            throws IllegalThumbException {

            super(JPEG.APP0);
            ColorModel cm = thumbnail.getColorModel();
            int csType = cm.getColorSpace().getType();
            if (cm.hasAlpha()) {
                throw new IllegalThumbException();
            }
            if (cm instanceof IndexColorModel) {
                code = THUMB_PALETTE;
                thumb = new JFIFThumbPalette(thumbnail);
            } else if (csType == ColorSpace.TYPE_RGB) {
                code = THUMB_RGB;
                thumb = new JFIFThumbRGB(thumbnail);
            } else if (csType == ColorSpace.TYPE_GRAY) {
                code = THUMB_JPEG;
                thumb = new JFIFThumbJPEG(thumbnail);
            } else {
                throw new IllegalThumbException();
            }
        }

        void setThumbnail(BufferedImage thumbnail) {
            try {
                switch (code) {
                case THUMB_PALETTE:
                    thumb = new JFIFThumbPalette(thumbnail);
                    break;
                case THUMB_RGB:
                    thumb = new JFIFThumbRGB(thumbnail);
                    break;
                case THUMB_JPEG:
                    thumb = new JFIFThumbJPEG(thumbnail);
                    break;
                }
            } catch (IllegalThumbException e) {
                // Should never happen
                throw new InternalError("Illegal thumb in setThumbnail!");
            }
        }

        protected Object clone() {
            JFIFExtensionMarkerSegment newGuy =
                (JFIFExtensionMarkerSegment) super.clone();
            if (thumb != null) {
                newGuy.thumb = (JFIFThumb) thumb.clone();
            }
            return newGuy;
        }

        IIOMetadataNode getNativeNode() {
            IIOMetadataNode node = new IIOMetadataNode("app0JFXX");
            node.setAttribute("extensionCode", Integer.toString(code));
            node.appendChild(thumb.getNativeNode());
            return node;
        }

        void write(ImageOutputStream ios,
                   JPEGImageWriter writer) throws IOException {
            length = LENGTH_SIZE + DATA_SIZE + thumb.getLength();
            writeTag(ios);
            byte [] id = {0x4A, 0x46, 0x58, 0x58, 0x00};
            ios.write(id);
            ios.write(code);
            thumb.write(ios, writer);
        }

        void print() {
            printTag("JFXX");
            thumb.print();
        }
    }

    /**
     * A superclass for the varieties of thumbnails that can
     * be stored in a JFIF extension marker segment.
     */
    abstract class JFIFThumb implements Cloneable {
        long streamPos = -1L;  // Save the thumbnail pos when reading
        abstract int getLength(); // When writing
        abstract int getWidth();
        abstract int getHeight();
        abstract BufferedImage getThumbnail(ImageInputStream iis,
                                            JPEGImageReader reader)
            throws IOException;

        protected JFIFThumb() {}

        protected JFIFThumb(JPEGBuffer buffer) throws IOException{
            // Save the stream position for reading the thumbnail later
            streamPos = buffer.getStreamPosition();
        }

        abstract void print();

        abstract IIOMetadataNode getNativeNode();

        abstract void write(ImageOutputStream ios,
                            JPEGImageWriter writer) throws IOException;

        protected Object clone() {
            try {
                return super.clone();
            } catch (CloneNotSupportedException e) {} // won't happen
            return null;
        }

    }

    abstract class JFIFThumbUncompressed extends JFIFThumb {
        BufferedImage thumbnail = null;
        int thumbWidth;
        int thumbHeight;
        String name;

        JFIFThumbUncompressed(JPEGBuffer buffer,
                              int width,
                              int height,
                              int skip,
                              String name)
            throws IOException {
            super(buffer);
            thumbWidth = width;
            thumbHeight = height;
            // Now skip the thumbnail data
            buffer.skipData(skip);
            this.name = name;
        }

        JFIFThumbUncompressed(Node node, String name)
            throws IIOInvalidTreeException {

            thumbWidth = 0;
            thumbHeight = 0;
            this.name = name;
            NamedNodeMap attrs = node.getAttributes();
            int count = attrs.getLength();
            if (count > 2) {
                throw new IIOInvalidTreeException
                    (name +" node cannot have > 2 attributes", node);
            }
            if (count != 0) {
                int value = getAttributeValue(node, attrs, "thumbWidth",
                                              0, 255, false);
                thumbWidth = (value != -1) ? value : thumbWidth;
                value = getAttributeValue(node, attrs, "thumbHeight",
                                          0, 255, false);
                thumbHeight = (value != -1) ? value : thumbHeight;
            }
        }

        JFIFThumbUncompressed(BufferedImage thumb) {
            thumbnail = thumb;
            thumbWidth = thumb.getWidth();
            thumbHeight = thumb.getHeight();
            name = null;  // not used when writing
        }

        void readByteBuffer(ImageInputStream iis,
                            byte [] data,
                            JPEGImageReader reader,
                            float workPortion,
                            float workOffset) throws IOException {
            int progInterval = Math.max((int)(data.length/20/workPortion),
                                        1);
            for (int offset = 0;
                 offset < data.length;) {
                int len = Math.min(progInterval, data.length-offset);
                iis.read(data, offset, len);
                offset += progInterval;
                float percentDone = ((float) offset* 100)
                    / data.length
                    * workPortion + workOffset;
                if (percentDone > 100.0F) {
                    percentDone = 100.0F;
                }
                reader.thumbnailProgress (percentDone);
            }
        }


        int getWidth() {
            return thumbWidth;
        }

        int getHeight() {
            return thumbHeight;
        }

        IIOMetadataNode getNativeNode() {
            IIOMetadataNode node = new IIOMetadataNode(name);
            node.setAttribute("thumbWidth", Integer.toString(thumbWidth));
            node.setAttribute("thumbHeight", Integer.toString(thumbHeight));
            return node;
        }

        void write(ImageOutputStream ios,
                   JPEGImageWriter writer) throws IOException {
            if ((thumbWidth > MAX_THUMB_WIDTH)
                || (thumbHeight > MAX_THUMB_HEIGHT)) {
                writer.warningOccurred(JPEGImageWriter.WARNING_THUMB_CLIPPED);
            }
            thumbWidth = Math.min(thumbWidth, MAX_THUMB_WIDTH);
            thumbHeight = Math.min(thumbHeight, MAX_THUMB_HEIGHT);
            ios.write(thumbWidth);
            ios.write(thumbHeight);
        }

        void writePixels(ImageOutputStream ios,
                         JPEGImageWriter writer) throws IOException {
            if ((thumbWidth > MAX_THUMB_WIDTH)
                || (thumbHeight > MAX_THUMB_HEIGHT)) {
                writer.warningOccurred(JPEGImageWriter.WARNING_THUMB_CLIPPED);
            }
            thumbWidth = Math.min(thumbWidth, MAX_THUMB_WIDTH);
            thumbHeight = Math.min(thumbHeight, MAX_THUMB_HEIGHT);
            int [] data = thumbnail.getRaster().getPixels(0, 0,
                                                          thumbWidth,
                                                          thumbHeight,
                                                          (int []) null);
            writeThumbnailData(ios, data, writer);
        }

        void print() {
            System.out.print(name + " width: ");
            System.out.println(thumbWidth);
            System.out.print(name + " height: ");
            System.out.println(thumbHeight);
        }

    }

    /**
     * A JFIF thumbnail stored as RGB, one byte per channel,
     * interleaved.
     */
    class JFIFThumbRGB extends JFIFThumbUncompressed {

        JFIFThumbRGB(JPEGBuffer buffer, int width, int height)
            throws IOException {

            super(buffer, width, height, width*height*3, "JFIFthumbRGB");
        }

        JFIFThumbRGB(Node node) throws IIOInvalidTreeException {
            super(node, "JFIFthumbRGB");
        }

        JFIFThumbRGB(BufferedImage thumb) throws IllegalThumbException {
            super(thumb);
        }

        int getLength() {
            return (thumbWidth*thumbHeight*3);
        }

        BufferedImage getThumbnail(ImageInputStream iis,
                                   JPEGImageReader reader)
            throws IOException {
            iis.mark();
            iis.seek(streamPos);
            DataBufferByte buffer = new DataBufferByte(getLength());
            readByteBuffer(iis,
                           buffer.getData(),
                           reader,
                           1.0F,
                           0.0F);
            iis.reset();

            WritableRaster raster =
                Raster.createInterleavedRaster(buffer,
                                               thumbWidth,
                                               thumbHeight,
                                               thumbWidth*3,
                                               3,
                                               new int [] {0, 1, 2},
                                               null);
            ColorModel cm = new ComponentColorModel(JPEG.JCS.sRGB,
                                                    false,
                                                    false,
                                                    ColorModel.OPAQUE,
                                                    DataBuffer.TYPE_BYTE);
            return new BufferedImage(cm,
                                     raster,
                                     false,
                                     null);
        }

        void write(ImageOutputStream ios,
                   JPEGImageWriter writer) throws IOException {
            super.write(ios, writer); // width and height
            writePixels(ios, writer);
        }

    }

    /**
     * A JFIF thumbnail stored as an indexed palette image
     * using an RGB palette.
     */
    class JFIFThumbPalette extends JFIFThumbUncompressed {
        private static final int PALETTE_SIZE = 768;

        JFIFThumbPalette(JPEGBuffer buffer, int width, int height)
            throws IOException {
            super(buffer,
                  width,
                  height,
                  PALETTE_SIZE + width * height,
                  "JFIFThumbPalette");
        }

        JFIFThumbPalette(Node node) throws IIOInvalidTreeException {
            super(node, "JFIFThumbPalette");
        }

        JFIFThumbPalette(BufferedImage thumb) throws IllegalThumbException {
            super(thumb);
            IndexColorModel icm = (IndexColorModel) thumbnail.getColorModel();
            if (icm.getMapSize() > 256) {
                throw new IllegalThumbException();
            }
        }

        int getLength() {
            return (thumbWidth*thumbHeight + PALETTE_SIZE);
        }

        BufferedImage getThumbnail(ImageInputStream iis,
                                   JPEGImageReader reader)
            throws IOException {
            iis.mark();
            iis.seek(streamPos);
            // read the palette
            byte [] palette = new byte [PALETTE_SIZE];
            float palettePart = ((float) PALETTE_SIZE) / getLength();
            readByteBuffer(iis,
                           palette,
                           reader,
                           palettePart,
                           0.0F);
            DataBufferByte buffer = new DataBufferByte(thumbWidth*thumbHeight);
            readByteBuffer(iis,
                           buffer.getData(),
                           reader,
                           1.0F-palettePart,
                           palettePart);
            iis.read();
            iis.reset();

            IndexColorModel cm = new IndexColorModel(8,
                                                     256,
                                                     palette,
                                                     0,
                                                     false);
            SampleModel sm = cm.createCompatibleSampleModel(thumbWidth,
                                                            thumbHeight);
            WritableRaster raster =
                Raster.createWritableRaster(sm, buffer, null);
            return new BufferedImage(cm,
                                     raster,
                                     false,
                                     null);
        }

        void write(ImageOutputStream ios,
                   JPEGImageWriter writer) throws IOException {
            super.write(ios, writer); // width and height
            // Write the palette (must be 768 bytes)
            byte [] palette = new byte[768];
            IndexColorModel icm = (IndexColorModel) thumbnail.getColorModel();
            byte [] reds = new byte [256];
            byte [] greens = new byte [256];
            byte [] blues = new byte [256];
            icm.getReds(reds);
            icm.getGreens(greens);
            icm.getBlues(blues);
            for (int i = 0; i < 256; i++) {
                palette[i*3] = reds[i];
                palette[i*3+1] = greens[i];
                palette[i*3+2] = blues[i];
            }
            ios.write(palette);
            writePixels(ios, writer);
        }
    }


    /**
     * A JFIF thumbnail stored as a JPEG stream.  No JFIF or
     * JFIF extension markers are permitted.  There is no need
     * to clip these, but the entire image must fit into a
     * single JFXX marker segment.
     */
    class JFIFThumbJPEG extends JFIFThumb {
        JPEGMetadata thumbMetadata = null;
        byte [] data = null;  // Compressed image data, for writing
        private static final int PREAMBLE_SIZE = 6;

        JFIFThumbJPEG(JPEGBuffer buffer,
                      int length,
                      JPEGImageReader reader) throws IOException {
            super(buffer);
            // Compute the final stream position
            long finalPos = streamPos + (length - PREAMBLE_SIZE);
            // Set the stream back to the start of the thumbnail
            // and read its metadata (but don't decode the image)
            buffer.iis.seek(streamPos);
            thumbMetadata = new JPEGMetadata(false, true, buffer.iis, reader);
            // Set the stream to the computed final position
            buffer.iis.seek(finalPos);
            // Clear the now invalid buffer
            buffer.bufAvail = 0;
            buffer.bufPtr = 0;
        }

        JFIFThumbJPEG(Node node) throws IIOInvalidTreeException {
            if (node.getChildNodes().getLength() > 1) {
                throw new IIOInvalidTreeException
                    ("JFIFThumbJPEG node must have 0 or 1 child", node);
            }
            Node child = node.getFirstChild();
            if (child != null) {
                String name = child.getNodeName();
                if (!name.equals("markerSequence")) {
                    throw new IIOInvalidTreeException
                        ("JFIFThumbJPEG child must be a markerSequence node",
                         node);
                }
                thumbMetadata = new JPEGMetadata(false, true);
                thumbMetadata.setFromMarkerSequenceNode(child);
            }
        }

        JFIFThumbJPEG(BufferedImage thumb) throws IllegalThumbException {
            int INITIAL_BUFSIZE = 4096;
            int MAZ_BUFSIZE = 65535 - 2 - PREAMBLE_SIZE;
            try {
                ByteArrayOutputStream baos =
                    new ByteArrayOutputStream(INITIAL_BUFSIZE);
                MemoryCacheImageOutputStream mos =
                    new MemoryCacheImageOutputStream(baos);

                JPEGImageWriter thumbWriter = new JPEGImageWriter(null);

                thumbWriter.setOutput(mos);

                // get default metadata for the thumb
                JPEGMetadata metadata =
                    (JPEGMetadata) thumbWriter.getDefaultImageMetadata
                    (new ImageTypeSpecifier(thumb), null);

                // Remove the jfif segment, which should be there.
                MarkerSegment jfif = metadata.findMarkerSegment
                    (JFIFMarkerSegment.class, true);
                if (jfif == null) {
                    throw new IllegalThumbException();
                }

                metadata.markerSequence.remove(jfif);

                /*  Use this if removing leaves a hole and causes trouble

                // Get the tree
                String format = metadata.getNativeMetadataFormatName();
                IIOMetadataNode tree =
                (IIOMetadataNode) metadata.getAsTree(format);

                // If there is no app0jfif node, the image is bad
                NodeList jfifs = tree.getElementsByTagName("app0JFIF");
                if (jfifs.getLength() == 0) {
                throw new IllegalThumbException();
                }

                // remove the app0jfif node
                Node jfif = jfifs.item(0);
                Node parent = jfif.getParentNode();
                parent.removeChild(jfif);

                metadata.setFromTree(format, tree);
                */

                thumbWriter.write(new IIOImage(thumb, null, metadata));

                thumbWriter.dispose();
                // Now check that the size is OK
                if (baos.size() > MAZ_BUFSIZE) {
                    throw new IllegalThumbException();
                }
                data = baos.toByteArray();
            } catch (IOException e) {
                throw new IllegalThumbException();
            }
        }

        int getWidth() {
            int retval = 0;
            SOFMarkerSegment sof =
                (SOFMarkerSegment) thumbMetadata.findMarkerSegment
                (SOFMarkerSegment.class, true);
            if (sof != null) {
                retval = sof.samplesPerLine;
            }
            return retval;
        }

        int getHeight() {
            int retval = 0;
            SOFMarkerSegment sof =
                (SOFMarkerSegment) thumbMetadata.findMarkerSegment
                (SOFMarkerSegment.class, true);
            if (sof != null) {
                retval = sof.numLines;
            }
            return retval;
        }

        private class ThumbnailReadListener
            implements IIOReadProgressListener {
            JPEGImageReader reader = null;
            ThumbnailReadListener (JPEGImageReader reader) {
                this.reader = reader;
            }
            public void sequenceStarted(ImageReader source, int minIndex) {}
            public void sequenceComplete(ImageReader source) {}
            public void imageStarted(ImageReader source, int imageIndex) {}
            public void imageProgress(ImageReader source,
                                      float percentageDone) {
                reader.thumbnailProgress(percentageDone);
            }
            public void imageComplete(ImageReader source) {}
            public void thumbnailStarted(ImageReader source,
                int imageIndex, int thumbnailIndex) {}
            public void thumbnailProgress(ImageReader source, float percentageDone) {}
            public void thumbnailComplete(ImageReader source) {}
            public void readAborted(ImageReader source) {}
        }

        BufferedImage getThumbnail(ImageInputStream iis,
                                   JPEGImageReader reader)
            throws IOException {
            iis.mark();
            iis.seek(streamPos);
            JPEGImageReader thumbReader = new JPEGImageReader(null);
            thumbReader.setInput(iis);
            thumbReader.addIIOReadProgressListener
                (new ThumbnailReadListener(reader));
            BufferedImage ret = thumbReader.read(0, null);
            thumbReader.dispose();
            iis.reset();
            return ret;
        }

        protected Object clone() {
            JFIFThumbJPEG newGuy = (JFIFThumbJPEG) super.clone();
            if (thumbMetadata != null) {
                newGuy.thumbMetadata = (JPEGMetadata) thumbMetadata.clone();
            }
            return newGuy;
        }

        IIOMetadataNode getNativeNode() {
            IIOMetadataNode node = new IIOMetadataNode("JFIFthumbJPEG");
            if (thumbMetadata != null) {
                node.appendChild(thumbMetadata.getNativeTree());
            }
            return node;
        }

        int getLength() {
            if (data == null) {
                return 0;
            } else {
                return data.length;
            }
        }

        void write(ImageOutputStream ios,
                   JPEGImageWriter writer) throws IOException {
            int progInterval = data.length / 20;  // approx. every 5%
            if (progInterval == 0) {
                progInterval = 1;
            }
            for (int offset = 0;
                 offset < data.length;) {
                int len = Math.min(progInterval, data.length-offset);
                ios.write(data, offset, len);
                offset += progInterval;
                float percentDone = ((float) offset * 100) / data.length;
                if (percentDone > 100.0F) {
                    percentDone = 100.0F;
                }
                writer.thumbnailProgress (percentDone);
            }
        }

        void print () {
            System.out.println("JFIF thumbnail stored as JPEG");
        }
    }

    /**
     * Write out the given profile to the stream, embedded in
     * the necessary number of APP2 segments, per the ICC spec.
     * This is the only mechanism for writing an ICC profile
     * to a stream.
     */
    static void writeICC(ICC_Profile profile, ImageOutputStream ios)
        throws IOException {
        int LENGTH_LENGTH = 2;
        final String ID = "ICC_PROFILE";
        int ID_LENGTH = ID.length()+1; // spec says it's null-terminated
        int COUNTS_LENGTH = 2;
        int MAX_ICC_CHUNK_SIZE =
            65535 - LENGTH_LENGTH - ID_LENGTH - COUNTS_LENGTH;

        byte [] data = profile.getData();
        int numChunks = data.length / MAX_ICC_CHUNK_SIZE;
        if ((data.length % MAX_ICC_CHUNK_SIZE) != 0) {
            numChunks++;
        }
        int chunkNum = 1;
        int offset = 0;
        for (int i = 0; i < numChunks; i++) {
            int dataLength = Math.min(data.length-offset, MAX_ICC_CHUNK_SIZE);
            int segLength = dataLength+COUNTS_LENGTH+ID_LENGTH+LENGTH_LENGTH;
            ios.write(0xff);
            ios.write(JPEG.APP2);
            MarkerSegment.write2bytes(ios, segLength);
            byte [] id = ID.getBytes("US-ASCII");
            ios.write(id);
            ios.write(0); // Null-terminate the string
            ios.write(chunkNum++);
            ios.write(numChunks);
            ios.write(data, offset, dataLength);
            offset += dataLength;
        }
    }

    /**
     * An APP2 marker segment containing an ICC profile.  In the stream
     * a profile larger than 64K is broken up into a series of chunks.
     * This inner class represents the complete profile as a single objec,
     * combining chunks as necessary.
     */
    class ICCMarkerSegment extends MarkerSegment {
        ArrayList chunks = null;
        byte [] profile = null; // The complete profile when it's fully read
                         // May remain null when writing
        private static final int ID_SIZE = 12;
        int chunksRead;
        int numChunks;

        ICCMarkerSegment(ICC_ColorSpace cs) {
            super(JPEG.APP2);
            chunks = null;
            chunksRead = 0;
            numChunks = 0;
            profile = cs.getProfile().getData();
        }

        ICCMarkerSegment(JPEGBuffer buffer) throws IOException {
            super(buffer);  // gets whole segment or fills the buffer
            if (debug) {
                System.out.println("Creating new ICC segment");
            }
            buffer.bufPtr += ID_SIZE; // Skip the id
            buffer.bufAvail -= ID_SIZE;
            /*
             * Reduce the stored length by the id size.  The stored
             * length is used to store the length of the profile
             * data only.
             */
            length -= ID_SIZE;

            // get the chunk number
            int chunkNum = buffer.buf[buffer.bufPtr] & 0xff;
            // get the total number of chunks
            numChunks = buffer.buf[buffer.bufPtr+1] & 0xff;

            if (chunkNum > numChunks) {
                throw new IIOException
                    ("Image format Error; chunk num > num chunks");
            }

            // if there are no more chunks, set up the data
            if (numChunks == 1) {
                // reduce the stored length by the two chunk numbering bytes
                length -= 2;
                profile = new byte[length];
                buffer.bufPtr += 2;
                buffer.bufAvail-=2;
                buffer.readData(profile);
                inICC = false;
            } else {
                // If we store them away, include the chunk numbering bytes
                byte [] profileData = new byte[length];
                // Now reduce the stored length by the
                // two chunk numbering bytes
                length -= 2;
                buffer.readData(profileData);
                chunks = new ArrayList();
                chunks.add(profileData);
                chunksRead = 1;
                inICC = true;
            }
        }

        ICCMarkerSegment(Node node) throws IIOInvalidTreeException {
            super(JPEG.APP2);
            if (node instanceof IIOMetadataNode) {
                IIOMetadataNode ourNode = (IIOMetadataNode) node;
                ICC_Profile prof = (ICC_Profile) ourNode.getUserObject();
                if (prof != null) {  // May be null
                    profile = prof.getData();
                }
            }
        }

        protected Object clone () {
            ICCMarkerSegment newGuy = (ICCMarkerSegment) super.clone();
            if (profile != null) {
                newGuy.profile = (byte[]) profile.clone();
            }
            return newGuy;
        }

        boolean addData(JPEGBuffer buffer) throws IOException {
            if (debug) {
                System.out.println("Adding to ICC segment");
            }
            // skip the tag
            buffer.bufPtr++;
            buffer.bufAvail--;
            // Get the length, but not in length
            int dataLen = (buffer.buf[buffer.bufPtr++] & 0xff) << 8;
            dataLen |= buffer.buf[buffer.bufPtr++] & 0xff;
            buffer.bufAvail -= 2;
            // Don't include length itself
            dataLen -= 2;
            // skip the id
            buffer.bufPtr += ID_SIZE; // Skip the id
            buffer.bufAvail -= ID_SIZE;
            /*
             * Reduce the stored length by the id size.  The stored
             * length is used to store the length of the profile
             * data only.
             */
            dataLen -= ID_SIZE;

            // get the chunk number
            int chunkNum = buffer.buf[buffer.bufPtr] & 0xff;
            if (chunkNum > numChunks) {
                throw new IIOException
                    ("Image format Error; chunk num > num chunks");
            }

            // get the number of chunks, which should match
            int newNumChunks = buffer.buf[buffer.bufPtr+1] & 0xff;
            if (numChunks != newNumChunks) {
                throw new IIOException
                    ("Image format Error; icc num chunks mismatch");
            }
            dataLen -= 2;
            if (debug) {
                System.out.println("chunkNum: " + chunkNum
                                   + ", numChunks: " + numChunks
                                   + ", dataLen: " + dataLen);
            }
            boolean retval = false;
            byte [] profileData = new byte[dataLen];
            buffer.readData(profileData);
            chunks.add(profileData);
            length += dataLen;
            chunksRead++;
            if (chunksRead < numChunks) {
                inICC = true;
            } else {
                if (debug) {
                    System.out.println("Completing profile; total length is "
                                       + length);
                }
                // create an array for the whole thing
                profile = new byte[length];
                // copy the existing chunks, releasing them
                // Note that they may be out of order

                int index = 0;
                for (int i = 1; i <= numChunks; i++) {
                    boolean foundIt = false;
                    for (int chunk = 0; chunk < chunks.size(); chunk++) {
                        byte [] chunkData = (byte []) chunks.get(chunk);
                        if (chunkData[0] == i) { // Right one
                            System.arraycopy(chunkData, 2,
                                             profile, index,
                                             chunkData.length-2);
                            index += chunkData.length-2;
                            foundIt = true;
                        }
                    }
                    if (foundIt == false) {
                        throw new IIOException
                            ("Image Format Error: Missing ICC chunk num " + i);
                    }
                }

                chunks = null;
                chunksRead = 0;
                numChunks = 0;
                inICC = false;
                retval = true;
            }
            return retval;
        }

        IIOMetadataNode getNativeNode() {
            IIOMetadataNode node = new IIOMetadataNode("app2ICC");
            if (profile != null) {
                node.setUserObject(ICC_Profile.getInstance(profile));
            }
            return node;
        }

        /**
         * No-op.  Profiles are never written from metadata.
         * They are written from the ColorSpace of the image.
         */
        void write(ImageOutputStream ios) throws IOException {
            // No-op
        }

        void print () {
            printTag("ICC Profile APP2");
        }
    }
}
