alvinalexander.com | career | drupal | java | mac | mysql | perl | scala | uml | unix  

Java example source code file (JPEGMetadata.java)

This example Java source code file (JPEGMetadata.java) is included in the alvinalexander.com "Java Source Code Warehouse" project. The intent of this project is to help you "Learn Java by Example" TM.

Learn more about this Java project at its project page.

Java - Java tags/keywords

adobemarkersegment, arraylist, awt, color, dhtmarkersegment, dom, dqtmarkersegment, iioexception, iioinvalidtreeexception, iiometadatanode, image, imageio, iterator, jfifmarkersegment, list, markersegment, sofmarkersegment, sosmarkersegment, string, util

The JPEGMetadata.java Java example source code

/*
 * Copyright (c) 2001, 2013, 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.ImageTypeSpecifier;
import javax.imageio.ImageWriteParam;
import javax.imageio.IIOException;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.metadata.IIOMetadataFormat;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.metadata.IIOInvalidTreeException;
import javax.imageio.plugins.jpeg.JPEGQTable;
import javax.imageio.plugins.jpeg.JPEGHuffmanTable;
import javax.imageio.plugins.jpeg.JPEGImageWriteParam;

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

import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.ListIterator;
import java.io.IOException;
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.Point;

/**
 * Metadata for the JPEG plug-in.
 */
public class JPEGMetadata extends IIOMetadata implements Cloneable {

    //////// Private variables

    private static final boolean debug = false;

    /**
     * A copy of <code>markerSequence, created the first time the
     * <code>markerSequence is modified.  This is used by reset
     * to restore the original state.
     */
    private List resetSequence = null;

    /**
     * Set to <code>true when reading a thumbnail stored as
     * JPEG.  This is used to enforce the prohibition of JFIF thumbnails
     * containing any JFIF marker segments, and to ensure generation of
     * a correct native subtree during <code>getAsTree.
     */
    private boolean inThumb = false;

    /**
     * Set by the chroma node construction method to signal the
     * presence or absence of an alpha channel to the transparency
     * node construction method.  Used only when constructing a
     * standard metadata tree.
     */
    private boolean hasAlpha;

    //////// end of private variables

    /////// Package-access variables

    /**
     * All data is a list of <code>MarkerSegment objects.
     * When accessing the list, use the tag to identify the particular
     * subclass.  Any JFIF marker segment must be the first element
     * of the list if it is present, and any JFXX or APP2ICC marker
     * segments are subordinate to the JFIF marker segment.  This
     * list is package visible so that the writer can access it.
     * @see #MarkerSegment
     */
    List markerSequence = new ArrayList();

    /**
     * Indicates whether this object represents stream or image
     * metadata.  Package-visible so the writer can see it.
     */
    final boolean isStream;

    /////// End of package-access variables

    /////// Constructors

    /**
     * Constructor containing code shared by other constructors.
     */
    JPEGMetadata(boolean isStream, boolean inThumb) {
        super(true,  // Supports standard format
              JPEG.nativeImageMetadataFormatName,  // and a native format
              JPEG.nativeImageMetadataFormatClassName,
              null, null);  // No other formats
        this.inThumb = inThumb;
        // But if we are stream metadata, adjust the variables
        this.isStream = isStream;
        if (isStream) {
            nativeMetadataFormatName = JPEG.nativeStreamMetadataFormatName;
            nativeMetadataFormatClassName =
                JPEG.nativeStreamMetadataFormatClassName;
        }
    }

    /*
     * Constructs a <code>JPEGMetadata object by reading the
     * contents of an <code>ImageInputStream.  Has package-only
     * access.
     *
     * @param isStream A boolean indicating whether this object will be
     * stream or image metadata.
     * @param isThumb A boolean indicating whether this metadata object
     * is for an image or for a thumbnail stored as JPEG.
     * @param iis An <code>ImageInputStream from which to read
     * the metadata.
     * @param reader The <code>JPEGImageReader calling this
     * constructor, to which warnings should be sent.
     */
    JPEGMetadata(boolean isStream,
                 boolean isThumb,
                 ImageInputStream iis,
                 JPEGImageReader reader) throws IOException {
        this(isStream, isThumb);

        JPEGBuffer buffer = new JPEGBuffer(iis);

        buffer.loadBuf(0);

        // The first three bytes should be FF, SOI, FF
        if (((buffer.buf[0] & 0xff) != 0xff)
            || ((buffer.buf[1] & 0xff) != JPEG.SOI)
            || ((buffer.buf[2] & 0xff) != 0xff)) {
            throw new IIOException ("Image format error");
        }

        boolean done = false;
        buffer.bufAvail -=2;  // Next byte should be the ff before a marker
        buffer.bufPtr = 2;
        MarkerSegment newGuy = null;
        while (!done) {
            byte [] buf;
            int ptr;
            buffer.loadBuf(1);
            if (debug) {
                System.out.println("top of loop");
                buffer.print(10);
            }
            buffer.scanForFF(reader);
            switch (buffer.buf[buffer.bufPtr] & 0xff) {
            case 0:
                if (debug) {
                    System.out.println("Skipping 0");
                }
                buffer.bufAvail--;
                buffer.bufPtr++;
                break;
            case JPEG.SOF0:
            case JPEG.SOF1:
            case JPEG.SOF2:
                if (isStream) {
                    throw new IIOException
                        ("SOF not permitted in stream metadata");
                }
                newGuy = new SOFMarkerSegment(buffer);
                break;
            case JPEG.DQT:
                newGuy = new DQTMarkerSegment(buffer);
                break;
            case JPEG.DHT:
                newGuy = new DHTMarkerSegment(buffer);
                break;
            case JPEG.DRI:
                newGuy = new DRIMarkerSegment(buffer);
                break;
            case JPEG.APP0:
                // Either JFIF, JFXX, or unknown APP0
                buffer.loadBuf(8); // tag, length, id
                buf = buffer.buf;
                ptr = buffer.bufPtr;
                if ((buf[ptr+3] == 'J')
                    && (buf[ptr+4] == 'F')
                    && (buf[ptr+5] == 'I')
                    && (buf[ptr+6] == 'F')
                    && (buf[ptr+7] == 0)) {
                    if (inThumb) {
                        reader.warningOccurred
                            (JPEGImageReader.WARNING_NO_JFIF_IN_THUMB);
                        // Leave newGuy null
                        // Read a dummy to skip the segment
                        JFIFMarkerSegment dummy =
                            new JFIFMarkerSegment(buffer);
                    } else if (isStream) {
                        throw new IIOException
                            ("JFIF not permitted in stream metadata");
                    } else if (markerSequence.isEmpty() == false) {
                        throw new IIOException
                            ("JFIF APP0 must be first marker after SOI");
                    } else {
                        newGuy = new JFIFMarkerSegment(buffer);
                    }
                } else if ((buf[ptr+3] == 'J')
                           && (buf[ptr+4] == 'F')
                           && (buf[ptr+5] == 'X')
                           && (buf[ptr+6] == 'X')
                           && (buf[ptr+7] == 0)) {
                    if (isStream) {
                        throw new IIOException
                            ("JFXX not permitted in stream metadata");
                    }
                    if (inThumb) {
                        throw new IIOException
                          ("JFXX markers not allowed in JFIF JPEG thumbnail");
                    }
                    JFIFMarkerSegment jfif =
                        (JFIFMarkerSegment) findMarkerSegment
                               (JFIFMarkerSegment.class, true);
                    if (jfif == null) {
                        throw new IIOException
                            ("JFXX encountered without prior JFIF!");
                    }
                    jfif.addJFXX(buffer, reader);
                    // newGuy remains null
                } else {
                    newGuy = new MarkerSegment(buffer);
                    newGuy.loadData(buffer);
                }
                break;
            case JPEG.APP2:
                // Either an ICC profile or unknown APP2
                buffer.loadBuf(15); // tag, length, id
                if ((buffer.buf[buffer.bufPtr+3] == 'I')
                    && (buffer.buf[buffer.bufPtr+4] == 'C')
                    && (buffer.buf[buffer.bufPtr+5] == 'C')
                    && (buffer.buf[buffer.bufPtr+6] == '_')
                    && (buffer.buf[buffer.bufPtr+7] == 'P')
                    && (buffer.buf[buffer.bufPtr+8] == 'R')
                    && (buffer.buf[buffer.bufPtr+9] == 'O')
                    && (buffer.buf[buffer.bufPtr+10] == 'F')
                    && (buffer.buf[buffer.bufPtr+11] == 'I')
                    && (buffer.buf[buffer.bufPtr+12] == 'L')
                    && (buffer.buf[buffer.bufPtr+13] == 'E')
                    && (buffer.buf[buffer.bufPtr+14] == 0)
                    ) {
                    if (isStream) {
                        throw new IIOException
                            ("ICC profiles not permitted in stream metadata");
                    }

                    JFIFMarkerSegment jfif =
                        (JFIFMarkerSegment) findMarkerSegment
                        (JFIFMarkerSegment.class, true);
                    if (jfif == null) {
                        newGuy = new MarkerSegment(buffer);
                        newGuy.loadData(buffer);
                    } else {
                        jfif.addICC(buffer);
                    }
                    // newGuy remains null
                } else {
                    newGuy = new MarkerSegment(buffer);
                    newGuy.loadData(buffer);
                }
                break;
            case JPEG.APP14:
                // Either Adobe or unknown APP14
                buffer.loadBuf(8); // tag, length, id
                if ((buffer.buf[buffer.bufPtr+3] == 'A')
                    && (buffer.buf[buffer.bufPtr+4] == 'd')
                    && (buffer.buf[buffer.bufPtr+5] == 'o')
                    && (buffer.buf[buffer.bufPtr+6] == 'b')
                    && (buffer.buf[buffer.bufPtr+7] == 'e')) {
                    if (isStream) {
                        throw new IIOException
                      ("Adobe APP14 markers not permitted in stream metadata");
                    }
                    newGuy = new AdobeMarkerSegment(buffer);
                } else {
                    newGuy = new MarkerSegment(buffer);
                    newGuy.loadData(buffer);
                }

                break;
            case JPEG.COM:
                newGuy = new COMMarkerSegment(buffer);
                break;
            case JPEG.SOS:
                if (isStream) {
                    throw new IIOException
                        ("SOS not permitted in stream metadata");
                }
                newGuy = new SOSMarkerSegment(buffer);
                break;
            case JPEG.RST0:
            case JPEG.RST1:
            case JPEG.RST2:
            case JPEG.RST3:
            case JPEG.RST4:
            case JPEG.RST5:
            case JPEG.RST6:
            case JPEG.RST7:
                if (debug) {
                    System.out.println("Restart Marker");
                }
                buffer.bufPtr++; // Just skip it
                buffer.bufAvail--;
                break;
            case JPEG.EOI:
                done = true;
                buffer.bufPtr++;
                buffer.bufAvail--;
                break;
            default:
                newGuy = new MarkerSegment(buffer);
                newGuy.loadData(buffer);
                newGuy.unknown = true;
                break;
            }
            if (newGuy != null) {
                markerSequence.add(newGuy);
                if (debug) {
                    newGuy.print();
                }
                newGuy = null;
            }
        }

        // Now that we've read up to the EOI, we need to push back
        // whatever is left in the buffer, so that the next read
        // in the native code will work.

        buffer.pushBack();

        if (!isConsistent()) {
            throw new IIOException("Inconsistent metadata read from stream");
        }
    }

