/*
 * @(#)PNGImageWriter.java	1.31 01/12/03
 *
 * Copyright 2002 Sun Microsystems, Inc. All rights reserved.
 * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */

package com.sun.imageio.plugins.png;

import java.awt.Rectangle;
import java.awt.image.ColorModel;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel;
import java.io.ByteArrayOutputStream;
import java.io.DataOutput;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Iterator;
import java.util.Locale;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import javax.imageio.IIOException;
import javax.imageio.IIOImage;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.spi.ImageWriterSpi;
import javax.imageio.stream.ImageOutputStream;
import javax.imageio.stream.ImageOutputStreamImpl;

class CRC {

    private static int[] crcTable = new int[256];
    private int crc = 0xffffffff;

    static {
        // Initialize CRC table
        for (int n = 0; n < 256; n++) {
            int c = n;
            for (int k = 0; k < 8; k++) {
                if ((c & 1) == 1) {
                    c = 0xedb88320 ^ (c >>> 1);
                } else {
                    c >>>= 1;
                }

                crcTable[n] = c;
            }
        }
    }

    public CRC() {}

    public void reset() {
        crc = 0xffffffff;
    }

    public void update(byte[] data, int off, int len) {
        for (int n = 0; n < len; n++) {
            crc = crcTable[(crc ^ data[off + n]) & 0xff] ^ (crc >>> 8);
        }
    }

    public void update(int data) {
        crc = crcTable[(crc ^ data) & 0xff] ^ (crc >>> 8);
    }

    public int getValue() {
        return crc ^ 0xffffffff;
    }
}


class ChunkStream extends ImageOutputStreamImpl {
    
    private ImageOutputStream stream;
    private long startPos;
    private CRC crc = new CRC();

    public ChunkStream(int type, ImageOutputStream stream) throws IOException {
        this.stream = stream;
        this.startPos = stream.getStreamPosition();
        
        stream.writeInt(-1); // length, will backpatch
        writeInt(type);
    }

    public int read() throws IOException {
        throw new RuntimeException("Method not available");
    }

    public int read(byte[] b, int off, int len) throws IOException {
        throw new RuntimeException("Method not available");
    }

    public void write(byte[] b, int off, int len) throws IOException {
        crc.update(b, off, len);
        stream.write(b, off, len);
    }

    public void write(int b) throws IOException {
        crc.update(b);
        stream.write(b);
    }

    public void finish() throws IOException {
        // Write CRC
        stream.writeInt(crc.getValue());

        // Write length
        long pos = stream.getStreamPosition();
        stream.seek(startPos);
        stream.writeInt((int)(pos - startPos) - 12);

        // Return to end of chunk and flush to minimize buffering
        stream.seek(pos);
        stream.flushBefore(pos);
    }
}

// Compress output and write as a series of 'IDAT' chunks of
// fixed length.
class IDATOutputStream extends ImageOutputStreamImpl {
    
    private static byte[] chunkType = {
        (byte)'I', (byte)'D', (byte)'A', (byte)'T'
    };

    private ImageOutputStream stream;
    private int chunkLength;
    private long startPos;
    private CRC crc = new CRC();

    Deflater def = new Deflater(Deflater.BEST_COMPRESSION);
    byte[] buf = new byte[512];

    private int bytesRemaining;

    public IDATOutputStream(ImageOutputStream stream, int chunkLength) 
        throws IOException {
        this.stream = stream;
        this.chunkLength = chunkLength;
        startChunk();
    }

    private void startChunk() throws IOException {
        crc.reset();
        this.startPos = stream.getStreamPosition();
        stream.writeInt(-1); // length, will backpatch

        crc.update(chunkType, 0, 4);
        stream.write(chunkType, 0, 4);

        this.bytesRemaining = chunkLength;
    }

    private void finishChunk() throws IOException {
        // Write CRC
        stream.writeInt(crc.getValue());

        // Write length
        long pos = stream.getStreamPosition();
        stream.seek(startPos);
        stream.writeInt((int)(pos - startPos) - 12);

        // Return to end of chunk and flush to minimize buffering
        stream.seek(pos);
        stream.flushBefore(pos);
    }

    public int read() throws IOException {
        throw new RuntimeException("Method not available");
    }

    public int read(byte[] b, int off, int len) throws IOException {
        throw new RuntimeException("Method not available");
    }

