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