    /**
     * Constructs a default stream <code>JPEGMetadata object appropriate
     * for the given write parameters.
     */
    JPEGMetadata(ImageWriteParam param, JPEGImageWriter writer) {
        this(true, false);

        JPEGImageWriteParam jparam = null;

        if ((param != null) && (param instanceof JPEGImageWriteParam)) {
            jparam = (JPEGImageWriteParam) param;
            if (!jparam.areTablesSet()) {
                jparam = null;
            }
        }
        if (jparam != null) {
            markerSequence.add(new DQTMarkerSegment(jparam.getQTables()));
            markerSequence.add
                (new DHTMarkerSegment(jparam.getDCHuffmanTables(),
                                      jparam.getACHuffmanTables()));
        } else {
            // default tables.
            markerSequence.add(new DQTMarkerSegment(JPEG.getDefaultQTables()));
            markerSequence.add(new DHTMarkerSegment(JPEG.getDefaultHuffmanTables(true),
                                                    JPEG.getDefaultHuffmanTables(false)));
        }

        // Defensive programming
        if (!isConsistent()) {
            throw new InternalError("Default stream metadata is inconsistent");
        }
    }

    /**
     * Constructs a default image <code>JPEGMetadata object appropriate
     * for the given image type and write parameters.
     */
    JPEGMetadata(ImageTypeSpecifier imageType,
                 ImageWriteParam param,
                 JPEGImageWriter writer) {
        this(false, false);

        boolean wantJFIF = true;
        boolean wantAdobe = false;
        int transform = JPEG.ADOBE_UNKNOWN;
        boolean willSubsample = true;
        boolean wantICC = false;
        boolean wantProg = false;
        boolean wantOptimized = false;
        boolean wantExtended = false;
        boolean wantQTables = true;
        boolean wantHTables = true;
        float quality = JPEG.DEFAULT_QUALITY;
        byte[] componentIDs = { 1, 2, 3, 4};
        int numComponents = 0;

        ImageTypeSpecifier destType = null;

        if (param != null) {
            destType = param.getDestinationType();
            if (destType != null) {
                if (imageType != null) {
                    // Ignore the destination type.
                    writer.warningOccurred
                        (JPEGImageWriter.WARNING_DEST_IGNORED);
                    destType = null;
                }
            }
            // The only progressive mode that makes sense here is MODE_DEFAULT
            if (param.canWriteProgressive()) {
                // the param may not be one of ours, so it may return false.
                // If so, the following would throw an exception
                if (param.getProgressiveMode() == ImageWriteParam.MODE_DEFAULT) {
                    wantProg = true;
                    wantOptimized = true;
                    wantHTables = false;
                }
            }

            if (param instanceof JPEGImageWriteParam) {
                JPEGImageWriteParam jparam = (JPEGImageWriteParam) param;
                if (jparam.areTablesSet()) {
                    wantQTables = false;  // If the param has them, metadata shouldn't
                    wantHTables = false;
                    if ((jparam.getDCHuffmanTables().length > 2)
                            || (jparam.getACHuffmanTables().length > 2)) {
                        wantExtended = true;
                    }
                }
                // Progressive forces optimized, regardless of param setting
                // so consult the param re optimized only if not progressive
                if (!wantProg) {
                    wantOptimized = jparam.getOptimizeHuffmanTables();
                    if (wantOptimized) {
                        wantHTables = false;
                    }
                }
            }

            // compression quality should determine the q tables.  Note that this
            // will be ignored if we already decided not to create any.
            // Again, the param may not be one of ours, so we must check that it
            // supports compression settings
            if (param.canWriteCompressed()) {
                if (param.getCompressionMode() == ImageWriteParam.MODE_EXPLICIT) {
                    quality = param.getCompressionQuality();
                }
            }
        }

        // We are done with the param, now for the image types

        ColorSpace cs = null;
        if (destType != null) {
            ColorModel cm = destType.getColorModel();
            numComponents = cm.getNumComponents();
            boolean hasExtraComponents = (cm.getNumColorComponents() != numComponents);
            boolean hasAlpha = cm.hasAlpha();
            cs = cm.getColorSpace();
            int type = cs.getType();
            switch(type) {
            case ColorSpace.TYPE_GRAY:
                willSubsample = false;
                if (hasExtraComponents) {  // e.g. alpha
                    wantJFIF = false;
                }
                break;
            case ColorSpace.TYPE_3CLR:
                if (cs == JPEG.JCS.getYCC()) {
                    wantJFIF = false;
                    componentIDs[0] = (byte) 'Y';
                    componentIDs[1] = (byte) 'C';
                    componentIDs[2] = (byte) 'c';
                    if (hasAlpha) {
                        componentIDs[3] = (byte) 'A';
                    }
                }
                break;
            case ColorSpace.TYPE_YCbCr:
                if (hasExtraComponents) { // e.g. K or alpha
                    wantJFIF = false;
                    if (!hasAlpha) { // Not alpha, so must be K
                        wantAdobe = true;
                        transform = JPEG.ADOBE_YCCK;
                    }
                }
                break;
            case ColorSpace.TYPE_RGB:  // with or without alpha
                wantJFIF = false;
                wantAdobe = true;
                willSubsample = false;
                componentIDs[0] = (byte) 'R';
                componentIDs[1] = (byte) 'G';
                componentIDs[2] = (byte) 'B';
                if (hasAlpha) {
                    componentIDs[3] = (byte) 'A';
                }
                break;
            default:
                // Everything else is not subsampled, gets no special marker,
                // and component ids are 1 - N
                wantJFIF = false;
                willSubsample = false;
            }
        } else if (imageType != null) {
            ColorModel cm = imageType.getColorModel();
            numComponents = cm.getNumComponents();
            boolean hasExtraComponents = (cm.getNumColorComponents() != numComponents);
            boolean hasAlpha = cm.hasAlpha();
            cs = cm.getColorSpace();
            int type = cs.getType();
            switch(type) {
            case ColorSpace.TYPE_GRAY:
                willSubsample = false;
                if (hasExtraComponents) {  // e.g. alpha
                    wantJFIF = false;
                }
                break;
            case ColorSpace.TYPE_RGB:  // with or without alpha
                // without alpha we just accept the JFIF defaults
                if (hasAlpha) {
                    wantJFIF = false;
                }
                break;
            case ColorSpace.TYPE_3CLR:
                wantJFIF = false;
                willSubsample = false;
                if (cs.equals(ColorSpace.getInstance(ColorSpace.CS_PYCC))) {
                    willSubsample = true;
                    wantAdobe = true;
                    componentIDs[0] = (byte) 'Y';
                    componentIDs[1] = (byte) 'C';
                    componentIDs[2] = (byte) 'c';
                    if (hasAlpha) {
                        componentIDs[3] = (byte) 'A';
                    }
                }
                break;
            case ColorSpace.TYPE_YCbCr:
                if (hasExtraComponents) { // e.g. K or alpha
                    wantJFIF = false;
                    if (!hasAlpha) {  // then it must be K
                        wantAdobe = true;
                        transform = JPEG.ADOBE_YCCK;
                    }
                }
                break;
            case ColorSpace.TYPE_CMYK:
                wantJFIF = false;
                wantAdobe = true;
                transform = JPEG.ADOBE_YCCK;
                break;

            default:
                // Everything else is not subsampled, gets no special marker,
                // and component ids are 0 - N
                wantJFIF = false;
                willSubsample = false;
            }

        }

        // do we want an ICC profile?
        if (wantJFIF && JPEG.isNonStandardICC(cs)) {
            wantICC = true;
        }

        // Now step through the markers, consulting our variables.
        if (wantJFIF) {
            JFIFMarkerSegment jfif = new JFIFMarkerSegment();
            markerSequence.add(jfif);
            if (wantICC) {
                try {
                    jfif.addICC((ICC_ColorSpace)cs);
                } catch (IOException e) {} // Can't happen here
            }
        }
        // Adobe
        if (wantAdobe) {
            markerSequence.add(new AdobeMarkerSegment(transform));
        }

        // dqt
        if (wantQTables) {
            markerSequence.add(new DQTMarkerSegment(quality, willSubsample));
        }

        // dht
        if (wantHTables) {
            markerSequence.add(new DHTMarkerSegment(willSubsample));
        }

        // sof
        markerSequence.add(new SOFMarkerSegment(wantProg,
                                                wantExtended,
                                                willSubsample,
                                                componentIDs,
                                                numComponents));

        // sos
        if (!wantProg) {  // Default progression scans are done in the writer
            markerSequence.add(new SOSMarkerSegment(willSubsample,
                                                    componentIDs,
                                                    numComponents));
        }

        // Defensive programming
        if (!isConsistent()) {
            throw new InternalError("Default image metadata is inconsistent");
        }
    }

    ////// End of constructors

    // Utilities for dealing with the marker sequence.
    // The first ones have package access for access from the writer.

    /**
     * Returns the first MarkerSegment object in the list
     * with the given tag, or null if none is found.
     */
    MarkerSegment findMarkerSegment(int tag) {
        Iterator iter = markerSequence.iterator();
        while (iter.hasNext()) {
            MarkerSegment seg = (MarkerSegment)iter.next();
            if (seg.tag == tag) {
                return seg;
            }
        }
        return null;
    }

    /**
     * Returns the first or last MarkerSegment object in the list
     * of the given class, or null if none is found.
     */
    MarkerSegment findMarkerSegment(Class cls, boolean first) {
        if (first) {
            Iterator iter = markerSequence.iterator();
            while (iter.hasNext()) {
                MarkerSegment seg = (MarkerSegment)iter.next();
                if (cls.isInstance(seg)) {
                    return seg;
                }
            }
        } else {
            ListIterator iter = markerSequence.listIterator(markerSequence.size());
            while (iter.hasPrevious()) {
                MarkerSegment seg = (MarkerSegment)iter.previous();
                if (cls.isInstance(seg)) {
                    return seg;
                }
            }
        }
        return null;
    }

