/*
 * PNMCodec
 * 
 * Copyright (c) 2000, 2001, 2002, 2003, 2004, 2005, 2006 Marco Schmidt.
 * All rights reserved.
 */

package net.sourceforge.jiu.codecs;

import java.io.DataOutput;
import java.io.InputStream;
import java.io.IOException;
import java.io.PushbackInputStream;
import java.util.NoSuchElementException;
import java.util.StringTokenizer;
import net.sourceforge.jiu.data.BilevelImage;
import net.sourceforge.jiu.data.GrayImage;
import net.sourceforge.jiu.data.Gray16Image;
import net.sourceforge.jiu.data.Gray8Image;
import net.sourceforge.jiu.data.GrayIntegerImage;
import net.sourceforge.jiu.data.IntegerImage;
import net.sourceforge.jiu.data.MemoryBilevelImage;
import net.sourceforge.jiu.data.MemoryGray16Image;
import net.sourceforge.jiu.data.MemoryGray8Image;
import net.sourceforge.jiu.data.MemoryRGB24Image;
import net.sourceforge.jiu.data.MemoryRGB48Image;
import net.sourceforge.jiu.data.PixelImage;
import net.sourceforge.jiu.data.RGB24Image;
import net.sourceforge.jiu.data.RGB48Image;
import net.sourceforge.jiu.data.RGBIndex;
import net.sourceforge.jiu.data.RGBIntegerImage;
import net.sourceforge.jiu.ops.MissingParameterException;
import net.sourceforge.jiu.ops.OperationFailedException;
import net.sourceforge.jiu.ops.WrongParameterException;

/**
 * A codec to read and write Portable Anymap (PNM) image files.
 * This format includes three file types well-known in the Unix world:
 * <ul>
 * <li>PBM (Portable Bitmap - 1 bit per pixel bilevel image),</li>
 * <li>PGM (Portable Graymap - grayscale image) and</li>
 * <li>PPM (Portable Pixmap - RGB truecolor image).</li>
 * </ul>
 * <p>
 *
 * <h3>Compression</h3>
 * The file format only allows for uncompressed storage.
 *
 * <h3>ASCII mode / binary mode</h3>
 * PNM streams can be stored in binary mode or ASCII mode.
 * ASCII mode files are text files with numbers representing the pixels.
 * They become larger than their binary counterparts, but as they are
 * very redundant they can be compressed well with archive programs.
 * ASCII PGM and PPM files can have all kinds of maximum sample values,
 * thus allowing for arbitrary precision.
 * They are not restricted by byte limits.
 * PBM streams always have two colors, no matter if they are ASCII or binary.
 *
 * <h3>Color depth for PGM / PPM</h3>
 * <p>
 * The header of a PGM and PPM file stores a maximum sample value
 * (such a value is not stored for PBM, where the maximum value is always 1).
 * When in binary mode, PGM and PPM typically have a maximum sample value of 255,
 * which makes PGM 8 bits per pixel and PPM 24 bits per pixel large.
 * One sample will be stored as a single byte.
 * However, there also exist binary PGM files with a maximum sample value larger than
 * 255 and smaller than 65536.
 * These files use two bytes per sample, in network byte order (big endian).
 * I have yet to see PPM files with that property, but they are of course imagineable.
 * 16 bpp
 * </p>
 *
 * <h3>DPI values</h3>
 * PNM files cannot store the physical resolution in DPI.
 *
 * <h3>Number of images</h3>
 * Only one image can be stored in a PNM file.
 *
 * <h3>Usage example - load an image from a PNM file</h3>
 * <pre>
 * PNMCodec codec = new PNMCodec();
 * codec.setFile("test.ppm", CodecMode.LOAD);
 * codec.process();
 * codec.close();
 * PixelImage image = codec.getImage();
 * </pre>
 *
 * <h3>Usage example - save an image to a PNM file</h3>
 * <pre>
 * PNMCodec codec = new PNMCodec();
 * BilevelImage myFax = ...; // initialize
 * codec.setImage(myFax);
 * codec.setFile("out.pbm", CodecMode.SAVE);
 * codec.process();
 * codec.close();
 * </pre>
 *
 * @author Marco Schmidt
 */
public class PNMCodec extends ImageCodec
{
	/**
	 * Image type constant for images of unknown type.
	 */
	public static final int IMAGE_TYPE_UNKNOWN = -1;

