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.bytesource.ByteSource;
055import org.apache.commons.imaging.formats.png.chunks.PngChunk;
056import org.apache.commons.imaging.formats.png.chunks.PngChunkGama;
057import org.apache.commons.imaging.formats.png.chunks.PngChunkIccp;
058import org.apache.commons.imaging.formats.png.chunks.PngChunkIdat;
059import org.apache.commons.imaging.formats.png.chunks.PngChunkIhdr;
060import org.apache.commons.imaging.formats.png.chunks.PngChunkItxt;
061import org.apache.commons.imaging.formats.png.chunks.PngChunkPhys;
062import org.apache.commons.imaging.formats.png.chunks.PngChunkPlte;
063import org.apache.commons.imaging.formats.png.chunks.PngChunkScal;
064import org.apache.commons.imaging.formats.png.chunks.PngChunkText;
065import org.apache.commons.imaging.formats.png.chunks.PngChunkZtxt;
066import org.apache.commons.imaging.formats.png.chunks.PngTextChunk;
067import org.apache.commons.imaging.formats.png.transparencyfilters.TransparencyFilter;
068import org.apache.commons.imaging.formats.png.transparencyfilters.TransparencyFilterGrayscale;
069import org.apache.commons.imaging.formats.png.transparencyfilters.TransparencyFilterIndexedColor;
070import org.apache.commons.imaging.formats.png.transparencyfilters.TransparencyFilterTrueColor;
071import org.apache.commons.imaging.icc.IccProfileParser;
072
073public class PngImageParser extends ImageParser {
074
075    private static final Logger LOGGER = Logger.getLogger(PngImageParser.class.getName());
076
077    private static final String DEFAULT_EXTENSION = ".png";
078    private static final String[] ACCEPTED_EXTENSIONS = { DEFAULT_EXTENSION, };
079
080    @Override
081    public String getName() {
082        return "Png-Custom";
083    }
084
085    @Override
086    public String getDefaultExtension() {
087        return DEFAULT_EXTENSION;
088    }
089
090    @Override
091    protected String[] getAcceptedExtensions() {
092        return ACCEPTED_EXTENSIONS.clone();
093    }
094
095    @Override
096    protected ImageFormat[] getAcceptedTypes() {
097        return new ImageFormat[] { ImageFormats.PNG, //
098        };
099    }
100
101    // private final static int tRNS = CharsToQuad('t', 'R', 'N', 's');
102
103    public static String getChunkTypeName(final int chunkType) {
104        final StringBuilder result = new StringBuilder();
105        result.append((char) (0xff & (chunkType >> 24)));
106        result.append((char) (0xff & (chunkType >> 16)));
107        result.append((char) (0xff & (chunkType >> 8)));
108        result.append((char) (0xff & (chunkType >> 0)));
109        return result.toString();
110    }
111
112    /**
113     * @return List of String-formatted chunk types, ie. "tRNs".
114     */
115    public List<String> getChunkTypes(final InputStream is)
116            throws ImageReadException, IOException {
117        final List<PngChunk> chunks = readChunks(is, null, false);
118        final List<String> chunkTypes = new ArrayList<>(chunks.size());
119        for (final PngChunk chunk : chunks) {
120            chunkTypes.add(getChunkTypeName(chunk.chunkType));
121        }
122        return chunkTypes;
123    }
124
125    public boolean hasChunkType(final ByteSource byteSource, final ChunkType chunkType)
126            throws ImageReadException, IOException {
127        try (InputStream is = byteSource.getInputStream()) {
128            readSignature(is);
129            final List<PngChunk> chunks = readChunks(is, new ChunkType[] { chunkType }, true);
130            return !chunks.isEmpty();
131        }
132    }
133
134    private boolean keepChunk(final int chunkType, final ChunkType[] chunkTypes) {
135        // System.out.println("keepChunk: ");
136        if (chunkTypes == null) {
137            return true;
138        }
139
140        for (final ChunkType chunkType2 : chunkTypes) {
141            if (chunkType2.value == chunkType) {
142                return true;
143            }
144        }
145        return false;
146    }
147
148    private List<PngChunk> readChunks(final InputStream is, final ChunkType[] chunkTypes,
149            final boolean returnAfterFirst) throws ImageReadException, IOException {
150        final List<PngChunk> result = new ArrayList<>();
151
152        while (true) {
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 (LOGGER.isLoggable(Level.FINEST)) {
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 (LOGGER.isLoggable(Level.FINEST)) {
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, }, false);
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 int chunkCount = tEXts.size() + zTXts.size() + iTXts.size();
396        final List<String> comments = new ArrayList<>(chunkCount);
397        final List<PngText> textChunks = new ArrayList<>(chunkCount);
398
399        for (final PngChunk tEXt : tEXts) {
400            final PngChunkText pngChunktEXt = (PngChunkText) tEXt;
401            comments.add(pngChunktEXt.keyword + ": " + pngChunktEXt.text);
402            textChunks.add(pngChunktEXt.getContents());
403        }
404        for (final PngChunk zTXt : zTXts) {
405            final PngChunkZtxt pngChunkzTXt = (PngChunkZtxt) zTXt;
406            comments.add(pngChunkzTXt.keyword + ": " + pngChunkzTXt.text);
407            textChunks.add(pngChunkzTXt.getContents());
408        }
409        for (final PngChunk iTXt : iTXts) {
410            final PngChunkItxt pngChunkiTXt = (PngChunkItxt) iTXt;
411            comments.add(pngChunkiTXt.keyword + ": " + pngChunkiTXt.text);
412            textChunks.add(pngChunkiTXt.getContents());
413        }
414
415        final int bitsPerPixel = pngChunkIHDR.bitDepth * pngChunkIHDR.pngColorType.getSamplesPerPixel();
416        final ImageFormat format = ImageFormats.PNG;
417        final String formatName = "PNG Portable Network Graphics";
418        final int height = pngChunkIHDR.height;
419        final String mimeType = "image/png";
420        final int numberOfImages = 1;
421        final int width = pngChunkIHDR.width;
422        final boolean progressive = pngChunkIHDR.interlaceMethod.isProgressive();
423
424        int physicalHeightDpi = -1;
425        float physicalHeightInch = -1;
426        int physicalWidthDpi = -1;
427        float physicalWidthInch = -1;
428
429        // if (pngChunkpHYs != null)
430        // {
431        // System.out.println("\t" + "pngChunkpHYs.UnitSpecifier: " +
432        // pngChunkpHYs.UnitSpecifier );
433        // System.out.println("\t" + "pngChunkpHYs.PixelsPerUnitYAxis: " +
434        // pngChunkpHYs.PixelsPerUnitYAxis );
435        // System.out.println("\t" + "pngChunkpHYs.PixelsPerUnitXAxis: " +
436        // pngChunkpHYs.PixelsPerUnitXAxis );
437        // }
438        if ((pngChunkpHYs != null) && (pngChunkpHYs.unitSpecifier == 1)) { // meters
439            final double metersPerInch = 0.0254;
440
441            physicalWidthDpi = (int) Math.round(pngChunkpHYs.pixelsPerUnitXAxis * metersPerInch);
442            physicalWidthInch = (float) (width / (pngChunkpHYs.pixelsPerUnitXAxis * metersPerInch));
443            physicalHeightDpi = (int) Math.round(pngChunkpHYs.pixelsPerUnitYAxis * metersPerInch);
444            physicalHeightInch = (float) (height / (pngChunkpHYs.pixelsPerUnitYAxis * metersPerInch));
445        }
446
447        boolean usesPalette = false;
448
449        final List<PngChunk> PLTEs = filterChunks(chunks, ChunkType.PLTE);
450        if (PLTEs.size() > 1) {
451            usesPalette = true;
452        }
453
454        ImageInfo.ColorType colorType;
455        switch (pngChunkIHDR.pngColorType) {
456            case GREYSCALE:
457            case GREYSCALE_WITH_ALPHA:
458                colorType = ImageInfo.ColorType.GRAYSCALE;
459                break;
460            case TRUE_COLOR:
461            case INDEXED_COLOR:
462            case TRUE_COLOR_WITH_ALPHA:
463                colorType = ImageInfo.ColorType.RGB;
464                break;
465            default:
466                throw new ImageReadException("Png: Unknown ColorType: " + pngChunkIHDR.pngColorType);
467        }
468
469        final String formatDetails = "Png";
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<>() : new HashMap<>(params);
484
485        // if (params.size() > 0) {
486        // Object firstKey = params.keySet().iterator().next();
487        // throw new ImageWriteException("Unknown parameter: " + firstKey);
488        // }
489
490        final List<PngChunk> chunks = readChunks(byteSource, new ChunkType[] {
491                ChunkType.IHDR,
492                ChunkType.PLTE,
493                ChunkType.IDAT,
494                ChunkType.tRNS,
495                ChunkType.iCCP,
496                ChunkType.gAMA,
497                ChunkType.sRGB,
498            }, false);
499
500        if ((chunks == null) || (chunks.isEmpty())) {
501            throw new ImageReadException("PNG: no chunks");
502        }
503
504        final List<PngChunk> IHDRs = filterChunks(chunks, ChunkType.IHDR);
505        if (IHDRs.size() != 1) {
506            throw new ImageReadException("PNG contains more than one Header");
507        }
508
509        final PngChunkIhdr pngChunkIHDR = (PngChunkIhdr) IHDRs.get(0);
510
511        final List<PngChunk> PLTEs = filterChunks(chunks, ChunkType.PLTE);
512        if (PLTEs.size() > 1) {
513            throw new ImageReadException("PNG contains more than one Palette");
514        }
515
516        PngChunkPlte pngChunkPLTE = null;
517        if (PLTEs.size() == 1) {
518            pngChunkPLTE = (PngChunkPlte) PLTEs.get(0);
519        }
520
521        // -----
522
523        final List<PngChunk> IDATs = filterChunks(chunks, ChunkType.IDAT);
524        if (IDATs.isEmpty()) {
525            throw new ImageReadException("PNG missing image data");
526        }
527
528        ByteArrayOutputStream baos = new ByteArrayOutputStream();
529        for (final PngChunk IDAT : IDATs) {
530            final PngChunkIdat pngChunkIDAT = (PngChunkIdat) IDAT;
531            final byte[] bytes = pngChunkIDAT.getBytes();
532            // System.out.println(i + ": bytes: " + bytes.length);
533            baos.write(bytes);
534        }
535
536        final byte[] compressed = baos.toByteArray();
537
538        baos = null;
539
540        TransparencyFilter transparencyFilter = null;
541
542        final List<PngChunk> tRNSs = filterChunks(chunks, ChunkType.tRNS);
543        if (!tRNSs.isEmpty()) {
544            final PngChunk pngChunktRNS = tRNSs.get(0);
545            transparencyFilter = getTransparencyFilter(pngChunkIHDR.pngColorType, pngChunktRNS);
546        }
547
548        ICC_Profile iccProfile = null;
549        GammaCorrection gammaCorrection = null;
550        {
551            final List<PngChunk> sRGBs = filterChunks(chunks, ChunkType.sRGB);
552            final List<PngChunk> gAMAs = filterChunks(chunks, ChunkType.gAMA);
553            final List<PngChunk> iCCPs = filterChunks(chunks, ChunkType.iCCP);
554            if (sRGBs.size() > 1) {
555                throw new ImageReadException("PNG: unexpected sRGB chunk");
556            }
557            if (gAMAs.size() > 1) {
558                throw new ImageReadException("PNG: unexpected gAMA chunk");
559            }
560            if (iCCPs.size() > 1) {
561                throw new ImageReadException("PNG: unexpected iCCP chunk");
562            }
563
564            if (sRGBs.size() == 1) {
565                // no color management neccesary.
566                if (LOGGER.isLoggable(Level.FINEST)) {
567                    LOGGER.finest("sRGB, no color management neccesary.");
568                }
569            } else if (iCCPs.size() == 1) {
570                if (LOGGER.isLoggable(Level.FINEST)) {
571                    LOGGER.finest("iCCP.");
572                }
573
574                final PngChunkIccp pngChunkiCCP = (PngChunkIccp) iCCPs.get(0);
575                final byte[] bytes = pngChunkiCCP.getUncompressedProfile();
576
577                iccProfile = ICC_Profile.getInstance(bytes);
578            } else if (gAMAs.size() == 1) {
579                final PngChunkGama pngChunkgAMA = (PngChunkGama) gAMAs.get(0);
580                final double gamma = pngChunkgAMA.getGamma();
581
582                // charles: what is the correct target value here?
583                // double targetGamma = 2.2;
584                final double targetGamma = 1.0;
585                final double diff = Math.abs(targetGamma - gamma);
586                if (diff >= 0.5) {
587                    gammaCorrection = new GammaCorrection(gamma, targetGamma);
588                }
589
590                if (gammaCorrection != null) {
591                    if (pngChunkPLTE != null) {
592                        pngChunkPLTE.correct(gammaCorrection);
593                    }
594                }
595
596            }
597        }
598
599        {
600            final int width = pngChunkIHDR.width;
601            final int height = pngChunkIHDR.height;
602            final PngColorType pngColorType = pngChunkIHDR.pngColorType;
603            final int bitDepth = pngChunkIHDR.bitDepth;
604
605            if (pngChunkIHDR.filterMethod != 0) {
606                throw new ImageReadException("PNG: unknown FilterMethod: " + pngChunkIHDR.filterMethod);
607            }
608
609            final int bitsPerPixel = bitDepth * pngColorType.getSamplesPerPixel();
610
611            final boolean hasAlpha = pngColorType.hasAlpha() || transparencyFilter != null;
612
613            BufferedImage result;
614            if (pngColorType.isGreyscale()) {
615                result = getBufferedImageFactory(params).getGrayscaleBufferedImage(width, height, hasAlpha);
616            } else {
617                result = getBufferedImageFactory(params).getColorBufferedImage(width, height, hasAlpha);
618            }
619
620            final ByteArrayInputStream bais = new ByteArrayInputStream(compressed);
621            final InflaterInputStream iis = new InflaterInputStream(bais);
622
623            ScanExpediter scanExpediter;
624
625            switch (pngChunkIHDR.interlaceMethod) {
626                case NONE:
627                    scanExpediter = new ScanExpediterSimple(width, height, iis,
628                            result, pngColorType, bitDepth, bitsPerPixel,
629                            pngChunkPLTE, gammaCorrection, transparencyFilter);
630                    break;
631                case ADAM7:
632                    scanExpediter = new ScanExpediterInterlaced(width, height, iis,
633                            result, pngColorType, bitDepth, bitsPerPixel,
634                            pngChunkPLTE, gammaCorrection, transparencyFilter);
635                    break;
636                default:
637                    throw new ImageReadException("Unknown InterlaceMethod: " + pngChunkIHDR.interlaceMethod);
638            }
639
640            scanExpediter.drive();
641
642            if (iccProfile != null) {
643                final Boolean is_srgb = new IccProfileParser().issRGB(iccProfile);
644                if (is_srgb == null || !is_srgb.booleanValue()) {
645                    final ICC_ColorSpace cs = new ICC_ColorSpace(iccProfile);
646
647                    final ColorModel srgbCM = ColorModel.getRGBdefault();
648                    final ColorSpace cs_sRGB = srgbCM.getColorSpace();
649
650                    result = new ColorTools().convertBetweenColorSpaces(result, cs, cs_sRGB);
651                }
652            }
653
654            return result;
655
656        }
657
658    }
659
660    @Override
661    public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource)
662            throws ImageReadException, IOException {
663        final ImageInfo imageInfo = getImageInfo(byteSource);
664        if (imageInfo == null) {
665            return false;
666        }
667
668        imageInfo.toString(pw, "");
669
670        final List<PngChunk> chunks = readChunks(byteSource, null, false);
671        final List<PngChunk> IHDRs = filterChunks(chunks, ChunkType.IHDR);
672        if (IHDRs.size() != 1) {
673            if (LOGGER.isLoggable(Level.FINEST)) {
674                LOGGER.finest("PNG contains more than one Header");
675            }
676            return false;
677        }
678        final PngChunkIhdr pngChunkIHDR = (PngChunkIhdr) IHDRs.get(0);
679        pw.println("Color: " + pngChunkIHDR.pngColorType.name());
680
681        pw.println("chunks: " + chunks.size());
682
683        if ((chunks.isEmpty())) {
684            return false;
685        }
686
687        for (int i = 0; i < chunks.size(); i++) {
688            final PngChunk chunk = chunks.get(i);
689            printCharQuad(pw, "\t" + i + ": ", chunk.chunkType);
690        }
691
692        pw.println("");
693
694        pw.flush();
695
696        return true;
697    }
698
699    @Override
700    public void writeImage(final BufferedImage src, final OutputStream os, final Map<String, Object> params)
701            throws ImageWriteException, IOException {
702        new PngWriter().writeImage(src, os, params);
703    }
704
705    @Override
706    public String getXmpXml(final ByteSource byteSource, final Map<String, Object> params)
707            throws ImageReadException, IOException {
708
709        final List<PngChunk> chunks = readChunks(byteSource, new ChunkType[] { ChunkType.iTXt }, false);
710
711        if ((chunks == null) || (chunks.isEmpty())) {
712            return null;
713        }
714
715        final List<PngChunkItxt> xmpChunks = new ArrayList<>();
716        for (final PngChunk chunk : chunks) {
717            final PngChunkItxt itxtChunk = (PngChunkItxt) chunk;
718            if (!itxtChunk.getKeyword().equals(PngConstants.XMP_KEYWORD)) {
719                continue;
720            }
721            xmpChunks.add(itxtChunk);
722        }
723
724        if (xmpChunks.isEmpty()) {
725            return null;
726        }
727        if (xmpChunks.size() > 1) {
728            throw new ImageReadException(
729                    "PNG contains more than one XMP chunk.");
730        }
731
732        final PngChunkItxt chunk = xmpChunks.get(0);
733        return chunk.getText();
734    }
735
736}