/*
 * Copyright 2003-2008 by Paulo Soares.
 *
 * The contents of this file are subject to the Mozilla Public License Version 1.1
 * (the "License"); you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the License.
 *
 * The Original Code is 'iText, a free JAVA-PDF library'.
 *
 * The Initial Developer of the Original Code is Bruno Lowagie. Portions created by
 * the Initial Developer are Copyright (C) 1999, 2000, 2001, 2002 by Bruno Lowagie.
 * All Rights Reserved.
 * Co-Developer of the code is Paulo Soares. Portions created by the Co-Developer
 * are Copyright (C) 2000, 2001, 2002 by Paulo Soares. All Rights Reserved.
 *
 * Contributor(s): all the names of the contributors are added in the source code
 * where applicable.
 *
 * Alternatively, the contents of this file may be used under the terms of the
 * LGPL license (the "GNU LIBRARY GENERAL PUBLIC LICENSE"), in which case the
 * provisions of LGPL are applicable instead of those above.  If you wish to
 * allow use of your version of this file only under the terms of the LGPL
 * License and not to allow others to use your version of this file under
 * the MPL, indicate your decision by deleting the provisions above and
 * replace them with the notice and other provisions required by the LGPL.
 * If you do not delete the provisions above, a recipient may use your version
 * of this file under either the MPL or the GNU LIBRARY GENERAL PUBLIC LICENSE.
 *
 * This library is free software; you can redistribute it and/or modify it
 * under the terms of the MPL as stated above or under the terms of the GNU
 * Library General Public License as published by the Free Software Foundation;
 * either version 2 of the License, or any later version.
 *
 * This library is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Library general Public License for more
 * details.
 *
 * If you didn't download this code from the following link, you should check if
 * you aren't using an obsolete version:
 * http://www.lowagie.com/iText/
 *
 * This code was originally released in 2001 by SUN (see class
 * com.sun.media.imageioimpl.plugins.bmp.BMPImageReader.java)
 * using the BSD license in a specific wording. In a mail dating from
 * January 23, 2008, Brian Burkhalter (@sun.com) gave us permission
 * to use the code under the following version of the BSD license:
 *
 * Copyright (c) 2005 Sun Microsystems, Inc. All  Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * - Redistribution of source code must retain the above copyright
 *   notice, this  list of conditions and the following disclaimer.
 *
 * - Redistribution 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 Sun Microsystems, Inc. or the names of
 * contributors may be used to endorse or promote products derived
 * from this software without specific prior written permission.
 *
 * This software is provided "AS IS," without a warranty of any
 * kind. ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND
 * WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY
 * EXCLUDED. SUN MIDROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL
 * NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF
 * USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS
 * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR
 * ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL,
 * CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND
 * REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF OR
 * INABILITY TO USE THIS SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGES.
 *
 * You acknowledge that this software is not designed or intended for
 * use in the design, construction, operation or maintenance of any
 * nuclear facility.
 */
package com.lowagie.text.pdf.codec;

import com.lowagie.text.BadElementException;
import com.lowagie.text.ExceptionConverter;
import com.lowagie.text.Image;
import com.lowagie.text.ImgRaw;
import com.lowagie.text.Utilities;
import com.lowagie.text.pdf.PdfArray;
import com.lowagie.text.pdf.PdfDictionary;
import com.lowagie.text.pdf.PdfName;
import com.lowagie.text.pdf.PdfNumber;
import com.lowagie.text.pdf.PdfString;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.HashMap;

/**
 * Reads a BMP image. All types of BMP can be read.
 * <p>
 * It is based in the JAI codec.
 *
 * @author Paulo Soares ([email protected])
 */
@SuppressWarnings("unchecked")
public class BmpImage {

	// BMP variables
	private InputStream			inputStream;
	private long				bitmapFileSize;
	private long				bitmapOffset;
	private long				compression;
	private long				imageSize;
	private byte				palette[];
	private int					imageType;
	private int					numBands;
	private boolean				isBottomUp;
	private int					bitsPerPixel;
	private int					redMask, greenMask, blueMask, alphaMask;
	public HashMap				properties			= new HashMap();
	private long				xPelsPerMeter;
	private long				yPelsPerMeter;
	// BMP Image types
	private static final int	VERSION_2_1_BIT		= 0;
	private static final int	VERSION_2_4_BIT		= 1;
	private static final int	VERSION_2_8_BIT		= 2;
	private static final int	VERSION_2_24_BIT	= 3;

	private static final int	VERSION_3_1_BIT		= 4;
	private static final int	VERSION_3_4_BIT		= 5;
	private static final int	VERSION_3_8_BIT		= 6;
	private static final int	VERSION_3_24_BIT	= 7;

	private static final int	VERSION_3_NT_16_BIT	= 8;
	private static final int	VERSION_3_NT_32_BIT	= 9;

	private static final int	VERSION_4_1_BIT		= 10;
	private static final int	VERSION_4_4_BIT		= 11;
	private static final int	VERSION_4_8_BIT		= 12;
	private static final int	VERSION_4_16_BIT	= 13;
	private static final int	VERSION_4_24_BIT	= 14;
	private static final int	VERSION_4_32_BIT	= 15;