	/**
	 * Image type constant for bilevel images, stored in PBM files.
	 */
	public static final int IMAGE_TYPE_BILEVEL = 0;

	/**
	 * Image type constant for grayscale images, stored in PGM files.
	 */
	public static final int IMAGE_TYPE_GRAY = 1;

	/**
	 * Image type constant for RGB truecolor images, stored in PPM files.
	 */
	public static final int IMAGE_TYPE_COLOR = 2;
	private static final String[] IMAGE_TYPE_FILE_EXTENSIONS = 
		{".pbm", ".pgm", ".ppm"};
	private Boolean ascii;
	private int columns;
	private int imageType;
	private PushbackInputStream in;
	private DataOutput out;
	private int height;
	private int maxSample;
	private int width;

	/**
	 * Attempts to find the appropriate image type by looking at a file's name.	
	 * Ignores case when comparing.
	 * Returns {@link #IMAGE_TYPE_BILEVEL} for <code>.pbm</code>,
	 * {@link #IMAGE_TYPE_GRAY} for <code>.pgm</code> and
	 * {@link #IMAGE_TYPE_COLOR} for <code>.ppm</code>.
	 * Otherwise, {@link #IMAGE_TYPE_UNKNOWN} is returned.
	 * To get a file extension given that you have an image type, use
	 * {@link #getTypicalFileExtension}.
	 * 
	 * @param fileName the file name to be examined
	 * @return one of the <code>IMAGE_TYPE_xxx</code> constants of this class
	 */
	public static int determineImageTypeFromFileName(String fileName)
	{
		if (fileName == null || fileName.length() < 4)
		{
			return IMAGE_TYPE_UNKNOWN;
		}
		String ext = fileName.substring(fileName.length() - 3);
		ext = ext.toLowerCase();
		for (int i = 0; i < IMAGE_TYPE_FILE_EXTENSIONS.length; i++)
		{
			if (IMAGE_TYPE_FILE_EXTENSIONS[i].equals(ext))
			{
				return i;
			}
		}
		return IMAGE_TYPE_UNKNOWN;
	}

	/**
	 * Returns if ASCII mode was used for loading an image or will
	 * be used to store an image.
	 * @return true for ASCII mode, false for binary mode, null if that information is not available
	 * @see #setAscii
	 */
	public Boolean getAscii()
	{
		return ascii;
	}

	public String getFormatName()
	{
		return "Portable Anymap (PBM, PGM, PPM)";
	}

	public String[] getMimeTypes()
	{
		return new String[] {"image/x-ppm", "image/x-pgm", "image/x-pbm", "image/x-pnm", 
			"image/x-portable-pixmap", "image/x-portable-bitmap", "image/x-portable-graymap", 
			"image/x-portable-anymap"};
	}

	/**
	 * Returns the typical file extension (including leading dot) for an
	 * image type.
	 * Returns <code>null</code> for {@link #IMAGE_TYPE_UNKNOWN}.
	 * To get the image type given that you have a file name, use
	 * {@link #determineImageTypeFromFileName}.
	 *
	 * @param imageType the image type for which the extension is required
	 * @return the file extension or null
	 */
	public static String getTypicalFileExtension(int imageType)
	{
		if (imageType >= 0 && imageType < IMAGE_TYPE_FILE_EXTENSIONS.length)
		{
			return IMAGE_TYPE_FILE_EXTENSIONS[imageType];
		}
		else
		{
			return null;
		}
	}

	public boolean isLoadingSupported()
	{
		return true;
	}

	public boolean isSavingSupported()
	{
		return true;
	}

