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