	// Color space types
	private static final int	LCS_CALIBRATED_RGB	= 0;
	private static final int	LCS_sRGB			= 1;
	private static final int	LCS_CMYK			= 2;

	// Compression Types
	private static final int	BI_RGB				= 0;
	private static final int	BI_RLE8				= 1;
	private static final int	BI_RLE4				= 2;
	private static final int	BI_BITFIELDS		= 3;

	int							width;
	int							height;

	BmpImage(InputStream is, boolean noHeader, int size) throws IOException {
		bitmapFileSize = size;
		bitmapOffset = 0;
		process(is, noHeader);
	}

	/**
	 * Reads a BMP from an url.
	 * 
	 * @param url the url
	 * @throws IOException on error
	 * @return the image
	 */
	public static Image getImage(URL url) throws IOException {
		InputStream is = null;
		try {
			is = url.openStream();
			Image img = getImage(is);
			img.setUrl(url);
			return img;
		} finally {
			if (is != null) {
				is.close();
			}
		}
	}

	/**
	 * Reads a BMP from a stream. The stream is not closed.
	 * 
	 * @param is the stream
	 * @throws IOException on error
	 * @return the image
	 */
	public static Image getImage(InputStream is) throws IOException {
		return getImage(is, false, 0);
	}

	/**
	 * Reads a BMP from a stream. The stream is not closed. The BMP may not have a header and be
	 * considered as a plain DIB.
	 * 
	 * @param is the stream
	 * @param noHeader true to process a plain DIB
	 * @param size the size of the DIB. Not used for a BMP
	 * @throws IOException on error
	 * @return the image
	 */
	public static Image getImage(InputStream is, boolean noHeader, int size) throws IOException {
		BmpImage bmp = new BmpImage(is, noHeader, size);
		try {
			Image img = bmp.getImage();
			img.setDpi((int) (bmp.xPelsPerMeter * 0.0254d + 0.5d), (int) (bmp.yPelsPerMeter * 0.0254d + 0.5d));
			img.setOriginalType(Image.ORIGINAL_BMP);
			return img;
		} catch (BadElementException be) {
			throw new ExceptionConverter(be);
		}
	}

	/**
	 * Reads a BMP from a file.
	 * 
	 * @param file the file
	 * @throws IOException on error
	 * @return the image
	 */
	public static Image getImage(String file) throws IOException {
		return getImage(Utilities.toURL(file));
	}

	/**
	 * Reads a BMP from a byte array.
	 * 
	 * @param data the byte array
	 * @throws IOException on error
	 * @return the image
	 */
	public static Image getImage(byte data[]) throws IOException {
		ByteArrayInputStream is = new ByteArrayInputStream(data);
		Image img = getImage(is);
		img.setOriginalData(data);
		return img;
	}

