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