package ij.plugin;
import ij.*;
import ij.io.*;
import ij.process.*;
import java.awt.*;
import java.io.*;
import java.awt.image.*;

/** Implements the File/Save As/BMP command. Based on BMPFile class from
   http://www.javaworld.com/javaworld/javatips/jw-javatip60-p2.html */

public class BMP_Writer implements PlugIn {
 //--- Private constants
 private final static int BITMAPFILEHEADER_SIZE = 14;
 private final static int BITMAPINFOHEADER_SIZE = 40;
 //--- Private variable declaration
 //--- Bitmap file header
 private byte bitmapFileHeader [] = new byte [14];
 private byte bfType [] =  {(byte)'B', (byte)'M'};
 private int bfSize = 0;
 private int bfReserved1 = 0;
 private int bfReserved2 = 0;
 private int bfOffBits = BITMAPFILEHEADER_SIZE + BITMAPINFOHEADER_SIZE;
 //--- Bitmap info header
 private byte bitmapInfoHeader [] = new byte [40];
 private int biSize = BITMAPINFOHEADER_SIZE;
 private int biWidth = 0;
 private int padWidth = 0;
 private int biHeight = 0;
 private int biPlanes = 1;
 private int biBitCount = 24;
 private int biCompression = 0;
 private int biSizeImage = 0;
 private int biXPelsPerMeter = 0x0;
 private int biYPelsPerMeter = 0x0;
 private int biClrUsed = 0;
 private int biClrImportant = 0;
 //--- Bitmap raw data
 private int intBitmap [];
 private byte byteBitmap [];
 //--- File section
 private FileOutputStream fo;
 private BufferedOutputStream bfo;
 ImagePlus imp;

 public void run(String path) {
   IJ.showProgress(0);
   imp = WindowManager.getCurrentImage();
   if (imp==null)
     {IJ.noImage(); return;}
   try {
     writeImage(imp, path);
   } catch (Exception e) {
     String msg = e.getMessage();
     if (msg==null || msg.equals(""))
   msg = ""+e;
     IJ.error("BMP Writer", "An error occured writing the file.\n \n" + msg);
   }
   IJ.showProgress(1);
   IJ.showStatus("");
 }

 void writeImage(ImagePlus imp, String path) throws Exception {
   if(imp.getBitDepth()==24)
     biBitCount = 24;
   else {
     biBitCount = 8;
     LookUpTable lut = imp.createLut();
     biClrUsed=lut.getMapSize(); // 8 bit color image may use less
     bfOffBits+=biClrUsed*4;
   }
   if (path==null || path.equals("")) {
     String prompt = "Save as " + biBitCount + " bit BMP";
     SaveDialog sd = new SaveDialog(prompt, imp.getTitle(), ".bmp");
     if(sd.getFileName()==null)
   return;
     path = sd.getDirectory()+sd.getFileName();
   }
   imp.startTiming();
   saveBitmap (path, imp.getImage(), imp.getWidth(), imp.getHeight() );
 }


 public void saveBitmap (String parFilename, Image parImage, int parWidth, int parHeight) throws Exception {
   fo = new FileOutputStream (parFilename);
   bfo = new BufferedOutputStream(fo);
   save (parImage, parWidth, parHeight);
   bfo.close();
   fo.close ();
 }

 /*
  *   The saveMethod is the main method of the process. This method
  *   will call the convertImage method to convert the memory image to
  *   a byte array; method writeBitmapFileHeader creates and writes
  *   the bitmap file header; writeBitmapInfoHeader creates the
  *   information header; and writeBitmap writes the image.
  *
  */
 private void save (Image parImage, int parWidth, int parHeight) throws Exception {
   convertImage (parImage, parWidth, parHeight);
   writeBitmapFileHeader ();
   writeBitmapInfoHeader ();
   if(biBitCount == 8)
     writeBitmapPalette ();
   writeBitmap ();
 }

 private void writeBitmapPalette() throws Exception {
   LookUpTable lut = imp.createLut();
   byte[] g = lut.getGreens();
   byte[] r = lut.getReds();
   byte[] b = lut.getBlues();
   for(int i = 0;i<lut.getMapSize();i++) {
     bfo.write(b[i]);
     bfo.write(g[i]);
     bfo.write(r[i]);
     bfo.write(0x00);
   }
 }