	protected void process(InputStream stream, boolean noHeader) throws IOException {
		if (noHeader || stream instanceof BufferedInputStream) {
			inputStream = stream;
		} else {
			inputStream = new BufferedInputStream(stream);
		}
		if (!noHeader) {
			// Start File Header
			if (!(readUnsignedByte(inputStream) == 'B' && readUnsignedByte(inputStream) == 'M')) {
				throw new RuntimeException("Invalid magic value for BMP file.");
			}

			// Read file size
			bitmapFileSize = readDWord(inputStream);

			// Read the two reserved fields
			readWord(inputStream);
			readWord(inputStream);

			// Offset to the bitmap from the beginning
			bitmapOffset = readDWord(inputStream);

			// End File Header
		}
		// Start BitmapCoreHeader
		long size = readDWord(inputStream);

		if (size == 12) {
			width = readWord(inputStream);
			height = readWord(inputStream);
		} else {
			width = readLong(inputStream);
			height = readLong(inputStream);
		}

		int planes = readWord(inputStream);
		bitsPerPixel = readWord(inputStream);

		properties.put("color_planes", Integer.valueOf(planes));
		properties.put("bits_per_pixel", Integer.valueOf(bitsPerPixel));

		// As BMP always has 3 rgb bands, except for Version 5,
		// which is bgra
		numBands = 3;
		if (bitmapOffset == 0) {
			bitmapOffset = size;
		}
		if (size == 12) {
			// Windows 2.x and OS/2 1.x
			properties.put("bmp_version", "BMP v. 2.x");

			// Classify the image type
			if (bitsPerPixel == 1) {
				imageType = VERSION_2_1_BIT;
			} else if (bitsPerPixel == 4) {
				imageType = VERSION_2_4_BIT;
			} else if (bitsPerPixel == 8) {
				imageType = VERSION_2_8_BIT;
			} else if (bitsPerPixel == 24) {
				imageType = VERSION_2_24_BIT;
			}

			// Read in the palette
			int numberOfEntries = (int) ((bitmapOffset - 14 - size) / 3);
			int sizeOfPalette = numberOfEntries * 3;
			if (bitmapOffset == size) {
				switch (imageType) {
					case VERSION_2_1_BIT:
						sizeOfPalette = 2 * 3;
						break;
					case VERSION_2_4_BIT:
						sizeOfPalette = 16 * 3;
						break;
					case VERSION_2_8_BIT:
						sizeOfPalette = 256 * 3;
						break;
					case VERSION_2_24_BIT:
						sizeOfPalette = 0;
						break;
				}
				bitmapOffset = size + sizeOfPalette;
			}
			readPalette(sizeOfPalette);
		} else {

			compression = readDWord(inputStream);
			imageSize = readDWord(inputStream);
			xPelsPerMeter = readLong(inputStream);
			yPelsPerMeter = readLong(inputStream);
			long colorsUsed = readDWord(inputStream);
			long colorsImportant = readDWord(inputStream);

			switch ((int) compression) {
				case BI_RGB:
					properties.put("compression", "BI_RGB");
					break;

				case BI_RLE8:
					properties.put("compression", "BI_RLE8");
					break;

				case BI_RLE4:
					properties.put("compression", "BI_RLE4");
					break;

				case BI_BITFIELDS:
					properties.put("compression", "BI_BITFIELDS");
					break;
			}

			properties.put("x_pixels_per_meter", Long.valueOf(xPelsPerMeter));
			properties.put("y_pixels_per_meter", Long.valueOf(yPelsPerMeter));
			properties.put("colors_used", Long.valueOf(colorsUsed));
			properties.put("colors_important", Long.valueOf(colorsImportant));

			if (size == 40) {
				// Windows 3.x and Windows NT
				switch ((int) compression) {

					case BI_RGB:  // No compression
					case BI_RLE8:  // 8-bit RLE compression
					case BI_RLE4:  // 4-bit RLE compression

						if (bitsPerPixel == 1) {
							imageType = VERSION_3_1_BIT;
						} else if (bitsPerPixel == 4) {
							imageType = VERSION_3_4_BIT;
						} else if (bitsPerPixel == 8) {
							imageType = VERSION_3_8_BIT;
						} else if (bitsPerPixel == 24) {
							imageType = VERSION_3_24_BIT;
						} else if (bitsPerPixel == 16) {
							imageType = VERSION_3_NT_16_BIT;
							redMask = 0x7C00;
							greenMask = 0x3E0;
							blueMask = 0x1F;
							properties.put("red_mask", Integer.valueOf(redMask));
							properties.put("green_mask", Integer.valueOf(greenMask));
							properties.put("blue_mask", Integer.valueOf(blueMask));
						} else if (bitsPerPixel == 32) {
							imageType = VERSION_3_NT_32_BIT;
							redMask = 0x00FF0000;
							greenMask = 0x0000FF00;
							blueMask = 0x000000FF;
							properties.put("red_mask", Integer.valueOf(redMask));
							properties.put("green_mask", Integer.valueOf(greenMask));
							properties.put("blue_mask", Integer.valueOf(blueMask));
						}

						// Read in the palette
						int numberOfEntries = (int) ((bitmapOffset - 14 - size) / 4);
						int sizeOfPalette = numberOfEntries * 4;
						if (bitmapOffset == size) {
							switch (imageType) {
								case VERSION_3_1_BIT:
									sizeOfPalette = (int) (colorsUsed == 0 ? 2 : colorsUsed) * 4;
									break;
								case VERSION_3_4_BIT:
									sizeOfPalette = (int) (colorsUsed == 0 ? 16 : colorsUsed) * 4;
									break;
								case VERSION_3_8_BIT:
									sizeOfPalette = (int) (colorsUsed == 0 ? 256 : colorsUsed) * 4;
									break;
								default:
									sizeOfPalette = 0;
									break;
							}
							bitmapOffset = size + sizeOfPalette;
						}
						readPalette(sizeOfPalette);

						properties.put("bmp_version", "BMP v. 3.x");
						break;

					case BI_BITFIELDS:

						if (bitsPerPixel == 16) {
							imageType = VERSION_3_NT_16_BIT;
						} else if (bitsPerPixel == 32) {
							imageType = VERSION_3_NT_32_BIT;
						}

						// BitsField encoding
						redMask = (int) readDWord(inputStream);
						greenMask = (int) readDWord(inputStream);
						blueMask = (int) readDWord(inputStream);

						properties.put("red_mask", Integer.valueOf(redMask));
						properties.put("green_mask", Integer.valueOf(greenMask));
						properties.put("blue_mask", Integer.valueOf(blueMask));

						if (colorsUsed != 0) {
							// there is a palette
							sizeOfPalette = (int) colorsUsed * 4;
							readPalette(sizeOfPalette);
						}

						properties.put("bmp_version", "BMP v. 3.x NT");
						break;

					default:
						throw new RuntimeException("Invalid compression specified in BMP file.");
				}
			} else if (size == 108) {
				// Windows 4.x BMP

				properties.put("bmp_version", "BMP v. 4.x");

				// rgb masks, valid only if comp is BI_BITFIELDS
				redMask = (int) readDWord(inputStream);
				greenMask = (int) readDWord(inputStream);
				blueMask = (int) readDWord(inputStream);
				// Only supported for 32bpp BI_RGB argb
				alphaMask = (int) readDWord(inputStream);
				long csType = readDWord(inputStream);
				int redX = readLong(inputStream);
				int redY = readLong(inputStream);
				int redZ = readLong(inputStream);
				int greenX = readLong(inputStream);
				int greenY = readLong(inputStream);
				int greenZ = readLong(inputStream);
				int blueX = readLong(inputStream);
				int blueY = readLong(inputStream);
				int blueZ = readLong(inputStream);
				long gammaRed = readDWord(inputStream);
				long gammaGreen = readDWord(inputStream);
				long gammaBlue = readDWord(inputStream);

				if (bitsPerPixel == 1) {
					imageType = VERSION_4_1_BIT;
				} else if (bitsPerPixel == 4) {
					imageType = VERSION_4_4_BIT;
				} else if (bitsPerPixel == 8) {
					imageType = VERSION_4_8_BIT;
				} else if (bitsPerPixel == 16) {
					imageType = VERSION_4_16_BIT;
					if ((int) compression == BI_RGB) {
						redMask = 0x7C00;
						greenMask = 0x3E0;
						blueMask = 0x1F;
					}
				} else if (bitsPerPixel == 24) {
					imageType = VERSION_4_24_BIT;
				} else if (bitsPerPixel == 32) {
					imageType = VERSION_4_32_BIT;
					if ((int) compression == BI_RGB) {
						redMask = 0x00FF0000;
						greenMask = 0x0000FF00;
						blueMask = 0x000000FF;
					}
				}

				properties.put("red_mask", Integer.valueOf(redMask));
				properties.put("green_mask", Integer.valueOf(greenMask));
				properties.put("blue_mask", Integer.valueOf(blueMask));
				properties.put("alpha_mask", Integer.valueOf(alphaMask));

				// Read in the palette
				int numberOfEntries = (int) ((bitmapOffset - 14 - size) / 4);
				int sizeOfPalette = numberOfEntries * 4;
				if (bitmapOffset == size) {
					switch (imageType) {
						case VERSION_4_1_BIT:
							sizeOfPalette = (int) (colorsUsed == 0 ? 2 : colorsUsed) * 4;
							break;
						case VERSION_4_4_BIT:
							sizeOfPalette = (int) (colorsUsed == 0 ? 16 : colorsUsed) * 4;
							break;
						case VERSION_4_8_BIT:
							sizeOfPalette = (int) (colorsUsed == 0 ? 256 : colorsUsed) * 4;
							break;
						default:
							sizeOfPalette = 0;
							break;
					}
					bitmapOffset = size + sizeOfPalette;
				}
				readPalette(sizeOfPalette);

				switch ((int) csType) {
					case LCS_CALIBRATED_RGB:
						// All the new fields are valid only for this case
						properties.put("color_space", "LCS_CALIBRATED_RGB");
						properties.put("redX", Integer.valueOf(redX));
						properties.put("redY", Integer.valueOf(redY));
						properties.put("redZ", Integer.valueOf(redZ));
						properties.put("greenX", Integer.valueOf(greenX));
						properties.put("greenY", Integer.valueOf(greenY));
						properties.put("greenZ", Integer.valueOf(greenZ));
						properties.put("blueX", Integer.valueOf(blueX));
						properties.put("blueY", Integer.valueOf(blueY));
						properties.put("blueZ", Integer.valueOf(blueZ));
						properties.put("gamma_red", Long.valueOf(gammaRed));
						properties.put("gamma_green", Long.valueOf(gammaGreen));
						properties.put("gamma_blue", Long.valueOf(gammaBlue));

						// break;
						throw new RuntimeException("Not implemented yet.");

					case LCS_sRGB:
						// Default Windows color space
						properties.put("color_space", "LCS_sRGB");
						break;

					case LCS_CMYK:
						properties.put("color_space", "LCS_CMYK");
						// break;
						throw new RuntimeException("Not implemented yet.");
				}

			} else {
				properties.put("bmp_version", "BMP v. 5.x");
				throw new RuntimeException("BMP version 5 not implemented yet.");
			}
		}

		if (height > 0) {
			// bottom up image
			isBottomUp = true;
		} else {
			// top down image
			isBottomUp = false;
			height = Math.abs(height);
		}
		// When number of bitsPerPixel is <= 8, we use IndexColorModel.
		if (bitsPerPixel == 1 || bitsPerPixel == 4 || bitsPerPixel == 8) {

			numBands = 1;

			// Create IndexColorModel from the palette.
			byte r[], g[], b[];
			int sizep;
			if (imageType == VERSION_2_1_BIT || imageType == VERSION_2_4_BIT || imageType == VERSION_2_8_BIT) {

				sizep = palette.length / 3;

				if (sizep > 256) {
					sizep = 256;
				}

				int off;
				r = new byte[sizep];
				g = new byte[sizep];
				b = new byte[sizep];
				for (int i = 0; i < sizep; i++) {
					off = 3 * i;
					b[i] = palette[off];
					g[i] = palette[off + 1];
					r[i] = palette[off + 2];
				}
			} else {
				sizep = palette.length / 4;

				if (sizep > 256) {
					sizep = 256;
				}

				int off;
				r = new byte[sizep];
				g = new byte[sizep];
				b = new byte[sizep];
				for (int i = 0; i < sizep; i++) {
					off = 4 * i;
					b[i] = palette[off];
					g[i] = palette[off + 1];
					r[i] = palette[off + 2];
				}
			}

		} else if (bitsPerPixel == 16) {
			numBands = 3;
		} else if (bitsPerPixel == 32) {
			numBands = alphaMask == 0 ? 3 : 4;

			// The number of bands in the SampleModel is determined by
			// the length of the mask array passed in.
		} else {
			numBands = 3;
		}
	}