	/**
	 * Loads an image from a PNM input stream.
	 * It is assumed that a stream was given to this codec using {@link #setInputStream(InputStream)}.
	 *
	 * @return the image as an instance of a class that implements {@link IntegerImage}
	 * @throws InvalidFileStructureException if the input stream is not a valid PNM stream (or unsupported)
	 * @throws java.io.IOException if there were problems reading from the input stream
	 */
	private void load() throws 
		InvalidFileStructureException,
		IOException,
		MissingParameterException,
		UnsupportedTypeException,
		WrongFileFormatException,
		WrongParameterException
	{
		InputStream is = getInputStream();
		if (is != null)
		{
			if (is instanceof PushbackInputStream)
			{
				in = (PushbackInputStream)is;
			}
			else
			{
				in = new PushbackInputStream(is);
			}
		}
		else
		{
			throw new MissingParameterException("InputStream object required for loading.");
		}
		loadType();
		String resolutionLine = loadTextLine();
		setResolution(resolutionLine);
		setBoundsIfNecessary(width, height);
		if (imageType == IMAGE_TYPE_BILEVEL)
		{
			maxSample = 1;
		}
		else
		{
			// load maximum value
			String maxSampleLine = loadTextLine();
			setMaximumSample(maxSampleLine);
		}
		if (maxSample > 65535)
		{
			throw new UnsupportedTypeException("Cannot deal with samples larger than 65535.");
		}
		checkImageResolution();
		switch (imageType)
		{
			case(IMAGE_TYPE_BILEVEL):
			{
				loadBilevelImage();
				break;
			}
			case(IMAGE_TYPE_COLOR):
			{
				loadColorImage();
				break;
			}
			case(IMAGE_TYPE_GRAY):
			{
				loadGrayImage();
				break;
			}
			default:
			{
				throw new UnsupportedTypeException("Cannot deal with image type.");
			}
		}
	}

	private int loadAsciiNumber() throws
		InvalidFileStructureException,
		IOException
	{
		boolean hasDigit = false;
		int result = -1;
		do
		{
			int b = in.read();
			if (b >= 48 && b <= 57)
			{
				// decimal digit
				if (hasDigit)
				{
					result = result * 10 + (b - 48);
				}
				else
				{
					hasDigit = true;
					result = b - 48;
				}
			}
			else
			if (b == 32 || b == 10 || b == 13 || b == 9)
			{
				// whitespace
				if (hasDigit)
				{
					if (result > maxSample)
					{
						throw new InvalidFileStructureException("Read number " +
							"from PNM stream that is larger than allowed " +
							"maximum sample value " + maxSample + " (" + result + ").");
					}
					return result;
				}
				// ignore whitespace
			}
			else
			if (b == 35) 
			{
				// the # character, indicating a comment row
				if (hasDigit)
				{
					in.unread(b);
					if (result > maxSample)
					{
						throw new InvalidFileStructureException("Read " +
							"number from PNM stream that is larger than " +
							"allowed maximum sample value " + maxSample + 
							" (" + result + ").");
					}
					return result;
				}
				do
				{
					b = in.read();
				}
				while (b != -1 && b != 10 && b != 13);
				if (b == 13)
				{
				}
				// put it into the comment list
			}
			else
			if (b == -1)
			{
				// the end of file character
				if (hasDigit)
				{
					if (result > maxSample)
					{
						throw new InvalidFileStructureException("Read number from PNM stream that is larger than allowed maximum sample value " +
							maxSample + " (" + result + ")");
					}
					return result;
				}
				throw new InvalidFileStructureException("Unexpected end of file while reading ASCII number from PNM stream.");
			}
			else
			{
				throw new InvalidFileStructureException("Read invalid character from PNM stream: " + b +
					" dec.");
			}
		}
		while(true);
	}

	private void loadBilevelImage() throws
		InvalidFileStructureException,
		IOException,
		WrongParameterException
	{
		PixelImage image = getImage();
		if (image == null)
		{
			setImage(new MemoryBilevelImage(getBoundsWidth(), getBoundsHeight()));
		}
		else
		{
			if (!(image instanceof BilevelImage))
			{
				throw new WrongParameterException("Specified input image must implement BilevelImage for this image type.");
			}
		}
		if (getAscii().booleanValue())
		{
			loadBilevelImageAscii();
		}
		else
		{
			loadBilevelImageBinary();
		}
	}

	private void loadBilevelImageAscii() throws
		InvalidFileStructureException,
		IOException
	{
		BilevelImage image = (BilevelImage)getImage();
		// skip the pixels of the first getBoundsY1() rows
		int pixelsToSkip = width * getBoundsY1();
		for (int i = 0; i < pixelsToSkip; i++)
		{
			loadAsciiNumber();
		}
		final int NUM_ROWS = getBoundsHeight();
		final int COLUMNS = getBoundsWidth();
		final int X1 = getBoundsX1();
		int[] row = new int[width];
		// now read and store getBoundsHeight() rows
		for (int y = 0; y < NUM_ROWS; y++)
		{
			for (int x = 0; x < width; x++)
			{
				int value = loadAsciiNumber();
				if (value == 0)
				{
					row[x] = BilevelImage.WHITE;
				}
				else
				if (value == 1)
				{
					row[x] = BilevelImage.BLACK;
				}
				else
				{
					throw new InvalidFileStructureException("Loaded " +
						"number for position x=" + x + ", y=" + (y + getBoundsY1()) + 
						" is neither 0 nor 1 in PBM stream: " + value);
				}
			}
			image.putSamples(0, 0, y, COLUMNS, 1, row, X1);
			setProgress(y, NUM_ROWS);
		}
	}

