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