    /**
     * Returns the index of the first or last MarkerSegment in the list
     * of the given class, or -1 if none is found.
     */
    private int findMarkerSegmentPosition(Class cls, boolean first) {
        if (first) {
            ListIterator iter = markerSequence.listIterator();
            for (int i = 0; iter.hasNext(); i++) {
                MarkerSegment seg = (MarkerSegment)iter.next();
                if (cls.isInstance(seg)) {
                    return i;
                }
            }
        } else {
            ListIterator iter = markerSequence.listIterator(markerSequence.size());
            for (int i = markerSequence.size()-1; iter.hasPrevious(); i--) {
                MarkerSegment seg = (MarkerSegment)iter.previous();
                if (cls.isInstance(seg)) {
                    return i;
                }
            }
        }
        return -1;
    }

    private int findLastUnknownMarkerSegmentPosition() {
        ListIterator iter = markerSequence.listIterator(markerSequence.size());
        for (int i = markerSequence.size()-1; iter.hasPrevious(); i--) {
            MarkerSegment seg = (MarkerSegment)iter.previous();
            if (seg.unknown == true) {
                return i;
            }
        }
        return -1;
    }

    // Implement Cloneable, but restrict access

    protected Object clone() {
        JPEGMetadata newGuy = null;
        try {
            newGuy = (JPEGMetadata) super.clone();
        } catch (CloneNotSupportedException e) {} // won't happen
        if (markerSequence != null) {
            newGuy.markerSequence = (List) cloneSequence();
        }
        newGuy.resetSequence = null;
        return newGuy;
    }

    /**
     * Returns a deep copy of the current marker sequence.
     */
    private List cloneSequence() {
        if (markerSequence == null) {
            return null;
        }
        List retval = new ArrayList(markerSequence.size());
        Iterator iter = markerSequence.iterator();
        while(iter.hasNext()) {
            MarkerSegment seg = (MarkerSegment)iter.next();
            retval.add(seg.clone());
        }

        return retval;
    }


    // Tree methods

    public Node getAsTree(String formatName) {
        if (formatName == null) {
            throw new IllegalArgumentException("null formatName!");
        }
        if (isStream) {
            if (formatName.equals(JPEG.nativeStreamMetadataFormatName)) {
                return getNativeTree();
            }
        } else {
            if (formatName.equals(JPEG.nativeImageMetadataFormatName)) {
                return getNativeTree();
            }
            if (formatName.equals
                    (IIOMetadataFormatImpl.standardMetadataFormatName)) {
                return getStandardTree();
            }
        }
        throw  new IllegalArgumentException("Unsupported format name: "
                                                + formatName);
    }

    IIOMetadataNode getNativeTree() {
        IIOMetadataNode root;
        IIOMetadataNode top;
        Iterator iter = markerSequence.iterator();
        if (isStream) {
            root = new IIOMetadataNode(JPEG.nativeStreamMetadataFormatName);
            top = root;
        } else {
            IIOMetadataNode sequence = new IIOMetadataNode("markerSequence");
            if (!inThumb) {
                root = new IIOMetadataNode(JPEG.nativeImageMetadataFormatName);
                IIOMetadataNode header = new IIOMetadataNode("JPEGvariety");
                root.appendChild(header);
                JFIFMarkerSegment jfif = (JFIFMarkerSegment)
                    findMarkerSegment(JFIFMarkerSegment.class, true);
                if (jfif != null) {
                    iter.next();  // JFIF must be first, so this skips it
                    header.appendChild(jfif.getNativeNode());
                }
                root.appendChild(sequence);
            } else {
                root = sequence;
            }
            top = sequence;
        }
        while(iter.hasNext()) {
            MarkerSegment seg = (MarkerSegment) iter.next();
            top.appendChild(seg.getNativeNode());
        }
        return root;
    }

    // Standard tree node methods

    protected IIOMetadataNode getStandardChromaNode() {
        hasAlpha = false;  // Unless we find otherwise

        // Colorspace type - follow the rules in the spec
        // First get the SOF marker segment, if there is one
        SOFMarkerSegment sof = (SOFMarkerSegment)
            findMarkerSegment(SOFMarkerSegment.class, true);
        if (sof == null) {
            // No image, so no chroma
            return null;
        }

        IIOMetadataNode chroma = new IIOMetadataNode("Chroma");
        IIOMetadataNode csType = new IIOMetadataNode("ColorSpaceType");
        chroma.appendChild(csType);

        // get the number of channels
        int numChannels = sof.componentSpecs.length;

        IIOMetadataNode numChanNode = new IIOMetadataNode("NumChannels");
        chroma.appendChild(numChanNode);
        numChanNode.setAttribute("value", Integer.toString(numChannels));

        // is there a JFIF marker segment?
        if (findMarkerSegment(JFIFMarkerSegment.class, true) != null) {
            if (numChannels == 1) {
                csType.setAttribute("name", "GRAY");
            } else {
                csType.setAttribute("name", "YCbCr");
            }
            return chroma;
        }

        // How about an Adobe marker segment?
        AdobeMarkerSegment adobe =
            (AdobeMarkerSegment) findMarkerSegment(AdobeMarkerSegment.class, true);
        if (adobe != null){
            switch (adobe.transform) {
            case JPEG.ADOBE_YCCK:
                csType.setAttribute("name", "YCCK");
                break;
            case JPEG.ADOBE_YCC:
                csType.setAttribute("name", "YCbCr");
                break;
            case JPEG.ADOBE_UNKNOWN:
                if (numChannels == 3) {
                    csType.setAttribute("name", "RGB");
                } else if (numChannels == 4) {
                    csType.setAttribute("name", "CMYK");
                }
                break;
            }
            return chroma;
        }

        // Neither marker.  Check components
        if (numChannels < 3) {
            csType.setAttribute("name", "GRAY");
            if (numChannels == 2) {
                hasAlpha = true;
            }
            return chroma;
        }

        boolean idsAreJFIF = true;

        for (int i = 0; i < sof.componentSpecs.length; i++) {
            int id = sof.componentSpecs[i].componentId;
            if ((id < 1) || (id >= sof.componentSpecs.length)) {
                idsAreJFIF = false;
            }
        }

        if (idsAreJFIF) {
            csType.setAttribute("name", "YCbCr");
            if (numChannels == 4) {
                hasAlpha = true;
            }
            return chroma;
        }

        // Check against the letters
        if ((sof.componentSpecs[0].componentId == 'R')
            && (sof.componentSpecs[1].componentId == 'G')
            && (sof.componentSpecs[2].componentId == 'B')){

            csType.setAttribute("name", "RGB");
            if ((numChannels == 4)
                && (sof.componentSpecs[3].componentId == 'A')) {
                hasAlpha = true;
            }
            return chroma;
        }

        if ((sof.componentSpecs[0].componentId == 'Y')
            && (sof.componentSpecs[1].componentId == 'C')
            && (sof.componentSpecs[2].componentId == 'c')){

            csType.setAttribute("name", "PhotoYCC");
            if ((numChannels == 4)
                && (sof.componentSpecs[3].componentId == 'A')) {
                hasAlpha = true;
            }
            return chroma;
        }

        // Finally, 3-channel subsampled are YCbCr, unsubsampled are RGB
        // 4-channel subsampled are YCbCrA, unsubsampled are CMYK

        boolean subsampled = false;

        int hfactor = sof.componentSpecs[0].HsamplingFactor;
        int vfactor = sof.componentSpecs[0].VsamplingFactor;

        for (int i = 1; i<sof.componentSpecs.length; i++) {
            if ((sof.componentSpecs[i].HsamplingFactor != hfactor)
                || (sof.componentSpecs[i].VsamplingFactor != vfactor)){
                subsampled = true;
                break;
            }
        }

        if (subsampled) {
            csType.setAttribute("name", "YCbCr");
            if (numChannels == 4) {
                hasAlpha = true;
            }
            return chroma;
        }

        // Not subsampled.  numChannels < 3 is taken care of above
        if (numChannels == 3) {
            csType.setAttribute("name", "RGB");
        } else {
            csType.setAttribute("name", "CMYK");
        }

        return chroma;
    }

    protected IIOMetadataNode getStandardCompressionNode() {

        IIOMetadataNode compression = new IIOMetadataNode("Compression");

        // CompressionTypeName
        IIOMetadataNode name = new IIOMetadataNode("CompressionTypeName");
        name.setAttribute("value", "JPEG");
        compression.appendChild(name);

        // Lossless - false
        IIOMetadataNode lossless = new IIOMetadataNode("Lossless");
        lossless.setAttribute("value", "FALSE");
        compression.appendChild(lossless);

        // NumProgressiveScans - count sos segments
        int sosCount = 0;
        Iterator iter = markerSequence.iterator();
        while (iter.hasNext()) {
            MarkerSegment ms = (MarkerSegment) iter.next();
            if (ms.tag == JPEG.SOS) {
                sosCount++;
            }
        }
        if (sosCount != 0) {
            IIOMetadataNode prog = new IIOMetadataNode("NumProgressiveScans");
            prog.setAttribute("value", Integer.toString(sosCount));
            compression.appendChild(prog);
        }

        return compression;
    }

    protected IIOMetadataNode getStandardDimensionNode() {
        // If we have a JFIF marker segment, we know a little
        // otherwise all we know is the orientation, which is always normal
        IIOMetadataNode dim = new IIOMetadataNode("Dimension");
        IIOMetadataNode orient = new IIOMetadataNode("ImageOrientation");
        orient.setAttribute("value", "normal");
        dim.appendChild(orient);

        JFIFMarkerSegment jfif =
            (JFIFMarkerSegment) findMarkerSegment(JFIFMarkerSegment.class, true);
        if (jfif != null) {

            // Aspect Ratio is width of pixel / height of pixel
            float aspectRatio;
            if (jfif.resUnits == 0) {
                // In this case they just encode aspect ratio directly
                aspectRatio = ((float) jfif.Xdensity)/jfif.Ydensity;
            } else {
                // They are true densities (e.g. dpi) and must be inverted
                aspectRatio = ((float) jfif.Ydensity)/jfif.Xdensity;
            }
            IIOMetadataNode aspect = new IIOMetadataNode("PixelAspectRatio");
            aspect.setAttribute("value", Float.toString(aspectRatio));
            dim.insertBefore(aspect, orient);

            // Pixel size
            if (jfif.resUnits != 0) {
                // 1 == dpi, 2 == dpc
                float scale = (jfif.resUnits == 1) ? 25.4F : 10.0F;

                IIOMetadataNode horiz =
                    new IIOMetadataNode("HorizontalPixelSize");
                horiz.setAttribute("value",
                                   Float.toString(scale/jfif.Xdensity));
                dim.appendChild(horiz);

                IIOMetadataNode vert =
                    new IIOMetadataNode("VerticalPixelSize");
                vert.setAttribute("value",
                                  Float.toString(scale/jfif.Ydensity));
                dim.appendChild(vert);
            }
        }
        return dim;
    }

