/*
 *  FSImageConstructor.java
 *  Transform Utilities
 *
 * Copyright (c) 2001-2008 Flagstone Software Ltd. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, 
 * are permitted provided that the following conditions are met:
 *
 *  * Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 *  * Redistributions in binary form must reproduce the above copyright notice, 
 *    this list of conditions and the following disclaimer in the documentation 
 *    and/or other materials provided with the distribution.
 *  * Neither the name of Flagstone Software Ltd. nor the names of its contributors 
 *    may be used to endorse or promote products derived from this software 
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
 * IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.flagstone.transform.util;

import java.io.*;
import java.util.*;
import java.util.zip.*;

import com.flagstone.transform.FSDefineObject;
import com.flagstone.transform.FSDefineImage;
import com.flagstone.transform.FSDefineImage2;
import com.flagstone.transform.FSDefineJPEGImage2;
import com.flagstone.transform.FSDefineShape3;

import com.flagstone.transform.FSCoder;
import com.flagstone.transform.FSImageBlock;
import com.flagstone.transform.FSShape;
import com.flagstone.transform.FSCoordTransform;
import com.flagstone.transform.FSBounds;
import com.flagstone.transform.FSShapeStyle;
import com.flagstone.transform.FSLine;

import com.flagstone.transform.FSSolidLine;

import com.flagstone.transform.FSFillStyle;
import com.flagstone.transform.FSBitmapFill;

/**
 * The FSImageConstructor class is used to generate an image definition object from an image stored
 * in a file. Currently PNG, BMP and JPEG encoded images are supported. The name of the file containing 
 * the image can be specified directly in the class constructor:</p>
 *
 * <pre>
 *     FSImageConstructor imageConstructor = new FSImageConstructor(imageFile);
  * </pre>
 *
 * A single FSImageConstructor object can also be used to load a series of images:
 *
 * <pre>
 *     imageConstructor.setImageFromFile(imageFile);
 * </pre>
 *
 * <P>If an error occurs each method will throw either a FileNotFoundException, IOException or 
 * DataFormatException depending on the error detected.</P>
 *
 * <P>When an image is loaded the getFormat() method identifies the way the image is encoded:</P>
 *
 * <table>
 * <tr><th>Format</th><th>Description</th></tr>
 *
 * <tr>
 * <td>JPEG</td>
 * <td>A JPEG encoded image.</td>
 * </tr>
 *
 * <tr>
 * <td>IDX8</td>
 * <td>An indexed image where each pixel specifies an index into a colour table containing up to 
 * 256 24-bit colours. Transparent colours are not supported.</td>
 * </tr>
 *
 * <tr>
 * <td>IDXA</td>
 * <td>An indexed image where each pixel specifies an index into a colour table containing up to 
 * 256 32-bit colours - 8-bits for each colour channel and 8-bits for transparency.</td>
 * </tr>
 *
 * <tr>
 * <td>RGB5</td>
 * <td>An true colour image where each pixel specifies a 16-bit colour, with 5-bits per colour
 * channel. Transparent colours are not supported.</td>
 * </tr>
 *
 * <tr>
 * <td>RGB8</td>
 * <td>An true colour image where each pixel specifies a 24-bit colour, with 8-bits per colour
 * channel. Each pixel occupies 32-bits. Transparent colours are not supported so the first 
 * (most significant) byte is set to 255 by default.</td>
 * </tr>
 *
 * <tr>
 * <td>RGBA</td>
 * <td>An true colour image where each pixel specifies a 32-bit colour, with 8-bits per colour
 * channel and 8-bits for transparency. The first (most significant) byte contains the 
 * transparency information.</td>
 * </tr>
 *
 * </table>
 *
 * <P>Once an image is loaded the definition required to add the image to a Flash file is 
 * generated using the defineImage() method:</p>
 *
 * <pre>
 *     movie.add(imageConstructor.defineImage(movie.newIdentifier()));
 * </pre>
 *
 * <P>The defineImage()method returns an FSDefineObject (the abstract base class for all objects used 
 * to define shapes etc. in a Flash file. The exact class of the object generated depends of the format 
 * of the image loaded.</P>
 *
 * <table>
 * <tr><th>Class</th><th>Generated when...</th></tr>
 *
 * <tr>
 * <td>FSDefineJPEGImage2</td>
 * <td>A JPEG encoded image is loaded. The getFormat() method returns the class constant JPEG.</td>
 * </tr>
 *
 * <tr>
 * <td>FSDefineImage</td>
 * <td>An indexed BMP or PNG image contains a colour table without transparent colours or 
 * when a true colour image contains 16-bit or 24-bit colours is loaded. The getFormat() method returns 
 * the class constants IDX8, RGB5 or RGB8.</td>
 * </tr>
 *
 * <tr>
 * <td>FSDefineImage2</td>
 * <td>A BMP or PNG indexed image contains a colour table with transparent colours is loaded or 
 * when a true colour image contains 32-bit bit colours. The getFormat() method returns the class 
 * constants IDXA or RGBA.</td>
 * </tr>
 *
 * </table>
 * 
 * <P>Images are displayed in Flash by filling a shape with the image bitmap. The defineEnclosingShape()
 * method generates a rectangular shape object which wraps the image:
 *
 * <pre>
 *     int imageId = movie.newIdentifier();
 *     int shapeId = movie.newIdentifier();
 * 
 *     int xOrigin = imageConstructor.getWidth()/2;
 *     int yOrigin = imageConstructor.getHeight()/2;
 * 
 *     boolean border = false;
 * 
 *     movie.add(imageConstructor.defineImage(imageId));
 *     movie.add(imageConstructor.defineEnclosingShape(shapeId, imageId, xOrigin, yOrigin, border));
 * </pre>
 * 
 * <P>Here the origin, used when placing the shape on the screen, is defined as the centre of the shape.
 * Other points may be defined to suit the alignment of the shape when it is placed on the display list.</P>
 * 
 * <h2>Post Processing</h2>
 * 
 * <P>Once an image has been loaded, FSImageConstructor supports a range of methods to access the colour table 
 * or image data - depending on the image format. This allows the image data to be processed before the objects
 * used to add the image to a Flash file are defined.
 * 
 * <pre>
 *     imageConstructor.getColorTable();
 *     imageConstructor.getIndexedImage();
 *     imageConstructor.setColorImage();
 *     imageConstructor.getJPEGImage();
 * </pre>
 * 
 * <P>The information returned will depend on the image format. If an indexed image is loaded then the 
 * getColourTable() and getIndexedImage() will return the arrays of bytes for the colour table and image
 * respectively. If getColorImage() or getJPEGImage() are used on an indexed image then they will return 
 * null objects.</P>
 * 
 * <P>Once the image data has been processed separate set methods are available depending on the type of 
 * image:</P>
 *
 * <pre>
 *     imageConstructor.setIndexedImage(format, imageWidth, imageHeight, colourTable, image);
 *     imageConstructor.setColorImage(format, imageWidth, imageHeight, image);
 *     imageConstructor.setJPEGImage(imageWidth, imageHeight, image);
 * </pre>
 */