 /*
  * convertImage converts the memory image to the bitmap format (BRG).
  * It also computes some information for the bitmap info header.
  *
  */
 private boolean convertImage (Image parImage, int parWidth, int parHeight) {
   int pad;
   if(biBitCount == 24)
     intBitmap = (int[]) imp.getProcessor().getPixels();
   else
     byteBitmap = (byte[]) imp.getProcessor().convertToByte(true).getPixels();
   biWidth = parWidth;
   biHeight = parHeight;
   if(biBitCount==24)
     pad = 4 - ((biWidth * 3) % 4);
   else
     pad = 4 - ((biWidth) % 4);
   if (pad == 4)       // <==== Bug correction
     pad = 0;            // <==== Bug correction
   padWidth = biWidth*(biBitCount==24?3:1)+pad;
   return (true);
 }

 /*
  * writeBitmap converts the image returned from the pixel grabber to
  * the format required. Remember: scan lines are inverted in
  * a bitmap file!
  *
  * Each scan line must be padded to an even 4-byte boundary.
  */
 private void writeBitmap () throws Exception {
   int value;
   int i;
   int pad;
   byte rgb [] = new byte [3];
   if(biBitCount==24)
     pad = 4 - ((biWidth * 3) % 4);
   else
     pad = 4 - ((biWidth) % 4);
   if (pad == 4)       // <==== Bug correction
     pad = 0;            // <==== Bug correction

   int counter=0;
   for(int row = biHeight; row>0; row--) {
     if (row%20==0)
   IJ.showProgress((double)(biHeight-row)/biHeight);
     for(int col = 0; col<biWidth; col++) {
   if(biBitCount==24) {
     value = intBitmap [(row-1)*biWidth + col ];
     rgb [0] = (byte) (value & 0xFF);
     rgb [1] = (byte) ((value >> 8) & 0xFF);
     rgb [2] = (byte) ((value >> 16) & 0xFF);
     bfo.write(rgb);
   } else
     bfo.write(byteBitmap [(row-1)*biWidth + col ]);
   ++counter;
     }
     for (i = 1; i <= pad; i++)
   bfo.write (0x00);
     counter += pad;
   }
 }


 /*
  * writeBitmapFileHeader writes the bitmap file header to the file.
  *
  */
 private void writeBitmapFileHeader() throws Exception {
   fo.write (bfType);
   // calculate bfSize
   bfSize = bfOffBits+padWidth*biHeight;
   fo.write (intToDWord (bfSize));
   fo.write (intToWord (bfReserved1));
   fo.write (intToWord (bfReserved2));
   fo.write (intToDWord (bfOffBits));
 }

 /*
  *
  * writeBitmapInfoHeader writes the bitmap information header
  * to the file.
  *
  */
 private void writeBitmapInfoHeader () throws Exception {
   fo.write (intToDWord (biSize));
   fo.write (intToDWord (biWidth));
   fo.write (intToDWord (biHeight));
   fo.write (intToWord (biPlanes));
   fo.write (intToWord (biBitCount));
   fo.write (intToDWord (biCompression));
   fo.write (intToDWord (biSizeImage));
   fo.write (intToDWord (biXPelsPerMeter));
   fo.write (intToDWord (biYPelsPerMeter));
   fo.write (intToDWord (biClrUsed));
   fo.write (intToDWord (biClrImportant));
 }

 /*
  *
  * intToWord converts an int to a word, where the return
  * value is stored in a 2-byte array.
  *
  */
 private byte [] intToWord (int parValue) {
   byte retValue [] = new byte [2];
   retValue [0] = (byte) (parValue & 0x00FF);
   retValue [1] = (byte) ((parValue >> 8) & 0x00FF);
   return (retValue);
 }

 /*
  *
  * intToDWord converts an int to a double word, where the return
  * value is stored in a 4-byte array.
  *
  */
 private byte [] intToDWord (int parValue) {
   byte retValue [] = new byte [4];
   retValue [0] = (byte) (parValue & 0x00FF);
   retValue [1] = (byte) ((parValue >> 8) & 0x000000FF);
   retValue [2] = (byte) ((parValue >> 16) & 0x000000FF);
   retValue [3] = (byte) ((parValue >> 24) & 0x000000FF);
   return (retValue);
 }
}
