org.apache.xmlgraphics.image.codec.png.PNGRed.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.xmlgraphics.image.codec.png.PNGRed.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/* $Id: PNGRed.java 1345683 2012-06-03 14:50:33Z gadams $ */

package org.apache.xmlgraphics.image.codec.png;

import java.awt.Color;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Transparency;
import java.awt.color.ColorSpace;
import java.awt.image.ColorModel;
import java.awt.image.ComponentColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferUShort;
import java.awt.image.IndexColorModel;
import java.awt.image.Raster;
import java.awt.image.SampleModel;
import java.awt.image.WritableRaster;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.SequenceInputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;

import lombok.extern.slf4j.Slf4j;

import org.apache.commons.io.IOUtils;
import org.apache.xmlgraphics.image.GraphicsUtil;
import org.apache.xmlgraphics.image.codec.util.PropertyUtil;
import org.apache.xmlgraphics.image.rendered.AbstractRed;
import org.apache.xmlgraphics.image.rendered.CachableRed;

// CSOFF: ConstantName
// CSOFF: InnerAssignment
// CSOFF: MethodName
// CSOFF: MissingSwitchDefault
// CSOFF: MultipleVariableDeclarations
// CSOFF: NoWhitespaceAfter
// CSOFF: OperatorWrap
// CSOFF: WhitespaceAround

/**
 * @version $Id: PNGRed.java 1345683 2012-06-03 14:50:33Z gadams $
 */
@Slf4j
public class PNGRed extends AbstractRed {

    static class PNGChunk {
        int length;
        int type;
        byte[] data;
        int crc;

        String typeString;

        public PNGChunk(final int length, final int type, final byte[] data, final int crc) {
            this.length = length;
            this.type = type;
            this.data = data;
            this.crc = crc;

            this.typeString = "";
            this.typeString += (char) (type >> 24);
            this.typeString += (char) (type >> 16 & 0xff);
            this.typeString += (char) (type >> 8 & 0xff);
            this.typeString += (char) (type & 0xff);
        }

        public int getLength() {
            return this.length;
        }

        public int getType() {
            return this.type;
        }

        public String getTypeString() {
            return this.typeString;
        }

        public byte[] getData() {
            return this.data;
        }

        public byte getByte(final int offset) {
            return this.data[offset];
        }

        public int getInt1(final int offset) {
            return this.data[offset] & 0xff;
        }

        public int getInt2(final int offset) {
            return (this.data[offset] & 0xff) << 8 | this.data[offset + 1] & 0xff;
        }

        public int getInt4(final int offset) {
            return (this.data[offset] & 0xff) << 24 | (this.data[offset + 1] & 0xff) << 16
                    | (this.data[offset + 2] & 0xff) << 8 | this.data[offset + 3] & 0xff;
        }

        public String getString4(final int offset) {
            String s = new String();
            s += (char) this.data[offset];
            s += (char) this.data[offset + 1];
            s += (char) this.data[offset + 2];
            s += (char) this.data[offset + 3];
            return s;
        }

        public boolean isType(final String typeName) {
            return this.typeString.equals(typeName);
        }
    }

    public static final int PNG_COLOR_GRAY = 0;
    public static final int PNG_COLOR_RGB = 2;
    public static final int PNG_COLOR_PALETTE = 3;
    public static final int PNG_COLOR_GRAY_ALPHA = 4;
    public static final int PNG_COLOR_RGB_ALPHA = 6;

    private static final String[] colorTypeNames = { "Grayscale", "Error", "Truecolor", "Index",
            "Grayscale with alpha", "Error", "Truecolor with alpha" };

    public static final int PNG_FILTER_NONE = 0;
    public static final int PNG_FILTER_SUB = 1;
    public static final int PNG_FILTER_UP = 2;
    public static final int PNG_FILTER_AVERAGE = 3;
    public static final int PNG_FILTER_PAETH = 4;

    private 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 int bitDepth;
    private int colorType;

    private int compressionMethod;
    private int filterMethod;
    private int interlaceMethod;

    private int paletteEntries;
    private byte[] redPalette;
    private byte[] greenPalette;
    private byte[] bluePalette;
    private byte[] alphaPalette;

    private int bkgdRed;
    private int bkgdGreen;
    private int bkgdBlue;

    private int grayTransparentAlpha;
    private int redTransparentAlpha;
    private int greenTransparentAlpha;
    private int blueTransparentAlpha;

    private int maxOpacity;

    private int[] significantBits = null;

    // Parameter information

    // If true, the user wants destination alpha where applicable.
    private boolean suppressAlpha = false;

    // If true, perform palette lookup internally
    private boolean expandPalette = false;

    // If true, output < 8 bit gray images in 8 bit components format
    private boolean output8BitGray = false;

    // Create an alpha channel in the destination color model.
    private boolean outputHasAlphaPalette = false;

    // Perform gamma correction on the image
    private boolean performGammaCorrection = false;

    // Expand GA to GGGA for compatbility with Java2D
    private boolean expandGrayAlpha = false;

    // Produce an instance of PNGEncodeParam
    private boolean generateEncodeParam = false;

    // PNGDecodeParam controlling decode process
    private PNGDecodeParam decodeParam = null;

    // PNGEncodeParam to store file details in
    private PNGEncodeParam encodeParam = null;

    private final boolean emitProperties = true;

    private float fileGamma = 45455 / 100000.0F;

    private float userExponent = 1.0F;

    private float displayExponent = 2.2F;

    private float[] chromaticity = null;

    private int sRGBRenderingIntent = -1;

    // Post-processing step implied by above parameters
    private int postProcess = POST_NONE;

    // Possible post-processing steps

    // Do nothing
    private static final int POST_NONE = 0;

    // Gamma correct only
    private static final int POST_GAMMA = 1;

    // Push gray values through grayLut to expand to 8 bits
    private static final int POST_GRAY_LUT = 2;

    // Push gray values through grayLut to expand to 8 bits, add alpha
    private static final int POST_GRAY_LUT_ADD_TRANS = 3;

    // Push palette value through R,G,B lookup tables
    private static final int POST_PALETTE_TO_RGB = 4;

    // Push palette value through R,G,B,A lookup tables
    private static final int POST_PALETTE_TO_RGBA = 5;

    // Add transparency to a given gray value (w/ optional gamma)
    private static final int POST_ADD_GRAY_TRANS = 6;

    // Add transparency to a given RGB value (w/ optional gamma)
    private static final int POST_ADD_RGB_TRANS = 7;

    // Remove the alpha channel from a gray image (w/ optional gamma)
    private static final int POST_REMOVE_GRAY_TRANS = 8;

    // Remove the alpha channel from an RGB image (w/optional gamma)
    private static final int POST_REMOVE_RGB_TRANS = 9;

    // Mask to add expansion of GA -> GGGA
    private static final int POST_EXP_MASK = 16;

    // Expand gray to G/G/G
    private static final int POST_GRAY_ALPHA_EXP = POST_NONE | POST_EXP_MASK;

    // Expand gray to G/G/G through a gamma lut
    private static final int POST_GAMMA_EXP = POST_GAMMA | POST_EXP_MASK;

    // Push gray values through grayLut to expand to 8 bits, expand, add alpha
    private static final int POST_GRAY_LUT_ADD_TRANS_EXP = POST_GRAY_LUT_ADD_TRANS | POST_EXP_MASK;

    // Add transparency to a given gray value, expand
    private static final int POST_ADD_GRAY_TRANS_EXP = POST_ADD_GRAY_TRANS | POST_EXP_MASK;

    private List<InputStream> streamVec = new ArrayList<>();
    private DataInputStream dataStream;

    private int bytesPerPixel; // number of bytes per input pixel
    private int inputBands;
    private int outputBands;