	private byte[] getPalette(int group) {
		if (palette == null) {
			return null;
		}
		byte np[] = new byte[palette.length / group * 3];
		int e = palette.length / group;
		for (int k = 0; k < e; ++k) {
			int src = k * group;
			int dest = k * 3;
			np[dest + 2] = palette[src++];
			np[dest + 1] = palette[src++];
			np[dest] = palette[src];
		}
		return np;
	}

	private Image getImage() throws IOException, BadElementException {
		byte bdata[] = null; // buffer for byte data

		// if (sampleModel.getDataType() == DataBuffer.TYPE_BYTE)
		// bdata = (byte[])((DataBufferByte)tile.getDataBuffer()).getData();
		// else if (sampleModel.getDataType() == DataBuffer.TYPE_USHORT)
		// sdata = (short[])((DataBufferUShort)tile.getDataBuffer()).getData();
		// else if (sampleModel.getDataType() == DataBuffer.TYPE_INT)
		// idata = (int[])((DataBufferInt)tile.getDataBuffer()).getData();

		// There should only be one tile.
		switch (imageType) {

			case VERSION_2_1_BIT:
				// no compression
				return read1Bit(3);

			case VERSION_2_4_BIT:
				// no compression
				return read4Bit(3);

			case VERSION_2_8_BIT:
				// no compression
				return read8Bit(3);

			case VERSION_2_24_BIT:
				// no compression
				bdata = new byte[width * height * 3];
				read24Bit(bdata);
				return new ImgRaw(width, height, 3, 8, bdata);

			case VERSION_3_1_BIT:
				// 1-bit images cannot be compressed.
				return read1Bit(4);

			case VERSION_3_4_BIT:
				switch ((int) compression) {
					case BI_RGB:
						return read4Bit(4);

					case BI_RLE4:
						return readRLE4();

					default:
						throw new RuntimeException("Invalid compression specified for BMP file.");
				}

			case VERSION_3_8_BIT:
				switch ((int) compression) {
					case BI_RGB:
						return read8Bit(4);

					case BI_RLE8:
						return readRLE8();

					default:
						throw new RuntimeException("Invalid compression specified for BMP file.");
				}

			case VERSION_3_24_BIT:
				// 24-bit images are not compressed
				bdata = new byte[width * height * 3];
				read24Bit(bdata);
				return new ImgRaw(width, height, 3, 8, bdata);

			case VERSION_3_NT_16_BIT:
				return read1632Bit(false);

			case VERSION_3_NT_32_BIT:
				return read1632Bit(true);

			case VERSION_4_1_BIT:
				return read1Bit(4);

			case VERSION_4_4_BIT:
				switch ((int) compression) {

					case BI_RGB:
						return read4Bit(4);

					case BI_RLE4:
						return readRLE4();

					default:
						throw new RuntimeException("Invalid compression specified for BMP file.");
				}

			case VERSION_4_8_BIT:
				switch ((int) compression) {

					case BI_RGB:
						return read8Bit(4);

					case BI_RLE8:
						return readRLE8();

					default:
						throw new RuntimeException("Invalid compression specified for BMP file.");
				}

			case VERSION_4_16_BIT:
				return read1632Bit(false);

			case VERSION_4_24_BIT:
				bdata = new byte[width * height * 3];
				read24Bit(bdata);
				return new ImgRaw(width, height, 3, 8, bdata);

			case VERSION_4_32_BIT:
				return read1632Bit(true);
		}
		return null;
	}