    protected IIOMetadataNode getStandardTextNode() {
        IIOMetadataNode text = null;
        // Add a text entry for each COM Marker Segment
        if (findMarkerSegment(JPEG.COM) != null) {
            text = new IIOMetadataNode("Text");
            Iterator iter = markerSequence.iterator();
            while (iter.hasNext()) {
                MarkerSegment seg = (MarkerSegment) iter.next();
                if (seg.tag == JPEG.COM) {
                    COMMarkerSegment com = (COMMarkerSegment) seg;
                    IIOMetadataNode entry = new IIOMetadataNode("TextEntry");
                    entry.setAttribute("keyword", "comment");
                    entry.setAttribute("value", com.getComment());
                text.appendChild(entry);
                }
            }
        }
        return text;
    }

    protected IIOMetadataNode getStandardTransparencyNode() {
        IIOMetadataNode trans = null;
        if (hasAlpha == true) {
            trans = new IIOMetadataNode("Transparency");
            IIOMetadataNode alpha = new IIOMetadataNode("Alpha");
            alpha.setAttribute("value", "nonpremultiplied"); // Always assume
            trans.appendChild(alpha);
        }
        return trans;
    }

    // Editing

    public boolean isReadOnly() {
        return false;
    }

    public void mergeTree(String formatName, Node root)
        throws IIOInvalidTreeException {
        if (formatName == null) {
            throw new IllegalArgumentException("null formatName!");
        }
        if (root == null) {
            throw new IllegalArgumentException("null root!");
        }
        List copy = null;
        if (resetSequence == null) {
            resetSequence = cloneSequence();  // Deep copy
            copy = resetSequence;  // Avoid cloning twice
        } else {
            copy = cloneSequence();
        }
        if (isStream &&
            (formatName.equals(JPEG.nativeStreamMetadataFormatName))) {
                mergeNativeTree(root);
        } else if (!isStream &&
                   (formatName.equals(JPEG.nativeImageMetadataFormatName))) {
            mergeNativeTree(root);
        } else if (!isStream &&
                   (formatName.equals
                    (IIOMetadataFormatImpl.standardMetadataFormatName))) {
            mergeStandardTree(root);
        } else {
            throw  new IllegalArgumentException("Unsupported format name: "
                                                + formatName);
        }
        if (!isConsistent()) {
            markerSequence = copy;
            throw new IIOInvalidTreeException
                ("Merged tree is invalid; original restored", root);
        }
    }

    private void mergeNativeTree(Node root) throws IIOInvalidTreeException {
        String name = root.getNodeName();
        if (name != ((isStream) ? JPEG.nativeStreamMetadataFormatName
                                : JPEG.nativeImageMetadataFormatName)) {
            throw new IIOInvalidTreeException("Invalid root node name: " + name,
                                              root);
        }
        if (root.getChildNodes().getLength() != 2) { // JPEGvariety and markerSequence
            throw new IIOInvalidTreeException(
                "JPEGvariety and markerSequence nodes must be present", root);
        }
        mergeJFIFsubtree(root.getFirstChild());
        mergeSequenceSubtree(root.getLastChild());
    }

    /**
     * Merge a JFIF subtree into the marker sequence, if the subtree
     * is non-empty.
     * If a JFIF marker exists, update it from the subtree.
     * If none exists, create one from the subtree and insert it at the
     * beginning of the marker sequence.
     */
    private void mergeJFIFsubtree(Node JPEGvariety)
        throws IIOInvalidTreeException {
        if (JPEGvariety.getChildNodes().getLength() != 0) {
            Node jfifNode = JPEGvariety.getFirstChild();
            // is there already a jfif marker segment?
            JFIFMarkerSegment jfifSeg =
                (JFIFMarkerSegment) findMarkerSegment(JFIFMarkerSegment.class, true);
            if (jfifSeg != null) {
                jfifSeg.updateFromNativeNode(jfifNode, false);
            } else {
                // Add it as the first element in the list.
                markerSequence.add(0, new JFIFMarkerSegment(jfifNode));
            }
        }
    }

    private void mergeSequenceSubtree(Node sequenceTree)
        throws IIOInvalidTreeException {
        NodeList children = sequenceTree.getChildNodes();
        for (int i = 0; i < children.getLength(); i++) {
            Node node = children.item(i);
            String name = node.getNodeName();
            if (name.equals("dqt")) {
                mergeDQTNode(node);
            } else if (name.equals("dht")) {
                mergeDHTNode(node);
            } else if (name.equals("dri")) {
                mergeDRINode(node);
            } else if (name.equals("com")) {
                mergeCOMNode(node);
            } else if (name.equals("app14Adobe")) {
                mergeAdobeNode(node);
            } else if (name.equals("unknown")) {
                mergeUnknownNode(node);
            } else if (name.equals("sof")) {
                mergeSOFNode(node);
            } else if (name.equals("sos")) {
                mergeSOSNode(node);
            } else {
                throw new IIOInvalidTreeException("Invalid node: " + name, node);
            }
        }
    }

    /**
     * Merge the given DQT node into the marker sequence.  If there already
     * exist DQT marker segments in the sequence, then each table in the
     * node replaces the first table, in any DQT segment, with the same
     * table id.  If none of the existing DQT segments contain a table with
     * the same id, then the table is added to the last existing DQT segment.
     * If there are no DQT segments, then a new one is created and added
     * as follows:
     * If there are DHT segments, the new DQT segment is inserted before the
     * first one.
     * If there are no DHT segments, the new DQT segment is inserted before
     * an SOF segment, if there is one.
     * If there is no SOF segment, the new DQT segment is inserted before
     * the first SOS segment, if there is one.
     * If there is no SOS segment, the new DQT segment is added to the end
     * of the sequence.
     */
    private void mergeDQTNode(Node node) throws IIOInvalidTreeException {
        // First collect any existing DQT nodes into a local list
        ArrayList oldDQTs = new ArrayList();
        Iterator iter = markerSequence.iterator();
        while (iter.hasNext()) {
            MarkerSegment seg = (MarkerSegment) iter.next();
            if (seg instanceof DQTMarkerSegment) {
                oldDQTs.add(seg);
            }
        }
        if (!oldDQTs.isEmpty()) {
            NodeList children = node.getChildNodes();
            for (int i = 0; i < children.getLength(); i++) {
                Node child = children.item(i);
                int childID = MarkerSegment.getAttributeValue(child,
                                                              null,
                                                              "qtableId",
                                                              0, 3,
                                                              true);
                DQTMarkerSegment dqt = null;
                int tableIndex = -1;
                for (int j = 0; j < oldDQTs.size(); j++) {
                    DQTMarkerSegment testDQT = (DQTMarkerSegment) oldDQTs.get(j);
                    for (int k = 0; k < testDQT.tables.size(); k++) {
                        DQTMarkerSegment.Qtable testTable =
                            (DQTMarkerSegment.Qtable) testDQT.tables.get(k);
                        if (childID == testTable.tableID) {
                            dqt = testDQT;
                            tableIndex = k;
                            break;
                        }
                    }
                    if (dqt != null) break;
                }
                if (dqt != null) {
                    dqt.tables.set(tableIndex, dqt.getQtableFromNode(child));
                } else {
                    dqt = (DQTMarkerSegment) oldDQTs.get(oldDQTs.size()-1);
                    dqt.tables.add(dqt.getQtableFromNode(child));
                }
            }
        } else {
            DQTMarkerSegment newGuy = new DQTMarkerSegment(node);
            int firstDHT = findMarkerSegmentPosition(DHTMarkerSegment.class, true);
            int firstSOF = findMarkerSegmentPosition(SOFMarkerSegment.class, true);
            int firstSOS = findMarkerSegmentPosition(SOSMarkerSegment.class, true);
            if (firstDHT != -1) {
                markerSequence.add(firstDHT, newGuy);
            } else if (firstSOF != -1) {
                markerSequence.add(firstSOF, newGuy);
            } else if (firstSOS != -1) {
                markerSequence.add(firstSOS, newGuy);
            } else {
                markerSequence.add(newGuy);
            }
        }
    }

    /**
     * Merge the given DHT node into the marker sequence.  If there already
     * exist DHT marker segments in the sequence, then each table in the
     * node replaces the first table, in any DHT segment, with the same
     * table class and table id.  If none of the existing DHT segments contain
     * a table with the same class and id, then the table is added to the last
     * existing DHT segment.
     * If there are no DHT segments, then a new one is created and added
     * as follows:
     * If there are DQT segments, the new DHT segment is inserted immediately
     * following the last DQT segment.
     * If there are no DQT segments, the new DHT segment is inserted before
     * an SOF segment, if there is one.
     * If there is no SOF segment, the new DHT segment is inserted before
     * the first SOS segment, if there is one.
     * If there is no SOS segment, the new DHT segment is added to the end
     * of the sequence.
     */
    private void mergeDHTNode(Node node) throws IIOInvalidTreeException {
        // First collect any existing DQT nodes into a local list
        ArrayList oldDHTs = new ArrayList();
        Iterator iter = markerSequence.iterator();
        while (iter.hasNext()) {
            MarkerSegment seg = (MarkerSegment) iter.next();
            if (seg instanceof DHTMarkerSegment) {
                oldDHTs.add(seg);
            }
        }
        if (!oldDHTs.isEmpty()) {
            NodeList children = node.getChildNodes();
            for (int i = 0; i < children.getLength(); i++) {
                Node child = children.item(i);
                NamedNodeMap attrs = child.getAttributes();
                int childID = MarkerSegment.getAttributeValue(child,
                                                              attrs,
                                                              "htableId",
                                                              0, 3,
                                                              true);
                int childClass = MarkerSegment.getAttributeValue(child,
                                                                 attrs,
                                                                 "class",
                                                                 0, 1,
                                                                 true);
                DHTMarkerSegment dht = null;
                int tableIndex = -1;
                for (int j = 0; j < oldDHTs.size(); j++) {
                    DHTMarkerSegment testDHT = (DHTMarkerSegment) oldDHTs.get(j);
                    for (int k = 0; k < testDHT.tables.size(); k++) {
                        DHTMarkerSegment.Htable testTable =
                            (DHTMarkerSegment.Htable) testDHT.tables.get(k);
                        if ((childID == testTable.tableID) &&
                            (childClass == testTable.tableClass)) {
                            dht = testDHT;
                            tableIndex = k;
                            break;
                        }
                    }
                    if (dht != null) break;
                }
                if (dht != null) {
                    dht.tables.set(tableIndex, dht.getHtableFromNode(child));
                } else {
                    dht = (DHTMarkerSegment) oldDHTs.get(oldDHTs.size()-1);
                    dht.tables.add(dht.getHtableFromNode(child));
                }
            }
        } else {
            DHTMarkerSegment newGuy = new DHTMarkerSegment(node);
            int lastDQT = findMarkerSegmentPosition(DQTMarkerSegment.class, false);
            int firstSOF = findMarkerSegmentPosition(SOFMarkerSegment.class, true);
            int firstSOS = findMarkerSegmentPosition(SOSMarkerSegment.class, true);
            if (lastDQT != -1) {
                markerSequence.add(lastDQT+1, newGuy);
            } else if (firstSOF != -1) {
                markerSequence.add(firstSOF, newGuy);
            } else if (firstSOS != -1) {
                markerSequence.add(firstSOS, newGuy);
            } else {
                markerSequence.add(newGuy);
            }
        }
    }

