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}