view patches/openjdk/6782079-png_metadata_oom.patch @ 2021:3eec8b65af65

Backport S6687968, S6541476, S6782079 iTXt chunk handling for png now works correctly.
author Deepak Bhole <dbhole@redhat.com>
date Tue, 04 Jan 2011 08:50:49 -0500
parents
children
line wrap: on
line source

diff -urN openjdk.orig/jdk/src/share/classes/com/sun/imageio/plugins/png/PNGImageReader.java openjdk/jdk/src/share/classes/com/sun/imageio/plugins/png/PNGImageReader.java
--- openjdk.orig/jdk/src/share/classes/com/sun/imageio/plugins/png/PNGImageReader.java	2010-12-24 11:22:28.802101947 -0500
+++ openjdk/jdk/src/share/classes/com/sun/imageio/plugins/png/PNGImageReader.java	2010-12-24 11:24:47.272101847 -0500
@@ -37,6 +37,7 @@
 import java.io.BufferedInputStream;
 import java.io.ByteArrayInputStream;
 import java.io.DataInputStream;
+import java.io.EOFException;
 import java.io.InputStream;
 import java.io.IOException;
 import java.io.SequenceInputStream;
@@ -59,7 +60,7 @@
 import java.io.ByteArrayOutputStream;
 import sun.awt.image.ByteInterleavedRaster;
 
