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}