	private void loadBilevelImageBinary() throws
		InvalidFileStructureException,
		IOException
	{
		BilevelImage image = (BilevelImage)getImage();
		int bytesPerRow = (width + 7) / 8;
		// skip the first getBoundsY1() rows
		long bytesToSkip = (long)getBoundsY1() * (long)bytesPerRow;
		// Note:
		// removed in.skip(bytesToSkip) because that was only available in Java 1.2
		// instead the following while loop is used
		while (bytesToSkip-- > 0)
		{
			in.read();
		}
		// allocate buffer large enough for a complete row
		byte[] row = new byte[bytesPerRow];
		final int numRows = getBoundsHeight();
		// read and store the next getBoundsHeight() rows
		for (int y = 0; y < numRows; y++)
		{
			// read bytesPerRow bytes into row
			int bytesToRead = bytesPerRow;
			int index = 0;
			while (bytesToRead > 0)
			{
				int result = in.read(row, index, bytesToRead);
				if (result >= 0)
				{
					index += result;
					bytesToRead -= result;
				}
				else
				{
					throw new InvalidFileStructureException("Unexpected end of input stream while reading.");
				}
			}
			// invert values
			for (int x = 0; x < row.length; x++)
			{
				row[x] = (byte)~row[x];
			}
			//image.putPackedBytes(0, y, bytesPerRow, buffer, 0);
			if (isRowRequired(y))
			{
				image.putPackedBytes(0, y - getBoundsY1(), getBoundsWidth(), row, getBoundsX1() >> 3, getBoundsX1() & 7);
			}
			setProgress(y, numRows);
		}
	}

	private void loadColorImage() throws InvalidFileStructureException, IOException
	{
		RGBIntegerImage image = null;
		RGB24Image image24 = null;
		if (maxSample <= 255)
		{
			image24 = new MemoryRGB24Image(width, height);
			image = image24;
			setImage(image);
		}
		else
		{
			image = new MemoryRGB48Image(width, height);
			setImage(image);
		}
		for (int y = 0, destY = - getBoundsY1(); y < height; y++, destY++)
		{
			if (getAscii().booleanValue())
			{
				for (int x = 0; x < width; x++)
				{
					int red = loadAsciiNumber();
					if (red < 0 || red > maxSample)
					{
						throw new InvalidFileStructureException("Invalid " +
							"sample value " + red + " for red sample at " +
							"(x=" + x + ", y=" + y + ").");
					}
					image.putSample(RGBIndex.INDEX_RED, x, y, red);

					int green = loadAsciiNumber();
					if (green < 0 || green > maxSample)
					{
						throw new InvalidFileStructureException("Invalid " +
							"sample value " + green + " for green sample at " +
							"(x=" + x + ", y=" + y + ").");
					}
					image.putSample(RGBIndex.INDEX_GREEN, x, y, green);

					int blue = loadAsciiNumber();
					if (blue < 0 || blue > maxSample)
					{
						throw new InvalidFileStructureException("Invalid " +
							"sample value " + blue + " for blue sample at " +
							"(x=" + x + ", y=" + y + ").");
					}
					image.putSample(RGBIndex.INDEX_BLUE, x, y, blue);
				}
			}
			else
			{
				if (image24 != null)
				{
					for (int x = 0; x < width; x++)
					{
						int red = in.read();
						if (red == -1)
						{
							throw new InvalidFileStructureException("Unexpected " +
								"end of file while reading red sample for pixel " +
								"x=" + x + ", y=" + y + ".");
						}
						image24.putByteSample(RGBIndex.INDEX_RED, x, y, (byte)(red & 0xff));
						int green = in.read();
						if (green == -1)
						{
							throw new InvalidFileStructureException("Unexpected " +
								"end of file while reading green sample for pixel " +
								"x=" + x + ", y=" + y + ".");
						}
						image24.putByteSample(RGBIndex.INDEX_GREEN, x, y, (byte)(green & 0xff));
						int blue = in.read();
						if (blue == -1)
						{
							throw new InvalidFileStructureException("Unexpected " +
								"end of file while reading blue sample for pixel " +
								"x=" + x + ", y=" + y + ".");
						}
						image24.putByteSample(RGBIndex.INDEX_BLUE, x, y, (byte)(blue & 0xff));
					}
				}
			}
			setProgress(y, getBoundsHeight());
		}
	}