public class FSImageConstructor
{
    /** Format for JPEG encoded images */
    public static final int JPEG = 0;
    /** Format for indexed images containing a colour table with 24-bit colours. */
    public static final int IDX8 = 1;
    /** Format for indexed images containing a colour table with 32-bit colours. */
    public static final int IDXA = 2;
    /** Format for true colour images containing 16-bit colours, 5-bits for each colour channel. */
    public static final int RGB5 = 3;
    /** Format for true colour images containing 24-bit colours, 8-bits for each colour channel. */
    public static final int RGB8 = 4;
    /** Format for true colour images containing 32-bit colours, 8-bits for each colour channel plus transparency. */
    public static final int RGBA = 5;
    
    // Tables mapping greyscale values onto 8-bit colour channels
    
    private static final int[] monochrome = { 0, 255 };
    private static final int[] greyscale2 = { 0, 85, 170, 255 };
    private static final int[] greyscale4 = { 0, 17, 34, 51, 68, 85, 102, 119, 136, 153, 170, 187, 204, 221, 238, 255 };
    
    // Constants used for BMP images 
    
    private static final int[] bmpSignature = { 66, 77 };

    private final int BI_RGB = 0;
    private final int BI_RLE8 = 1;
    private final int BI_RLE4 = 2;
    private final int BI_BITFIELDS = 3;

    // Constants used for PNG images 
        
    private static final int[] pngSignature = { 137, 80, 78, 71, 13, 10, 26, 10 };

    /*
     * The constants commented out in the following section are defined
     * for completeness when decoding LNG format images.
     */
     
    // private static final int CRITICAL_CHUNK = 0x20000000;
    
    private static final int IHDR = 0x49484452;
    private static final int PLTE = 0x504c5445;
    private static final int IDAT = 0x49444154;
    private static final int IEND = 0x49454e44;
    private static final int tRNS = 0x74524e53;
    // private static final int bKGD = 0x624b4744;
    // private static final int cHRM = 0x6348524d;
    // private static final int fRAc = 0x66524163;
    // private static final int gAMA = 0x67414d41;
    // private static final int gIFg = 0x67494667;
    // private static final int gIFt = 0x67494674;
    // private static final int gIFx = 0x67494678;
    // private static final int hIST = 0x68495354;
    // private static final int iCCP = 0x69434350;
    // private static final int iTXt = 0x69545874;
    // private static final int oFFs = 0x6f464673;
    // private static final int pCAL = 0x7043414c;
    // private static final int pHYs = 0x70485973;
    // private static final int sBIT = 0x73424954;
    // private static final int sCAL = 0x7343414c;
    // private static final int sPLT = 0x73504c54;
    // private static final int sRGB = 0x73524742;
    // private static final int tEXt = 0x74455874;
    // private static final int tIME = 0x74494d45;
    // private static final int zTXt = 0x7a545874;
    
    private static final int GREYSCALE = 0;
    private static final int TRUE_COLOUR = 2;
    private static final int INDEXED_COLOUR = 3;
    private static final int ALPHA_GREYSCALE = 4;
    private static final int ALPHA_TRUECOLOUR = 6;
    
    private static final int NO_FILTER = 0;
    private static final int SUB_FILTER = 1;
    private static final int UP_FILTER = 2;
    private static final int AVG_FILTER = 3;
    private static final int PAETH_FILTER = 4;
    
    private static final int[] startRow =        { 0, 0, 4, 0, 2, 0, 1 };
    private static final int[] startColumn =     { 0, 4, 0, 2, 0, 1, 0 };
    private static final int[] rowIncrement =    { 8, 8, 8, 4, 4, 2, 2 };
    private static final int[] columnIncrement = { 8, 8, 4, 4, 2, 2, 1 };
    
    // Attributes shred between different image formats
    private static final int BIT_DEPTH = 0;
    private static final int COLOUR_COMPONENTS = 1;
    private static final int COMPRESSION_METHOD = 2;
    
    // BMP specific attributes
    private static final int RED_MASK = 4;
    private static final int GREEN_MASK = 5;
    private static final int BLUE_MASK = 6;

    // PNG specific attributes
    private static final int COLOUR_TYPE = 4;
    private static final int FILTER_METHOD = 5;
    private static final int INTERLACE_METHOD = 6;
    private static final int TRANSPARENT_GREY = 7;
    private static final int TRANSPARENT_RED = 8;
    private static final int TRANSPARENT_GREEN = 9;
    private static final int TRANSPARENT_BLUE = 10;
    
    private int format = 0;
    private int width = 0;
    private int height = 0;

    private byte[][] colourTable = null;
    private byte[][] indexedImage = null;
    private byte[][][] colorImage = null;
    private byte[] jpegImage = null;
    
    private int[] attributes = new int[16];
    private byte[] chunkData = new byte[0];
    
    /**
     * Constructs an FSImageConstructor object with no image.
     */
    public FSImageConstructor()
    {
    }
    
    /**
     * Constructs and FSImageConstructor object and loads the image from the specified file. The FSImageConstructor
     * class support Windows bitmap (BMP), Portable Network Graphics (PNG) or JPEG encoded images.
     *
     * @param filename the name of the file containing the image.
     *
     * @throws FileNotFoundException is the file cannot be found.
     *
     * @throws IOException if an error occurs while reading the file.
     *
     * @throws DataFormatException if the file contains an unsupported image format or if an error occurs 
     * while decoding the image in the file.
     */
    public FSImageConstructor(String filename) throws IOException, DataFormatException
    {
        setImage(dataFromFile(filename));
    }   
    /**
     * Constructs and FSImageConstructor object and loads the encoded image data. The FSImageConstructor
     * class support Windows bitmap (BMP), Portable Network Graphics (PNG) or JPEG encoded images.
     *
     * @param bytes an array of bytes containing the encoded image.
     *
     * @throws DataFormatException if the data contains an unsupported image format or if an error occurs 
     * while decoding the image.
     */
    public FSImageConstructor(byte[] bytes) throws DataFormatException
    {
        setImage(bytes);
    }
    
    /**
     * Initialises the FSImageConstructor object with the image in the specified file. This method can be used
     * to generate the image definition objects for a Flash movie using the same FSImageConstructor object.
     *
     * @param filename the name of the file containing the image.
     *
     * @throws FileNotFoundException is the file cannot be found.
     *
     * @throws IOException if an error occurs while reading the file.
     *
     * @throws DataFormatException if the file contains an unsupported image format or if an error occurs 
     * while decoding the image in the file.
     */
    public void setImageFromFile(String filename) throws IOException, DataFormatException
    {
        setImage(dataFromFile(filename));
    }
    
    /**
     * Initialises the FSImageConstructor object with the image data.
     *
     * @param bytes an array of bytes containing the encoded image.
     *
     * @throws DataFormatException if the data contains an unsupported image format or if an error occurs 
     * while decoding the image.
     */
    public void setImage(byte[] bytes) throws DataFormatException
    {
        int signature = ((bytes[0] & 0xFF) << 8) | (bytes[1] & 0xFF);

        format = 0;
        width = 0;
        height = 0;
    
        attributes[BIT_DEPTH] = 0;
        attributes[COLOUR_COMPONENTS] = 0;
        attributes[COLOUR_TYPE] = 0;
        attributes[COMPRESSION_METHOD] = 0;
        attributes[FILTER_METHOD] = 0;
        attributes[INTERLACE_METHOD] = 0;
        chunkData = new byte[0];

        attributes[TRANSPARENT_GREY] = -1;    
        attributes[TRANSPARENT_RED] = -1;
        attributes[TRANSPARENT_GREEN] = -1;
        attributes[TRANSPARENT_BLUE] = -1;

        attributes[RED_MASK] = 0;
        attributes[GREEN_MASK] = 0;
        attributes[BLUE_MASK] = 0;

        colourTable = null;
        indexedImage = null;
        colorImage = null;
        jpegImage = null;
        
        switch (signature)
        {
            case 0xffd8: decodeJPEG(bytes); break;
            case 0x424d: decodeBMP(bytes); break;
            case 0x8950: decodePNG(bytes); break;
            default: 
                throw new DataFormatException("Unsupported image format");  
        }
    }
    
