/*
 * BMPCodec
 *
 * Copyright (c) 2000, 2001, 2002, 2003, 2004, 2005, 2006 Marco Schmidt.
 * All rights reserved.
 */

package net.sourceforge.jiu.codecs;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import net.sourceforge.jiu.codecs.ImageCodec;
import net.sourceforge.jiu.codecs.InvalidFileStructureException;
import net.sourceforge.jiu.codecs.UnsupportedTypeException;
import net.sourceforge.jiu.codecs.WrongFileFormatException;
import net.sourceforge.jiu.data.BilevelImage;
import net.sourceforge.jiu.data.ByteChannelImage;
import net.sourceforge.jiu.data.Gray8Image;
import net.sourceforge.jiu.data.MemoryBilevelImage;
import net.sourceforge.jiu.data.MemoryPaletted8Image;
import net.sourceforge.jiu.data.MemoryRGB24Image;
import net.sourceforge.jiu.data.Paletted8Image;
import net.sourceforge.jiu.data.PixelImage;
import net.sourceforge.jiu.data.Palette;
import net.sourceforge.jiu.data.RGB24Image;
import net.sourceforge.jiu.data.RGBIndex;
import net.sourceforge.jiu.ops.MissingParameterException;
import net.sourceforge.jiu.ops.OperationFailedException;
import net.sourceforge.jiu.util.ArrayConverter;

/**
 * A codec to read and write Windows BMP image files.
 * <p>
 * Typical file extensions are <code>.bmp</code> and <code>.rle</code>
 * (the latter is only used for compressed files).
 * <h3>Bounds</h3>
 * <p>
 * This codec supports the bounds concept for loading and saving.
 * </p>
 * <h3>Supported BMP types when loading</h3>
 * <ul>
 * <li>Bilevel, 1 bit per pixel, uncompressed.
 *  BMP supports palettes for bilevel images, but the content of that
 *  palette is ignored and 0 is considered black and 1 white.
 *  Any class implementing {@link net.sourceforge.jiu.data.BilevelImage}
 *  can be given to the codec and it will load the image to that object
 *  (if the image's resolution is sufficient).
 *  If no image object is given to the codec, a new
 *  {@link net.sourceforge.jiu.data.MemoryBilevelImage} will be created.</li>
 * <li>Paletted, 4 bits per pixel, uncompressed or RLE4 compression.
 *  Both types are loaded to a {@link net.sourceforge.jiu.data.Paletted8Image} object.
 *  This requires 50 % more space than is necessary, but there is
 *  no dedicated 4 bit image data class in JIU.</li>
 * <li>Paletted, 8 bits per pixel, uncompressed or RLE8 compression.
 *  Both types are loaded to a {@link net.sourceforge.jiu.data.Paletted8Image} object.</li>
 * <li>RGB truecolor, 24 bits per pixel, uncompressed.
 *  This is loaded to a {@link net.sourceforge.jiu.data.RGB24Image} object.</li>
 * </ul>
 * There is no support for 16 bpp images or BI_BITFIELDS compression (for lack of test files).
 * <p>
 * <h3>Supported JIU image data classes when saving to BMP</h3>
 * <ul>
 * <li>{@link net.sourceforge.jiu.data.BilevelImage} objects are stored as 1 bit per pixel BMP files.</li>
 * <li>{@link net.sourceforge.jiu.data.Gray8Image} and 
 *  {@link net.sourceforge.jiu.data.Paletted8Image} objects are stored as 
 *  paletted 8 bits per pixel files.
 *  It doesn't really matter how many entries the palette has, the BMP file's
 *  palette will always have 256 entries,  filled up with zero entries if necessary.</li>
 * <li>{@link net.sourceforge.jiu.data.RGB24Image} objects are stored as 24 bpp BMP files.</li>
 * </ul>
 * There is no support for compressed BMP files when saving.
 * <p>
 * <h3>I/O classes</h3>
 * BMPCodec works with all input and output classes supported by ImageCodec
 * ({@link java.io.InputStream}, {@link java.io.OutputStream}, 
 *  {@link java.io.DataInput}, {@link java.io.DataOutput},
 *  {@link java.io.RandomAccessFile}).
 * <h3>Problems</h3>
 * <p>The RLE-compressed BMP files that I could test this codec on seem to
 *  have an end-of-line code at the end of every line instead of relying 
 *  on the decoder to know when it has unpacked enough bytes for a line.
 *  Whenever this codec encounters an EOL symbol and has a current column
 *  value of <code>0</code>, the EOL is ignored.
 * <h3>Usage examples</h3>
 * Write an image to a BMP file.
 * <pre>
 * BMPCodec codec = new BMPCodec();
 * codec.setImage(image);
 * codec.setFile("out.bmp", CodecMode.SAVE);
 * codec.process();
 * codec.close();
 * </pre>
 * Read an image from a BMP file.
 * <pre>
 * BMPCodec codec = new BMPCodec();
 * codec.setFile("image.bmp", CodecMode.LOAD);
 * codec.process();
 * codec.close();
 * PixelImage image = codec.getImage();
 * </pre>
 * @author Marco Schmidt
 * @since 0.7.0
 */