-class PNGImageDataEnumeration implements Enumeration {
+class PNGImageDataEnumeration implements Enumeration<InputStream> {
 
     boolean firstTime = true;
     ImageInputStream stream;
@@ -72,7 +73,7 @@
         int type = stream.readInt(); // skip chunk type
     }
 
-    public Object nextElement() {
+    public InputStream nextElement() {
         try {
             firstTime = false;
             ImageInputStream iis = new SubImageInputStream(stream, length);
@@ -209,25 +210,17 @@
         resetStreamSettings();
     }
 
-    private String readNullTerminatedString(String charset) throws IOException {
+    private String readNullTerminatedString(String charset, int maxLen) throws IOException {
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
         int b;
-        while ((b = stream.read()) != 0) {
+        int count = 0;
+        while ((maxLen > count++) && ((b = stream.read()) != 0)) {
+            if (b == -1) throw new EOFException();
             baos.write(b);
         }
         return new String(baos.toByteArray(), charset);
     }
 
-    private String readNullTerminatedString() throws IOException {
-        StringBuilder b = new StringBuilder();
-        int c;
-
-        while ((c = stream.read()) != 0) {
-            b.append((char)c);
-        }
-        return b.toString();
-    }
-
     private void readHeader() throws IIOException {
         if (gotHeader) {
             return;
@@ -436,7 +429,7 @@
     }
 
     private void parse_iCCP_chunk(int chunkLength) throws IOException {
-        String keyword = readNullTerminatedString();
+        String keyword = readNullTerminatedString("ISO-8859-1", 80);
         metadata.iCCP_profileName = keyword;
 
         metadata.iCCP_compressionMethod = stream.readUnsignedByte();
@@ -452,7 +445,7 @@
     private void parse_iTXt_chunk(int chunkLength) throws IOException {
         long chunkStart = stream.getStreamPosition();
 
-        String keyword = readNullTerminatedString();
+        String keyword = readNullTerminatedString("ISO-8859-1", 80);
         metadata.iTXt_keyword.add(keyword);
 
         int compressionFlag = stream.readUnsignedByte();
@@ -461,15 +454,17 @@
         int compressionMethod = stream.readUnsignedByte();
         metadata.iTXt_compressionMethod.add(Integer.valueOf(compressionMethod));
 
-        String languageTag = readNullTerminatedString("UTF8");
+        String languageTag = readNullTerminatedString("UTF8", 80);
         metadata.iTXt_languageTag.add(languageTag);
 
+        long pos = stream.getStreamPosition();
+        int maxLen = (int)(chunkStart + chunkLength - pos);
         String translatedKeyword =
-            readNullTerminatedString("UTF8");
+            readNullTerminatedString("UTF8", maxLen);
         metadata.iTXt_translatedKeyword.add(translatedKeyword);
 
         String text;
-        long pos = stream.getStreamPosition();
+        pos = stream.getStreamPosition();
         byte[] b = new byte[(int)(chunkStart + chunkLength - pos)];
         stream.readFully(b);
 
@@ -513,7 +508,7 @@
 
     private void parse_sPLT_chunk(int chunkLength)
         throws IOException, IIOException {
-        metadata.sPLT_paletteName = readNullTerminatedString();
+        metadata.sPLT_paletteName = readNullTerminatedString("ISO-8859-1", 80);
         chunkLength -= metadata.sPLT_paletteName.length() + 1;
 
         int sampleDepth = stream.readUnsignedByte();
@@ -556,12 +551,12 @@
     }
 
     private void parse_tEXt_chunk(int chunkLength) throws IOException {
-        String keyword = readNullTerminatedString();
+        String keyword = readNullTerminatedString("ISO-8859-1", 80);
         metadata.tEXt_keyword.add(keyword);
 
         byte[] b = new byte[chunkLength - keyword.length() - 1];
         stream.readFully(b);
-        metadata.tEXt_text.add(new String(b));
+        metadata.tEXt_text.add(new String(b, "ISO-8859-1"));
     }
 
     private void parse_tIME_chunk() throws IOException {
@@ -642,7 +637,7 @@
     }
 
     private void parse_zTXt_chunk(int chunkLength) throws IOException {
-        String keyword = readNullTerminatedString();
+        String keyword = readNullTerminatedString("ISO-8859-1", 80);
         metadata.zTXt_keyword.add(keyword);
 
         int method = stream.readUnsignedByte();
@@ -650,7 +645,7 @@
 
         byte[] b = new byte[chunkLength - keyword.length() - 2];
         stream.readFully(b);
-        metadata.zTXt_text.add(new String(inflate(b)));
+        metadata.zTXt_text.add(new String(inflate(b), "ISO-8859-1"));
     }
 
     private void readMetadata() throws IIOException {
@@ -1265,7 +1260,7 @@
         try {
             stream.seek(imageStartPosition);
 
-            Enumeration e = new PNGImageDataEnumeration(stream);
+            Enumeration<InputStream> e = new PNGImageDataEnumeration(stream);
             InputStream is = new SequenceInputStream(e);
 
            /* InflaterInputStream uses an Inflater instance which consumes
diff -urN openjdk.orig/jdk/src/share/classes/com/sun/imageio/plugins/png/PNGImageReader.java.orig openjdk/jdk/src/share/classes/com/sun/imageio/plugins/png/PNGImageReader.java.orig
--- openjdk.orig/jdk/src/share/classes/com/sun/imageio/plugins/png/PNGImageReader.java.orig	1969-12-31 19:00:00.000000000 -0500
+++ openjdk/jdk/src/share/classes/com/sun/imageio/plugins/png/PNGImageReader.java.orig	2010-12-24 11:22:50.400102016 -0500
@@ -0,0 +1,1599 @@
+/*
+ * Copyright (c) 2000, 2006, 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.png;
+
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.color.ColorSpace;
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBuffer;
+import java.awt.image.DataBufferByte;
+import java.awt.image.DataBufferUShort;
+import java.awt.image.Raster;
+import java.awt.image.WritableRaster;
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.InputStream;
+import java.io.IOException;
+import java.io.SequenceInputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.zip.Inflater;
+import java.util.zip.InflaterInputStream;
+import javax.imageio.IIOException;
+import javax.imageio.ImageReader;
+import javax.imageio.ImageReadParam;
+import javax.imageio.ImageTypeSpecifier;
+import javax.imageio.metadata.IIOMetadata;
+import javax.imageio.spi.ImageReaderSpi;
+import javax.imageio.stream.ImageInputStream;
+import com.sun.imageio.plugins.common.InputStreamAdapter;
+import com.sun.imageio.plugins.common.ReaderUtil;
+import com.sun.imageio.plugins.common.SubImageInputStream;
+import java.io.ByteArrayOutputStream;
+import sun.awt.image.ByteInterleavedRaster;
+
+class PNGImageDataEnumeration implements Enumeration {
+
+    boolean firstTime = true;
+    ImageInputStream stream;
+    int length;
+
+    public PNGImageDataEnumeration(ImageInputStream stream)
+        throws IOException {
+        this.stream = stream;
+        this.length = stream.readInt();
+        int type = stream.readInt(); // skip chunk type
+    }
+
+    public Object nextElement() {
+        try {
+            firstTime = false;
+            ImageInputStream iis = new SubImageInputStream(stream, length);
+            return new InputStreamAdapter(iis);
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
+    public boolean hasMoreElements() {
+        if (firstTime) {
+            return true;
+        }
+
+        try {
+            int crc = stream.readInt();
+            this.length = stream.readInt();
+            int type = stream.readInt();
+            if (type == PNGImageReader.IDAT_TYPE) {
+                return true;
+            } else {
+                return false;
+            }
+        } catch (IOException e) {
+            return false;
+        }
+    }
+}
+
+/**
+ */
+public class PNGImageReader extends ImageReader {
+
+    /*
+     * Note: The following chunk type constants are autogenerated.  Each
+     * one is derived from the ASCII values of its 4-character name.  For
+     * example, IHDR_TYPE is calculated as follows:
+     *            ('I' << 24) | ('H' << 16) | ('D' << 8) | 'R'
+     */
+
+    // Critical chunks
+    static final int IHDR_TYPE = 0x49484452;
+    static final int PLTE_TYPE = 0x504c5445;
+    static final int IDAT_TYPE = 0x49444154;
+    static final int IEND_TYPE = 0x49454e44;
+
+    // Ancillary chunks
+    static final int bKGD_TYPE = 0x624b4744;
+    static final int cHRM_TYPE = 0x6348524d;
+    static final int gAMA_TYPE = 0x67414d41;
+    static final int hIST_TYPE = 0x68495354;
+    static final int iCCP_TYPE = 0x69434350;
+    static final int iTXt_TYPE = 0x69545874;
+    static final int pHYs_TYPE = 0x70485973;
+    static final int sBIT_TYPE = 0x73424954;
+    static final int sPLT_TYPE = 0x73504c54;
+    static final int sRGB_TYPE = 0x73524742;
+    static final int tEXt_TYPE = 0x74455874;
+    static final int tIME_TYPE = 0x74494d45;
+    static final int tRNS_TYPE = 0x74524e53;
+    static final int zTXt_TYPE = 0x7a545874;
+
+    static final int PNG_COLOR_GRAY = 0;
+    static final int PNG_COLOR_RGB = 2;
+    static final int PNG_COLOR_PALETTE = 3;
+    static final int PNG_COLOR_GRAY_ALPHA = 4;
+    static final int PNG_COLOR_RGB_ALPHA = 6;
+
+    // The number of bands by PNG color type
+    static final int[] inputBandsForColorType = {
+         1, // gray
+        -1, // unused
+         3, // rgb
+         1, // palette
+         2, // gray + alpha
+        -1, // unused
+         4  // rgb + alpha
+    };
+
+    static final int PNG_FILTER_NONE = 0;
+    static final int PNG_FILTER_SUB = 1;
+    static final int PNG_FILTER_UP = 2;
+    static final int PNG_FILTER_AVERAGE = 3;
+    static final int PNG_FILTER_PAETH = 4;
+
+    static final int[] adam7XOffset = { 0, 4, 0, 2, 0, 1, 0 };
+    static final int[] adam7YOffset = { 0, 0, 4, 0, 2, 0, 1 };
+    static final int[] adam7XSubsampling = { 8, 8, 4, 4, 2, 2, 1, 1 };
+    static final int[] adam7YSubsampling = { 8, 8, 8, 4, 4, 2, 2, 1 };
+
+    private static final boolean debug = true;
+
+    ImageInputStream stream = null;
+
+    boolean gotHeader = false;
+    boolean gotMetadata = false;
+
+    ImageReadParam lastParam = null;
+
+    long imageStartPosition = -1L;
+
+    Rectangle sourceRegion = null;
+    int sourceXSubsampling = -1;
+    int sourceYSubsampling = -1;
+    int sourceMinProgressivePass = 0;
+    int sourceMaxProgressivePass = 6;
+    int[] sourceBands = null;
+    int[] destinationBands = null;
+    Point destinationOffset = new Point(0, 0);
+
+    PNGMetadata metadata = new PNGMetadata();
+
+    DataInputStream pixelStream = null;
+
+    BufferedImage theImage = null;
+
+    // The number of source pixels processed
+    int pixelsDone = 0;
+
+    // The total number of pixels in the source image
+    int totalPixels;
+
+    public PNGImageReader(ImageReaderSpi originatingProvider) {
+        super(originatingProvider);
+    }
+
+    public void setInput(Object input,
+                         boolean seekForwardOnly,
+                         boolean ignoreMetadata) {
+        super.setInput(input, seekForwardOnly, ignoreMetadata);
+        this.stream = (ImageInputStream)input; // Always works
+
+        // Clear all values based on the previous stream contents
+        resetStreamSettings();
+    }
+
+    private String readNullTerminatedString(String charset) throws IOException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        int b;
+        while ((b = stream.read()) != 0) {
+            baos.write(b);
+        }
+        return new String(baos.toByteArray(), charset);
+    }
+
+    private String readNullTerminatedString() throws IOException {
+        StringBuilder b = new StringBuilder();
+        int c;
+
+        while ((c = stream.read()) != 0) {
+            b.append((char)c);
+        }
+        return b.toString();
+    }
+
+    private void readHeader() throws IIOException {
+        if (gotHeader) {
+            return;
+        }
+        if (stream == null) {
+            throw new IllegalStateException("Input source not set!");
+        }
+
+        try {
+            byte[] signature = new byte[8];
+            stream.readFully(signature);
+
+            if (signature[0] != (byte)137 ||
+                signature[1] != (byte)80 ||
+                signature[2] != (byte)78 ||
+                signature[3] != (byte)71 ||
+                signature[4] != (byte)13 ||
+                signature[5] != (byte)10 ||
+                signature[6] != (byte)26 ||
+                signature[7] != (byte)10) {
+                throw new IIOException("Bad PNG signature!");
+            }
+
+            int IHDR_length = stream.readInt();
+            if (IHDR_length != 13) {
+                throw new IIOException("Bad length for IHDR chunk!");
+            }
+            int IHDR_type = stream.readInt();
+            if (IHDR_type != IHDR_TYPE) {
+                throw new IIOException("Bad type for IHDR chunk!");
+            }
+
+            this.metadata = new PNGMetadata();
+
+            int width = stream.readInt();
+            int height = stream.readInt();
+
+            // Re-use signature array to bulk-read these unsigned byte values
+            stream.readFully(signature, 0, 5);
+            int bitDepth          = signature[0] & 0xff;
+            int colorType         = signature[1] & 0xff;
+            int compressionMethod = signature[2] & 0xff;
+            int filterMethod      = signature[3] & 0xff;
+            int interlaceMethod   = signature[4] & 0xff;
+
+            // Skip IHDR CRC
+            stream.skipBytes(4);
+
+            stream.flushBefore(stream.getStreamPosition());
+
+            if (width == 0) {
+                throw new IIOException("Image width == 0!");
+            }
+            if (height == 0) {
+                throw new IIOException("Image height == 0!");
+            }
+            if (bitDepth != 1 && bitDepth != 2 && bitDepth != 4 &&
+                bitDepth != 8 && bitDepth != 16) {
+                throw new IIOException("Bit depth must be 1, 2, 4, 8, or 16!");
+            }
+            if (colorType != 0 && colorType != 2 && colorType != 3 &&
+                colorType != 4 && colorType != 6) {
+                throw new IIOException("Color type must be 0, 2, 3, 4, or 6!");
+            }
+            if (colorType == PNG_COLOR_PALETTE && bitDepth == 16) {
+                throw new IIOException("Bad color type/bit depth combination!");
+            }
+            if ((colorType == PNG_COLOR_RGB ||
+                 colorType == PNG_COLOR_RGB_ALPHA ||
+                 colorType == PNG_COLOR_GRAY_ALPHA) &&
+                (bitDepth != 8 && bitDepth != 16)) {
+                throw new IIOException("Bad color type/bit depth combination!");
+            }
+            if (compressionMethod != 0) {
+                throw new IIOException("Unknown compression method (not 0)!");
+            }
+            if (filterMethod != 0) {
+                throw new IIOException("Unknown filter method (not 0)!");
+            }
+            if (interlaceMethod != 0 && interlaceMethod != 1) {
+                throw new IIOException("Unknown interlace method (not 0 or 1)!");
+            }
+
+            metadata.IHDR_present = true;
+            metadata.IHDR_width = width;
+            metadata.IHDR_height = height;
+            metadata.IHDR_bitDepth = bitDepth;
+            metadata.IHDR_colorType = colorType;
+            metadata.IHDR_compressionMethod = compressionMethod;
+            metadata.IHDR_filterMethod = filterMethod;
+            metadata.IHDR_interlaceMethod = interlaceMethod;
+            gotHeader = true;
+        } catch (IOException e) {
+            throw new IIOException("I/O error reading PNG header!", e);
+        }
+    }
+
+    private void parse_PLTE_chunk(int chunkLength) throws IOException {
+        if (metadata.PLTE_present) {
+            processWarningOccurred(
+"A PNG image may not contain more than one PLTE chunk.\n" +
+"The chunk wil be ignored.");
+            return;
+        } else if (metadata.IHDR_colorType == PNG_COLOR_GRAY ||
+                   metadata.IHDR_colorType == PNG_COLOR_GRAY_ALPHA) {
+            processWarningOccurred(
+"A PNG gray or gray alpha image cannot have a PLTE chunk.\n" +
+"The chunk wil be ignored.");
+            return;
+        }
+
+        byte[] palette = new byte[chunkLength];
+        stream.readFully(palette);
+
+        int numEntries = chunkLength/3;
+        if (metadata.IHDR_colorType == PNG_COLOR_PALETTE) {
+            int maxEntries = 1 << metadata.IHDR_bitDepth;
+            if (numEntries > maxEntries) {
+                processWarningOccurred(
+"PLTE chunk contains too many entries for bit depth, ignoring extras.");
+                numEntries = maxEntries;
+            }
+            numEntries = Math.min(numEntries, maxEntries);
+        }
+
+        // Round array sizes up to 2^2^n
+        int paletteEntries;
+        if (numEntries > 16) {
+            paletteEntries = 256;
+        } else if (numEntries > 4) {
+            paletteEntries = 16;
+        } else if (numEntries > 2) {
+            paletteEntries = 4;
+        } else {
+            paletteEntries = 2;
+        }
+
+        metadata.PLTE_present = true;
+        metadata.PLTE_red = new byte[paletteEntries];
+        metadata.PLTE_green = new byte[paletteEntries];
+        metadata.PLTE_blue = new byte[paletteEntries];
+
+        int index = 0;
+        for (int i = 0; i < numEntries; i++) {
+            metadata.PLTE_red[i] = palette[index++];
+            metadata.PLTE_green[i] = palette[index++];
+            metadata.PLTE_blue[i] = palette[index++];
+        }
+    }
+
+    private void parse_bKGD_chunk() throws IOException {
+        if (metadata.IHDR_colorType == PNG_COLOR_PALETTE) {
+            metadata.bKGD_colorType = PNG_COLOR_PALETTE;
+            metadata.bKGD_index = stream.readUnsignedByte();
+        } else if (metadata.IHDR_colorType == PNG_COLOR_GRAY ||
+                   metadata.IHDR_colorType == PNG_COLOR_GRAY_ALPHA) {
+            metadata.bKGD_colorType = PNG_COLOR_GRAY;
+            metadata.bKGD_gray = stream.readUnsignedShort();
+        } else { // RGB or RGB_ALPHA
+            metadata.bKGD_colorType = PNG_COLOR_RGB;
+            metadata.bKGD_red = stream.readUnsignedShort();
+            metadata.bKGD_green = stream.readUnsignedShort();
+            metadata.bKGD_blue = stream.readUnsignedShort();
+        }
+
+        metadata.bKGD_present = true;
+    }
+
+    private void parse_cHRM_chunk() throws IOException {
+        metadata.cHRM_whitePointX = stream.readInt();
+        metadata.cHRM_whitePointY = stream.readInt();
+        metadata.cHRM_redX = stream.readInt();
+        metadata.cHRM_redY = stream.readInt();
+        metadata.cHRM_greenX = stream.readInt();
+        metadata.cHRM_greenY = stream.readInt();
+        metadata.cHRM_blueX = stream.readInt();
+        metadata.cHRM_blueY = stream.readInt();
+
+        metadata.cHRM_present = true;
+    }
+
+    private void parse_gAMA_chunk() throws IOException {
+        int gamma = stream.readInt();
+        metadata.gAMA_gamma = gamma;
+
+        metadata.gAMA_present = true;
+    }
+
+    private void parse_hIST_chunk(int chunkLength) throws IOException,
+        IIOException
+    {
+        if (!metadata.PLTE_present) {
+            throw new IIOException("hIST chunk without prior PLTE chunk!");
+        }
+
+        /* According to PNG specification length of
+         * hIST chunk is specified in bytes and
+         * hIST chunk consists of 2 byte elements
+         * (so we expect length is even).
+         */
+        metadata.hIST_histogram = new char[chunkLength/2];
+        stream.readFully(metadata.hIST_histogram,
+                         0, metadata.hIST_histogram.length);
+
+        metadata.hIST_present = true;
+    }
+
+    private void parse_iCCP_chunk(int chunkLength) throws IOException {
+        String keyword = readNullTerminatedString();
+        metadata.iCCP_profileName = keyword;
+
+        metadata.iCCP_compressionMethod = stream.readUnsignedByte();
+
+        byte[] compressedProfile =
+          new byte[chunkLength - keyword.length() - 2];
+        stream.readFully(compressedProfile);
+        metadata.iCCP_compressedProfile = compressedProfile;
+
+        metadata.iCCP_present = true;
+    }
+
+    private void parse_iTXt_chunk(int chunkLength) throws IOException {
+        long chunkStart = stream.getStreamPosition();
+
+        String keyword = readNullTerminatedString();
+        metadata.iTXt_keyword.add(keyword);
+
+        int compressionFlag = stream.readUnsignedByte();
+        metadata.iTXt_compressionFlag.add(Boolean.valueOf(compressionFlag == 1));
+
+        int compressionMethod = stream.readUnsignedByte();
+        metadata.iTXt_compressionMethod.add(Integer.valueOf(compressionMethod));
+
+        String languageTag = readNullTerminatedString("UTF8");
+        metadata.iTXt_languageTag.add(languageTag);
+
+        String translatedKeyword =
+            readNullTerminatedString("UTF8");
+        metadata.iTXt_translatedKeyword.add(translatedKeyword);
+
+        String text;
+        long pos = stream.getStreamPosition();
+        byte[] b = new byte[(int)(chunkStart + chunkLength - pos)];
+        stream.readFully(b);
+
+        if (compressionFlag == 1) { // Decompress the text
+            text = new String(inflate(b), "UTF8");
+        } else {
+            text = new String(b, "UTF8");
+        }
+        metadata.iTXt_text.add(text);
+    }
+
+    private void parse_pHYs_chunk() throws IOException {
+        metadata.pHYs_pixelsPerUnitXAxis = stream.readInt();
+        metadata.pHYs_pixelsPerUnitYAxis = stream.readInt();
+        metadata.pHYs_unitSpecifier = stream.readUnsignedByte();
+
+        metadata.pHYs_present = true;
+    }
+
+    private void parse_sBIT_chunk() throws IOException {
+        int colorType = metadata.IHDR_colorType;
+        if (colorType == PNG_COLOR_GRAY ||
+            colorType == PNG_COLOR_GRAY_ALPHA) {
+            metadata.sBIT_grayBits = stream.readUnsignedByte();
+        } else if (colorType == PNG_COLOR_RGB ||
+                   colorType == PNG_COLOR_PALETTE ||
+                   colorType == PNG_COLOR_RGB_ALPHA) {
+            metadata.sBIT_redBits = stream.readUnsignedByte();
+            metadata.sBIT_greenBits = stream.readUnsignedByte();
+            metadata.sBIT_blueBits = stream.readUnsignedByte();
+        }
+
+        if (colorType == PNG_COLOR_GRAY_ALPHA ||
+            colorType == PNG_COLOR_RGB_ALPHA) {
+            metadata.sBIT_alphaBits = stream.readUnsignedByte();
+        }
+
+        metadata.sBIT_colorType = colorType;
+        metadata.sBIT_present = true;
+    }
+
+    private void parse_sPLT_chunk(int chunkLength)
+        throws IOException, IIOException {
+        metadata.sPLT_paletteName = readNullTerminatedString();
+        chunkLength -= metadata.sPLT_paletteName.length() + 1;
+
+        int sampleDepth = stream.readUnsignedByte();
+        metadata.sPLT_sampleDepth = sampleDepth;
+
+        int numEntries = chunkLength/(4*(sampleDepth/8) + 2);
+        metadata.sPLT_red = new int[numEntries];
+        metadata.sPLT_green = new int[numEntries];
+        metadata.sPLT_blue = new int[numEntries];
+        metadata.sPLT_alpha = new int[numEntries];
+        metadata.sPLT_frequency = new int[numEntries];
+
+        if (sampleDepth == 8) {
+            for (int i = 0; i < numEntries; i++) {
+                metadata.sPLT_red[i] = stream.readUnsignedByte();
+                metadata.sPLT_green[i] = stream.readUnsignedByte();
+                metadata.sPLT_blue[i] = stream.readUnsignedByte();
+                metadata.sPLT_alpha[i] = stream.readUnsignedByte();
+                metadata.sPLT_frequency[i] = stream.readUnsignedShort();
+            }
+        } else if (sampleDepth == 16) {
+            for (int i = 0; i < numEntries; i++) {
+                metadata.sPLT_red[i] = stream.readUnsignedShort();
+                metadata.sPLT_green[i] = stream.readUnsignedShort();
+                metadata.sPLT_blue[i] = stream.readUnsignedShort();
+                metadata.sPLT_alpha[i] = stream.readUnsignedShort();
+                metadata.sPLT_frequency[i] = stream.readUnsignedShort();
+            }
+        } else {
+            throw new IIOException("sPLT sample depth not 8 or 16!");
+        }
+
+        metadata.sPLT_present = true;
+    }
+
+    private void parse_sRGB_chunk() throws IOException {
+        metadata.sRGB_renderingIntent = stream.readUnsignedByte();
+
+        metadata.sRGB_present = true;
+    }
+
+    private void parse_tEXt_chunk(int chunkLength) throws IOException {
+        String keyword = readNullTerminatedString();
+        metadata.tEXt_keyword.add(keyword);
+
+        byte[] b = new byte[chunkLength - keyword.length() - 1];
+        stream.readFully(b);
+        metadata.tEXt_text.add(new String(b));
+    }
+
+    private void parse_tIME_chunk() throws IOException {
+        metadata.tIME_year = stream.readUnsignedShort();
+        metadata.tIME_month = stream.readUnsignedByte();
+        metadata.tIME_day = stream.readUnsignedByte();
+        metadata.tIME_hour = stream.readUnsignedByte();
+        metadata.tIME_minute = stream.readUnsignedByte();
+        metadata.tIME_second = stream.readUnsignedByte();
+
+        metadata.tIME_present = true;
+    }
+
+    private void parse_tRNS_chunk(int chunkLength) throws IOException {
+        int colorType = metadata.IHDR_colorType;
+        if (colorType == PNG_COLOR_PALETTE) {
+            if (!metadata.PLTE_present) {
+                processWarningOccurred(
+"tRNS chunk without prior PLTE chunk, ignoring it.");
+                return;
+            }
+
+            // Alpha table may have fewer entries than RGB palette
+            int maxEntries = metadata.PLTE_red.length;
+            int numEntries = chunkLength;
+            if (numEntries > maxEntries) {
+                processWarningOccurred(
+"tRNS chunk has more entries than prior PLTE chunk, ignoring extras.");
+                numEntries = maxEntries;
+            }
+            metadata.tRNS_alpha = new byte[numEntries];
+            metadata.tRNS_colorType = PNG_COLOR_PALETTE;
+            stream.read(metadata.tRNS_alpha, 0, numEntries);
+            stream.skipBytes(chunkLength - numEntries);
+        } else if (colorType == PNG_COLOR_GRAY) {
+            if (chunkLength != 2) {
+                processWarningOccurred(
+"tRNS chunk for gray image must have length 2, ignoring chunk.");
+                stream.skipBytes(chunkLength);
+                return;
+            }
+            metadata.tRNS_gray = stream.readUnsignedShort();
+            metadata.tRNS_colorType = PNG_COLOR_GRAY;
+        } else if (colorType == PNG_COLOR_RGB) {
+            if (chunkLength != 6) {
+                processWarningOccurred(
+"tRNS chunk for RGB image must have length 6, ignoring chunk.");
+                stream.skipBytes(chunkLength);
+                return;
+            }
+            metadata.tRNS_red = stream.readUnsignedShort();
+            metadata.tRNS_green = stream.readUnsignedShort();
+            metadata.tRNS_blue = stream.readUnsignedShort();
+            metadata.tRNS_colorType = PNG_COLOR_RGB;
+        } else {
+            processWarningOccurred(
+"Gray+Alpha and RGBS images may not have a tRNS chunk, ignoring it.");
+            return;
+        }
+
+        metadata.tRNS_present = true;
+    }
+
+    private static byte[] inflate(byte[] b) throws IOException {
+        InputStream bais = new ByteArrayInputStream(b);
+        InputStream iis = new InflaterInputStream(bais);
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+        int c;
+        try {
+            while ((c = iis.read()) != -1) {
+                baos.write(c);
+            }
+        } finally {
+            iis.close();
+        }
+        return baos.toByteArray();
+    }
+
+    private void parse_zTXt_chunk(int chunkLength) throws IOException {
+        String keyword = readNullTerminatedString();
+        metadata.zTXt_keyword.add(keyword);
+
+        int method = stream.readUnsignedByte();
+        metadata.zTXt_compressionMethod.add(new Integer(method));
+
+        byte[] b = new byte[chunkLength - keyword.length() - 2];
+        stream.readFully(b);
+        metadata.zTXt_text.add(new String(inflate(b)));
+    }
+
+    private void readMetadata() throws IIOException {
+        if (gotMetadata) {
+            return;
+        }
+
+        readHeader();
+
+        /*
+         * Optimization: We can skip the remaining metadata if the
+         * ignoreMetadata flag is set, and only if this is not a palette
+         * image (in that case, we need to read the metadata to get the
+         * tRNS chunk, which is needed for the getImageTypes() method).
+         */
+        int colorType = metadata.IHDR_colorType;
+        if (ignoreMetadata && colorType != PNG_COLOR_PALETTE) {
+            try {
+                while (true) {
+                    int chunkLength = stream.readInt();
+                    int chunkType = stream.readInt();
+
+                    if (chunkType == IDAT_TYPE) {
+                        // We've reached the image data
+                        stream.skipBytes(-8);
+                        imageStartPosition = stream.getStreamPosition();
+                        break;
+                    } else {
+                        // Skip the chunk plus the 4 CRC bytes that follow
+                        stream.skipBytes(chunkLength + 4);
+                    }
+                }
+            } catch (IOException e) {
+                throw new IIOException("Error skipping PNG metadata", e);
+            }
+
+            gotMetadata = true;
+            return;
+        }
+
+        try {
+            loop: while (true) {
+                int chunkLength = stream.readInt();
+                int chunkType = stream.readInt();
+
+                switch (chunkType) {
+                case IDAT_TYPE:
+                    // If chunk type is 'IDAT', we've reached the image data.
+                    stream.skipBytes(-8);
+                    imageStartPosition = stream.getStreamPosition();
+                    break loop;
+                case PLTE_TYPE:
+                    parse_PLTE_chunk(chunkLength);
+                    break;
+                case bKGD_TYPE:
+                    parse_bKGD_chunk();
+                    break;
+                case cHRM_TYPE:
+                    parse_cHRM_chunk();
+                    break;
+                case gAMA_TYPE:
+                    parse_gAMA_chunk();
+                    break;
+                case hIST_TYPE:
+                    parse_hIST_chunk(chunkLength);
+                    break;
+                case iCCP_TYPE:
+                    parse_iCCP_chunk(chunkLength);
+                    break;
+                case iTXt_TYPE:
+                    parse_iTXt_chunk(chunkLength);
+                    break;
+                case pHYs_TYPE:
+                    parse_pHYs_chunk();
+                    break;
+                case sBIT_TYPE:
+                    parse_sBIT_chunk();
+                    break;
+                case sPLT_TYPE:
+                    parse_sPLT_chunk(chunkLength);
+                    break;
+                case sRGB_TYPE:
+                    parse_sRGB_chunk();
+                    break;
+                case tEXt_TYPE:
+                    parse_tEXt_chunk(chunkLength);
+                    break;
+                case tIME_TYPE:
+                    parse_tIME_chunk();
+                    break;
+                case tRNS_TYPE:
+                    parse_tRNS_chunk(chunkLength);
+                    break;
+                case zTXt_TYPE:
+                    parse_zTXt_chunk(chunkLength);
+                    break;
+                default:
+                    // Read an unknown chunk
+                    byte[] b = new byte[chunkLength];
+                    stream.readFully(b);
+
+                    StringBuilder chunkName = new StringBuilder(4);
+                    chunkName.append((char)(chunkType >>> 24));
+                    chunkName.append((char)((chunkType >> 16) & 0xff));
+                    chunkName.append((char)((chunkType >> 8) & 0xff));
+                    chunkName.append((char)(chunkType & 0xff));
+
+                    int ancillaryBit = chunkType >>> 28;
+                    if (ancillaryBit == 0) {
+                        processWarningOccurred(
+"Encountered unknown chunk with critical bit set!");
+                    }
+
+                    metadata.unknownChunkType.add(chunkName.toString());
+                    metadata.unknownChunkData.add(b);
+                    break;
+                }
+
+                int chunkCRC = stream.readInt();
+                stream.flushBefore(stream.getStreamPosition());
+            }
+        } catch (IOException e) {
+            throw new IIOException("Error reading PNG metadata", e);
+        }
+
+        gotMetadata = true;
+    }
+
+    // Data filtering methods
+
+    private static void decodeSubFilter(byte[] curr, int coff, int count,
+                                        int bpp) {
+        for (int i = bpp; i < count; i++) {
+            int val;
+
+            val = curr[i + coff] & 0xff;
+            val += curr[i + coff - bpp] & 0xff;
+
+            curr[i + coff] = (byte)val;
+        }
+    }
+
+    private static void decodeUpFilter(byte[] curr, int coff,
+                                       byte[] prev, int poff,
+                                       int count) {
+        for (int i = 0; i < count; i++) {
+            int raw = curr[i + coff] & 0xff;
+            int prior = prev[i + poff] & 0xff;
+
+            curr[i + coff] = (byte)(raw + prior);
+        }
+    }
+
+    private static void decodeAverageFilter(byte[] curr, int coff,
+                                            byte[] prev, int poff,
+                                            int count, int bpp) {
+        int raw, priorPixel, priorRow;
+
+        for (int i = 0; i < bpp; i++) {
+            raw = curr[i + coff] & 0xff;
+            priorRow = prev[i + poff] & 0xff;
+
+            curr[i + coff] = (byte)(raw + priorRow/2);
+        }
+
+        for (int i = bpp; i < count; i++) {
+            raw = curr[i + coff] & 0xff;
+            priorPixel = curr[i + coff - bpp] & 0xff;
+            priorRow = prev[i + poff] & 0xff;
+
+            curr[i + coff] = (byte)(raw + (priorPixel + priorRow)/2);
+        }
+    }
+
+    private static int paethPredictor(int a, int b, int c) {
+        int p = a + b - c;
+        int pa = Math.abs(p - a);
+        int pb = Math.abs(p - b);
+        int pc = Math.abs(p - c);
+
+        if ((pa <= pb) && (pa <= pc)) {
+            return a;
+        } else if (pb <= pc) {
+            return b;
+        } else {
+            return c;
+        }
+    }
+
+    private static void decodePaethFilter(byte[] curr, int coff,
+                                          byte[] prev, int poff,
+                                          int count, int bpp) {
+        int raw, priorPixel, priorRow, priorRowPixel;
+
+        for (int i = 0; i < bpp; i++) {
+            raw = curr[i + coff] & 0xff;
+            priorRow = prev[i + poff] & 0xff;
+
+            curr[i + coff] = (byte)(raw + priorRow);
+        }
+
+        for (int i = bpp; i < count; i++) {
+            raw = curr[i + coff] & 0xff;
+            priorPixel = curr[i + coff - bpp] & 0xff;
+            priorRow = prev[i + poff] & 0xff;
+            priorRowPixel = prev[i + poff - bpp] & 0xff;
+
+            curr[i + coff] = (byte)(raw + paethPredictor(priorPixel,
+                                                         priorRow,
+                                                         priorRowPixel));
+        }
+    }
+
+    private static final int[][] bandOffsets = {
+        null,
+        { 0 }, // G
+        { 0, 1 }, // GA in GA order
+        { 0, 1, 2 }, // RGB in RGB order
+        { 0, 1, 2, 3 } // RGBA in RGBA order
+    };
+
+    private WritableRaster createRaster(int width, int height, int bands,
+                                        int scanlineStride,
+                                        int bitDepth) {
+
+        DataBuffer dataBuffer;
+        WritableRaster ras = null;
+        Point origin = new Point(0, 0);
+        if ((bitDepth < 8) && (bands == 1)) {
+            dataBuffer = new DataBufferByte(height*scanlineStride);
+            ras = Raster.createPackedRaster(dataBuffer,
+                                            width, height,
+                                            bitDepth,
+                                            origin);
+        } else if (bitDepth <= 8) {
+            dataBuffer = new DataBufferByte(height*scanlineStride);
+            ras = Raster.createInterleavedRaster(dataBuffer,
+                                                 width, height,
+                                                 scanlineStride,
+                                                 bands,
+                                                 bandOffsets[bands],
+                                                 origin);
+        } else {
+            dataBuffer = new DataBufferUShort(height*scanlineStride);
+            ras = Raster.createInterleavedRaster(dataBuffer,
+                                                 width, height,
+                                                 scanlineStride,
+                                                 bands,
+                                                 bandOffsets[bands],
+                                                 origin);
+        }
+
+        return ras;
+    }
+
+    private void skipPass(int passWidth, int passHeight)
+        throws IOException, IIOException  {
+        if ((passWidth == 0) || (passHeight == 0)) {
+            return;
+        }
+
+        int inputBands = inputBandsForColorType[metadata.IHDR_colorType];
+        int bytesPerRow = (inputBands*passWidth*metadata.IHDR_bitDepth + 7)/8;
+
+        // Read the image row-by-row
+        for (int srcY = 0; srcY < passHeight; srcY++) {
+            // Skip filter byte and the remaining row bytes
+            pixelStream.skipBytes(1 + bytesPerRow);
+
+            // If read has been aborted, just return
+            // processReadAborted will be called later
+            if (abortRequested()) {
+                return;
+            }
+        }
+    }
+
+    private void updateImageProgress(int newPixels) {
+        pixelsDone += newPixels;
+        processImageProgress(100.0F*pixelsDone/totalPixels);
+    }
+
+    private void decodePass(int passNum,
+                            int xStart, int yStart,
+                            int xStep, int yStep,
+                            int passWidth, int passHeight) throws IOException {
+
+        if ((passWidth == 0) || (passHeight == 0)) {
+            return;
+        }
+
+        WritableRaster imRas = theImage.getWritableTile(0, 0);
+        int dstMinX = imRas.getMinX();
+        int dstMaxX = dstMinX + imRas.getWidth() - 1;
+        int dstMinY = imRas.getMinY();
+        int dstMaxY = dstMinY + imRas.getHeight() - 1;
+
+        // Determine which pixels will be updated in this pass
+        int[] vals =
+          ReaderUtil.computeUpdatedPixels(sourceRegion,
+                                          destinationOffset,
+                                          dstMinX, dstMinY,
+                                          dstMaxX, dstMaxY,
+                                          sourceXSubsampling,
+                                          sourceYSubsampling,
+                                          xStart, yStart,
+                                          passWidth, passHeight,
+                                          xStep, yStep);
+        int updateMinX = vals[0];
+        int updateMinY = vals[1];
+        int updateWidth = vals[2];
+        int updateXStep = vals[4];
+        int updateYStep = vals[5];
+
+        int bitDepth = metadata.IHDR_bitDepth;
+        int inputBands = inputBandsForColorType[metadata.IHDR_colorType];
+        int bytesPerPixel = (bitDepth == 16) ? 2 : 1;
+        bytesPerPixel *= inputBands;
+
+        int bytesPerRow = (inputBands*passWidth*bitDepth + 7)/8;
+        int eltsPerRow = (bitDepth == 16) ? bytesPerRow/2 : bytesPerRow;
+
+        // If no pixels need updating, just skip the input data
+        if (updateWidth == 0) {
+            for (int srcY = 0; srcY < passHeight; srcY++) {
+                // Update count of pixels read
+                updateImageProgress(passWidth);
+                // Skip filter byte and the remaining row bytes
+                pixelStream.skipBytes(1 + bytesPerRow);
+            }
+            return;
+        }
+
+        // Backwards map from destination pixels
+        // (dstX = updateMinX + k*updateXStep)
+        // to source pixels (sourceX), and then
+        // to offset and skip in passRow (srcX and srcXStep)
+        int sourceX =
+            (updateMinX - destinationOffset.x)*sourceXSubsampling +
+            sourceRegion.x;
+        int srcX = (sourceX - xStart)/xStep;
+
+        // Compute the step factor in the source
+        int srcXStep = updateXStep*sourceXSubsampling/xStep;
+
+        byte[] byteData = null;
+        short[] shortData = null;
+        byte[] curr = new byte[bytesPerRow];
+        byte[] prior = new byte[bytesPerRow];
+
+        // Create a 1-row tall Raster to hold the data
+        WritableRaster passRow = createRaster(passWidth, 1, inputBands,
+                                              eltsPerRow,
+                                              bitDepth);
+
+        // Create an array suitable for holding one pixel
+        int[] ps = passRow.getPixel(0, 0, (int[])null);
+
+        DataBuffer dataBuffer = passRow.getDataBuffer();
+        int type = dataBuffer.getDataType();
+        if (type == DataBuffer.TYPE_BYTE) {
+            byteData = ((DataBufferByte)dataBuffer).getData();
+        } else {
+            shortData = ((DataBufferUShort)dataBuffer).getData();
+        }
+
+        processPassStarted(theImage,
+                           passNum,
+                           sourceMinProgressivePass,
+                           sourceMaxProgressivePass,
+                           updateMinX, updateMinY,
+                           updateXStep, updateYStep,
+                           destinationBands);
+
+        // Handle source and destination bands
+        if (sourceBands != null) {
+            passRow = passRow.createWritableChild(0, 0,
+                                                  passRow.getWidth(), 1,
+                                                  0, 0,
+                                                  sourceBands);
+        }
+        if (destinationBands != null) {
+            imRas = imRas.createWritableChild(0, 0,
+                                              imRas.getWidth(),
+                                              imRas.getHeight(),
+                                              0, 0,
+                                              destinationBands);
+        }
+
+        // Determine if all of the relevant output bands have the
+        // same bit depth as the source data
+        boolean adjustBitDepths = false;
+        int[] outputSampleSize = imRas.getSampleModel().getSampleSize();
+        int numBands = outputSampleSize.length;
+        for (int b = 0; b < numBands; b++) {
+            if (outputSampleSize[b] != bitDepth) {
+                adjustBitDepths = true;
+                break;
+            }
+        }
+
+        // If the bit depths differ, create a lookup table per band to perform
+        // the conversion
+        int[][] scale = null;
+        if (adjustBitDepths) {
+            int maxInSample = (1 << bitDepth) - 1;
+            int halfMaxInSample = maxInSample/2;
+            scale = new int[numBands][];
+            for (int b = 0; b < numBands; b++) {
+                int maxOutSample = (1 << outputSampleSize[b]) - 1;
+                scale[b] = new int[maxInSample + 1];
+                for (int s = 0; s <= maxInSample; s++) {
+                    scale[b][s] =
+                        (s*maxOutSample + halfMaxInSample)/maxInSample;
+                }
+            }
+        }
+
+        // Limit passRow to relevant area for the case where we
+        // will can setRect to copy a contiguous span
+        boolean useSetRect = srcXStep == 1 &&
+            updateXStep == 1 &&
+            !adjustBitDepths &&
+            (imRas instanceof ByteInterleavedRaster);
+
+        if (useSetRect) {
+            passRow = passRow.createWritableChild(srcX, 0,
+                                                  updateWidth, 1,
+                                                  0, 0,
+                                                  null);
+        }
+
+        // Decode the (sub)image row-by-row
+        for (int srcY = 0; srcY < passHeight; srcY++) {
+            // Update count of pixels read
+            updateImageProgress(passWidth);
+
+            // Read the filter type byte and a row of data
+            int filter = pixelStream.read();
+            try {
+                // Swap curr and prior
+                byte[] tmp = prior;
+                prior = curr;
+                curr = tmp;
+
+                pixelStream.readFully(curr, 0, bytesPerRow);
+            } catch (java.util.zip.ZipException ze) {
+                // TODO - throw a more meaningful exception
+                throw ze;
+            }
+
+            switch (filter) {
+            case PNG_FILTER_NONE:
+                break;
+            case PNG_FILTER_SUB:
+                decodeSubFilter(curr, 0, bytesPerRow, bytesPerPixel);
+                break;
+            case PNG_FILTER_UP:
+                decodeUpFilter(curr, 0, prior, 0, bytesPerRow);
+                break;
+            case PNG_FILTER_AVERAGE:
+                decodeAverageFilter(curr, 0, prior, 0, bytesPerRow,
+                                    bytesPerPixel);
+                break;
+            case PNG_FILTER_PAETH:
+                decodePaethFilter(curr, 0, prior, 0, bytesPerRow,
+                                  bytesPerPixel);
+                break;
+            default:
+                throw new IIOException("Unknown row filter type (= " +
+                                       filter + ")!");
+            }
+
+            // Copy data into passRow byte by byte
+            if (bitDepth < 16) {
+                System.arraycopy(curr, 0, byteData, 0, bytesPerRow);
+            } else {
+                int idx = 0;
+                for (int j = 0; j < eltsPerRow; j++) {
+                    shortData[j] =
+                        (short)((curr[idx] << 8) | (curr[idx + 1] & 0xff));
+                    idx += 2;
+                }
+            }
+
+            // True Y position in source
+            int sourceY = srcY*yStep + yStart;
+            if ((sourceY >= sourceRegion.y) &&
+                (sourceY < sourceRegion.y + sourceRegion.height) &&
+                (((sourceY - sourceRegion.y) %
+                  sourceYSubsampling) == 0)) {
+
+                int dstY = destinationOffset.y +
+                    (sourceY - sourceRegion.y)/sourceYSubsampling;
+                if (dstY < dstMinY) {
+                    continue;
+                }
+                if (dstY > dstMaxY) {
+                    break;
+                }
+
+                if (useSetRect) {
+                    imRas.setRect(updateMinX, dstY, passRow);
+                } else {
+                    int newSrcX = srcX;
+
+                    for (int dstX = updateMinX;
+                         dstX < updateMinX + updateWidth;
+                         dstX += updateXStep) {
+
+                        passRow.getPixel(newSrcX, 0, ps);
+                        if (adjustBitDepths) {
+                            for (int b = 0; b < numBands; b++) {
+                                ps[b] = scale[b][ps[b]];
+                            }
+                        }
+                        imRas.setPixel(dstX, dstY, ps);
+                        newSrcX += srcXStep;
+                    }
+                }
+
+                processImageUpdate(theImage,
+                                   updateMinX, dstY,
+                                   updateWidth, 1,
+                                   updateXStep, updateYStep,
+                                   destinationBands);
+
+                // If read has been aborted, just return
+                // processReadAborted will be called later
+                if (abortRequested()) {
+                    return;
+                }
+            }
+        }
+
+        processPassComplete(theImage);
+    }
+
+    private void decodeImage()
+        throws IOException, IIOException  {
+        int width = metadata.IHDR_width;
+        int height = metadata.IHDR_height;
+
+        this.pixelsDone = 0;
+        this.totalPixels = width*height;
+
+        clearAbortRequest();
+
+        if (metadata.IHDR_interlaceMethod == 0) {
+            decodePass(0, 0, 0, 1, 1, width, height);
+        } else {
+            for (int i = 0; i <= sourceMaxProgressivePass; i++) {
+                int XOffset = adam7XOffset[i];
+                int YOffset = adam7YOffset[i];
+                int XSubsampling = adam7XSubsampling[i];
+                int YSubsampling = adam7YSubsampling[i];
+                int xbump = adam7XSubsampling[i + 1] - 1;
+                int ybump = adam7YSubsampling[i + 1] - 1;
+
+                if (i >= sourceMinProgressivePass) {
+                    decodePass(i,
+                               XOffset,
+                               YOffset,
+                               XSubsampling,
+                               YSubsampling,
+                               (width + xbump)/XSubsampling,
+                               (height + ybump)/YSubsampling);
+                } else {
+                    skipPass((width + xbump)/XSubsampling,
+                             (height + ybump)/YSubsampling);
+                }
+
+                // If read has been aborted, just return
+                // processReadAborted will be called later
+                if (abortRequested()) {
+                    return;
+                }
+            }
+        }
+    }
+
+    private void readImage(ImageReadParam param) throws IIOException {
+        readMetadata();
+
+        int width = metadata.IHDR_width;
+        int height = metadata.IHDR_height;
+
+        // Init default values
+        sourceXSubsampling = 1;
+        sourceYSubsampling = 1;
+        sourceMinProgressivePass = 0;
+        sourceMaxProgressivePass = 6;
+        sourceBands = null;
+        destinationBands = null;
+        destinationOffset = new Point(0, 0);
+
+        // If an ImageReadParam is available, get values from it
+        if (param != null) {
+            sourceXSubsampling = param.getSourceXSubsampling();
+            sourceYSubsampling = param.getSourceYSubsampling();
+
+            sourceMinProgressivePass =
+                Math.max(param.getSourceMinProgressivePass(), 0);
+            sourceMaxProgressivePass =
+                Math.min(param.getSourceMaxProgressivePass(), 6);
+
+            sourceBands = param.getSourceBands();
+            destinationBands = param.getDestinationBands();
+            destinationOffset = param.getDestinationOffset();
+        }
+        Inflater inf = null;
+        try {
+            stream.seek(imageStartPosition);
+
+            Enumeration e = new PNGImageDataEnumeration(stream);
+            InputStream is = new SequenceInputStream(e);
+
+           /* InflaterInputStream uses an Inflater instance which consumes
+            * native (non-GC visible) resources. This is normally implicitly
+            * freed when the stream is closed. However since the
+            * InflaterInputStream wraps a client-supplied input stream,
+            * we cannot close it.
+            * But the app may depend on GC finalization to close the stream.
+            * Therefore to ensure timely freeing of native resources we
+            * explicitly create the Inflater instance and free its resources
+            * when we are done with the InflaterInputStream by calling
+            * inf.end();
+            */
+            inf = new Inflater();
+            is = new InflaterInputStream(is, inf);
+            is = new BufferedInputStream(is);
+            this.pixelStream = new DataInputStream(is);
+
+            theImage = getDestination(param,
+                                      getImageTypes(0),
+                                      width,
+                                      height);
+
+            Rectangle destRegion = new Rectangle(0, 0, 0, 0);
+            sourceRegion = new Rectangle(0, 0, 0, 0);
+            computeRegions(param, width, height,
+                           theImage,
+                           sourceRegion, destRegion);
+            destinationOffset.setLocation(destRegion.getLocation());
+
+            // At this point the header has been read and we know
+            // how many bands are in the image, so perform checking
+            // of the read param.
+            int colorType = metadata.IHDR_colorType;
+            checkReadParamBandSettings(param,
+                                       inputBandsForColorType[colorType],
+                                      theImage.getSampleModel().getNumBands());
+
+            processImageStarted(0);
+            decodeImage();
+            if (abortRequested()) {
+                processReadAborted();
+            } else {
+                processImageComplete();
+            }
+        } catch (IOException e) {
+            throw new IIOException("Error reading PNG image data", e);
+        } finally {
+            if (inf != null) {
+                inf.end();
+            }
+        }
+    }
+
+    public int getNumImages(boolean allowSearch) throws IIOException {
+        if (stream == null) {
+            throw new IllegalStateException("No input source set!");
+        }
+        if (seekForwardOnly && allowSearch) {
+            throw new IllegalStateException
+                ("seekForwardOnly and allowSearch can't both be true!");
+        }
+        return 1;
+    }
+
+    public int getWidth(int imageIndex) throws IIOException {
+        if (imageIndex != 0) {
+            throw new IndexOutOfBoundsException("imageIndex != 0!");
+        }
+
+        readHeader();
+
+        return metadata.IHDR_width;
+    }
+
+    public int getHeight(int imageIndex) throws IIOException {
+        if (imageIndex != 0) {
+            throw new IndexOutOfBoundsException("imageIndex != 0!");
+        }
+
+        readHeader();
+
+        return metadata.IHDR_height;
+    }
+
+    public Iterator<ImageTypeSpecifier> getImageTypes(int imageIndex)
+      throws IIOException
+    {
+        if (imageIndex != 0) {
+            throw new IndexOutOfBoundsException("imageIndex != 0!");
+        }
+
+        readHeader();
+
+        ArrayList<ImageTypeSpecifier> l =
+            new ArrayList<ImageTypeSpecifier>(1);
+
+        ColorSpace rgb;
+        ColorSpace gray;
+        int[] bandOffsets;
+
+        int bitDepth = metadata.IHDR_bitDepth;
+        int colorType = metadata.IHDR_colorType;
+
+        int dataType;
+        if (bitDepth <= 8) {
+            dataType = DataBuffer.TYPE_BYTE;
+        } else {
+            dataType = DataBuffer.TYPE_USHORT;
+        }
+
+        switch (colorType) {
+        case PNG_COLOR_GRAY:
+            // Packed grayscale
+            l.add(ImageTypeSpecifier.createGrayscale(bitDepth,
+                                                     dataType,
+                                                     false));
+            break;
+
+        case PNG_COLOR_RGB:
+            if (bitDepth == 8) {
+                // some standard types of buffered images
+                // which can be used as destination
+                l.add(ImageTypeSpecifier.createFromBufferedImageType(
+                          BufferedImage.TYPE_3BYTE_BGR));
+
+                l.add(ImageTypeSpecifier.createFromBufferedImageType(
+                          BufferedImage.TYPE_INT_RGB));
+
+                l.add(ImageTypeSpecifier.createFromBufferedImageType(
+                          BufferedImage.TYPE_INT_BGR));
+
+            }
+            // Component R, G, B
+            rgb = ColorSpace.getInstance(ColorSpace.CS_sRGB);
+            bandOffsets = new int[3];
+            bandOffsets[0] = 0;
+            bandOffsets[1] = 1;
+            bandOffsets[2] = 2;
+            l.add(ImageTypeSpecifier.createInterleaved(rgb,
+                                                       bandOffsets,
+                                                       dataType,
+                                                       false,
+                                                       false));
+            break;
+
+        case PNG_COLOR_PALETTE:
+            readMetadata(); // Need tRNS chunk
+
+            /*
+             * The PLTE chunk spec says:
+             *
+             * The number of palette entries must not exceed the range that
+             * can be represented in the image bit depth (for example, 2^4 = 16
+             * for a bit depth of 4). It is permissible to have fewer entries
+             * than the bit depth would allow. In that case, any out-of-range
+             * pixel value found in the image data is an error.
+             *
+             * http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html#C.PLTE
+             *
+             * Consequently, the case when the palette length is smaller than
+             * 2^bitDepth is legal in the view of PNG spec.
+             *
+             * However the spec of createIndexed() method demands the exact
+             * equality of the palette lengh and number of possible palette
+             * entries (2^bitDepth).
+             *
+             * {@link javax.imageio.ImageTypeSpecifier.html#createIndexed}
+             *
+             * In order to avoid this contradiction we need to extend the
+             * palette arrays to the limit defined by the bitDepth.
+             */
+
+            int plength = 1 << bitDepth;
+
+            byte[] red = metadata.PLTE_red;
+            byte[] green = metadata.PLTE_green;
+            byte[] blue = metadata.PLTE_blue;
+
+            if (metadata.PLTE_red.length < plength) {
+                red = Arrays.copyOf(metadata.PLTE_red, plength);
+                Arrays.fill(red, metadata.PLTE_red.length, plength,
+                            metadata.PLTE_red[metadata.PLTE_red.length - 1]);
+
+                green = Arrays.copyOf(metadata.PLTE_green, plength);
+                Arrays.fill(green, metadata.PLTE_green.length, plength,
+                            metadata.PLTE_green[metadata.PLTE_green.length - 1]);
+
+                blue = Arrays.copyOf(metadata.PLTE_blue, plength);
+                Arrays.fill(blue, metadata.PLTE_blue.length, plength,
+                            metadata.PLTE_blue[metadata.PLTE_blue.length - 1]);
+
+            }
+
+            // Alpha from tRNS chunk may have fewer entries than
+            // the RGB LUTs from the PLTE chunk; if so, pad with
+            // 255.
+            byte[] alpha = null;
+            if (metadata.tRNS_present && (metadata.tRNS_alpha != null)) {
+                if (metadata.tRNS_alpha.length == red.length) {
+                    alpha = metadata.tRNS_alpha;
+                } else {
+                    alpha = Arrays.copyOf(metadata.tRNS_alpha, red.length);
+                    Arrays.fill(alpha,
+                                metadata.tRNS_alpha.length,
+                                red.length, (byte)255);
+                }
+            }
+
+            l.add(ImageTypeSpecifier.createIndexed(red, green,
+                                                   blue, alpha,
+                                                   bitDepth,
+                                                   DataBuffer.TYPE_BYTE));
+            break;
+
+        case PNG_COLOR_GRAY_ALPHA:
+            // Component G, A
+            gray = ColorSpace.getInstance(ColorSpace.CS_GRAY);
+            bandOffsets = new int[2];
+            bandOffsets[0] = 0;
+            bandOffsets[1] = 1;
+            l.add(ImageTypeSpecifier.createInterleaved(gray,
+                                                       bandOffsets,
+                                                       dataType,
+                                                       true,
+                                                       false));
+            break;
+
+        case PNG_COLOR_RGB_ALPHA:
+            if (bitDepth == 8) {
+                // some standard types of buffered images
+                // wich can be used as destination
+                l.add(ImageTypeSpecifier.createFromBufferedImageType(
+                          BufferedImage.TYPE_4BYTE_ABGR));
+
+                l.add(ImageTypeSpecifier.createFromBufferedImageType(
+                          BufferedImage.TYPE_INT_ARGB));
+            }
+
+            // Component R, G, B, A (non-premultiplied)
+            rgb = ColorSpace.getInstance(ColorSpace.CS_sRGB);
+            bandOffsets = new int[4];
+            bandOffsets[0] = 0;
+            bandOffsets[1] = 1;
+            bandOffsets[2] = 2;
+            bandOffsets[3] = 3;
+
+            l.add(ImageTypeSpecifier.createInterleaved(rgb,
+                                                       bandOffsets,
+                                                       dataType,
+                                                       true,
+                                                       false));
+            break;
+
+        default:
+            break;
+        }
+
+        return l.iterator();
+    }
+
+    /*
+     * Super class implementation uses first element
+     * of image types list as raw image type.
+     *
+     * Also, super implementation uses first element of this list
+     * as default destination type image read param does not specify
+     * anything other.
+     *
+     * However, in case of RGB and RGBA color types, raw image type
+     * produces buffered image of custom type. It causes some
+     * performance degradation of subsequent rendering operations.
+     *
+     * To resolve this contradiction we put standard image types
+     * at the first positions of image types list (to produce standard
+     * images by default) and put raw image type (which is custom)
+     * at the last position of this list.
+     *
+     * After this changes we should override getRawImageType()
+     * to return last element of image types list.
+     */
+    public ImageTypeSpecifier getRawImageType(int imageIndex)
+      throws IOException {
+
+        Iterator<ImageTypeSpecifier> types = getImageTypes(imageIndex);
+        ImageTypeSpecifier raw = null;
+        do {
+            raw = types.next();
+        } while (types.hasNext());
+        return raw;
+    }
+
+    public ImageReadParam getDefaultReadParam() {
+        return new ImageReadParam();
+    }
+
+    public IIOMetadata getStreamMetadata()
+        throws IIOException {
+        return null;
+    }
+
+    public IIOMetadata getImageMetadata(int imageIndex) throws IIOException {
+        if (imageIndex != 0) {
+            throw new IndexOutOfBoundsException("imageIndex != 0!");
+        }
+        readMetadata();
+        return metadata;
+    }
+
+    public BufferedImage read(int imageIndex, ImageReadParam param)
+        throws IIOException {
+        if (imageIndex != 0) {
+            throw new IndexOutOfBoundsException("imageIndex != 0!");
+        }
+
+        readImage(param);
+        return theImage;
+    }
+
+    public void reset() {
+        super.reset();
+        resetStreamSettings();
+    }
+
+    private void resetStreamSettings() {
+        gotHeader = false;
+        gotMetadata = false;
+        metadata = null;
+        pixelStream = null;
+    }
+}
diff -urN openjdk.orig/jdk/src/share/classes/com/sun/imageio/plugins/png/PNGImageWriter.java openjdk/jdk/src/share/classes/com/sun/imageio/plugins/png/PNGImageWriter.java
--- openjdk.orig/jdk/src/share/classes/com/sun/imageio/plugins/png/PNGImageWriter.java	2010-12-24 11:22:28.803101963 -0500
+++ openjdk/jdk/src/share/classes/com/sun/imageio/plugins/png/PNGImageWriter.java	2010-12-24 11:24:47.273101870 -0500
@@ -674,13 +674,8 @@
     private byte[] deflate(byte[] b) throws IOException {
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
         DeflaterOutputStream dos = new DeflaterOutputStream(baos);
-
-        int len = b.length;
-        for (int i = 0; i < len; i++) {
-            dos.write((int)(0xff & b[i]));
-        }
+        dos.write(b);
         dos.close();
-
         return baos.toByteArray();
     }
 
@@ -736,7 +731,7 @@
             cs.writeByte(compressionMethod);
 
             String text = (String)textIter.next();
-            cs.write(deflate(text.getBytes()));
+            cs.write(deflate(text.getBytes("ISO-8859-1")));
             cs.finish();
         }
     }
diff -urN openjdk.orig/jdk/src/share/classes/com/sun/imageio/plugins/png/PNGMetadata.java openjdk/jdk/src/share/classes/com/sun/imageio/plugins/png/PNGMetadata.java
--- openjdk.orig/jdk/src/share/classes/com/sun/imageio/plugins/png/PNGMetadata.java	2010-12-24 11:22:28.803101963 -0500
+++ openjdk/jdk/src/share/classes/com/sun/imageio/plugins/png/PNGMetadata.java	2010-12-24 11:24:47.274101892 -0500
@@ -213,8 +213,8 @@
     public int sRGB_renderingIntent;
 
     // tEXt chunk
-    public ArrayList tEXt_keyword = new ArrayList(); // 1-79 char Strings
-    public ArrayList tEXt_text = new ArrayList(); // Strings
+    public ArrayList<String> tEXt_keyword = new ArrayList<String>(); // 1-79 characters
+    public ArrayList<String> tEXt_text = new ArrayList<String>();
 
     // tIME chunk
     public boolean tIME_present;
@@ -237,13 +237,13 @@
     public int tRNS_blue;
 
     // zTXt chunk
-    public ArrayList zTXt_keyword = new ArrayList(); // Strings
-    public ArrayList zTXt_compressionMethod = new ArrayList(); // Integers
-    public ArrayList zTXt_text = new ArrayList(); // Strings
+    public ArrayList<String> zTXt_keyword = new ArrayList<String>();
+    public ArrayList<Integer> zTXt_compressionMethod = new ArrayList<Integer>();
+    public ArrayList<String> zTXt_text = new ArrayList<String>();
 
     // Unknown chunks
-    public ArrayList unknownChunkType = new ArrayList(); // Strings
-    public ArrayList unknownChunkData = new ArrayList(); // byte arrays
+    public ArrayList<String> unknownChunkType = new ArrayList<String>();
+    public ArrayList<byte[]> unknownChunkData = new ArrayList<byte[]>();
 
     public PNGMetadata() {
         super(true,
@@ -428,21 +428,14 @@
         return false;
     }
 
-    private ArrayList cloneBytesArrayList(ArrayList in) {
+    private ArrayList<byte[]> cloneBytesArrayList(ArrayList<byte[]> in) {
         if (in == null) {
             return null;
         } else {
-            ArrayList list = new ArrayList(in.size());
-            Iterator iter = in.iterator();
-            while (iter.hasNext()) {
-                Object o = iter.next();
-                if (o == null) {
-                    list.add(null);
-                } else {
-                    list.add(((byte[])o).clone());
-                }
+            ArrayList<byte[]> list = new ArrayList<byte[]>(in.size());
+            for (byte[] b: in) {
+                list.add((b == null) ? null : (byte[])b.clone());
             }
-
             return list;
         }
     }
@@ -2000,14 +1993,14 @@
         sBIT_present = false;
         sPLT_present = false;
         sRGB_present = false;
-        tEXt_keyword = new ArrayList();
-        tEXt_text = new ArrayList();
+        tEXt_keyword = new ArrayList<String>();
+        tEXt_text = new ArrayList<String>();
         tIME_present = false;
         tRNS_present = false;
-        zTXt_keyword = new ArrayList();
-        zTXt_compressionMethod = new ArrayList();
-        zTXt_text = new ArrayList();
-        unknownChunkType = new ArrayList();
-        unknownChunkData = new ArrayList();
+        zTXt_keyword = new ArrayList<String>();
+        zTXt_compressionMethod = new ArrayList<Integer>();
+        zTXt_text = new ArrayList<String>();
+        unknownChunkType = new ArrayList<String>();
+        unknownChunkData = new ArrayList<byte[]>();
     }
 }
diff -urN openjdk.orig/jdk/src/share/classes/com/sun/imageio/plugins/png/PNGMetadata.java.orig openjdk/jdk/src/share/classes/com/sun/imageio/plugins/png/PNGMetadata.java.orig
--- openjdk.orig/jdk/src/share/classes/com/sun/imageio/plugins/png/PNGMetadata.java.orig	1969-12-31 19:00:00.000000000 -0500
+++ openjdk/jdk/src/share/classes/com/sun/imageio/plugins/png/PNGMetadata.java.orig	2010-12-24 11:22:50.401102014 -0500
@@ -0,0 +1,2013 @@
+/*
+ * Copyright (c) 2000, 2001, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.sun.imageio.plugins.png;
+
+import java.awt.image.ColorModel;
+import java.awt.image.IndexColorModel;
+import java.awt.image.SampleModel;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.StringTokenizer;
+import javax.imageio.ImageTypeSpecifier;
+import javax.imageio.metadata.IIOInvalidTreeException;
+import javax.imageio.metadata.IIOMetadata;
+import javax.imageio.metadata.IIOMetadataFormat;
+import javax.imageio.metadata.IIOMetadataFormatImpl;
+import javax.imageio.metadata.IIOMetadataNode;
+import org.w3c.dom.Node;
+
+/**
+ */
+public class PNGMetadata extends IIOMetadata implements Cloneable {
+
+    // package scope
+    public static final String
+        nativeMetadataFormatName = "javax_imageio_png_1.0";
+
+    protected static final String nativeMetadataFormatClassName
+        = "com.sun.imageio.plugins.png.PNGMetadataFormat";
+
+    // Color types for IHDR chunk
+    public static final String[] IHDR_colorTypeNames = {
+        "Grayscale", null, "RGB", "Palette",
+        "GrayAlpha", null, "RGBAlpha"
+    };
+
+    public static final int[] IHDR_numChannels = {
+        1, 0, 3, 3, 2, 0, 4
+    };
+
+    // Bit depths for IHDR chunk
+    public static final String[] IHDR_bitDepths = {
+        "1", "2", "4", "8", "16"
+    };
+
+    // Compression methods for IHDR chunk
+    public static final String[] IHDR_compressionMethodNames = {
+        "deflate"
+    };
+
+    // Filter methods for IHDR chunk
+    public static final String[] IHDR_filterMethodNames = {
+        "adaptive"
+    };
+
+    // Interlace methods for IHDR chunk
+    public static final String[] IHDR_interlaceMethodNames = {
+        "none", "adam7"
+    };
+
+    // Compression methods for iCCP chunk
+    public static final String[] iCCP_compressionMethodNames = {
+        "deflate"
+    };
+
+    // Compression methods for zTXt chunk
+    public static final String[] zTXt_compressionMethodNames = {
+        "deflate"
+    };
+
+    // "Unknown" unit for pHYs chunk
+    public static final int PHYS_UNIT_UNKNOWN = 0;
+
+    // "Meter" unit for pHYs chunk
+    public static final int PHYS_UNIT_METER = 1;
+
+    // Unit specifiers for pHYs chunk
+    public static final String[] unitSpecifierNames = {
+        "unknown", "meter"
+    };
+
+    // Rendering intents for sRGB chunk
+    public static final String[] renderingIntentNames = {
+        "Perceptual", // 0
+        "Relative colorimetric", // 1
+        "Saturation", // 2
+        "Absolute colorimetric" // 3
+
+    };
+
+    // Color space types for Chroma->ColorSpaceType node
+    public static final String[] colorSpaceTypeNames = {
+        "GRAY", null, "RGB", "RGB",
+        "GRAY", null, "RGB"
+    };
+
+    // IHDR chunk
+    public boolean IHDR_present;
+    public int IHDR_width;
+    public int IHDR_height;
+    public int IHDR_bitDepth;
+    public int IHDR_colorType;
+    public int IHDR_compressionMethod;
+    public int IHDR_filterMethod;
+    public int IHDR_interlaceMethod; // 0 == none, 1 == adam7
+
+    // PLTE chunk
+    public boolean PLTE_present;
+    public byte[] PLTE_red;
+    public byte[] PLTE_green;
+    public byte[] PLTE_blue;
+
+    // If non-null, used to reorder palette entries during encoding in
+    // order to minimize the size of the tRNS chunk.  Thus an index of
+    // 'i' in the source should be encoded as index 'PLTE_order[i]'.
+    // PLTE_order will be null unless 'initialize' is called with an
+    // IndexColorModel image type.
+    public int[] PLTE_order = null;
+
+    // bKGD chunk
+    // If external (non-PNG sourced) data has red = green = blue,
+    // always store it as gray and promote when writing
+    public boolean bKGD_present;
+    public int bKGD_colorType; // PNG_COLOR_GRAY, _RGB, or _PALETTE
+    public int bKGD_index;
+    public int bKGD_gray;
+    public int bKGD_red;
+    public int bKGD_green;
+    public int bKGD_blue;
+
+    // cHRM chunk
+    public boolean cHRM_present;
+    public int cHRM_whitePointX;
+    public int cHRM_whitePointY;
+    public int cHRM_redX;
+    public int cHRM_redY;
+    public int cHRM_greenX;
+    public int cHRM_greenY;
+    public int cHRM_blueX;
+    public int cHRM_blueY;
+
+    // gAMA chunk
+    public boolean gAMA_present;
+    public int gAMA_gamma;
+
+    // hIST chunk
+    public boolean hIST_present;
+    public char[] hIST_histogram;
+
+    // iCCP chunk
+    public boolean iCCP_present;
+    public String iCCP_profileName;
+    public int iCCP_compressionMethod;
+    public byte[] iCCP_compressedProfile;
+
+    // iTXt chunk
+    public ArrayList<String> iTXt_keyword = new ArrayList<String>();
+    public ArrayList<Boolean> iTXt_compressionFlag = new ArrayList<Boolean>();
+    public ArrayList<Integer> iTXt_compressionMethod = new ArrayList<Integer>();
+    public ArrayList<String> iTXt_languageTag = new ArrayList<String>();
+    public ArrayList<String> iTXt_translatedKeyword = new ArrayList<String>();
+    public ArrayList<String> iTXt_text = new ArrayList<String>();
+
+    // pHYs chunk
+    public boolean pHYs_present;
+    public int pHYs_pixelsPerUnitXAxis;
+    public int pHYs_pixelsPerUnitYAxis;
+    public int pHYs_unitSpecifier; // 0 == unknown, 1 == meter
+
+    // sBIT chunk
+    public boolean sBIT_present;
+    public int sBIT_colorType; // PNG_COLOR_GRAY, _GRAY_ALPHA, _RGB, _RGB_ALPHA
+    public int sBIT_grayBits;
+    public int sBIT_redBits;
+    public int sBIT_greenBits;
+    public int sBIT_blueBits;
+    public int sBIT_alphaBits;
+
+    // sPLT chunk
+    public boolean sPLT_present;
+    public String sPLT_paletteName; // 1-79 characters
+    public int sPLT_sampleDepth; // 8 or 16
+    public int[] sPLT_red;
+    public int[] sPLT_green;
+    public int[] sPLT_blue;
+    public int[] sPLT_alpha;
+    public int[] sPLT_frequency;
+
+    // sRGB chunk
+    public boolean sRGB_present;
+    public int sRGB_renderingIntent;
+
+    // tEXt chunk
+    public ArrayList tEXt_keyword = new ArrayList(); // 1-79 char Strings
+    public ArrayList tEXt_text = new ArrayList(); // Strings
+
+    // tIME chunk
+    public boolean tIME_present;
+    public int tIME_year;
+    public int tIME_month;
+    public int tIME_day;
+    public int tIME_hour;
+    public int tIME_minute;
+    public int tIME_second;
+
+    // tRNS chunk
+    // If external (non-PNG sourced) data has red = green = blue,
+    // always store it as gray and promote when writing
+    public boolean tRNS_present;
+    public int tRNS_colorType; // PNG_COLOR_GRAY, _RGB, or _PALETTE
+    public byte[] tRNS_alpha; // May have fewer entries than PLTE_red, etc.
+    public int tRNS_gray;
+    public int tRNS_red;
+    public int tRNS_green;
+    public int tRNS_blue;
+
+    // zTXt chunk
+    public ArrayList zTXt_keyword = new ArrayList(); // Strings
+    public ArrayList zTXt_compressionMethod = new ArrayList(); // Integers
+    public ArrayList zTXt_text = new ArrayList(); // Strings
+
+    // Unknown chunks
+    public ArrayList unknownChunkType = new ArrayList(); // Strings
+    public ArrayList unknownChunkData = new ArrayList(); // byte arrays
+
+    public PNGMetadata() {
+        super(true,
+              nativeMetadataFormatName,
+              nativeMetadataFormatClassName,
+              null, null);
+    }
+
+    public PNGMetadata(IIOMetadata metadata) {
+        // TODO -- implement
+    }
+
+    /**
+     * Sets the IHDR_bitDepth and IHDR_colorType variables.
+     * The <code>numBands</code> parameter is necessary since
+     * we may only be writing a subset of the image bands.
+     */
+    public void initialize(ImageTypeSpecifier imageType, int numBands) {
+        ColorModel colorModel = imageType.getColorModel();
+        SampleModel sampleModel = imageType.getSampleModel();
+
+        // Initialize IHDR_bitDepth
+        int[] sampleSize = sampleModel.getSampleSize();
+        int bitDepth = sampleSize[0];
+        // Choose max bit depth over all channels
+        // Fixes bug 4413109
+        for (int i = 1; i < sampleSize.length; i++) {
+            if (sampleSize[i] > bitDepth) {
+                bitDepth = sampleSize[i];
+            }
+        }
+        // Multi-channel images must have a bit depth of 8 or 16
+        if (sampleSize.length > 1 && bitDepth < 8) {
+            bitDepth = 8;
+        }
+
+        // Round bit depth up to a power of 2
+        if (bitDepth > 2 && bitDepth < 4) {
+            bitDepth = 4;
+        } else if (bitDepth > 4 && bitDepth < 8) {
+            bitDepth = 8;
+        } else if (bitDepth > 8 && bitDepth < 16) {
+            bitDepth = 16;
+        } else if (bitDepth > 16) {
+            throw new RuntimeException("bitDepth > 16!");
+        }
+        IHDR_bitDepth = bitDepth;
+
+        // Initialize IHDR_colorType
+        if (colorModel instanceof IndexColorModel) {
+            IndexColorModel icm = (IndexColorModel)colorModel;
+            int size = icm.getMapSize();
+
+            byte[] reds = new byte[size];
+            icm.getReds(reds);
+            byte[] greens = new byte[size];
+            icm.getGreens(greens);
+            byte[] blues = new byte[size];
+            icm.getBlues(blues);
+
+            // Determine whether the color tables are actually a gray ramp
+            // if the color type has not been set previously
+            boolean isGray = false;
+            if (!IHDR_present ||
+                (IHDR_colorType != PNGImageReader.PNG_COLOR_PALETTE)) {
+                isGray = true;
+                int scale = 255/((1 << IHDR_bitDepth) - 1);
+                for (int i = 0; i < size; i++) {
+                    byte red = reds[i];
+                    if ((red != (byte)(i*scale)) ||
+                        (red != greens[i]) ||
+                        (red != blues[i])) {
+                        isGray = false;
+                        break;
+                    }
+                }
+            }
+
+            // Determine whether transparency exists
+            boolean hasAlpha = colorModel.hasAlpha();
+
+            byte[] alpha = null;
+            if (hasAlpha) {
+                alpha = new byte[size];
+                icm.getAlphas(alpha);
+            }
+
+            /*
+             * NB: PNG_COLOR_GRAY_ALPHA color type may be not optimal for images
+             * contained more than 1024 pixels (or even than 768 pixels in case of
+             * single transparent pixel in palette).
+             * For such images alpha samples in raster will occupy more space than
+             * it is required to store palette so it could be reasonable to
+             * use PNG_COLOR_PALETTE color type for large images.
+             */
+
+            if (isGray && hasAlpha && (bitDepth == 8 || bitDepth == 16)) {
+                IHDR_colorType = PNGImageReader.PNG_COLOR_GRAY_ALPHA;
+            } else if (isGray && !hasAlpha) {
+                IHDR_colorType = PNGImageReader.PNG_COLOR_GRAY;
+            } else {
+                IHDR_colorType = PNGImageReader.PNG_COLOR_PALETTE;
+                PLTE_present = true;
+                PLTE_order = null;
+                PLTE_red = (byte[])reds.clone();
+                PLTE_green = (byte[])greens.clone();
+                PLTE_blue = (byte[])blues.clone();
+
+                if (hasAlpha) {
+                    tRNS_present = true;
+                    tRNS_colorType = PNGImageReader.PNG_COLOR_PALETTE;
+
+                    PLTE_order = new int[alpha.length];
+
+                    // Reorder the palette so that non-opaque entries
+                    // come first.  Since the tRNS chunk does not have
+                    // to store trailing 255's, this can save a
+                    // considerable amount of space when encoding
+                    // images with only one transparent pixel value,
+                    // e.g., images from GIF sources.
+
+                    byte[] newAlpha = new byte[alpha.length];
+
+                    // Scan for non-opaque entries and assign them
+                    // positions starting at 0.
+                    int newIndex = 0;
+                    for (int i = 0; i < alpha.length; i++) {
+                        if (alpha[i] != (byte)255) {
+                            PLTE_order[i] = newIndex;
+                            newAlpha[newIndex] = alpha[i];
+                            ++newIndex;
+                        }
+                    }
+                    int numTransparent = newIndex;
+
+                    // Scan for opaque entries and assign them
+                    // positions following the non-opaque entries.
+                    for (int i = 0; i < alpha.length; i++) {
+                        if (alpha[i] == (byte)255) {
+                            PLTE_order[i] = newIndex++;
+                        }
+                    }
+
+                    // Reorder the palettes
+                    byte[] oldRed = PLTE_red;
+                    byte[] oldGreen = PLTE_green;
+                    byte[] oldBlue = PLTE_blue;
+                    int len = oldRed.length; // All have the same length
+                    PLTE_red = new byte[len];
+                    PLTE_green = new byte[len];
+                    PLTE_blue = new byte[len];
+                    for (int i = 0; i < len; i++) {
+                        PLTE_red[PLTE_order[i]] = oldRed[i];
+                        PLTE_green[PLTE_order[i]] = oldGreen[i];
+                        PLTE_blue[PLTE_order[i]] = oldBlue[i];
+                    }
+
+                    // Copy only the transparent entries into tRNS_alpha
+                    tRNS_alpha = new byte[numTransparent];
+                    System.arraycopy(newAlpha, 0,
+                                     tRNS_alpha, 0, numTransparent);
+                }
+            }
+        } else {
+            if (numBands == 1) {
+                IHDR_colorType = PNGImageReader.PNG_COLOR_GRAY;
+            } else if (numBands == 2) {
+                IHDR_colorType = PNGImageReader.PNG_COLOR_GRAY_ALPHA;
+            } else if (numBands == 3) {
+                IHDR_colorType = PNGImageReader.PNG_COLOR_RGB;
+            } else if (numBands == 4) {
+                IHDR_colorType = PNGImageReader.PNG_COLOR_RGB_ALPHA;
+            } else {
+                throw new RuntimeException("Number of bands not 1-4!");
+            }
+        }
+
+        IHDR_present = true;
+    }
+
+    public boolean isReadOnly() {
+        return false;
+    }
+
+    private ArrayList cloneBytesArrayList(ArrayList in) {
+        if (in == null) {
+            return null;
+        } else {
+            ArrayList list = new ArrayList(in.size());
+            Iterator iter = in.iterator();
+            while (iter.hasNext()) {
+                Object o = iter.next();
+                if (o == null) {
+                    list.add(null);
+                } else {
+                    list.add(((byte[])o).clone());
+                }
+            }
+
+            return list;
+        }
+    }
+
+    // Deep clone
+    public Object clone() {
+        PNGMetadata metadata;
+        try {
+            metadata = (PNGMetadata)super.clone();
+        } catch (CloneNotSupportedException e) {
+            return null;
+        }
+
+        // unknownChunkData needs deep clone
+        metadata.unknownChunkData =
+            cloneBytesArrayList(this.unknownChunkData);
+
+        return metadata;
+    }
+
+    public Node getAsTree(String formatName) {
+        if (formatName.equals(nativeMetadataFormatName)) {
+            return getNativeTree();
+        } else if (formatName.equals
+                   (IIOMetadataFormatImpl.standardMetadataFormatName)) {
+            return getStandardTree();
+        } else {
+            throw new IllegalArgumentException("Not a recognized format!");
+        }
+    }
+
+    private Node getNativeTree() {
+        IIOMetadataNode node = null; // scratch node
+        IIOMetadataNode root = new IIOMetadataNode(nativeMetadataFormatName);
+
+        // IHDR
+        if (IHDR_present) {
+            IIOMetadataNode IHDR_node = new IIOMetadataNode("IHDR");
+            IHDR_node.setAttribute("width", Integer.toString(IHDR_width));
+            IHDR_node.setAttribute("height", Integer.toString(IHDR_height));
+            IHDR_node.setAttribute("bitDepth",
+                                   Integer.toString(IHDR_bitDepth));
+            IHDR_node.setAttribute("colorType",
+                                   IHDR_colorTypeNames[IHDR_colorType]);
+            // IHDR_compressionMethod must be 0 in PNG 1.1
+            IHDR_node.setAttribute("compressionMethod",
+                          IHDR_compressionMethodNames[IHDR_compressionMethod]);
+            // IHDR_filterMethod must be 0 in PNG 1.1
+            IHDR_node.setAttribute("filterMethod",
+                                    IHDR_filterMethodNames[IHDR_filterMethod]);
+            IHDR_node.setAttribute("interlaceMethod",
+                              IHDR_interlaceMethodNames[IHDR_interlaceMethod]);
+            root.appendChild(IHDR_node);
+        }
+
+        // PLTE
+        if (PLTE_present) {
+            IIOMetadataNode PLTE_node = new IIOMetadataNode("PLTE");
+            int numEntries = PLTE_red.length;
+            for (int i = 0; i < numEntries; i++) {
+                IIOMetadataNode entry = new IIOMetadataNode("PLTEEntry");
+                entry.setAttribute("index", Integer.toString(i));
+                entry.setAttribute("red",
+                                   Integer.toString(PLTE_red[i] & 0xff));
+                entry.setAttribute("green",
+                                   Integer.toString(PLTE_green[i] & 0xff));
+                entry.setAttribute("blue",
+                                   Integer.toString(PLTE_blue[i] & 0xff));
+                PLTE_node.appendChild(entry);
+            }
+
+            root.appendChild(PLTE_node);
+        }
+
+        // bKGD
+        if (bKGD_present) {
+            IIOMetadataNode bKGD_node = new IIOMetadataNode("bKGD");
+
+            if (bKGD_colorType == PNGImageReader.PNG_COLOR_PALETTE) {
+                node = new IIOMetadataNode("bKGD_Palette");
+                node.setAttribute("index", Integer.toString(bKGD_index));
+            } else if (bKGD_colorType == PNGImageReader.PNG_COLOR_GRAY) {
+                node = new IIOMetadataNode("bKGD_Grayscale");
+                node.setAttribute("gray", Integer.toString(bKGD_gray));
+            } else if (bKGD_colorType == PNGImageReader.PNG_COLOR_RGB) {
+                node = new IIOMetadataNode("bKGD_RGB");
+                node.setAttribute("red", Integer.toString(bKGD_red));
+                node.setAttribute("green", Integer.toString(bKGD_green));
+                node.setAttribute("blue", Integer.toString(bKGD_blue));
+            }
+            bKGD_node.appendChild(node);
+
+            root.appendChild(bKGD_node);
+        }
+
+        // cHRM
+        if (cHRM_present) {
+            IIOMetadataNode cHRM_node = new IIOMetadataNode("cHRM");
+            cHRM_node.setAttribute("whitePointX",
+                              Integer.toString(cHRM_whitePointX));
+            cHRM_node.setAttribute("whitePointY",
+                              Integer.toString(cHRM_whitePointY));
+            cHRM_node.setAttribute("redX", Integer.toString(cHRM_redX));
+            cHRM_node.setAttribute("redY", Integer.toString(cHRM_redY));
+            cHRM_node.setAttribute("greenX", Integer.toString(cHRM_greenX));
+            cHRM_node.setAttribute("greenY", Integer.toString(cHRM_greenY));
+            cHRM_node.setAttribute("blueX", Integer.toString(cHRM_blueX));
+            cHRM_node.setAttribute("blueY", Integer.toString(cHRM_blueY));
+
+            root.appendChild(cHRM_node);
+        }
+
+        // gAMA
+        if (gAMA_present) {
+            IIOMetadataNode gAMA_node = new IIOMetadataNode("gAMA");
+            gAMA_node.setAttribute("value", Integer.toString(gAMA_gamma));
+
+            root.appendChild(gAMA_node);
+        }
+
+        // hIST
+        if (hIST_present) {
+            IIOMetadataNode hIST_node = new IIOMetadataNode("hIST");
+
+            for (int i = 0; i < hIST_histogram.length; i++) {
+                IIOMetadataNode hist =
+                    new IIOMetadataNode("hISTEntry");
+                hist.setAttribute("index", Integer.toString(i));
+                hist.setAttribute("value",
+                                  Integer.toString(hIST_histogram[i]));
+                hIST_node.appendChild(hist);
+            }
+
+            root.appendChild(hIST_node);
+        }
+
+        // iCCP
+        if (iCCP_present) {
+            IIOMetadataNode iCCP_node = new IIOMetadataNode("iCCP");
+            iCCP_node.setAttribute("profileName", iCCP_profileName);
+            iCCP_node.setAttribute("compressionMethod",
+                          iCCP_compressionMethodNames[iCCP_compressionMethod]);
+
+            Object profile = iCCP_compressedProfile;
+            if (profile != null) {
+                profile = ((byte[])profile).clone();
+            }
+            iCCP_node.setUserObject(profile);
+
+            root.appendChild(iCCP_node);
+        }
+
+        // iTXt
+        if (iTXt_keyword.size() > 0) {
+            IIOMetadataNode iTXt_parent = new IIOMetadataNode("iTXt");
+            for (int i = 0; i < iTXt_keyword.size(); i++) {
+                IIOMetadataNode iTXt_node = new IIOMetadataNode("iTXtEntry");
+                iTXt_node.setAttribute("keyword", iTXt_keyword.get(i));
+                iTXt_node.setAttribute("compressionFlag",
+                        iTXt_compressionFlag.get(i) ? "1" : "0");
+                iTXt_node.setAttribute("compressionMethod",
+                        iTXt_compressionMethod.get(i).toString());
+                iTXt_node.setAttribute("languageTag",
+                                       iTXt_languageTag.get(i));
+                iTXt_node.setAttribute("translatedKeyword",
+                                       iTXt_translatedKeyword.get(i));
+                iTXt_node.setAttribute("text", iTXt_text.get(i));
+
+                iTXt_parent.appendChild(iTXt_node);
+            }
+
+            root.appendChild(iTXt_parent);
+        }
+
+        // pHYs
+        if (pHYs_present) {
+            IIOMetadataNode pHYs_node = new IIOMetadataNode("pHYs");
+            pHYs_node.setAttribute("pixelsPerUnitXAxis",
+                              Integer.toString(pHYs_pixelsPerUnitXAxis));
+            pHYs_node.setAttribute("pixelsPerUnitYAxis",
+                                   Integer.toString(pHYs_pixelsPerUnitYAxis));
+            pHYs_node.setAttribute("unitSpecifier",
+                                   unitSpecifierNames[pHYs_unitSpecifier]);
+
+            root.appendChild(pHYs_node);
+        }
+
+        // sBIT
+        if (sBIT_present) {
+            IIOMetadataNode sBIT_node = new IIOMetadataNode("sBIT");
+
+            if (sBIT_colorType == PNGImageReader.PNG_COLOR_GRAY) {
+                node = new IIOMetadataNode("sBIT_Grayscale");
+                node.setAttribute("gray",
+                                  Integer.toString(sBIT_grayBits));
+            } else if (sBIT_colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA) {
+                node = new IIOMetadataNode("sBIT_GrayAlpha");
+                node.setAttribute("gray",
+                                  Integer.toString(sBIT_grayBits));
+                node.setAttribute("alpha",
+                                  Integer.toString(sBIT_alphaBits));
+            } else if (sBIT_colorType == PNGImageReader.PNG_COLOR_RGB) {
+                node = new IIOMetadataNode("sBIT_RGB");
+                node.setAttribute("red",
+                                  Integer.toString(sBIT_redBits));
+                node.setAttribute("green",
+                                  Integer.toString(sBIT_greenBits));
+                node.setAttribute("blue",
+                                  Integer.toString(sBIT_blueBits));
+            } else if (sBIT_colorType == PNGImageReader.PNG_COLOR_RGB_ALPHA) {
+                node = new IIOMetadataNode("sBIT_RGBAlpha");
+                node.setAttribute("red",
+                                  Integer.toString(sBIT_redBits));
+                node.setAttribute("green",
+                                  Integer.toString(sBIT_greenBits));
+                node.setAttribute("blue",
+                                  Integer.toString(sBIT_blueBits));
+                node.setAttribute("alpha",
+                                  Integer.toString(sBIT_alphaBits));
+            } else if (sBIT_colorType == PNGImageReader.PNG_COLOR_PALETTE) {
+                node = new IIOMetadataNode("sBIT_Palette");
+                node.setAttribute("red",
+                                  Integer.toString(sBIT_redBits));
+                node.setAttribute("green",
+                                  Integer.toString(sBIT_greenBits));
+                node.setAttribute("blue",
+                                  Integer.toString(sBIT_blueBits));
+            }
+            sBIT_node.appendChild(node);
+
+            root.appendChild(sBIT_node);
+        }
+
+        // sPLT
+        if (sPLT_present) {
+            IIOMetadataNode sPLT_node = new IIOMetadataNode("sPLT");
+
+            sPLT_node.setAttribute("name", sPLT_paletteName);
+            sPLT_node.setAttribute("sampleDepth",
+                                   Integer.toString(sPLT_sampleDepth));
+
+            int numEntries = sPLT_red.length;
+            for (int i = 0; i < numEntries; i++) {
+                IIOMetadataNode entry = new IIOMetadataNode("sPLTEntry");
+                entry.setAttribute("index", Integer.toString(i));
+                entry.setAttribute("red", Integer.toString(sPLT_red[i]));
+                entry.setAttribute("green", Integer.toString(sPLT_green[i]));
+                entry.setAttribute("blue", Integer.toString(sPLT_blue[i]));
+                entry.setAttribute("alpha", Integer.toString(sPLT_alpha[i]));
+                entry.setAttribute("frequency",
+                                  Integer.toString(sPLT_frequency[i]));
+                sPLT_node.appendChild(entry);
+            }
+
+            root.appendChild(sPLT_node);
+        }
+
+        // sRGB
+        if (sRGB_present) {
+            IIOMetadataNode sRGB_node = new IIOMetadataNode("sRGB");
+            sRGB_node.setAttribute("renderingIntent",
+                                   renderingIntentNames[sRGB_renderingIntent]);
+
+            root.appendChild(sRGB_node);
+        }
+
+        // tEXt
+        if (tEXt_keyword.size() > 0) {
+            IIOMetadataNode tEXt_parent = new IIOMetadataNode("tEXt");
+            for (int i = 0; i < tEXt_keyword.size(); i++) {
+                IIOMetadataNode tEXt_node = new IIOMetadataNode("tEXtEntry");
+                tEXt_node.setAttribute("keyword" , (String)tEXt_keyword.get(i));
+                tEXt_node.setAttribute("value" , (String)tEXt_text.get(i));
+
+                tEXt_parent.appendChild(tEXt_node);
+            }
+
+            root.appendChild(tEXt_parent);
+        }
+
+        // tIME
+        if (tIME_present) {
+            IIOMetadataNode tIME_node = new IIOMetadataNode("tIME");
+            tIME_node.setAttribute("year", Integer.toString(tIME_year));
+            tIME_node.setAttribute("month", Integer.toString(tIME_month));
+            tIME_node.setAttribute("day", Integer.toString(tIME_day));
+            tIME_node.setAttribute("hour", Integer.toString(tIME_hour));
+            tIME_node.setAttribute("minute", Integer.toString(tIME_minute));
+            tIME_node.setAttribute("second", Integer.toString(tIME_second));
+
+            root.appendChild(tIME_node);
+        }
+
+        // tRNS
+        if (tRNS_present) {
+            IIOMetadataNode tRNS_node = new IIOMetadataNode("tRNS");
+
+            if (tRNS_colorType == PNGImageReader.PNG_COLOR_PALETTE) {
+                node = new IIOMetadataNode("tRNS_Palette");
+
+                for (int i = 0; i < tRNS_alpha.length; i++) {
+                    IIOMetadataNode entry =
+                        new IIOMetadataNode("tRNS_PaletteEntry");
+                    entry.setAttribute("index", Integer.toString(i));
+                    entry.setAttribute("alpha",
+                                       Integer.toString(tRNS_alpha[i] & 0xff));
+                    node.appendChild(entry);
+                }
+            } else if (tRNS_colorType == PNGImageReader.PNG_COLOR_GRAY) {
+                node = new IIOMetadataNode("tRNS_Grayscale");
+                node.setAttribute("gray", Integer.toString(tRNS_gray));
+            } else if (tRNS_colorType == PNGImageReader.PNG_COLOR_RGB) {
+                node = new IIOMetadataNode("tRNS_RGB");
+                node.setAttribute("red", Integer.toString(tRNS_red));
+                node.setAttribute("green", Integer.toString(tRNS_green));
+                node.setAttribute("blue", Integer.toString(tRNS_blue));
+            }
+            tRNS_node.appendChild(node);
+
+            root.appendChild(tRNS_node);
+        }
+
+        // zTXt
+        if (zTXt_keyword.size() > 0) {
+            IIOMetadataNode zTXt_parent = new IIOMetadataNode("zTXt");
+            for (int i = 0; i < zTXt_keyword.size(); i++) {
+                IIOMetadataNode zTXt_node = new IIOMetadataNode("zTXtEntry");
+                zTXt_node.setAttribute("keyword", (String)zTXt_keyword.get(i));
+
+                int cm = ((Integer)zTXt_compressionMethod.get(i)).intValue();
+                zTXt_node.setAttribute("compressionMethod",
+                                       zTXt_compressionMethodNames[cm]);
+
+                zTXt_node.setAttribute("text", (String)zTXt_text.get(i));
+
+                zTXt_parent.appendChild(zTXt_node);
+            }
+
+            root.appendChild(zTXt_parent);
+        }
+
+        // Unknown chunks
+        if (unknownChunkType.size() > 0) {
+            IIOMetadataNode unknown_parent =
+                new IIOMetadataNode("UnknownChunks");
+            for (int i = 0; i < unknownChunkType.size(); i++) {
+                IIOMetadataNode unknown_node =
+                    new IIOMetadataNode("UnknownChunk");
+                unknown_node.setAttribute("type",
+                                          (String)unknownChunkType.get(i));
+                unknown_node.setUserObject((byte[])unknownChunkData.get(i));
+
+                unknown_parent.appendChild(unknown_node);
+            }
+
+            root.appendChild(unknown_parent);
+        }
+
+        return root;
+    }
+
+    private int getNumChannels() {
+        // Determine number of channels
+        // Be careful about palette color with transparency
+        int numChannels = IHDR_numChannels[IHDR_colorType];
+        if (IHDR_colorType == PNGImageReader.PNG_COLOR_PALETTE &&
+            tRNS_present && tRNS_colorType == IHDR_colorType) {
+            numChannels = 4;
+        }
+        return numChannels;
+    }
+
+    public IIOMetadataNode getStandardChromaNode() {
+        IIOMetadataNode chroma_node = new IIOMetadataNode("Chroma");
+        IIOMetadataNode node = null; // scratch node
+
+        node = new IIOMetadataNode("ColorSpaceType");
+        node.setAttribute("name", colorSpaceTypeNames[IHDR_colorType]);
+        chroma_node.appendChild(node);
+
+        node = new IIOMetadataNode("NumChannels");
+        node.setAttribute("value", Integer.toString(getNumChannels()));
+        chroma_node.appendChild(node);
+
+        if (gAMA_present) {
+            node = new IIOMetadataNode("Gamma");
+            node.setAttribute("value", Float.toString(gAMA_gamma*1.0e-5F));
+            chroma_node.appendChild(node);
+        }
+
+        node = new IIOMetadataNode("BlackIsZero");
+        node.setAttribute("value", "true");
+        chroma_node.appendChild(node);
+
+        if (PLTE_present) {
+            boolean hasAlpha = tRNS_present &&
+                (tRNS_colorType == PNGImageReader.PNG_COLOR_PALETTE);
+
+            node = new IIOMetadataNode("Palette");
+            for (int i = 0; i < PLTE_red.length; i++) {
+                IIOMetadataNode entry =
+                    new IIOMetadataNode("PaletteEntry");
+                entry.setAttribute("index", Integer.toString(i));
+                entry.setAttribute("red",
+                                   Integer.toString(PLTE_red[i] & 0xff));
+                entry.setAttribute("green",
+                                   Integer.toString(PLTE_green[i] & 0xff));
+                entry.setAttribute("blue",
+                                   Integer.toString(PLTE_blue[i] & 0xff));
+                if (hasAlpha) {
+                    int alpha = (i < tRNS_alpha.length) ?
+                        (tRNS_alpha[i] & 0xff) : 255;
+                    entry.setAttribute("alpha", Integer.toString(alpha));
+                }
+                node.appendChild(entry);
+            }
+            chroma_node.appendChild(node);
+        }
+
+        if (bKGD_present) {
+            if (bKGD_colorType == PNGImageReader.PNG_COLOR_PALETTE) {
+                node = new IIOMetadataNode("BackgroundIndex");
+                node.setAttribute("value", Integer.toString(bKGD_index));
+            } else {
+                node = new IIOMetadataNode("BackgroundColor");
+                int r, g, b;
+
+                if (bKGD_colorType == PNGImageReader.PNG_COLOR_GRAY) {
+                    r = g = b = bKGD_gray;
+                } else {
+                    r = bKGD_red;
+                    g = bKGD_green;
+                    b = bKGD_blue;
+                }
+                node.setAttribute("red", Integer.toString(r));
+                node.setAttribute("green", Integer.toString(g));
+                node.setAttribute("blue", Integer.toString(b));
+            }
+            chroma_node.appendChild(node);
+        }
+
+        return chroma_node;
+    }
+
+    public IIOMetadataNode getStandardCompressionNode() {
+        IIOMetadataNode compression_node = new IIOMetadataNode("Compression");
+        IIOMetadataNode node = null; // scratch node
+
+        node = new IIOMetadataNode("CompressionTypeName");
+        node.setAttribute("value", "deflate");
+        compression_node.appendChild(node);
+
+        node = new IIOMetadataNode("Lossless");
+        node.setAttribute("value", "true");
+        compression_node.appendChild(node);
+
+        node = new IIOMetadataNode("NumProgressiveScans");
+        node.setAttribute("value",
+                          (IHDR_interlaceMethod == 0) ? "1" : "7");
+        compression_node.appendChild(node);
+
+        return compression_node;
+    }
+
+    private String repeat(String s, int times) {
+        if (times == 1) {
+            return s;
+        }
+        StringBuffer sb = new StringBuffer((s.length() + 1)*times - 1);
+        sb.append(s);
+        for (int i = 1; i < times; i++) {
+            sb.append(" ");
+            sb.append(s);
+        }
+        return sb.toString();
+    }
+
+    public IIOMetadataNode getStandardDataNode() {
+        IIOMetadataNode data_node = new IIOMetadataNode("Data");
+        IIOMetadataNode node = null; // scratch node
+
+        node = new IIOMetadataNode("PlanarConfiguration");
+        node.setAttribute("value", "PixelInterleaved");
+        data_node.appendChild(node);
+
+        node = new IIOMetadataNode("SampleFormat");
+        node.setAttribute("value",
+                          IHDR_colorType == PNGImageReader.PNG_COLOR_PALETTE ?
+                          "Index" : "UnsignedIntegral");
+        data_node.appendChild(node);
+
+        String bitDepth = Integer.toString(IHDR_bitDepth);
+        node = new IIOMetadataNode("BitsPerSample");
+        node.setAttribute("value", repeat(bitDepth, getNumChannels()));
+        data_node.appendChild(node);
+
+        if (sBIT_present) {
+            node = new IIOMetadataNode("SignificantBitsPerSample");
+            String sbits;
+            if (sBIT_colorType == PNGImageReader.PNG_COLOR_GRAY ||
+                sBIT_colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA) {
+                sbits = Integer.toString(sBIT_grayBits);
+            } else { // sBIT_colorType == PNGImageReader.PNG_COLOR_RGB ||
+                     // sBIT_colorType == PNGImageReader.PNG_COLOR_RGB_ALPHA
+                sbits = Integer.toString(sBIT_redBits) + " " +
+                    Integer.toString(sBIT_greenBits) + " " +
+                    Integer.toString(sBIT_blueBits);
+            }
+
+            if (sBIT_colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA ||
+                sBIT_colorType == PNGImageReader.PNG_COLOR_RGB_ALPHA) {
+                sbits += " " + Integer.toString(sBIT_alphaBits);
+            }
+
+            node.setAttribute("value", sbits);
+            data_node.appendChild(node);
+        }
+
+        // SampleMSB
+
+        return data_node;
+    }
+
+    public IIOMetadataNode getStandardDimensionNode() {
+        IIOMetadataNode dimension_node = new IIOMetadataNode("Dimension");
+        IIOMetadataNode node = null; // scratch node
+
+        node = new IIOMetadataNode("PixelAspectRatio");
+        float ratio = pHYs_present ?
+            (float)pHYs_pixelsPerUnitXAxis/pHYs_pixelsPerUnitYAxis : 1.0F;
+        node.setAttribute("value", Float.toString(ratio));
+        dimension_node.appendChild(node);
+
+        node = new IIOMetadataNode("ImageOrientation");
+        node.setAttribute("value", "Normal");
+        dimension_node.appendChild(node);
+
+        if (pHYs_present && pHYs_unitSpecifier == PHYS_UNIT_METER) {
+            node = new IIOMetadataNode("HorizontalPixelSize");
+            node.setAttribute("value",
+                              Float.toString(1000.0F/pHYs_pixelsPerUnitXAxis));
+            dimension_node.appendChild(node);
+
+            node = new IIOMetadataNode("VerticalPixelSize");
+            node.setAttribute("value",
+                              Float.toString(1000.0F/pHYs_pixelsPerUnitYAxis));
+            dimension_node.appendChild(node);
+        }
+
+        return dimension_node;
+    }
+
+    public IIOMetadataNode getStandardDocumentNode() {
+        if (!tIME_present) {
+            return null;
+        }
+
+        IIOMetadataNode document_node = new IIOMetadataNode("Document");
+        IIOMetadataNode node = null; // scratch node
+
+        node = new IIOMetadataNode("ImageModificationTime");
+        node.setAttribute("year", Integer.toString(tIME_year));
+        node.setAttribute("month", Integer.toString(tIME_month));
+        node.setAttribute("day", Integer.toString(tIME_day));
+        node.setAttribute("hour", Integer.toString(tIME_hour));
+        node.setAttribute("minute", Integer.toString(tIME_minute));
+        node.setAttribute("second", Integer.toString(tIME_second));
+        document_node.appendChild(node);
+
+        return document_node;
+    }
+
+    public IIOMetadataNode getStandardTextNode() {
+        int numEntries = tEXt_keyword.size() +
+            iTXt_keyword.size() + zTXt_keyword.size();
+        if (numEntries == 0) {
+            return null;
+        }
+
+        IIOMetadataNode text_node = new IIOMetadataNode("Text");
+        IIOMetadataNode node = null; // scratch node
+
+        for (int i = 0; i < tEXt_keyword.size(); i++) {
+            node = new IIOMetadataNode("TextEntry");
+            node.setAttribute("keyword", (String)tEXt_keyword.get(i));
+            node.setAttribute("value", (String)tEXt_text.get(i));
+            node.setAttribute("encoding", "ISO-8859-1");
+            node.setAttribute("compression", "none");
+
+            text_node.appendChild(node);
+        }
+
+        for (int i = 0; i < iTXt_keyword.size(); i++) {
+            node = new IIOMetadataNode("TextEntry");
+            node.setAttribute("keyword", iTXt_keyword.get(i));
+            node.setAttribute("value", iTXt_text.get(i));
+            node.setAttribute("language",
+                              iTXt_languageTag.get(i));
+            if (iTXt_compressionFlag.get(i)) {
+                node.setAttribute("compression", "deflate");
+            } else {
+                node.setAttribute("compression", "none");
+            }
+
+            text_node.appendChild(node);
+        }
+
+        for (int i = 0; i < zTXt_keyword.size(); i++) {
+            node = new IIOMetadataNode("TextEntry");
+            node.setAttribute("keyword", (String)zTXt_keyword.get(i));
+            node.setAttribute("value", (String)zTXt_text.get(i));
+            node.setAttribute("compression", "deflate");
+
+            text_node.appendChild(node);
+        }
+
+        return text_node;
+    }
+
+    public IIOMetadataNode getStandardTransparencyNode() {
+        IIOMetadataNode transparency_node =
+            new IIOMetadataNode("Transparency");
+        IIOMetadataNode node = null; // scratch node
+
+        node = new IIOMetadataNode("Alpha");
+        boolean hasAlpha =
+            (IHDR_colorType == PNGImageReader.PNG_COLOR_RGB_ALPHA) ||
+            (IHDR_colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA) ||
+            (IHDR_colorType == PNGImageReader.PNG_COLOR_PALETTE &&
+             tRNS_present &&
+             (tRNS_colorType == IHDR_colorType) &&
+             (tRNS_alpha != null));
+        node.setAttribute("value", hasAlpha ? "nonpremultipled" : "none");
+        transparency_node.appendChild(node);
+
+        if (tRNS_present) {
+            node = new IIOMetadataNode("TransparentColor");
+            if (tRNS_colorType == PNGImageReader.PNG_COLOR_RGB) {
+                node.setAttribute("value",
+                                  Integer.toString(tRNS_red) + " " +
+                                  Integer.toString(tRNS_green) + " " +
+                                  Integer.toString(tRNS_blue));
+            } else if (tRNS_colorType == PNGImageReader.PNG_COLOR_GRAY) {
+                node.setAttribute("value", Integer.toString(tRNS_gray));
+            }
+            transparency_node.appendChild(node);
+        }
+
+        return transparency_node;
+    }
+
+    // Shorthand for throwing an IIOInvalidTreeException
+    private void fatal(Node node, String reason)
+        throws IIOInvalidTreeException {
+        throw new IIOInvalidTreeException(reason, node);
+    }
+
+    // Get an integer-valued attribute
+    private String getStringAttribute(Node node, String name,
+                                      String defaultValue, boolean required)
+        throws IIOInvalidTreeException {
+        Node attr = node.getAttributes().getNamedItem(name);
+        if (attr == null) {
+            if (!required) {
+                return defaultValue;
+            } else {
+                fatal(node, "Required attribute " + name + " not present!");
+            }
+        }
+        return attr.getNodeValue();
+    }
+
+
+    // Get an integer-valued attribute
+    private int getIntAttribute(Node node, String name,
+                                int defaultValue, boolean required)
+        throws IIOInvalidTreeException {
+        String value = getStringAttribute(node, name, null, required);
+        if (value == null) {
+            return defaultValue;
+        }
+        return Integer.parseInt(value);
+    }
+
+    // Get a float-valued attribute
+    private float getFloatAttribute(Node node, String name,
+                                    float defaultValue, boolean required)
+        throws IIOInvalidTreeException {
+        String value = getStringAttribute(node, name, null, required);
+        if (value == null) {
+            return defaultValue;
+        }
+        return Float.parseFloat(value);
+    }
+
+    // Get a required integer-valued attribute
+    private int getIntAttribute(Node node, String name)
+        throws IIOInvalidTreeException {
+        return getIntAttribute(node, name, -1, true);
+    }
+
+    // Get a required float-valued attribute
+    private float getFloatAttribute(Node node, String name)
+        throws IIOInvalidTreeException {
+        return getFloatAttribute(node, name, -1.0F, true);
+    }
+
+    // Get a boolean-valued attribute
+    private boolean getBooleanAttribute(Node node, String name,
+                                        boolean defaultValue,
+                                        boolean required)
+        throws IIOInvalidTreeException {
+        Node attr = node.getAttributes().getNamedItem(name);
+        if (attr == null) {
+            if (!required) {
+                return defaultValue;
+            } else {
+                fatal(node, "Required attribute " + name + " not present!");
+            }
+        }
+        String value = attr.getNodeValue();
+        if (value.equals("true")) {
+            return true;
+        } else if (value.equals("false")) {
+            return false;
+        } else {
+            fatal(node, "Attribute " + name + " must be 'true' or 'false'!");
+            return false;
+        }
+    }
+
+    // Get a required boolean-valued attribute
+    private boolean getBooleanAttribute(Node node, String name)
+        throws IIOInvalidTreeException {
+        return getBooleanAttribute(node, name, false, true);
+    }
+
+    // Get an enumerated attribute as an index into a String array
+    private int getEnumeratedAttribute(Node node,
+                                       String name, String[] legalNames,
+                                       int defaultValue, boolean required)
+        throws IIOInvalidTreeException {
+        Node attr = node.getAttributes().getNamedItem(name);
+        if (attr == null) {
+            if (!required) {
+                return defaultValue;
+            } else {
+                fatal(node, "Required attribute " + name + " not present!");
+            }
+        }
+        String value = attr.getNodeValue();
+        for (int i = 0; i < legalNames.length; i++) {
+            if (value.equals(legalNames[i])) {
+                return i;
+            }
+        }
+
+        fatal(node, "Illegal value for attribute " + name + "!");
+        return -1;
+    }
+
+    // Get a required enumerated attribute as an index into a String array
+    private int getEnumeratedAttribute(Node node,
+                                       String name, String[] legalNames)
+        throws IIOInvalidTreeException {
+        return getEnumeratedAttribute(node, name, legalNames, -1, true);
+    }
+
+    // Get a String-valued attribute
+    private String getAttribute(Node node, String name,
+                                String defaultValue, boolean required)
+        throws IIOInvalidTreeException {
+        Node attr = node.getAttributes().getNamedItem(name);
+        if (attr == null) {
+            if (!required) {
+                return defaultValue;
+            } else {
+                fatal(node, "Required attribute " + name + " not present!");
+            }
+        }
+        return attr.getNodeValue();
+    }
+
+    // Get a required String-valued attribute
+    private String getAttribute(Node node, String name)
+        throws IIOInvalidTreeException {
+            return getAttribute(node, name, null, true);
+    }
+
+    public void mergeTree(String formatName, Node root)
+        throws IIOInvalidTreeException {
+        if (formatName.equals(nativeMetadataFormatName)) {
+            if (root == null) {
+                throw new IllegalArgumentException("root == null!");
+            }
+            mergeNativeTree(root);
+        } else if (formatName.equals
+                   (IIOMetadataFormatImpl.standardMetadataFormatName)) {
+            if (root == null) {
+                throw new IllegalArgumentException("root == null!");
+            }
+            mergeStandardTree(root);
+        } else {
+            throw new IllegalArgumentException("Not a recognized format!");
+        }
+    }
+
+    private void mergeNativeTree(Node root)
+        throws IIOInvalidTreeException {
+        Node node = root;
+        if (!node.getNodeName().equals(nativeMetadataFormatName)) {
+            fatal(node, "Root must be " + nativeMetadataFormatName);
+        }
+
+        node = node.getFirstChild();
+        while (node != null) {
+            String name = node.getNodeName();
+
+            if (name.equals("IHDR")) {
+                IHDR_width = getIntAttribute(node, "width");
+                IHDR_height = getIntAttribute(node, "height");
+                IHDR_bitDepth = getEnumeratedAttribute(node, "bitDepth",
+                                                       IHDR_bitDepths);
+                IHDR_colorType = getEnumeratedAttribute(node, "colorType",
+                                                        IHDR_colorTypeNames);
+                IHDR_compressionMethod =
+                    getEnumeratedAttribute(node, "compressionMethod",
+                                           IHDR_compressionMethodNames);
+                IHDR_filterMethod =
+                    getEnumeratedAttribute(node,
+                                           "filterMethod",
+                                           IHDR_filterMethodNames);
+                IHDR_interlaceMethod =
+                    getEnumeratedAttribute(node, "interlaceMethod",
+                                           IHDR_interlaceMethodNames);
+                IHDR_present = true;
+            } else if (name.equals("PLTE")) {
+                byte[] red = new byte[256];
+                byte[] green  = new byte[256];
+                byte[] blue = new byte[256];
+                int maxindex = -1;
+
+                Node PLTE_entry = node.getFirstChild();
+                if (PLTE_entry == null) {
+                    fatal(node, "Palette has no entries!");
+                }
+
+                while (PLTE_entry != null) {
+                    if (!PLTE_entry.getNodeName().equals("PLTEEntry")) {
+                        fatal(node,
+                              "Only a PLTEEntry may be a child of a PLTE!");
+                    }
+
+                    int index = getIntAttribute(PLTE_entry, "index");
+                    if (index < 0 || index > 255) {
+                        fatal(node,
+                              "Bad value for PLTEEntry attribute index!");
+                    }
+                    if (index > maxindex) {
+                        maxindex = index;
+                    }
+                    red[index] =
+                        (byte)getIntAttribute(PLTE_entry, "red");
+                    green[index] =
+                        (byte)getIntAttribute(PLTE_entry, "green");
+                    blue[index] =
+                        (byte)getIntAttribute(PLTE_entry, "blue");
+
+                    PLTE_entry = PLTE_entry.getNextSibling();
+                }
+
+                int numEntries = maxindex + 1;
+                PLTE_red = new byte[numEntries];
+                PLTE_green = new byte[numEntries];
+                PLTE_blue = new byte[numEntries];
+                System.arraycopy(red, 0, PLTE_red, 0, numEntries);
+                System.arraycopy(green, 0, PLTE_green, 0, numEntries);
+                System.arraycopy(blue, 0, PLTE_blue, 0, numEntries);
+                PLTE_present = true;
+            } else if (name.equals("bKGD")) {
+                bKGD_present = false; // Guard against partial overwrite
+                Node bKGD_node = node.getFirstChild();
+                if (bKGD_node == null) {
+                    fatal(node, "bKGD node has no children!");
+                }
+                String bKGD_name = bKGD_node.getNodeName();
+                if (bKGD_name.equals("bKGD_Palette")) {
+                    bKGD_index = getIntAttribute(bKGD_node, "index");
+                    bKGD_colorType = PNGImageReader.PNG_COLOR_PALETTE;
+                } else if (bKGD_name.equals("bKGD_Grayscale")) {
+                    bKGD_gray = getIntAttribute(bKGD_node, "gray");
+                    bKGD_colorType = PNGImageReader.PNG_COLOR_GRAY;
+                } else if (bKGD_name.equals("bKGD_RGB")) {
+                    bKGD_red = getIntAttribute(bKGD_node, "red");
+                    bKGD_green = getIntAttribute(bKGD_node, "green");
+                    bKGD_blue = getIntAttribute(bKGD_node, "blue");
+                    bKGD_colorType = PNGImageReader.PNG_COLOR_RGB;
+                } else {
+                    fatal(node, "Bad child of a bKGD node!");
+                }
+                if (bKGD_node.getNextSibling() != null) {
+                    fatal(node, "bKGD node has more than one child!");
+                }
+
+                bKGD_present = true;
+            } else if (name.equals("cHRM")) {
+                cHRM_whitePointX = getIntAttribute(node, "whitePointX");
+                cHRM_whitePointY = getIntAttribute(node, "whitePointY");
+                cHRM_redX = getIntAttribute(node, "redX");
+                cHRM_redY = getIntAttribute(node, "redY");
+                cHRM_greenX = getIntAttribute(node, "greenX");
+                cHRM_greenY = getIntAttribute(node, "greenY");
+                cHRM_blueX = getIntAttribute(node, "blueX");
+                cHRM_blueY = getIntAttribute(node, "blueY");
+
+                cHRM_present = true;
+            } else if (name.equals("gAMA")) {
+                gAMA_gamma = getIntAttribute(node, "value");
+                gAMA_present = true;
+            } else if (name.equals("hIST")) {
+                char[] hist = new char[256];
+                int maxindex = -1;
+
+                Node hIST_entry = node.getFirstChild();
+                if (hIST_entry == null) {
+                    fatal(node, "hIST node has no children!");
+                }
+
+                while (hIST_entry != null) {
+                    if (!hIST_entry.getNodeName().equals("hISTEntry")) {
+                        fatal(node,
+                              "Only a hISTEntry may be a child of a hIST!");
+                    }
+
+                    int index = getIntAttribute(hIST_entry, "index");
+                    if (index < 0 || index > 255) {
+                        fatal(node,
+                              "Bad value for histEntry attribute index!");
+                    }
+                    if (index > maxindex) {
+                        maxindex = index;
+                    }
+                    hist[index] =
+                        (char)getIntAttribute(hIST_entry, "value");
+
+                    hIST_entry = hIST_entry.getNextSibling();
+                }
+
+                int numEntries = maxindex + 1;
+                hIST_histogram = new char[numEntries];
+                System.arraycopy(hist, 0, hIST_histogram, 0, numEntries);
+
+                hIST_present = true;
+            } else if (name.equals("iCCP")) {
+                iCCP_profileName = getAttribute(node, "profileName");
+                iCCP_compressionMethod =
+                    getEnumeratedAttribute(node, "compressionMethod",
+                                           iCCP_compressionMethodNames);
+                Object compressedProfile =
+                    ((IIOMetadataNode)node).getUserObject();
+                if (compressedProfile == null) {
+                    fatal(node, "No ICCP profile present in user object!");
+                }
+                if (!(compressedProfile instanceof byte[])) {
+                    fatal(node, "User object not a byte array!");
+                }
+
+                iCCP_compressedProfile =
+                    (byte[])((byte[])compressedProfile).clone();
+
+                iCCP_present = true;
+            } else if (name.equals("iTXt")) {
+                Node iTXt_node = node.getFirstChild();
+                while (iTXt_node != null) {
+                    if (!iTXt_node.getNodeName().equals("iTXtEntry")) {
+                        fatal(node,
+                              "Only an iTXtEntry may be a child of an iTXt!");
+                    }
+
+                    String keyword = getAttribute(iTXt_node, "keyword");
+                    iTXt_keyword.add(keyword);
+
+                    boolean compressionFlag =
+                        getBooleanAttribute(iTXt_node, "compressionFlag");
+                    iTXt_compressionFlag.add(Boolean.valueOf(compressionFlag));
+
+                    String compressionMethod =
+                        getAttribute(iTXt_node, "compressionMethod");
+                    iTXt_compressionMethod.add(Integer.valueOf(compressionMethod));
+
+                    String languageTag =
+                        getAttribute(iTXt_node, "languageTag");
+                    iTXt_languageTag.add(languageTag);
+
+                    String translatedKeyword =
+                        getAttribute(iTXt_node, "translatedKeyword");
+                    iTXt_translatedKeyword.add(translatedKeyword);
+
+                    String text = getAttribute(iTXt_node, "text");
+                    iTXt_text.add(text);
+
+                    iTXt_node = iTXt_node.getNextSibling();
+                }
+            } else if (name.equals("pHYs")) {
+                pHYs_pixelsPerUnitXAxis =
+                    getIntAttribute(node, "pixelsPerUnitXAxis");
+                pHYs_pixelsPerUnitYAxis =
+                    getIntAttribute(node, "pixelsPerUnitYAxis");
+                pHYs_unitSpecifier =
+                    getEnumeratedAttribute(node, "unitSpecifier",
+                                           unitSpecifierNames);
+
+                pHYs_present = true;
+            } else if (name.equals("sBIT")) {
+                sBIT_present = false; // Guard against partial overwrite
+                Node sBIT_node = node.getFirstChild();
+                if (sBIT_node == null) {
+                    fatal(node, "sBIT node has no children!");
+                }
+                String sBIT_name = sBIT_node.getNodeName();
+                if (sBIT_name.equals("sBIT_Grayscale")) {
+                    sBIT_grayBits = getIntAttribute(sBIT_node, "gray");
+                    sBIT_colorType = PNGImageReader.PNG_COLOR_GRAY;
+                } else if (sBIT_name.equals("sBIT_GrayAlpha")) {
+                    sBIT_grayBits = getIntAttribute(sBIT_node, "gray");
+                    sBIT_alphaBits = getIntAttribute(sBIT_node, "alpha");
+                    sBIT_colorType = PNGImageReader.PNG_COLOR_GRAY_ALPHA;
+                } else if (sBIT_name.equals("sBIT_RGB")) {
+                    sBIT_redBits = getIntAttribute(sBIT_node, "red");
+                    sBIT_greenBits = getIntAttribute(sBIT_node, "green");
+                    sBIT_blueBits = getIntAttribute(sBIT_node, "blue");
+                    sBIT_colorType = PNGImageReader.PNG_COLOR_RGB;
+                } else if (sBIT_name.equals("sBIT_RGBAlpha")) {
+                    sBIT_redBits = getIntAttribute(sBIT_node, "red");
+                    sBIT_greenBits = getIntAttribute(sBIT_node, "green");
+                    sBIT_blueBits = getIntAttribute(sBIT_node, "blue");
+                    sBIT_alphaBits = getIntAttribute(sBIT_node, "alpha");
+                    sBIT_colorType = PNGImageReader.PNG_COLOR_RGB_ALPHA;
+                } else if (sBIT_name.equals("sBIT_Palette")) {
+                    sBIT_redBits = getIntAttribute(sBIT_node, "red");
+                    sBIT_greenBits = getIntAttribute(sBIT_node, "green");
+                    sBIT_blueBits = getIntAttribute(sBIT_node, "blue");
+                    sBIT_colorType = PNGImageReader.PNG_COLOR_PALETTE;
+                } else {
+                    fatal(node, "Bad child of an sBIT node!");
+                }
+                if (sBIT_node.getNextSibling() != null) {
+                    fatal(node, "sBIT node has more than one child!");
+                }
+
+                sBIT_present = true;
+            } else if (name.equals("sPLT")) {
+                sPLT_paletteName = getAttribute(node, "name");
+                sPLT_sampleDepth = getIntAttribute(node, "sampleDepth");
+
+                int[] red = new int[256];
+                int[] green  = new int[256];
+                int[] blue = new int[256];
+                int[] alpha = new int[256];
+                int[] frequency = new int[256];
+                int maxindex = -1;
+
+                Node sPLT_entry = node.getFirstChild();
+                if (sPLT_entry == null) {
+                    fatal(node, "sPLT node has no children!");
+                }
+
+                while (sPLT_entry != null) {
+                    if (!sPLT_entry.getNodeName().equals("sPLTEntry")) {
+                        fatal(node,
+                              "Only an sPLTEntry may be a child of an sPLT!");
+                    }
+
+                    int index = getIntAttribute(sPLT_entry, "index");
+                    if (index < 0 || index > 255) {
+                        fatal(node,
+                              "Bad value for PLTEEntry attribute index!");
+                    }
+                    if (index > maxindex) {
+                        maxindex = index;
+                    }
+                    red[index] = getIntAttribute(sPLT_entry, "red");
+                    green[index] = getIntAttribute(sPLT_entry, "green");
+                    blue[index] = getIntAttribute(sPLT_entry, "blue");
+                    alpha[index] = getIntAttribute(sPLT_entry, "alpha");
+                    frequency[index] =
+                        getIntAttribute(sPLT_entry, "frequency");
+
+                    sPLT_entry = sPLT_entry.getNextSibling();
+                }
+
+                int numEntries = maxindex + 1;
+                sPLT_red = new int[numEntries];
+                sPLT_green = new int[numEntries];
+                sPLT_blue = new int[numEntries];
+                sPLT_alpha = new int[numEntries];
+                sPLT_frequency = new int[numEntries];
+                System.arraycopy(red, 0, sPLT_red, 0, numEntries);
+                System.arraycopy(green, 0, sPLT_green, 0, numEntries);
+                System.arraycopy(blue, 0, sPLT_blue, 0, numEntries);
+                System.arraycopy(alpha, 0, sPLT_alpha, 0, numEntries);
+                System.arraycopy(frequency, 0,
+                                 sPLT_frequency, 0, numEntries);
+
+                sPLT_present = true;
+            } else if (name.equals("sRGB")) {
+                sRGB_renderingIntent =
+                    getEnumeratedAttribute(node, "renderingIntent",
+                                           renderingIntentNames);
+
+                sRGB_present = true;
+            } else if (name.equals("tEXt")) {
+                Node tEXt_node = node.getFirstChild();
+                while (tEXt_node != null) {
+                    if (!tEXt_node.getNodeName().equals("tEXtEntry")) {
+                        fatal(node,
+                              "Only an tEXtEntry may be a child of an tEXt!");
+                    }
+
+                    String keyword = getAttribute(tEXt_node, "keyword");
+                    tEXt_keyword.add(keyword);
+
+                    String text = getAttribute(tEXt_node, "value");
+                    tEXt_text.add(text);
+
+                    tEXt_node = tEXt_node.getNextSibling();
+                }
+            } else if (name.equals("tIME")) {
+                tIME_year = getIntAttribute(node, "year");
+                tIME_month = getIntAttribute(node, "month");
+                tIME_day = getIntAttribute(node, "day");
+                tIME_hour = getIntAttribute(node, "hour");
+                tIME_minute = getIntAttribute(node, "minute");
+                tIME_second = getIntAttribute(node, "second");
+
+                tIME_present = true;
+            } else if (name.equals("tRNS")) {
+                tRNS_present = false; // Guard against partial overwrite
+                Node tRNS_node = node.getFirstChild();
+                if (tRNS_node == null) {
+                    fatal(node, "tRNS node has no children!");
+                }
+                String tRNS_name = tRNS_node.getNodeName();
+                if (tRNS_name.equals("tRNS_Palette")) {
+                    byte[] alpha = new byte[256];
+                    int maxindex = -1;
+
+                    Node tRNS_paletteEntry = tRNS_node.getFirstChild();
+                    if (tRNS_paletteEntry == null) {
+                        fatal(node, "tRNS_Palette node has no children!");
+                    }
+                    while (tRNS_paletteEntry != null) {
+                        if (!tRNS_paletteEntry.getNodeName().equals(
+                                                        "tRNS_PaletteEntry")) {
+                            fatal(node,
+                 "Only a tRNS_PaletteEntry may be a child of a tRNS_Palette!");
+                        }
+                        int index =
+                            getIntAttribute(tRNS_paletteEntry, "index");
+                        if (index < 0 || index > 255) {
+                            fatal(node,
+                           "Bad value for tRNS_PaletteEntry attribute index!");
+                        }
+                        if (index > maxindex) {
+                            maxindex = index;
+                        }
+                        alpha[index] =
+                            (byte)getIntAttribute(tRNS_paletteEntry,
+                                                  "alpha");
+
+                        tRNS_paletteEntry =
+                            tRNS_paletteEntry.getNextSibling();
+                    }
+
+                    int numEntries = maxindex + 1;
+                    tRNS_alpha = new byte[numEntries];
+                    tRNS_colorType = PNGImageReader.PNG_COLOR_PALETTE;
+                    System.arraycopy(alpha, 0, tRNS_alpha, 0, numEntries);
+                } else if (tRNS_name.equals("tRNS_Grayscale")) {
+                    tRNS_gray = getIntAttribute(tRNS_node, "gray");
+                    tRNS_colorType = PNGImageReader.PNG_COLOR_GRAY;
+                } else if (tRNS_name.equals("tRNS_RGB")) {
+                    tRNS_red = getIntAttribute(tRNS_node, "red");
+                    tRNS_green = getIntAttribute(tRNS_node, "green");
+                    tRNS_blue = getIntAttribute(tRNS_node, "blue");
+                    tRNS_colorType = PNGImageReader.PNG_COLOR_RGB;
+                } else {
+                    fatal(node, "Bad child of a tRNS node!");
+                }
+                if (tRNS_node.getNextSibling() != null) {
+                    fatal(node, "tRNS node has more than one child!");
+                }
+
+                tRNS_present = true;
+            } else if (name.equals("zTXt")) {
+                Node zTXt_node = node.getFirstChild();
+                while (zTXt_node != null) {
+                    if (!zTXt_node.getNodeName().equals("zTXtEntry")) {
+                        fatal(node,
+                              "Only an zTXtEntry may be a child of an zTXt!");
+                    }
+
+                    String keyword = getAttribute(zTXt_node, "keyword");
+                    zTXt_keyword.add(keyword);
+
+                    int compressionMethod =
+                        getEnumeratedAttribute(zTXt_node, "compressionMethod",
+                                               zTXt_compressionMethodNames);
+                    zTXt_compressionMethod.add(new Integer(compressionMethod));
+
+                    String text = getAttribute(zTXt_node, "text");
+                    zTXt_text.add(text);
+
+                    zTXt_node = zTXt_node.getNextSibling();
+                }
+            } else if (name.equals("UnknownChunks")) {
+                Node unknown_node = node.getFirstChild();
+                while (unknown_node != null) {
+                    if (!unknown_node.getNodeName().equals("UnknownChunk")) {
+                        fatal(node,
+                   "Only an UnknownChunk may be a child of an UnknownChunks!");
+                    }
+                    String chunkType = getAttribute(unknown_node, "type");
+                    Object chunkData =
+                        ((IIOMetadataNode)unknown_node).getUserObject();
+
+                    if (chunkType.length() != 4) {
+                        fatal(unknown_node,
+                              "Chunk type must be 4 characters!");
+                    }
+                    if (chunkData == null) {
+                        fatal(unknown_node,
+                              "No chunk data present in user object!");
+                    }
+                    if (!(chunkData instanceof byte[])) {
+                        fatal(unknown_node,
+                              "User object not a byte array!");
+                    }
+                    unknownChunkType.add(chunkType);
+                    unknownChunkData.add(((byte[])chunkData).clone());
+
+                    unknown_node = unknown_node.getNextSibling();
+                }
+            } else {
+                fatal(node, "Unknown child of root node!");
+            }
+
+            node = node.getNextSibling();
+        }
+    }
+
+    private boolean isISOLatin(String s) {
+        int len = s.length();
+        for (int i = 0; i < len; i++) {
+            if (s.charAt(i) > 255) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private void mergeStandardTree(Node root)
+        throws IIOInvalidTreeException {
+        Node node = root;
+        if (!node.getNodeName()
+            .equals(IIOMetadataFormatImpl.standardMetadataFormatName)) {
+            fatal(node, "Root must be " +
+                  IIOMetadataFormatImpl.standardMetadataFormatName);
+        }
+
+        node = node.getFirstChild();
+        while (node != null) {
+            String name = node.getNodeName();
+
+            if (name.equals("Chroma")) {
+                Node child = node.getFirstChild();
+                while (child != null) {
+                    String childName = child.getNodeName();
+                    if (childName.equals("Gamma")) {
+                        float gamma = getFloatAttribute(child, "value");
+                        gAMA_present = true;
+                        gAMA_gamma = (int)(gamma*100000 + 0.5);
+                    } else if (childName.equals("Palette")) {
+                        byte[] red = new byte[256];
+                        byte[] green = new byte[256];
+                        byte[] blue = new byte[256];
+                        int maxindex = -1;
+
+                        Node entry = child.getFirstChild();
+                        while (entry != null) {
+                            int index = getIntAttribute(entry, "index");
+                            if (index >= 0 && index <= 255) {
+                                red[index] =
+                                    (byte)getIntAttribute(entry, "red");
+                                green[index] =
+                                    (byte)getIntAttribute(entry, "green");
+                                blue[index] =
+                                    (byte)getIntAttribute(entry, "blue");
+                                if (index > maxindex) {
+                                    maxindex = index;
+                                }
+                            }
+                            entry = entry.getNextSibling();
+                        }
+
+                        int numEntries = maxindex + 1;
+                        PLTE_red = new byte[numEntries];
+                        PLTE_green = new byte[numEntries];
+                        PLTE_blue = new byte[numEntries];
+                        System.arraycopy(red, 0, PLTE_red, 0, numEntries);
+                        System.arraycopy(green, 0, PLTE_green, 0, numEntries);
+                        System.arraycopy(blue, 0, PLTE_blue, 0, numEntries);
+                        PLTE_present = true;
+                    } else if (childName.equals("BackgroundIndex")) {
+                        bKGD_present = true;
+                        bKGD_colorType = PNGImageReader.PNG_COLOR_PALETTE;
+                        bKGD_index = getIntAttribute(child, "value");
+                    } else if (childName.equals("BackgroundColor")) {
+                        int red = getIntAttribute(child, "red");
+                        int green = getIntAttribute(child, "green");
+                        int blue = getIntAttribute(child, "blue");
+                        if (red == green && red == blue) {
+                            bKGD_colorType = PNGImageReader.PNG_COLOR_GRAY;
+                            bKGD_gray = red;
+                        } else {
+                            bKGD_red = red;
+                            bKGD_green = green;
+                            bKGD_blue = blue;
+                        }
+                        bKGD_present = true;
+                    }
+//                  } else if (childName.equals("ColorSpaceType")) {
+//                  } else if (childName.equals("NumChannels")) {
+
+                    child = child.getNextSibling();
+                }
+            } else if (name.equals("Compression")) {
+                Node child = node.getFirstChild();
+                while (child != null) {
+                    String childName = child.getNodeName();
+                    if (childName.equals("NumProgressiveScans")) {
+                        // Use Adam7 if NumProgressiveScans > 1
+                        int scans = getIntAttribute(child, "value");
+                        IHDR_interlaceMethod = (scans > 1) ? 1 : 0;
+//                  } else if (childName.equals("CompressionTypeName")) {
+//                  } else if (childName.equals("Lossless")) {
+//                  } else if (childName.equals("BitRate")) {
+                    }
+                    child = child.getNextSibling();
+                }
+            } else if (name.equals("Data")) {
+                Node child = node.getFirstChild();
+                while (child != null) {
+                    String childName = child.getNodeName();
+                    if (childName.equals("BitsPerSample")) {
+                        String s = getAttribute(child, "value");
+                        StringTokenizer t = new StringTokenizer(s);
+                        int maxBits = -1;
+                        while (t.hasMoreTokens()) {
+                            int bits = Integer.parseInt(t.nextToken());
+                            if (bits > maxBits) {
+                                maxBits = bits;
+                            }
+                        }
+                        if (maxBits < 1) {
+                            maxBits = 1;
+                        }
+                        if (maxBits == 3) maxBits = 4;
+                        if (maxBits > 4 || maxBits < 8) {
+                            maxBits = 8;
+                        }
+                        if (maxBits > 8) {
+                            maxBits = 16;
+                        }
+                        IHDR_bitDepth = maxBits;
+                    } else if (childName.equals("SignificantBitsPerSample")) {
+                        String s = getAttribute(child, "value");
+                        StringTokenizer t = new StringTokenizer(s);
+                        int numTokens = t.countTokens();
+                        if (numTokens == 1) {
+                            sBIT_colorType = PNGImageReader.PNG_COLOR_GRAY;
+                            sBIT_grayBits = Integer.parseInt(t.nextToken());
+                        } else if (numTokens == 2) {
+                            sBIT_colorType =
+                              PNGImageReader.PNG_COLOR_GRAY_ALPHA;
+                            sBIT_grayBits = Integer.parseInt(t.nextToken());
+                            sBIT_alphaBits = Integer.parseInt(t.nextToken());
+                        } else if (numTokens == 3) {
+                            sBIT_colorType = PNGImageReader.PNG_COLOR_RGB;
+                            sBIT_redBits = Integer.parseInt(t.nextToken());
+                            sBIT_greenBits = Integer.parseInt(t.nextToken());
+                            sBIT_blueBits = Integer.parseInt(t.nextToken());
+                        } else if (numTokens == 4) {
+                            sBIT_colorType =
+                              PNGImageReader.PNG_COLOR_RGB_ALPHA;
+                            sBIT_redBits = Integer.parseInt(t.nextToken());
+                            sBIT_greenBits = Integer.parseInt(t.nextToken());
+                            sBIT_blueBits = Integer.parseInt(t.nextToken());
+                            sBIT_alphaBits = Integer.parseInt(t.nextToken());
+                        }
+                        if (numTokens >= 1 && numTokens <= 4) {
+                            sBIT_present = true;
+                        }
+//                      } else if (childName.equals("PlanarConfiguration")) {
+//                      } else if (childName.equals("SampleFormat")) {
+//                      } else if (childName.equals("SampleMSB")) {
+                    }
+                    child = child.getNextSibling();
+                }
+            } else if (name.equals("Dimension")) {
+                boolean gotWidth = false;
+                boolean gotHeight = false;
+                boolean gotAspectRatio = false;
+
+                float width = -1.0F;
+                float height = -1.0F;
+                float aspectRatio = -1.0F;
+
+                Node child = node.getFirstChild();
+                while (child != null) {
+                    String childName = child.getNodeName();
+                    if (childName.equals("PixelAspectRatio")) {
+                        aspectRatio = getFloatAttribute(child, "value");
+                        gotAspectRatio = true;
+                    } else if (childName.equals("HorizontalPixelSize")) {
+                        width = getFloatAttribute(child, "value");
+                        gotWidth = true;
+                    } else if (childName.equals("VerticalPixelSize")) {
+                        height = getFloatAttribute(child, "value");
+                        gotHeight = true;
+//                  } else if (childName.equals("ImageOrientation")) {
+//                  } else if
+//                      (childName.equals("HorizontalPhysicalPixelSpacing")) {
+//                  } else if
+//                      (childName.equals("VerticalPhysicalPixelSpacing")) {
+//                  } else if (childName.equals("HorizontalPosition")) {
+//                  } else if (childName.equals("VerticalPosition")) {
+//                  } else if (childName.equals("HorizontalPixelOffset")) {
+//                  } else if (childName.equals("VerticalPixelOffset")) {
+                    }
+                    child = child.getNextSibling();
+                }
+
+                if (gotWidth && gotHeight) {
+                    pHYs_present = true;
+                    pHYs_unitSpecifier = 1;
+                    pHYs_pixelsPerUnitXAxis = (int)(width*1000 + 0.5F);
+                    pHYs_pixelsPerUnitYAxis = (int)(height*1000 + 0.5F);
+                } else if (gotAspectRatio) {
+                    pHYs_present = true;
+                    pHYs_unitSpecifier = 0;
+
+                    // Find a reasonable rational approximation
+                    int denom = 1;
+                    for (; denom < 100; denom++) {
+                        int num = (int)(aspectRatio*denom);
+                        if (Math.abs(num/denom - aspectRatio) < 0.001) {
+                            break;
+                        }
+                    }
+                    pHYs_pixelsPerUnitXAxis = (int)(aspectRatio*denom);
+                    pHYs_pixelsPerUnitYAxis = denom;
+                }
+            } else if (name.equals("Document")) {
+                Node child = node.getFirstChild();
+                while (child != null) {
+                    String childName = child.getNodeName();
+                    if (childName.equals("ImageModificationTime")) {
+                        tIME_present = true;
+                        tIME_year = getIntAttribute(child, "year");
+                        tIME_month = getIntAttribute(child, "month");
+                        tIME_day = getIntAttribute(child, "day");
+                        tIME_hour =
+                            getIntAttribute(child, "hour", 0, false);
+                        tIME_minute =
+                            getIntAttribute(child, "minute", 0, false);
+                        tIME_second =
+                            getIntAttribute(child, "second", 0, false);
+//                  } else if (childName.equals("SubimageInterpretation")) {
+//                  } else if (childName.equals("ImageCreationTime")) {
+                    }
+                    child = child.getNextSibling();
+                }
+            } else if (name.equals("Text")) {
+                Node child = node.getFirstChild();
+                while (child != null) {
+                    String childName = child.getNodeName();
+                    if (childName.equals("TextEntry")) {
+                        String keyword = getAttribute(child, "keyword");
+                        String value = getAttribute(child, "value");
+                        String encoding = getAttribute(child, "encoding");
+                        String language = getAttribute(child, "language");
+                        String compression =
+                            getAttribute(child, "compression");
+
+                        if (isISOLatin(value)) {
+                            if (compression.equals("zip")) {
+                                // Use a zTXt node
+                                zTXt_keyword.add(keyword);
+                                zTXt_text.add(value);
+                                zTXt_compressionMethod.add(new Integer(0));
+                            } else {
+                                // Use a tEXt node
+                                tEXt_keyword.add(keyword);
+                                tEXt_text.add(value);
+                            }
+                        } else {
+                            // Use an iTXt node
+                            iTXt_keyword.add(keyword);
+                            iTXt_compressionFlag.add(Boolean.valueOf(compression.equals("zip")));
+                            iTXt_compressionMethod.add(Integer.valueOf(0));
+                            iTXt_languageTag.add(language);
+                            iTXt_translatedKeyword.add(keyword); // fake it
+                            iTXt_text.add(value);
+                        }
+                    }
+                    child = child.getNextSibling();
+                }
+//          } else if (name.equals("Transparency")) {
+//              Node child = node.getFirstChild();
+//              while (child != null) {
+//                  String childName = child.getNodeName();
+//                  if (childName.equals("Alpha")) {
+//                  } else if (childName.equals("TransparentIndex")) {
+//                  } else if (childName.equals("TransparentColor")) {
+//                  } else if (childName.equals("TileTransparencies")) {
+//                  } else if (childName.equals("TileOpacities")) {
+//                  }
+//                  child = child.getNextSibling();
+//              }
+//          } else {
+//              // fatal(node, "Unknown child of root node!");
+            }
+
+            node = node.getNextSibling();
+        }
+    }
+
+    // Reset all instance variables to their initial state
+    public void reset() {
+        IHDR_present = false;
+        PLTE_present = false;
+        bKGD_present = false;
+        cHRM_present = false;
+        gAMA_present = false;
+        hIST_present = false;
+        iCCP_present = false;
+        iTXt_keyword = new ArrayList<String>();
+        iTXt_compressionFlag = new ArrayList<Boolean>();
+        iTXt_compressionMethod = new ArrayList<Integer>();
+        iTXt_languageTag = new ArrayList<String>();
+        iTXt_translatedKeyword = new ArrayList<String>();
+        iTXt_text = new ArrayList<String>();
+        pHYs_present = false;
+        sBIT_present = false;
+        sPLT_present = false;
+        sRGB_present = false;
+        tEXt_keyword = new ArrayList();
+        tEXt_text = new ArrayList();
+        tIME_present = false;
+        tRNS_present = false;
+        zTXt_keyword = new ArrayList();
+        zTXt_compressionMethod = new ArrayList();
+        zTXt_text = new ArrayList();
+        unknownChunkType = new ArrayList();
+        unknownChunkData = new ArrayList();
+    }
+}
diff -urN openjdk.orig/jdk/test/javax/imageio/plugins/png/ItxtUtf8Test.java openjdk/jdk/test/javax/imageio/plugins/png/ItxtUtf8Test.java
--- openjdk.orig/jdk/test/javax/imageio/plugins/png/ItxtUtf8Test.java	1969-12-31 19:00:00.000000000 -0500
+++ openjdk/jdk/test/javax/imageio/plugins/png/ItxtUtf8Test.java	2010-12-24 11:25:19.144101998 -0500
@@ -0,0 +1,241 @@
+/*
+ * Copyright (c) 2008, 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.
+ *
+ * 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.
+ */
+
+/**
+ * @test
+ * @bug 6541476 6782079
+ * @summary Write and read a PNG file including an non-latin1 iTXt chunk
+ *          Test also verifies that trunkated png images does not cause
+ *          an OoutOfMemory error.
+ *
+ * @run main ItxtUtf8Test
+ *
+ * @run main/othervm/timeout=10 -Xmx2m ItxtUtf8Test truncate
+ */
+
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.OutputStream;
+import java.util.Arrays;
+import java.util.List;
+import javax.imageio.IIOException;
+import javax.imageio.IIOImage;
+import javax.imageio.ImageIO;
+import javax.imageio.ImageReader;
+import javax.imageio.ImageTypeSpecifier;
+import javax.imageio.ImageWriter;
+import javax.imageio.metadata.IIOMetadata;
+import javax.imageio.stream.ImageInputStream;
+import javax.imageio.stream.ImageOutputStream;
+import javax.imageio.stream.MemoryCacheImageInputStream;
+import javax.imageio.stream.MemoryCacheImageOutputStream;
+import org.w3c.dom.DOMImplementation;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.bootstrap.DOMImplementationRegistry;
+
+public class ItxtUtf8Test {
+
+    public static final String
+    TEXT = "\u24c9\u24d4\u24e7\u24e3" +
+      "\ud835\udc13\ud835\udc1e\ud835\udc31\ud835\udc2d" +
+      "\u24c9\u24d4\u24e7\u24e3", // a repetition for compression
+    VERBATIM = "\u24e5\u24d4\u24e1\u24d1\u24d0\u24e3\u24d8\u24dc",
+    COMPRESSED = "\u24d2\u24de\u24dc\u24df\u24e1\u24d4\u24e2\u24e2\u24d4\u24d3";
+
+    public static final byte[]
+    VBYTES = {
+        (byte)0x00, (byte)0x00, (byte)0x00, (byte)0x56, // chunk length
+        (byte)0x69, (byte)0x54, (byte)0x58, (byte)0x74, // chunk type "iTXt"
+        (byte)0x76, (byte)0x65, (byte)0x72, (byte)0x62,
+        (byte)0x61, (byte)0x74, (byte)0x69, (byte)0x6d, // keyword "verbatim"
+        (byte)0x00, // separator terminating keyword
+        (byte)0x00, // compression flag
+        (byte)0x00, // compression method, must be zero
+        (byte)0x78, (byte)0x2d, (byte)0x63, (byte)0x69,
+        (byte)0x72, (byte)0x63, (byte)0x6c, (byte)0x65,
+        (byte)0x64, // language tag "x-circled"
+        (byte)0x00, // separator terminating language tag
+        (byte)0xe2, (byte)0x93, (byte)0xa5, // '\u24e5'
+        (byte)0xe2, (byte)0x93, (byte)0x94, // '\u24d4'
+        (byte)0xe2, (byte)0x93, (byte)0xa1, // '\u24e1'
+        (byte)0xe2, (byte)0x93, (byte)0x91, // '\u24d1'
+        (byte)0xe2, (byte)0x93, (byte)0x90, // '\u24d0'
+        (byte)0xe2, (byte)0x93, (byte)0xa3, // '\u24e3'
+        (byte)0xe2, (byte)0x93, (byte)0x98, // '\u24d8'
+        (byte)0xe2, (byte)0x93, (byte)0x9c, // '\u24dc'
+        (byte)0x00, // separator terminating the translated keyword
+        (byte)0xe2, (byte)0x93, (byte)0x89, // '\u24c9'
+        (byte)0xe2, (byte)0x93, (byte)0x94, // '\u24d4'
+        (byte)0xe2, (byte)0x93, (byte)0xa7, // '\u24e7'
+        (byte)0xe2, (byte)0x93, (byte)0xa3, // '\u24e3'
+        (byte)0xf0, (byte)0x9d, (byte)0x90, (byte)0x93, // '\ud835\udc13'
+        (byte)0xf0, (byte)0x9d, (byte)0x90, (byte)0x9e, // '\ud835\udc1e'
+        (byte)0xf0, (byte)0x9d, (byte)0x90, (byte)0xb1, // '\ud835\udc31'
+        (byte)0xf0, (byte)0x9d, (byte)0x90, (byte)0xad, // '\ud835\udc2d'
+        (byte)0xe2, (byte)0x93, (byte)0x89, // '\u24c9'
+        (byte)0xe2, (byte)0x93, (byte)0x94, // '\u24d4'
+        (byte)0xe2, (byte)0x93, (byte)0xa7, // '\u24e7'
+        (byte)0xe2, (byte)0x93, (byte)0xa3, // '\u24e3'
+        (byte)0xb5, (byte)0xcc, (byte)0x97, (byte)0x56 // CRC
+    },
+    CBYTES = {
+        // we don't want to check the chunk length,
+        // as this might depend on implementation.
+        (byte)0x69, (byte)0x54, (byte)0x58, (byte)0x74, // chunk type "iTXt"
+        (byte)0x63, (byte)0x6f, (byte)0x6d, (byte)0x70,
+        (byte)0x72, (byte)0x65, (byte)0x73, (byte)0x73,
+        (byte)0x65, (byte)0x64, // keyword "compressed"
+        (byte)0x00, // separator terminating keyword
+        (byte)0x01, // compression flag
+        (byte)0x00, // compression method, 0=deflate
+        (byte)0x78, (byte)0x2d, (byte)0x63, (byte)0x69,
+        (byte)0x72, (byte)0x63, (byte)0x6c, (byte)0x65,
+        (byte)0x64, // language tag "x-circled"
+        (byte)0x00, // separator terminating language tag
+        // we don't want to check the actual compressed data,
+        // as this might depend on implementation.
+    };
+/*
+*/
+
+    public static void main(String[] args) throws Exception {
+        List argList = Arrays.asList(args);
+        if (argList.contains("truncate")) {
+            try {
+                runTest(false, true);
+                throw new AssertionError("Expect an error for truncated file");
+            }
+            catch (IIOException e) {
+                // expected an error for a truncated image file.
+            }
+        }
+        else {
+            runTest(argList.contains("dump"), false);
+        }
+    }
+
+    public static void runTest(boolean dump, boolean truncate)
+        throws Exception
+    {
+        String format = "javax_imageio_png_1.0";
+        BufferedImage img =
+            new BufferedImage(16, 16, BufferedImage.TYPE_INT_RGB);
+        ImageWriter iw = ImageIO.getImageWritersByMIMEType("image/png").next();
+        ByteArrayOutputStream os = new ByteArrayOutputStream();
+        ImageOutputStream ios = new MemoryCacheImageOutputStream(os);
+        iw.setOutput(ios);
+        IIOMetadata meta =
+            iw.getDefaultImageMetadata(new ImageTypeSpecifier(img), null);
+        DOMImplementationRegistry registry;
+        registry = DOMImplementationRegistry.newInstance();
+        DOMImplementation impl = registry.getDOMImplementation("XML 3.0");
+        Document doc = impl.createDocument(null, format, null);
+        Element root, itxt, entry;
+        root = doc.getDocumentElement();
+        root.appendChild(itxt = doc.createElement("iTXt"));
+        itxt.appendChild(entry = doc.createElement("iTXtEntry"));
+        entry.setAttribute("keyword", "verbatim");
+        entry.setAttribute("compressionFlag", "false");
+        entry.setAttribute("compressionMethod", "0");
+        entry.setAttribute("languageTag", "x-circled");
+        entry.setAttribute("translatedKeyword", VERBATIM);
+        entry.setAttribute("text", TEXT);
+        itxt.appendChild(entry = doc.createElement("iTXtEntry"));
+        entry.setAttribute("keyword", "compressed");
+        entry.setAttribute("compressionFlag", "true");
+        entry.setAttribute("compressionMethod", "0");
+        entry.setAttribute("languageTag", "x-circled");
+        entry.setAttribute("translatedKeyword", COMPRESSED);
+        entry.setAttribute("text", TEXT);
+        meta.mergeTree(format, root);
+        iw.write(new IIOImage(img, null, meta));
+        iw.dispose();
+
+        byte[] bytes = os.toByteArray();
+        if (dump)
+            System.out.write(bytes);
+        if (findBytes(VBYTES, bytes) < 0)
+            throw new AssertionError("verbatim block not found");
+        if (findBytes(CBYTES, bytes) < 0)
+            throw new AssertionError("compressed block not found");
+        int length = bytes.length;
+        if (truncate)
+            length = findBytes(VBYTES, bytes) + 32;
+
+        ImageReader ir = ImageIO.getImageReader(iw);
+        ByteArrayInputStream is = new ByteArrayInputStream(bytes, 0, length);
+        ImageInputStream iis = new MemoryCacheImageInputStream(is);
+        ir.setInput(iis);
+        meta = ir.getImageMetadata(0);
+        Node node = meta.getAsTree(format);
+        for (node = node.getFirstChild();
+             !"iTXt".equals(node.getNodeName());
+             node = node.getNextSibling());
+        boolean verbatimSeen = false, compressedSeen = false;
+        for (node = node.getFirstChild();
+             node != null;
+             node = node.getNextSibling()) {
+            entry = (Element)node;
+            String keyword = entry.getAttribute("keyword");
+            String translatedKeyword = entry.getAttribute("translatedKeyword");
+            String text = entry.getAttribute("text");
+            if ("verbatim".equals(keyword)) {
+                if (verbatimSeen) throw new AssertionError("Duplicate");
+                verbatimSeen = true;
+                if (!VERBATIM.equals(translatedKeyword))
+                    throw new AssertionError("Wrong translated keyword");
+                if (!TEXT.equals(text))
+                    throw new AssertionError("Wrong text");
+            }
+            else if ("compressed".equals(keyword)) {
+                if (compressedSeen) throw new AssertionError("Duplicate");
+                compressedSeen = true;
+                if (!COMPRESSED.equals(translatedKeyword))
+                    throw new AssertionError("Wrong translated keyword");
+                if (!TEXT.equals(text))
+                    throw new AssertionError("Wrong text");
+            }
+            else {
+                throw new AssertionError("Unexpected keyword");
+            }
+        }
+        if (!(verbatimSeen && compressedSeen))
+            throw new AssertionError("Missing chunk");
+    }
+
+    private static final int findBytes(byte[] needle, byte[] haystack) {
+        HAYSTACK: for (int h = 0; h <= haystack.length - needle.length; ++h) {
+            for (int n = 0; n < needle.length; ++n) {
+                if (needle[n] != haystack[h + n]) {
+                    continue HAYSTACK;
+                }
+            }
+            return h;
+        }
+        return -1;
+    }
+
+}