	private void loadGrayImage() throws InvalidFileStructureException, IOException, UnsupportedTypeException
	{
		final int WIDTH = getBoundsWidth();
		final int HEIGHT = getBoundsHeight();
		PixelImage pimage = getImage();
		if (pimage == null)
		{
			if (maxSample < 256)
			{
				pimage = new MemoryGray8Image(WIDTH, HEIGHT);
			}
			else
			if (maxSample < 65536)
			{
				pimage = new MemoryGray16Image(WIDTH, HEIGHT);
			}
			else
			{
				throw new UnsupportedTypeException("Gray images with more than 16 bits per pixel are not supported.");
			}
			setImage(pimage);
		}
		else
		{
		}
		GrayIntegerImage image = (GrayIntegerImage)pimage;
		int[] buffer = new int[width];
		for (int y = 0, destY = -getBoundsY1(); destY < getBoundsHeight(); y++, destY++)
		{
			if (getAscii().booleanValue())
			{
				for (int x = 0; x < width; x++)
				{
					buffer[x] = loadAsciiNumber();
				}
			}
			else
			{
				if (maxSample < 256)
				{
					for (int x = 0; x < width; x++)
					{
						buffer[x] = in.read();
					}
				}
				else
				{
					for (int x = 0; x < width; x++)
					{
						int msb = in.read();
						int lsb = in.read();
						buffer[x] = (msb << 8) | lsb;
					}
				}
			}
			if (destY >= 0 && destY < getBoundsHeight())
			{
				image.putSamples(0, 0, destY, getBoundsWidth(), 1, buffer, getBoundsX1());
			}
			setProgress(y, getBoundsY2() + 1);
		}
	}

	private String loadTextLine() throws InvalidFileStructureException, IOException
	{
		// load text lines until
		// 1) a normal text line is found
		// 2) an error occurs
		// any comment lines starting with # are added to the
		// comments Vector
		boolean isComment;
		StringBuffer sb;
		do
		{
			sb = new StringBuffer();
			int b;
			boolean crOrLf;
			do
			{
				b = in.read();
				if (b == -1)
				{
					throw new InvalidFileStructureException("Unexpected end of file in PNM stream.");
				}
				crOrLf = (b == 0x0a || b == 0x0d);
				if (!crOrLf)
				{
					sb.append((char)b);
				}
			}
			while (!crOrLf);
			if (b == 0x0d)
			{
				b = in.read();
				if (b != 0x0a)
				{
					throw new InvalidFileStructureException("Unexpected end of file in PNM stream.");
				}
			}
			isComment = (sb.length() > 0 && sb.charAt(0) == '#');
			if (isComment)
			{
				//sb.deleteCharAt(0);
				//sb.delete(0, 1);
				StringBuffer result = new StringBuffer(sb.length() - 1);
				int i = 1;
				while (i < sb.length())
				{
					result.append(sb.charAt(i++));
				}
				appendComment(result.toString());
			}
		}
		while (isComment);
		return sb.toString();
	}

