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