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