    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *      http://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017package org.apache.commons.imaging.formats.png;
    018
    019import static org.apache.commons.imaging.common.BinaryFunctions.printCharQuad;
    020import static org.apache.commons.imaging.common.BinaryFunctions.read4Bytes;
    021import static org.apache.commons.imaging.common.BinaryFunctions.readAndVerifyBytes;
    022import static org.apache.commons.imaging.common.BinaryFunctions.readBytes;
    023import static org.apache.commons.imaging.common.BinaryFunctions.skipBytes;
    024
    025import java.awt.Dimension;
    026import java.awt.color.ColorSpace;
    027import java.awt.color.ICC_ColorSpace;
    028import java.awt.color.ICC_Profile;
    029import java.awt.image.BufferedImage;
    030import java.awt.image.ColorModel;
    031import java.io.ByteArrayInputStream;
    032import java.io.ByteArrayOutputStream;
    033import java.io.IOException;
    034import java.io.InputStream;
    035import java.io.OutputStream;
    036import java.io.PrintWriter;
    037import java.util.ArrayList;
    038import java.util.List;
    039import java.util.logging.Level;
    040import java.util.logging.Logger;
    041import java.util.zip.InflaterInputStream;
    042
    043import org.apache.commons.imaging.ColorTools;
    044import org.apache.commons.imaging.ImageFormat;
    045import org.apache.commons.imaging.ImageFormats;
    046import org.apache.commons.imaging.ImageInfo;
    047import org.apache.commons.imaging.ImageParser;
    048import org.apache.commons.imaging.ImageReadException;
    049import org.apache.commons.imaging.ImageWriteException;
    050import org.apache.commons.imaging.common.GenericImageMetadata;
    051import org.apache.commons.imaging.common.ImageMetadata;
    052import org.apache.commons.imaging.common.XmpEmbeddable;
    053import org.apache.commons.imaging.common.XmpImagingParameters;
    054import org.apache.commons.imaging.common.bytesource.ByteSource;
    055import org.apache.commons.imaging.formats.png.chunks.PngChunk;
    056import org.apache.commons.imaging.formats.png.chunks.PngChunkGama;
    057import org.apache.commons.imaging.formats.png.chunks.PngChunkIccp;
    058import org.apache.commons.imaging.formats.png.chunks.PngChunkIdat;
    059import org.apache.commons.imaging.formats.png.chunks.PngChunkIhdr;
    060import org.apache.commons.imaging.formats.png.chunks.PngChunkItxt;
    061import org.apache.commons.imaging.formats.png.chunks.PngChunkPhys;
    062import org.apache.commons.imaging.formats.png.chunks.PngChunkPlte;
    063import org.apache.commons.imaging.formats.png.chunks.PngChunkScal;
    064import org.apache.commons.imaging.formats.png.chunks.PngChunkText;
    065import org.apache.commons.imaging.formats.png.chunks.PngChunkZtxt;
    066import org.apache.commons.imaging.formats.png.chunks.PngTextChunk;
    067import org.apache.commons.imaging.formats.png.transparencyfilters.TransparencyFilter;
    068import org.apache.commons.imaging.formats.png.transparencyfilters.TransparencyFilterGrayscale;
    069import org.apache.commons.imaging.formats.png.transparencyfilters.TransparencyFilterIndexedColor;
    070import org.apache.commons.imaging.formats.png.transparencyfilters.TransparencyFilterTrueColor;
    071import org.apache.commons.imaging.icc.IccProfileParser;
    072
    073public class PngImageParser extends ImageParser<PngImagingParameters>  implements XmpEmbeddable {
    074
    075    private static final Logger LOGGER = Logger.getLogger(PngImageParser.class.getName());
    076
    077    private static final String DEFAULT_EXTENSION = ImageFormats.PNG.getDefaultExtension();
    078    private static final String[] ACCEPTED_EXTENSIONS = ImageFormats.PNG.getExtensions();
    079
    080    @Override
    081    public PngImagingParameters getDefaultParameters() {
    082        return new PngImagingParameters();
    083    }
    084
    085    @Override
    086    public String getName() {
    087        return "Png-Custom";
    088    }
    089
    090    @Override
    091    public String getDefaultExtension() {
    092        return DEFAULT_EXTENSION;
    093    }
    094
    095    @Override
    096    protected String[] getAcceptedExtensions() {
    097        return ACCEPTED_EXTENSIONS.clone();
    098    }
    099
    100    @Override
    101    protected ImageFormat[] getAcceptedTypes() {
    102        return new ImageFormat[] { ImageFormats.PNG, //
    103        };
    104    }
    105
    106    // private final static int tRNS = CharsToQuad('t', 'R', 'N', 's');
    107
    108    public static String getChunkTypeName(final int chunkType) {
    109        final StringBuilder result = new StringBuilder();
    110        result.append((char) (0xff & (chunkType >> 24)));
    111        result.append((char) (0xff & (chunkType >> 16)));
    112        result.append((char) (0xff & (chunkType >> 8)));
    113        result.append((char) (0xff & (chunkType >> 0)));
    114        return result.toString();
    115    }
    116
    117    /**
    118     * @param is PNG image input stream
    119     * @return List of String-formatted chunk types, ie. "tRNs".
    120     * @throws ImageReadException if it fail to read the PNG chunks
    121     * @throws IOException if it fails to read the input stream data
    122     */
    123    public List<String> getChunkTypes(final InputStream is)
    124            throws ImageReadException, IOException {
    125        final List<PngChunk> chunks = readChunks(is, null, false);
    126        final List<String> chunkTypes = new ArrayList<>(chunks.size());
    127        for (final PngChunk chunk : chunks) {
    128            chunkTypes.add(getChunkTypeName(chunk.chunkType));
    129        }
    130        return chunkTypes;
    131    }
    132
    133    public boolean hasChunkType(final ByteSource byteSource, final ChunkType chunkType)
    134            throws ImageReadException, IOException {
    135        try (InputStream is = byteSource.getInputStream()) {
    136            readSignature(is);
    137            final List<PngChunk> chunks = readChunks(is, new ChunkType[] { chunkType }, true);
    138            return !chunks.isEmpty();
    139        }
    140    }
    141
    142    private boolean keepChunk(final int chunkType, final ChunkType[] chunkTypes) {
    143        // System.out.println("keepChunk: ");
    144        if (chunkTypes == null) {
    145            return true;
    146        }
    147
    148        for (final ChunkType chunkType2 : chunkTypes) {
    149            if (chunkType2.value == chunkType) {
    150                return true;
    151            }
    152        }
    153        return false;
    154    }
    155
    156    private List<PngChunk> readChunks(final InputStream is, final ChunkType[] chunkTypes,
    157            final boolean returnAfterFirst) throws ImageReadException, IOException {
    158        final List<PngChunk> result = new ArrayList<>();
    159
    160        while (true) {
    161            final int length = read4Bytes("Length", is, "Not a Valid PNG File", getByteOrder());
    162            if (length < 0) {
    163                throw new ImageReadException("Invalid PNG chunk length: " + length);
    164            }
    165            final int chunkType = read4Bytes("ChunkType", is, "Not a Valid PNG File", getByteOrder());
    166
    167            if (LOGGER.isLoggable(Level.FINEST)) {
    168                printCharQuad("ChunkType", chunkType);
    169                debugNumber("Length", length, 4);
    170            }
    171            final boolean keep = keepChunk(chunkType, chunkTypes);
    172
    173            byte[] bytes = null;
    174            if (keep) {
    175                bytes = readBytes("Chunk Data", is, length,
    176                        "Not a Valid PNG File: Couldn't read Chunk Data.");
    177            } else {
    178                skipBytes(is, length, "Not a Valid PNG File");
    179            }
    180
    181            if (LOGGER.isLoggable(Level.FINEST)) {
    182                if (bytes != null) {
    183                    debugNumber("bytes", bytes.length, 4);
    184                }
    185            }
    186
    187            final int crc = read4Bytes("CRC", is, "Not a Valid PNG File", getByteOrder());
    188
    189            if (keep) {
    190                if (chunkType == ChunkType.iCCP.value) {
    191                    result.add(new PngChunkIccp(length, chunkType, crc, bytes));
    192                } else if (chunkType == ChunkType.tEXt.value) {
    193                    result.add(new PngChunkText(length, chunkType, crc, bytes));
    194                } else if (chunkType == ChunkType.zTXt.value) {
    195                    result.add(new PngChunkZtxt(length, chunkType, crc, bytes));
    196                } else if (chunkType == ChunkType.IHDR.value) {
    197                    result.add(new PngChunkIhdr(length, chunkType, crc, bytes));
    198                } else if (chunkType == ChunkType.PLTE.value) {
    199                    result.add(new PngChunkPlte(length, chunkType, crc, bytes));
    200                } else if (chunkType == ChunkType.pHYs.value) {
    201                    result.add(new PngChunkPhys(length, chunkType, crc, bytes));
    202                } else if (chunkType == ChunkType.sCAL.value) {
    203                    result.add(new PngChunkScal(length, chunkType, crc, bytes));
    204                } else if (chunkType == ChunkType.IDAT.value) {
    205                    result.add(new PngChunkIdat(length, chunkType, crc, bytes));
    206                } else if (chunkType == ChunkType.gAMA.value) {
    207                    result.add(new PngChunkGama(length, chunkType, crc, bytes));
    208                } else if (chunkType == ChunkType.iTXt.value) {
    209                    result.add(new PngChunkItxt(length, chunkType, crc, bytes));
    210                } else {
    211                    result.add(new PngChunk(length, chunkType, crc, bytes));
    212                }
    213
    214                if (returnAfterFirst) {
    215                    return result;
    216                }
    217            }
    218
    219            if (chunkType == ChunkType.IEND.value) {
    220                break;
    221            }
    222
    223        }
    224
    225        return result;
    226
    227    }
    228
    229    public void readSignature(final InputStream is) throws ImageReadException,
    230            IOException {
    231        readAndVerifyBytes(is, PngConstants.PNG_SIGNATURE,
    232                "Not a Valid PNG Segment: Incorrect Signature");
    233
    234    }
    235
    236    private List<PngChunk> readChunks(final ByteSource byteSource, final ChunkType[] chunkTypes,
    237            final boolean returnAfterFirst) throws ImageReadException, IOException {
    238        try (InputStream is = byteSource.getInputStream()) {
    239            readSignature(is);
    240            return readChunks(is, chunkTypes, returnAfterFirst);
    241        }
    242    }
    243
    244    @Override
    245    public byte[] getICCProfileBytes(final ByteSource byteSource, final PngImagingParameters params)
    246            throws ImageReadException, IOException {
    247        final List<PngChunk> chunks = readChunks(byteSource, new ChunkType[] { ChunkType.iCCP },
    248                true);
    249
    250        if (chunks.isEmpty()) {
    251            return null;
    252        }
    253
    254        if (chunks.size() > 1) {
    255            throw new ImageReadException(
    256                    "PNG contains more than one ICC Profile ");
    257        }
    258
    259        final PngChunkIccp pngChunkiCCP = (PngChunkIccp) chunks.get(0);
    260
    261        return (pngChunkiCCP.getUncompressedProfile());// TODO should this be a clone?
    262    }
    263
    264    @Override
    265    public Dimension getImageSize(final ByteSource byteSource, final PngImagingParameters params)
    266            throws ImageReadException, IOException {
    267        final List<PngChunk> chunks = readChunks(byteSource, new ChunkType[] { ChunkType.IHDR, }, true);
    268
    269        if (chunks.isEmpty()) {
    270            throw new ImageReadException("Png: No chunks");
    271        }
    272
    273        if (chunks.size() > 1) {
    274            throw new ImageReadException("PNG contains more than one Header");
    275        }
    276
    277        final PngChunkIhdr pngChunkIHDR = (PngChunkIhdr) chunks.get(0);
    278
    279        return new Dimension(pngChunkIHDR.width, pngChunkIHDR.height);
    280    }
    281
    282    @Override
    283    public ImageMetadata getMetadata(final ByteSource byteSource, final PngImagingParameters params)
    284            throws ImageReadException, IOException {
    285        final List<PngChunk> chunks = readChunks(byteSource, new ChunkType[] { ChunkType.tEXt, ChunkType.zTXt, }, false);
    286
    287        if (chunks.isEmpty()) {
    288            return null;
    289        }
    290
    291        final GenericImageMetadata result = new GenericImageMetadata();
    292
    293        for (final PngChunk chunk : chunks) {
    294            final PngTextChunk textChunk = (PngTextChunk) chunk;
    295
    296            result.add(textChunk.getKeyword(), textChunk.getText());
    297        }
    298
    299        return result;
    300    }
    301
    302    private List<PngChunk> filterChunks(final List<PngChunk> chunks, final ChunkType type) {
    303        final List<PngChunk> result = new ArrayList<>();
    304
    305        for (final PngChunk chunk : chunks) {
    306            if (chunk.chunkType == type.value) {
    307                result.add(chunk);
    308            }
    309        }
    310
    311        return result;
    312    }
    313
    314    // TODO: I have been too casual about making inner classes subclass of
    315    // BinaryFileParser
    316    // I may not have always preserved byte order correctly.
    317
    318    private TransparencyFilter getTransparencyFilter(final PngColorType pngColorType, final PngChunk pngChunktRNS)
    319            throws ImageReadException, IOException {
    320        switch (pngColorType) {
    321            case GREYSCALE: // 1,2,4,8,16 Each pixel is a grayscale sample.
    322                return new TransparencyFilterGrayscale(pngChunktRNS.getBytes());
    323            case TRUE_COLOR: // 8,16 Each pixel is an R,G,B triple.
    324                return new TransparencyFilterTrueColor(pngChunktRNS.getBytes());
    325            case INDEXED_COLOR: // 1,2,4,8 Each pixel is a palette index;
    326                return new TransparencyFilterIndexedColor(pngChunktRNS.getBytes());
    327            case GREYSCALE_WITH_ALPHA: // 8,16 Each pixel is a grayscale sample,
    328            case TRUE_COLOR_WITH_ALPHA: // 8,16 Each pixel is an R,G,B triple,
    329            default:
    330                throw new ImageReadException("Simple Transparency not compatible with ColorType: " + pngColorType);
    331        }
    332    }
    333
    334    @Override
    335    public ImageInfo getImageInfo(final ByteSource byteSource, final PngImagingParameters params)
    336            throws ImageReadException, IOException {
    337        final List<PngChunk> chunks = readChunks(byteSource, new ChunkType[] {
    338                ChunkType.IHDR,
    339                ChunkType.pHYs,
    340                ChunkType.sCAL,
    341                ChunkType.tEXt,
    342                ChunkType.zTXt,
    343                ChunkType.tRNS,
    344                ChunkType.PLTE,
    345                ChunkType.iTXt,
    346            }, false);
    347
    348        if (chunks.isEmpty()) {
    349            throw new ImageReadException("PNG: no chunks");
    350        }
    351
    352        final List<PngChunk> IHDRs = filterChunks(chunks, ChunkType.IHDR);
    353        if (IHDRs.size() != 1) {
    354            throw new ImageReadException("PNG contains more than one Header");
    355        }
    356
    357        final PngChunkIhdr pngChunkIHDR = (PngChunkIhdr) IHDRs.get(0);
    358
    359        boolean transparent = false;
    360
    361        final List<PngChunk> tRNSs = filterChunks(chunks, ChunkType.tRNS);
    362        if (!tRNSs.isEmpty()) {
    363            transparent = true;
    364        } else {
    365            // CE - Fix Alpha.
    366            transparent = pngChunkIHDR.pngColorType.hasAlpha();
    367            // END FIX
    368        }
    369
    370        PngChunkPhys pngChunkpHYs = null;
    371
    372        final List<PngChunk> pHYss = filterChunks(chunks, ChunkType.pHYs);
    373        if (pHYss.size() > 1) {
    374            throw new ImageReadException("PNG contains more than one pHYs: "
    375                    + pHYss.size());
    376        }
    377        if (pHYss.size() == 1) {
    378            pngChunkpHYs = (PngChunkPhys) pHYss.get(0);
    379        }
    380
    381        PhysicalScale physicalScale = PhysicalScale.UNDEFINED;
    382
    383        final List<PngChunk> sCALs = filterChunks(chunks, ChunkType.sCAL);
    384        if (sCALs.size() > 1) {
    385            throw new ImageReadException("PNG contains more than one sCAL:"
    386                    + sCALs.size());
    387        }
    388        if (sCALs.size() == 1) {
    389            final PngChunkScal pngChunkScal = (PngChunkScal) sCALs.get(0);
    390            if (pngChunkScal.unitSpecifier == 1) {
    391                physicalScale = PhysicalScale.createFromMeters(pngChunkScal.unitsPerPixelXAxis,
    392                      pngChunkScal.unitsPerPixelYAxis);
    393            } else {
    394                physicalScale = PhysicalScale.createFromRadians(pngChunkScal.unitsPerPixelXAxis,
    395                      pngChunkScal.unitsPerPixelYAxis);
    396            }
    397        }
    398
    399        final List<PngChunk> tEXts = filterChunks(chunks, ChunkType.tEXt);
    400        final List<PngChunk> zTXts = filterChunks(chunks, ChunkType.zTXt);
    401        final List<PngChunk> iTXts = filterChunks(chunks, ChunkType.iTXt);
    402
    403        final int chunkCount = tEXts.size() + zTXts.size() + iTXts.size();
    404        final List<String> comments = new ArrayList<>(chunkCount);
    405        final List<PngText> textChunks = new ArrayList<>(chunkCount);
    406
    407        for (final PngChunk tEXt : tEXts) {
    408            final PngChunkText pngChunktEXt = (PngChunkText) tEXt;
    409            comments.add(pngChunktEXt.keyword + ": " + pngChunktEXt.text);
    410            textChunks.add(pngChunktEXt.getContents());
    411        }
    412        for (final PngChunk zTXt : zTXts) {
    413            final PngChunkZtxt pngChunkzTXt = (PngChunkZtxt) zTXt;
    414            comments.add(pngChunkzTXt.keyword + ": " + pngChunkzTXt.text);
    415            textChunks.add(pngChunkzTXt.getContents());
    416        }
    417        for (final PngChunk iTXt : iTXts) {
    418            final PngChunkItxt pngChunkiTXt = (PngChunkItxt) iTXt;
    419            comments.add(pngChunkiTXt.keyword + ": " + pngChunkiTXt.text);
    420            textChunks.add(pngChunkiTXt.getContents());
    421        }
    422
    423        final int bitsPerPixel = pngChunkIHDR.bitDepth * pngChunkIHDR.pngColorType.getSamplesPerPixel();
    424        final ImageFormat format = ImageFormats.PNG;
    425        final String formatName = "PNG Portable Network Graphics";
    426        final int height = pngChunkIHDR.height;
    427        final String mimeType = "image/png";
    428        final int numberOfImages = 1;
    429        final int width = pngChunkIHDR.width;
    430        final boolean progressive = pngChunkIHDR.interlaceMethod.isProgressive();
    431
    432        int physicalHeightDpi = -1;
    433        float physicalHeightInch = -1;
    434        int physicalWidthDpi = -1;
    435        float physicalWidthInch = -1;
    436
    437        // if (pngChunkpHYs != null)
    438        // {
    439        // System.out.println("\t" + "pngChunkpHYs.UnitSpecifier: " +
    440        // pngChunkpHYs.UnitSpecifier );
    441        // System.out.println("\t" + "pngChunkpHYs.PixelsPerUnitYAxis: " +
    442        // pngChunkpHYs.PixelsPerUnitYAxis );
    443        // System.out.println("\t" + "pngChunkpHYs.PixelsPerUnitXAxis: " +
    444        // pngChunkpHYs.PixelsPerUnitXAxis );
    445        // }
    446        if ((pngChunkpHYs != null) && (pngChunkpHYs.unitSpecifier == 1)) { // meters
    447            final double metersPerInch = 0.0254;
    448
    449            physicalWidthDpi = (int) Math.round(pngChunkpHYs.pixelsPerUnitXAxis * metersPerInch);
    450            physicalWidthInch = (float) (width / (pngChunkpHYs.pixelsPerUnitXAxis * metersPerInch));
    451            physicalHeightDpi = (int) Math.round(pngChunkpHYs.pixelsPerUnitYAxis * metersPerInch);
    452            physicalHeightInch = (float) (height / (pngChunkpHYs.pixelsPerUnitYAxis * metersPerInch));
    453        }
    454
    455        boolean usesPalette = false;
    456
    457        final List<PngChunk> PLTEs = filterChunks(chunks, ChunkType.PLTE);
    458        if (PLTEs.size() > 1) {
    459            usesPalette = true;
    460        }
    461
    462        ImageInfo.ColorType colorType;
    463        switch (pngChunkIHDR.pngColorType) {
    464            case GREYSCALE:
    465            case GREYSCALE_WITH_ALPHA:
    466                colorType = ImageInfo.ColorType.GRAYSCALE;
    467                break;
    468            case TRUE_COLOR:
    469            case INDEXED_COLOR:
    470            case TRUE_COLOR_WITH_ALPHA:
    471                colorType = ImageInfo.ColorType.RGB;
    472                break;
    473            default:
    474                throw new ImageReadException("Png: Unknown ColorType: " + pngChunkIHDR.pngColorType);
    475        }
    476
    477        final String formatDetails = "Png";
    478        final ImageInfo.CompressionAlgorithm compressionAlgorithm = ImageInfo.CompressionAlgorithm.PNG_FILTER;
    479
    480        return new PngImageInfo(formatDetails, bitsPerPixel, comments,
    481                format, formatName, height, mimeType, numberOfImages,
    482                physicalHeightDpi, physicalHeightInch, physicalWidthDpi,
    483                physicalWidthInch, width, progressive, transparent,
    484                usesPalette, colorType, compressionAlgorithm, textChunks,
    485                physicalScale);
    486    }
    487
    488    @Override
    489    public BufferedImage getBufferedImage(final ByteSource byteSource, PngImagingParameters params)
    490            throws ImageReadException, IOException {
    491
    492        final List<PngChunk> chunks = readChunks(byteSource, new ChunkType[] {
    493                ChunkType.IHDR,
    494                ChunkType.PLTE,
    495                ChunkType.IDAT,
    496                ChunkType.tRNS,
    497                ChunkType.iCCP,
    498                ChunkType.gAMA,
    499                ChunkType.sRGB,
    500            }, false);
    501
    502        if (chunks.isEmpty()) {
    503            throw new ImageReadException("PNG: no chunks");
    504        }
    505
    506        final List<PngChunk> IHDRs = filterChunks(chunks, ChunkType.IHDR);
    507        if (IHDRs.size() != 1) {
    508            throw new ImageReadException("PNG contains more than one Header");
    509        }
    510
    511        final PngChunkIhdr pngChunkIHDR = (PngChunkIhdr) IHDRs.get(0);
    512
    513        final List<PngChunk> PLTEs = filterChunks(chunks, ChunkType.PLTE);
    514        if (PLTEs.size() > 1) {
    515            throw new ImageReadException("PNG contains more than one Palette");
    516        }
    517
    518        PngChunkPlte pngChunkPLTE = null;
    519        if (PLTEs.size() == 1) {
    520            pngChunkPLTE = (PngChunkPlte) PLTEs.get(0);
    521        }
    522
    523        // -----
    524
    525        final List<PngChunk> IDATs = filterChunks(chunks, ChunkType.IDAT);
    526        if (IDATs.isEmpty()) {
    527            throw new ImageReadException("PNG missing image data");
    528        }
    529
    530        ByteArrayOutputStream baos = new ByteArrayOutputStream();
    531        for (final PngChunk IDAT : IDATs) {
    532            final PngChunkIdat pngChunkIDAT = (PngChunkIdat) IDAT;
    533            final byte[] bytes = pngChunkIDAT.getBytes();
    534            // System.out.println(i + ": bytes: " + bytes.length);
    535            baos.write(bytes);
    536        }
    537
    538        final byte[] compressed = baos.toByteArray();
    539
    540        baos = null;
    541
    542        TransparencyFilter transparencyFilter = null;
    543
    544        final List<PngChunk> tRNSs = filterChunks(chunks, ChunkType.tRNS);
    545        if (!tRNSs.isEmpty()) {
    546            final PngChunk pngChunktRNS = tRNSs.get(0);
    547            transparencyFilter = getTransparencyFilter(pngChunkIHDR.pngColorType, pngChunktRNS);
    548        }
    549
    550        ICC_Profile iccProfile = null;
    551        GammaCorrection gammaCorrection = null;
    552        {
    553            final List<PngChunk> sRGBs = filterChunks(chunks, ChunkType.sRGB);
    554            final List<PngChunk> gAMAs = filterChunks(chunks, ChunkType.gAMA);
    555            final List<PngChunk> iCCPs = filterChunks(chunks, ChunkType.iCCP);
    556            if (sRGBs.size() > 1) {
    557                throw new ImageReadException("PNG: unexpected sRGB chunk");
    558            }
    559            if (gAMAs.size() > 1) {
    560                throw new ImageReadException("PNG: unexpected gAMA chunk");
    561            }
    562            if (iCCPs.size() > 1) {
    563                throw new ImageReadException("PNG: unexpected iCCP chunk");
    564            }
    565
    566            if (sRGBs.size() == 1) {
    567                // no color management necessary.
    568                if (LOGGER.isLoggable(Level.FINEST)) {
    569                    LOGGER.finest("sRGB, no color management necessary.");
    570                }
    571            } else if (iCCPs.size() == 1) {
    572                if (LOGGER.isLoggable(Level.FINEST)) {
    573                    LOGGER.finest("iCCP.");
    574                }
    575
    576                final PngChunkIccp pngChunkiCCP = (PngChunkIccp) iCCPs.get(0);
    577                final byte[] bytes = pngChunkiCCP.getUncompressedProfile();
    578
    579                try {
    580                    iccProfile = ICC_Profile.getInstance(bytes);
    581                } catch (IllegalArgumentException iae) {
    582                    throw new ImageReadException("The image data does not correspond to a valid ICC Profile", iae);
    583                }
    584            } else if (gAMAs.size() == 1) {
    585                final PngChunkGama pngChunkgAMA = (PngChunkGama) gAMAs.get(0);
    586                final double gamma = pngChunkgAMA.getGamma();
    587
    588                // charles: what is the correct target value here?
    589                // double targetGamma = 2.2;
    590                final double targetGamma = 1.0;
    591                final double diff = Math.abs(targetGamma - gamma);
    592                if (diff >= 0.5) {
    593                    gammaCorrection = new GammaCorrection(gamma, targetGamma);
    594                }
    595
    596                if (gammaCorrection != null) {
    597                    if (pngChunkPLTE != null) {
    598                        pngChunkPLTE.correct(gammaCorrection);
    599                    }
    600                }
    601
    602            }
    603        }
    604
    605        {
    606            final int width = pngChunkIHDR.width;
    607            final int height = pngChunkIHDR.height;
    608            final PngColorType pngColorType = pngChunkIHDR.pngColorType;
    609            final int bitDepth = pngChunkIHDR.bitDepth;
    610
    611            if (pngChunkIHDR.filterMethod != 0) {
    612                throw new ImageReadException("PNG: unknown FilterMethod: " + pngChunkIHDR.filterMethod);
    613            }
    614
    615            final int bitsPerPixel = bitDepth * pngColorType.getSamplesPerPixel();
    616
    617            final boolean hasAlpha = pngColorType.hasAlpha() || transparencyFilter != null;
    618
    619            BufferedImage result;
    620            if (pngColorType.isGreyscale()) {
    621                result = getBufferedImageFactory(params).getGrayscaleBufferedImage(width, height, hasAlpha);
    622            } else {
    623                result = getBufferedImageFactory(params).getColorBufferedImage(width, height, hasAlpha);
    624            }
    625
    626            final ByteArrayInputStream bais = new ByteArrayInputStream(compressed);
    627            final InflaterInputStream iis = new InflaterInputStream(bais);
    628
    629            ScanExpediter scanExpediter;
    630
    631            switch (pngChunkIHDR.interlaceMethod) {
    632                case NONE:
    633                    scanExpediter = new ScanExpediterSimple(width, height, iis,
    634                            result, pngColorType, bitDepth, bitsPerPixel,
    635                            pngChunkPLTE, gammaCorrection, transparencyFilter);
    636                    break;
    637                case ADAM7:
    638                    scanExpediter = new ScanExpediterInterlaced(width, height, iis,
    639                            result, pngColorType, bitDepth, bitsPerPixel,
    640                            pngChunkPLTE, gammaCorrection, transparencyFilter);
    641                    break;
    642                default:
    643                    throw new ImageReadException("Unknown InterlaceMethod: " + pngChunkIHDR.interlaceMethod);
    644            }
    645
    646            scanExpediter.drive();
    647
    648            if (iccProfile != null) {
    649                final boolean is_srgb = new IccProfileParser().issRGB(iccProfile);
    650                if (!is_srgb) {
    651                    final ICC_ColorSpace cs = new ICC_ColorSpace(iccProfile);
    652
    653                    final ColorModel srgbCM = ColorModel.getRGBdefault();
    654                    final ColorSpace cs_sRGB = srgbCM.getColorSpace();
    655
    656                    result = new ColorTools().convertBetweenColorSpaces(result, cs, cs_sRGB);
    657                }
    658            }
    659
    660            return result;
    661
    662        }
    663
    664    }
    665
    666    @Override
    667    public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource)
    668            throws ImageReadException, IOException {
    669        final ImageInfo imageInfo = getImageInfo(byteSource);
    670        if (imageInfo == null) {
    671            return false;
    672        }
    673
    674        imageInfo.toString(pw, "");
    675
    676        final List<PngChunk> chunks = readChunks(byteSource, null, false);
    677        final List<PngChunk> IHDRs = filterChunks(chunks, ChunkType.IHDR);
    678        if (IHDRs.size() != 1) {
    679            if (LOGGER.isLoggable(Level.FINEST)) {
    680                LOGGER.finest("PNG contains more than one Header");
    681            }
    682            return false;
    683        }
    684        final PngChunkIhdr pngChunkIHDR = (PngChunkIhdr) IHDRs.get(0);
    685        pw.println("Color: " + pngChunkIHDR.pngColorType.name());
    686
    687        pw.println("chunks: " + chunks.size());
    688
    689        if ((chunks.isEmpty())) {
    690            return false;
    691        }
    692
    693        for (int i = 0; i < chunks.size(); i++) {
    694            final PngChunk chunk = chunks.get(i);
    695            printCharQuad(pw, "\t" + i + ": ", chunk.chunkType);
    696        }
    697
    698        pw.println("");
    699
    700        pw.flush();
    701
    702        return true;
    703    }
    704
    705    @Override
    706    public void writeImage(final BufferedImage src, final OutputStream os, final PngImagingParameters params)
    707            throws ImageWriteException, IOException {
    708        new PngWriter().writeImage(src, os, params);
    709    }
    710
    711    @Override
    712    public String getXmpXml(final ByteSource byteSource, final XmpImagingParameters params)
    713            throws ImageReadException, IOException {
    714
    715        final List<PngChunk> chunks = readChunks(byteSource, new ChunkType[] { ChunkType.iTXt }, false);
    716
    717        if (chunks.isEmpty()) {
    718            return null;
    719        }
    720
    721        final List<PngChunkItxt> xmpChunks = new ArrayList<>();
    722        for (final PngChunk chunk : chunks) {
    723            final PngChunkItxt itxtChunk = (PngChunkItxt) chunk;
    724            if (!itxtChunk.getKeyword().equals(PngConstants.XMP_KEYWORD)) {
    725                continue;
    726            }
    727            xmpChunks.add(itxtChunk);
    728        }
    729
    730        if (xmpChunks.isEmpty()) {
    731            return null;
    732        }
    733        if (xmpChunks.size() > 1) {
    734            throw new ImageReadException(
    735                    "PNG contains more than one XMP chunk.");
    736        }
    737
    738        final PngChunkItxt chunk = xmpChunks.get(0);
    739        return chunk.getText();
    740    }
    741
    742}