public class BMPCodec extends ImageCodec
{
	private int colorDepth;
	private int compression;
	private int dataOffset;
	private int imageHeight;
	private int imageWidth;
	private DataInput in;
	private DataOutput out;
	private Palette palette;

	public String[] getFileExtensions()
	{
		return new String[] {".bmp", ".rle"};
	}

	public String getFormatName()
	{
		return "Windows BMP";
	}

	public String[] getMimeTypes()
	{
		return new String[] {"image/bmp", "image/x-ms-bmp"};
	}

	public boolean isLoadingSupported()
	{
		return true;
	}

	public boolean isSavingSupported()
	{
		return true;
	}

	private void load() throws
		MissingParameterException, 
		OperationFailedException, 
		UnsupportedTypeException,
		WrongFileFormatException
	{
		in = getInputAsDataInput();
		if (in == null)
		{
			throw new MissingParameterException("Input stream / random access file parameter missing.");
		}
		// now write the output stream
		try
		{
			loadHeader();
			loadStream();
		}
		catch (IOException ioe)
		{
			// wrap any I/O failures in an OperationFailedException
			throw new OperationFailedException("I/O failure: " + ioe.toString());
		}
	}

	private void loadCompressedPaletted4Stream() throws IOException
	{
		Paletted8Image image = (Paletted8Image)getImage();
		int imageBytesPerRow = imageWidth;
		int bytesPerRow = imageBytesPerRow;
		int mod = bytesPerRow % 4;
		if (mod != 0)
		{
			bytesPerRow += 4 - mod;
		}
		final int COLUMNS = getBoundsWidth();
		final int ROWS = imageHeight - getBoundsY1();
		final int X1 = getBoundsX1();
		int processedRows = 0;
		byte[] row = new byte[bytesPerRow];
		int x = 0;
		int y = imageHeight - 1;
		boolean endOfBitmap = false;
		boolean delta = false;
		int newX = 0;
		int newY = 0;
		while (processedRows < ROWS)
		{
			int v1 = in.readUnsignedByte();
			int v2 = in.readUnsignedByte();
			if (v1 == 0)
			{
				switch(v2)
				{
					case(0):
					{
						// end of line
						if (x != 0)
						{
							x = bytesPerRow;
						}
						break;
					}
					case(1):
					{
						// end of bitmap
						x = bytesPerRow;
						endOfBitmap = true;
						break;
					}
					case(2):
					{
						// delta
						delta = true;
						newX = x + in.readUnsignedByte();
						newY = y - in.readUnsignedByte();
						x = bytesPerRow;
						break;
					}
					default:
					{
						// copy the next v2 (3..255) samples from file to output
						// two samples are packed into one byte
						// if the number of bytes used to pack is not a multiple of 2,
						// an additional padding byte is in the stream and must be skipped
						boolean paddingByte = (((v2 + 1) / 2) % 2) != 0;
						while (v2 > 1)
						{
							int packed = in.readUnsignedByte();
							int sample1 = (packed >> 4) & 0x0f;
							int sample2 = packed & 0x0f;
							row[x++] = (byte)sample1;
							row[x++] = (byte)sample2;
							v2 -= 2;
						}
						if (v2 == 1)
						{
							int packed = in.readUnsignedByte();
							int sample = (packed >> 4) & 0x0f;
							row[x++] = (byte)sample;
						}
						if (paddingByte)
						{
							v2 = in.readUnsignedByte();
						}
						break;
					}
				}
			}
			else
			{
				// rle: replicate the two samples in v2 as many times as v1 says
				byte sample1 = (byte)((v2 >> 4) & 0x0f);
				byte sample2 = (byte)(v2 & 0x0f);
				while (v1 > 1)
				{
					row[x++] = sample1;
					row[x++] = sample2;
					v1 -= 2;
				}
				if (v1 == 1)
				{
					row[x++] = sample1;
				}
			}
			// end of line?
			if (x == bytesPerRow)
			{
				if (y <= getBoundsY2())
				{
					image.putByteSamples(0, 0, y - getBoundsY1(), COLUMNS, 1, row, X1);
				}
				if (delta)
				{
					x = newX;
					y = newY;
				}
				else
				{
					x = 0;
					y--;
				}
				if (endOfBitmap)
				{
					processedRows = ROWS - 1;
				}
				setProgress(processedRows, ROWS);
				processedRows++;
				delta = false;
			}
		}
	}