    /**
     * Returns a constant identifying the format for the way the pixels are encoded.
     *
     * @return the image format.
     */
    public int getFormat()
    {
        return format;
    }

    /**
     * Returns the width of the image in pixels.
     * 
     * @return the image width in pixels.
     */
    public int getWidth()
    {
        return width;
    }
    /**
     * Returns the height of the image in pixels.
     * 
     * @return the image height in pixels.
     */
    public int getHeight()
    {
        return height;
    }   
    /**
     * Returns a copy of the colour table used in an indexed image. Each entry in the 
     * colour table contains 4 bytes with one byte for the alpha channel and each
     * of the three colour channels, red, green and blue. The alpha channel occupied the 
     * most significant byte (3) followed by red, green and blue (the least significant byte).
     * 
     * For images in IDX8 format the alpha channel defaults to 255 (completely opaque).
     * 
     * If the image format is JPEG or one of the true colour formats (RGB5, RGB8, RGBA)
     * then the colour table returned is null.
     *
     * @return a two-dimensional array of bytes containing the colours used in an indexed image.
     */
    public byte[][] getColorTable()
    {
        byte[][] table = null;
        
        if (colourTable != null)
        {
            table = new byte[colourTable.length][4];
        
            for (int i=0; i<colourTable.length; i++)
            {
                for (int j=0; j<4; j++)
                    table[i][j] = colourTable[i][j];            
            }
        }
        return table;
    }
    /**
     * Returns a copy of the image data decoded from an indexed image. A two
     * dimensional array is returned, byte[height][width] with each entry containing an 
     * index into the colour table.
     * 
     * If the image format is JPEG or one of the true colour formats (RGB5, RGB8, RGBA)
     * then the indexed image returned is null.
     *
     * @return a two-dimensional array of bytes (height x width) with each entry containing 
     * an index into the colour table.
     */
    public byte[][] getIndexedImage()
    {
        byte[][] image = null;
        
        if (indexedImage != null)
        {
            image = new byte[height][width];
        
            for (int i=0; i<height; i++)
            {
                for (int j=0; j<width; j++)
                    image[i][j] = indexedImage[i][j];            
            }
        }
        return image;
    }
    /**
     * Returns a copy of the image data decoded from a true colour file. A three
     * dimensional array is returned with four bytes for each pixel in the image,
     * byte[height][width][4] - one byte for the red [0], green [1], blue [2] and
     * alpha [3] channels.
     * 
     * If the image format is JPEG or an indexed image (IDX8, IDXA) then the image
     * data returned is null.
     *
     * @return an array of bytes containing the colour channels for each pixel in
     * the image.
     */
    public byte[][][] getColorImage()
    {
        byte[][][] image = null;
        
        if (colorImage != null)
        {
            image = new byte[height][width][4];

            for (int h=0; h<height; h++)
            {
                for (int w=0; w<width; w++)
                {
                    image[h][w][0] = colorImage[h][w][0];    
                    image[h][w][1] = colorImage[h][w][1];    
                    image[h][w][2] = colorImage[h][w][2];    
                    image[h][w][3] = colorImage[h][w][3];    
                }
            }
        }
        return image;
    }
    /**
     * Returns a copy of the encoded data decoded from a JPEG image file.
     * 
     * If the image format is an indexed or true colour image (IDX8, IDXA, RGB5, 
     * RGB8, RGBA) then the indexed image returned is null.
     *
     * @return an array of bytes containing the JPEG encoded image.
     */
    public byte[] getJPEGImage()
    {
        byte[] image = null;
        
        if (jpegImage != null)
        {
            image = new byte[jpegImage.length];
        
            System.arraycopy(jpegImage, 0, image, 0, jpegImage.length);
        }
        return image;
    }
    
    /**
     * Return the image as an array of FSImageBlock objects that can be used
     * when creating ScreenVideo streams.
     * 
     * The image is divided by tiling blocks of the specified width and height 
     * across the image. For blocks at the right and bottom edges the size of 
     * the block may be reduced so that it fits the image exactly. In other 
     * words the blocks are not padded with extra pixel information.
     * 
     * @param blockWidth the width of a block in pixels.
     * @param blockHeight the height of a block in pixels
     * @return an array of FMImageBlock objects.
     */   
	public ArrayList getImageAsBlocks(int blockWidth, int blockHeight)
	{
        int row = 0;
        int col = 0;
        int index = 0;

        byte[] formattedImage = new byte[width*height*3];
        
        switch (format)
        {
            case IDX8:
            case IDXA: 
                for (row=height-1; row>=0; row--)
                {
                    for (col=0; col<width; col++) 
                    {                  	
                        formattedImage[index++] = colourTable[indexedImage[row][col]][2];
                        formattedImage[index++] = colourTable[indexedImage[row][col]][1];
                        formattedImage[index++] = colourTable[indexedImage[row][col]][0];                    	
                    }
                }
                break;
            case RGB5: 
            case RGB8: 
            case RGBA: 
                for (row=height-1; row>=0; row--)
                {
                    for (col=0; col<width; col++)
                    {
                        formattedImage[index++] = colorImage[row][col][2];
                        formattedImage[index++] = colorImage[row][col][1];
                        formattedImage[index++] = colorImage[row][col][0];
                    }
                }
                break;
        }

		int columns = (width+blockWidth-1)/blockWidth;
		int rows = (height+blockHeight-1)/blockHeight;
		
		ArrayList blocks = new ArrayList(rows*columns);
		byte[] blockData = new byte[blockHeight*blockWidth*3];
		
		for (int i=0; i<rows; i++)
		{
			for (int j=0; j<columns; j++)
			{
				int xOffset = j*blockWidth;
				int yOffset = i*blockHeight;
				
				int xSpan = (width-xOffset > blockWidth) ? blockWidth : width-xOffset;
				int ySpan = (height-yOffset > blockHeight) ? blockHeight : height-yOffset;
				int offset = 0;
				
				int idx;
				
				for (int k=0; k<ySpan; k++) 
				{
					for (int l=0; l<xSpan; l++, offset+=3) 
					{
						idx = (yOffset+k)*(width*3)+(xOffset+l)*3;
						
						blockData[offset] = formattedImage[idx];					
						blockData[offset+1] = formattedImage[idx+1];					
						blockData[offset+2] = formattedImage[idx+2];					
					}
				}
				
				blocks.add(new FSImageBlock(xSpan, ySpan, zip(blockData, offset)));
			}
		}
		return blocks;
	}
	
	private static byte[] zip(byte[] image, int length)
	{
		Deflater deflater = new Deflater();
		deflater.setInput(image, 0, length);
		deflater.finish();

		byte[] compressedData = new byte[image.length];
		int bytesCompressed = deflater.deflate(compressedData);

		byte[] newData = new byte[bytesCompressed];

		for (int i = 0; i < bytesCompressed; i++)
			newData[i] = compressedData[i];

		return newData;
	}
    
