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