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