    /**
     * Sets the image data for an indexed image.
     * 
     * @param encoding the format for the encoded image, either IDX8, or IDXA.
     * @param imageWidth the width of the image in pixels.
     * @param imageHeight the height of the image in pixels.
     * @param table a two-dimensional array containing four bytes for each colour, [n][4]. For each 
     * entry [n][3] contains alpha, [n][2] red, [n][1] green and [n][0] blue.
     * @param image a two-dimensional array of bytes, [imageHeight][imageWidth] containing
     * the index into the colour table for each pixel in the image.
     */
    public void setIndexedImage(int encoding, int imageWidth, int imageHeight, byte[][] table, byte[][] image)
    {
        if (encoding != IDX8 && encoding != IDXA)
            throw new IllegalArgumentException("Indexed images must have a format of IDX8 or IDXA");
            
        format = encoding;
        width = imageWidth;
        height = imageHeight;
        
        colourTable = new byte[table.length][4];

        for (int i=0; i<table.length; i++)
        {
            for (int j=0; j<4; j++)
                colourTable[i][j] = table[i][j];    
        }

        indexedImage = new byte[height][width];

        for (int h=0; h<height; h++)
        {
            for (int w=0; w<width; w++)
                indexedImage[h][w] = image[h][w];    
        }
    }

    /**
     * Sets the image data for a true colour image.
     * 
     * @param encoding the format for the encoded image, either RGB5, RGB8 or RGBA.
     * @param imageWidth the width of the image in pixels.
     * @param imageHeight the height of the image in pixels.
     * @param image a three-dimensional array of bytes, [imageHeight][imageWidth][4] containing
     * the colours for each pixel in the image. Four bytes are used to specify the colour. The
     * order of the colour information from the most significant [3] to the least [0] is alpha, red, 
     * green and blue.   
     */
    public void setColorImage(int encoding, int imageWidth, int imageHeight, byte[][][] image)
    {
        if (encoding != RGB5 && encoding != RGB8 && encoding != RGBA)
            throw new IllegalArgumentException("True colour images must has a format of RGB5, RGB8 or RGBA");
            
        format = encoding;
        width = imageWidth;
        height = imageHeight;
        
        colorImage = new byte[height][width][4];

        for (int h=0; h<height; h++)
        {
            for (int w=0; w<width; w++)
            {
                colorImage[h][w][0] = image[h][w][0];    
                colorImage[h][w][1] = image[h][w][1];    
                colorImage[h][w][2] = image[h][w][2];    
                colorImage[h][w][3] = image[h][w][3];    
            }
        }
    }

    /**
     * Sets the image data for a JPEG encoded image.
     * 
     * @param imageWidth the width of the image in pixels.
     * @param imageHeight the height of the image in pixels.
     * @param image an array of bytes containing the JPEG encoded image.   
     */
    public void setJPEGImage(int imageWidth, int imageHeight, byte[] image)
    {
        format = JPEG;
        width = imageWidth;
        height = imageHeight;
        
        jpegImage = new byte[image.length];
        
        System.arraycopy(image, 0, jpegImage, 0, image.length);
    }

    /**
     * Generates an object used to define an image in a Flash file. The class of the object returned is 
     * determined by the format of the image data:
     *
     * <table>
     * <tr><th>Class</th><th>Generated when...</th></tr>
     *
     * <tr>
     * <td>FSDefineJPEGImage2</td>
     * <td>The image format is JPEG and the level of transparency is not defined.</td>
     * </tr>
     *
     * <tr>
     * <td>FSDefineImage</td>
     * <td>The image format is IDX8, RGB5 or RGB8.</td>
     * </tr>
     *
     * <tr>
     * <td>FSDefineImage2</td>
     * <td>The image format is IDXA, RGBA.</td>
     * </tr>
     *
     * </table>
     *
     * @param identifier an unique identifier that is used to reference the image definition in a Flash movie.
     */
    public FSDefineObject defineImage(int identifier)
    {
        FSDefineObject object = null;
        
        switch (format)
        {
            case JPEG: object = new FSDefineJPEGImage2(identifier, jpegImage); break;
            case IDX8: object = new FSDefineImage(identifier, width, height, colourTable.length, zip(getImage(), colourTable, false)); break;
            case IDXA: object = new FSDefineImage2(identifier, width, height, colourTable.length, zip(getImage(), colourTable, true)); break;
            case RGB5: object = new FSDefineImage(identifier, width, height, zip(getImage()), 16); break;
            case RGB8: object = new FSDefineImage(identifier, width, height, zip(getImage()), 24); break;
            case RGBA: object = new FSDefineImage2(identifier, width, height, zip(getImage())); break;
        }
        return object;
    }

    private byte[] getImage()
    {
        int row = 0;
        int col = 0;
        int index = 0;
        int scanLength = 0;
        byte[] formattedImage = null;
        
        switch (format)
        {
            case IDX8:
            case IDXA: 
                scanLength = (width + 3) & ~3;
                formattedImage = new byte[scanLength*height];
                for (row=0; row<height; row++)
                {
                    for (col=0; col<width; col++)
                        formattedImage[index++] = indexedImage[row][col];
                    
                    for (; col<scanLength; col++)
                        formattedImage[index++] = 0;
                }
                break;
            case RGB5: 
                scanLength = width + (width & 1);
                formattedImage = new byte[scanLength*height*2];
                for (row=0; row<height; row++)
                {
                    for (col=0; col<width; col++)
                    {
                        int red = (colorImage[row][col][0] & 0xF8) << 7;
                        int green = (colorImage[row][col][1] & 0xF8) << 2;
                        int blue = (colorImage[row][col][2] & 0xF8) >> 3;
                        int colour = (red | green | blue) & 0x7FFF;
                        
                        formattedImage[index++] = (byte) (colour >> 8);
                        formattedImage[index++] = (byte) colour;
                    }

                    for (; col<scanLength; col++)
                    {
                        formattedImage[index++] = 0;
                        formattedImage[index++] = 0;
                    }
                }
                break;
            case RGB8: 
                formattedImage = new byte[width*height*4];
                for (row=0; row<height; row++)
                {
                    for (col=0; col<width; col++)
                    {
                        formattedImage[index++] = (byte)0xFF;
                        formattedImage[index++] = colorImage[row][col][0];
                        formattedImage[index++] = colorImage[row][col][1];
                        formattedImage[index++] = colorImage[row][col][2];
                    }
                }
                break;
            case RGBA: 
                formattedImage = new byte[width*height*4];
                for (row=0; row<height; row++)
                {
                    for (col=0; col<width; col++)
                    {
                        int alpha = colorImage[row][col][3] & 0xFF;
                        int red = ((colorImage[row][col][0] & 0xFF) * alpha) / 255;
                        int green = ((colorImage[row][col][1] & 0xFF) * alpha) / 255;
                        int blue = ((colorImage[row][col][2] & 0xFf) * alpha) / 255;
                            
                        formattedImage[index++] = (byte)alpha;
                        formattedImage[index++] = (byte)red;
                        formattedImage[index++] = (byte)green;
                        formattedImage[index++] = (byte)blue;
                    }
                }
                break;
        }
        return formattedImage;
    }
    
