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