View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.imaging.formats.png;
18  
19  import java.awt.Dimension;
20  import java.awt.color.ColorSpace;
21  import java.awt.color.ICC_ColorSpace;
22  import java.awt.color.ICC_Profile;
23  import java.awt.image.BufferedImage;
24  import java.awt.image.ColorModel;
25  import java.io.ByteArrayInputStream;
26  import java.io.ByteArrayOutputStream;
27  import java.io.IOException;
28  import java.io.InputStream;
29  import java.io.OutputStream;
30  import java.io.PrintWriter;
31  import java.util.ArrayList;
32  import java.util.List;
33  import java.util.logging.Level;
34  import java.util.logging.Logger;
35  import java.util.zip.InflaterInputStream;
36  
37  import org.apache.commons.imaging.AbstractImageParser;
38  import org.apache.commons.imaging.ColorTools;
39  import org.apache.commons.imaging.ImageFormat;
40  import org.apache.commons.imaging.ImageFormats;
41  import org.apache.commons.imaging.ImageInfo;
42  import org.apache.commons.imaging.ImagingException;
43  import org.apache.commons.imaging.bytesource.ByteSource;
44  import org.apache.commons.imaging.common.Allocator;
45  import org.apache.commons.imaging.common.BinaryFunctions;
46  import org.apache.commons.imaging.common.GenericImageMetadata;
47  import org.apache.commons.imaging.common.ImageMetadata;
48  import org.apache.commons.imaging.common.XmpEmbeddable;
49  import org.apache.commons.imaging.common.XmpImagingParameters;
50  import org.apache.commons.imaging.formats.png.chunks.AbstractPngTextChunk;
51  import org.apache.commons.imaging.formats.png.chunks.PngChunk;
52  import org.apache.commons.imaging.formats.png.chunks.PngChunkGama;
53  import org.apache.commons.imaging.formats.png.chunks.PngChunkIccp;
54  import org.apache.commons.imaging.formats.png.chunks.PngChunkIdat;
55  import org.apache.commons.imaging.formats.png.chunks.PngChunkIhdr;
56  import org.apache.commons.imaging.formats.png.chunks.PngChunkItxt;
57  import org.apache.commons.imaging.formats.png.chunks.PngChunkPhys;
58  import org.apache.commons.imaging.formats.png.chunks.PngChunkPlte;
59  import org.apache.commons.imaging.formats.png.chunks.PngChunkScal;
60  import org.apache.commons.imaging.formats.png.chunks.PngChunkText;
61  import org.apache.commons.imaging.formats.png.chunks.PngChunkZtxt;
62  import org.apache.commons.imaging.formats.png.transparencyfilters.AbstractTransparencyFilter;
63  import org.apache.commons.imaging.formats.png.transparencyfilters.TransparencyFilterGrayscale;
64  import org.apache.commons.imaging.formats.png.transparencyfilters.TransparencyFilterIndexedColor;
65  import org.apache.commons.imaging.formats.png.transparencyfilters.TransparencyFilterTrueColor;
66  import org.apache.commons.imaging.formats.tiff.TiffImageMetadata;
67  import org.apache.commons.imaging.formats.tiff.TiffImageParser;
68  import org.apache.commons.imaging.formats.tiff.TiffImagingParameters;
69  import org.apache.commons.imaging.icc.IccProfileParser;
70  
71  /**
72   * Parses PNG images.
73   */
74  public class PngImageParser extends AbstractImageParser<PngImagingParameters> implements XmpEmbeddable<PngImagingParameters> {
75  
76      private static final Logger LOGGER = Logger.getLogger(PngImageParser.class.getName());
77  
78      private static final String DEFAULT_EXTENSION = ImageFormats.PNG.getDefaultExtension();
79      private static final String[] ACCEPTED_EXTENSIONS = ImageFormats.PNG.getExtensions();
80  
81      public static String getChunkTypeName(final int chunkType) {
82          final StringBuilder result = new StringBuilder();
83          result.append((char) (0xff & chunkType >> 24));
84          result.append((char) (0xff & chunkType >> 16));
85          result.append((char) (0xff & chunkType >> 8));
86          result.append((char) (0xff & chunkType >> 0));
87          return result.toString();
88      }
89  
90      /**
91       * Constructs a new instance with the big-endian byte order.
92       */
93      public PngImageParser() {
94          // empty
95      }
96  
97      @Override
98      public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) throws ImagingException, IOException {
99          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 }