	/**
	 * Loads the first two characters (which are expected to be a capital P
	 * followed by a decimal digit between 1 and 6, inclusively) and skips
	 * following LF and CR characters.
	 * This method not only checks the two bytes, it also initializes internal fields
	 * for storage mode (ASCII or binary) and image type.
	 *
	 * @throws WrongFileFormatException if the input stream is not a PNM stream
	 * @throws InvalidFileStructureException if the format that
	 *  is described above was not encountered
	 * @throws java.io.IOException if there were errors reading data
	 * @throws java.lang.IllegalArgumentException if the input stream was not given to this codec
	 */
	private void loadType() throws InvalidFileStructureException, IOException, WrongFileFormatException 
	{
		// read two bytes
		int v1 = in.read();
		int v2 = in.read();
		// check if first byte is P
		if (v1 != 0x50)
		{
			throw new WrongFileFormatException("Not a PNM stream. First byte " +
				"in PNM stream is expected to be 0x50 ('P'); found: " +
				v1 + " (dec).");
		}
		// check if second byte is ASCII of digit from 1 to 6
		if (v2 < 0x31 || v2 > 0x36)
		{
			throw new WrongFileFormatException("Not a PNM stream. Second byte " +
				"in PNM stream is expected to be the ASCII value of decimal " +
				"digit between 1 and 6 (49 dec to 54 dec); found " +
				v2 + " dec.");
		}
		// determine mode (ASCII or binary) from second byte
		ascii = new Boolean(v2 < 0x34);
		// determine image type from second byte
		v2 = v2 - 0x30;
		imageType = (v2 - 1) % 3;
		// skip LF and CR
		int b;
		do
		{
			b = in.read();
		}
		while (b == 0x0a || b == 0x0d || b == ' ');
		if (b == -1)
		{
			throw new InvalidFileStructureException("Read type (" +
				v2 + "). Unexpected end of file in input PNM stream.");
		}
		in.unread(b);
	}

	public void process() throws
		MissingParameterException,
		OperationFailedException
	{
		initModeFromIOObjects();
		try
		{
			if (getMode() == CodecMode.LOAD)
			{
				load();
			}
			else
			{
				save();
			}
		}
		catch (IOException ioe)
		{
			throw new OperationFailedException("I/O error: " + ioe.toString());
		}
	}

	private void save() throws
		IOException, 
		MissingParameterException,
		WrongParameterException
	{
		out = getOutputAsDataOutput();
		if (out == null)
		{
			throw new WrongParameterException("Cannot get a DataOutput object to use for saving.");
		}
		PixelImage pi = getImage();
		if (pi == null)
		{
			throw new MissingParameterException("Input image missing.");
		}
		if (!(pi instanceof IntegerImage))
		{
			throw new WrongParameterException("Input image must implement IntegerImage.");
		}
		IntegerImage image = (IntegerImage)pi;
		width = image.getWidth();
		height = image.getHeight();
		setBoundsIfNecessary(width, height);
		if (image instanceof RGB24Image)
		{
			imageType = IMAGE_TYPE_COLOR;
			maxSample = 255;
			save((RGB24Image)image);
		}
		else
		if (image instanceof RGB48Image)
		{
			imageType = IMAGE_TYPE_COLOR;
			maxSample = 65535;
			save((RGB48Image)image);
		}
		else
		if (image instanceof BilevelImage)
		{
			imageType = IMAGE_TYPE_BILEVEL;
			maxSample = 1;
			save((BilevelImage)image);
		}
		else
		if (image instanceof Gray8Image)
		{
			imageType = IMAGE_TYPE_GRAY;
			maxSample = 255;
			save((Gray8Image)image);
		}
		else
		if (image instanceof Gray16Image)
		{
			imageType = IMAGE_TYPE_GRAY;
			maxSample = 65535;
			save((Gray16Image)image);
		}
		else
		{
			throw new WrongParameterException("Unsupported input image type: " +
				image.getClass().getName());
		}
		close();
	}

	private void save(BilevelImage image) throws IOException
	{
		saveHeader();
		final int WIDTH = getBoundsWidth();
		final int HEIGHT = getBoundsHeight();
		final int BYTES_PER_ROW = (WIDTH + 7) / 8;
		byte[] buffer = new byte[BYTES_PER_ROW];
		for (int y = 0, srcY = getBoundsY1(); y < HEIGHT; y++, srcY++)
		{
			if (getAscii().booleanValue())
			{
				for (int x = 0, srcX = getBoundsX1(); x < WIDTH; x++, srcX++)
				{
					if (image.isBlack(srcX, srcY))
					{
						out.write(49); // 1
					}
					else
					{
						out.write(48); // 0
					}
					columns ++;
					if (columns > 70)
					{
						columns = 0;
						out.write(10);
					}
					else
					{
						out.write(32);
						columns++;
					}
				}
			}
			else
			{
				image.getPackedBytes(getBoundsX1(), srcY, WIDTH, buffer, 0, 0);
				for (int x = 0; x < buffer.length; x++)
				{
					buffer[x] = (byte)(~buffer[x]);
				}
				out.write(buffer);
			}
			setProgress(y, HEIGHT);
		}
	}

