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