	private Image indexedModel(byte bdata[], int bpc, int paletteEntries) throws BadElementException {
		Image img = new ImgRaw(width, height, 1, bpc, bdata);
		PdfArray colorspace = new PdfArray();
		colorspace.add(PdfName.INDEXED);
		colorspace.add(PdfName.DEVICERGB);
		byte np[] = getPalette(paletteEntries);
		int len = np.length;
		colorspace.add(new PdfNumber(len / 3 - 1));
		colorspace.add(new PdfString(np));
		PdfDictionary ad = new PdfDictionary();
		ad.put(PdfName.COLORSPACE, colorspace);
		img.setAdditional(ad);
		return img;
	}

	private void readPalette(int sizeOfPalette) throws IOException {
		if (sizeOfPalette == 0) {
			return;
		}

		palette = new byte[sizeOfPalette];
		int bytesRead = 0;
		while (bytesRead < sizeOfPalette) {
			int r = inputStream.read(palette, bytesRead, sizeOfPalette - bytesRead);
			if (r < 0) {
				throw new RuntimeException("incomplete palette");
			}
			bytesRead += r;
		}
		properties.put("palette", palette);
	}

	// Deal with 1 Bit images using IndexColorModels
	private Image read1Bit(int paletteEntries) throws IOException, BadElementException {
		byte bdata[] = new byte[(width + 7) / 8 * height];
		int padding = 0;
		int bytesPerScanline = (int) Math.ceil(width / 8.0d);

		int remainder = bytesPerScanline % 4;
		if (remainder != 0) {
			padding = 4 - remainder;
		}

		int imSize = (bytesPerScanline + padding) * height;

		// Read till we have the whole image
		byte values[] = new byte[imSize];
		int bytesRead = 0;
		while (bytesRead < imSize) {
			bytesRead += inputStream.read(values, bytesRead, imSize - bytesRead);
		}

		if (isBottomUp) {

			// Convert the bottom up image to a top down format by copying
			// one scanline from the bottom to the top at a time.

			for (int i = 0; i < height; i++) {
				System.arraycopy(values, imSize - (i + 1) * (bytesPerScanline + padding), bdata, i * bytesPerScanline, bytesPerScanline);
			}
		} else {

			for (int i = 0; i < height; i++) {
				System.arraycopy(values, i * (bytesPerScanline + padding), bdata, i * bytesPerScanline, bytesPerScanline);
			}
		}
		return indexedModel(bdata, 1, paletteEntries);
	}