	private void save(Gray8Image image) throws IOException
	{
		saveHeader();
		final int HEIGHT = getBoundsHeight();
		final int WIDTH = getBoundsWidth();
		final int X1 = getBoundsX1();
		System.out.println(WIDTH + " " + HEIGHT + " " + X1);
		byte[] buffer = new byte[WIDTH];
		for (int y = 0, srcY = getBoundsY1(); y < HEIGHT; y++, srcY++)
		{
			image.getByteSamples(0, X1, srcY, WIDTH, 1, buffer, 0);
			if (getAscii().booleanValue())
			{
				for (int x = 0; x < WIDTH; x++)
				{
					saveAsciiNumber(buffer[x] & 0xff);
					out.write(32);
					columns += 2;
					if (columns > 70)
					{
						columns = 0;
						out.write(10);
					}
					else
					{
						out.write(32);
						columns++;
					}
				}
			}
			else
			{
				out.write(buffer);
			}
			setProgress(y, HEIGHT);
		}
	}

	private void save(Gray16Image image) throws IOException
	{
		saveHeader();
		final int HEIGHT = getBoundsHeight();
		final int WIDTH = getBoundsWidth();
		final int X1 = getBoundsX1();
		short[] buffer = new short[WIDTH];
		for (int y = 0, srcY = getBoundsY1(); y < HEIGHT; y++, srcY++)
		{
			image.getShortSamples(0, X1, srcY, WIDTH, 1, buffer, 0);
			if (getAscii().booleanValue())
			{
				for (int x = 0; x < WIDTH; x++)
				{
					saveAsciiNumber(buffer[x] & 0xffff);
					out.write(32);
					columns += 4;
					if (columns > 70)
					{
						columns = 0;
						out.write(10);
					}
					else
					{
						out.write(32);
						columns++;
					}
				}
			}
			else
			{
				for (int x = 0; x < WIDTH; x++)
				{
					int sample = buffer[x] & 0xffff;
					out.write((sample >> 8) & 0xff);
					out.write(sample & 0xff);
				}
			}
			setProgress(y, HEIGHT);
		}
	}

	private void save(RGB24Image image) throws IOException
	{
		saveHeader();
		final int WIDTH = getBoundsWidth();
		final int HEIGHT = getBoundsHeight();
		for (int y = 0, srcY = getBoundsY1(); y < HEIGHT; y++, srcY++)
		{
			if (getAscii().booleanValue())
			{
				for (int x = 0, srcX = getBoundsX1(); x < WIDTH; x++, srcX++)
				{
					int red = image.getSample(RGBIndex.INDEX_RED, srcX, srcY);
					int green = image.getSample(RGBIndex.INDEX_GREEN, srcX, srcY);
					int blue = image.getSample(RGBIndex.INDEX_BLUE, srcX, srcY);
					saveAsciiNumber(red);
					out.write(32);
					saveAsciiNumber(green);
					out.write(32);
					saveAsciiNumber(blue);
					columns += 11;
					if (columns > 80)
					{
						columns = 0;
						out.write(10);
					}
					else
					{
						out.write(32);
						columns++;
					}
				}
			}
			else
			{
				for (int x = 0, srcX = getBoundsX1(); x < WIDTH; x++, srcX++)
				{
					out.write(image.getSample(RGBIndex.INDEX_RED, srcX, srcY));
					out.write(image.getSample(RGBIndex.INDEX_GREEN, srcX, srcY));
					out.write(image.getSample(RGBIndex.INDEX_BLUE, srcX, srcY));
				}
			}
			setProgress(y, HEIGHT);
		}
	}

	private void save(RGB48Image image) throws IOException
	{
		saveHeader();
		final int WIDTH = getBoundsWidth();
		final int HEIGHT = getBoundsHeight();
		for (int y = 0, srcY = getBoundsY1(); y < HEIGHT; y++, srcY++)
		{
			if (getAscii().booleanValue())
			{
				for (int x = 0, srcX = getBoundsX1(); x < WIDTH; x++, srcX++)
				{
					int red = image.getSample(RGBIndex.INDEX_RED, srcX, srcY);
					int green = image.getSample(RGBIndex.INDEX_GREEN, srcX, srcY);
					int blue = image.getSample(RGBIndex.INDEX_BLUE, srcX, srcY);
					saveAsciiNumber(red);
					out.write(32);
					saveAsciiNumber(green);
					out.write(32);
					saveAsciiNumber(blue);
					columns += 13;
					if (columns > 80)
					{
						columns = 0;
						out.write(10);
					}
					else
					{
						out.write(32);
						columns++;
					}
				}
			}
			else
			{
				/*
				for (int x = 0, srcX = getBoundsX1(); x < WIDTH; x++, srcX++)
				{
					out.write(image.getSample(RGBIndex.INDEX_RED, srcX, srcY));
					out.write(image.getSample(RGBIndex.INDEX_GREEN, srcX, srcY));
					out.write(image.getSample(RGBIndex.INDEX_BLUE, srcX, srcY));
				}
				*/
			}
			setProgress(y, HEIGHT);
		}
	}

