1 /* 2 * Copyright (c) 2000, 2017, 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 71 CRC() {} 72 73 void reset() { 74 crc = 0xffffffff; 75 } 76 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 85 void update(int data) { 86 crc = crcTable[(crc ^ data) & 0xff] ^ (crc >>> 8); 87 } 88 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 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 110 public int read() throws IOException { 111 throw new RuntimeException("Method not available"); 112 } 113 114 @Override 115 public int read(byte[] b, int off, int len) throws IOException { 116 throw new RuntimeException("Method not available"); 117 } 118 119 @Override 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 126 public void write(int b) throws IOException { 127 crc.update(b); 128 stream.write(b); 129 } 130 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") 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 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 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 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 219 public int read() throws IOException { 220 throw new RuntimeException("Method not available"); 221 } 222 223 @Override 224 public int read(byte[] b, int off, int len) throws IOException { 225 throw new RuntimeException("Method not available"); 226 } 227 228 @Override 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 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 263 public void write(int b) throws IOException { 264 wbuf1[0] = (byte)b; 265 write(wbuf1, 0, 1); 266 } 267 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") 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 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 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 338 public boolean isCompressionLossless() { 339 return true; 340 } 341 342 @Override 343 public String[] getCompressionQualityDescriptions() { 344 super.getCompressionQualityDescriptions(); 345 return qualityDescs.clone(); 346 } 347 348 @Override 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 405 public PNGImageWriter(ImageWriterSpi originatingProvider) { 406 super(originatingProvider); 407 } 408 409 @Override 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 423 public ImageWriteParam getDefaultWriteParam() { 424 return new PNGImageWriteParam(getLocale()); 425 } 426 427 @Override 428 public IIOMetadata getDefaultStreamMetadata(ImageWriteParam param) { 429 return null; 430 } 431 432 @Override 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 441 public IIOMetadata convertStreamMetadata(IIOMetadata inData, 442 ImageWriteParam param) { 443 return null; 444 } 445 446 @Override 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 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 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 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 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 513 private void write_iCCP() throws IOException { 514 if (metadata.iCCP_present) { 515 ChunkStream cs = new ChunkStream(PNGImageReader.iCCP_TYPE, stream); 516 cs.writeBytes(metadata.iCCP_profileName); 517 cs.writeByte(0); // null terminator 518 519 cs.writeByte(metadata.iCCP_compressionMethod); 520 cs.write(metadata.iCCP_compressedProfile); 521 cs.finish(); 522 } 523 } 524 525 private void write_sBIT() throws IOException { 526 if (metadata.sBIT_present) { 527 ChunkStream cs = new ChunkStream(PNGImageReader.sBIT_TYPE, stream); 528 int colorType = metadata.IHDR_colorType; 529 if (metadata.sBIT_colorType != colorType) { 530 processWarningOccurred(0, 531 "sBIT metadata has wrong color type.\n" + 532 "The chunk will not be written."); 533 return; 534 } 535 536 if (colorType == PNGImageReader.PNG_COLOR_GRAY || 537 colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA) { 538 cs.writeByte(metadata.sBIT_grayBits); 539 } else if (colorType == PNGImageReader.PNG_COLOR_RGB || 540 colorType == PNGImageReader.PNG_COLOR_PALETTE || 541 colorType == PNGImageReader.PNG_COLOR_RGB_ALPHA) { 542 cs.writeByte(metadata.sBIT_redBits); 543 cs.writeByte(metadata.sBIT_greenBits); 544 cs.writeByte(metadata.sBIT_blueBits); 545 } 546 547 if (colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA || 548 colorType == PNGImageReader.PNG_COLOR_RGB_ALPHA) { 549 cs.writeByte(metadata.sBIT_alphaBits); 550 } 551 cs.finish(); 552 } 553 } 554 555 private void write_sRGB() throws IOException { 556 if (metadata.sRGB_present) { 557 ChunkStream cs = new ChunkStream(PNGImageReader.sRGB_TYPE, stream); 558 cs.writeByte(metadata.sRGB_renderingIntent); 559 cs.finish(); 560 } 561 } 562 563 private void write_PLTE() throws IOException { 564 if (metadata.PLTE_present) { 565 if (metadata.IHDR_colorType == PNGImageReader.PNG_COLOR_GRAY || 566 metadata.IHDR_colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA) { 567 // PLTE cannot occur in a gray image 568 569 processWarningOccurred(0, 570 "A PLTE chunk may not appear in a gray or gray alpha image.\n" + 571 "The chunk will not be written"); 572 return; 573 } 574 575 ChunkStream cs = new ChunkStream(PNGImageReader.PLTE_TYPE, stream); 576 577 int numEntries = metadata.PLTE_red.length; 578 byte[] palette = new byte[numEntries*3]; 579 int index = 0; 580 for (int i = 0; i < numEntries; i++) { 581 palette[index++] = metadata.PLTE_red[i]; 582 palette[index++] = metadata.PLTE_green[i]; 583 palette[index++] = metadata.PLTE_blue[i]; 584 } 585 586 cs.write(palette); 587 cs.finish(); 588 } 589 } 590 591 private void write_hIST() throws IOException, IIOException { 592 if (metadata.hIST_present) { 593 ChunkStream cs = new ChunkStream(PNGImageReader.hIST_TYPE, stream); 594 595 if (!metadata.PLTE_present) { 596 throw new IIOException("hIST chunk without PLTE chunk!"); 597 } 598 599 cs.writeChars(metadata.hIST_histogram, 600 0, metadata.hIST_histogram.length); 601 cs.finish(); 602 } 603 } 604 605 private void write_tRNS() throws IOException, IIOException { 606 if (metadata.tRNS_present) { 607 ChunkStream cs = new ChunkStream(PNGImageReader.tRNS_TYPE, stream); 608 int colorType = metadata.IHDR_colorType; 609 int chunkType = metadata.tRNS_colorType; 610 611 // Special case: image is RGB and chunk is Gray 612 // Promote chunk contents to RGB 613 int chunkRed = metadata.tRNS_red; 614 int chunkGreen = metadata.tRNS_green; 615 int chunkBlue = metadata.tRNS_blue; 616 if (colorType == PNGImageReader.PNG_COLOR_RGB && 617 chunkType == PNGImageReader.PNG_COLOR_GRAY) { 618 chunkType = colorType; 619 chunkRed = chunkGreen = chunkBlue = 620 metadata.tRNS_gray; 621 } 622 623 if (chunkType != colorType) { 624 processWarningOccurred(0, 625 "tRNS metadata has incompatible color type.\n" + 626 "The chunk will not be written."); 627 return; 628 } 629 630 if (colorType == PNGImageReader.PNG_COLOR_PALETTE) { 631 if (!metadata.PLTE_present) { 632 throw new IIOException("tRNS chunk without PLTE chunk!"); 633 } 634 cs.write(metadata.tRNS_alpha); 635 } else if (colorType == PNGImageReader.PNG_COLOR_GRAY) { 636 cs.writeShort(metadata.tRNS_gray); 637 } else if (colorType == PNGImageReader.PNG_COLOR_RGB) { 638 cs.writeShort(chunkRed); 639 cs.writeShort(chunkGreen); 640 cs.writeShort(chunkBlue); 641 } else { 642 throw new IIOException("tRNS chunk for color type 4 or 6!"); 643 } 644 cs.finish(); 645 } 646 } 647 648 private void write_bKGD() throws IOException { 649 if (metadata.bKGD_present) { 650 ChunkStream cs = new ChunkStream(PNGImageReader.bKGD_TYPE, stream); 651 int colorType = metadata.IHDR_colorType & 0x3; 652 int chunkType = metadata.bKGD_colorType; 653 654 // Special case: image is RGB(A) and chunk is Gray 655 // Promote chunk contents to RGB 656 int chunkRed = metadata.bKGD_red; 657 int chunkGreen = metadata.bKGD_red; 658 int chunkBlue = metadata.bKGD_red; 659 if (colorType == PNGImageReader.PNG_COLOR_RGB && 660 chunkType == PNGImageReader.PNG_COLOR_GRAY) { 661 // Make a gray bKGD chunk look like RGB 662 chunkType = colorType; 663 chunkRed = chunkGreen = chunkBlue = 664 metadata.bKGD_gray; 665 } 666 667 // Ignore status of alpha in colorType 668 if (chunkType != colorType) { 669 processWarningOccurred(0, 670 "bKGD metadata has incompatible color type.\n" + 671 "The chunk will not be written."); 672 return; 673 } 674 675 if (colorType == PNGImageReader.PNG_COLOR_PALETTE) { 676 cs.writeByte(metadata.bKGD_index); 677 } else if (colorType == PNGImageReader.PNG_COLOR_GRAY || 678 colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA) { 679 cs.writeShort(metadata.bKGD_gray); 680 } else { // colorType == PNGImageReader.PNG_COLOR_RGB || 681 // colorType == PNGImageReader.PNG_COLOR_RGB_ALPHA 682 cs.writeShort(chunkRed); 683 cs.writeShort(chunkGreen); 684 cs.writeShort(chunkBlue); 685 } 686 cs.finish(); 687 } 688 } 689 690 private void write_pHYs() throws IOException { 691 if (metadata.pHYs_present) { 692 ChunkStream cs = new ChunkStream(PNGImageReader.pHYs_TYPE, stream); 693 cs.writeInt(metadata.pHYs_pixelsPerUnitXAxis); 694 cs.writeInt(metadata.pHYs_pixelsPerUnitYAxis); 695 cs.writeByte(metadata.pHYs_unitSpecifier); 696 cs.finish(); 697 } 698 } 699 700 private void write_sPLT() throws IOException { 701 if (metadata.sPLT_present) { 702 ChunkStream cs = new ChunkStream(PNGImageReader.sPLT_TYPE, stream); 703 704 cs.writeBytes(metadata.sPLT_paletteName); 705 cs.writeByte(0); // null terminator 706 707 cs.writeByte(metadata.sPLT_sampleDepth); 708 int numEntries = metadata.sPLT_red.length; 709 710 if (metadata.sPLT_sampleDepth == 8) { 711 for (int i = 0; i < numEntries; i++) { 712 cs.writeByte(metadata.sPLT_red[i]); 713 cs.writeByte(metadata.sPLT_green[i]); 714 cs.writeByte(metadata.sPLT_blue[i]); 715 cs.writeByte(metadata.sPLT_alpha[i]); 716 cs.writeShort(metadata.sPLT_frequency[i]); 717 } 718 } else { // sampleDepth == 16 719 for (int i = 0; i < numEntries; i++) { 720 cs.writeShort(metadata.sPLT_red[i]); 721 cs.writeShort(metadata.sPLT_green[i]); 722 cs.writeShort(metadata.sPLT_blue[i]); 723 cs.writeShort(metadata.sPLT_alpha[i]); 724 cs.writeShort(metadata.sPLT_frequency[i]); 725 } 726 } 727 cs.finish(); 728 } 729 } 730 731 private void write_tIME() throws IOException { 732 if (metadata.tIME_present) { 733 ChunkStream cs = new ChunkStream(PNGImageReader.tIME_TYPE, stream); 734 cs.writeShort(metadata.tIME_year); 735 cs.writeByte(metadata.tIME_month); 736 cs.writeByte(metadata.tIME_day); 737 cs.writeByte(metadata.tIME_hour); 738 cs.writeByte(metadata.tIME_minute); 739 cs.writeByte(metadata.tIME_second); 740 cs.finish(); 741 } 742 } 743 744 private void write_tEXt() throws IOException { 745 Iterator<String> keywordIter = metadata.tEXt_keyword.iterator(); 746 Iterator<String> textIter = metadata.tEXt_text.iterator(); 747 748 while (keywordIter.hasNext()) { 749 ChunkStream cs = new ChunkStream(PNGImageReader.tEXt_TYPE, stream); 750 String keyword = keywordIter.next(); 751 cs.writeBytes(keyword); 752 cs.writeByte(0); 753 754 String text = textIter.next(); 755 cs.writeBytes(text); 756 cs.finish(); 757 } 758 } 759 760 private byte[] deflate(byte[] b) throws IOException { 761 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 762 DeflaterOutputStream dos = new DeflaterOutputStream(baos); 763 dos.write(b); 764 dos.close(); 765 return baos.toByteArray(); 766 } 767 768 private void write_iTXt() throws IOException { 769 Iterator<String> keywordIter = metadata.iTXt_keyword.iterator(); 770 Iterator<Boolean> flagIter = metadata.iTXt_compressionFlag.iterator(); 771 Iterator<Integer> methodIter = metadata.iTXt_compressionMethod.iterator(); 772 Iterator<String> languageIter = metadata.iTXt_languageTag.iterator(); 773 Iterator<String> translatedKeywordIter = 774 metadata.iTXt_translatedKeyword.iterator(); 775 Iterator<String> textIter = metadata.iTXt_text.iterator(); 776 777 while (keywordIter.hasNext()) { 778 ChunkStream cs = new ChunkStream(PNGImageReader.iTXt_TYPE, stream); 779 780 cs.writeBytes(keywordIter.next()); 781 cs.writeByte(0); 782 783 Boolean compressed = flagIter.next(); 784 cs.writeByte(compressed ? 1 : 0); 785 786 cs.writeByte(methodIter.next().intValue()); 787 788 cs.writeBytes(languageIter.next()); 789 cs.writeByte(0); 790 791 792 cs.write(translatedKeywordIter.next().getBytes("UTF8")); 793 cs.writeByte(0); 794 795 String text = textIter.next(); 796 if (compressed) { 797 cs.write(deflate(text.getBytes("UTF8"))); 798 } else { 799 cs.write(text.getBytes("UTF8")); 800 } 801 cs.finish(); 802 } 803 } 804 805 private void write_zTXt() throws IOException { 806 Iterator<String> keywordIter = metadata.zTXt_keyword.iterator(); 807 Iterator<Integer> methodIter = metadata.zTXt_compressionMethod.iterator(); 808 Iterator<String> textIter = metadata.zTXt_text.iterator(); 809 810 while (keywordIter.hasNext()) { 811 ChunkStream cs = new ChunkStream(PNGImageReader.zTXt_TYPE, stream); 812 String keyword = keywordIter.next(); 813 cs.writeBytes(keyword); 814 cs.writeByte(0); 815 816 int compressionMethod = (methodIter.next()).intValue(); 817 cs.writeByte(compressionMethod); 818 819 String text = textIter.next(); 820 cs.write(deflate(text.getBytes("ISO-8859-1"))); 821 cs.finish(); 822 } 823 } 824 825 private void writeUnknownChunks() throws IOException { 826 Iterator<String> typeIter = metadata.unknownChunkType.iterator(); 827 Iterator<byte[]> dataIter = metadata.unknownChunkData.iterator(); 828 829 while (typeIter.hasNext() && dataIter.hasNext()) { 830 String type = typeIter.next(); 831 ChunkStream cs = new ChunkStream(chunkType(type), stream); 832 byte[] data = dataIter.next(); 833 cs.write(data); 834 cs.finish(); 835 } 836 } 837 838 private static int chunkType(String typeString) { 839 char c0 = typeString.charAt(0); 840 char c1 = typeString.charAt(1); 841 char c2 = typeString.charAt(2); 842 char c3 = typeString.charAt(3); 843 844 int type = (c0 << 24) | (c1 << 16) | (c2 << 8) | c3; 845 return type; 846 } 847 848 private void encodePass(ImageOutputStream os, 849 RenderedImage image, 850 int xOffset, int yOffset, 851 int xSkip, int ySkip) throws IOException { 852 int minX = sourceXOffset; 853 int minY = sourceYOffset; 854 int width = sourceWidth; 855 int height = sourceHeight; 856 857 // Adjust offsets and skips based on source subsampling factors 858 xOffset *= periodX; 859 xSkip *= periodX; 860 yOffset *= periodY; 861 ySkip *= periodY; 862 863 // Early exit if no data for this pass 864 int hpixels = (width - xOffset + xSkip - 1)/xSkip; 865 int vpixels = (height - yOffset + ySkip - 1)/ySkip; 866 if (hpixels == 0 || vpixels == 0) { 867 return; 868 } 869 870 // Convert X offset and skip from pixels to samples 871 xOffset *= numBands; 872 xSkip *= numBands; 873 874 // Create row buffers 875 int samplesPerByte = 8/metadata.IHDR_bitDepth; 876 int numSamples = width*numBands; 877 int[] samples = new int[numSamples]; 878 879 int bytesPerRow = hpixels*numBands; 880 if (metadata.IHDR_bitDepth < 8) { 881 bytesPerRow = (bytesPerRow + samplesPerByte - 1)/samplesPerByte; 882 } else if (metadata.IHDR_bitDepth == 16) { 883 bytesPerRow *= 2; 884 } 885 886 IndexColorModel icm_gray_alpha = null; 887 if (metadata.IHDR_colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA && 888 image.getColorModel() instanceof IndexColorModel) 889 { 890 // reserve space for alpha samples 891 bytesPerRow *= 2; 892 893 // will be used to calculate alpha value for the pixel 894 icm_gray_alpha = (IndexColorModel)image.getColorModel(); 895 } 896 897 currRow = new byte[bytesPerRow + bpp]; 898 prevRow = new byte[bytesPerRow + bpp]; 899 filteredRows = new byte[5][bytesPerRow + bpp]; 900 901 int bitDepth = metadata.IHDR_bitDepth; 902 for (int row = minY + yOffset; row < minY + height; row += ySkip) { 903 Rectangle rect = new Rectangle(minX, row, width, 1); 904 Raster ras = image.getData(rect); 905 if (sourceBands != null) { 906 ras = ras.createChild(minX, row, width, 1, minX, row, 907 sourceBands); 908 } 909 910 ras.getPixels(minX, row, width, 1, samples); 911 912 if (image.getColorModel().isAlphaPremultiplied()) { 913 WritableRaster wr = ras.createCompatibleWritableRaster(); 914 wr.setPixels(wr.getMinX(), wr.getMinY(), 915 wr.getWidth(), wr.getHeight(), 916 samples); 917 918 image.getColorModel().coerceData(wr, false); 919 wr.getPixels(wr.getMinX(), wr.getMinY(), 920 wr.getWidth(), wr.getHeight(), 921 samples); 922 } 923 924 // Reorder palette data if necessary 925 int[] paletteOrder = metadata.PLTE_order; 926 if (paletteOrder != null) { 927 for (int i = 0; i < numSamples; i++) { 928 samples[i] = paletteOrder[samples[i]]; 929 } 930 } 931 932 int count = bpp; // leave first 'bpp' bytes zero 933 int pos = 0; 934 int tmp = 0; 935 936 switch (bitDepth) { 937 case 1: case 2: case 4: 938 // Image can only have a single band 939 940 int mask = samplesPerByte - 1; 941 for (int s = xOffset; s < numSamples; s += xSkip) { 942 byte val = scale0[samples[s]]; 943 tmp = (tmp << bitDepth) | val; 944 945 if ((pos++ & mask) == mask) { 946 currRow[count++] = (byte)tmp; 947 tmp = 0; 948 pos = 0; 949 } 950 } 951 952 // Left shift the last byte 953 if ((pos & mask) != 0) { 954 tmp <<= ((8/bitDepth) - pos)*bitDepth; 955 currRow[count++] = (byte)tmp; 956 } 957 break; 958 959 case 8: 960 if (numBands == 1) { 961 for (int s = xOffset; s < numSamples; s += xSkip) { 962 currRow[count++] = scale0[samples[s]]; 963 if (icm_gray_alpha != null) { 964 currRow[count++] = 965 scale0[icm_gray_alpha.getAlpha(0xff & samples[s])]; 966 } 967 } 968 } else { 969 for (int s = xOffset; s < numSamples; s += xSkip) { 970 for (int b = 0; b < numBands; b++) { 971 currRow[count++] = scale[b][samples[s + b]]; 972 } 973 } 974 } 975 break; 976 977 case 16: 978 for (int s = xOffset; s < numSamples; s += xSkip) { 979 for (int b = 0; b < numBands; b++) { 980 currRow[count++] = scaleh[b][samples[s + b]]; 981 currRow[count++] = scalel[b][samples[s + b]]; 982 } 983 } 984 break; 985 } 986 987 // Perform filtering 988 int filterType = rowFilter.filterRow(metadata.IHDR_colorType, 989 currRow, prevRow, 990 filteredRows, 991 bytesPerRow, bpp); 992 993 os.write(filterType); 994 os.write(filteredRows[filterType], bpp, bytesPerRow); 995 996 // Swap current and previous rows 997 byte[] swap = currRow; 998 currRow = prevRow; 999 prevRow = swap; 1000 1001 pixelsDone += hpixels; 1002 processImageProgress(100.0F*pixelsDone/totalPixels); 1003 1004 // If write has been aborted, just return; 1005 // processWriteAborted will be called later 1006 if (abortRequested()) { 1007 return; 1008 } 1009 } 1010 } 1011 1012 // Use sourceXOffset, etc. 1013 private void write_IDAT(RenderedImage image, int deflaterLevel) 1014 throws IOException 1015 { 1016 IDATOutputStream ios = new IDATOutputStream(stream, 32768, 1017 deflaterLevel); 1018 try { 1019 if (metadata.IHDR_interlaceMethod == 1) { 1020 for (int i = 0; i < 7; i++) { 1021 encodePass(ios, image, 1022 PNGImageReader.adam7XOffset[i], 1023 PNGImageReader.adam7YOffset[i], 1024 PNGImageReader.adam7XSubsampling[i], 1025 PNGImageReader.adam7YSubsampling[i]); 1026 if (abortRequested()) { 1027 break; 1028 } 1029 } 1030 } else { 1031 encodePass(ios, image, 0, 0, 1, 1); 1032 } 1033 } finally { 1034 ios.finish(); 1035 } 1036 } 1037 1038 private void writeIEND() throws IOException { 1039 ChunkStream cs = new ChunkStream(PNGImageReader.IEND_TYPE, stream); 1040 cs.finish(); 1041 } 1042 1043 // Check two int arrays for value equality, always returns false 1044 // if either array is null 1045 private boolean equals(int[] s0, int[] s1) { 1046 if (s0 == null || s1 == null) { 1047 return false; 1048 } 1049 if (s0.length != s1.length) { 1050 return false; 1051 } 1052 for (int i = 0; i < s0.length; i++) { 1053 if (s0[i] != s1[i]) { 1054 return false; 1055 } 1056 } 1057 return true; 1058 } 1059 1060 // Initialize the scale/scale0 or scaleh/scalel arrays to 1061 // hold the results of scaling an input value to the desired 1062 // output bit depth 1063 private void initializeScaleTables(int[] sampleSize) { 1064 int bitDepth = metadata.IHDR_bitDepth; 1065 1066 // If the existing tables are still valid, just return 1067 if (bitDepth == scalingBitDepth && 1068 equals(sampleSize, this.sampleSize)) { 1069 return; 1070 } 1071 1072 // Compute new tables 1073 this.sampleSize = sampleSize; 1074 this.scalingBitDepth = bitDepth; 1075 int maxOutSample = (1 << bitDepth) - 1; 1076 if (bitDepth <= 8) { 1077 scale = new byte[numBands][]; 1078 for (int b = 0; b < numBands; b++) { 1079 int maxInSample = (1 << sampleSize[b]) - 1; 1080 int halfMaxInSample = maxInSample/2; 1081 scale[b] = new byte[maxInSample + 1]; 1082 for (int s = 0; s <= maxInSample; s++) { 1083 scale[b][s] = 1084 (byte)((s*maxOutSample + halfMaxInSample)/maxInSample); 1085 } 1086 } 1087 scale0 = scale[0]; 1088 scaleh = scalel = null; 1089 } else { // bitDepth == 16 1090 // Divide scaling table into high and low bytes 1091 scaleh = new byte[numBands][]; 1092 scalel = new byte[numBands][]; 1093 1094 for (int b = 0; b < numBands; b++) { 1095 int maxInSample = (1 << sampleSize[b]) - 1; 1096 int halfMaxInSample = maxInSample/2; 1097 scaleh[b] = new byte[maxInSample + 1]; 1098 scalel[b] = new byte[maxInSample + 1]; 1099 for (int s = 0; s <= maxInSample; s++) { 1100 int val = (s*maxOutSample + halfMaxInSample)/maxInSample; 1101 scaleh[b][s] = (byte)(val >> 8); 1102 scalel[b][s] = (byte)(val & 0xff); 1103 } 1104 } 1105 scale = null; 1106 scale0 = null; 1107 } 1108 } 1109 1110 @Override 1111 public void write(IIOMetadata streamMetadata, 1112 IIOImage image, 1113 ImageWriteParam param) throws IIOException { 1114 if (stream == null) { 1115 throw new IllegalStateException("output == null!"); 1116 } 1117 if (image == null) { 1118 throw new IllegalArgumentException("image == null!"); 1119 } 1120 if (image.hasRaster()) { 1121 throw new UnsupportedOperationException("image has a Raster!"); 1122 } 1123 1124 RenderedImage im = image.getRenderedImage(); 1125 SampleModel sampleModel = im.getSampleModel(); 1126 this.numBands = sampleModel.getNumBands(); 1127 1128 // Set source region and subsampling to default values 1129 this.sourceXOffset = im.getMinX(); 1130 this.sourceYOffset = im.getMinY(); 1131 this.sourceWidth = im.getWidth(); 1132 this.sourceHeight = im.getHeight(); 1133 this.sourceBands = null; 1134 this.periodX = 1; 1135 this.periodY = 1; 1136 1137 if (param != null) { 1138 // Get source region and subsampling factors 1139 Rectangle sourceRegion = param.getSourceRegion(); 1140 if (sourceRegion != null) { 1141 Rectangle imageBounds = new Rectangle(im.getMinX(), 1142 im.getMinY(), 1143 im.getWidth(), 1144 im.getHeight()); 1145 // Clip to actual image bounds 1146 sourceRegion = sourceRegion.intersection(imageBounds); 1147 sourceXOffset = sourceRegion.x; 1148 sourceYOffset = sourceRegion.y; 1149 sourceWidth = sourceRegion.width; 1150 sourceHeight = sourceRegion.height; 1151 } 1152 1153 // Adjust for subsampling offsets 1154 int gridX = param.getSubsamplingXOffset(); 1155 int gridY = param.getSubsamplingYOffset(); 1156 sourceXOffset += gridX; 1157 sourceYOffset += gridY; 1158 sourceWidth -= gridX; 1159 sourceHeight -= gridY; 1160 1161 // Get subsampling factors 1162 periodX = param.getSourceXSubsampling(); 1163 periodY = param.getSourceYSubsampling(); 1164 1165 int[] sBands = param.getSourceBands(); 1166 if (sBands != null) { 1167 sourceBands = sBands; 1168 numBands = sourceBands.length; 1169 } 1170 } 1171 1172 // Compute output dimensions 1173 int destWidth = (sourceWidth + periodX - 1)/periodX; 1174 int destHeight = (sourceHeight + periodY - 1)/periodY; 1175 if (destWidth <= 0 || destHeight <= 0) { 1176 throw new IllegalArgumentException("Empty source region!"); 1177 } 1178 1179 // Compute total number of pixels for progress notification 1180 this.totalPixels = destWidth*destHeight; 1181 this.pixelsDone = 0; 1182 1183 // Create metadata 1184 IIOMetadata imd = image.getMetadata(); 1185 if (imd != null) { 1186 metadata = (PNGMetadata)convertImageMetadata(imd, 1187 ImageTypeSpecifier.createFromRenderedImage(im), 1188 null); 1189 } else { 1190 metadata = new PNGMetadata(); 1191 } 1192 1193 // reset compression level to default: 1194 int deflaterLevel = DEFAULT_COMPRESSION_LEVEL; 1195 1196 if (param != null) { 1197 switch(param.getCompressionMode()) { 1198 case ImageWriteParam.MODE_DISABLED: 1199 deflaterLevel = Deflater.NO_COMPRESSION; 1200 break; 1201 case ImageWriteParam.MODE_EXPLICIT: 1202 float quality = param.getCompressionQuality(); 1203 if (quality >= 0f && quality <= 1f) { 1204 deflaterLevel = 9 - Math.round(9f * quality); 1205 } 1206 break; 1207 default: 1208 } 1209 1210 // Use Adam7 interlacing if set in write param 1211 switch (param.getProgressiveMode()) { 1212 case ImageWriteParam.MODE_DEFAULT: 1213 metadata.IHDR_interlaceMethod = 1; 1214 break; 1215 case ImageWriteParam.MODE_DISABLED: 1216 metadata.IHDR_interlaceMethod = 0; 1217 break; 1218 // MODE_COPY_FROM_METADATA should already be taken care of 1219 // MODE_EXPLICIT is not allowed 1220 default: 1221 } 1222 } 1223 1224 // Initialize bitDepth and colorType 1225 metadata.initialize(new ImageTypeSpecifier(im), numBands); 1226 1227 // Overwrite IHDR width and height values with values from image 1228 metadata.IHDR_width = destWidth; 1229 metadata.IHDR_height = destHeight; 1230 1231 this.bpp = numBands*((metadata.IHDR_bitDepth == 16) ? 2 : 1); 1232 1233 // Initialize scaling tables for this image 1234 initializeScaleTables(sampleModel.getSampleSize()); 1235 1236 clearAbortRequest(); 1237 1238 processImageStarted(0); 1239 if (abortRequested()) { 1240 processWriteAborted(); 1241 } else { 1242 try { 1243 write_magic(); 1244 write_IHDR(); 1245 1246 write_cHRM(); 1247 write_gAMA(); 1248 write_iCCP(); 1249 write_sBIT(); 1250 write_sRGB(); 1251 1252 write_PLTE(); 1253 1254 write_hIST(); 1255 write_tRNS(); 1256 write_bKGD(); 1257 1258 write_pHYs(); 1259 write_sPLT(); 1260 write_tIME(); 1261 write_tEXt(); 1262 write_iTXt(); 1263 write_zTXt(); 1264 1265 writeUnknownChunks(); 1266 1267 write_IDAT(im, deflaterLevel); 1268 1269 if (abortRequested()) { 1270 processWriteAborted(); 1271 } else { 1272 // Finish up and inform the listeners we are done 1273 writeIEND(); 1274 processImageComplete(); 1275 } 1276 } catch (IOException e) { 1277 throw new IIOException("I/O error writing PNG file!", e); 1278 } 1279 } 1280 } 1281 }