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