    public void write(byte[] b, int off, int len) throws IOException {
        if (len == 0) {
            return;
        }

	if (!def.finished()) {
	    def.setInput(b, off, len);
	    while (!def.needsInput()) {
		deflate();
	    }
	}
    }

    public void deflate() throws IOException {
	int len = def.deflate(buf, 0, buf.length);
        int off = 0;

        while (len > 0) {
            if (bytesRemaining == 0) {
                finishChunk();
                startChunk();
            }

            int nbytes = Math.min(len, bytesRemaining);
            crc.update(buf, off, nbytes);
            stream.write(buf, off, nbytes);

            off += nbytes;
            len -= nbytes;
            bytesRemaining -= nbytes;
        }
    }

    public void write(int b) throws IOException {
        byte[] wbuf = new byte[1];
        wbuf[0] = (byte)b;
        write(wbuf, 0, 1);
    }

    public void finish() throws IOException {
	if (!def.finished()) {
	    def.finish();
	    while (!def.finished()) {
		deflate();
	    }
	}
        finishChunk();
    }
}


class PNGImageWriteParam extends ImageWriteParam {
    
    public PNGImageWriteParam(Locale locale) {
        super();
        this.canWriteProgressive = true;
        this.locale = locale;
    }
}

/**
 * @version 0.5
 */
public class PNGImageWriter extends ImageWriter {

    ImageOutputStream stream = null;

    PNGMetadata metadata = null;

    // Factors from the ImageWriteParam
    int sourceXOffset = 0;
    int sourceYOffset = 0;
    int sourceWidth = 0;
    int sourceHeight = 0;
    int[] sourceBands = null;
    int periodX = 1;
    int periodY = 1;

    int numBands;
    int bpp;

    RowFilter rowFilter = new RowFilter();
    byte[] prevRow = null;
    byte[] currRow = null;
    byte[][] filteredRows = null;

    // Per-band scaling tables
    //
    // After the first call to initializeScaleTables, either scale and scale0
    // will be valid, or scaleh and scalel will be valid, but not both.
    //
    // The tables will be designed for use with a set of input but depths
    // given by sampleSize, and an output bit depth given by scalingBitDepth.
    //
    int[] sampleSize = null; // Sample size per band, in bits
    int scalingBitDepth = -1; // Output bit depth of the scaling tables

    // Tables for 1, 2, 4, or 8 bit output
    byte[][] scale = null; // 8 bit table
    byte[] scale0 = null; // equivalent to scale[0]

    // Tables for 16 bit output
    byte[][] scaleh = null; // High bytes of output
    byte[][] scalel = null; // Low bytes of output

    int totalPixels; // Total number of pixels to be written by write_IDAT
    int pixelsDone; // Running count of pixels written by write_IDAT

    public PNGImageWriter(ImageWriterSpi originatingProvider) {
        super(originatingProvider);
    }
 
    public void setOutput(Object output) {
        super.setOutput(output);
        if (output != null) {
            if (!(output instanceof ImageOutputStream)) {
                throw new IllegalArgumentException("output not an ImageOutputStream!");
            }
            this.stream = (ImageOutputStream)output;
        } else {
            this.stream = null;
        }
    }

    private static int[] allowedProgressivePasses = { 1, 7 };

    public ImageWriteParam getDefaultWriteParam() {
        return new PNGImageWriteParam(getLocale());
    }

    public IIOMetadata getDefaultStreamMetadata(ImageWriteParam param) {
        return null;
    }