    // Number of private chunks
    private int chunkIndex = 0;

    private final List<String> textKeys = new ArrayList<>();
    private final List<String> textStrings = new ArrayList<>();

    private final List<String> ztextKeys = new ArrayList<>();
    private final List<String> ztextStrings = new ArrayList<>();

    private WritableRaster theTile;
    private Rectangle bounds;

    /** A Hashtable containing the image properties. */
    private final Map<String, Object> properties = new HashMap<>();

    private int[] gammaLut = null;

    private void initGammaLut(final int bits) {
        final double exp = (double) this.userExponent / (this.fileGamma * this.displayExponent);
        final int numSamples = 1 << bits;
        final int maxOutSample = bits == 16 ? 65535 : 255;

        this.gammaLut = new int[numSamples];
        for (int i = 0; i < numSamples; ++i) {
            final double gbright = (double) i / (numSamples - 1);
            final double gamma = Math.pow(gbright, exp);
            int igamma = (int) (gamma * maxOutSample + 0.5);
            if (igamma > maxOutSample) {
                igamma = maxOutSample;
            }
            this.gammaLut[i] = igamma;
        }
    }

    private final byte[][] expandBits = { null, { (byte) 0x00, (byte) 0xff },
            { (byte) 0x00, (byte) 0x55, (byte) 0xaa, (byte) 0xff }, null,
            { (byte) 0x00, (byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44, (byte) 0x55, (byte) 0x66,
                    (byte) 0x77, (byte) 0x88, (byte) 0x99, (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, (byte) 0xdd,
                    (byte) 0xee, (byte) 0xff } };

    private int[] grayLut = null;

    private void initGrayLut(final int bits) {
        final int len = 1 << bits;
        this.grayLut = new int[len];

        if (this.performGammaCorrection) {
            System.arraycopy(this.gammaLut, 0, this.grayLut, 0, len);
        } else {
            for (int i = 0; i < len; ++i) {
                this.grayLut[i] = this.expandBits[bits][i];
            }
        }
    }

    public PNGRed(final InputStream stream) {
        this(stream, null);
    }

    public PNGRed(final InputStream inStream, final PNGDecodeParam inDecodeParam) {
        final InputStream stream;
        if (!inStream.markSupported()) {
            stream = new BufferedInputStream(inStream);
        } else {
            stream = inStream;
        }
        DataInputStream distream = null;
        try {
            distream = new DataInputStream(stream);

            final PNGDecodeParam decodeParam;
            if (inDecodeParam == null) {
                decodeParam = new PNGDecodeParam();
            } else {
                decodeParam = inDecodeParam;
            }
            this.decodeParam = decodeParam;

            // Get parameter values
            this.suppressAlpha = decodeParam.getSuppressAlpha();
            this.expandPalette = decodeParam.getExpandPalette();
            this.output8BitGray = decodeParam.getOutput8BitGray();
            this.expandGrayAlpha = decodeParam.getExpandGrayAlpha();
            if (decodeParam.getPerformGammaCorrection()) {
                this.userExponent = decodeParam.getUserExponent();
                this.displayExponent = decodeParam.getDisplayExponent();
                this.performGammaCorrection = true;
                this.output8BitGray = true;
            }
            this.generateEncodeParam = decodeParam.getGenerateEncodeParam();

            if (this.emitProperties) {
                this.properties.put("file_type", "PNG v. 1.0");
            }

            try {
                final long magic = distream.readLong();
                if (magic != 0x89504e470d0a1a0aL) {
                    final String msg = PropertyUtil.getString("PNGImageDecoder0");
                    throw new RuntimeException(msg);
                }
            } catch (final Exception e) {
                log.error("Exception", e);
                final String msg = PropertyUtil.getString("PNGImageDecoder1");
                throw new RuntimeException(msg);
            }

            do {
                try {
                    PNGChunk chunk;

                    final String chunkType = getChunkType(distream);
                    if (chunkType.equals("IHDR")) {
                        chunk = readChunk(distream);
                        parse_IHDR_chunk(chunk);
                    } else if (chunkType.equals("PLTE")) {
                        chunk = readChunk(distream);
                        parse_PLTE_chunk(chunk);
                    } else if (chunkType.equals("IDAT")) {
                        chunk = readChunk(distream);
                        this.streamVec.add(new ByteArrayInputStream(chunk.getData()));
                    } else if (chunkType.equals("IEND")) {
                        chunk = readChunk(distream);
                        parse_IEND_chunk(chunk);
                        break; // fall through to the bottom
                    } else if (chunkType.equals("bKGD")) {
                        chunk = readChunk(distream);
                        parse_bKGD_chunk(chunk);
                    } else if (chunkType.equals("cHRM")) {
                        chunk = readChunk(distream);
                        parse_cHRM_chunk(chunk);
                    } else if (chunkType.equals("gAMA")) {
                        chunk = readChunk(distream);
                        parse_gAMA_chunk(chunk);
                    } else if (chunkType.equals("hIST")) {
                        chunk = readChunk(distream);
                        parse_hIST_chunk(chunk);
                    } else if (chunkType.equals("iCCP")) {
                        chunk = readChunk(distream);
                        parse_iCCP_chunk(chunk);
                    } else if (chunkType.equals("pHYs")) {
                        chunk = readChunk(distream);
                        parse_pHYs_chunk(chunk);
                    } else if (chunkType.equals("sBIT")) {
                        chunk = readChunk(distream);
                        parse_sBIT_chunk(chunk);
                    } else if (chunkType.equals("sRGB")) {
                        chunk = readChunk(distream);
                        parse_sRGB_chunk(chunk);
                    } else if (chunkType.equals("tEXt")) {
                        chunk = readChunk(distream);
                        parse_tEXt_chunk(chunk);
                    } else if (chunkType.equals("tIME")) {
                        chunk = readChunk(distream);
                        parse_tIME_chunk(chunk);
                    } else if (chunkType.equals("tRNS")) {
                        chunk = readChunk(distream);
                        parse_tRNS_chunk(chunk);
                    } else if (chunkType.equals("zTXt")) {
                        chunk = readChunk(distream);
                        parse_zTXt_chunk(chunk);
                    } else {
                        chunk = readChunk(distream);
                        // Output the chunk data in raw form

                        final String type = chunk.getTypeString();
                        final byte[] data = chunk.getData();
                        if (this.encodeParam != null) {
                            this.encodeParam.addPrivateChunk(type, data);
                        }
                        if (this.emitProperties) {
                            final String key = "chunk_" + this.chunkIndex++ + ':' + type;
                            this.properties.put(key.toLowerCase(), data);
                        }
                    }
                } catch (final Exception e) {
                    log.error("Exception", e);
                    final String msg = PropertyUtil.getString("PNGImageDecoder2");
                    throw new RuntimeException(msg);
                }
            } while (true);

            // Final post-processing

            if (this.significantBits == null) {
                this.significantBits = new int[this.inputBands];
                for (int i = 0; i < this.inputBands; ++i) {
                    this.significantBits[i] = this.bitDepth;
                }

                if (this.emitProperties) {
                    this.properties.put("significant_bits", this.significantBits);
                }
            }
        } finally {
            IOUtils.closeQuietly(distream);
            IOUtils.closeQuietly(stream);
        }
    }

    private static String getChunkType(final DataInputStream distream) {
        try {
            distream.mark(8);
            /* int length = */distream.readInt();
            final int type = distream.readInt();
            distream.reset();

            final String typeString = "" + (char) (type >> 24 & 0xff) + (char) (type >> 16 & 0xff)
                    + (char) (type >> 8 & 0xff) + (char) (type & 0xff);
            return typeString;
        } catch (final Exception e) {
            log.error("Exception", e);
            return null;
        }
    }

    private static PNGChunk readChunk(final DataInputStream distream) {
        try {
            final int length = distream.readInt();
            final int type = distream.readInt();
            final byte[] data = new byte[length];
            distream.readFully(data);
            final int crc = distream.readInt();

            return new PNGChunk(length, type, data, crc);
        } catch (final Exception e) {
            log.error("Exception", e);
            return null;
        }
    }

    private void parse_IHDR_chunk(final PNGChunk chunk) {
        final int width = chunk.getInt4(0);
        final int height = chunk.getInt4(4);

        this.bounds = new Rectangle(0, 0, width, height);

        this.bitDepth = chunk.getInt1(8);

        final int validMask = 1 << 1 | 1 << 2 | 1 << 4 | 1 << 8 | 1 << 16;
        if ((1 << this.bitDepth & validMask) == 0) {
            // bitDepth is not one of { 1, 2, 4, 8, 16 }: Error -- bad bit depth
            final String msg = PropertyUtil.getString("PNGImageDecoder3");
            throw new RuntimeException(msg);
        }
        this.maxOpacity = (1 << this.bitDepth) - 1;

        this.colorType = chunk.getInt1(9);
        if (this.colorType != PNG_COLOR_GRAY && this.colorType != PNG_COLOR_RGB
                && this.colorType != PNG_COLOR_PALETTE && this.colorType != PNG_COLOR_GRAY_ALPHA
                && this.colorType != PNG_COLOR_RGB_ALPHA) {
            log.info(PropertyUtil.getString("PNGImageDecoder4"));
        }

        if (this.colorType == PNG_COLOR_RGB && this.bitDepth < 8) {
            // Error -- RGB images must have 8 or 16 bits
            final String msg = PropertyUtil.getString("PNGImageDecoder5");
            throw new RuntimeException(msg);
        }

        if (this.colorType == PNG_COLOR_PALETTE && this.bitDepth == 16) {
            // Error -- palette images must have < 16 bits
            final String msg = PropertyUtil.getString("PNGImageDecoder6");
            throw new RuntimeException(msg);
        }

        if (this.colorType == PNG_COLOR_GRAY_ALPHA && this.bitDepth < 8) {
            // Error -- gray/alpha images must have >= 8 bits
            final String msg = PropertyUtil.getString("PNGImageDecoder7");
            throw new RuntimeException(msg);
        }

        if (this.colorType == PNG_COLOR_RGB_ALPHA && this.bitDepth < 8) {
            // Error -- RGB/alpha images must have >= 8 bits
            final String msg = PropertyUtil.getString("PNGImageDecoder8");
            throw new RuntimeException(msg);
        }

        if (this.emitProperties) {
            this.properties.put("color_type", colorTypeNames[this.colorType]);
        }

        if (this.generateEncodeParam) {
            if (this.colorType == PNG_COLOR_PALETTE) {
                this.encodeParam = new PNGEncodeParam.Palette();
            } else if (this.colorType == PNG_COLOR_GRAY || this.colorType == PNG_COLOR_GRAY_ALPHA) {
                this.encodeParam = new PNGEncodeParam.Gray();
            } else {
                this.encodeParam = new PNGEncodeParam.RGB();
            }
            this.decodeParam.setEncodeParam(this.encodeParam);
        }

        if (this.encodeParam != null) {
            this.encodeParam.setBitDepth(this.bitDepth);
        }
        if (this.emitProperties) {
            this.properties.put("bit_depth", this.bitDepth);
        }

        if (this.performGammaCorrection) {
            // Assume file gamma is 1/2.2 unless we get a gAMA chunk
            final float gamma = 1.0F / 2.2F * (this.displayExponent / this.userExponent);
            if (this.encodeParam != null) {
                this.encodeParam.setGamma(gamma);
            }
            if (this.emitProperties) {
                this.properties.put("gamma", new Float(gamma));
            }
        }

        this.compressionMethod = chunk.getInt1(10);
        if (this.compressionMethod != 0) {
            // Error -- only know about compression method 0
            final String msg = PropertyUtil.getString("PNGImageDecoder9");
            throw new RuntimeException(msg);
        }

        this.filterMethod = chunk.getInt1(11);
        if (this.filterMethod != 0) {
            // Error -- only know about filter method 0
            final String msg = PropertyUtil.getString("PNGImageDecoder10");
            throw new RuntimeException(msg);
        }

        this.interlaceMethod = chunk.getInt1(12);
        if (this.interlaceMethod == 0) {
            if (this.encodeParam != null) {
                this.encodeParam.setInterlacing(false);
            }
            if (this.emitProperties) {
                this.properties.put("interlace_method", "None");
            }
        } else if (this.interlaceMethod == 1) {
            if (this.encodeParam != null) {
                this.encodeParam.setInterlacing(true);
            }
            if (this.emitProperties) {
                this.properties.put("interlace_method", "Adam7");
            }
        } else {
            // Error -- only know about Adam7 interlacing
            final String msg = PropertyUtil.getString("PNGImageDecoder11");
            throw new RuntimeException(msg);
        }

        this.bytesPerPixel = this.bitDepth == 16 ? 2 : 1;

        switch (this.colorType) {
        case PNG_COLOR_GRAY:
            this.inputBands = 1;
            this.outputBands = 1;

            if (this.output8BitGray && this.bitDepth < 8) {
                this.postProcess = POST_GRAY_LUT;
            } else if (this.performGammaCorrection) {
                this.postProcess = POST_GAMMA;
            } else {
                this.postProcess = POST_NONE;
            }
            break;

        case PNG_COLOR_RGB:
            this.inputBands = 3;
            this.bytesPerPixel *= 3;
            this.outputBands = 3;

            if (this.performGammaCorrection) {
                this.postProcess = POST_GAMMA;
            } else {
                this.postProcess = POST_NONE;
            }
            break;

        case PNG_COLOR_PALETTE:
            this.inputBands = 1;
            this.bytesPerPixel = 1;
            this.outputBands = this.expandPalette ? 3 : 1;

            if (this.expandPalette) {
                this.postProcess = POST_PALETTE_TO_RGB;
            } else {
                this.postProcess = POST_NONE;
            }
            break;

        case PNG_COLOR_GRAY_ALPHA:
            this.inputBands = 2;
            this.bytesPerPixel *= 2;

            if (this.suppressAlpha) {
                this.outputBands = 1;
                this.postProcess = POST_REMOVE_GRAY_TRANS;
            } else {
                if (this.performGammaCorrection) {
                    this.postProcess = POST_GAMMA;
                } else {
                    this.postProcess = POST_NONE;
                }
                if (this.expandGrayAlpha) {
                    this.postProcess |= POST_EXP_MASK;
                    this.outputBands = 4;
                } else {
                    this.outputBands = 2;
                }
            }
            break;

        case PNG_COLOR_RGB_ALPHA:
            this.inputBands = 4;
            this.bytesPerPixel *= 4;
            this.outputBands = !this.suppressAlpha ? 4 : 3;

            if (this.suppressAlpha) {
                this.postProcess = POST_REMOVE_RGB_TRANS;
            } else if (this.performGammaCorrection) {
                this.postProcess = POST_GAMMA;
            } else {
                this.postProcess = POST_NONE;
            }
            break;
        }
    }

    private void parse_IEND_chunk(final PNGChunk chunk) throws IOException {
        // Store text strings
        final int textLen = this.textKeys.size();
        final String[] textArray = new String[2 * textLen];
        for (int i = 0; i < textLen; ++i) {
            final String key = this.textKeys.get(i);
            final String val = this.textStrings.get(i);
            textArray[2 * i] = key;
            textArray[2 * i + 1] = val;
            if (this.emitProperties) {
                final String uniqueKey = "text_" + i + ':' + key;
                this.properties.put(uniqueKey.toLowerCase(), val);
            }
        }
        if (this.encodeParam != null) {
            this.encodeParam.setText(textArray);
        }

        // Store compressed text strings
        final int ztextLen = this.ztextKeys.size();
        final String[] ztextArray = new String[2 * ztextLen];
        for (int i = 0; i < ztextLen; ++i) {
            final String key = this.ztextKeys.get(i);
            final String val = this.ztextStrings.get(i);
            ztextArray[2 * i] = key;
            ztextArray[2 * i + 1] = val;
            if (this.emitProperties) {
                final String uniqueKey = "ztext_" + i + ':' + key;
                this.properties.put(uniqueKey.toLowerCase(), val);
            }
        }
        if (this.encodeParam != null) {
            this.encodeParam.setCompressedText(ztextArray);
        }

        // Parse prior IDAT chunks
        final InputStream seqStream = new SequenceInputStream(Collections.enumeration(this.streamVec));
        final InputStream infStream = new InflaterInputStream(seqStream, new Inflater());
        this.dataStream = new DataInputStream(infStream);

        // Create an empty WritableRaster
        int depth = this.bitDepth;
        if (this.colorType == PNG_COLOR_GRAY && this.bitDepth < 8 && this.output8BitGray) {
            depth = 8;
        }
        if (this.colorType == PNG_COLOR_PALETTE && this.expandPalette) {
            depth = 8;
        }
        final int width = this.bounds.width;
        final int height = this.bounds.height;

        final int bytesPerRow = (this.outputBands * width * depth + 7) / 8;
        final int scanlineStride = depth == 16 ? bytesPerRow / 2 : bytesPerRow;

        this.theTile = createRaster(width, height, this.outputBands, scanlineStride, depth);

        if (this.performGammaCorrection && this.gammaLut == null) {
            initGammaLut(this.bitDepth);
        }
        if (this.postProcess == POST_GRAY_LUT || this.postProcess == POST_GRAY_LUT_ADD_TRANS
                || this.postProcess == POST_GRAY_LUT_ADD_TRANS_EXP) {
            initGrayLut(this.bitDepth);
        }

        decodeImage(this.interlaceMethod == 1);

        // Free resources associated with compressed data.
        this.dataStream.close();
        infStream.close();
        seqStream.close();
        this.streamVec = null;

        final SampleModel sm = this.theTile.getSampleModel();
        ColorModel cm;

        if (this.colorType == PNG_COLOR_PALETTE && !this.expandPalette) {
            if (this.outputHasAlphaPalette) {
                cm = new IndexColorModel(this.bitDepth, this.paletteEntries, this.redPalette, this.greenPalette,
                        this.bluePalette, this.alphaPalette);
            } else {
                cm = new IndexColorModel(this.bitDepth, this.paletteEntries, this.redPalette, this.greenPalette,
                        this.bluePalette);
            }
        } else if (this.colorType == PNG_COLOR_GRAY && this.bitDepth < 8 && !this.output8BitGray) {
            final byte[] palette = this.expandBits[this.bitDepth];
            cm = new IndexColorModel(this.bitDepth, palette.length, palette, palette, palette);
        } else {
            cm = createComponentColorModel(sm);
        }

        init((CachableRed) null, this.bounds, cm, sm, 0, 0, this.properties);
    }

    private static final int[] GrayBits8 = { 8 };
    private static final ComponentColorModel colorModelGray8 = new ComponentColorModel(
            ColorSpace.getInstance(ColorSpace.CS_GRAY), GrayBits8, false, false, Transparency.OPAQUE,
            DataBuffer.TYPE_BYTE);

    private static final int[] GrayAlphaBits8 = { 8, 8 };
    private static final ComponentColorModel colorModelGrayAlpha8 = new ComponentColorModel(
            ColorSpace.getInstance(ColorSpace.CS_GRAY), GrayAlphaBits8, true, false, Transparency.TRANSLUCENT,
            DataBuffer.TYPE_BYTE);

    private static final int[] GrayBits16 = { 16 };
    private static final ComponentColorModel colorModelGray16 = new ComponentColorModel(
            ColorSpace.getInstance(ColorSpace.CS_GRAY), GrayBits16, false, false, Transparency.OPAQUE,
            DataBuffer.TYPE_USHORT);

    private static final int[] GrayAlphaBits16 = { 16, 16 };
    private static final ComponentColorModel colorModelGrayAlpha16 = new ComponentColorModel(
            ColorSpace.getInstance(ColorSpace.CS_GRAY), GrayAlphaBits16, true, false, Transparency.TRANSLUCENT,
            DataBuffer.TYPE_USHORT);

    private static final int[] GrayBits32 = { 32 };
    private static final ComponentColorModel colorModelGray32 = new ComponentColorModel(
            ColorSpace.getInstance(ColorSpace.CS_GRAY), GrayBits32, false, false, Transparency.OPAQUE,
            DataBuffer.TYPE_INT);

    private static final int[] GrayAlphaBits32 = { 32, 32 };
    private static final ComponentColorModel colorModelGrayAlpha32 = new ComponentColorModel(
            ColorSpace.getInstance(ColorSpace.CS_GRAY), GrayAlphaBits32, true, false, Transparency.TRANSLUCENT,
            DataBuffer.TYPE_INT);

    private static final int[] RGBBits8 = { 8, 8, 8 };
    private static final ComponentColorModel colorModelRGB8 = new ComponentColorModel(
            ColorSpace.getInstance(ColorSpace.CS_sRGB), RGBBits8, false, false, Transparency.OPAQUE,
            DataBuffer.TYPE_BYTE);

    private static final int[] RGBABits8 = { 8, 8, 8, 8 };
    private static final ComponentColorModel colorModelRGBA8 = new ComponentColorModel(
            ColorSpace.getInstance(ColorSpace.CS_sRGB), RGBABits8, true, false, Transparency.TRANSLUCENT,
            DataBuffer.TYPE_BYTE);

    private static final int[] RGBBits16 = { 16, 16, 16 };
    private static final ComponentColorModel colorModelRGB16 = new ComponentColorModel(
            ColorSpace.getInstance(ColorSpace.CS_sRGB), RGBBits16, false, false, Transparency.OPAQUE,
            DataBuffer.TYPE_USHORT);

    private static final int[] RGBABits16 = { 16, 16, 16, 16 };
    private static final ComponentColorModel colorModelRGBA16 = new ComponentColorModel(
            ColorSpace.getInstance(ColorSpace.CS_sRGB), RGBABits16, true, false, Transparency.TRANSLUCENT,
            DataBuffer.TYPE_USHORT);

    private static final int[] RGBBits32 = { 32, 32, 32 };
    private static final ComponentColorModel colorModelRGB32 = new ComponentColorModel(
            ColorSpace.getInstance(ColorSpace.CS_sRGB), RGBBits32, false, false, Transparency.OPAQUE,
            DataBuffer.TYPE_INT);

    private static final int[] RGBABits32 = { 32, 32, 32, 32 };
    private static final ComponentColorModel colorModelRGBA32 = new ComponentColorModel(
            ColorSpace.getInstance(ColorSpace.CS_sRGB), RGBABits32, true, false, Transparency.TRANSLUCENT,
            DataBuffer.TYPE_INT);

    /**
     * A convenience method to create an instance of
     * <code>ComponentColorModel</code> suitable for use with the given
     * <code>SampleModel</code>. The <code>SampleModel</code> should have a data
     * type of <code>DataBuffer.TYPE_BYTE</code>, <code>TYPE_USHORT</code>, or
     * <code>TYPE_INT</code> and between 1 and 4 bands. Depending on the number
     * of bands of the <code>SampleModel</code>, either a gray, gray+alpha, rgb,
     * or rgb+alpha <code>ColorModel</code> is returned.
     */
    public static ColorModel createComponentColorModel(final SampleModel sm) {
        final int type = sm.getDataType();
        final int bands = sm.getNumBands();
        ComponentColorModel cm = null;

        if (type == DataBuffer.TYPE_BYTE) {
            switch (bands) {
            case 1:
                cm = colorModelGray8;
                break;
            case 2:
                cm = colorModelGrayAlpha8;
                break;
            case 3:
                cm = colorModelRGB8;
                break;
            case 4:
                cm = colorModelRGBA8;
                break;
            }
        } else if (type == DataBuffer.TYPE_USHORT) {
            switch (bands) {
            case 1:
                cm = colorModelGray16;
                break;
            case 2:
                cm = colorModelGrayAlpha16;
                break;
            case 3:
                cm = colorModelRGB16;
                break;
            case 4:
                cm = colorModelRGBA16;
                break;
            }
        } else if (type == DataBuffer.TYPE_INT) {
            switch (bands) {
            case 1:
                cm = colorModelGray32;
                break;
            case 2:
                cm = colorModelGrayAlpha32;
                break;
            case 3:
                cm = colorModelRGB32;
                break;
            case 4:
                cm = colorModelRGBA32;
                break;
            }
        }

        return cm;
    }

    private void parse_PLTE_chunk(final PNGChunk chunk) {
        this.paletteEntries = chunk.getLength() / 3;
        this.redPalette = new byte[this.paletteEntries];
        this.greenPalette = new byte[this.paletteEntries];
        this.bluePalette = new byte[this.paletteEntries];

        int pltIndex = 0;

        // gAMA chunk must precede PLTE chunk
        if (this.performGammaCorrection) {
            if (this.gammaLut == null) {
                initGammaLut(this.bitDepth == 16 ? 16 : 8);
            }

            for (int i = 0; i < this.paletteEntries; ++i) {
                final byte r = chunk.getByte(pltIndex++);
                final byte g = chunk.getByte(pltIndex++);
                final byte b = chunk.getByte(pltIndex++);

                this.redPalette[i] = (byte) this.gammaLut[r & 0xff];
                this.greenPalette[i] = (byte) this.gammaLut[g & 0xff];
                this.bluePalette[i] = (byte) this.gammaLut[b & 0xff];
            }
        } else {
            for (int i = 0; i < this.paletteEntries; ++i) {
                this.redPalette[i] = chunk.getByte(pltIndex++);
                this.greenPalette[i] = chunk.getByte(pltIndex++);
                this.bluePalette[i] = chunk.getByte(pltIndex++);
            }
        }
    }

    private void parse_bKGD_chunk(final PNGChunk chunk) {
        switch (this.colorType) {
        case PNG_COLOR_PALETTE:
            final int bkgdIndex = chunk.getByte(0) & 0xff;

            this.bkgdRed = this.redPalette[bkgdIndex] & 0xff;
            this.bkgdGreen = this.greenPalette[bkgdIndex] & 0xff;
            this.bkgdBlue = this.bluePalette[bkgdIndex] & 0xff;

            if (this.encodeParam != null) {
                ((PNGEncodeParam.Palette) this.encodeParam).setBackgroundPaletteIndex(bkgdIndex);
            }
            break;
        case PNG_COLOR_GRAY:
        case PNG_COLOR_GRAY_ALPHA:
            final int bkgdGray = chunk.getInt2(0);
            this.bkgdRed = this.bkgdGreen = this.bkgdBlue = bkgdGray;

            if (this.encodeParam != null) {
                ((PNGEncodeParam.Gray) this.encodeParam).setBackgroundGray(bkgdGray);
            }
            break;
        case PNG_COLOR_RGB:
        case PNG_COLOR_RGB_ALPHA:
            this.bkgdRed = chunk.getInt2(0);
            this.bkgdGreen = chunk.getInt2(2);
            this.bkgdBlue = chunk.getInt2(4);

            final int[] bkgdRGB = new int[3];
            bkgdRGB[0] = this.bkgdRed;
            bkgdRGB[1] = this.bkgdGreen;
            bkgdRGB[2] = this.bkgdBlue;
            if (this.encodeParam != null) {
                ((PNGEncodeParam.RGB) this.encodeParam).setBackgroundRGB(bkgdRGB);
            }
            break;
        }

        if (this.emitProperties) {
            int r = 0, g = 0, b = 0;
            if (this.colorType == PNG_COLOR_PALETTE || this.bitDepth == 8) {
                r = this.bkgdRed;
                g = this.bkgdGreen;
                b = this.bkgdBlue;
            } else if (this.bitDepth < 8) {
                r = this.expandBits[this.bitDepth][this.bkgdRed];
                g = this.expandBits[this.bitDepth][this.bkgdGreen];
                b = this.expandBits[this.bitDepth][this.bkgdBlue];
            } else if (this.bitDepth == 16) {
                r = this.bkgdRed >> 8;
                g = this.bkgdGreen >> 8;
                b = this.bkgdBlue >> 8;
            }
            this.properties.put("background_color", new Color(r, g, b));
        }
    }

    private void parse_cHRM_chunk(final PNGChunk chunk) {
        // If an sRGB chunk exists, ignore cHRM chunks
        if (this.sRGBRenderingIntent != -1) {
            return;
        }

        this.chromaticity = new float[8];
        this.chromaticity[0] = chunk.getInt4(0) / 100000.0F;
        this.chromaticity[1] = chunk.getInt4(4) / 100000.0F;
        this.chromaticity[2] = chunk.getInt4(8) / 100000.0F;
        this.chromaticity[3] = chunk.getInt4(12) / 100000.0F;
        this.chromaticity[4] = chunk.getInt4(16) / 100000.0F;
        this.chromaticity[5] = chunk.getInt4(20) / 100000.0F;
        this.chromaticity[6] = chunk.getInt4(24) / 100000.0F;
        this.chromaticity[7] = chunk.getInt4(28) / 100000.0F;

        if (this.encodeParam != null) {
            this.encodeParam.setChromaticity(this.chromaticity);
        }
        if (this.emitProperties) {
            this.properties.put("white_point_x", new Float(this.chromaticity[0]));
            this.properties.put("white_point_y", new Float(this.chromaticity[1]));
            this.properties.put("red_x", new Float(this.chromaticity[2]));
            this.properties.put("red_y", new Float(this.chromaticity[3]));
            this.properties.put("green_x", new Float(this.chromaticity[4]));
            this.properties.put("green_y", new Float(this.chromaticity[5]));
            this.properties.put("blue_x", new Float(this.chromaticity[6]));
            this.properties.put("blue_y", new Float(this.chromaticity[7]));
        }
    }

    private void parse_gAMA_chunk(final PNGChunk chunk) {
        // If an sRGB chunk exists, ignore gAMA chunks
        if (this.sRGBRenderingIntent != -1) {
            return;
        }

        this.fileGamma = chunk.getInt4(0) / 100000.0F;
        // log.info("Gamma: " + fileGamma);
        final float exp = this.performGammaCorrection ? this.displayExponent / this.userExponent : 1.0F;
        if (this.encodeParam != null) {
            this.encodeParam.setGamma(this.fileGamma * exp);
        }
        if (this.emitProperties) {
            this.properties.put("gamma", new Float(this.fileGamma * exp));
        }
    }

    private void parse_hIST_chunk(final PNGChunk chunk) {
        if (this.redPalette == null) {
            final String msg = PropertyUtil.getString("PNGImageDecoder18");
            throw new RuntimeException(msg);
        }

        final int length = this.redPalette.length;
        final int[] hist = new int[length];
        for (int i = 0; i < length; ++i) {
            hist[i] = chunk.getInt2(2 * i);
        }

        if (this.encodeParam != null) {
            this.encodeParam.setPaletteHistogram(hist);
        }
    }

    private void parse_iCCP_chunk(final PNGChunk chunk) {
        final StringBuilder name = new StringBuilder("");
        byte b;

        int textIndex = 0;
        while ((b = chunk.getByte(textIndex++)) != 0) {
            name.append((char) b);
        }
    }

    private void parse_pHYs_chunk(final PNGChunk chunk) {
        final int xPixelsPerUnit = chunk.getInt4(0);
        final int yPixelsPerUnit = chunk.getInt4(4);
        final int unitSpecifier = chunk.getInt1(8);

        if (this.encodeParam != null) {
            this.encodeParam.setPhysicalDimension(xPixelsPerUnit, yPixelsPerUnit, unitSpecifier);
        }
        if (this.emitProperties) {
            this.properties.put("x_pixels_per_unit", xPixelsPerUnit);
            this.properties.put("y_pixels_per_unit", yPixelsPerUnit);
            this.properties.put("pixel_aspect_ratio", new Float((float) xPixelsPerUnit / yPixelsPerUnit));
            if (unitSpecifier == 1) {
                this.properties.put("pixel_units", "Meters");
            } else if (unitSpecifier != 0) {
                // Error -- unit specifier must be 0 or 1
                final String msg = PropertyUtil.getString("PNGImageDecoder12");
                throw new RuntimeException(msg);
            }
        }
    }

    private void parse_sBIT_chunk(final PNGChunk chunk) {
        if (this.colorType == PNG_COLOR_PALETTE) {
            this.significantBits = new int[3];
        } else {
            this.significantBits = new int[this.inputBands];
        }
        for (int i = 0; i < this.significantBits.length; ++i) {
            final int bits = chunk.getByte(i);
            final int depth = this.colorType == PNG_COLOR_PALETTE ? 8 : this.bitDepth;
            if (bits <= 0 || bits > depth) {
                // Error -- significant bits must be between 0 and
                // image bit depth.
                final String msg = PropertyUtil.getString("PNGImageDecoder13");
                throw new RuntimeException(msg);
            }
            this.significantBits[i] = bits;
        }

        if (this.encodeParam != null) {
            this.encodeParam.setSignificantBits(this.significantBits);
        }
        if (this.emitProperties) {
            this.properties.put("significant_bits", this.significantBits);
        }
    }

    private void parse_sRGB_chunk(final PNGChunk chunk) {
        this.sRGBRenderingIntent = chunk.getByte(0);

        // The presence of an sRGB chunk implies particular
        // settings for gamma and chroma.
        this.fileGamma = 45455 / 100000.0F;

        this.chromaticity = new float[8];
        this.chromaticity[0] = 31270 / 10000.0F;
        this.chromaticity[1] = 32900 / 10000.0F;
        this.chromaticity[2] = 64000 / 10000.0F;
        this.chromaticity[3] = 33000 / 10000.0F;
        this.chromaticity[4] = 30000 / 10000.0F;
        this.chromaticity[5] = 60000 / 10000.0F;
        this.chromaticity[6] = 15000 / 10000.0F;
        this.chromaticity[7] = 6000 / 10000.0F;

        if (this.performGammaCorrection) {
            // File gamma is 1/2.2
            final float gamma = this.fileGamma * (this.displayExponent / this.userExponent);
            if (this.encodeParam != null) {
                this.encodeParam.setGamma(gamma);
                this.encodeParam.setChromaticity(this.chromaticity);
            }
            if (this.emitProperties) {
                this.properties.put("gamma", new Float(gamma));
                this.properties.put("white_point_x", new Float(this.chromaticity[0]));
                this.properties.put("white_point_y", new Float(this.chromaticity[1]));
                this.properties.put("red_x", new Float(this.chromaticity[2]));
                this.properties.put("red_y", new Float(this.chromaticity[3]));
                this.properties.put("green_x", new Float(this.chromaticity[4]));
                this.properties.put("green_y", new Float(this.chromaticity[5]));
                this.properties.put("blue_x", new Float(this.chromaticity[6]));
                this.properties.put("blue_y", new Float(this.chromaticity[7]));
            }
        }
    }

    private void parse_tEXt_chunk(final PNGChunk chunk) {
        final StringBuilder key = new StringBuilder();
        final StringBuilder value = new StringBuilder();
        byte b;

        int textIndex = 0;
        while ((b = chunk.getByte(textIndex++)) != 0) {
            key.append((char) b);
        }

        for (int i = textIndex; i < chunk.getLength(); ++i) {
            value.append((char) chunk.getByte(i));
        }

        this.textKeys.add(key.toString());
        this.textStrings.add(value.toString());
    }

    private void parse_tIME_chunk(final PNGChunk chunk) {
        final int year = chunk.getInt2(0);
        final int month = chunk.getInt1(2) - 1;
        final int day = chunk.getInt1(3);
        final int hour = chunk.getInt1(4);
        final int minute = chunk.getInt1(5);
        final int second = chunk.getInt1(6);

        final TimeZone gmt = TimeZone.getTimeZone("GMT");

        final GregorianCalendar cal = new GregorianCalendar(gmt);
        cal.set(year, month, day, hour, minute, second);
        final Date date = cal.getTime();

        if (this.encodeParam != null) {
            this.encodeParam.setModificationTime(date);
        }
        if (this.emitProperties) {
            this.properties.put("timestamp", date);
        }
    }

    private void parse_tRNS_chunk(final PNGChunk chunk) {
        if (this.colorType == PNG_COLOR_PALETTE) {
            final int entries = chunk.getLength();
            if (entries > this.paletteEntries) {
                // Error -- mustn't have more alpha than RGB palette entries
                final String msg = PropertyUtil.getString("PNGImageDecoder14");
                throw new RuntimeException(msg);
            }

            // Load beginning of palette from the chunk
            this.alphaPalette = new byte[this.paletteEntries];
            for (int i = 0; i < entries; ++i) {
                this.alphaPalette[i] = chunk.getByte(i);
            }

            // Fill rest of palette with 255
            for (int i = entries; i < this.paletteEntries; ++i) {
                this.alphaPalette[i] = (byte) 255;
            }

            if (!this.suppressAlpha) {
                if (this.expandPalette) {
                    this.postProcess = POST_PALETTE_TO_RGBA;
                    this.outputBands = 4;
                } else {
                    this.outputHasAlphaPalette = true;
                }
            }
        } else if (this.colorType == PNG_COLOR_GRAY) {
            this.grayTransparentAlpha = chunk.getInt2(0);

            if (!this.suppressAlpha) {
                if (this.bitDepth < 8) {
                    this.output8BitGray = true;
                    this.maxOpacity = 255;
                    this.postProcess = POST_GRAY_LUT_ADD_TRANS;
                } else {
                    this.postProcess = POST_ADD_GRAY_TRANS;
                }

                if (this.expandGrayAlpha) {
                    this.outputBands = 4;
                    this.postProcess |= POST_EXP_MASK;
                } else {
                    this.outputBands = 2;
                }

                if (this.encodeParam != null) {
                    ((PNGEncodeParam.Gray) this.encodeParam).setTransparentGray(this.grayTransparentAlpha);
                }
            }
        } else if (this.colorType == PNG_COLOR_RGB) {
            this.redTransparentAlpha = chunk.getInt2(0);
            this.greenTransparentAlpha = chunk.getInt2(2);
            this.blueTransparentAlpha = chunk.getInt2(4);

            if (!this.suppressAlpha) {
                this.outputBands = 4;
                this.postProcess = POST_ADD_RGB_TRANS;

                if (this.encodeParam != null) {
                    final int[] rgbTrans = new int[3];
                    rgbTrans[0] = this.redTransparentAlpha;
                    rgbTrans[1] = this.greenTransparentAlpha;
                    rgbTrans[2] = this.blueTransparentAlpha;
                    ((PNGEncodeParam.RGB) this.encodeParam).setTransparentRGB(rgbTrans);
                }
            }
        } else if (this.colorType == PNG_COLOR_GRAY_ALPHA || this.colorType == PNG_COLOR_RGB_ALPHA) {
            // Error -- GA or RGBA image can't have a tRNS chunk.
            final String msg = PropertyUtil.getString("PNGImageDecoder15");
            throw new RuntimeException(msg);
        }
    }

    private void parse_zTXt_chunk(final PNGChunk chunk) {
        final StringBuilder key = new StringBuilder();
        final StringBuilder value = new StringBuilder();
        byte b;

        int textIndex = 0;
        while ((b = chunk.getByte(textIndex++)) != 0) {
            key.append((char) b);
        }
        /* int method = */chunk.getByte(textIndex++);

        try {
            final int length = chunk.getLength() - textIndex;
            final byte[] data = chunk.getData();
            final InputStream cis = new ByteArrayInputStream(data, textIndex, length);
            final InputStream iis = new InflaterInputStream(cis);

            int c;
            while ((c = iis.read()) != -1) {
                value.append((char) c);
            }

            this.ztextKeys.add(key.toString());
            this.ztextStrings.add(value.toString());
        } catch (final Exception e) {
            log.error("Exception", e);
        }
    }

    private WritableRaster createRaster(final int width, final int height, final int bands,
            final int scanlineStride, final int bitDepth) {

        DataBuffer dataBuffer;
        WritableRaster ras = null;
        final 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,
                    this.bandOffsets[bands], origin);
        } else {
            dataBuffer = new DataBufferUShort(height * scanlineStride);
            ras = Raster.createInterleavedRaster(dataBuffer, width, height, scanlineStride, bands,
                    this.bandOffsets[bands], origin);
        }

        return ras;
    }

