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