	private void loadCompressedPaletted8Stream() throws IOException
	{
		Paletted8Image image = (Paletted8Image)getImage();
		int imageBytesPerRow = imageWidth;
		int bytesPerRow = imageBytesPerRow;
		int mod = bytesPerRow % 4;
		if (mod != 0)
		{
			bytesPerRow += 4 - mod;
		}
		final int COLUMNS = getBoundsWidth();
		final int ROWS = imageHeight - getBoundsY1();
		final int X1 = getBoundsX1();
		int processedRows = 0;
		byte[] row = new byte[bytesPerRow];
		int x = 0;
		int y = imageHeight - 1;
		boolean endOfBitmap = false;
		boolean delta = false;
		int newX = 0;
		int newY = 0;
		while (processedRows < ROWS)
		{
			int v1 = in.readUnsignedByte();
			int v2 = in.readUnsignedByte();
			if (v1 == 0)
			{
				switch(v2)
				{
					case(0):
					{
						// end of line
						if (x != 0)
						{
							x = bytesPerRow;
						}
						break;
					}
					case(1):
					{
						// end of bitmap
						x = bytesPerRow;
						endOfBitmap = true;
						break;
					}
					case(2):
					{
						// delta
						delta = true;
						newX = x + in.readUnsignedByte();
						newY = y - in.readUnsignedByte();
						x = bytesPerRow;
						break;
					}
					default:
					{
						// copy the next v2 (3..255) bytes from file to output
						boolean paddingByte = (v2 % 2) != 0;
						while (v2-- > 0)
						{
							row[x++] = (byte)in.readUnsignedByte();
						}
						if (paddingByte)
						{
							v2 = in.readUnsignedByte();
						}
						break;
					}
				}
			}
			else
			{
				// rle: replicate v2 as many times as v1 says
				byte value = (byte)v2;
				while (v1-- > 0)
				{
					row[x++] = value;
				}
			}
			// end of line?
			if (x == bytesPerRow)
			{
				if (y <= getBoundsY2())
				{
					image.putByteSamples(0, 0, y - getBoundsY1(), COLUMNS, 1, row, X1);
				}
				if (delta)
				{
					x = newX;
					y = newY;
				}
				else
				{
					x = 0;
					y--;
				}
				if (endOfBitmap)
				{
					processedRows = ROWS - 1;
				}
				setProgress(processedRows, ROWS);
				processedRows++;
				delta = false;
			}
		}
	}