	// Method to read a 4 bit BMP image data
	private Image read4Bit(int paletteEntries) throws IOException, BadElementException {
		byte bdata[] = new byte[(width + 1) / 2 * height];

		// Padding bytes at the end of each scanline
		int padding = 0;

		int bytesPerScanline = (int) Math.ceil(width / 2.0d);
		int remainder = bytesPerScanline % 4;
		if (remainder != 0) {
			padding = 4 - remainder;
		}

		int imSize = (bytesPerScanline + padding) * height;

		// Read till we have the whole image
		byte values[] = new byte[imSize];
		int bytesRead = 0;
		while (bytesRead < imSize) {
			bytesRead += inputStream.read(values, bytesRead, imSize - bytesRead);
		}

		if (isBottomUp) {

			// Convert the bottom up image to a top down format by copying
			// one scanline from the bottom to the top at a time.
			for (int i = 0; i < height; i++) {
				System.arraycopy(values, imSize - (i + 1) * (bytesPerScanline + padding), bdata, i * bytesPerScanline, bytesPerScanline);
			}
		} else {
			for (int i = 0; i < height; i++) {
				System.arraycopy(values, i * (bytesPerScanline + padding), bdata, i * bytesPerScanline, bytesPerScanline);
			}
		}
		return indexedModel(bdata, 4, paletteEntries);
	}

	// Method to read 8 bit BMP image data
	private Image read8Bit(int paletteEntries) throws IOException, BadElementException {
		byte bdata[] = new byte[width * height];
		// Padding bytes at the end of each scanline
		int padding = 0;

		// width * bitsPerPixel should be divisible by 32
		int bitsPerScanline = width * 8;
		if (bitsPerScanline % 32 != 0) {
			padding = (bitsPerScanline / 32 + 1) * 32 - bitsPerScanline;
			padding = (int) Math.ceil(padding / 8.0);
		}

		int imSize = (width + padding) * height;

		// Read till we have the whole image
		byte values[] = new byte[imSize];
		int bytesRead = 0;
		while (bytesRead < imSize) {
			bytesRead += inputStream.read(values, bytesRead, imSize - bytesRead);
		}

		if (isBottomUp) {

			// Convert the bottom up image to a top down format by copying
			// one scanline from the bottom to the top at a time.
			for (int i = 0; i < height; i++) {
				System.arraycopy(values, imSize - (i + 1) * (width + padding), bdata, i * width, width);
			}
		} else {
			for (int i = 0; i < height; i++) {
				System.arraycopy(values, i * (width + padding), bdata, i * width, width);
			}
		}
		return indexedModel(bdata, 8, paletteEntries);
	}