    /**
     * Merge the given DRI node into the marker sequence.
     * If there already exists a DRI marker segment, the restart interval
     * value is updated.
     * If there is no DRI segment, then a new one is created and added as
     * follows:
     * If there is an SOF segment, the new DRI segment is inserted before
     * it.
     * If there is no SOF segment, the new DRI segment is inserted before
     * the first SOS segment, if there is one.
     * If there is no SOS segment, the new DRI segment is added to the end
     * of the sequence.
     */
    private void mergeDRINode(Node node) throws IIOInvalidTreeException {
        DRIMarkerSegment dri =
            (DRIMarkerSegment) findMarkerSegment(DRIMarkerSegment.class, true);
        if (dri != null) {
            dri.updateFromNativeNode(node, false);
        } else {
            DRIMarkerSegment newGuy = new DRIMarkerSegment(node);
            int firstSOF = findMarkerSegmentPosition(SOFMarkerSegment.class, true);
            int firstSOS = findMarkerSegmentPosition(SOSMarkerSegment.class, true);
            if (firstSOF != -1) {
                markerSequence.add(firstSOF, newGuy);
            } else if (firstSOS != -1) {
                markerSequence.add(firstSOS, newGuy);
            } else {
                markerSequence.add(newGuy);
            }
        }
    }

    /**
     * Merge the given COM node into the marker sequence.
     * A new COM marker segment is created and added to the sequence
     * using insertCOMMarkerSegment.
     */
    private void mergeCOMNode(Node node) throws IIOInvalidTreeException {
        COMMarkerSegment newGuy = new COMMarkerSegment(node);
        insertCOMMarkerSegment(newGuy);
    }

     /**
      * Insert a new COM marker segment into an appropriate place in the
      * marker sequence, as follows:
      * If there already exist COM marker segments, the new one is inserted
      * after the last one.
      * If there are no COM segments, the new COM segment is inserted after the
      * JFIF segment, if there is one.
      * If there is no JFIF segment, the new COM segment is inserted after the
      * Adobe marker segment, if there is one.
      * If there is no Adobe segment, the new COM segment is inserted
      * at the beginning of the sequence.
      */
    private void insertCOMMarkerSegment(COMMarkerSegment newGuy) {
        int lastCOM = findMarkerSegmentPosition(COMMarkerSegment.class, false);
        boolean hasJFIF = (findMarkerSegment(JFIFMarkerSegment.class, true) != null);
        int firstAdobe = findMarkerSegmentPosition(AdobeMarkerSegment.class, true);
        if (lastCOM != -1) {
            markerSequence.add(lastCOM+1, newGuy);
        } else if (hasJFIF) {
            markerSequence.add(1, newGuy);  // JFIF is always 0
        } else if (firstAdobe != -1) {
            markerSequence.add(firstAdobe+1, newGuy);
        } else {
            markerSequence.add(0, newGuy);
        }
    }

    /**
     * Merge the given Adobe APP14 node into the marker sequence.
     * If there already exists an Adobe marker segment, then its attributes
     * are updated from the node.
     * If there is no Adobe segment, then a new one is created and added
     * using insertAdobeMarkerSegment.
     */
    private void mergeAdobeNode(Node node) throws IIOInvalidTreeException {
        AdobeMarkerSegment adobe =
            (AdobeMarkerSegment) findMarkerSegment(AdobeMarkerSegment.class, true);
        if (adobe != null) {
            adobe.updateFromNativeNode(node, false);
        } else {
            AdobeMarkerSegment newGuy = new AdobeMarkerSegment(node);
            insertAdobeMarkerSegment(newGuy);
        }
    }

    /**
     * Insert the given AdobeMarkerSegment into the marker sequence, as
     * follows (we assume there is no Adobe segment yet):
     * If there is a JFIF segment, then the new Adobe segment is inserted
     * after it.
     * If there is no JFIF segment, the new Adobe segment is inserted after the
     * last Unknown segment, if there are any.
     * If there are no Unknown segments, the new Adobe segment is inserted
     * at the beginning of the sequence.
     */
    private void insertAdobeMarkerSegment(AdobeMarkerSegment newGuy) {
        boolean hasJFIF =
            (findMarkerSegment(JFIFMarkerSegment.class, true) != null);
        int lastUnknown = findLastUnknownMarkerSegmentPosition();
        if (hasJFIF) {
            markerSequence.add(1, newGuy);  // JFIF is always 0
        } else if (lastUnknown != -1) {
            markerSequence.add(lastUnknown+1, newGuy);
        } else {
            markerSequence.add(0, newGuy);
        }
    }

    /**
     * Merge the given Unknown node into the marker sequence.
     * A new Unknown marker segment is created and added to the sequence as
     * follows:
     * If there already exist Unknown marker segments, the new one is inserted
     * after the last one.
     * If there are no Unknown marker segments, the new Unknown marker segment
     * is inserted after the JFIF segment, if there is one.
     * If there is no JFIF segment, the new Unknown segment is inserted before
     * the Adobe marker segment, if there is one.
     * If there is no Adobe segment, the new Unknown segment is inserted
     * at the beginning of the sequence.
     */
    private void mergeUnknownNode(Node node) throws IIOInvalidTreeException {
        MarkerSegment newGuy = new MarkerSegment(node);
        int lastUnknown = findLastUnknownMarkerSegmentPosition();
        boolean hasJFIF = (findMarkerSegment(JFIFMarkerSegment.class, true) != null);
        int firstAdobe = findMarkerSegmentPosition(AdobeMarkerSegment.class, true);
        if (lastUnknown != -1) {
            markerSequence.add(lastUnknown+1, newGuy);
        } else if (hasJFIF) {
            markerSequence.add(1, newGuy);  // JFIF is always 0
        } if (firstAdobe != -1) {
            markerSequence.add(firstAdobe, newGuy);
        } else {
            markerSequence.add(0, newGuy);
        }
    }

    /**
     * Merge the given SOF node into the marker sequence.
     * If there already exists an SOF marker segment in the sequence, then
     * its values are updated from the node.
     * If there is no SOF segment, then a new one is created and added as
     * follows:
     * If there are any SOS segments, the new SOF segment is inserted before
     * the first one.
     * If there is no SOS segment, the new SOF segment is added to the end
     * of the sequence.
     *
     */
    private void mergeSOFNode(Node node) throws IIOInvalidTreeException {
        SOFMarkerSegment sof =
            (SOFMarkerSegment) findMarkerSegment(SOFMarkerSegment.class, true);
        if (sof != null) {
            sof.updateFromNativeNode(node, false);
        } else {
            SOFMarkerSegment newGuy = new SOFMarkerSegment(node);
            int firstSOS = findMarkerSegmentPosition(SOSMarkerSegment.class, true);
            if (firstSOS != -1) {
                markerSequence.add(firstSOS, newGuy);
            } else {
                markerSequence.add(newGuy);
            }
        }
    }

    /**
     * Merge the given SOS node into the marker sequence.
     * If there already exists a single SOS marker segment, then the values
     * are updated from the node.
     * If there are more than one existing SOS marker segments, then an
     * IIOInvalidTreeException is thrown, as SOS segments cannot be merged
     * into a set of progressive scans.
     * If there are no SOS marker segments, a new one is created and added
     * to the end of the sequence.
     */
    private void mergeSOSNode(Node node) throws IIOInvalidTreeException {
        SOSMarkerSegment firstSOS =
            (SOSMarkerSegment) findMarkerSegment(SOSMarkerSegment.class, true);
        SOSMarkerSegment lastSOS =
            (SOSMarkerSegment) findMarkerSegment(SOSMarkerSegment.class, false);
        if (firstSOS != null) {
            if (firstSOS != lastSOS) {
                throw new IIOInvalidTreeException
                    ("Can't merge SOS node into a tree with > 1 SOS node", node);
            }
            firstSOS.updateFromNativeNode(node, false);
        } else {
            markerSequence.add(new SOSMarkerSegment(node));
        }
    }

    private boolean transparencyDone;

    private void mergeStandardTree(Node root) throws IIOInvalidTreeException {
        transparencyDone = false;
        NodeList children = root.getChildNodes();
        for (int i = 0; i < children.getLength(); i++) {
            Node node = children.item(i);
            String name = node.getNodeName();
            if (name.equals("Chroma")) {
                mergeStandardChromaNode(node, children);
            } else if (name.equals("Compression")) {
                mergeStandardCompressionNode(node);
            } else if (name.equals("Data")) {
                mergeStandardDataNode(node);
            } else if (name.equals("Dimension")) {
                mergeStandardDimensionNode(node);
            } else if (name.equals("Document")) {
                mergeStandardDocumentNode(node);
            } else if (name.equals("Text")) {
                mergeStandardTextNode(node);
            } else if (name.equals("Transparency")) {
                mergeStandardTransparencyNode(node);
            } else {
                throw new IIOInvalidTreeException("Invalid node: " + name, node);
            }
        }
    }

    /*
     * In general, it could be possible to convert all non-pixel data to some
     * textual form and include it in comments, but then this would create the
     * expectation that these comment forms be recognized by the reader, thus
     * creating a defacto extension to JPEG metadata capabilities.  This is
     * probably best avoided, so the following convert only text nodes to
     * comments, and lose the keywords as well.
     */