	private void loadHeader() throws
		IOException,
		MissingParameterException, 
		OperationFailedException, 
		UnsupportedTypeException,
		WrongFileFormatException
	{
		byte[] header = new byte[54];
		in.readFully(header);
		if (header[0] != 'B' || header[1] != 'M')
		{
			throw new WrongFileFormatException("Not a BMP file (first two bytes are not 0x42 0x4d).");
		}
		dataOffset = ArrayConverter.getIntLE(header, 0x0a);
		if (dataOffset < 54)
		{
			throw new InvalidFileStructureException("BMP data expected to be 54dec or larger, got " + dataOffset);
		}
		imageWidth = ArrayConverter.getIntLE(header, 0x12);
		imageHeight = ArrayConverter.getIntLE(header, 0x16);
		if (imageWidth < 1 || imageHeight < 1)
		{
			throw new InvalidFileStructureException("BMP image width and height must be larger than 0, got " + imageWidth + " x " + imageHeight);
		}
		int planes = ArrayConverter.getShortLE(header, 0x1a);
		if (planes != 1)
		{
			throw new InvalidFileStructureException("Can only handle BMP number of planes = 1, got " + planes);
		}
		colorDepth = ArrayConverter.getShortLE(header, 0x1c);
		if (colorDepth != 1 && colorDepth != 4 && colorDepth != 8 && colorDepth != 24)
		{
			// TO DO: add support for 16 bpp BMP reading
			throw new InvalidFileStructureException("Unsupported BMP color depth: " + colorDepth);
		}
		compression = ArrayConverter.getIntLE(header, 0x1e);
		if (compression != 0 && !(compression == 1 && colorDepth == 8) && !(compression == 2 && colorDepth == 4))
		{
			throw new InvalidFileStructureException("Unsupported BMP compression type / color depth combination: " + 
				compression + " / " + colorDepth);
		}
		float dpiXValue = ArrayConverter.getIntLE(header, 0x26) / (100.0f / 2.54f);
		float dpiYValue = ArrayConverter.getIntLE(header, 0x2a) / (100.0f / 2.54f);
		setDpi((int)dpiXValue, (int)dpiYValue);
	}

	private void loadStream() throws
		IOException,
		MissingParameterException, 
		OperationFailedException, 
		UnsupportedTypeException
	{
		// 1. check bounds, initialize them if necessary
		setBoundsIfNecessary(imageWidth, imageHeight);
		checkBounds(imageWidth, imageHeight);
		// 2. read palette if the image isn't truecolor (even monochrome BMPs have a palette)
		int bytesToSkip;
		if (colorDepth <= 8)
		{
			int numPaletteEntries = 1 << colorDepth;
			int expectedPaletteSize = 4 * numPaletteEntries;
			int headerSpaceLeft = dataOffset - 54;
			bytesToSkip = headerSpaceLeft - expectedPaletteSize;
			if (bytesToSkip < 0)
			{
				throw new InvalidFileStructureException("Not enough space in header for palette with " + 
					numPaletteEntries + "entries.");
			}
			palette = new Palette(numPaletteEntries);
			for (int index = 0; index < numPaletteEntries; index++)
			{
				int blue = in.readUnsignedByte();
				int green = in.readUnsignedByte();
				int red = in.readUnsignedByte();
				in.readUnsignedByte();
				palette.put(index, red, green, blue);
			}
		}
		else
		{
			bytesToSkip = dataOffset - 54;
		}
		// 3. seek to beginning of image data
		while (bytesToSkip > 0)
		{
			int skipped = in.skipBytes(bytesToSkip);
			if (skipped > 0)
			{
				bytesToSkip -= skipped;
			}
		}
		// 4. check if we have an image object that we are supposed to reuse
		//    if there is one, check if it has the correct type
		//    if there is none, create a new one
		PixelImage image = getImage();
		if (image == null)
		{
			switch(colorDepth)
			{
				case(1):
				{
					setImage(new MemoryBilevelImage(getBoundsWidth(), getBoundsHeight()));
					break;
				}
				case(4):
				case(8):
				{
					setImage(new MemoryPaletted8Image(getBoundsWidth(), getBoundsHeight(), palette));
					break;
				}
				case(24):
				{
					setImage(new MemoryRGB24Image(getBoundsWidth(), getBoundsHeight()));
					break;
				}
				// loadHeader would have thrown an exception for any other color depths
			}
		}
		else
		{
			// TODO: check if image is of correct type
		}
		// now read actual image data
		if (compression == 0)
		{
			loadUncompressedStream();
		}
		else
		if (compression == 1)
		{
			loadCompressedPaletted8Stream();
		}
		else
		if (compression == 2)
		{
			loadCompressedPaletted4Stream();
		}
	}