	// Method to read 24 bit BMP image data
	private void read24Bit(byte[] bdata) {
		// Padding bytes at the end of each scanline
		int padding = 0;

		// width * bitsPerPixel should be divisible by 32
		int bitsPerScanline = width * 24;
		if (bitsPerScanline % 32 != 0) {
			padding = (bitsPerScanline / 32 + 1) * 32 - bitsPerScanline;
			padding = (int) Math.ceil(padding / 8.0);
		}

		int imSize = (width * 3 + 3) / 4 * 4 * height;
		// Read till we have the whole image
		byte values[] = new byte[imSize];
		try {
			int bytesRead = 0;
			while (bytesRead < imSize) {
				int r = inputStream.read(values, bytesRead, imSize - bytesRead);
				if (r < 0) {
					break;
				}
				bytesRead += r;
			}
		} catch (IOException ioe) {
			throw new ExceptionConverter(ioe);
		}

		int l = 0, count;

		if (isBottomUp) {
			int max = width * height * 3 - 1;

			count = -padding;
			for (int i = 0; i < height; i++) {
				l = max - (i + 1) * width * 3 + 1;
				count += padding;
				for (int j = 0; j < width; j++) {
					bdata[l + 2] = values[count++];
					bdata[l + 1] = values[count++];
					bdata[l] = values[count++];
					l += 3;
				}
			}
		} else {
			count = -padding;
			for (int i = 0; i < height; i++) {
				count += padding;
				for (int j = 0; j < width; j++) {
					bdata[l + 2] = values[count++];
					bdata[l + 1] = values[count++];
					bdata[l] = values[count++];
					l += 3;
				}
			}
		}
	}

	private int findMask(int mask) {
		int k = 0;
		for (; k < 32; ++k) {
			if ((mask & 1) == 1) {
				break;
			}
			mask >>>= 1;
		}
		return mask;
	}

	private int findShift(int mask) {
		int k = 0;
		for (; k < 32; ++k) {
			if ((mask & 1) == 1) {
				break;
			}
			mask >>>= 1;
		}
		return k;
	}

	private Image read1632Bit(boolean is32) throws IOException, BadElementException {

		int red_mask = findMask(redMask);
		int red_shift = findShift(redMask);
		int red_factor = red_mask + 1;
		int green_mask = findMask(greenMask);
		int green_shift = findShift(greenMask);
		int green_factor = green_mask + 1;
		int blue_mask = findMask(blueMask);
		int blue_shift = findShift(blueMask);
		int blue_factor = blue_mask + 1;
		byte bdata[] = new byte[width * height * 3];
		// Padding bytes at the end of each scanline
		int padding = 0;

		if (!is32) {
			// width * bitsPerPixel should be divisible by 32
			int bitsPerScanline = width * 16;
			if (bitsPerScanline % 32 != 0) {
				padding = (bitsPerScanline / 32 + 1) * 32 - bitsPerScanline;
				padding = (int) Math.ceil(padding / 8.0);
			}
		}

		int imSize = (int) imageSize;
		if (imSize == 0) {
			imSize = (int) (bitmapFileSize - bitmapOffset);
		}

		int l = 0;
		int v;
		if (isBottomUp) {
			for (int i = height - 1; i >= 0; --i) {
				l = width * 3 * i;
				for (int j = 0; j < width; j++) {
					if (is32) {
						v = (int) readDWord(inputStream);
					} else {
						v = readWord(inputStream);
					}
					bdata[l++] = (byte) ((v >>> red_shift & red_mask) * 256 / red_factor);
					bdata[l++] = (byte) ((v >>> green_shift & green_mask) * 256 / green_factor);
					bdata[l++] = (byte) ((v >>> blue_shift & blue_mask) * 256 / blue_factor);
				}
				for (int m = 0; m < padding; m++) {
					inputStream.read();
				}
			}
		} else {
			for (int i = 0; i < height; i++) {
				for (int j = 0; j < width; j++) {
					if (is32) {
						v = (int) readDWord(inputStream);
					} else {
						v = readWord(inputStream);
					}
					bdata[l++] = (byte) ((v >>> red_shift & red_mask) * 256 / red_factor);
					bdata[l++] = (byte) ((v >>> green_shift & green_mask) * 256 / green_factor);
					bdata[l++] = (byte) ((v >>> blue_shift & blue_mask) * 256 / blue_factor);
				}
				for (int m = 0; m < padding; m++) {
					inputStream.read();
				}
			}
		}
		return new ImgRaw(width, height, 3, 8, bdata);
	}

	private Image readRLE8() throws IOException, BadElementException {

		// If imageSize field is not provided, calculate it.
		int imSize = (int) imageSize;
		if (imSize == 0) {
			imSize = (int) (bitmapFileSize - bitmapOffset);
		}

		// Read till we have the whole image
		byte values[] = new byte[imSize];
		int bytesRead = 0;
		while (bytesRead < imSize) {
			bytesRead += inputStream.read(values, bytesRead, imSize - bytesRead);
		}

		// Since data is compressed, decompress it
		byte val[] = decodeRLE(true, values);

		// Uncompressed data does not have any padding
		imSize = width * height;

		if (isBottomUp) {

			// Convert the bottom up image to a top down format by copying
			// one scanline from the bottom to the top at a time.
			// int bytesPerScanline = (int)Math.ceil((double)width/8.0);
			byte temp[] = new byte[val.length];
			int bytesPerScanline = width;
			for (int i = 0; i < height; i++) {
				System.arraycopy(val, imSize - (i + 1) * bytesPerScanline, temp, i * bytesPerScanline, bytesPerScanline);
			}
			val = temp;
		}
		return indexedModel(val, 8, 4);
	}