    /**
     * Generates the shape definition object that is required to display an image in a Flash movie.
     * The shape is generated with a single fill style (FSBitmapFill object). The origin of the shape
     * is specified relative to the top left corner of the image.
     *
     * The borderStyle argument specifies a border that will be drawn around the image. The style
     * may be set to null is no border is drawn.
     * 
     * @param shapeIdentifier an unique identifier that is used to reference the shape definition in a  
     * Flash movie.
     *
     * @param imageIdentifier the unique identifier of the image generated using the defineImage() method.
     * 
     * @param xOrigin the offset in pixels along the x-axis, relative to the top left corner of 
     * the image, where the origin (0,0) of the shape will be located.
     * 
     * @param yOrigin the offset in pixels along the y-axis, relative to the top left corner of 
     * the image, where the origin (0,0) of the shape will be located.
     *
     * @param borderStyle the style drawn around the border of the image. May be null if no 
     * border is drawn.
     */
    public FSDefineShape3 defineEnclosingShape(int shapeIdentifier, int imageIdentifier, int xOrigin, int yOrigin, FSSolidLine borderStyle)
    {
        int lineWidth = 0;
        
        if (borderStyle != null)
            lineWidth = borderStyle.getWidth() / 2;
        
        FSBounds bounds = new FSBounds(-xOrigin*20-lineWidth, -yOrigin*20-lineWidth, 
            (width-xOrigin)*20+lineWidth, (height-yOrigin)*20+lineWidth);

        FSShape shape = new FSShape();
    
        shape.add(new FSShapeStyle((borderStyle != null) ? 1 : 0, 1, 0, -xOrigin*20, -yOrigin*20));
        shape.add(new FSLine(width*20, 0));
        shape.add(new FSLine(0, height*20));
        shape.add(new FSLine(-width*20, 0));
        shape.add(new FSLine(0, -height*20));
        
        FSDefineShape3 definition = new FSDefineShape3(shapeIdentifier, bounds, new ArrayList(), new ArrayList(), shape);
        FSCoordTransform transform = new FSCoordTransform(-xOrigin*20, -yOrigin*20, 20.0, 20.0);
    
        if (borderStyle != null)
            definition.add(borderStyle);
        
        definition.add(new FSBitmapFill(FSFillStyle.Clipped, imageIdentifier, transform));

        return definition;
    }
    
    private boolean jpegInfo()
    {
        FSCoder coder = new FSCoder(FSCoder.BIG_ENDIAN, jpegImage);
                 
        if (coder.readWord(2, false) != 0xffd8)
            return false;

        while (true) 
        {
            int marker = coder.readWord(2, false);
            int size = coder.readWord(2, false);
            
            if ((marker & 0xff00) != 0xff00) 
                return false;
            
            if (marker >= 0xffc0 && marker <= 0xffcf && marker != 0xffc4 && marker != 0xffc8) 
            {
                coder.readWord(1, false);
                height = coder.readWord(2, false);
                width = coder.readWord(2, false);
                return true;
            } 
            else 
            {
                coder.adjustPointer((size - 2) << 3);
            }
        }
    }
    
    private void decodeJPEG(byte[] bytes) throws DataFormatException
    {
        format = JPEG;
        jpegImage = bytes;

        if (jpegInfo() == false)
            throw new DataFormatException();
    }

    private void decodeBMP(byte[] bytes) throws DataFormatException
    {
        FSCoder coder = new FSCoder(FSCoder.LITTLE_ENDIAN, bytes);
        
        for (int i=0; i<2; i++)
        {
            if (coder.readWord(1, false) != bmpSignature[i])
                throw new DataFormatException("Not a valid BMP file");
        }

        coder.readWord(4, false); // fileSize
        coder.readWord(4, false); // reserved
        int offset = coder.readWord(4, false);
        int headerSize = coder.readWord(4, false);
        
        int bitsPerPixel = 0;
        int coloursUsed = 0;

        switch (headerSize)
        {
            case 12:
                width = coder.readWord(2, false);
                height = coder.readWord(2, false);
                coder.readWord(2, false); // bitPlanes
                bitsPerPixel = coder.readWord(2, false);
                break;
            case 40:
                width = coder.readWord(4, false);
                height = coder.readWord(4, false);
                coder.readWord(2, false); // bitPlanes
                bitsPerPixel = coder.readWord(2, false);
                attributes[COMPRESSION_METHOD] = coder.readWord(4, false);
                coder.readWord(4, false); //imageSize
                coder.readWord(4, false); // horizontalResolution
                coder.readWord(4, false); // verticalResolution
                coloursUsed = coder.readWord(4, false);
                coder.readWord(4, false); // importantColours
                break;
            default:
                break;
        }
        
        if (attributes[COMPRESSION_METHOD] == BI_BITFIELDS)
        {
            attributes[RED_MASK] = coder.readWord(4, false);
            attributes[GREEN_MASK] = coder.readWord(4, false);
            attributes[BLUE_MASK] = coder.readWord(4, false);
        }
        
        switch (bitsPerPixel)
        {
            case 1: format = IDX8; attributes[BIT_DEPTH] = 1; attributes[COLOUR_COMPONENTS] = 1; break;
            case 2: format = IDX8; attributes[BIT_DEPTH] = 2; attributes[COLOUR_COMPONENTS] = 1; break;
            case 4: format = IDX8; attributes[BIT_DEPTH] = 4; attributes[COLOUR_COMPONENTS] = 1; break;
            case 8: format = IDX8; attributes[BIT_DEPTH] = 8; attributes[COLOUR_COMPONENTS] = 1; break;
            case 16: format = RGB5; attributes[BIT_DEPTH] = 5; attributes[COLOUR_COMPONENTS] = 3; break;
            case 24: format = RGB8; attributes[BIT_DEPTH] = 8; attributes[COLOUR_COMPONENTS] = 3; break;
            case 32: format = RGBA; attributes[BIT_DEPTH] = 8; attributes[COLOUR_COMPONENTS] = 4; break;
        }
        
        if (format == IDX8) 
        {
            coloursUsed = 1 << bitsPerPixel;
            colourTable = new byte[coloursUsed][4];
            indexedImage = new byte[height][width];

            if (headerSize == 12)
            {
                for (int i=0; i < coloursUsed; i++) 
                {
                    colourTable[i][3] = (byte)0xFF;
                    colourTable[i][2] = (byte)coder.readWord(1, false);
                    colourTable[i][1] = (byte)coder.readWord(1, false);
                    colourTable[i][0] = (byte)coder.readWord(1, false);
                }
            }
            else
            {
                for (int i=0; i < coloursUsed; i++)
                {
                    colourTable[i][0] = (byte)coder.readWord(1, false);
                    colourTable[i][1] = (byte)coder.readWord(1, false);
                    colourTable[i][2] = (byte)coder.readWord(1, false);
                    colourTable[i][3] = (byte)(coder.readWord(1, false) | 0xFF);
                }
            }
                
            coder.setPointer(offset<<3);

            switch (attributes[COMPRESSION_METHOD])
            {
                case BI_RGB:  decodeIDX8(coder); break;
                case BI_RLE8: decodeRLE8(coder); break;
                case BI_RLE4: decodeRLE4(coder); break;
            }
        }
        else
        {
            colorImage = new byte[height][width][4];

            coder.setPointer(offset<<3);

            switch (format)
            {
                case RGB5: decodeRGB5(coder); break;
                case RGB8: decodeRGB8(coder); break;
                case RGBA: decodeRGBA(coder); break;
            }
        }
    }

