/*
 * Copyright 2003 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/
 *
 *
 * The original JAI codecs have the following license
 *
 * Copyright (c) 2001 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:
 *
 * -Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * -Redistribution in binary form must reproduct 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 AND ITS LICENSORS SHALL NOT BE
 * LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING
 * OR DISTRIBUTING THE 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 SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGES.
 *
 * You acknowledge that Software is not designed,licensed or intended for use in
 * the design, construction, operation or maintenance of any nuclear facility.
 */
package com.lowagie.text.pdf.codec;

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

import com.lowagie.text.BadElementException;
import com.lowagie.text.ExceptionConverter;
import com.lowagie.text.Image;
import com.lowagie.text.ImgRaw;
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;

/** Reads a BMP image. All types of BMP can be read.
 * <p>
 * It is based in the JAI codec.
 *
 * @author  Paulo Soares (psoares@consiste.pt)
 */
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)((double)bmp.xPelsPerMeter * 0.0254), (int)((double)bmp.yPelsPerMeter * 0.0254));
            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(Image.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 {
        InputStream is = null;
        try {
            is = new ByteArrayInputStream(data);
            Image img = getImage(is);
            img.setOriginalData(data);
            return img;
        }
        finally {
            if (is != null) {
                is.close();
            }
        }
    }

    
    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", new Integer(planes));
        properties.put("bits_per_pixel", new Integer(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", new Long(xPelsPerMeter));
            properties.put("y_pixels_per_meter", new Long(yPelsPerMeter));
            properties.put("colors_used", new Long(colorsUsed));
            properties.put("colors_important", new Long(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", new Integer(redMask));
                            properties.put("green_mask", new Integer(greenMask));
                            properties.put("blue_mask", new Integer(blueMask));
                        } else if (bitsPerPixel == 32) {
                            imageType = VERSION_3_NT_32_BIT;
                            redMask   = 0x00FF0000;
                            greenMask = 0x0000FF00;
                            blueMask  = 0x000000FF;
                            properties.put("red_mask", new Integer(redMask));
                            properties.put("green_mask", new Integer(greenMask));
                            properties.put("blue_mask", new Integer(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", new Integer(redMask));
                        properties.put("green_mask", new Integer(greenMask));
                        properties.put("blue_mask", new Integer(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", new Integer(redMask));
                properties.put("green_mask", new Integer(greenMask));
                properties.put("blue_mask", new Integer(blueMask));
                properties.put("alpha_mask", new Integer(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", new Integer(redX));
                        properties.put("redY", new Integer(redY));
                        properties.put("redZ", new Integer(redZ));
                        properties.put("greenX", new Integer(greenX));
                        properties.put("greenY", new Integer(greenY));
                        properties.put("greenZ", new Integer(greenZ));
                        properties.put("blueX", new Integer(blueX));
                        properties.put("blueY", new Integer(blueY));
                        properties.put("blueZ", new Integer(blueZ));
                        properties.put("gamma_red", new Long(gammaRed));
                        properties.put("gamma_green", new Long(gammaGreen));
                        properties.put("gamma_blue", new Long(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((double)width/8.0);
        
        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((double)width/2.0);
        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 = (long)((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);
    }
}