    public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType,
                                               ImageWriteParam param) {
        PNGMetadata m = new PNGMetadata();
        m.initialize(imageType, imageType.getSampleModel().getNumBands());
        return m;
    }

    public IIOMetadata convertStreamMetadata(IIOMetadata inData,
                                             ImageWriteParam param) {
        return null;
    }

    public IIOMetadata convertImageMetadata(IIOMetadata inData,
                                            ImageTypeSpecifier imageType,
                                            ImageWriteParam param) {
        // TODO - deal with imageType
        if (inData instanceof PNGMetadata) {
            return (PNGMetadata)((PNGMetadata)inData).clone();
        } else {
            return new PNGMetadata(inData);
        }
    }

    private void write_magic() throws IOException {
        // Write signature
        byte[] magic = { (byte)137, 80, 78, 71, 13, 10, 26, 10 };
        stream.write(magic);
    }

    private void write_IHDR() throws IOException {
        // Write IHDR chunk
        ChunkStream cs = new ChunkStream(PNGImageReader.IHDR_TYPE, stream);
        cs.writeInt(metadata.IHDR_width);
        cs.writeInt(metadata.IHDR_height);
        cs.writeByte(metadata.IHDR_bitDepth);
        cs.writeByte(metadata.IHDR_colorType);
        if (metadata.IHDR_compressionMethod != 0) {
            throw new IIOException(
"Only compression method 0 is defined in PNG 1.1");
        }
        cs.writeByte(metadata.IHDR_compressionMethod);
        if (metadata.IHDR_filterMethod != 0) {
            throw new IIOException(
"Only filter method 0 is defined in PNG 1.1");
        }
        cs.writeByte(metadata.IHDR_filterMethod);
        if (metadata.IHDR_interlaceMethod < 0 ||
            metadata.IHDR_interlaceMethod > 1) {
            throw new IIOException(
"Only interlace methods 0 (node) and 1 (adam7) are defined in PNG 1.1");
        }
        cs.writeByte(metadata.IHDR_interlaceMethod);
        cs.finish();
    }

    private void write_cHRM() throws IOException {
        if (metadata.cHRM_present) {
            ChunkStream cs = new ChunkStream(PNGImageReader.cHRM_TYPE, stream);
            cs.writeInt(metadata.cHRM_whitePointX);
            cs.writeInt(metadata.cHRM_whitePointY); 
            cs.writeInt(metadata.cHRM_redX);
            cs.writeInt(metadata.cHRM_redY);
            cs.writeInt(metadata.cHRM_greenX);
            cs.writeInt(metadata.cHRM_greenY);
            cs.writeInt(metadata.cHRM_blueX);
            cs.writeInt(metadata.cHRM_blueY);
            cs.finish();
        }
    }

    private void write_gAMA() throws IOException {
        if (metadata.gAMA_present) {
            ChunkStream cs = new ChunkStream(PNGImageReader.gAMA_TYPE, stream);
            cs.writeInt(metadata.gAMA_gamma);
            cs.finish();
        }
    }

    private void write_iCCP() throws IOException {
        if (metadata.iCCP_present) {
            ChunkStream cs = new ChunkStream(PNGImageReader.iCCP_TYPE, stream);
            cs.writeBytes(metadata.iCCP_profileName);
            cs.writeByte(0); // null terminator

            cs.writeByte(metadata.iCCP_compressionMethod);
            cs.write(metadata.iCCP_compressedProfile);
            cs.finish();
        }
    }

    private void write_sBIT() throws IOException {
        if (metadata.sBIT_present) {
            ChunkStream cs = new ChunkStream(PNGImageReader.sBIT_TYPE, stream);
            int colorType = metadata.IHDR_colorType;
            if (metadata.sBIT_colorType != colorType) {
                processWarningOccurred(0,
"sBIT metadata has wrong color type.\n" +
"The chunk will not be written.");
                return;
            }

            if (colorType == PNGImageReader.PNG_COLOR_GRAY ||
                colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA) {
                cs.writeByte(metadata.sBIT_grayBits);
            } else if (colorType == PNGImageReader.PNG_COLOR_RGB ||
                       colorType == PNGImageReader.PNG_COLOR_PALETTE ||
                       colorType == PNGImageReader.PNG_COLOR_RGB_ALPHA) {
                cs.writeByte(metadata.sBIT_redBits);
                cs.writeByte(metadata.sBIT_greenBits);
                cs.writeByte(metadata.sBIT_blueBits);
            }

            if (colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA ||
                colorType == PNGImageReader.PNG_COLOR_RGB_ALPHA) {
                cs.writeByte(metadata.sBIT_alphaBits);
            }
            cs.finish();
        }
    }

    private void write_sRGB() throws IOException {
        if (metadata.sRGB_present) {
            ChunkStream cs = new ChunkStream(PNGImageReader.sRGB_TYPE, stream);
            cs.writeByte(metadata.sRGB_renderingIntent);
            cs.finish();
        }
    }

    private void write_PLTE() throws IOException {
        if (metadata.PLTE_present) {
            if (metadata.IHDR_colorType == PNGImageReader.PNG_COLOR_GRAY ||
              metadata.IHDR_colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA) {
                // PLTE cannot occur in a gray image

                processWarningOccurred(0,
"A PLTE chunk may not appear in a gray or gray alpha image.\n" +
"The chunk will not be written");
                return;
            }

            ChunkStream cs = new ChunkStream(PNGImageReader.PLTE_TYPE, stream);

            int numEntries = metadata.PLTE_red.length;
            byte[] palette = new byte[numEntries*3];
            int index = 0;
            for (int i = 0; i < numEntries; i++) {
                palette[index++] = metadata.PLTE_red[i];
                palette[index++] = metadata.PLTE_green[i];
                palette[index++] = metadata.PLTE_blue[i];
            }

            cs.write(palette);
            cs.finish();
        }
    }

    private void write_hIST() throws IOException, IIOException {
        if (metadata.hIST_present) {
            ChunkStream cs = new ChunkStream(PNGImageReader.hIST_TYPE, stream);

            if (!metadata.PLTE_present) {
                throw new IIOException("hIST chunk without PLTE chunk!");
            }
            
            cs.writeChars(metadata.hIST_histogram,
                          0, metadata.hIST_histogram.length);
            cs.finish();
        }
    }

    private void write_tRNS() throws IOException, IIOException {
        if (metadata.tRNS_present) {
            ChunkStream cs = new ChunkStream(PNGImageReader.tRNS_TYPE, stream);
            int colorType = metadata.IHDR_colorType;
            int chunkType = metadata.tRNS_colorType;

            // Special case: image is RGB and chunk is Gray
            // Promote chunk contents to RGB
            int chunkRed = metadata.tRNS_red;
            int chunkGreen = metadata.tRNS_green;
            int chunkBlue = metadata.tRNS_blue;
            if (colorType == PNGImageReader.PNG_COLOR_RGB &&
                chunkType == PNGImageReader.PNG_COLOR_GRAY) {
                chunkType = colorType;
                chunkRed = chunkGreen = chunkBlue = 
                    metadata.tRNS_gray;
            }

            if (chunkType != colorType) {
                processWarningOccurred(0,
"tRNS metadata has incompatible color type.\n" +
"The chunk will not be written.");
                return;
            }

            if (colorType == PNGImageReader.PNG_COLOR_PALETTE) {
                if (!metadata.PLTE_present) {
                    throw new IIOException("tRNS chunk without PLTE chunk!");
                }
                cs.write(metadata.tRNS_alpha);
            } else if (colorType == PNGImageReader.PNG_COLOR_GRAY) {
                cs.writeShort(metadata.tRNS_gray);
            } else if (colorType == PNGImageReader.PNG_COLOR_RGB) {
                cs.writeShort(chunkRed);
                cs.writeShort(chunkGreen);
                cs.writeShort(chunkBlue);
            } else {
                throw new IIOException("tRNS chunk for color type 4 or 6!");
            }
            cs.finish();
        }
    }

    private void write_bKGD() throws IOException {
        if (metadata.bKGD_present) {
            ChunkStream cs = new ChunkStream(PNGImageReader.bKGD_TYPE, stream);
            int colorType = metadata.IHDR_colorType & 0x3;
            int chunkType = metadata.bKGD_colorType;

            // Special case: image is RGB(A) and chunk is Gray
            // Promote chunk contents to RGB
            int chunkRed = metadata.bKGD_red;
            int chunkGreen = metadata.bKGD_red;
            int chunkBlue = metadata.bKGD_red;
            if (colorType == PNGImageReader.PNG_COLOR_RGB &&
                chunkType == PNGImageReader.PNG_COLOR_GRAY) {
                // Make a gray bKGD chunk look like RGB
                chunkType = colorType;
                chunkRed = chunkGreen = chunkBlue = 
                    metadata.bKGD_gray;
            }

            // Ignore status of alpha in colorType 
            if (chunkType != colorType) {
                processWarningOccurred(0,
"bKGD metadata has incompatible color type.\n" +
"The chunk will not be written.");
                return;
            }

            if (colorType == PNGImageReader.PNG_COLOR_PALETTE) {
                cs.writeByte(metadata.bKGD_index);
            } else if (colorType == PNGImageReader.PNG_COLOR_GRAY ||
                       colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA) {
                cs.writeShort(metadata.bKGD_gray);
            } else { // colorType == PNGImageReader.PNG_COLOR_RGB ||
                     // colorType == PNGImageReader.PNG_COLOR_RGB_ALPHA
                cs.writeShort(chunkRed);
                cs.writeShort(chunkGreen);
                cs.writeShort(chunkBlue);
            }
            cs.finish();
        }
    }

    private void write_pHYs() throws IOException {
        if (metadata.pHYs_present) {
            ChunkStream cs = new ChunkStream(PNGImageReader.pHYs_TYPE, stream);
            cs.writeInt(metadata.pHYs_pixelsPerUnitXAxis);
            cs.writeInt(metadata.pHYs_pixelsPerUnitYAxis);
            cs.writeByte(metadata.pHYs_unitSpecifier);
            cs.finish();
        }
    }

    private void write_sPLT() throws IOException {
        if (metadata.sPLT_present) {
            ChunkStream cs = new ChunkStream(PNGImageReader.sPLT_TYPE, stream);

            cs.writeBytes(metadata.sPLT_paletteName);
            cs.writeByte(0); // null terminator

            cs.writeByte(metadata.sPLT_sampleDepth);
            int numEntries = metadata.sPLT_red.length;

            if (metadata.sPLT_sampleDepth == 8) {
                for (int i = 0; i < numEntries; i++) {
                    cs.writeByte(metadata.sPLT_red[i]);
                    cs.writeByte(metadata.sPLT_green[i]);
                    cs.writeByte(metadata.sPLT_blue[i]);
                    cs.writeByte(metadata.sPLT_alpha[i]);
                    cs.writeShort(metadata.sPLT_frequency[i]);
                }
            } else { // sampleDepth == 16
                for (int i = 0; i < numEntries; i++) {
                    cs.writeShort(metadata.sPLT_red[i]);
                    cs.writeShort(metadata.sPLT_green[i]);
                    cs.writeShort(metadata.sPLT_blue[i]);
                    cs.writeShort(metadata.sPLT_alpha[i]);
                    cs.writeShort(metadata.sPLT_frequency[i]);
                }
            }
            cs.finish();
        }
    }

    private void write_tIME() throws IOException {
        if (metadata.tIME_present) {
            ChunkStream cs = new ChunkStream(PNGImageReader.tIME_TYPE, stream);
            cs.writeShort(metadata.tIME_year);
            cs.writeByte(metadata.tIME_month);
            cs.writeByte(metadata.tIME_day);
            cs.writeByte(metadata.tIME_hour);
            cs.writeByte(metadata.tIME_minute);
            cs.writeByte(metadata.tIME_second);
            cs.finish();
        }
    }

    private void write_tEXt() throws IOException {
        Iterator keywordIter = metadata.tEXt_keyword.iterator();
        Iterator textIter = metadata.tEXt_text.iterator();

        while (keywordIter.hasNext()) {
            ChunkStream cs = new ChunkStream(PNGImageReader.tEXt_TYPE, stream);
            String keyword = (String)keywordIter.next();
            cs.writeBytes(keyword);
            cs.writeByte(0);

            String text = (String)textIter.next();
            cs.writeBytes(text);
            cs.finish();
        }
    }

    private byte[] deflate(String s) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        DeflaterOutputStream dos = new DeflaterOutputStream(baos);

        int len = s.length();
        for (int i = 0; i < len; i++) {
            dos.write((int)s.charAt(i));
        }
        dos.close();
        
        return baos.toByteArray();
    }

    private void write_iTXt() throws IOException {
        Iterator keywordIter = metadata.iTXt_keyword.iterator();
        Iterator flagIter = metadata.iTXt_compressionFlag.iterator();
        Iterator methodIter = metadata.iTXt_compressionMethod.iterator();
        Iterator languageIter = metadata.iTXt_languageTag.iterator();
        Iterator translatedKeywordIter =
            metadata.iTXt_translatedKeyword.iterator();
        Iterator textIter = metadata.iTXt_text.iterator();

        while (keywordIter.hasNext()) {
            ChunkStream cs = new ChunkStream(PNGImageReader.iTXt_TYPE, stream);
            String keyword = (String)keywordIter.next();
            cs.writeBytes(keyword);
            cs.writeByte(0);

            int flag = ((Integer)flagIter.next()).intValue();
            cs.writeByte(flag);
            int method = ((Integer)methodIter.next()).intValue();
            cs.writeByte(method);

            String languageTag = (String)languageIter.next();
            cs.writeBytes(languageTag);
            cs.writeByte(0);

            String translatedKeyword = (String)translatedKeywordIter.next();
            cs.writeBytes(translatedKeyword);
            cs.writeByte(0);

            String text = (String)textIter.next();
            if (flag == 1) {
                cs.write(deflate(text));
            } else {
                cs.writeUTF(text);
            }
            cs.finish();
        }
    }

    private void write_zTXt() throws IOException {
        Iterator keywordIter = metadata.zTXt_keyword.iterator();
        Iterator methodIter = metadata.zTXt_compressionMethod.iterator();
        Iterator textIter = metadata.zTXt_text.iterator();

        while (keywordIter.hasNext()) {
            ChunkStream cs = new ChunkStream(PNGImageReader.zTXt_TYPE, stream);
            String keyword = (String)keywordIter.next();
            cs.writeBytes(keyword);
            cs.writeByte(0);

            int compressionMethod = ((Integer)methodIter.next()).intValue();
            cs.writeByte(compressionMethod);

            String text = (String)textIter.next();
            cs.write(deflate(text));
            cs.finish();
        }
    }

    private void writeUnknownChunks() throws IOException {
        Iterator typeIter = metadata.unknownChunkType.iterator();
        Iterator dataIter = metadata.unknownChunkData.iterator();

        while (typeIter.hasNext() && dataIter.hasNext()) {
            String type = (String)typeIter.next();
            ChunkStream cs = new ChunkStream(PNGImageReader.chunkType(type),
                                             stream);
            byte[] data = (byte[])dataIter.next();
            cs.write(data);
            cs.finish();
        }
    }

    private void encodePass(ImageOutputStream os,
                            RenderedImage image,
                            int xOffset, int yOffset,
                            int xSkip, int ySkip) throws IOException {
        int minX = sourceXOffset;
        int minY = sourceYOffset;
        int width = sourceWidth;
        int height = sourceHeight;
        
        // Adjust offsets and skips based on source subsampling factors
        xOffset *= periodX;
        xSkip *= periodX;
        yOffset *= periodY;
        ySkip *= periodY;

        // Early exit if no data for this pass
        int hpixels = (width - xOffset + xSkip - 1)/xSkip;
        int vpixels = (height - yOffset + ySkip - 1)/ySkip;
        if (hpixels == 0 || vpixels == 0) {
            return;
        }

        // Convert X offset and skip from pixels to samples
        xOffset *= numBands;
        xSkip *= numBands;

        // Create row buffers
        int samplesPerByte = 8/metadata.IHDR_bitDepth;
        int numSamples = width*numBands;
        int[] samples = new int[numSamples];

        int bytesPerRow = hpixels*numBands;
        if (metadata.IHDR_bitDepth < 8) {
            bytesPerRow = (bytesPerRow + samplesPerByte - 1)/samplesPerByte;
        } else if (metadata.IHDR_bitDepth == 16) {
            bytesPerRow *= 2;
        }
        currRow = new byte[bytesPerRow + bpp];
        prevRow = new byte[bytesPerRow + bpp];
        filteredRows = new byte[5][bytesPerRow + bpp];

	int bitDepth = metadata.IHDR_bitDepth;
        for (int row = minY + yOffset; row < minY + height; row += ySkip) {
            Rectangle rect = new Rectangle(minX, row, width, 1);
            Raster ras = image.getData(rect);
            if (sourceBands != null) {
                ras = ras.createChild(minX, row, width, 1, minX, row,
                                      sourceBands);
            }

            // TODO - need to divide out premultiplied alpha?
            ras.getPixels(minX, row, width, 1, samples);

            // Reorder palette data if necessary
            int[] paletteOrder = metadata.PLTE_order;
            if (paletteOrder != null) {
                for (int i = 0; i < numSamples; i++) {
                    samples[i] = paletteOrder[samples[i]];
                }
            }

            int count = bpp; // leave first 'bpp' bytes zero
            int pos = 0;
            int tmp = 0;

            switch (bitDepth) {
            case 1: case 2: case 4:
                // Image can only have a single band
                
                int mask = samplesPerByte - 1;
                for (int s = xOffset; s < numSamples; s += xSkip) {
		    byte val = scale0[samples[s]];
                    tmp = (tmp << bitDepth) | val;

                    if ((pos++ & mask) == mask) {
                        currRow[count++] = (byte)tmp;
                        tmp = 0;
                        pos = 0;
                    }
                }

                // Left shift the last byte
                if ((pos & mask) != 0) {
                    tmp <<= ((8/bitDepth) - pos)*bitDepth;
                    currRow[count++] = (byte)tmp;
                }
                break;

            case 8:
		if (numBands == 1) {
		    for (int s = xOffset; s < numSamples; s += xSkip) {
			currRow[count++] = scale0[samples[s]];
		    }
		} else {
		    for (int s = xOffset; s < numSamples; s += xSkip) {
			for (int b = 0; b < numBands; b++) {
			    currRow[count++] = scale[b][samples[s + b]];
			}
		    }
		}
                break;

            case 16:
                for (int s = xOffset; s < numSamples; s += xSkip) {
                    for (int b = 0; b < numBands; b++) {
                        currRow[count++] = scaleh[b][samples[s + b]];
                        currRow[count++] = scalel[b][samples[s + b]];
                    }
                }
                break;
            }

            // Perform filtering
            int filterType = rowFilter.filterRow(metadata.IHDR_colorType,
                                                 currRow, prevRow,
                                                 filteredRows,
                                                 bytesPerRow, bpp);
            
            os.write(filterType);
            os.write(filteredRows[filterType], bpp, bytesPerRow);
            
            // Swap current and previous rows
            byte[] swap = currRow;
            currRow = prevRow;
            prevRow = swap;

            pixelsDone += hpixels;
            processImageProgress(100.0F*pixelsDone/totalPixels);

            // If write has been aborted, just return;
            // processWriteAborted will be called later
            if (abortRequested()) {
                return;
            }
        }
    }

    // Use sourceXOffset, etc.
    private void write_IDAT(RenderedImage image) throws IOException {
        IDATOutputStream ios = new IDATOutputStream(stream, 32768);

        if (metadata.IHDR_interlaceMethod == 1) {
            for (int i = 0; i < 7; i++) {
                encodePass(ios, image,
                           PNGImageReader.adam7XOffset[i],
                           PNGImageReader.adam7YOffset[i],
                           PNGImageReader.adam7XSubsampling[i],
                           PNGImageReader.adam7YSubsampling[i]);
                if (abortRequested()) {
                    break;
                }
            }
        } else {
            encodePass(ios, image, 0, 0, 1, 1);
        }

        ios.finish();
    }

    private void writeIEND() throws IOException {
        ChunkStream cs = new ChunkStream(PNGImageReader.IEND_TYPE, stream);
        cs.finish();
    }

    // Check two int arrays for value equality, always returns false
    // if either array is null
    private boolean equals(int[] s0, int[] s1) {
	if (s0 == null || s1 == null) {
	    return false;
	}
	if (s0.length != s1.length) {
	    return false;
	}
	for (int i = 0; i < s0.length; i++) {
	    if (s0[i] != s1[i]) {
		return false;
	    }
	}
	return true;
    }

    // Initialize the scale/scale0 or scaleh/scalel arrays to
    // hold the results of scaling an input value to the desired
    // output bit depth
    private void initializeScaleTables(int[] sampleSize) {
	int bitDepth = metadata.IHDR_bitDepth;

	// If the existing tables are still valid, just return
	if (bitDepth == scalingBitDepth &&
	    equals(sampleSize, this.sampleSize)) {
	    return;
	}
	
	// Compute new tables
	this.sampleSize = sampleSize;
	this.scalingBitDepth = bitDepth;
	int maxOutSample = (1 << bitDepth) - 1;
	if (bitDepth <= 8) {
	    scale = new byte[numBands][];
	    for (int b = 0; b < numBands; b++) {
		int maxInSample = (1 << sampleSize[b]) - 1;
		int halfMaxInSample = maxInSample/2;
		scale[b] = new byte[maxInSample + 1];
		for (int s = 0; s <= maxInSample; s++) {
		    scale[b][s] =
			(byte)((s*maxOutSample + halfMaxInSample)/maxInSample);
		}
	    }
	    scale0 = scale[0];
	    scaleh = scalel = null;
	} else { // bitDepth == 16
	    // Divide scaling table into high and low bytes
	    scaleh = new byte[numBands][];
	    scalel = new byte[numBands][];

	    for (int b = 0; b < numBands; b++) {
		int maxInSample = (1 << sampleSize[b]) - 1;
		int halfMaxInSample = maxInSample/2;
		scaleh[b] = new byte[maxInSample + 1];
		scalel[b] = new byte[maxInSample + 1];
		for (int s = 0; s <= maxInSample; s++) {
		    int val = (s*maxOutSample + halfMaxInSample)/maxInSample;
		    scaleh[b][s] = (byte)(val >> 8);
		    scalel[b][s] = (byte)(val & 0xff);
		}
	    }
	    scale = null;
	    scale0 = null;
	}
    }

    public void write(IIOMetadata streamMetadata,
                      IIOImage image,
                      ImageWriteParam param) throws IIOException {
        if (stream == null) {
            throw new IllegalStateException("output == null!");
        }
        if (image == null) {
            throw new IllegalArgumentException("image == null!");
        }
        if (image.hasRaster()) {
            throw new UnsupportedOperationException("image has a Raster!");
        }

        RenderedImage im = image.getRenderedImage();
        SampleModel sampleModel = im.getSampleModel();
        this.numBands = sampleModel.getNumBands();

        // Set source region and subsampling to default values
        this.sourceXOffset = im.getMinX();
        this.sourceYOffset = im.getMinY();
        this.sourceWidth = im.getWidth();
        this.sourceHeight = im.getHeight();
        this.sourceBands = null;
        this.periodX = 1;
        this.periodY = 1;

        if (param != null) {
            // Get source region and subsampling factors
            Rectangle sourceRegion = param.getSourceRegion();
            if (sourceRegion != null) {
                Rectangle imageBounds = new Rectangle(im.getMinX(),
                                                      im.getMinY(),
                                                      im.getWidth(),
                                                      im.getHeight());
                // Clip to actual image bounds
                sourceRegion = sourceRegion.intersection(imageBounds);
                sourceXOffset = sourceRegion.x;
                sourceYOffset = sourceRegion.y;
                sourceWidth = sourceRegion.width;
                sourceHeight = sourceRegion.height;
            }

            // Adjust for subsampling offsets
            int gridX = param.getSubsamplingXOffset();
            int gridY = param.getSubsamplingYOffset();
            sourceXOffset += gridX;
            sourceYOffset += gridY;
            sourceWidth -= gridX;
            sourceHeight -= gridY;

            // Get subsampling factors
            periodX = param.getSourceXSubsampling();
            periodY = param.getSourceYSubsampling();

            int[] sBands = param.getSourceBands();
            if (sBands != null) {
                sourceBands = sBands;
                numBands = sourceBands.length;
            }
        }

        // Compute output dimensions
        int destWidth = (sourceWidth + periodX - 1)/periodX;
        int destHeight = (sourceHeight + periodY - 1)/periodY;
        if (destWidth <= 0 || destHeight <= 0) {
            throw new IllegalArgumentException("Empty source region!");
        }

        // Compute total number of pixels for progress notification
        this.totalPixels = destWidth*destHeight;
        this.pixelsDone = 0;

        // Create metadata
        IIOMetadata imd = image.getMetadata();
        if (imd != null) {
            metadata = (PNGMetadata)convertImageMetadata(imd,
                               ImageTypeSpecifier.createFromRenderedImage(im),
                                                         null);
        } else {
            metadata = new PNGMetadata();
        }

        if (param != null) {
            // Use Adam7 interlacing if set in write param
            switch (param.getProgressiveMode()) {
            case ImageWriteParam.MODE_DEFAULT:
                metadata.IHDR_interlaceMethod = 1;
                break;
            case ImageWriteParam.MODE_DISABLED:
                metadata.IHDR_interlaceMethod = 0;
                break;
                // MODE_COPY_FROM_METADATA should alreay be taken care of
                // MODE_EXPLICIT is not allowed
            }
        }

        // Initialize bitDepth and colorType
        metadata.initialize(new ImageTypeSpecifier(im), numBands);

        // Overwrite IHDR width and height values with values from image
        metadata.IHDR_width = destWidth;
        metadata.IHDR_height = destHeight;

        this.bpp = numBands*((metadata.IHDR_bitDepth == 16) ? 2 : 1);

	// Initialize scaling tables for this image
	initializeScaleTables(sampleModel.getSampleSize());

        clearAbortRequest();

        processImageStarted(0);

        try {
            write_magic();
            write_IHDR();

            write_cHRM();
            write_gAMA();
            write_iCCP();
            write_sBIT();
            write_sRGB();
        
            write_PLTE();
            
            write_hIST();
            write_tRNS();
            write_bKGD();
            
            write_pHYs();
            write_sPLT();
            write_tIME();
            write_tEXt();
            write_iTXt();
            write_zTXt();
            
            writeUnknownChunks();
            
            write_IDAT(im);

            if (abortRequested()) {
                processWriteAborted();
            } else {
                // Finish up and inform the listeners we are done
                writeIEND();
                processImageComplete();
            }
        } catch (IOException e) {
            throw new IIOException("I/O error writing PNG file!", e);
        }
    }
}