	private void loadUncompressedBilevelStream() throws 
		IOException, 
		OperationFailedException
	{
		if ((getBoundsX1() % 8) != 0)
		{
			throw new OperationFailedException("When loading bilevel images, horizontal X1 bounds must be a multiple of 8; got " + getBoundsX1());
		}
		BilevelImage image = (BilevelImage)getImage();
		int imageBytesPerRow = (imageWidth + 7) / 8;
		int bytesPerRow = imageBytesPerRow;
		int mod = bytesPerRow % 4;
		if (mod != 0)
		{
			bytesPerRow += 4 - mod;
		}
		int bottomRowsToSkip = imageHeight - 1 - getBoundsY2();
		int bytesToSkip = bottomRowsToSkip * bytesPerRow;
		while (bytesToSkip > 0)
		{
			int skipped = in.skipBytes(bytesToSkip);
			if (skipped > 0)
			{
				bytesToSkip -= skipped;
			}
		}
		final int COLUMNS = getBoundsWidth();
		final int ROWS = getBoundsHeight();
		final int SRC_OFFSET = getBoundsX1() / 8;
		final int SRC_BIT_OFFSET = getBoundsX1() % 8;
		int y = image.getHeight() - 1;
		int processedRows = 0;
		byte[] row = new byte[bytesPerRow];
		while (processedRows < ROWS)
		{
			in.readFully(row);
			image.putPackedBytes(0, y, COLUMNS, row, SRC_OFFSET, SRC_BIT_OFFSET);
			y--;
			setProgress(processedRows, ROWS);
			processedRows++;
		}
	}

	private void loadUncompressedPaletted4Stream() throws
		IOException
	{
		Paletted8Image image = (Paletted8Image)getImage();
		int imageBytesPerRow = (imageWidth + 1) / 2;
		int bytesPerRow = imageBytesPerRow;
		int mod = bytesPerRow % 4;
		if (mod != 0)
		{
			bytesPerRow += 4 - mod;
		}
		int bottomRowsToSkip = imageHeight - 1 - getBoundsY2();
		int bytesToSkip = bottomRowsToSkip * bytesPerRow;
		while (bytesToSkip > 0)
		{
			int skipped = in.skipBytes(bytesToSkip);
			if (skipped > 0)
			{
				bytesToSkip -= skipped;
			}
		}
		final int COLUMNS = getBoundsWidth();
		final int ROWS = getBoundsHeight();
		final int X1 = getBoundsX1();
		int y = image.getHeight() - 1;
		int processedRows = 0;
		byte[] row = new byte[bytesPerRow];
		byte[] samples = new byte[bytesPerRow * 2];
		while (processedRows < ROWS)
		{
			in.readFully(row);
			ArrayConverter.decodePacked4Bit(row, 0, samples, 0, row.length);
			image.putByteSamples(0, 0, y, COLUMNS, 1, samples, X1);
			y--;
			setProgress(processedRows, ROWS);
			processedRows++;
		}
	}

	private void loadUncompressedPaletted8Stream() throws IOException
	{
		Paletted8Image image = (Paletted8Image)getImage();
		int imageBytesPerRow = imageWidth;
		int bytesPerRow = imageBytesPerRow;
		int mod = bytesPerRow % 4;
		if (mod != 0)
		{
			bytesPerRow += 4 - mod;
		}
		int bottomRowsToSkip = imageHeight - 1 - getBoundsY2();
		int bytesToSkip = bottomRowsToSkip * bytesPerRow;
		while (bytesToSkip > 0)
		{
			int skipped = in.skipBytes(bytesToSkip);
			if (skipped > 0)
			{
				bytesToSkip -= skipped;
			}
		}
		final int COLUMNS = getBoundsWidth();
		final int ROWS = getBoundsHeight();
		final int X1 = getBoundsX1();
		int y = image.getHeight() - 1;
		int processedRows = 0;
		byte[] row = new byte[bytesPerRow];
		while (processedRows < ROWS)
		{
			in.readFully(row);
			image.putByteSamples(0, 0, y, COLUMNS, 1, row, X1);
			y--;
			setProgress(processedRows, ROWS);
			processedRows++;
		}
	}

