# PNGConverter.java

    /*
     * 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.
     */
    package org.apache.pdfbox.pdmodel.graphics.image;

    import java.awt.color.ColorSpace;
    import java.awt.color.ICC_Profile;
    import java.io.ByteArrayInputStream;
    import java.io.ByteArrayOutputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.OutputStream;
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.List;
    import javax.imageio.stream.ImageInputStream;
    import javax.imageio.stream.MemoryCacheImageInputStream;

    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.apache.pdfbox.cos.COSArray;
    import org.apache.pdfbox.cos.COSDictionary;
    import org.apache.pdfbox.cos.COSInteger;
    import org.apache.pdfbox.cos.COSName;
    import org.apache.pdfbox.cos.COSStream;
    import org.apache.pdfbox.filter.Filter;
    import org.apache.pdfbox.filter.FilterFactory;
    import org.apache.pdfbox.pdmodel.PDDocument;
    import org.apache.pdfbox.pdmodel.common.PDStream;
    import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace;
    import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceGray;
    import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceRGB;
    import org.apache.pdfbox.pdmodel.graphics.color.PDICCBased;
    import org.apache.pdfbox.pdmodel.graphics.color.PDIndexed;

    /**
     * This factory tries to encode a PNG given as byte array into a PDImageXObject
     * by directly coping the image data into the PDF streams without
     * decoding/encoding and re-compressing the PNG data.
     * <p>
     * If this is for any reason not possible, the factory will return null. You
     * must then encode the image by loading it and using the LosslessFactory.
     * <p>
     * The W3C PNG spec was used to implement this class:
     * https://www.w3.org/TR/2003/REC-PNG-20031110
     *
     * @author Emmeran Seehuber
     */
    final class PNGConverter
    {
        private static final Log LOG = LogFactory.getLog(PNGConverter.class);

        // Chunk Type definitions. The bytes in the comments are the bytes in the spec.
        private static final int CHUNK_IHDR = 0x49484452; // IHDR: 73 72 68 82
        private static final int CHUNK_IDAT = 0x49444154; // IDAT: 73 68 65 84
        private static final int CHUNK_PLTE = 0x504C5445; // PLTE: 80 76 84 69
        private static final int CHUNK_IEND = 0x49454E44; // IEND: 73 69 78 68
        private static final int CHUNK_TRNS = 0x74524E53; // tRNS: 116 82 78 83
        private static final int CHUNK_CHRM = 0x6348524D; // cHRM: 99 72 82 77
        private static final int CHUNK_GAMA = 0x67414D41; // gAMA: 103 65 77 65
        private static final int CHUNK_ICCP = 0x69434350; // iCCP: 105 67 67 80
        private static final int CHUNK_SBIT = 0x73424954; // sBIT: 115 66 73 84
        private static final int CHUNK_SRGB = 0x73524742; // sRGB: 115 82 71 66
        private static final int CHUNK_TEXT = 0x74455874; // tEXt: 116 69 88 116
        private static final int CHUNK_ZTXT = 0x7A545874; // zTXt: 122 84 88 116
        private static final int CHUNK_ITXT = 0x69545874; // iTXt: 105 84 88 116
        private static final int CHUNK_KBKG = 0x6B424B47; // kBKG: 107 66 75 71
        private static final int CHUNK_HIST = 0x68495354; // hIST: 104 73 83 84
        private static final int CHUNK_PHYS = 0x70485973; // pHYs: 112 72 89 115
        private static final int CHUNK_SPLT = 0x73504C54; // sPLT: 115 80 76 84
        private static final int CHUNK_TIME = 0x74494D45; // tIME: 116 73 77 69

        // CRC Reference Implementation, see
        // https://www.w3.org/TR/2003/REC-PNG-20031110/#D-CRCAppendix
        // for details

        /* Table of CRCs of all 8-bit messages. */
        private static final int[] CRC_TABLE = new int[256];

        static
        {
            makeCrcTable();
        }

        private PNGConverter()
        {
        }

        /**
         * Try to convert a PNG into a PDImageXObject. If for any reason the PNG can not
         * be converted, null is returned.
         * <p>
         * This usually means the PNG structure is damaged (CRC error, etc.) or it uses
         * some features which can not be mapped to PDF.
         *
         * @param doc       the document to put the image in
         * @param imageData the byte data of the PNG
         * @return null or the PDImageXObject built from the png
         */
        static PDImageXObject convertPNGImage(PDDocument doc, byte[] imageData) throws IOException
        {
            PNGConverterState state = parsePNGChunks(imageData);
            if (!checkConverterState(state))
            {
                // There is something wrong, we can't convert this PNG
                return null;
            }

            return convertPng(doc, state);
        }

        /**
         * Convert the image using the state.
         *
         * @param doc   the document to put the image in
         * @param state the parser state containing the PNG chunks.
         * @return null or the converted image
         */
        private static PDImageXObject convertPng(PDDocument doc, PNGConverterState state)
                throws IOException
        {
            Chunk ihdr = state.IHDR;
            int ihdrStart = ihdr.start;
            int width = readInt(ihdr.bytes, ihdrStart);
            int height = readInt(ihdr.bytes, ihdrStart + 4);
            int bitDepth = ihdr.bytes[ihdrStart + 8] & 0xFF;
            int colorType = ihdr.bytes[ihdrStart + 9] & 0xFF;
            int compressionMethod = ihdr.bytes[ihdrStart + 10] & 0xFF;
            int filterMethod = ihdr.bytes[ihdrStart + 11] & 0xFF;
            int interlaceMethod = ihdr.bytes[ihdrStart + 12] & 0xFF;

            if (bitDepth != 1 && bitDepth != 2 && bitDepth != 4 && bitDepth != 8 && bitDepth != 16)
            {
                LOG.error(String.format("Invalid bit depth %d.", bitDepth));
                return null;
            }
            if (width <= 0 || height <= 0)
            {
                LOG.error(String.format("Invalid image size %d x %d", width, height));
                return null;
            }
            if (compressionMethod != 0)
            {
                LOG.error(String.format("Unknown PNG compression method %d.", compressionMethod));
                return null;
            }
            if (filterMethod != 0)
            {
                LOG.error(String.format("Unknown PNG filtering method %d.", compressionMethod));
                return null;
            }
            if (interlaceMethod != 0)
            {
                LOG.debug(String.format("Can't handle interlace method %d.", interlaceMethod));
                return null;
            }

            state.width = width;
            state.height = height;
            state.bitsPerComponent = bitDepth;

            switch (colorType)
            {
            case 0:
                // Grayscale
                LOG.debug("Can't handle grayscale yet.");
                return null;
            case 2:
                // Truecolor
                if (state.tRNS != null)
                {
                    LOG.debug("Can't handle images with transparent colors.");
                    return null;
                }
                return buildImageObject(doc, state);
            case 3:
                // Indexed image
                return buildIndexImage(doc, state);
            case 4:
                // Grayscale with alpha.
                LOG.debug(
                        "Can't handle grayscale with alpha, would need to separate alpha from image data");
                return null;
            case 6:
                // Truecolor with alpha.
                LOG.debug(
                        "Can't handle truecolor with alpha, would need to separate alpha from image data");
                return null;
            default:
                LOG.error("Unknown PNG color type " + colorType);
                return null;
            }
        }

        /**
         * Build a indexed image
         */
        private static PDImageXObject buildIndexImage(PDDocument doc, PNGConverterState state)
                throws IOException
        {
            Chunk plte = state.PLTE;
            if (plte == null)
            {
                LOG.error("Indexed image without PLTE chunk.");
                return null;
            }
            if (plte.length % 3 != 0)
            {
                LOG.error("PLTE table corrupted, last (r,g,b) tuple is not complete.");
                return null;
            }
            if (state.bitsPerComponent > 8)
            {
                LOG.debug(String.format("Can only convert indexed images with bit depth <= 8, not %d.",
                        state.bitsPerComponent));
                return null;
            }

            PDImageXObject image = buildImageObject(doc, state);
            if (image == null)
            {
                return null;
            }

            int highVal = (plte.length / 3) - 1;
            if (highVal > 255)
            {
                LOG.error(String.format("Too much colors in PLTE, only 256 allowed, found %d colors.",
                        highVal + 1));
                return null;
            }

            setupIndexedColorSpace(doc, plte, image, highVal);

            if (state.tRNS != null)
            {
                image.getCOSObject().setItem(COSName.SMASK,
                        buildTransparencyMaskFromIndexedData(doc, image, state));
            }

            return image;
        }

        private static PDImageXObject buildTransparencyMaskFromIndexedData(PDDocument doc,
                PDImageXObject image, PNGConverterState state) throws IOException
        {
            Filter flateDecode = FilterFactory.INSTANCE.getFilter(COSName.FLATE_DECODE);
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            COSDictionary decodeParams = buildDecodeParams(state, PDDeviceGray.INSTANCE);
            COSDictionary imageDict = new COSDictionary();
            imageDict.setItem(COSName.FILTER, COSName.FLATE_DECODE);
            imageDict.setItem(COSName.DECODE_PARMS, decodeParams);
            flateDecode.decode(getIDATInputStream(state), outputStream, imageDict, 0);
            int length = image.getWidth() * image.getHeight();
            byte[] bytes = new byte[length];
            byte[] transparencyTable = state.tRNS.getData();
            byte[] decodedIDAT = outputStream.toByteArray();
            try (ImageInputStream iis = new MemoryCacheImageInputStream(
                    new ByteArrayInputStream(decodedIDAT)))
            {
                int bitsPerComponent = state.bitsPerComponent;
                int w = 0;
                int neededBits = bitsPerComponent * state.width;
                int bitPadding = neededBits % 8;
                for (int i = 0; i < bytes.length; i++)
                {
                    int idx = (int) iis.readBits(bitsPerComponent);
                    if (idx < transparencyTable.length)
                    {
                        // Inside the table, use the transparency value
                        bytes[i] = transparencyTable[idx];
                    }
                    else
                    {
                        // Outside the table -> transparent value is 0xFF here.
                        bytes[i] = (byte) 0xFF;
                    }
                    w++;
                    if (w == state.width)
                    {
                        w = 0;
                        iis.readBits(bitPadding);
                    }
                }
            }
            return LosslessFactory
                    .prepareImageXObject(doc, bytes, image.getWidth(), image.getHeight(), 8,
                            PDDeviceGray.INSTANCE);
        }

        private static void setupIndexedColorSpace(PDDocument doc, Chunk lookupTable,
                PDImageXObject image, int highVal) throws IOException
        {
            COSArray indexedArray = new COSArray();
            indexedArray.add(COSName.INDEXED);
            indexedArray.add(image.getColorSpace());
            ((COSDictionary) image.getCOSObject().getItem(COSName.DECODE_PARMS))
                    .setItem(COSName.COLORS, COSInteger.ONE);

            indexedArray.add(COSInteger.get(highVal));

            PDStream colorTable = new PDStream(doc);
            try (OutputStream colorTableStream = colorTable.createOutputStream(COSName.FLATE_DECODE))
            {
                colorTableStream.write(lookupTable.bytes, lookupTable.start, lookupTable.length);
            }
            indexedArray.add(colorTable);

            PDIndexed indexed = new PDIndexed(indexedArray);
            image.setColorSpace(indexed);
        }

        /**
         * Build the base image object from the IDATs and profile information
         */
        private static PDImageXObject buildImageObject(PDDocument document, PNGConverterState state)
                throws IOException
        {
            InputStream encodedByteStream = getIDATInputStream(state);

            PDColorSpace colorSpace = PDDeviceRGB.INSTANCE;

            PDImageXObject imageXObject = new PDImageXObject(document, encodedByteStream,
                    COSName.FLATE_DECODE, state.width, state.height, state.bitsPerComponent,
                    colorSpace);

            COSDictionary decodeParams = buildDecodeParams(state, colorSpace);
            imageXObject.getCOSObject().setItem(COSName.DECODE_PARMS, decodeParams);

            // We ignore gAMA and cHRM chunks if we have a ICC profile, as the ICC profile
            // takes preference
            boolean hasICCColorProfile = state.sRGB != null || state.iCCP != null;

            if (state.gAMA != null && !hasICCColorProfile)
            {
                if (state.gAMA.length != 4)
                {
                    LOG.error("Invalid gAMA chunk length " + state.gAMA.length);
                    return null;
                }
                float gamma = readPNGFloat(state.gAMA.bytes, state.gAMA.start);
                // If the gamma is 2.2 for sRGB everything is fine. Otherwise bail out.
                // The gamma is stored as 1 / gamma.
                if (Math.abs(gamma - (1 / 2.2f)) > 0.00001)
                {
                    LOG.debug(String.format("We can't handle gamma of %f yet.", gamma));
                    return null;
                }
            }

            if (state.sRGB != null)
            {
                if (state.sRGB.length != 1)
                {
                    LOG.error(
                            String.format("sRGB chunk has an invalid length of %d", state.sRGB.length));
                    return null;
                }

                // Store the specified rendering intent
                int renderIntent = state.sRGB.bytes[state.sRGB.start];
                COSName value = mapPNGRenderIntent(renderIntent);
                imageXObject.getCOSObject().setItem(COSName.INTENT, value);
            }

            if (state.cHRM != null && !hasICCColorProfile)
            {
                if (state.cHRM.length != 32)
                {
                    LOG.error("Invalid cHRM chunk length " + state.cHRM.length);
                    return null;
                }
                LOG.debug("We can not handle cHRM chunks yet.");
                return null;
            }

            // If possible we prefer a ICCBased color profile, just because its way faster
            // to decode ...
            if (state.iCCP != null || state.sRGB != null)
            {
                // We have got a color profile, which we must attach
                COSStream cosStream = createCOSStreamwithIccProfile(document, colorSpace, state);
                if (cosStream == null)
                {
                    return null;
                }
                COSArray array = new COSArray();
                array.add(COSName.ICCBASED);
                array.add(cosStream);
                PDICCBased profile = PDICCBased.create(array, null);
                imageXObject.setColorSpace(profile);
            }
            return imageXObject;
        }

        private static COSStream createCOSStreamwithIccProfile
            (PDDocument document, PDColorSpace colorSpace, PNGConverterState state) throws IOException
        {
            COSStream cosStream = document.getDocument().createCOSStream();
            cosStream.setInt(COSName.N, colorSpace.getNumberOfComponents());
            cosStream.setItem(COSName.ALTERNATE, colorSpace.getNumberOfComponents()
                    == 1 ? COSName.DEVICEGRAY : COSName.DEVICERGB);
            cosStream.setItem(COSName.FILTER, COSName.FLATE_DECODE);
            if (state.iCCP != null)
            {
                // We need to skip over the name
                int iccProfileDataStart = 0;
                while (iccProfileDataStart < 80 && iccProfileDataStart < state.iCCP.length)
                {
                    if (state.iCCP.bytes[state.iCCP.start + iccProfileDataStart] == 0)
                    {
                        break;
                    }
                    iccProfileDataStart++;
                }
                iccProfileDataStart++;
                if (iccProfileDataStart >= state.iCCP.length)
                {
                    LOG.error("Invalid iCCP chunk, to few bytes");
                    return null;
                }
                byte compressionMethod = state.iCCP.bytes[state.iCCP.start + iccProfileDataStart];
                if (compressionMethod != 0)
                {
                    LOG.error(String.format("iCCP chunk: invalid compression method %d",
                            compressionMethod));
                    return null;
                }
                // Skip over the compression method
                iccProfileDataStart++;
                try (OutputStream rawOutputStream = cosStream.createRawOutputStream())
                {
                    rawOutputStream.write(state.iCCP.bytes, state.iCCP.start + iccProfileDataStart,
                            state.iCCP.length - iccProfileDataStart);
                }
            }
            else
            {
                // We tag the image with the sRGB profile
                ICC_Profile rgbProfile = ICC_Profile.getInstance(ColorSpace.CS_sRGB);
                try (OutputStream outputStream = cosStream.createOutputStream())
                {
                    outputStream.write(rgbProfile.getData());
                }
            }
            return cosStream;
        }

        private static COSDictionary buildDecodeParams(PNGConverterState state, PDColorSpace colorSpace)
        {
            COSDictionary decodeParms = new COSDictionary();
            decodeParms.setItem(COSName.BITS_PER_COMPONENT, COSInteger.get(state.bitsPerComponent));
            decodeParms.setItem(COSName.PREDICTOR, COSInteger.get(15));
            decodeParms.setItem(COSName.COLUMNS, COSInteger.get(state.width));
            decodeParms.setItem(COSName.COLORS, COSInteger.get(colorSpace.getNumberOfComponents()));
            return decodeParms;
        }

        /**
         * Build an input stream for the IDAT data. May need to concat multiple IDAT
         * chunks.
         *
         * @param state the converter state.
         * @return a input stream with the IDAT data.
         */
        private static InputStream getIDATInputStream(PNGConverterState state)
        {
            MultipleInputStream inputStream = new MultipleInputStream();
            for (Chunk idat : state.IDATs)
            {
                inputStream.inputStreams
                        .add(new ByteArrayInputStream(idat.bytes, idat.start, idat.length));
            }
            return inputStream;
        }

        private static class MultipleInputStream extends InputStream
        {

            final List<InputStream> inputStreams = new ArrayList<>();
            int currentStreamIdx;
            InputStream currentStream;

            private boolean ensureStream()
            {
                if (currentStream == null)
                {
                    if (currentStreamIdx >= inputStreams.size())
                    {
                        return false;
                    }
                    currentStream = inputStreams.get(currentStreamIdx++);
                }
                return true;
            }

            @Override
            public int read() throws IOException
            {
                if (!ensureStream())
                {
                    return -1;
                }
                int ret = currentStream.read();
                if (ret == -1)
                {
                    currentStream = null;
                    return read();
                }
                return ret;
            }

            @Override
            public int available() throws IOException
            {
                if (!ensureStream())
                {
                    return 0;
                }
                return 1;
            }

            @Override
            public int read(byte[] b, int off, int len) throws IOException
            {
                if (!ensureStream())
                {
                    return -1;
                }
                int ret = currentStream.read(b, off, len);
                if (ret == -1)
                {
                    currentStream = null;
                    return read(b, off, len);
                }
                return ret;
            }
        }

        /**
         * Map the renderIntent int to a PDF render intent. See also
         * https://www.w3.org/TR/2003/REC-PNG-20031110/#11sRGB
         *
         * @param renderIntent the PNG render intent
         * @return the matching PDF Render Intent or null
         */
        static COSName mapPNGRenderIntent(int renderIntent)
        {
            COSName value;
            switch (renderIntent)
            {
            case 0:
                value = COSName.PERCEPTUAL;
                break;
            case 1:
                value = COSName.RELATIVE_COLORIMETRIC;
                break;
            case 2:
                value = COSName.SATURATION;
                break;
            case 3:
                value = COSName.ABSOLUTE_COLORIMETRIC;
                break;
            default:
                value = null;
                break;
            }
            return value;
        }

        /**
         * Check if the converter state is sane.
         *
         * @param state the parsed converter state
         * @return true if the state seems plausible
         */
        static boolean checkConverterState(PNGConverterState state)
        {
            if (state == null)
            {
                return false;
            }
            if (state.IHDR == null || !checkChunkSane(state.IHDR))
            {
                LOG.error("Invalid IHDR chunk.");
                return false;
            }
            if (!checkChunkSane(state.PLTE))
            {
                LOG.error("Invalid PLTE chunk.");
                return false;
            }
            if (!checkChunkSane(state.iCCP))
            {
                LOG.error("Invalid iCCP chunk.");
                return false;
            }
            if (!checkChunkSane(state.tRNS))
            {
                LOG.error("Invalid tRNS chunk.");
                return false;
            }
            if (!checkChunkSane(state.sRGB))
            {
                LOG.error("Invalid sRGB chunk.");
                return false;
            }
            if (!checkChunkSane(state.cHRM))
            {
                LOG.error("Invalid cHRM chunk.");
                return false;
            }
            if (!checkChunkSane(state.gAMA))
            {
                LOG.error("Invalid gAMA chunk.");
                return false;
            }

            // Check the IDATs
            if (state.IDATs.isEmpty())
            {
                LOG.error("No IDAT chunks.");
                return false;
            }
            for (Chunk idat : state.IDATs)
            {
                if (!checkChunkSane(idat))
                {
                    LOG.error("Invalid IDAT chunk.");
                    return false;
                }
            }
            return true;
        }

        /**
         * Check if the chunk is sane, i.e. CRC matches and offsets and lengths in the
         * byte array
         */
        static boolean checkChunkSane(Chunk chunk)
        {
            if (chunk == null)
            {
                // If the chunk does not exist, it can not be wrong...
                return true;
            }

            if (chunk.start + chunk.length > chunk.bytes.length)
            {
                return false;
            }

            if (chunk.start < 4)
            {
                return false;
            }

            // We must include the chunk type in the CRC calculation
            int ourCRC = crc(chunk.bytes, chunk.start - 4, chunk.length + 4);
            if (ourCRC != chunk.crc)
            {
                LOG.error(String.format("Invalid CRC %08X on chunk %08X, expected %08X.", ourCRC,
                        chunk.chunkType, chunk.crc));
                return false;
            }
            return true;
        }

        /**
         * Holds the information about a chunks
         */
        static final class Chunk
        {
            /**
             * This field holds the whole byte array; In that it's redundant, as all chunks
             * will have the same byte array. But have this byte array per chunk makes it
             * easier to validate and pass around. And we won't have that many chunks, so
             * those 8 bytes for the pointer (on 64-bit systems) don't matter.
             */
            byte[] bytes;
            /**
             * The chunk type, see the CHUNK_??? constants.
             */
            int chunkType;
            /**
             * The crc of the chunk data, as stored in the PNG stream.
             */
            int crc;
            /**
             * The start index of the chunk data within bytes.
             */
            int start;
            /**
             * The length of the data within the byte array.
             */
            int length;

            /**
             * Get the data of this chunk as a byte array
             *
             * @return a byte-array with only the data of the chunk
             */
            byte[] getData()
            {
                return Arrays.copyOfRange(bytes, start, start + length);
            }
        }

        /**
         * Holds all relevant chunks of the PNG
         */
        static final class PNGConverterState
        {
            List<Chunk> IDATs = new ArrayList<>();
            @SuppressWarnings("SpellCheckingInspection") Chunk IHDR;
            @SuppressWarnings("SpellCheckingInspection") Chunk PLTE;
            Chunk iCCP;
            Chunk tRNS;
            Chunk sRGB;
            Chunk gAMA;
            Chunk cHRM;

            // Parsed header fields
            int width;
            int height;
            int bitsPerComponent;
        }

        private static int readInt(byte[] data, int offset)
        {
            int b1 = (data[offset] & 0xFF) << 24;
            int b2 = (data[offset + 1] & 0xFF) << 16;
            int b3 = (data[offset + 2] & 0xFF) << 8;
            int b4 = (data[offset + 3] & 0xFF);
            return b1 | b2 | b3 | b4;
        }

        private static float readPNGFloat(byte[] bytes, int offset)
        {
            int v = readInt(bytes, offset);
            return v / 100000f;
        }

        /**
         * Parse the PNG structure into the PNGConverterState. If we can't handle
         * something, this method will return null.
         *
         * @param imageData the byte array with the PNG data
         * @return null or the converter state with all relevant chunks
         */
        private static PNGConverterState parsePNGChunks(byte[] imageData)
        {
            if (imageData.length < 20)
            {
                LOG.error("ByteArray way to small: " + imageData.length);
                return null;
            }

            PNGConverterState state = new PNGConverterState();
            int ptr = 8;
            int firstChunkType = readInt(imageData, ptr + 4);

            if (firstChunkType != CHUNK_IHDR)
            {
                LOG.error(String.format("First Chunktype was %08X, not IHDR", firstChunkType));
                return null;
            }

            while (ptr + 12 <= imageData.length)
            {
                int chunkLength = readInt(imageData, ptr);
                int chunkType = readInt(imageData, ptr + 4);
                ptr += 8;

                if (ptr + chunkLength + 4 > imageData.length)
                {
                    LOG.error("Not enough bytes. At offset " + ptr + " are " + chunkLength
                            + " bytes expected. Overall length is " + imageData.length);
                    return null;
                }

                Chunk chunk = new Chunk();
                chunk.chunkType = chunkType;
                chunk.bytes = imageData;
                chunk.start = ptr;
                chunk.length = chunkLength;

                switch (chunkType)
                {
                case CHUNK_IHDR:
                    if (state.IHDR != null)
                    {
                        LOG.error("Two IHDR chunks? There is something wrong.");
                        return null;
                    }
                    state.IHDR = chunk;
                    break;
                case CHUNK_IDAT:
                    // The image data itself
                    state.IDATs.add(chunk);
                    break;
                case CHUNK_PLTE:
                    // For indexed images the palette table
                    if (state.PLTE != null)
                    {
                        LOG.error("Two PLTE chunks? There is something wrong.");
                        return null;
                    }
                    state.PLTE = chunk;
                    break;
                case CHUNK_IEND:
                    // We are done, return the state
                    return state;
                case CHUNK_TRNS:
                    // For indexed images the alpha transparency table
                    if (state.tRNS != null)
                    {
                        LOG.error("Two tRNS chunks? There is something wrong.");
                        return null;
                    }
                    state.tRNS = chunk;
                    break;
                case CHUNK_GAMA:
                    // Gama
                    state.gAMA = chunk;
                    break;
                case CHUNK_CHRM:
                    // Chroma
                    state.cHRM = chunk;
                    break;
                case CHUNK_ICCP:
                    // ICC Profile
                    state.iCCP = chunk;
                    break;
                case CHUNK_SBIT:
                    LOG.debug("Can't convert PNGs with sBIT chunk.");
                    break;
                case CHUNK_SRGB:
                    // We use the rendering intent from the chunk
                    state.sRGB = chunk;
                    break;
                case CHUNK_TEXT:
                case CHUNK_ZTXT:
                case CHUNK_ITXT:
                    // We don't care about this text infos / metadata
                    break;
                case CHUNK_KBKG:
                    // As we can handle transparency we don't need the background color information.
                    break;
                case CHUNK_HIST:
                    // We don't need the color histogram
                    break;
                case CHUNK_PHYS:
                    // The PDImageXObject will be placed by the user however he wants,
                    // so we can not enforce the physical dpi information stored here.
                    // We just ignore it.
                    break;
                case CHUNK_SPLT:
                    // This palette stuff seems editor related, we don't need it.
                    break;
                case CHUNK_TIME:
                    // We don't need the last image change time either
                    break;
                default:
                    LOG.debug(String.format("Unknown chunk type %08X, skipping.", chunkType));
                    break;
                }
                ptr += chunkLength;

                // Read the CRC
                chunk.crc = readInt(imageData, ptr);
                ptr += 4;
            }
            LOG.error("No IEND chunk found.");
            return null;
        }

        /* Make the table for a fast CRC. */
        private static void makeCrcTable()
        {
            int c;

            for (int n = 0; n < 256; n++)
            {
                c = n;
                for (int k = 0; k < 8; k++)
                {
                    if ((c & 1) != 0)
                    {
                        c = 0xEDB88320 ^ (c >>> 1);
                    }
                    else
                    {
                        c = c >>> 1;
                    }
                }
                CRC_TABLE[n] = c;
            }
        }

        /*
         * Update a running CRC with the bytes buf[0..len-1]--the CRC should be
         * initialized to all 1's, and the transmitted value is the 1's complement of
         * the final running CRC (see the crc() routine below).
         */
        private static int updateCrc(byte[] buf, int offset, int len)
        {
            int c = -1;
            int end = offset + len;
            for (int n = offset; n < end; n++)
            {
                c = CRC_TABLE[(c ^ buf[n]) & 0xff] ^ (c >>> 8);
            }
            return c;
        }

        /* Return the CRC of the bytes buf[offset..(offset+len-1)]. */
        static int crc(byte[] buf, int offset, int len)
        {
            return ~updateCrc(buf, offset, len);
        }
    }