    private void mergeStandardChromaNode(Node node, NodeList siblings)
        throws IIOInvalidTreeException {
        // ColorSpaceType can change the target colorspace for compression
        // This must take any transparency node into account as well, as
        // that affects the number of channels (if alpha is present).  If
        // a transparency node is dealt with here, set a flag to indicate
        // this to the transparency processor below.  If we discover that
        // the nodes are not in order, throw an exception as the tree is
        // invalid.

        if (transparencyDone) {
            throw new IIOInvalidTreeException
                ("Transparency node must follow Chroma node", node);
        }

        Node csType = node.getFirstChild();
        if ((csType == null) || !csType.getNodeName().equals("ColorSpaceType")) {
            // If there is no ColorSpaceType node, we have nothing to do
            return;
        }

        String csName = csType.getAttributes().getNamedItem("name").getNodeValue();

        int numChannels = 0;
        boolean wantJFIF = false;
        boolean wantAdobe = false;
        int transform = 0;
        boolean willSubsample = false;
        byte [] ids = {1, 2, 3, 4};  // JFIF compatible
        if (csName.equals("GRAY")) {
            numChannels = 1;
            wantJFIF = true;
        } else if (csName.equals("YCbCr")) {
            numChannels = 3;
            wantJFIF = true;
            willSubsample = true;
        } else if (csName.equals("PhotoYCC")) {
            numChannels = 3;
            wantAdobe = true;
            transform = JPEG.ADOBE_YCC;
            ids[0] = (byte) 'Y';
            ids[1] = (byte) 'C';
            ids[2] = (byte) 'c';
        } else if (csName.equals("RGB")) {
            numChannels = 3;
            wantAdobe = true;
            transform = JPEG.ADOBE_UNKNOWN;
            ids[0] = (byte) 'R';
            ids[1] = (byte) 'G';
            ids[2] = (byte) 'B';
        } else if ((csName.equals("XYZ"))
                   || (csName.equals("Lab"))
                   || (csName.equals("Luv"))
                   || (csName.equals("YxY"))
                   || (csName.equals("HSV"))
                   || (csName.equals("HLS"))
                   || (csName.equals("CMY"))
                   || (csName.equals("3CLR"))) {
            numChannels = 3;
        } else if (csName.equals("YCCK")) {
            numChannels = 4;
            wantAdobe = true;
            transform = JPEG.ADOBE_YCCK;
            willSubsample = true;
        } else if (csName.equals("CMYK")) {
            numChannels = 4;
            wantAdobe = true;
            transform = JPEG.ADOBE_UNKNOWN;
        } else if (csName.equals("4CLR")) {
            numChannels = 4;
        } else { // We can't handle them, so don't modify any metadata
            return;
        }

        boolean wantAlpha = false;
        for (int i = 0; i < siblings.getLength(); i++) {
            Node trans = siblings.item(i);
            if (trans.getNodeName().equals("Transparency")) {
                wantAlpha = wantAlpha(trans);
                break;  // out of for
            }
        }

        if (wantAlpha) {
            numChannels++;
            wantJFIF = false;
            if (ids[0] == (byte) 'R') {
                ids[3] = (byte) 'A';
                wantAdobe = false;
            }
        }

        JFIFMarkerSegment jfif =
            (JFIFMarkerSegment) findMarkerSegment(JFIFMarkerSegment.class, true);
        AdobeMarkerSegment adobe =
            (AdobeMarkerSegment) findMarkerSegment(AdobeMarkerSegment.class, true);
        SOFMarkerSegment sof =
            (SOFMarkerSegment) findMarkerSegment(SOFMarkerSegment.class, true);
        SOSMarkerSegment sos =
            (SOSMarkerSegment) findMarkerSegment(SOSMarkerSegment.class, true);

        // If the metadata specifies progressive, then the number of channels
        // must match, so that we can modify all the existing SOS marker segments.
        // If they don't match, we don't know what to do with SOS so we can't do
        // the merge.  We then just return silently.
        // An exception would not be appropriate.  A warning might, but we have
        // nowhere to send it to.
        if ((sof != null) && (sof.tag == JPEG.SOF2)) { // Progressive
            if ((sof.componentSpecs.length != numChannels) && (sos != null)) {
                return;
            }
        }

        // JFIF header might be removed
        if (!wantJFIF && (jfif != null)) {
            markerSequence.remove(jfif);
        }

        // Now add a JFIF if we do want one, but only if it isn't stream metadata
        if (wantJFIF && !isStream) {
            markerSequence.add(0, new JFIFMarkerSegment());
        }

        // Adobe header might be removed or the transform modified, if it isn't
        // stream metadata
        if (wantAdobe) {
            if ((adobe == null) && !isStream) {
                adobe = new AdobeMarkerSegment(transform);
                insertAdobeMarkerSegment(adobe);
            } else {
                adobe.transform = transform;
            }
        } else if (adobe != null) {
            markerSequence.remove(adobe);
        }

        boolean updateQtables = false;
        boolean updateHtables = false;

        boolean progressive = false;

        int [] subsampledSelectors = {0, 1, 1, 0 } ;
        int [] nonSubsampledSelectors = { 0, 0, 0, 0};

        int [] newTableSelectors = willSubsample
                                   ? subsampledSelectors
                                   : nonSubsampledSelectors;

        // Keep the old componentSpecs array
        SOFMarkerSegment.ComponentSpec [] oldCompSpecs = null;
        // SOF might be modified
        if (sof != null) {
            oldCompSpecs = sof.componentSpecs;
            progressive = (sof.tag == JPEG.SOF2);
            // Now replace the SOF with a new one; it might be the same, but
            // this is easier.
            markerSequence.set(markerSequence.indexOf(sof),
                               new SOFMarkerSegment(progressive,
                                                    false, // we never need extended
                                                    willSubsample,
                                                    ids,
                                                    numChannels));

            // Now suss out if subsampling changed and set the boolean for
            // updating the q tables
            // if the old componentSpec q table selectors don't match
            // the new ones, update the qtables.  The new selectors are already
            // in place in the new SOF segment above.
            for (int i = 0; i < oldCompSpecs.length; i++) {
                if (oldCompSpecs[i].QtableSelector != newTableSelectors[i]) {
                    updateQtables = true;
                }
            }

            if (progressive) {
                // if the component ids are different, update all the existing scans
                // ignore Huffman tables
                boolean idsDiffer = false;
                for (int i = 0; i < oldCompSpecs.length; i++) {
                    if (ids[i] != oldCompSpecs[i].componentId) {
                        idsDiffer = true;
                    }
                }
                if (idsDiffer) {
                    // update the ids in each SOS marker segment
                    for (Iterator iter = markerSequence.iterator(); iter.hasNext();) {
                        MarkerSegment seg = (MarkerSegment) iter.next();
                        if (seg instanceof SOSMarkerSegment) {
                            SOSMarkerSegment target = (SOSMarkerSegment) seg;
                            for (int i = 0; i < target.componentSpecs.length; i++) {
                                int oldSelector =
                                    target.componentSpecs[i].componentSelector;
                                // Find the position in the old componentSpecs array
                                // of the old component with the old selector
                                // and replace the component selector with the
                                // new id at the same position, as these match
                                // the new component specs array in the SOF created
                                // above.
                                for (int j = 0; j < oldCompSpecs.length; j++) {
                                    if (oldCompSpecs[j].componentId == oldSelector) {
                                        target.componentSpecs[i].componentSelector =
                                            ids[j];
                                    }
                                }
                            }
                        }
                    }
                }
            } else {
                if (sos != null) {
                    // htables - if the old htable selectors don't match the new ones,
                    // update the tables.
                    for (int i = 0; i < sos.componentSpecs.length; i++) {
                        if ((sos.componentSpecs[i].dcHuffTable
                             != newTableSelectors[i])
                            || (sos.componentSpecs[i].acHuffTable
                                != newTableSelectors[i])) {
                            updateHtables = true;
                        }
                    }

                    // Might be the same as the old one, but this is easier.
                    markerSequence.set(markerSequence.indexOf(sos),
                               new SOSMarkerSegment(willSubsample,
                                                    ids,
                                                    numChannels));
                }
            }
        } else {
            // should be stream metadata if there isn't an SOF, but check it anyway
            if (isStream) {
                // update tables - routines below check if it's really necessary
                updateQtables = true;
                updateHtables = true;
            }
        }

        if (updateQtables) {
            List tableSegments = new ArrayList();
            for (Iterator iter = markerSequence.iterator(); iter.hasNext();) {
                MarkerSegment seg = (MarkerSegment) iter.next();
                if (seg instanceof DQTMarkerSegment) {
                    tableSegments.add(seg);
                }
            }
            // If there are no tables, don't add them, as the metadata encodes an
            // abbreviated stream.
            // If we are not subsampling, we just need one, so don't do anything
            if (!tableSegments.isEmpty() && willSubsample) {
                // Is it really necessary?  There should be at least 2 tables.
                // If there is only one, assume it's a scaled "standard"
                // luminance table, extract the scaling factor, and generate a
                // scaled "standard" chrominance table.

                // Find the table with selector 1.
                boolean found = false;
                for (Iterator iter = tableSegments.iterator(); iter.hasNext();) {
                    DQTMarkerSegment testdqt = (DQTMarkerSegment) iter.next();
                    for (Iterator tabiter = testdqt.tables.iterator();
                         tabiter.hasNext();) {
                        DQTMarkerSegment.Qtable tab =
                            (DQTMarkerSegment.Qtable) tabiter.next();
                        if (tab.tableID == 1) {
                            found = true;
                        }
                    }
                }
                if (!found) {
                    //    find the table with selector 0.  There should be one.
                    DQTMarkerSegment.Qtable table0 = null;
                    for (Iterator iter = tableSegments.iterator(); iter.hasNext();) {
                        DQTMarkerSegment testdqt = (DQTMarkerSegment) iter.next();
                        for (Iterator tabiter = testdqt.tables.iterator();
                             tabiter.hasNext();) {
                            DQTMarkerSegment.Qtable tab =
                                (DQTMarkerSegment.Qtable) tabiter.next();
                            if (tab.tableID == 0) {
                                table0 = tab;
                            }
                        }
                    }

                    // Assuming that the table with id 0 is a luminance table,
                    // compute a new chrominance table of the same quality and
                    // add it to the last DQT segment
                    DQTMarkerSegment dqt =
                        (DQTMarkerSegment) tableSegments.get(tableSegments.size()-1);
                    dqt.tables.add(dqt.getChromaForLuma(table0));
                }
            }
        }

        if (updateHtables) {
            List tableSegments = new ArrayList();
            for (Iterator iter = markerSequence.iterator(); iter.hasNext();) {
                MarkerSegment seg = (MarkerSegment) iter.next();
                if (seg instanceof DHTMarkerSegment) {
                    tableSegments.add(seg);
                }
            }
            // If there are no tables, don't add them, as the metadata encodes an
            // abbreviated stream.
            // If we are not subsampling, we just need one, so don't do anything
            if (!tableSegments.isEmpty() && willSubsample) {
                // Is it really necessary?  There should be at least 2 dc and 2 ac
                // tables.  If there is only one, add a
                // "standard " chrominance table.

                // find a table with selector 1. AC/DC is irrelevant
                boolean found = false;
                for (Iterator iter = tableSegments.iterator(); iter.hasNext();) {
                    DHTMarkerSegment testdht = (DHTMarkerSegment) iter.next();
                    for (Iterator tabiter = testdht.tables.iterator();
                         tabiter.hasNext();) {
                        DHTMarkerSegment.Htable tab =
                            (DHTMarkerSegment.Htable) tabiter.next();
                        if (tab.tableID == 1) {
                            found = true;
                        }
                    }
                }
                if (!found) {
                    // Create new standard dc and ac chrominance tables and add them
                    // to the last DHT segment
                    DHTMarkerSegment lastDHT =
                        (DHTMarkerSegment) tableSegments.get(tableSegments.size()-1);
                    lastDHT.addHtable(JPEGHuffmanTable.StdDCLuminance, true, 1);
                    lastDHT.addHtable(JPEGHuffmanTable.StdACLuminance, true, 1);
                }
            }
        }
    }