    private void decodeIDX8(FSCoder coder)
    {
        int h = 0;
        int w = 0;
        int bitsRead = 0;
        
        for (h=height-1; h>0; h--)
        {
            for (w=0, bitsRead=0; w<width; w++)
            {
                indexedImage[h][w] = (byte)coder.readBits(attributes[BIT_DEPTH], false);
                bitsRead += attributes[BIT_DEPTH];
            }
            if (bitsRead % 32 > 0)
                coder.adjustPointer(32 - (bitsRead % 32));
        }
    }

    private void decodeRLE4(FSCoder coder)
    {
        int row = height-1;
        int col = 0;
        
        boolean containsMorePixels = true;

        while (containsMorePixels) 
        {       
            int count = coder.readWord(1, false);
        
            if (count == 0)
            {
                int code = coder.readWord(1, false);
                
                switch (code)
                {
                    case 0: 
                        col = 0; 
                        row--; 
                        break;
                    case 1: 
                        containsMorePixels = false; 
                        break;
                    case 2: 
                        col += coder.readWord(2, false);
                        row -= coder.readWord(2, false);
                    default:
                        for (int i=0; i<code; i+=2)
                        {
                            indexedImage[row][col++] = (byte) coder.readBits(4, false);
                            indexedImage[row][col++] = (byte) coder.readBits(4, false);
                        }
                        
                        if ((code & 2) == 2) coder.readWord(1, false);
                        break;
                }
            }
            else
            {
                byte indexA = (byte)coder.readBits(4, false);
                byte indexB = (byte)coder.readBits(4, false);
                
                for (int i=0; i<count && col < width; i++) 
                    indexedImage[row][col++] = (i % 2 > 0) ? indexB : indexA;
            }
        }
    }
    
    private void decodeRLE8(FSCoder coder)
    {
        int row = height-1;
        int col = 0;
        
        boolean containsMorePixels = true;

        while (containsMorePixels) 
        {       
            int count = coder.readWord(1, false);
        
            if (count == 0)
            {
                int code = coder.readWord(1, false);
                
                switch (code)
                {
                    case 0: 
                        col = 0; 
                        row--; 
                        break;
                    case 1: 
                        containsMorePixels = false; 
                        break;
                    case 2: 
                        col += coder.readWord(2, false);
                        row -= coder.readWord(2, false);
                    default:
                        for (int i=0; i<code; i++)
                            indexedImage[row][col++] = (byte) coder.readWord(1, false);
                        
                        if ((code & 1) == 1) coder.readWord(1, false);
                        break;
                }
            }
            else
            {
                byte index = (byte)coder.readWord(1, false);
                
                for (int i=0; i<count; i++) 
                    indexedImage[row][col++] = index;
            }
        }
    }
    
    private void decodeRGB5(FSCoder coder)
    {
        int h = 0;
        int w = 0;
        int bitsRead = 0;
        
        if (attributes[COMPRESSION_METHOD] == BI_RGB)
        {
            for (h=height-1; h>0; h--)
            {
                for (w=0, bitsRead=0; w<width; w++)
                {                
                    int colour = coder.readWord(2, false) & 0xFFFF;
                    
                    colorImage[h][w][0] = (byte)((colour & 0x7C00) >> 7);
                    colorImage[h][w][1] = (byte)((colour & 0x03E0) >> 2);
                    colorImage[h][w][2] = (byte)((colour & 0x001F) << 3);
                    colorImage[h][w][3] = (byte)0xFF;

                    bitsRead += 16;
                }
                if (bitsRead % 32 > 0)
                    coder.adjustPointer(32 - (bitsRead % 32));
            }
        }
        else
        {
            for (h=height-1; h>0; h--)
            {
                for (w=0, bitsRead=0; w<width; w++)
                {
                    int colour = coder.readWord(2, false) & 0xFFFF;
                    
                    if (attributes[RED_MASK] == 0x7C00 && attributes[GREEN_MASK] == 0x03E0 && attributes[BLUE_MASK] == 0x001F)
                    {
                        colorImage[h][w][0] = (byte)((colour & 0x7C00) >> 7);
                        colorImage[h][w][1] = (byte)((colour & 0x03E0) >> 2);
                        colorImage[h][w][2] = (byte)((colour & 0x001F) << 3);
                        colorImage[h][w][3] = (byte)0xFF;
                    }
                    else if (attributes[RED_MASK] == 0xF800 && attributes[GREEN_MASK] == 0x07E0 && attributes[BLUE_MASK] == 0x001F)
                    {
                        colorImage[h][w][0] = (byte)((colour & 0xF800) >> 8);
                        colorImage[h][w][1] = (byte)((colour & 0x07E0) >> 3);
                        colorImage[h][w][2] = (byte)((colour & 0x001F) << 3);
                        colorImage[h][w][3] = (byte)0xFF;
                    }
                    bitsRead += 16;
                }
                if (bitsRead % 32 > 0)
                    coder.adjustPointer(32 - (bitsRead % 32));
            }
        }
        
    }

    private void decodeRGB8(FSCoder coder)
    {
        int h = 0;
        int w = 0;
        int bitsRead = 0;
        
        for (h=height-1; h>0; h--)
        {
            for (w=0, bitsRead=0; w<width; w++)
            {
                colorImage[h][w][0] = (byte)coder.readBits(attributes[BIT_DEPTH], false);
                colorImage[h][w][1] = (byte)coder.readBits(attributes[BIT_DEPTH], false);
                colorImage[h][w][2] = (byte)coder.readBits(attributes[BIT_DEPTH], false);
                colorImage[h][w][3] = (byte)0xFF;
                
                bitsRead += 24;
            }
            if (bitsRead % 32 > 0)
                coder.adjustPointer(32 - (bitsRead % 32));
        }
    }

    private void decodeRGBA(FSCoder coder)
    {
        int h = 0;
        int w = 0;
        
        for (h=height-1; h>0; h--)
        {
            for (w=0; w<width; w++)
            {
                colorImage[h][w][2] = (byte)coder.readWord(1, false);
                colorImage[h][w][1] = (byte)coder.readWord(1, false);
                colorImage[h][w][0] = (byte)coder.readWord(1, false);
                colorImage[h][w][3] = (byte)coder.readWord(1, false);
                colorImage[h][w][3] = (byte)0xFF;
            }
        }
    }

    private void decodePNG(byte[] bytes) throws DataFormatException
    {
        FSCoder coder = new FSCoder(FSCoder.BIG_ENDIAN, bytes);

        int length = 0;
        int chunkType = 0;
        boolean moreChunks = true;

        for (int i=0; i<8; i++)
        {
            if (coder.readWord(1, false) != pngSignature[i])
                throw new DataFormatException("Not a valid PNG file");
        }
        
        while (moreChunks)
        {
            length = coder.readWord(4, false);
            chunkType = coder.readWord(4, false);
            
            int current = coder.getPointer();
            int next = current + ((length+4) << 3);
            
            switch (chunkType)
            {
                case IHDR: decodeIHDR(coder, length); break;
                case PLTE: decodePLTE(coder, length); break;
                case tRNS: decodeTRNS(coder, length); break;
                case IDAT: decodeIDAT(coder, length); break;
                case IEND: moreChunks = false; coder.adjustPointer(32); break;
                default:
                    coder.adjustPointer((length+4) << 3);
                    break;
            }         
            length += 4; // include CRC at end of chunk
/*
            int bytesRead = (coder.getPointer() - current) >> 3;
            
            byte[] chars = new byte[4];
            
            chars[3] = (byte)chunkType;
            chars[2] = (byte)(chunkType >> 8);
            chars[1] = (byte)(chunkType >> 16);
            chars[0] = (byte)(chunkType >> 24);
            
            String chunk = new String(chars);
            
            if (bytesRead < length)
                System.err.println(chunk + " chunk underflowed by " + (length - bytesRead) + " bytes.");
            else if (bytesRead > length)
                System.err.println(chunk + " chunk overflowed by " + (bytesRead - length) + " bytes.");
*/
            coder.setPointer(next);
            
            if (coder.eof())
                moreChunks = false;
        }    
        decodeImage();
    }