	private void saveAsciiNumber(int number) throws
		IOException
	{
		String s = Integer.toString(number);
		for (int i = 0; i < s.length(); i++)
		{
			char c = s.charAt(i);
			out.write(c);
		}
		columns += s.length();
	}

	private void saveHeader() throws IOException
	{
		out.write(80); // 'P'
		int pnmType = 49 + imageType;
		if (getAscii() == null)
		{
			setAscii(maxSample > 255);
		}
		if (!getAscii().booleanValue())
		{
			pnmType += 3;
		}
		out.write(pnmType); // '1' .. '6'
		out.write(10); // line feed
		saveAsciiNumber(getBoundsWidth());
		out.write(32); // space
		saveAsciiNumber(getBoundsHeight());
		out.write(10); // line feed
		if (imageType != IMAGE_TYPE_BILEVEL)
		{
			// bilevel max sample is always 1 and MUST NOT be saved
			saveAsciiNumber(maxSample);
			out.write(10);// line feed
		}
	}

	/**
	 * Specify whether ASCII mode is to be used when saving an image.
	 * Default is binary mode.
	 * @param asciiMode if true, ASCII mode is used, binary mode otherwise
	 */
	public void setAscii(boolean asciiMode)
	{
		ascii = new Boolean(asciiMode);
	}

	private void setMaximumSample(String line) throws InvalidFileStructureException
	{
		line = line.trim();
		try
		{
			maxSample = Integer.parseInt(line);
		}
		catch (NumberFormatException nfe)
		{
			throw new InvalidFileStructureException("Not a valid value for the maximum sample: " + line);
		}
		if (maxSample < 0)
		{
			throw new InvalidFileStructureException("The value for the maximum sample must not be negative; found " + maxSample);
		}
	}

	/*
	 * Reads resolution from argument String and sets private variables
	 * width and height.
	 */
	private void setResolution(String line) throws InvalidFileStructureException
	{
		line = line.trim();
		StringTokenizer st = new StringTokenizer(line, " ");
		try
		{
			if (!st.hasMoreTokens())
			{
				throw new InvalidFileStructureException("No width value found in line \"" +
					line + "\".");
			}
			String number = st.nextToken();
			try
			{
				width = Integer.parseInt(number);
			}
			catch (NumberFormatException nfe)
			{
				throw new InvalidFileStructureException("Not a valid int value for width: " +
					number);
			}
			if (width < 1)
			{
				throw new InvalidFileStructureException("The width value must be larger than " +
					"zero; found " + width + ".");
			}
			if (!st.hasMoreTokens())
			{
				throw new InvalidFileStructureException("No height value found in line \"" +
					line + "\".");
			}
			number = st.nextToken();
			try
			{
				height = Integer.parseInt(number);
			}
			catch (NumberFormatException nfe)
			{
				throw new InvalidFileStructureException("Not a valid int value for height: " +
					number);
			}
			if (height < 1)
			{
				throw new InvalidFileStructureException("The height value must be larger than " +
					"zero; found " + width + ".");
			}
		}
		catch (NoSuchElementException nsee)
		{
			// should not happen because we always check if there is a token
		}
	}

	public String suggestFileExtension(PixelImage image)
	{
		if (image == null)
		{
			return null;
		}
		if (image instanceof BilevelImage)
		{
			return IMAGE_TYPE_FILE_EXTENSIONS[IMAGE_TYPE_BILEVEL];
		}
		else
		if (image instanceof GrayImage)
		{
			return IMAGE_TYPE_FILE_EXTENSIONS[IMAGE_TYPE_GRAY];
		}
		else
		if (image instanceof RGB24Image)
		{
			return IMAGE_TYPE_FILE_EXTENSIONS[IMAGE_TYPE_COLOR];
		}
		return null;
	}
}
