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