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