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