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}