/* $RCSfile$
 * $Author: nicove $
 * $Date: 2007-03-30 12:26:16 -0500 (Fri, 30 Mar 2007) $
 * $Revision: 7275 $
 *
 * Copyright (C) 2002-2005  The Jmol Development Team
 *
 * Contact: jmol-developers@lists.sf.net
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) 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
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 */
package javajs.img;

import java.io.BufferedInputStream;
import java.io.IOException;

import javajs.util.Rdr;

/**
 * src: http://www.javaworld.com/article/2077542/learn-java/java-tip-43--how-to-
 * read-8--and-24-bit-microsoft-windows-bitmaps-in-java-applications.html
 *
 * see also: http://en.wikipedia.org/wiki/BMP_file_format
 * 
 * Modified by Bob Hanson hansonr@stolaf.edu
 * 
 * @author Bob Hanson (hansonr@stolaf.edu)
 * 
 */
public class BMPDecoder {

  public BMPDecoder() {
    // for reflection
  }
  
  private BufferedInputStream bis;

  /**
   * original comment:
   * 
   * loadbitmap() method converted from Windows C code. Reads only uncompressed
   * 24- and 8-bit images. Tested with images saved using Microsoft Paint in
   * Windows 95. If the image is not a 24- or 8-bit image, the program refuses
   * to even try. I guess one could include 4-bit images by masking the byte by
   * first 1100 and then 0011. I am not really interested in such images. If a
   * compressed image is attempted, the routine will probably fail by generating
   * an IOException. Look for variable ncompression to be different from 0 to
   * indicate compression is present.
   * 
   * @param bytes
   * @return [image byte array, width, height]
   */
  public Object[] decodeWindowsBMP(byte[] bytes) {
    try {
      bis = Rdr.getBIS(bytes);
      temp = new byte[4];
      // read BITMAPFILEHEADER
      if (readByte() != 'B' || readByte() != 'M')
        return null;
      readInt(); // file size; ignored
      readShort(); // reserved
      readShort(); // reserved
      readInt(); // ptr to pixel array; ignored
      int imageWidth, imageHeight, bitsPerPixel, nColors = 0, imageSize = 0;
      // read BITMAP header
      int headerSize = readInt();
      switch (headerSize) {
      case 12:
        // BITMAPCOREHEADER
        imageWidth = readShort();
        imageHeight = readShort();
        readShort(); // planes
        bitsPerPixel = readShort();
        break;
      case 40:
        // BITMAPINFOHEADER
        imageWidth = readInt();
        imageHeight = readInt();
        readShort(); // planes
        bitsPerPixel = readShort();
        int ncompression = readInt();
        if (ncompression != 0) {
          System.out.println("BMP Compression is :" + ncompression
              + " -- aborting");
          return null;
        }
        imageSize = readInt();
        readInt(); // hres
        readInt(); // vres
        nColors = readInt();
        readInt(); // colors used
        break;
      default:
        System.out.println("BMP Header unrecognized, length=" + headerSize
            + " -- aborting");
        return null;
      }
      boolean isYReversed = (imageHeight < 0);
      if (isYReversed)
        imageHeight = -imageHeight;
      int nPixels = imageHeight * imageWidth;
      int bytesPerPixel = bitsPerPixel / 8;
      nColors = (nColors > 0 ? nColors : 1 << bitsPerPixel);
      int npad = (bytesPerPixel == 4 ? 0
          : imageSize == 0 ? 4 - (imageWidth % 4) : (imageSize / imageHeight)
              - imageWidth * bytesPerPixel) % 4;
      int[] palette;
      int[] buf = new int[nPixels];
      int dpt = (isYReversed ? imageWidth : -imageWidth);
      int pt0 = (isYReversed ? 0 : nPixels + dpt);
      int pt1 = (isYReversed ? nPixels : dpt);
      switch (bitsPerPixel) {
      case 32:
      case 24:
        for (int pt = pt0; pt != pt1; pt += dpt, pad(npad))
          for (int i = 0; i < imageWidth; i++)
            buf[pt + i] = readColor(bytesPerPixel);
        break;
      case 8:
        palette = new int[nColors];
        for (int i = 0; i < nColors; i++)
          palette[i] = readColor(4);
        for (int pt = pt0; pt != pt1; pt += dpt, pad(npad))
          for (int i = 0; i < imageWidth; i++)
            buf[pt + i] = palette[readByte()];
        break;
      case 4:
        npad = (4 - (((imageWidth + 1) / 2) % 4)) % 4;
        palette = new int[nColors];
        for (int i = 0; i < nColors; i++)
          palette[i] = readColor(4);
        int b4 = 0;
        for (int pt = pt0; pt != pt1; pt += dpt, pad(npad))
          for (int i = 0, shift = 4; i < imageWidth; i++, shift = 4 - shift)
            buf[pt + i] = palette[((shift == 4 ? (b4 = readByte()) : b4) >> shift) & 0xF];
        break;
      case 1:
        int color1 = readColor(3);
        int color2 = readColor(3);
        npad = (4 - (((imageWidth + 7) / 8) % 4)) % 4;
        int b = 0;
        for (int pt = pt0; pt != pt1; pt += dpt, pad(npad))
          for (int i = 0, bpt = -1; i < imageWidth; i++, bpt--) {
            if (bpt < 0) {
              b = readByte();
              bpt = 7;
            }
            buf[pt + i] = ((b & (1 << bpt)) == 0 ? color1 : color2);
          }
        break;
      case 64:
      case 2:
      default:
        System.out
            .println("Not a 32-, 24-, 8-, 4-, or 1-bit Windows Bitmap, aborting...");
        return null;
      }
      return new Object[] { buf, Integer.valueOf(imageWidth),
          Integer.valueOf(imageHeight) };
    } catch (Exception e) {
      System.out.println("Caught exception in loadbitmap!");
    }
    return null;
  }

  private boolean pad(int npad) throws IOException {
    for (int i = 0; i < npad; i++)
      readByte();
    return true;
  }

  private byte[] temp;
  
  private int readColor(int n) throws IOException {
    bis.read(temp, 0, n);
    return 0xff << 24 | ((temp[2] & 0xff) << 16)
        | ((temp[1] & 0xff) << 8) | temp[0] & 0xff;
  }

  private int readInt() throws IOException {
    bis.read(temp, 0, 4);
    return ((temp[3] & 0xff) << 24) | ((temp[2] & 0xff) << 16)
        | ((temp[1] & 0xff) << 8) | temp[0] & 0xff;
  }

  private int readShort() throws IOException {
    bis.read(temp, 0, 2);
    return ((temp[1] & 0xff) << 8) | temp[0] & 0xff;
  }

  private int readByte() throws IOException {
    bis.read(temp, 0, 1);
    return temp[0] & 0xff;
  }

}