    private boolean wantAlpha(Node transparency) {
        boolean returnValue = false;
        Node alpha = transparency.getFirstChild();  // Alpha must be first if present
        if (alpha.getNodeName().equals("Alpha")) {
            if (alpha.hasAttributes()) {
                String value =
                    alpha.getAttributes().getNamedItem("value").getNodeValue();
                if (!value.equals("none")) {
                    returnValue = true;
                }
            }
        }
        transparencyDone = true;
        return returnValue;
    }

    private void mergeStandardCompressionNode(Node node)
        throws IIOInvalidTreeException {
        // NumProgressiveScans is ignored.  Progression must be enabled on the
        // ImageWriteParam.
        // No-op
    }

    private void mergeStandardDataNode(Node node)
        throws IIOInvalidTreeException {
        // No-op
    }

    private void mergeStandardDimensionNode(Node node)
        throws IIOInvalidTreeException {
        // Pixel Aspect Ratio or pixel size can be incorporated if there is,
        // or can be, a JFIF segment
        JFIFMarkerSegment jfif =
            (JFIFMarkerSegment) findMarkerSegment(JFIFMarkerSegment.class, true);
        if (jfif == null) {
            // Can there be one?
            // Criteria:
            // SOF must be present with 1 or 3 channels, (stream metadata fails this)
            //     Component ids must be JFIF compatible.
            boolean canHaveJFIF = false;
            SOFMarkerSegment sof =
                (SOFMarkerSegment) findMarkerSegment(SOFMarkerSegment.class, true);
            if (sof != null) {
                int numChannels = sof.componentSpecs.length;
                if ((numChannels == 1) || (numChannels == 3)) {
                    canHaveJFIF = true; // remaining tests are negative
                    for (int i = 0; i < sof.componentSpecs.length; i++) {
                        if (sof.componentSpecs[i].componentId != i+1)
                            canHaveJFIF = false;
                    }
                    // if Adobe present, transform = ADOBE_UNKNOWN for 1-channel,
                    //     ADOBE_YCC for 3-channel.
                    AdobeMarkerSegment adobe =
                        (AdobeMarkerSegment) findMarkerSegment(AdobeMarkerSegment.class,
                                                               true);
                    if (adobe != null) {
                        if (adobe.transform != ((numChannels == 1)
                                                ? JPEG.ADOBE_UNKNOWN
                                                : JPEG.ADOBE_YCC)) {
                            canHaveJFIF = false;
                        }
                    }
                }
            }
            // If so, create one and insert it into the sequence.  Note that
            // default is just pixel ratio at 1:1
            if (canHaveJFIF) {
                jfif = new JFIFMarkerSegment();
                markerSequence.add(0, jfif);
            }
        }
        if (jfif != null) {
            NodeList children = node.getChildNodes();
            for (int i = 0; i < children.getLength(); i++) {
                Node child = children.item(i);
                NamedNodeMap attrs = child.getAttributes();
                String name = child.getNodeName();
                if (name.equals("PixelAspectRatio")) {
                    String valueString = attrs.getNamedItem("value").getNodeValue();
                    float value = Float.parseFloat(valueString);
                    Point p = findIntegerRatio(value);
                    jfif.resUnits = JPEG.DENSITY_UNIT_ASPECT_RATIO;
                    jfif.Xdensity = p.x;
                    jfif.Xdensity = p.y;
                } else if (name.equals("HorizontalPixelSize")) {
                    String valueString = attrs.getNamedItem("value").getNodeValue();
                    float value = Float.parseFloat(valueString);
                    // Convert from mm/dot to dots/cm
                    int dpcm = (int) Math.round(1.0/(value*10.0));
                    jfif.resUnits = JPEG.DENSITY_UNIT_DOTS_CM;
                    jfif.Xdensity = dpcm;
                } else if (name.equals("VerticalPixelSize")) {
                    String valueString = attrs.getNamedItem("value").getNodeValue();
                    float value = Float.parseFloat(valueString);
                    // Convert from mm/dot to dots/cm
                    int dpcm = (int) Math.round(1.0/(value*10.0));
                    jfif.resUnits = JPEG.DENSITY_UNIT_DOTS_CM;
                    jfif.Ydensity = dpcm;
                }

            }
        }
    }

    /*
     * Return a pair of integers whose ratio (x/y) approximates the given
     * float value.
     */
    private static Point findIntegerRatio(float value) {
        float epsilon = 0.005F;

        // Normalize
        value = Math.abs(value);

        // Deal with min case
        if (value <= epsilon) {
            return new Point(1, 255);
        }

        // Deal with max case
        if (value >= 255) {
            return new Point(255, 1);
        }

        // Remember if we invert
        boolean inverted = false;
        if (value < 1.0) {
            value = 1.0F/value;
            inverted = true;
        }

        // First approximation
        int y = 1;
        int x = (int) Math.round(value);

        float ratio = (float) x;
        float delta = Math.abs(value - ratio);
        while (delta > epsilon) { // not close enough
            // Increment y and compute a new x
            y++;
            x = (int) Math.round(y*value);
            ratio = (float)x/(float)y;
            delta = Math.abs(value - ratio);
        }
        return inverted ? new Point(y, x) : new Point(x, y);
    }

    private void mergeStandardDocumentNode(Node node)
        throws IIOInvalidTreeException {
        // No-op
    }

    private void mergeStandardTextNode(Node node)
        throws IIOInvalidTreeException {
        // Convert to comments.  For the moment ignore the encoding issue.
        // Ignore keywords, language, and encoding (for the moment).
        // If compression tag is present, use only entries with "none".
        NodeList children = node.getChildNodes();
        for (int i = 0; i < children.getLength(); i++) {
            Node child = children.item(i);
            NamedNodeMap attrs = child.getAttributes();
            Node comp = attrs.getNamedItem("compression");
            boolean copyIt = true;
            if (comp != null) {
                String compString = comp.getNodeValue();
                if (!compString.equals("none")) {
                    copyIt = false;
                }
            }
            if (copyIt) {
                String value = attrs.getNamedItem("value").getNodeValue();
                COMMarkerSegment com = new COMMarkerSegment(value);
                insertCOMMarkerSegment(com);
            }
        }
    }

    private void mergeStandardTransparencyNode(Node node)
        throws IIOInvalidTreeException {
        // This might indicate that an alpha channel is being added or removed.
        // The nodes must appear in order, and a Chroma node will process any
        // transparency, so process it here only if there was no Chroma node
        // Do nothing for stream metadata
        if (!transparencyDone && !isStream) {
            boolean wantAlpha = wantAlpha(node);
            // do we have alpha already?  If the number of channels is 2 or 4,
            // we do, as we don't support CMYK, nor can we add alpha to it
            // The number of channels can be determined from the SOF
            JFIFMarkerSegment jfif = (JFIFMarkerSegment) findMarkerSegment
                (JFIFMarkerSegment.class, true);
            AdobeMarkerSegment adobe = (AdobeMarkerSegment) findMarkerSegment
                (AdobeMarkerSegment.class, true);
            SOFMarkerSegment sof = (SOFMarkerSegment) findMarkerSegment
                (SOFMarkerSegment.class, true);
            SOSMarkerSegment sos = (SOSMarkerSegment) findMarkerSegment
                (SOSMarkerSegment.class, true);

            // We can do nothing for progressive, as we don't know how to
            // modify the scans.
            if ((sof != null) && (sof.tag == JPEG.SOF2)) { // Progressive
                return;
            }

            // Do we already have alpha?  We can tell by the number of channels
            // We must have an sof, or we can't do anything further
            if (sof != null) {
                int numChannels = sof.componentSpecs.length;
                boolean hadAlpha = (numChannels == 2) || (numChannels == 4);
                // proceed only if the old state and the new state differ
                if (hadAlpha != wantAlpha) {
                    if (wantAlpha) {  // Adding alpha
                        numChannels++;
                        if (jfif != null) {
                            markerSequence.remove(jfif);
                        }

                        // If an adobe marker is present, transform must be UNKNOWN
                        if (adobe != null) {
                            adobe.transform = JPEG.ADOBE_UNKNOWN;
                        }

                        // Add a component spec with appropriate parameters to SOF
                        SOFMarkerSegment.ComponentSpec [] newSpecs =
                            new SOFMarkerSegment.ComponentSpec[numChannels];
                        for (int i = 0; i < sof.componentSpecs.length; i++) {
                            newSpecs[i] = sof.componentSpecs[i];
                        }
                        byte oldFirstID = (byte) sof.componentSpecs[0].componentId;
                        byte newID = (byte) ((oldFirstID > 1) ? 'A' : 4);
                        newSpecs[numChannels-1] =
                            sof.getComponentSpec(newID,
                                sof.componentSpecs[0].HsamplingFactor,
                                sof.componentSpecs[0].QtableSelector);

                        sof.componentSpecs = newSpecs;

                        // Add a component spec with appropriate parameters to SOS
                        SOSMarkerSegment.ScanComponentSpec [] newScanSpecs =
                            new SOSMarkerSegment.ScanComponentSpec [numChannels];
                        for (int i = 0; i < sos.componentSpecs.length; i++) {
                            newScanSpecs[i] = sos.componentSpecs[i];
                        }
                        newScanSpecs[numChannels-1] =
                            sos.getScanComponentSpec (newID, 0);
                        sos.componentSpecs = newScanSpecs;
                    } else {  // Removing alpha
                        numChannels--;
                        // Remove a component spec from SOF
                        SOFMarkerSegment.ComponentSpec [] newSpecs =
                            new SOFMarkerSegment.ComponentSpec[numChannels];
                        for (int i = 0; i < numChannels; i++) {
                            newSpecs[i] = sof.componentSpecs[i];
                        }
                        sof.componentSpecs = newSpecs;

                        // Remove a component spec from SOS
                        SOSMarkerSegment.ScanComponentSpec [] newScanSpecs =
                            new SOSMarkerSegment.ScanComponentSpec [numChannels];
                        for (int i = 0; i < numChannels; i++) {
                            newScanSpecs[i] = sos.componentSpecs[i];
                        }
                        sos.componentSpecs = newScanSpecs;
                    }
                }
            }
        }
    }