	private void loadUncompressedRgb24Stream() throws IOException
	{
		RGB24Image image = (RGB24Image)getImage();
		int imageBytesPerRow = imageWidth * 3;
		int bytesPerRow = imageBytesPerRow;
		int mod = bytesPerRow % 4;
		if (mod != 0)
		{
			bytesPerRow += 4 - mod;
		}
		int bottomRowsToSkip = imageHeight - 1 - getBoundsY2();
		int bytesToSkip = bottomRowsToSkip * bytesPerRow;
		while (bytesToSkip > 0)
		{
			int skipped = in.skipBytes(bytesToSkip);
			if (skipped > 0)
			{
				bytesToSkip -= skipped;
			}
		}
		final int COLUMNS = getBoundsWidth();
		final int ROWS = getBoundsHeight();
		final int X1 = getBoundsX1();
		int y = image.getHeight() - 1;
		int processedRows = 0;
		byte[] row = new byte[bytesPerRow];
		byte[] samples = new byte[COLUMNS];
		while (processedRows < ROWS)
		{
			in.readFully(row);
			// copy red samples to array samples and store those samples
			for (int x = X1 * 3 + 2, i = 0; i < COLUMNS; x += 3, i++)
			{
				samples[i] = row[x];
			}
			image.putByteSamples(RGBIndex.INDEX_RED, 0, y, COLUMNS, 1, samples, 0);
			// copy green samples to array samples and store those samples
			for (int x = X1 * 3 + 1, i = 0; i < COLUMNS; x += 3, i++)
			{
				samples[i] = row[x];
			}
			image.putByteSamples(RGBIndex.INDEX_GREEN, 0, y, COLUMNS, 1, samples, 0);
			// copy blue samples to array samples and store those samples
			for (int x = X1 * 3, i = 0; i < COLUMNS; x += 3, i++)
			{
				samples[i] = row[x];
			}
			image.putByteSamples(RGBIndex.INDEX_BLUE, 0, y, COLUMNS, 1, samples, 0);
			y--;
			setProgress(processedRows, ROWS);
			processedRows++;
		}
	}

	private void loadUncompressedStream() throws
		IOException,
		OperationFailedException
	{
		switch(colorDepth)
		{
			case(1):
			{
				loadUncompressedBilevelStream();
				break;
			}
			case(4):
			{
				loadUncompressedPaletted4Stream();
				break;
			}
			case(8):
			{
				loadUncompressedPaletted8Stream();
				break;
			}
			case(24):
			{
				loadUncompressedRgb24Stream();
				break;
			}
		}
	}

	public void process() throws 
		MissingParameterException, 
		OperationFailedException
	{
		initModeFromIOObjects();
		if (getMode() == CodecMode.LOAD)
		{
			load();
		}
		else
		{
			save();
		}
	}

	private void save() throws
		MissingParameterException, 
		OperationFailedException, 
		UnsupportedTypeException
	{
		// check parameters of this operation
		// 1 image to be saved
		// 1.1 is it available?
		PixelImage image = getImage();
		if (image == null)
		{
			throw new MissingParameterException("No image available.");
		}
		// 1.2 is it supported?
		if (!(image instanceof Paletted8Image ||
		      image instanceof Gray8Image ||
		      image instanceof BilevelImage ||
		      image instanceof RGB24Image))
		{
			throw new UnsupportedTypeException("Unsupported image type: " + image.getClass().getName());
		}
		// 2 is output stream available?
		out = getOutputAsDataOutput();
		if (out == null)
		{
			throw new MissingParameterException("Output stream / random access file parameter missing.");
		}
		// now write the output stream
		try
		{
			writeStream();
		}
		catch (IOException ioe)
		{
			throw new OperationFailedException("I/O failure: " + ioe.toString());
		}
	}

	public String suggestFileExtension(PixelImage image)
	{
		return ".bmp";
	}

	private void writeHeader(PixelImage image, int filesize, int offset, int numBits) throws IOException
	{
		out.write(0x42); // 'B'
		out.write(0x4d); // 'M'
		writeInt(filesize);
		writeShort(0);
		writeShort(0);
		writeInt(offset);

		writeInt(40); // BITMAP_INFO header length
		writeInt(getBoundsWidth());
		writeInt(getBoundsHeight());
		writeShort(1); // # of planes
		writeShort(numBits);
		writeInt(0); // compression (0 = none)
		writeInt(filesize - offset); // size of image data in bytes
		writeInt((int)(getDpiX() * (100f / 2.54f))); // horizontal resolution in dpi
		writeInt((int)(getDpiY() * (100f / 2.54f))); // vertical resolution in dpi
		writeInt(0); // # of used colors
		writeInt(0); // # of important colors
	}

