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