    private void decodeIHDR(FSCoder coder, int length)
    {
        width = coder.readWord(4, false);
        height = coder.readWord(4, false);
        attributes[BIT_DEPTH] = coder.readWord(1, false);
        attributes[COLOUR_TYPE] = coder.readWord(1, false);
        attributes[COMPRESSION_METHOD] = coder.readWord(1, false);
        attributes[FILTER_METHOD] = coder.readWord(1, false);
        attributes[INTERLACE_METHOD] = coder.readWord(1, false);
        
        coder.readWord(4, false); // crc

        switch (attributes[COLOUR_TYPE])
        {
            case GREYSCALE: format = (attributes[TRANSPARENT_GREY] != -1) ? RGBA : RGB8; attributes[COLOUR_COMPONENTS] = 1; break;
            case TRUE_COLOUR: format = (attributes[TRANSPARENT_RED] != -1) ? RGBA : RGB8; attributes[COLOUR_COMPONENTS] = 3; break;
            case INDEXED_COLOUR: format = IDX8; attributes[COLOUR_COMPONENTS] = 1; break;
            case ALPHA_GREYSCALE: format = RGBA; attributes[COLOUR_COMPONENTS] = 2; break;
            case ALPHA_TRUECOLOUR: format = RGBA; attributes[COLOUR_COMPONENTS] = 4; break;
        }  
    }

    private void decodePLTE(FSCoder coder, int length)
    {
        if (attributes[COLOUR_TYPE] == 3)
        {
            int paletteSize = length / 3;
            
            colourTable = new byte[paletteSize][4];
            
            for (int i=0; i<paletteSize; i++)
            {
                colourTable[i][3] = (byte)0xFF;
                colourTable[i][2] = (byte)coder.readWord(1, false);
                colourTable[i][1] = (byte)coder.readWord(1, false);
                colourTable[i][0] = (byte)coder.readWord(1, false);
            }
        }
        else
        {
            coder.adjustPointer(length << 3);
        }        
        coder.readWord(4, false); // crc
    }

    private void decodeTRNS(FSCoder coder, int length)
    {
        switch(attributes[COLOUR_TYPE])
        {
            case GREYSCALE:
                attributes[TRANSPARENT_GREY] = coder.readWord(2, false);
                break;
            case TRUE_COLOUR:
                attributes[TRANSPARENT_RED] = coder.readWord(2, false);
                attributes[TRANSPARENT_GREEN] = coder.readWord(2, false);
                attributes[TRANSPARENT_BLUE] = coder.readWord(2, false);
                break;
            case INDEXED_COLOUR:
                format = IDXA;
                for (int i=0; i<length; i++) 
                {
                    colourTable[i][3] = (byte)coder.readWord(1, false);

                    if (colourTable[i][3] == 0) {
                        colourTable[i][0] = 0;
                        colourTable[i][1] = 0;
                        colourTable[i][2] = 0;
                    }
                }
                break;
            default:
                break;
        }
        coder.readWord(4, false); // crc
    }

    private void decodeIDAT(FSCoder coder, int length)
    {
        int currentLength = chunkData.length;
        int newLength = currentLength + length;
        
        byte[] data = new byte[newLength];
        
        System.arraycopy(chunkData, 0, data, 0, currentLength);

        for (int i=currentLength; i<newLength; i++)
            data[i] = (byte)coder.readWord(1, false);
            
        chunkData = data;
        
        coder.readWord(4, false); // crc
    }

    private void decodeImage() throws DataFormatException
    {
        if (format == RGB8 && attributes[BIT_DEPTH] <= 5)
            format = RGB5;

        if (format == RGB5 || format == RGB8 || format == RGBA)
            colorImage = new byte[height][width][4];
            
        if (format == IDX8 || format == IDXA)
            indexedImage = new byte[height][width];
            
        byte[] encodedImage = unzip(chunkData);
        
        int bitsPerPixel = attributes[BIT_DEPTH]*attributes[COLOUR_COMPONENTS];
        int bitsPerRow = width * bitsPerPixel;
        int rowWidth = (bitsPerRow % 8 > 0) ? (bitsPerRow/8)+1 : (bitsPerRow/8);
        int bytesPerPixel = (bitsPerPixel<8) ? 1 : bitsPerPixel/8;
        
        byte[] current = new byte[rowWidth];
        byte[] previous = new byte[rowWidth];
        
        for (int i=0; i<rowWidth; i++)
            previous[i] = (byte)0;
            
        int rowStart = 0;
        int rowInc = 0;
        int colStart = 0;
        int colInc = 0;
        
        int imageIndex = 0;
        int pixelCount = 0;
        
        int row = 0;
        int col = 0;
        int filter = 0;
        
        int scanBits = 0;
        int scanLength = 0;
        
        int numberOfPasses = (attributes[INTERLACE_METHOD] == 1) ? 7 : 1;

        int xc = 0;
        int xp = 0;

        for (int pass=0; pass<numberOfPasses; pass++)
        {
            rowStart = (attributes[INTERLACE_METHOD] == 1) ? startRow[pass] : 0;
            rowInc = (attributes[INTERLACE_METHOD] == 1) ? rowIncrement[pass] : 1;
            
            colStart = (attributes[INTERLACE_METHOD] == 1) ? startColumn[pass] : 0;
            colInc = (attributes[INTERLACE_METHOD] == 1) ? columnIncrement[pass] : 1;
            
            for (row=rowStart; row<height && imageIndex<encodedImage.length; row+=rowInc)
            {
                for (col=colStart, pixelCount=0, scanBits=0; col<width; pixelCount++, col += colInc, scanBits+=bitsPerPixel);

                scanLength = (scanBits%8 > 0) ? (scanBits/8)+1 : (scanBits/8);

                filter = encodedImage[imageIndex++];
                              
                for (int i=0; i<scanLength; i++, imageIndex++)
                    current[i] = (imageIndex < encodedImage.length) ? encodedImage[imageIndex] : previous[i];

                switch (filter) 
                {
                    case NO_FILTER:
                        break;
                    case SUB_FILTER:
                        for (xc = bytesPerPixel, xp = 0; xc < scanLength; xc++, xp++)
                            current[xc] = (byte)(current[xc] + current[xp]);
                        break;
                    case UP_FILTER:
                        for (xc = 0; xc < scanLength; xc++)
                            current[xc] = (byte)(current[xc] + previous[xc]);
                        break;
                    case AVG_FILTER:
                        for (xc = 0; xc < bytesPerPixel; xc++)
                            current[xc] = (byte)(current[xc] + (0 + (0xFF & previous[xc])) / 2);
                        
                        for (xc = bytesPerPixel, xp = 0; xc < scanLength; xc++, xp++)
                            current[xc] = (byte)(current[xc] + ((0xFF & current[xp]) + (0xFF & previous[xc])) / 2);
                        break;
                    case PAETH_FILTER:
                        for (xc = 0; xc < bytesPerPixel; xc++)
                            current[xc] = (byte)(current[xc] + Paeth((byte)0, previous[xc], (byte)0));
                        
                        for (xc = bytesPerPixel, xp = 0; xc < scanLength; xc++, xp++)
                            current[xc] = (byte)(current[xc] + Paeth(current[xp], previous[xc], previous[xp]));
                        break;
                }
        
                System.arraycopy(current, 0, previous, 0, scanLength);
                        
                FSCoder coder = new FSCoder(FSCoder.BIG_ENDIAN, current);
                
                for (col=colStart; col<width; col+=colInc)
                {
                    switch (attributes[COLOUR_TYPE])
                    {
                        case GREYSCALE: decodeGreyscale(coder, row, col); break;
                        case TRUE_COLOUR: decodeTrueColour(coder, row, col); break;
                        case INDEXED_COLOUR: decodeIndexedColour(coder, row, col); break;
                        case ALPHA_GREYSCALE: decodeAlphaGreyscale(coder, row, col); break;
                        case ALPHA_TRUECOLOUR: decodeAlphaTrueColour(coder, row, col); break;
                    }        
                }
            }
        }
    }

