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<String> keywordIter = metadata.tEXt_keyword.iterator();
 660         Iterator<String> textIter = metadata.tEXt_text.iterator();
 661 
 662         while (keywordIter.hasNext()) {
 663             ChunkStream cs = new ChunkStream(PNGImageReader.tEXt_TYPE, stream);
 664             String keyword = keywordIter.next();
 665             cs.writeBytes(keyword);
 666             cs.writeByte(0);
 667 
 668             String text = 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<String> keywordIter = metadata.zTXt_keyword.iterator();
 721         Iterator<Integer> methodIter = metadata.zTXt_compressionMethod.iterator();
 722         Iterator<String> textIter = metadata.zTXt_text.iterator();
 723 
 724         while (keywordIter.hasNext()) {
 725             ChunkStream cs = new ChunkStream(PNGImageReader.zTXt_TYPE, stream);
 726             String keyword = keywordIter.next();
 727             cs.writeBytes(keyword);
 728             cs.writeByte(0);
 729 
 730             int compressionMethod = (methodIter.next()).intValue();
 731             cs.writeByte(compressionMethod);
 732 
 733             String text = 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<String> typeIter = metadata.unknownChunkType.iterator();
 741         Iterator<byte[]> dataIter = metadata.unknownChunkData.iterator();
 742 
 743         while (typeIter.hasNext() && dataIter.hasNext()) {
 744             String type = typeIter.next();
 745             ChunkStream cs = new ChunkStream(chunkType(type), stream);
 746             byte[] data = 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 }