    public void setFromTree(String formatName, Node root)
        throws IIOInvalidTreeException {
        if (formatName == null) {
            throw new IllegalArgumentException("null formatName!");
        }
        if (root == null) {
            throw new IllegalArgumentException("null root!");
        }
        if (isStream &&
            (formatName.equals(JPEG.nativeStreamMetadataFormatName))) {
            setFromNativeTree(root);
        } else if (!isStream &&
                   (formatName.equals(JPEG.nativeImageMetadataFormatName))) {
            setFromNativeTree(root);
        } else if (!isStream &&
                   (formatName.equals
                    (IIOMetadataFormatImpl.standardMetadataFormatName))) {
            // In this case a reset followed by a merge is correct
            super.setFromTree(formatName, root);
        } else {
            throw  new IllegalArgumentException("Unsupported format name: "
                                                + formatName);
        }
    }

    private void setFromNativeTree(Node root) throws IIOInvalidTreeException {
        if (resetSequence == null) {
            resetSequence = markerSequence;
        }
        markerSequence = new ArrayList();

        // Build a whole new marker sequence from the tree

        String name = root.getNodeName();
        if (name != ((isStream) ? JPEG.nativeStreamMetadataFormatName
                                : JPEG.nativeImageMetadataFormatName)) {
            throw new IIOInvalidTreeException("Invalid root node name: " + name,
                                              root);
        }
        if (!isStream) {
            if (root.getChildNodes().getLength() != 2) { // JPEGvariety and markerSequence
                throw new IIOInvalidTreeException(
                    "JPEGvariety and markerSequence nodes must be present", root);
            }

            Node JPEGvariety = root.getFirstChild();

            if (JPEGvariety.getChildNodes().getLength() != 0) {
                markerSequence.add(new JFIFMarkerSegment(JPEGvariety.getFirstChild()));
            }
        }

        Node markerSequenceNode = isStream ? root : root.getLastChild();
        setFromMarkerSequenceNode(markerSequenceNode);

    }

    void setFromMarkerSequenceNode(Node markerSequenceNode)
        throws IIOInvalidTreeException{

        NodeList children = markerSequenceNode.getChildNodes();
        // for all the children, add a marker segment
        for (int i = 0; i < children.getLength(); i++) {
            Node node = children.item(i);
            String childName = node.getNodeName();
            if (childName.equals("dqt")) {
                markerSequence.add(new DQTMarkerSegment(node));
            } else if (childName.equals("dht")) {
                markerSequence.add(new DHTMarkerSegment(node));
            } else if (childName.equals("dri")) {
                markerSequence.add(new DRIMarkerSegment(node));
            } else if (childName.equals("com")) {
                markerSequence.add(new COMMarkerSegment(node));
            } else if (childName.equals("app14Adobe")) {
                markerSequence.add(new AdobeMarkerSegment(node));
            } else if (childName.equals("unknown")) {
                markerSequence.add(new MarkerSegment(node));
            } else if (childName.equals("sof")) {
                markerSequence.add(new SOFMarkerSegment(node));
            } else if (childName.equals("sos")) {
                markerSequence.add(new SOSMarkerSegment(node));
            } else {
                throw new IIOInvalidTreeException("Invalid "
                    + (isStream ? "stream " : "image ") + "child: "
                    + childName, node);
            }
        }
    }

    /**
     * Check that this metadata object is in a consistent state and
     * return <code>true if it is or false
     * otherwise.  All the constructors and modifiers should call
     * this method at the end to guarantee that the data is always
     * consistent, as the writer relies on this.
     */
    private boolean isConsistent() {
        SOFMarkerSegment sof =
            (SOFMarkerSegment) findMarkerSegment(SOFMarkerSegment.class,
                                                 true);
        JFIFMarkerSegment jfif =
            (JFIFMarkerSegment) findMarkerSegment(JFIFMarkerSegment.class,
                                                  true);
        AdobeMarkerSegment adobe =
            (AdobeMarkerSegment) findMarkerSegment(AdobeMarkerSegment.class,
                                                   true);
        boolean retval = true;
        if (!isStream) {
            if (sof != null) {
                // SOF numBands = total scan bands
                int numSOFBands = sof.componentSpecs.length;
                int numScanBands = countScanBands();
                if (numScanBands != 0) {  // No SOS is OK
                    if (numScanBands != numSOFBands) {
                        retval = false;
                    }
                }
                // If JFIF is present, component ids are 1-3, bands are 1 or 3
                if (jfif != null) {
                    if ((numSOFBands != 1) && (numSOFBands != 3)) {
                        retval = false;
                    }
                    for (int i = 0; i < numSOFBands; i++) {
                        if (sof.componentSpecs[i].componentId != i+1) {
                            retval = false;
                        }
                    }

                    // If both JFIF and Adobe are present,
                    // Adobe transform == unknown for gray,
                    // YCC for 3-chan.
                    if ((adobe != null)
                        && (((numSOFBands == 1)
                             && (adobe.transform != JPEG.ADOBE_UNKNOWN))
                            || ((numSOFBands == 3)
                                && (adobe.transform != JPEG.ADOBE_YCC)))) {
                        retval = false;
                    }
                }
            } else {
                // stream can't have jfif, adobe, sof, or sos
                SOSMarkerSegment sos =
                    (SOSMarkerSegment) findMarkerSegment(SOSMarkerSegment.class,
                                                         true);
                if ((jfif != null) || (adobe != null)
                    || (sof != null) || (sos != null)) {
                    retval = false;
                }
            }
        }
        return retval;
    }

    /**
     * Returns the total number of bands referenced in all SOS marker
     * segments, including 0 if there are no SOS marker segments.
     */
    private int countScanBands() {
        List ids = new ArrayList();
        Iterator iter = markerSequence.iterator();
        while(iter.hasNext()) {
            MarkerSegment seg = (MarkerSegment)iter.next();
            if (seg instanceof SOSMarkerSegment) {
                SOSMarkerSegment sos = (SOSMarkerSegment) seg;
                SOSMarkerSegment.ScanComponentSpec [] specs = sos.componentSpecs;
                for (int i = 0; i < specs.length; i++) {
                    Integer id = new Integer(specs[i].componentSelector);
                    if (!ids.contains(id)) {
                        ids.add(id);
                    }
                }
            }
        }

        return ids.size();
    }

    ///// Writer support

    void writeToStream(ImageOutputStream ios,
                       boolean ignoreJFIF,
                       boolean forceJFIF,
                       List thumbnails,
                       ICC_Profile iccProfile,
                       boolean ignoreAdobe,
                       int newAdobeTransform,
                       JPEGImageWriter writer)
        throws IOException {
        if (forceJFIF) {
            // Write a default JFIF segment, including thumbnails
            // This won't be duplicated below because forceJFIF will be
            // set only if there is no JFIF present already.
            JFIFMarkerSegment.writeDefaultJFIF(ios,
                                               thumbnails,
                                               iccProfile,
                                               writer);
            if ((ignoreAdobe == false)
                && (newAdobeTransform != JPEG.ADOBE_IMPOSSIBLE)) {
                if ((newAdobeTransform != JPEG.ADOBE_UNKNOWN)
                    && (newAdobeTransform != JPEG.ADOBE_YCC)) {
                    // Not compatible, so ignore Adobe.
                    ignoreAdobe = true;
                    writer.warningOccurred
                        (JPEGImageWriter.WARNING_METADATA_ADJUSTED_FOR_THUMB);
                }
            }
        }
        // Iterate over each MarkerSegment
        Iterator iter = markerSequence.iterator();
        while(iter.hasNext()) {
            MarkerSegment seg = (MarkerSegment)iter.next();
            if (seg instanceof JFIFMarkerSegment) {
                if (ignoreJFIF == false) {
                    JFIFMarkerSegment jfif = (JFIFMarkerSegment) seg;
                    jfif.writeWithThumbs(ios, thumbnails, writer);
                    if (iccProfile != null) {
                        JFIFMarkerSegment.writeICC(iccProfile, ios);
                    }
                } // Otherwise ignore it, as requested
            } else if (seg instanceof AdobeMarkerSegment) {
                if (ignoreAdobe == false) {
                    if (newAdobeTransform != JPEG.ADOBE_IMPOSSIBLE) {
                        AdobeMarkerSegment newAdobe =
                            (AdobeMarkerSegment) seg.clone();
                        newAdobe.transform = newAdobeTransform;
                        newAdobe.write(ios);
                    } else if (forceJFIF) {
                        // If adobe isn't JFIF compatible, ignore it
                        AdobeMarkerSegment adobe = (AdobeMarkerSegment) seg;
                        if ((adobe.transform == JPEG.ADOBE_UNKNOWN)
                            || (adobe.transform == JPEG.ADOBE_YCC)) {
                            adobe.write(ios);
                        } else {
                            writer.warningOccurred
                         (JPEGImageWriter.WARNING_METADATA_ADJUSTED_FOR_THUMB);
                        }
                    } else {
                        seg.write(ios);
                    }
                } // Otherwise ignore it, as requested
            } else {
                seg.write(ios);
            }
        }
    }

    //// End of writer support

    public void reset() {
        if (resetSequence != null) {  // Otherwise no need to reset
            markerSequence = resetSequence;
            resetSequence = null;
        }
    }

    public void print() {
        for (int i = 0; i < markerSequence.size(); i++) {
            MarkerSegment seg = (MarkerSegment) markerSequence.get(i);
            seg.print();
        }
    }

}

Other Java examples (source code examples)

Here is a short list of links related to this Java JPEGMetadata.java source code file:

... this post is sponsored by my books ...

#1 New Release!

FP Best Seller

 

new blog posts

 

Copyright 1998-2024 Alvin Alexander, alvinalexander.com
All Rights Reserved.

A percentage of advertising revenue from
pages under the /java/jwarehouse URI on this website is
paid back to open source projects.