	// we can't use out.writeInt because we need little endian byte order
	private void writeInt(int value) throws IOException
	{
		out.write(value & 0xff);
		out.write((value >> 8) & 0xff);
		out.write((value >> 16) & 0xff);
		out.write((value >> 24) & 0xff);
	}

	/**
	 * Write the palette associated with the image getImage().
	 * Required not only for image objects that implement PalettedImage
	 * but also for BilevelImage and Grayscale8Image.
	 * For the latter two the palette values must be explicitly written into the file.
	 */
	private void writePalette() throws IOException
	{
		PixelImage pi = getImage();
		if (pi == null)
		{
			return;
		}
		if (pi instanceof Paletted8Image)
		{
			// always write 256 entries; if there aren't enough
			// in the palette, fill it up to 256 with (0, 0, 0, 0)
			Palette palette = ((Paletted8Image)pi).getPalette();
			for (int i = 0; i < 256; i++)
			{
				if (i < palette.getNumEntries())
				{
					out.write(palette.getSample(RGBIndex.INDEX_BLUE, i));
					out.write(palette.getSample(RGBIndex.INDEX_GREEN, i));
					out.write(palette.getSample(RGBIndex.INDEX_RED, i));
					out.write(0);
				}
				else
				{
					out.writeInt(0); // writes four 0 bytes
				}
			}
		}
		if (pi instanceof Gray8Image)
		{
			for (int i = 0; i < 256; i++)
			{
				out.write(i);
				out.write(i);
				out.write(i);
				out.write(0);
			}
		}
		if (pi instanceof BilevelImage)
		{
			for (int i = 0; i < 2; i++)
			{
				out.write(i * 255);
				out.write(i * 255);
				out.write(i * 255);
				out.write(0);
			}
		}
	}

	// we can't use out.writeShort because we need little endian byte order
	private void writeShort(int value) throws IOException
	{
		out.write(value & 0xff);
		out.write((value >> 8) & 0xff);
	}

	private void writeStream() throws IOException
	{
		PixelImage image = getImage();
		setBoundsIfNecessary(image.getWidth(), image.getHeight());
		int width = getBoundsWidth();
		int height = getBoundsHeight();
		ByteChannelImage bcimg = null;
		BilevelImage bilevelImage = null;
		RGB24Image rgbimg = null;
		int bytesPerRow = 0;
		int offset = 54;
		int numBits = 0;
		int numPackedBytes = 0;
		if (image instanceof Paletted8Image ||
		    image instanceof Gray8Image)
		{
			bcimg = (ByteChannelImage)image;
			bytesPerRow = width;
			offset += 1024;
			numBits = 8;
		}
		else
		if (image instanceof BilevelImage)
		{
			bilevelImage = (BilevelImage)image;
			numPackedBytes = (width + 7) / 8;
			bytesPerRow = numPackedBytes;
			offset += 8;
			numBits = 1;
		}
		else
		if (image instanceof RGB24Image)
		{
			rgbimg = (RGB24Image)image;
			bytesPerRow = width * 3;
			numBits = 24;
		}
		if ((bytesPerRow % 4) != 0)
		{
			bytesPerRow = ((bytesPerRow + 3) / 4) * 4;
		}
		int filesize = offset + bytesPerRow * height;
		writeHeader(image, filesize, offset, numBits);
		writePalette();
		byte[] row = new byte[bytesPerRow];
		final int X1 = getBoundsX1();
		for (int y = getBoundsY2(), processed = 0; processed < height; y--, processed++)
		{
			if (bilevelImage != null)
			{
				bilevelImage.getPackedBytes(X1, y, width, row, 0, 0);
			}
			else
			if (bcimg != null)
			{
				bcimg.getByteSamples(0, 0, y, width, 1, row, 0);
			}
			else
			if (rgbimg != null)
			{
				int offs = 0;
				for (int x = X1; x < X1 + width; x++)
				{
					row[offs++] = rgbimg.getByteSample(RGBIndex.INDEX_BLUE, x, y);
					row[offs++] = rgbimg.getByteSample(RGBIndex.INDEX_GREEN, x, y);
					row[offs++] = rgbimg.getByteSample(RGBIndex.INDEX_RED, x, y);
				}
			}
			else
			{
				// error
			}
			out.write(row);
			setProgress(processed, height);
			if (getAbort())
			{
				break;
			}
		}
		close();
	}
}
