1 /* 2 * Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package com.sun.imageio.plugins.png; 27 28 import java.awt.Rectangle; 29 import java.awt.image.IndexColorModel; 30 import java.awt.image.Raster; 31 import java.awt.image.WritableRaster; 32 import java.awt.image.RenderedImage; 33 import java.awt.image.SampleModel; 34 import java.io.ByteArrayOutputStream; 35 import java.io.IOException; 36 import java.util.Iterator; 37 import java.util.Locale; 38 import java.util.zip.Deflater; 39 import java.util.zip.DeflaterOutputStream; 40 import javax.imageio.IIOException; 41 import javax.imageio.IIOImage; 42 import javax.imageio.ImageTypeSpecifier; 43 import javax.imageio.ImageWriteParam; 44 import javax.imageio.ImageWriter; 45 import javax.imageio.metadata.IIOMetadata; 46 import javax.imageio.spi.ImageWriterSpi; 47 import javax.imageio.stream.ImageOutputStream; 48 import javax.imageio.stream.ImageOutputStreamImpl; 49 50 final class CRC { 51 52 private static final int[] crcTable = new int[256]; 53 private int crc = 0xffffffff; 54 55 static { 56 // Initialize CRC table 57 for (int n = 0; n < 256; n++) { 58 int c = n; 59 for (int k = 0; k < 8; k++) { 60 if ((c & 1) == 1) { 61 c = 0xedb88320 ^ (c >>> 1); 62 } else { 63 c >>>= 1; 64 } 65 66 crcTable[n] = c; 67 } 68 } 69 } 70 CRC()71 CRC() {} 72 reset()73 void reset() { 74 crc = 0xffffffff; 75 } 76 update(byte[] data, int off, int len)77 void update(byte[] data, int off, int len) { 78 int c = crc; 79 for (int n = 0; n < len; n++) { 80 c = crcTable[(c ^ data[off + n]) & 0xff] ^ (c >>> 8); 81 } 82 crc = c; 83 } 84 update(int data)85 void update(int data) { 86 crc = crcTable[(crc ^ data) & 0xff] ^ (crc >>> 8); 87 } 88 getValue()89 int getValue() { 90 return crc ^ 0xffffffff; 91 } 92 } 93 94 95 final class ChunkStream extends ImageOutputStreamImpl { 96 97 private final ImageOutputStream stream; 98 private final long startPos; 99 private final CRC crc = new CRC(); 100 ChunkStream(int type, ImageOutputStream stream)101 ChunkStream(int type, ImageOutputStream stream) throws IOException { 102 this.stream = stream; 103 this.startPos = stream.getStreamPosition(); 104 105 stream.writeInt(-1); // length, will backpatch 106 writeInt(type); 107 } 108 109 @Override read()110 public int read() throws IOException { 111 throw new RuntimeException("Method not available"); 112 } 113 114 @Override read(byte[] b, int off, int len)115 public int read(byte[] b, int off, int len) throws IOException { 116 throw new RuntimeException("Method not available"); 117 } 118 119 @Override write(byte[] b, int off, int len)120 public void write(byte[] b, int off, int len) throws IOException { 121 crc.update(b, off, len); 122 stream.write(b, off, len); 123 } 124 125 @Override write(int b)126 public void write(int b) throws IOException { 127 crc.update(b); 128 stream.write(b); 129 } 130 finish()131 void finish() throws IOException { 132 // Write CRC 133 stream.writeInt(crc.getValue()); 134 135 // Write length 136 long pos = stream.getStreamPosition(); 137 stream.seek(startPos); 138 stream.writeInt((int)(pos - startPos) - 12); 139 140 // Return to end of chunk and flush to minimize buffering 141 stream.seek(pos); 142 stream.flushBefore(pos); 143 } 144 145 @Override 146 @SuppressWarnings("deprecation") finalize()147 protected void finalize() throws Throwable { 148 // Empty finalizer (for improved performance; no need to call 149 // super.finalize() in this case) 150 } 151 } 152 153 // Compress output and write as a series of 'IDAT' chunks of 154 // fixed length. 155 final class IDATOutputStream extends ImageOutputStreamImpl { 156 157 private static final byte[] chunkType = { 158 (byte)'I', (byte)'D', (byte)'A', (byte)'T' 159 }; 160 161 private final ImageOutputStream stream; 162 private final int chunkLength; 163 private long startPos; 164 private final CRC crc = new CRC(); 165 166 private final Deflater def; 167 private final byte[] buf = new byte[512]; 168 // reused 1 byte[] array: 169 private final byte[] wbuf1 = new byte[1]; 170 171 private int bytesRemaining; 172 IDATOutputStream(ImageOutputStream stream, int chunkLength, int deflaterLevel)173 IDATOutputStream(ImageOutputStream stream, int chunkLength, 174 int deflaterLevel) throws IOException 175 { 176 this.stream = stream; 177 this.chunkLength = chunkLength; 178 this.def = new Deflater(deflaterLevel); 179 180 startChunk(); 181 } 182 startChunk()183 private void startChunk() throws IOException { 184 crc.reset(); 185 this.startPos = stream.getStreamPosition(); 186 stream.writeInt(-1); // length, will backpatch 187 188 crc.update(chunkType, 0, 4); 189 stream.write(chunkType, 0, 4); 190 191 this.bytesRemaining = chunkLength; 192 } 193 finishChunk()194 private void finishChunk() throws IOException { 195 // Write CRC 196 stream.writeInt(crc.getValue()); 197 198 // Write length 199 long pos = stream.getStreamPosition(); 200 stream.seek(startPos); 201 stream.writeInt((int)(pos - startPos) - 12); 202 203 // Return to end of chunk and flush to minimize buffering 204 stream.seek(pos); 205 try { 206 stream.flushBefore(pos); 207 } catch (IOException e) { 208 /* 209 * If flushBefore() fails we try to access startPos in finally 210 * block of write_IDAT(). We should update startPos to avoid 211 * IndexOutOfBoundException while seek() is happening. 212 */ 213 this.startPos = stream.getStreamPosition(); 214 throw e; 215 } 216 } 217 218 @Override read()219 public int read() throws IOException { 220 throw new RuntimeException("Method not available"); 221 } 222 223 @Override read(byte[] b, int off, int len)224 public int read(byte[] b, int off, int len) throws IOException { 225 throw new RuntimeException("Method not available"); 226 } 227 228 @Override write(byte[] b, int off, int len)229 public void write(byte[] b, int off, int len) throws IOException { 230 if (len == 0) { 231 return; 232 } 233 234 if (!def.finished()) { 235 def.setInput(b, off, len); 236 while (!def.needsInput()) { 237 deflate(); 238 } 239 } 240 } 241 deflate()242 void deflate() throws IOException { 243 int len = def.deflate(buf, 0, buf.length); 244 int off = 0; 245 246 while (len > 0) { 247 if (bytesRemaining == 0) { 248 finishChunk(); 249 startChunk(); 250 } 251 252 int nbytes = Math.min(len, bytesRemaining); 253 crc.update(buf, off, nbytes); 254 stream.write(buf, off, nbytes); 255 256 off += nbytes; 257 len -= nbytes; 258 bytesRemaining -= nbytes; 259 } 260 } 261 262 @Override write(int b)263 public void write(int b) throws IOException { 264 wbuf1[0] = (byte)b; 265 write(wbuf1, 0, 1); 266 } 267 finish()268 void finish() throws IOException { 269 try { 270 if (!def.finished()) { 271 def.finish(); 272 while (!def.finished()) { 273 deflate(); 274 } 275 } 276 finishChunk(); 277 } finally { 278 def.end(); 279 } 280 } 281 282 @Override 283 @SuppressWarnings("deprecation") finalize()284 protected void finalize() throws Throwable { 285 // Empty finalizer (for improved performance; no need to call 286 // super.finalize() in this case) 287 } 288 } 289 290 291 final class PNGImageWriteParam extends ImageWriteParam { 292 293 /** Default quality level = 0.5 ie medium compression */ 294 private static final float DEFAULT_QUALITY = 0.5f; 295 296 private static final String[] compressionNames = {"Deflate"}; 297 private static final float[] qualityVals = { 0.00F, 0.30F, 0.75F, 1.00F }; 298 private static final String[] qualityDescs = { 299 "High compression", // 0.00 -> 0.30 300 "Medium compression", // 0.30 -> 0.75 301 "Low compression" // 0.75 -> 1.00 302 }; 303 PNGImageWriteParam(Locale locale)304 PNGImageWriteParam(Locale locale) { 305 super(); 306 this.canWriteProgressive = true; 307 this.locale = locale; 308 this.canWriteCompressed = true; 309 this.compressionTypes = compressionNames; 310 this.compressionType = compressionTypes[0]; 311 this.compressionMode = MODE_DEFAULT; 312 this.compressionQuality = DEFAULT_QUALITY; 313 } 314 315 /** 316 * Removes any previous compression quality setting. 317 * 318 * <p> The default implementation resets the compression quality 319 * to <code>0.5F</code>. 320 * 321 * @exception IllegalStateException if the compression mode is not 322 * <code>MODE_EXPLICIT</code>. 323 */ 324 @Override unsetCompression()325 public void unsetCompression() { 326 super.unsetCompression(); 327 this.compressionType = compressionTypes[0]; 328 this.compressionQuality = DEFAULT_QUALITY; 329 } 330 331 /** 332 * Returns <code>true</code> since the PNG plug-in only supports 333 * lossless compression. 334 * 335 * @return <code>true</code>. 336 */ 337 @Override isCompressionLossless()338 public boolean isCompressionLossless() { 339 return true; 340 } 341 342 @Override getCompressionQualityDescriptions()343 public String[] getCompressionQualityDescriptions() { 344 super.getCompressionQualityDescriptions(); 345 return qualityDescs.clone(); 346 } 347 348 @Override getCompressionQualityValues()349 public float[] getCompressionQualityValues() { 350 super.getCompressionQualityValues(); 351 return qualityVals.clone(); 352 } 353 } 354 355 /** 356 */ 357 public final class PNGImageWriter extends ImageWriter { 358 359 /** Default compression level = 4 ie medium compression */ 360 private static final int DEFAULT_COMPRESSION_LEVEL = 4; 361 362 ImageOutputStream stream = null; 363 364 PNGMetadata metadata = null; 365 366 // Factors from the ImageWriteParam 367 int sourceXOffset = 0; 368 int sourceYOffset = 0; 369 int sourceWidth = 0; 370 int sourceHeight = 0; 371 int[] sourceBands = null; 372 int periodX = 1; 373 int periodY = 1; 374 375 int numBands; 376 int bpp; 377 378 RowFilter rowFilter = new RowFilter(); 379 byte[] prevRow = null; 380 byte[] currRow = null; 381 byte[][] filteredRows = null; 382 383 // Per-band scaling tables 384 // 385 // After the first call to initializeScaleTables, either scale and scale0 386 // will be valid, or scaleh and scalel will be valid, but not both. 387 // 388 // The tables will be designed for use with a set of input but depths 389 // given by sampleSize, and an output bit depth given by scalingBitDepth. 390 // 391 int[] sampleSize = null; // Sample size per band, in bits 392 int scalingBitDepth = -1; // Output bit depth of the scaling tables 393 394 // Tables for 1, 2, 4, or 8 bit output 395 byte[][] scale = null; // 8 bit table 396 byte[] scale0 = null; // equivalent to scale[0] 397 398 // Tables for 16 bit output 399 byte[][] scaleh = null; // High bytes of output 400 byte[][] scalel = null; // Low bytes of output 401 402 int totalPixels; // Total number of pixels to be written by write_IDAT 403 int pixelsDone; // Running count of pixels written by write_IDAT 404 PNGImageWriter(ImageWriterSpi originatingProvider)405 public PNGImageWriter(ImageWriterSpi originatingProvider) { 406 super(originatingProvider); 407 } 408 409 @Override setOutput(Object output)410 public void setOutput(Object output) { 411 super.setOutput(output); 412 if (output != null) { 413 if (!(output instanceof ImageOutputStream)) { 414 throw new IllegalArgumentException("output not an ImageOutputStream!"); 415 } 416 this.stream = (ImageOutputStream)output; 417 } else { 418 this.stream = null; 419 } 420 } 421 422 @Override getDefaultWriteParam()423 public ImageWriteParam getDefaultWriteParam() { 424 return new PNGImageWriteParam(getLocale()); 425 } 426 427 @Override getDefaultStreamMetadata(ImageWriteParam param)428 public IIOMetadata getDefaultStreamMetadata(ImageWriteParam param) { 429 return null; 430 } 431 432 @Override getDefaultImageMetadata(ImageTypeSpecifier imageType, ImageWriteParam param)433 public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType, 434 ImageWriteParam param) { 435 PNGMetadata m = new PNGMetadata(); 436 m.initialize(imageType, imageType.getSampleModel().getNumBands()); 437 return m; 438 } 439 440 @Override convertStreamMetadata(IIOMetadata inData, ImageWriteParam param)441 public IIOMetadata convertStreamMetadata(IIOMetadata inData, 442 ImageWriteParam param) { 443 return null; 444 } 445 446 @Override convertImageMetadata(IIOMetadata inData, ImageTypeSpecifier imageType, ImageWriteParam param)447 public IIOMetadata convertImageMetadata(IIOMetadata inData, 448 ImageTypeSpecifier imageType, 449 ImageWriteParam param) { 450 // TODO - deal with imageType 451 if (inData instanceof PNGMetadata) { 452 return (PNGMetadata)((PNGMetadata)inData).clone(); 453 } else { 454 return new PNGMetadata(inData); 455 } 456 } 457 write_magic()458 private void write_magic() throws IOException { 459 // Write signature 460 byte[] magic = { (byte)137, 80, 78, 71, 13, 10, 26, 10 }; 461 stream.write(magic); 462 } 463 write_IHDR()464 private void write_IHDR() throws IOException { 465 // Write IHDR chunk 466 ChunkStream cs = new ChunkStream(PNGImageReader.IHDR_TYPE, stream); 467 cs.writeInt(metadata.IHDR_width); 468 cs.writeInt(metadata.IHDR_height); 469 cs.writeByte(metadata.IHDR_bitDepth); 470 cs.writeByte(metadata.IHDR_colorType); 471 if (metadata.IHDR_compressionMethod != 0) { 472 throw new IIOException( 473 "Only compression method 0 is defined in PNG 1.1"); 474 } 475 cs.writeByte(metadata.IHDR_compressionMethod); 476 if (metadata.IHDR_filterMethod != 0) { 477 throw new IIOException( 478 "Only filter method 0 is defined in PNG 1.1"); 479 } 480 cs.writeByte(metadata.IHDR_filterMethod); 481 if (metadata.IHDR_interlaceMethod < 0 || 482 metadata.IHDR_interlaceMethod > 1) { 483 throw new IIOException( 484 "Only interlace methods 0 (node) and 1 (adam7) are defined in PNG 1.1"); 485 } 486 cs.writeByte(metadata.IHDR_interlaceMethod); 487 cs.finish(); 488 } 489 write_cHRM()490 private void write_cHRM() throws IOException { 491 if (metadata.cHRM_present) { 492 ChunkStream cs = new ChunkStream(PNGImageReader.cHRM_TYPE, stream); 493 cs.writeInt(metadata.cHRM_whitePointX); 494 cs.writeInt(metadata.cHRM_whitePointY); 495 cs.writeInt(metadata.cHRM_redX); 496 cs.writeInt(metadata.cHRM_redY); 497 cs.writeInt(metadata.cHRM_greenX); 498 cs.writeInt(metadata.cHRM_greenY); 499 cs.writeInt(metadata.cHRM_blueX); 500 cs.writeInt(metadata.cHRM_blueY); 501 cs.finish(); 502 } 503 } 504 write_gAMA()505 private void write_gAMA() throws IOException { 506 if (metadata.gAMA_present) { 507 ChunkStream cs = new ChunkStream(PNGImageReader.gAMA_TYPE, stream); 508 cs.writeInt(metadata.gAMA_gamma); 509 cs.finish(); 510 } 511 } 512 write_iCCP()513 private void write_iCCP() throws IOException { 514 if (metadata.iCCP_present) { 515 ChunkStream cs = new ChunkStream(PNGImageReader.iCCP_TYPE, stream); 516 if (metadata.iCCP_profileName.length() > 79) { 517 throw new IIOException("iCCP profile name is longer than 79"); 518 } 519 cs.writeBytes(metadata.iCCP_profileName); 520 cs.writeByte(0); // null terminator 521 522 cs.writeByte(metadata.iCCP_compressionMethod); 523 cs.write(metadata.iCCP_compressedProfile); 524 cs.finish(); 525 } 526 } 527 write_sBIT()528 private void write_sBIT() throws IOException { 529 if (metadata.sBIT_present) { 530 ChunkStream cs = new ChunkStream(PNGImageReader.sBIT_TYPE, stream); 531 int colorType = metadata.IHDR_colorType; 532 if (metadata.sBIT_colorType != colorType) { 533 processWarningOccurred(0, 534 "sBIT metadata has wrong color type.\n" + 535 "The chunk will not be written."); 536 return; 537 } 538 539 if (colorType == PNGImageReader.PNG_COLOR_GRAY || 540 colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA) { 541 cs.writeByte(metadata.sBIT_grayBits); 542 } else if (colorType == PNGImageReader.PNG_COLOR_RGB || 543 colorType == PNGImageReader.PNG_COLOR_PALETTE || 544 colorType == PNGImageReader.PNG_COLOR_RGB_ALPHA) { 545 cs.writeByte(metadata.sBIT_redBits); 546 cs.writeByte(metadata.sBIT_greenBits); 547 cs.writeByte(metadata.sBIT_blueBits); 548 } 549 550 if (colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA || 551 colorType == PNGImageReader.PNG_COLOR_RGB_ALPHA) { 552 cs.writeByte(metadata.sBIT_alphaBits); 553 } 554 cs.finish(); 555 } 556 } 557 write_sRGB()558 private void write_sRGB() throws IOException { 559 if (metadata.sRGB_present) { 560 ChunkStream cs = new ChunkStream(PNGImageReader.sRGB_TYPE, stream); 561 cs.writeByte(metadata.sRGB_renderingIntent); 562 cs.finish(); 563 } 564 } 565 write_PLTE()566 private void write_PLTE() throws IOException { 567 if (metadata.PLTE_present) { 568 if (metadata.IHDR_colorType == PNGImageReader.PNG_COLOR_GRAY || 569 metadata.IHDR_colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA) { 570 // PLTE cannot occur in a gray image 571 572 processWarningOccurred(0, 573 "A PLTE chunk may not appear in a gray or gray alpha image.\n" + 574 "The chunk will not be written"); 575 return; 576 } 577 578 ChunkStream cs = new ChunkStream(PNGImageReader.PLTE_TYPE, stream); 579 580 int numEntries = metadata.PLTE_red.length; 581 byte[] palette = new byte[numEntries*3]; 582 int index = 0; 583 for (int i = 0; i < numEntries; i++) { 584 palette[index++] = metadata.PLTE_red[i]; 585 palette[index++] = metadata.PLTE_green[i]; 586 palette[index++] = metadata.PLTE_blue[i]; 587 } 588 589 cs.write(palette); 590 cs.finish(); 591 } 592 } 593 write_hIST()594 private void write_hIST() throws IOException, IIOException { 595 if (metadata.hIST_present) { 596 ChunkStream cs = new ChunkStream(PNGImageReader.hIST_TYPE, stream); 597 598 if (!metadata.PLTE_present) { 599 throw new IIOException("hIST chunk without PLTE chunk!"); 600 } 601 602 cs.writeChars(metadata.hIST_histogram, 603 0, metadata.hIST_histogram.length); 604 cs.finish(); 605 } 606 } 607 write_tRNS()608 private void write_tRNS() throws IOException, IIOException { 609 if (metadata.tRNS_present) { 610 ChunkStream cs = new ChunkStream(PNGImageReader.tRNS_TYPE, stream); 611 int colorType = metadata.IHDR_colorType; 612 int chunkType = metadata.tRNS_colorType; 613 614 // Special case: image is RGB and chunk is Gray 615 // Promote chunk contents to RGB 616 int chunkRed = metadata.tRNS_red; 617 int chunkGreen = metadata.tRNS_green; 618 int chunkBlue = metadata.tRNS_blue; 619 if (colorType == PNGImageReader.PNG_COLOR_RGB && 620 chunkType == PNGImageReader.PNG_COLOR_GRAY) { 621 chunkType = colorType; 622 chunkRed = chunkGreen = chunkBlue = 623 metadata.tRNS_gray; 624 } 625 626 if (chunkType != colorType) { 627 processWarningOccurred(0, 628 "tRNS metadata has incompatible color type.\n" + 629 "The chunk will not be written."); 630 return; 631 } 632 633 if (colorType == PNGImageReader.PNG_COLOR_PALETTE) { 634 if (!metadata.PLTE_present) { 635 throw new IIOException("tRNS chunk without PLTE chunk!"); 636 } 637 cs.write(metadata.tRNS_alpha); 638 } else if (colorType == PNGImageReader.PNG_COLOR_GRAY) { 639 cs.writeShort(metadata.tRNS_gray); 640 } else if (colorType == PNGImageReader.PNG_COLOR_RGB) { 641 cs.writeShort(chunkRed); 642 cs.writeShort(chunkGreen); 643 cs.writeShort(chunkBlue); 644 } else { 645 throw new IIOException("tRNS chunk for color type 4 or 6!"); 646 } 647 cs.finish(); 648 } 649 } 650 write_bKGD()651 private void write_bKGD() throws IOException { 652 if (metadata.bKGD_present) { 653 ChunkStream cs = new ChunkStream(PNGImageReader.bKGD_TYPE, stream); 654 int colorType = metadata.IHDR_colorType & 0x3; 655 int chunkType = metadata.bKGD_colorType; 656 657 int chunkRed = metadata.bKGD_red; 658 int chunkGreen = metadata.bKGD_green; 659 int chunkBlue = metadata.bKGD_blue; 660 // Special case: image is RGB(A) and chunk is Gray 661 // Promote chunk contents to RGB 662 if (colorType == PNGImageReader.PNG_COLOR_RGB && 663 chunkType == PNGImageReader.PNG_COLOR_GRAY) { 664 // Make a gray bKGD chunk look like RGB 665 chunkType = colorType; 666 chunkRed = chunkGreen = chunkBlue = 667 metadata.bKGD_gray; 668 } 669 670 // Ignore status of alpha in colorType 671 if (chunkType != colorType) { 672 processWarningOccurred(0, 673 "bKGD metadata has incompatible color type.\n" + 674 "The chunk will not be written."); 675 return; 676 } 677 678 if (colorType == PNGImageReader.PNG_COLOR_PALETTE) { 679 cs.writeByte(metadata.bKGD_index); 680 } else if (colorType == PNGImageReader.PNG_COLOR_GRAY || 681 colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA) { 682 cs.writeShort(metadata.bKGD_gray); 683 } else { // colorType == PNGImageReader.PNG_COLOR_RGB || 684 // colorType == PNGImageReader.PNG_COLOR_RGB_ALPHA 685 cs.writeShort(chunkRed); 686 cs.writeShort(chunkGreen); 687 cs.writeShort(chunkBlue); 688 } 689 cs.finish(); 690 } 691 } 692 write_pHYs()693 private void write_pHYs() throws IOException { 694 if (metadata.pHYs_present) { 695 ChunkStream cs = new ChunkStream(PNGImageReader.pHYs_TYPE, stream); 696 cs.writeInt(metadata.pHYs_pixelsPerUnitXAxis); 697 cs.writeInt(metadata.pHYs_pixelsPerUnitYAxis); 698 cs.writeByte(metadata.pHYs_unitSpecifier); 699 cs.finish(); 700 } 701 } 702 write_sPLT()703 private void write_sPLT() throws IOException { 704 if (metadata.sPLT_present) { 705 ChunkStream cs = new ChunkStream(PNGImageReader.sPLT_TYPE, stream); 706 707 if (metadata.sPLT_paletteName.length() > 79) { 708 throw new IIOException("sPLT palette name is longer than 79"); 709 } 710 cs.writeBytes(metadata.sPLT_paletteName); 711 cs.writeByte(0); // null terminator 712 713 cs.writeByte(metadata.sPLT_sampleDepth); 714 int numEntries = metadata.sPLT_red.length; 715 716 if (metadata.sPLT_sampleDepth == 8) { 717 for (int i = 0; i < numEntries; i++) { 718 cs.writeByte(metadata.sPLT_red[i]); 719 cs.writeByte(metadata.sPLT_green[i]); 720 cs.writeByte(metadata.sPLT_blue[i]); 721 cs.writeByte(metadata.sPLT_alpha[i]); 722 cs.writeShort(metadata.sPLT_frequency[i]); 723 } 724 } else { // sampleDepth == 16 725 for (int i = 0; i < numEntries; i++) { 726 cs.writeShort(metadata.sPLT_red[i]); 727 cs.writeShort(metadata.sPLT_green[i]); 728 cs.writeShort(metadata.sPLT_blue[i]); 729 cs.writeShort(metadata.sPLT_alpha[i]); 730 cs.writeShort(metadata.sPLT_frequency[i]); 731 } 732 } 733 cs.finish(); 734 } 735 } 736 write_tIME()737 private void write_tIME() throws IOException { 738 if (metadata.tIME_present) { 739 ChunkStream cs = new ChunkStream(PNGImageReader.tIME_TYPE, stream); 740 cs.writeShort(metadata.tIME_year); 741 cs.writeByte(metadata.tIME_month); 742 cs.writeByte(metadata.tIME_day); 743 cs.writeByte(metadata.tIME_hour); 744 cs.writeByte(metadata.tIME_minute); 745 cs.writeByte(metadata.tIME_second); 746 cs.finish(); 747 } 748 } 749 write_tEXt()750 private void write_tEXt() throws IOException { 751 Iterator<String> keywordIter = metadata.tEXt_keyword.iterator(); 752 Iterator<String> textIter = metadata.tEXt_text.iterator(); 753 754 while (keywordIter.hasNext()) { 755 ChunkStream cs = new ChunkStream(PNGImageReader.tEXt_TYPE, stream); 756 String keyword = keywordIter.next(); 757 if (keyword.length() > 79) { 758 throw new IIOException("tEXt keyword is longer than 79"); 759 } 760 cs.writeBytes(keyword); 761 cs.writeByte(0); 762 763 String text = textIter.next(); 764 cs.writeBytes(text); 765 cs.finish(); 766 } 767 } 768 deflate(byte[] b)769 private byte[] deflate(byte[] b) throws IOException { 770 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 771 DeflaterOutputStream dos = new DeflaterOutputStream(baos); 772 dos.write(b); 773 dos.close(); 774 return baos.toByteArray(); 775 } 776 write_iTXt()777 private void write_iTXt() throws IOException { 778 Iterator<String> keywordIter = metadata.iTXt_keyword.iterator(); 779 Iterator<Boolean> flagIter = metadata.iTXt_compressionFlag.iterator(); 780 Iterator<Integer> methodIter = metadata.iTXt_compressionMethod.iterator(); 781 Iterator<String> languageIter = metadata.iTXt_languageTag.iterator(); 782 Iterator<String> translatedKeywordIter = 783 metadata.iTXt_translatedKeyword.iterator(); 784 Iterator<String> textIter = metadata.iTXt_text.iterator(); 785 786 while (keywordIter.hasNext()) { 787 ChunkStream cs = new ChunkStream(PNGImageReader.iTXt_TYPE, stream); 788 789 String keyword = keywordIter.next(); 790 if (keyword.length() > 79) { 791 throw new IIOException("iTXt keyword is longer than 79"); 792 } 793 cs.writeBytes(keyword); 794 cs.writeByte(0); 795 796 Boolean compressed = flagIter.next(); 797 cs.writeByte(compressed ? 1 : 0); 798 799 cs.writeByte(methodIter.next().intValue()); 800 801 cs.writeBytes(languageIter.next()); 802 cs.writeByte(0); 803 804 805 cs.write(translatedKeywordIter.next().getBytes("UTF8")); 806 cs.writeByte(0); 807 808 String text = textIter.next(); 809 if (compressed) { 810 cs.write(deflate(text.getBytes("UTF8"))); 811 } else { 812 cs.write(text.getBytes("UTF8")); 813 } 814 cs.finish(); 815 } 816 } 817 write_zTXt()818 private void write_zTXt() throws IOException { 819 Iterator<String> keywordIter = metadata.zTXt_keyword.iterator(); 820 Iterator<Integer> methodIter = metadata.zTXt_compressionMethod.iterator(); 821 Iterator<String> textIter = metadata.zTXt_text.iterator(); 822 823 while (keywordIter.hasNext()) { 824 ChunkStream cs = new ChunkStream(PNGImageReader.zTXt_TYPE, stream); 825 String keyword = keywordIter.next(); 826 if (keyword.length() > 79) { 827 throw new IIOException("zTXt keyword is longer than 79"); 828 } 829 cs.writeBytes(keyword); 830 cs.writeByte(0); 831 832 int compressionMethod = (methodIter.next()).intValue(); 833 cs.writeByte(compressionMethod); 834 835 String text = textIter.next(); 836 cs.write(deflate(text.getBytes("ISO-8859-1"))); 837 cs.finish(); 838 } 839 } 840 writeUnknownChunks()841 private void writeUnknownChunks() throws IOException { 842 Iterator<String> typeIter = metadata.unknownChunkType.iterator(); 843 Iterator<byte[]> dataIter = metadata.unknownChunkData.iterator(); 844 845 while (typeIter.hasNext() && dataIter.hasNext()) { 846 String type = typeIter.next(); 847 ChunkStream cs = new ChunkStream(chunkType(type), stream); 848 byte[] data = dataIter.next(); 849 cs.write(data); 850 cs.finish(); 851 } 852 } 853 chunkType(String typeString)854 private static int chunkType(String typeString) { 855 char c0 = typeString.charAt(0); 856 char c1 = typeString.charAt(1); 857 char c2 = typeString.charAt(2); 858 char c3 = typeString.charAt(3); 859 860 int type = (c0 << 24) | (c1 << 16) | (c2 << 8) | c3; 861 return type; 862 } 863 encodePass(ImageOutputStream os, RenderedImage image, int xOffset, int yOffset, int xSkip, int ySkip)864 private void encodePass(ImageOutputStream os, 865 RenderedImage image, 866 int xOffset, int yOffset, 867 int xSkip, int ySkip) throws IOException { 868 int minX = sourceXOffset; 869 int minY = sourceYOffset; 870 int width = sourceWidth; 871 int height = sourceHeight; 872 873 // Adjust offsets and skips based on source subsampling factors 874 xOffset *= periodX; 875 xSkip *= periodX; 876 yOffset *= periodY; 877 ySkip *= periodY; 878 879 // Early exit if no data for this pass 880 int hpixels = (width - xOffset + xSkip - 1)/xSkip; 881 int vpixels = (height - yOffset + ySkip - 1)/ySkip; 882 if (hpixels == 0 || vpixels == 0) { 883 return; 884 } 885 886 // Convert X offset and skip from pixels to samples 887 xOffset *= numBands; 888 xSkip *= numBands; 889 890 // Create row buffers 891 int samplesPerByte = 8/metadata.IHDR_bitDepth; 892 int numSamples = width*numBands; 893 int[] samples = new int[numSamples]; 894 895 int bytesPerRow = hpixels*numBands; 896 if (metadata.IHDR_bitDepth < 8) { 897 bytesPerRow = (bytesPerRow + samplesPerByte - 1)/samplesPerByte; 898 } else if (metadata.IHDR_bitDepth == 16) { 899 bytesPerRow *= 2; 900 } 901 902 IndexColorModel icm_gray_alpha = null; 903 if (metadata.IHDR_colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA && 904 image.getColorModel() instanceof IndexColorModel) 905 { 906 // reserve space for alpha samples 907 bytesPerRow *= 2; 908 909 // will be used to calculate alpha value for the pixel 910 icm_gray_alpha = (IndexColorModel)image.getColorModel(); 911 } 912 913 currRow = new byte[bytesPerRow + bpp]; 914 prevRow = new byte[bytesPerRow + bpp]; 915 filteredRows = new byte[5][bytesPerRow + bpp]; 916 917 int bitDepth = metadata.IHDR_bitDepth; 918 for (int row = minY + yOffset; row < minY + height; row += ySkip) { 919 Rectangle rect = new Rectangle(minX, row, width, 1); 920 Raster ras = image.getData(rect); 921 if (sourceBands != null) { 922 ras = ras.createChild(minX, row, width, 1, minX, row, 923 sourceBands); 924 } 925 926 ras.getPixels(minX, row, width, 1, samples); 927 928 if (image.getColorModel().isAlphaPremultiplied()) { 929 WritableRaster wr = ras.createCompatibleWritableRaster(); 930 wr.setPixels(wr.getMinX(), wr.getMinY(), 931 wr.getWidth(), wr.getHeight(), 932 samples); 933 934 image.getColorModel().coerceData(wr, false); 935 wr.getPixels(wr.getMinX(), wr.getMinY(), 936 wr.getWidth(), wr.getHeight(), 937 samples); 938 } 939 940 // Reorder palette data if necessary 941 int[] paletteOrder = metadata.PLTE_order; 942 if (paletteOrder != null) { 943 for (int i = 0; i < numSamples; i++) { 944 samples[i] = paletteOrder[samples[i]]; 945 } 946 } 947 948 int count = bpp; // leave first 'bpp' bytes zero 949 int pos = 0; 950 int tmp = 0; 951 952 switch (bitDepth) { 953 case 1: case 2: case 4: 954 // Image can only have a single band 955 956 int mask = samplesPerByte - 1; 957 for (int s = xOffset; s < numSamples; s += xSkip) { 958 byte val = scale0[samples[s]]; 959 tmp = (tmp << bitDepth) | val; 960 961 if ((pos++ & mask) == mask) { 962 currRow[count++] = (byte)tmp; 963 tmp = 0; 964 pos = 0; 965 } 966 } 967 968 // Left shift the last byte 969 if ((pos & mask) != 0) { 970 tmp <<= ((8/bitDepth) - pos)*bitDepth; 971 currRow[count++] = (byte)tmp; 972 } 973 break; 974 975 case 8: 976 if (numBands == 1) { 977 for (int s = xOffset; s < numSamples; s += xSkip) { 978 currRow[count++] = scale0[samples[s]]; 979 if (icm_gray_alpha != null) { 980 currRow[count++] = 981 scale0[icm_gray_alpha.getAlpha(0xff & samples[s])]; 982 } 983 } 984 } else { 985 for (int s = xOffset; s < numSamples; s += xSkip) { 986 for (int b = 0; b < numBands; b++) { 987 currRow[count++] = scale[b][samples[s + b]]; 988 } 989 } 990 } 991 break; 992 993 case 16: 994 for (int s = xOffset; s < numSamples; s += xSkip) { 995 for (int b = 0; b < numBands; b++) { 996 currRow[count++] = scaleh[b][samples[s + b]]; 997 currRow[count++] = scalel[b][samples[s + b]]; 998 } 999 } 1000 break; 1001 } 1002 1003 // Perform filtering 1004 int filterType = rowFilter.filterRow(metadata.IHDR_colorType, 1005 currRow, prevRow, 1006 filteredRows, 1007 bytesPerRow, bpp); 1008 1009 os.write(filterType); 1010 os.write(filteredRows[filterType], bpp, bytesPerRow); 1011 1012 // Swap current and previous rows 1013 byte[] swap = currRow; 1014 currRow = prevRow; 1015 prevRow = swap; 1016 1017 pixelsDone += hpixels; 1018 processImageProgress(100.0F*pixelsDone/totalPixels); 1019 1020 // If write has been aborted, just return; 1021 // processWriteAborted will be called later 1022 if (abortRequested()) { 1023 return; 1024 } 1025 } 1026 } 1027 1028 // Use sourceXOffset, etc. write_IDAT(RenderedImage image, int deflaterLevel)1029 private void write_IDAT(RenderedImage image, int deflaterLevel) 1030 throws IOException 1031 { 1032 IDATOutputStream ios = new IDATOutputStream(stream, 32768, 1033 deflaterLevel); 1034 try { 1035 if (metadata.IHDR_interlaceMethod == 1) { 1036 for (int i = 0; i < 7; i++) { 1037 encodePass(ios, image, 1038 PNGImageReader.adam7XOffset[i], 1039 PNGImageReader.adam7YOffset[i], 1040 PNGImageReader.adam7XSubsampling[i], 1041 PNGImageReader.adam7YSubsampling[i]); 1042 if (abortRequested()) { 1043 break; 1044 } 1045 } 1046 } else { 1047 encodePass(ios, image, 0, 0, 1, 1); 1048 } 1049 } finally { 1050 ios.finish(); 1051 } 1052 } 1053 writeIEND()1054 private void writeIEND() throws IOException { 1055 ChunkStream cs = new ChunkStream(PNGImageReader.IEND_TYPE, stream); 1056 cs.finish(); 1057 } 1058 1059 // Check two int arrays for value equality, always returns false 1060 // if either array is null equals(int[] s0, int[] s1)1061 private boolean equals(int[] s0, int[] s1) { 1062 if (s0 == null || s1 == null) { 1063 return false; 1064 } 1065 if (s0.length != s1.length) { 1066 return false; 1067 } 1068 for (int i = 0; i < s0.length; i++) { 1069 if (s0[i] != s1[i]) { 1070 return false; 1071 } 1072 } 1073 return true; 1074 } 1075 1076 // Initialize the scale/scale0 or scaleh/scalel arrays to 1077 // hold the results of scaling an input value to the desired 1078 // output bit depth initializeScaleTables(int[] sampleSize)1079 private void initializeScaleTables(int[] sampleSize) { 1080 int bitDepth = metadata.IHDR_bitDepth; 1081 1082 // If the existing tables are still valid, just return 1083 if (bitDepth == scalingBitDepth && 1084 equals(sampleSize, this.sampleSize)) { 1085 return; 1086 } 1087 1088 // Compute new tables 1089 this.sampleSize = sampleSize; 1090 this.scalingBitDepth = bitDepth; 1091 int maxOutSample = (1 << bitDepth) - 1; 1092 if (bitDepth <= 8) { 1093 scale = new byte[numBands][]; 1094 for (int b = 0; b < numBands; b++) { 1095 int maxInSample = (1 << sampleSize[b]) - 1; 1096 int halfMaxInSample = maxInSample/2; 1097 scale[b] = new byte[maxInSample + 1]; 1098 for (int s = 0; s <= maxInSample; s++) { 1099 scale[b][s] = 1100 (byte)((s*maxOutSample + halfMaxInSample)/maxInSample); 1101 } 1102 } 1103 scale0 = scale[0]; 1104 scaleh = scalel = null; 1105 } else { // bitDepth == 16 1106 // Divide scaling table into high and low bytes 1107 scaleh = new byte[numBands][]; 1108 scalel = new byte[numBands][]; 1109 1110 for (int b = 0; b < numBands; b++) { 1111 int maxInSample = (1 << sampleSize[b]) - 1; 1112 int halfMaxInSample = maxInSample/2; 1113 scaleh[b] = new byte[maxInSample + 1]; 1114 scalel[b] = new byte[maxInSample + 1]; 1115 for (int s = 0; s <= maxInSample; s++) { 1116 int val = (s*maxOutSample + halfMaxInSample)/maxInSample; 1117 scaleh[b][s] = (byte)(val >> 8); 1118 scalel[b][s] = (byte)(val & 0xff); 1119 } 1120 } 1121 scale = null; 1122 scale0 = null; 1123 } 1124 } 1125 1126 @Override write(IIOMetadata streamMetadata, IIOImage image, ImageWriteParam param)1127 public void write(IIOMetadata streamMetadata, 1128 IIOImage image, 1129 ImageWriteParam param) throws IIOException { 1130 if (stream == null) { 1131 throw new IllegalStateException("output == null!"); 1132 } 1133 if (image == null) { 1134 throw new IllegalArgumentException("image == null!"); 1135 } 1136 if (image.hasRaster()) { 1137 throw new UnsupportedOperationException("image has a Raster!"); 1138 } 1139 1140 RenderedImage im = image.getRenderedImage(); 1141 SampleModel sampleModel = im.getSampleModel(); 1142 this.numBands = sampleModel.getNumBands(); 1143 1144 // Set source region and subsampling to default values 1145 this.sourceXOffset = im.getMinX(); 1146 this.sourceYOffset = im.getMinY(); 1147 this.sourceWidth = im.getWidth(); 1148 this.sourceHeight = im.getHeight(); 1149 this.sourceBands = null; 1150 this.periodX = 1; 1151 this.periodY = 1; 1152 1153 if (param != null) { 1154 // Get source region and subsampling factors 1155 Rectangle sourceRegion = param.getSourceRegion(); 1156 if (sourceRegion != null) { 1157 Rectangle imageBounds = new Rectangle(im.getMinX(), 1158 im.getMinY(), 1159 im.getWidth(), 1160 im.getHeight()); 1161 // Clip to actual image bounds 1162 sourceRegion = sourceRegion.intersection(imageBounds); 1163 sourceXOffset = sourceRegion.x; 1164 sourceYOffset = sourceRegion.y; 1165 sourceWidth = sourceRegion.width; 1166 sourceHeight = sourceRegion.height; 1167 } 1168 1169 // Adjust for subsampling offsets 1170 int gridX = param.getSubsamplingXOffset(); 1171 int gridY = param.getSubsamplingYOffset(); 1172 sourceXOffset += gridX; 1173 sourceYOffset += gridY; 1174 sourceWidth -= gridX; 1175 sourceHeight -= gridY; 1176 1177 // Get subsampling factors 1178 periodX = param.getSourceXSubsampling(); 1179 periodY = param.getSourceYSubsampling(); 1180 1181 int[] sBands = param.getSourceBands(); 1182 if (sBands != null) { 1183 sourceBands = sBands; 1184 numBands = sourceBands.length; 1185 } 1186 } 1187 1188 // Compute output dimensions 1189 int destWidth = (sourceWidth + periodX - 1)/periodX; 1190 int destHeight = (sourceHeight + periodY - 1)/periodY; 1191 if (destWidth <= 0 || destHeight <= 0) { 1192 throw new IllegalArgumentException("Empty source region!"); 1193 } 1194 1195 // Compute total number of pixels for progress notification 1196 this.totalPixels = destWidth*destHeight; 1197 this.pixelsDone = 0; 1198 1199 // Create metadata 1200 IIOMetadata imd = image.getMetadata(); 1201 if (imd != null) { 1202 metadata = (PNGMetadata)convertImageMetadata(imd, 1203 ImageTypeSpecifier.createFromRenderedImage(im), 1204 null); 1205 } else { 1206 metadata = new PNGMetadata(); 1207 } 1208 1209 // reset compression level to default: 1210 int deflaterLevel = DEFAULT_COMPRESSION_LEVEL; 1211 1212 if (param != null) { 1213 switch(param.getCompressionMode()) { 1214 case ImageWriteParam.MODE_DISABLED: 1215 deflaterLevel = Deflater.NO_COMPRESSION; 1216 break; 1217 case ImageWriteParam.MODE_EXPLICIT: 1218 float quality = param.getCompressionQuality(); 1219 if (quality >= 0f && quality <= 1f) { 1220 deflaterLevel = 9 - Math.round(9f * quality); 1221 } 1222 break; 1223 default: 1224 } 1225 1226 // Use Adam7 interlacing if set in write param 1227 switch (param.getProgressiveMode()) { 1228 case ImageWriteParam.MODE_DEFAULT: 1229 metadata.IHDR_interlaceMethod = 1; 1230 break; 1231 case ImageWriteParam.MODE_DISABLED: 1232 metadata.IHDR_interlaceMethod = 0; 1233 break; 1234 // MODE_COPY_FROM_METADATA should already be taken care of 1235 // MODE_EXPLICIT is not allowed 1236 default: 1237 } 1238 } 1239 1240 // Initialize bitDepth and colorType 1241 metadata.initialize(new ImageTypeSpecifier(im), numBands); 1242 1243 // Overwrite IHDR width and height values with values from image 1244 metadata.IHDR_width = destWidth; 1245 metadata.IHDR_height = destHeight; 1246 1247 this.bpp = numBands*((metadata.IHDR_bitDepth == 16) ? 2 : 1); 1248 1249 // Initialize scaling tables for this image 1250 initializeScaleTables(sampleModel.getSampleSize()); 1251 1252 clearAbortRequest(); 1253 1254 processImageStarted(0); 1255 if (abortRequested()) { 1256 processWriteAborted(); 1257 } else { 1258 try { 1259 write_magic(); 1260 write_IHDR(); 1261 1262 write_cHRM(); 1263 write_gAMA(); 1264 write_iCCP(); 1265 write_sBIT(); 1266 write_sRGB(); 1267 1268 write_PLTE(); 1269 1270 write_hIST(); 1271 write_tRNS(); 1272 write_bKGD(); 1273 1274 write_pHYs(); 1275 write_sPLT(); 1276 write_tIME(); 1277 write_tEXt(); 1278 write_iTXt(); 1279 write_zTXt(); 1280 1281 writeUnknownChunks(); 1282 1283 write_IDAT(im, deflaterLevel); 1284 1285 if (abortRequested()) { 1286 processWriteAborted(); 1287 } else { 1288 // Finish up and inform the listeners we are done 1289 writeIEND(); 1290 processImageComplete(); 1291 } 1292 } catch (IOException e) { 1293 throw new IIOException("I/O error writing PNG file!", e); 1294 } 1295 } 1296 } 1297 } 1298