	private Image readRLE4() throws IOException, BadElementException {

		// If imageSize field is not specified, calculate it.
		int imSize = (int) imageSize;
		if (imSize == 0) {
			imSize = (int) (bitmapFileSize - bitmapOffset);
		}

		// Read till we have the whole image
		byte values[] = new byte[imSize];
		int bytesRead = 0;
		while (bytesRead < imSize) {
			bytesRead += inputStream.read(values, bytesRead, imSize - bytesRead);
		}

		// Decompress the RLE4 compressed data.
		byte val[] = decodeRLE(false, values);

		// Invert it as it is bottom up format.
		if (isBottomUp) {

			byte inverted[] = val;
			val = new byte[width * height];
			int l = 0, index, lineEnd;

			for (int i = height - 1; i >= 0; i--) {
				index = i * width;
				lineEnd = l + width;
				while (l != lineEnd) {
					val[l++] = inverted[index++];
				}
			}
		}
		int stride = (width + 1) / 2;
		byte bdata[] = new byte[stride * height];
		int ptr = 0;
		int sh = 0;
		for (int h = 0; h < height; ++h) {
			for (int w = 0; w < width; ++w) {
				if ((w & 1) == 0) {
					bdata[sh + w / 2] = (byte) (val[ptr++] << 4);
				} else {
					bdata[sh + w / 2] |= (byte) (val[ptr++] & 0x0f);
				}
			}
			sh += stride;
		}
		return indexedModel(bdata, 4, 4);
	}

	private byte[] decodeRLE(boolean is8, byte values[]) {
		byte val[] = new byte[width * height];
		try {
			int ptr = 0;
			int x = 0;
			int q = 0;
			for (int y = 0; y < height && ptr < values.length;) {
				int count = values[ptr++] & 0xff;
				if (count != 0) {
					// encoded mode
					int bt = values[ptr++] & 0xff;
					if (is8) {
						for (int i = count; i != 0; --i) {
							val[q++] = (byte) bt;
						}
					} else {
						for (int i = 0; i < count; ++i) {
							val[q++] = (byte) ((i & 1) == 1 ? bt & 0x0f : bt >>> 4 & 0x0f);
						}
					}
					x += count;
				} else {
					// escape mode
					count = values[ptr++] & 0xff;
					if (count == 1) {
						break;
					}
					switch (count) {
						case 0:
							x = 0;
							++y;
							q = y * width;
							break;
						case 2:
							// delta mode
							x += values[ptr++] & 0xff;
							y += values[ptr++] & 0xff;
							q = y * width + x;
							break;
						default:
							// absolute mode
							if (is8) {
								for (int i = count; i != 0; --i) {
									val[q++] = (byte) (values[ptr++] & 0xff);
								}
							} else {
								int bt = 0;
								for (int i = 0; i < count; ++i) {
									if ((i & 1) == 0) {
										bt = values[ptr++] & 0xff;
									}
									val[q++] = (byte) ((i & 1) == 1 ? bt & 0x0f : bt >>> 4 & 0x0f);
								}
							}
							x += count;
							// read pad byte
							if (is8) {
								if ((count & 1) == 1) {
									++ptr;
								}
							} else {
								if ((count & 3) == 1 || (count & 3) == 2) {
									++ptr;
								}
							}
							break;
					}
				}
			}
		} catch (RuntimeException e) {
			// empty on purpose
		}

		return val;
	}

	// Windows defined data type reading methods - everything is little endian

	// Unsigned 8 bits
	private int readUnsignedByte(InputStream stream) throws IOException {
		return stream.read() & 0xff;
	}

	// Unsigned 2 bytes
	private int readUnsignedShort(InputStream stream) throws IOException {
		int b1 = readUnsignedByte(stream);
		int b2 = readUnsignedByte(stream);
		return (b2 << 8 | b1) & 0xffff;
	}

	// Signed 16 bits
	private int readShort(InputStream stream) throws IOException {
		int b1 = readUnsignedByte(stream);
		int b2 = readUnsignedByte(stream);
		return b2 << 8 | b1;
	}

	// Unsigned 16 bits
	private int readWord(InputStream stream) throws IOException {
		return readUnsignedShort(stream);
	}

	// Unsigned 4 bytes
	private long readUnsignedInt(InputStream stream) throws IOException {
		int b1 = readUnsignedByte(stream);
		int b2 = readUnsignedByte(stream);
		int b3 = readUnsignedByte(stream);
		int b4 = readUnsignedByte(stream);
		long l = b4 << 24 | b3 << 16 | b2 << 8 | b1;
		return l & 0xffffffff;
	}

	// Signed 4 bytes
	private int readInt(InputStream stream) throws IOException {
		int b1 = readUnsignedByte(stream);
		int b2 = readUnsignedByte(stream);
		int b3 = readUnsignedByte(stream);
		int b4 = readUnsignedByte(stream);
		return b4 << 24 | b3 << 16 | b2 << 8 | b1;
	}

	// Unsigned 4 bytes
	private long readDWord(InputStream stream) throws IOException {
		return readUnsignedInt(stream);
	}

	// 32 bit signed value
	private int readLong(InputStream stream) throws IOException {
		return readInt(stream);
	}
}