    private int Paeth(byte L, byte u, byte nw)
    {
        int a = 0xFF & L;
        int b = 0xFF & u; 
        int c = 0xFF & nw; 
        int p = a + b - c;
        int pa = p - a; if (pa < 0) pa = -pa;
        int pb = p - b; if (pb < 0) pb = -pb; 
        int pc = p - c; if (pc < 0) pc = -pc; 
        if (pa <= pb && pa <= pc) return a;
        if (pb <= pc) return b;
        return c;
    }

    private void decodeGreyscale(FSCoder coder, int row, int col)
    {
        int pixel = 0;
        byte colour = 0;
        
        switch (attributes[BIT_DEPTH])
        {
            case 1:  pixel = coder.readBits(1, false); colour = (byte) monochrome[pixel]; break;
            case 2:  pixel = coder.readBits(2, false); colour = (byte) greyscale2[pixel]; break;
            case 4:  pixel = coder.readBits(4, false); colour = (byte) greyscale4[pixel]; break;
            case 8:  pixel = coder.readWord(1, false); colour = (byte) pixel; break;
            case 16: pixel = coder.readWord(2, false); colour = (byte) (pixel >> 8); break;
        }                    

        colorImage[row][col][0] = colour;
        colorImage[row][col][1] = colour;
        colorImage[row][col][2] = colour;
        colorImage[row][col][3] = (byte)attributes[TRANSPARENT_GREY];
    }
    
    private void decodeTrueColour(FSCoder coder, int row, int col)
    {
        int pixel = 0;
        byte colour = 0;
        
        for (int i=0; i<attributes[COLOUR_COMPONENTS]; i++)
        {
            switch (attributes[BIT_DEPTH])
            {
                case 8:  pixel = coder.readWord(1, false); colour = (byte) pixel; break;
                case 16: pixel = coder.readWord(2, false); colour = (byte) (pixel >> 8); break;
            }                    
            colorImage[row][col][i] = colour;
        }
        colorImage[row][col][3] = (byte)attributes[TRANSPARENT_RED];
    }
    
    private void decodeIndexedColour(FSCoder coder, int row, int col)
    {
        int index = 0;
        
        switch (attributes[BIT_DEPTH])
        {
            case 1:  index = coder.readBits(1, false); break;
            case 2:  index = coder.readBits(2, false); break;
            case 4:  index = coder.readBits(4, false); break;
            case 8:  index = coder.readWord(1, false); break;
            case 16: index = coder.readWord(2, false); break;
        }                    
        indexedImage[row][col] = (byte)index;
    }
    
    private void decodeAlphaGreyscale(FSCoder coder, int row, int col)
    {
        int pixel = 0;
        byte colour = 0;
        int alpha = 0;
        
        switch (attributes[BIT_DEPTH])
        {
            case 1:  pixel = coder.readBits(1, false); colour = (byte) monochrome[pixel]; alpha = coder.readBits(1, false); break;
            case 2:  pixel = coder.readBits(2, false); colour = (byte) greyscale2[pixel]; alpha = coder.readBits(2, false); break;
            case 4:  pixel = coder.readBits(4, false); colour = (byte) greyscale4[pixel]; alpha = coder.readBits(4, false); break;
            case 8:  pixel = coder.readWord(1, false); colour = (byte) pixel; alpha = coder.readWord(1, false); break;
            case 16: pixel = coder.readWord(2, false); colour = (byte) (pixel >> 8); alpha = coder.readWord(2, false) >> 8; break;
        }                    

        colorImage[row][col][0] = colour;
        colorImage[row][col][1] = colour;
        colorImage[row][col][2] = colour;
        colorImage[row][col][3] = (byte) alpha;
    }
    
    private void decodeAlphaTrueColour(FSCoder coder, int row, int col)
    {
        int pixel = 0;
        byte colour = 0;
        
        for (int i=0; i<attributes[COLOUR_COMPONENTS]; i++)
        {
            switch (attributes[BIT_DEPTH])
            {
                case 8:  pixel = coder.readWord(1, false); colour = (byte) pixel; break;
                case 16: pixel = coder.readWord(2, false); colour = (byte) (pixel >> 8); break;
            }                    
            colorImage[row][col][i] = colour;
        }
    }
    
    private byte[] dataFromFile(String filename) throws FileNotFoundException, IOException
    {
        File aFile = new File(filename);
        FileInputStream imageContents = null;
        
        byte[] bytes = new byte[(int)aFile.length()];
        
        imageContents = new FileInputStream(aFile);            
        imageContents.read(bytes);
        imageContents.close();

        return bytes;
    }
    
    private byte[] unzip(byte[] bytes) throws DataFormatException
    {
        byte[] data = new byte[width*height*8];
        int count = 0;
        
        Inflater inflater = new Inflater();
        inflater.setInput(bytes);
        count = inflater.inflate(data);
        
        byte[] uncompressedData = new byte[count];
        
        System.arraycopy(data, 0, uncompressedData, 0, count);

        return uncompressedData;
    }
    
    private byte[] zip(byte[] image, byte[][] table, boolean hasAlpha)
    {
        int bytesPerColor = hasAlpha ? 4 : 3;
        
        byte[] combinedData = new byte[table.length*bytesPerColor+image.length];
        int index = 0;
        
        /*
         * Add colour table to start of combined data
         */
        for (int i=0; i<table.length; i++)
        {
            combinedData[index++] = table[i][2]; // R
            combinedData[index++] = table[i][1]; // G
            combinedData[index++] = table[i][0]; // B
            
            if (hasAlpha)
                combinedData[index++] = table[i][3]; // A
        }
        
        /*
         * Now add image to combined data
         */
        for (int i=0; i<image.length; i++)
            combinedData[index++] = image[i];

        return zip(combinedData);
    }

    private byte[] zip(byte[] image)
    {
        Deflater deflater = new Deflater();
        deflater.setInput(image);
        deflater.finish();
        
        byte[] compressedData = new byte[image.length*2];
        int bytesCompressed = deflater.deflate(compressedData);

        byte[] newData = new byte[bytesCompressed];
        
        for (int i=0; i<bytesCompressed; i++)
            newData[i] = compressedData[i];
            
        return newData;
    }

}