    // Data filtering methods

    private static void decodeSubFilter(final byte[] curr, final int count, final int bpp) {
        for (int i = bpp; i < count; ++i) {
            int val;

            val = curr[i] & 0xff;
            val += curr[i - bpp] & 0xff;

            curr[i] = (byte) val;
        }
    }

    private static void decodeUpFilter(final byte[] curr, final byte[] prev, final int count) {
        for (int i = 0; i < count; ++i) {
            final int raw = curr[i] & 0xff;
            final int prior = prev[i] & 0xff;

            curr[i] = (byte) (raw + prior);
        }
    }

    private static void decodeAverageFilter(final byte[] curr, final byte[] prev, final int count, final int bpp) {
        for (int i = 0; i < bpp; ++i) {
            final int raw = curr[i] & 0xff;
            final int priorRow = prev[i] & 0xff;

            curr[i] = (byte) (raw + priorRow / 2);
        }

        for (int i = bpp; i < count; ++i) {
            final int raw = curr[i] & 0xff;
            final int priorPixel = curr[i - bpp] & 0xff;
            final int priorRow = prev[i] & 0xff;

            curr[i] = (byte) (raw + (priorPixel + priorRow) / 2);
        }
    }

    private static int paethPredictor(final int a, final int b, final int c) {
        final int p = a + b - c;
        final int pa = Math.abs(p - a);
        final int pb = Math.abs(p - b);
        final 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(final byte[] curr, final byte[] prev, final int count, final int bpp) {
        int priorPixel, priorRowPixel;

        for (int i = 0; i < bpp; ++i) {
            final int raw = curr[i] & 0xff;
            final int priorRow = prev[i] & 0xff;

            curr[i] = (byte) (raw + priorRow);
        }

        for (int i = bpp; i < count; ++i) {
            final int raw = curr[i] & 0xff;
            priorPixel = curr[i - bpp] & 0xff;
            final int priorRow = prev[i] & 0xff;
            priorRowPixel = prev[i - bpp] & 0xff;

            curr[i] = (byte) (raw + paethPredictor(priorPixel, priorRow, priorRowPixel));
        }
    }

    private void processPixels(final int process, final Raster src, final WritableRaster dst, final int xOffset,
            final int step, final int y, final int width) {
        int srcX, dstX;

        // Create an array suitable for holding one pixel
        final int[] ps = src.getPixel(0, 0, (int[]) null);
        final int[] pd = dst.getPixel(0, 0, (int[]) null);

        dstX = xOffset;
        switch (process) {
        case POST_NONE:
            for (srcX = 0; srcX < width; srcX++) {
                src.getPixel(srcX, 0, ps);
                dst.setPixel(dstX, y, ps);
                dstX += step;
            }
            break;

        case POST_GAMMA:
            for (srcX = 0; srcX < width; srcX++) {
                src.getPixel(srcX, 0, ps);

                for (int i = 0; i < this.inputBands; ++i) {
                    final int x = ps[i];
                    ps[i] = this.gammaLut[x];
                }

                dst.setPixel(dstX, y, ps);
                dstX += step;
            }
            break;

        case POST_GRAY_LUT:
            for (srcX = 0; srcX < width; srcX++) {
                src.getPixel(srcX, 0, ps);

                pd[0] = this.grayLut[ps[0]];

                dst.setPixel(dstX, y, pd);
                dstX += step;
            }
            break;

        case POST_GRAY_LUT_ADD_TRANS:
            for (srcX = 0; srcX < width; srcX++) {
                src.getPixel(srcX, 0, ps);

                final int val = ps[0];
                pd[0] = this.grayLut[val];
                if (val == this.grayTransparentAlpha) {
                    pd[1] = 0;
                } else {
                    pd[1] = this.maxOpacity;
                }

                dst.setPixel(dstX, y, pd);
                dstX += step;
            }
            break;

        case POST_PALETTE_TO_RGB:
            for (srcX = 0; srcX < width; srcX++) {
                src.getPixel(srcX, 0, ps);

                final int val = ps[0];
                pd[0] = this.redPalette[val];
                pd[1] = this.greenPalette[val];
                pd[2] = this.bluePalette[val];

                dst.setPixel(dstX, y, pd);
                dstX += step;
            }
            break;

        case POST_PALETTE_TO_RGBA:
            for (srcX = 0; srcX < width; srcX++) {
                src.getPixel(srcX, 0, ps);

                final int val = ps[0];
                pd[0] = this.redPalette[val];
                pd[1] = this.greenPalette[val];
                pd[2] = this.bluePalette[val];
                pd[3] = this.alphaPalette[val];

                dst.setPixel(dstX, y, pd);
                dstX += step;
            }
            break;

        case POST_ADD_GRAY_TRANS:
            for (srcX = 0; srcX < width; srcX++) {
                src.getPixel(srcX, 0, ps);

                int val = ps[0];
                if (this.performGammaCorrection) {
                    val = this.gammaLut[val];
                }
                pd[0] = val;
                if (val == this.grayTransparentAlpha) {
                    pd[1] = 0;
                } else {
                    pd[1] = this.maxOpacity;
                }

                dst.setPixel(dstX, y, pd);
                dstX += step;
            }
            break;

        case POST_ADD_RGB_TRANS:
            final boolean flagGammaCorrection = this.performGammaCorrection; // local
            // is
            // cheaper
            final int[] workGammaLut = this.gammaLut;
            for (srcX = 0; srcX < width; srcX++) {
                src.getPixel(srcX, 0, ps);

                final int r = ps[0];
                final int g = ps[1];
                final int b = ps[2];
                if (flagGammaCorrection) {
                    pd[0] = workGammaLut[r];
                    pd[1] = workGammaLut[g];
                    pd[2] = workGammaLut[b];
                } else {
                    pd[0] = r;
                    pd[1] = g;
                    pd[2] = b;
                }
                if (r == this.redTransparentAlpha && g == this.greenTransparentAlpha
                        && b == this.blueTransparentAlpha) {
                    pd[3] = 0;
                } else {
                    pd[3] = this.maxOpacity;
                }

                dst.setPixel(dstX, y, pd);
                dstX += step;
            }
            break;

        case POST_REMOVE_GRAY_TRANS:
            for (srcX = 0; srcX < width; srcX++) {
                src.getPixel(srcX, 0, ps);

                final int g = ps[0];
                if (this.performGammaCorrection) {
                    pd[0] = this.gammaLut[g];
                } else {
                    pd[0] = g;
                }

                dst.setPixel(dstX, y, pd);
                dstX += step;
            }
            break;

        case POST_REMOVE_RGB_TRANS:
            for (srcX = 0; srcX < width; srcX++) {
                src.getPixel(srcX, 0, ps);

                final int r = ps[0];
                final int g = ps[1];
                final int b = ps[2];
                if (this.performGammaCorrection) {
                    pd[0] = this.gammaLut[r];
                    pd[1] = this.gammaLut[g];
                    pd[2] = this.gammaLut[b];
                } else {
                    pd[0] = r;
                    pd[1] = g;
                    pd[2] = b;
                }

                dst.setPixel(dstX, y, pd);
                dstX += step;
            }
            break;

        case POST_GAMMA_EXP:
            for (srcX = 0; srcX < width; srcX++) {
                src.getPixel(srcX, 0, ps);

                final int val = ps[0];
                final int alpha = ps[1];
                final int gamma = this.gammaLut[val];
                pd[0] = gamma;
                pd[1] = gamma;
                pd[2] = gamma;
                pd[3] = alpha;

                dst.setPixel(dstX, y, pd);
                dstX += step;
            }
            break;

        case POST_GRAY_ALPHA_EXP:
            for (srcX = 0; srcX < width; srcX++) {
                src.getPixel(srcX, 0, ps);

                final int val = ps[0];
                final int alpha = ps[1];
                pd[0] = val;
                pd[1] = val;
                pd[2] = val;
                pd[3] = alpha;

                dst.setPixel(dstX, y, pd);
                dstX += step;
            }
            break;

        case POST_ADD_GRAY_TRANS_EXP:
            for (srcX = 0; srcX < width; srcX++) {
                src.getPixel(srcX, 0, ps);

                int val = ps[0];
                if (this.performGammaCorrection) {
                    val = this.gammaLut[val];
                }
                pd[0] = val;
                pd[1] = val;
                pd[2] = val;
                if (val == this.grayTransparentAlpha) {
                    pd[3] = 0;
                } else {
                    pd[3] = this.maxOpacity;
                }

                dst.setPixel(dstX, y, pd);
                dstX += step;
            }
            break;

        case POST_GRAY_LUT_ADD_TRANS_EXP:
            for (srcX = 0; srcX < width; srcX++) {
                src.getPixel(srcX, 0, ps);

                final int val = ps[0];
                final int val2 = this.grayLut[val];
                pd[0] = val2;
                pd[1] = val2;
                pd[2] = val2;
                if (val == this.grayTransparentAlpha) {
                    pd[3] = 0;
                } else {
                    pd[3] = this.maxOpacity;
                }

                dst.setPixel(dstX, y, pd);
                dstX += step;
            }
            break;
        }
    }

    /**
     * Reads in an image of a given size and returns it as a WritableRaster.
     */
    private void decodePass(final WritableRaster imRas, final int xOffset, final int yOffset, final int xStep,
            final int yStep, final int passWidth, final int passHeight) {
        if (passWidth == 0 || passHeight == 0) {
            return;
        }

        final int bytesPerRow = (this.inputBands * passWidth * this.bitDepth + 7) / 8;
        final int eltsPerRow = this.bitDepth == 16 ? bytesPerRow / 2 : bytesPerRow;
        byte[] curr = new byte[bytesPerRow];
        byte[] prior = new byte[bytesPerRow];

        // Create a 1-row tall Raster to hold the data
        final WritableRaster passRow = createRaster(passWidth, 1, this.inputBands, eltsPerRow, this.bitDepth);
        final DataBuffer dataBuffer = passRow.getDataBuffer();
        final int type = dataBuffer.getDataType();
        byte[] byteData = null;
        short[] shortData = null;
        if (type == DataBuffer.TYPE_BYTE) {
            byteData = ((DataBufferByte) dataBuffer).getData();
        } else {
            shortData = ((DataBufferUShort) dataBuffer).getData();
        }

        // Decode the (sub)image row-by-row
        int srcY, dstY;
        for (srcY = 0, dstY = yOffset; srcY < passHeight; srcY++, dstY += yStep) {
            // Read the filter type byte and a row of data
            int filter = 0;
            try {
                filter = this.dataStream.read();
                this.dataStream.readFully(curr, 0, bytesPerRow);
            } catch (final Exception e) {
                log.error("Exception", e);
            }

            switch (filter) {
            case PNG_FILTER_NONE:
                break;
            case PNG_FILTER_SUB:
                decodeSubFilter(curr, bytesPerRow, this.bytesPerPixel);
                break;
            case PNG_FILTER_UP:
                decodeUpFilter(curr, prior, bytesPerRow);
                break;
            case PNG_FILTER_AVERAGE:
                decodeAverageFilter(curr, prior, bytesPerRow, this.bytesPerPixel);
                break;
            case PNG_FILTER_PAETH:
                decodePaethFilter(curr, prior, bytesPerRow, this.bytesPerPixel);
                break;
            default:
                // Error -- unknown filter type
                final String msg = PropertyUtil.getString("PNGImageDecoder16");
                throw new RuntimeException(msg);
            }

            // Copy data into passRow byte by byte
            if (this.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;
                }
            }

            processPixels(this.postProcess, passRow, imRas, xOffset, xStep, dstY, passWidth);

            // Swap curr and prior
            final byte[] tmp = prior;
            prior = curr;
            curr = tmp;
        }
    }

