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