    private void decodeImage(final boolean useInterlacing) {
        final int width = this.bounds.width;
        final int height = this.bounds.height;

        if (!useInterlacing) {
            decodePass(this.theTile, 0, 0, 1, 1, width, height);
        } else {
            decodePass(this.theTile, 0, 0, 8, 8, (width + 7) / 8, (height + 7) / 8);
            decodePass(this.theTile, 4, 0, 8, 8, (width + 3) / 8, (height + 7) / 8);
            decodePass(this.theTile, 0, 4, 4, 8, (width + 3) / 4, (height + 3) / 8);
            decodePass(this.theTile, 2, 0, 4, 4, (width + 1) / 4, (height + 3) / 4);
            decodePass(this.theTile, 0, 2, 2, 4, (width + 1) / 2, (height + 1) / 4);
            decodePass(this.theTile, 1, 0, 2, 2, width / 2, (height + 1) / 2);
            decodePass(this.theTile, 0, 1, 1, 2, width, height / 2);
        }
    }

    @Override
    public WritableRaster copyData(final WritableRaster wr) {
        GraphicsUtil.copyData(this.theTile, wr);
        return wr;
    }

    // RenderedImage stuff
    @Override
    public Raster getTile(final int tileX, final int tileY) {
        if (tileX != 0 || tileY != 0) {
            // Error -- bad tile requested
            final String msg = PropertyUtil.getString("PNGImageDecoder17");
            throw new IllegalArgumentException(msg);
        }
        return this.theTile;
    }
}