I have a Bitmap in memory and I need to save it in a bmp file (using the bmp file format).
Is there any way to do it on Android ?
(I read a lot of post suggesting to use the png format - which is loss-less - but, that's not what I need: I really need the bmp format).
I already have some code to save it in jpeg or png using the Bitmap.compress method :
/**
* Save data to file using format.
* When format is null : the bitmap will be saved in bmp format
**/
public void writeBitmapToFile(Bitmap data, File file, Bitmap.CompressFormat format) {
FileOutputStream os = null;
try {
os = new FileOutputStream(file);
if(format==null){
//TODO : write data to file using the bmp format
}else{
data.compress(format, 100, os); //ok for JPEG and PNG
}
os.flush();
} catch (Exception e) {
//irrelevant code
} finally {
//irrelevant code
}
}
Unfortunately, the answers above didn't fully work for grayscale images, in my case. Not sure why.
That's why I wrote this one (Additionally I'm using a ByteBuffer
, because of the performance):
import android.graphics.Bitmap;
import android.util.Log;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
public class BmpFile {
// 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[] = {'B', '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 biHeight = 0;
private int biPlanes = 1;
private int biBitCount = 24;
private int biCompression = 0;
private int biSizeImage = 0x030000;
private int biXPelsPerMeter = 0x0;
private int biYPelsPerMeter = 0x0;
private int biClrUsed = 0;
private int biClrImportant = 0;
// Bitmap raw data
private int pixels[];
// File section
private ByteBuffer buffer = null;
private OutputStream outputStream;
// Default constructor
public BmpFile() {
}
public void saveBitmap(
String parFilename,
Bitmap bitmap
) {
try {
outputStream = new FileOutputStream(parFilename);
save(bitmap);
outputStream.close();
} catch (Exception e) {
Log.e("Exception", e.getMessage());
}
}
public void saveBitmap(
Bitmap bitmap,
OutputStream outputStream
) {
this.outputStream = outputStream;
save(bitmap);
}
/*
* 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(
Bitmap bitmap
) {
try {
convertImage(bitmap);
writeBitmapFileHeader();
writeBitmapInfoHeader();
writeBitmap();
// write to output stream
outputStream.write(buffer.array());
} catch (Exception e) {
Log.e("Exception", e.getMessage());
}
}
/*
* convertImage converts the memory image to the bitmap format (BRG).
* It also computes some information for the bitmap info header.
*/
private boolean convertImage(
Bitmap bitmap
) {
int pad;
int width = bitmap.getWidth();
int height = bitmap.getHeight();
pixels = new int[width * height];
bitmap.getPixels(
pixels,
0,
width,
0,
0,
width,
height);
pad = (4 - ((width * 3) % 4)) * height;
biSizeImage = ((width * height) * 3) + pad;
bfSize = biSizeImage + BITMAPFILEHEADER_SIZE +
BITMAPINFOHEADER_SIZE;
buffer = ByteBuffer.allocate(bfSize);
biWidth = width;
biHeight = height;
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() {
int size;
int value;
int j;
int i;
int rowCount;
int rowIndex;
int lastRowIndex;
int pad;
int padCount;
byte rgb[] = new byte[3];
size = (biWidth * biHeight) - 1;
pad = 4 - ((biWidth * 3) % 4);
if (pad == 4) // Bug correction
pad = 0; // Bug correction
rowCount = 1;
padCount = 0;
rowIndex = size - biWidth;
lastRowIndex = rowIndex;
try {
for (j = 0; j < size; j++) {
value = pixels[rowIndex];
rgb[0] = (byte) (value & 0xFF);
rgb[1] = (byte) ((value >> 8) & 0xFF);
rgb[2] = (byte) ((value >> 16) & 0xFF);
buffer.put(rgb);
if (rowCount == biWidth) {
padCount += pad;
for (i = 1; i <= pad; i++) {
buffer.put((byte) 0x00);
}
rowCount = 1;
rowIndex = lastRowIndex - biWidth;
lastRowIndex = rowIndex;
} else
rowCount++;
rowIndex++;
}
// Update the size of the file
bfSize += padCount - pad;
biSizeImage += padCount - pad;
} catch (Exception e) {
Log.e("Exception", e.getMessage());
}
}
/*
* writeBitmapFileHeader writes the bitmap file header to the file.
*/
private void writeBitmapFileHeader() {
try {
buffer.put(bfType);
buffer.put(intToDWord(bfSize));
buffer.put(intToWord(bfReserved1));
buffer.put(intToWord(bfReserved2));
buffer.put(intToDWord(bfOffBits));
} catch (Exception e) {
Log.e("Exception", e.getMessage());
}
}
/*
* writeBitmapInfoHeader writes the bitmap information header
* to the file.
*/
private void writeBitmapInfoHeader() {
try {
buffer.put(intToDWord(biSize));
buffer.put(intToDWord(biWidth));
buffer.put(intToDWord(biHeight));
buffer.put(intToWord(biPlanes));
buffer.put(intToWord(biBitCount));
buffer.put(intToDWord(biCompression));
buffer.put(intToDWord(biSizeImage));
buffer.put(intToDWord(biXPelsPerMeter));
buffer.put(intToDWord(biYPelsPerMeter));
buffer.put(intToDWord(biClrUsed));
buffer.put(intToDWord(biClrImportant));
} catch (Exception e) {
Log.e("Exception", e.getMessage());
}
}
/*
* 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);
}
}
(I'm answering my own question)
Here is my current solution. It is derived from this source : https://github.com/ultrakain/AndroidBitmapUtil (thanks to ultrakain and @Francescoverheye )
I just fix a little bug in computation of the dummy bytes that must be added to each row (so that the length of each row in bytes is a multiple of 4 (as required by the bmp format specifications).
I also made some changes to improve the performances.
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import android.graphics.Bitmap;
import android.util.Log;
public class AndroidBmpUtil {
private static final int BMP_WIDTH_OF_TIMES = 4;
private static final int BYTE_PER_PIXEL = 3;
/**
* Android Bitmap Object to Window's v3 24bit Bmp Format File
* @param orgBitmap
* @param filePath
* @return file saved result
*/
public static boolean save(Bitmap orgBitmap, String filePath) throws IOException {
long start = System.currentTimeMillis();
if(orgBitmap == null){
return false;
}
if(filePath == null){
return false;
}
boolean isSaveSuccess = true;
//image size
int width = orgBitmap.getWidth();
int height = orgBitmap.getHeight();
//image dummy data size
//reason : the amount of bytes per image row must be a multiple of 4 (requirements of bmp format)
byte[] dummyBytesPerRow = null;
boolean hasDummy = false;
int rowWidthInBytes = BYTE_PER_PIXEL * width; //source image width * number of bytes to encode one pixel.
if(rowWidthInBytes%BMP_WIDTH_OF_TIMES>0){
hasDummy=true;
//the number of dummy bytes we need to add on each row
dummyBytesPerRow = new byte[(BMP_WIDTH_OF_TIMES-(rowWidthInBytes%BMP_WIDTH_OF_TIMES))];
//just fill an array with the dummy bytes we need to append at the end of each row
for(int i = 0; i < dummyBytesPerRow.length; i++){
dummyBytesPerRow[i] = (byte)0xFF;
}
}
//an array to receive the pixels from the source image
int[] pixels = new int[width * height];
//the number of bytes used in the file to store raw image data (excluding file headers)
int imageSize = (rowWidthInBytes+(hasDummy?dummyBytesPerRow.length:0)) * height;
//file headers size
int imageDataOffset = 0x36;
//final size of the file
int fileSize = imageSize + imageDataOffset;
//Android Bitmap Image Data
orgBitmap.getPixels(pixels, 0, width, 0, 0, width, height);
//ByteArrayOutputStream baos = new ByteArrayOutputStream(fileSize);
ByteBuffer buffer = ByteBuffer.allocate(fileSize);
/**
* BITMAP FILE HEADER Write Start
**/
buffer.put((byte)0x42);
buffer.put((byte)0x4D);
//size
buffer.put(writeInt(fileSize));
//reserved
buffer.put(writeShort((short)0));
buffer.put(writeShort((short)0));
//image data start offset
buffer.put(writeInt(imageDataOffset));
/** BITMAP FILE HEADER Write End */
//*******************************************
/** BITMAP INFO HEADER Write Start */
//size
buffer.put(writeInt(0x28));
//width, height
//if we add 3 dummy bytes per row : it means we add a pixel (and the image width is modified.
buffer.put(writeInt(width+(hasDummy?(dummyBytesPerRow.length==3?1:0):0)));
buffer.put(writeInt(height));
//planes
buffer.put(writeShort((short)1));
//bit count
buffer.put(writeShort((short)24));
//bit compression
buffer.put(writeInt(0));
//image data size
buffer.put(writeInt(imageSize));
//horizontal resolution in pixels per meter
buffer.put(writeInt(0));
//vertical resolution in pixels per meter (unreliable)
buffer.put(writeInt(0));
buffer.put(writeInt(0));
buffer.put(writeInt(0));
/** BITMAP INFO HEADER Write End */
int row = height;
int col = width;
int startPosition = (row - 1) * col;
int endPosition = row * col;
while( row > 0 ){
for(int i = startPosition; i < endPosition; i++ ){
buffer.put((byte)(pixels[i] & 0x000000FF));
buffer.put((byte)((pixels[i] & 0x0000FF00) >> 8));
buffer.put((byte)((pixels[i] & 0x00FF0000) >> 16));
}
if(hasDummy){
buffer.put(dummyBytesPerRow);
}
row--;
endPosition = startPosition;
startPosition = startPosition - col;
}
FileOutputStream fos = new FileOutputStream(filePath);
fos.write(buffer.array());
fos.close();
Log.v("AndroidBmpUtil" ,System.currentTimeMillis()-start+" ms");
return isSaveSuccess;
}
/**
* Write integer to little-endian
* @param value
* @return
* @throws IOException
*/
private static byte[] writeInt(int value) throws IOException {
byte[] b = new byte[4];
b[0] = (byte)(value & 0x000000FF);
b[1] = (byte)((value & 0x0000FF00) >> 8);
b[2] = (byte)((value & 0x00FF0000) >> 16);
b[3] = (byte)((value & 0xFF000000) >> 24);
return b;
}
/**
* Write short to little-endian byte array
* @param value
* @return
* @throws IOException
*/
private static byte[] writeShort(short value) throws IOException {
byte[] b = new byte[2];
b[0] = (byte)(value & 0x00FF);
b[1] = (byte)((value & 0xFF00) >> 8);
return b;
}
}
Code to convert android Bitmap object to 8bit bmp file. It is derived from this C# project : https://www.codeproject.com/articles/70442/c-rgb-to-palette-based-bit-greyscale-bitmap-clas
import android.graphics.Bitmap;
import android.graphics.Color;
public class BitmapConvertor {
private byte[] Color_palette = new byte[1024]; //a palette containing 256 colors
private byte[] BMP_File_Header = new byte[14];
private byte[] DIB_header = new byte[40];
private byte[] Bitmap_Data = null;
//returns a byte array of a grey scale bitmap image
public byte[] CreateGrayBitmapArray(Bitmap Image) {
try {
create_parts(Image);
//Create the array
byte[] bitmap_array = new byte[BMP_File_Header.length + DIB_header.length
+ Color_palette.length + Bitmap_Data.length];
Copy_to_Index(bitmap_array, BMP_File_Header, 0);
Copy_to_Index(bitmap_array, DIB_header, BMP_File_Header.length);
Copy_to_Index(bitmap_array, Color_palette, BMP_File_Header.length + DIB_header.length);
Copy_to_Index(bitmap_array, Bitmap_Data, BMP_File_Header.length + DIB_header.length + Color_palette.length);
return bitmap_array;
} catch (Exception e) {
return null; //return a null single byte array if fails
}
}
//creates byte array of 256 color grayscale palette
private byte[] create_palette() {
byte[] color_palette = new byte[1024];
for (int i = 0; i < 256; i++) {
color_palette[i * 4 + 0] = (byte) (i); //bule
color_palette[i * 4 + 1] = (byte) (i); //green
color_palette[i * 4 + 2] = (byte) (i); //red
color_palette[i * 4 + 3] = (byte) 0; //padding
}
return color_palette;
}
//adds dtata of Source array to Destinition array at the Index
private boolean Copy_to_Index(byte[] destination, byte[] source, int index) {
try {
for (int i = 0; i < source.length; i++) {
destination[i + index] = source[i];
}
return true;
} catch (Exception e) {
return false;
}
}
//create different part of a bitmap file
private void create_parts(Bitmap img) {
//Create Bitmap Data
Bitmap_Data = ConvertToGrayscale(img);
//Create Bitmap File Header (populate BMP_File_Header array)
Copy_to_Index(BMP_File_Header, new byte[]{(byte) 'B', (byte) 'M'}, 0); //magic number
Copy_to_Index(BMP_File_Header, writeInt(BMP_File_Header.length
+ DIB_header.length + Color_palette.length + Bitmap_Data.length), 2); //file size
Copy_to_Index(BMP_File_Header, new byte[]{(byte) 'M', (byte) 'C', (byte) 'A', (byte) 'T'}, 6); //reserved for application generating the bitmap file (not imprtant)
Copy_to_Index(BMP_File_Header, writeInt(BMP_File_Header.length
+ DIB_header.length + Color_palette.length), 10); //bitmap raw data offset
//Create DIB Header (populate DIB_header array)
Copy_to_Index(DIB_header, writeInt(DIB_header.length), 0); //DIB header length
Copy_to_Index(DIB_header, writeInt(((Bitmap) img).getWidth()), 4); //image width
Copy_to_Index(DIB_header, writeInt(((Bitmap) img).getHeight()), 8); //image height
Copy_to_Index(DIB_header, new byte[]{(byte) 1, (byte) 0}, 12); //color planes. N.B. Must be set to 1
Copy_to_Index(DIB_header, new byte[]{(byte) 8, (byte) 0}, 14); //bits per pixel
Copy_to_Index(DIB_header, writeInt(0), 16); //compression method N.B. BI_RGB = 0
Copy_to_Index(DIB_header, writeInt(Bitmap_Data.length), 20); //lenght of raw bitmap data
Copy_to_Index(DIB_header, writeInt(1000), 24); //horizontal reselution N.B. not important
Copy_to_Index(DIB_header, writeInt(1000), 28); //vertical reselution N.B. not important
Copy_to_Index(DIB_header, writeInt(256), 32); //number of colors in the palette
Copy_to_Index(DIB_header, writeInt(0), 36); //number of important colors used N.B. 0 = all colors are imprtant
//Create Color palett
Color_palette = create_palette();
}
//convert the color pixels of Source image into a grayscale bitmap (raw data)
private byte[] ConvertToGrayscale(Bitmap Source) {
Bitmap source = (Bitmap) Source;
int padding = (source.getWidth() % 4) != 0 ? 4 - (source.getWidth() % 4) : 0; //determine padding needed for bitmap file
byte[] bytes = new byte[source.getWidth() * source.getHeight() + padding * source.getHeight()]; //create array to contain bitmap data with paddin
for (int y = 0; y < source.getHeight(); y++) {
for (int x = 0; x < source.getWidth(); x++) {
int pixel = source.getPixel(x, y);
int g = (int) (0.3 * Color.red(pixel) + 0.59 * Color.green(pixel) + 0.11 * Color.blue(pixel)); //grayscale shade corresponding to rgb
bytes[(source.getHeight() - 1 - y) * source.getWidth() + (source.getHeight() - 1 - y) * padding + x] = (byte) g;
}
//add the padding
for (int i = 0; i < padding; i++) {
bytes[(source.getHeight() - y) * source.getWidth() + (source.getHeight() - 1 - y) * padding + i] = (byte) 0;
}
}
return bytes;
}
/**
* Write integer to little-endian
*
* @param value
* @return
* @throws IOException
*/
private byte[] writeInt(int value) {
byte[] b = new byte[4];
b[0] = (byte) (value & 0x000000FF);
b[1] = (byte) ((value & 0x0000FF00) >> 8);
b[2] = (byte) ((value & 0x00FF0000) >> 16);
b[3] = (byte) ((value & 0xFF000000) >> 24);
return b;
}
}
Note, the code above is very slow. I've found that to optimize the code above it's necessary to make a byte array to store all the data before you put it. Of course, I was working in Xamarin C# so maybe that's why. Anyway, here's my Xamarin code in case anyone has the same problem.
public class AndroidBmpUtil
{
private static int BMP_WIDTH_OF_TIMES = 4;
private static int BYTE_PER_PIXEL = 3;
/**
* Android Bitmap Object to Window's v3 24bit Bmp Format File
* @param orgBitmap
* @param filePath
* @return file saved result
*/
public static byte[] ConvertAndroidBitmapByteArray(Bitmap orgBitmap, String filePath)
{
if (orgBitmap == null)
{
return null;
}
if (filePath == null)
{
return null;
}
//image size
int width = orgBitmap.Width;
int height = orgBitmap.Height;
//image dummy data size
//reason : the amount of bytes per image row must be a multiple of 4 (requirements of bmp format)
byte[] dummyBytesPerRow = null;
bool hasDummy = false;
int rowWidthInBytes = BYTE_PER_PIXEL * width; //source image width * number of bytes to encode one pixel.
if (rowWidthInBytes % BMP_WIDTH_OF_TIMES > 0)
{
hasDummy = true;
//the number of dummy bytes we need to add on each row
dummyBytesPerRow = new byte[(BMP_WIDTH_OF_TIMES - (rowWidthInBytes % BMP_WIDTH_OF_TIMES))];
//just fill an array with the dummy bytes we need to append at the end of each row
for (int i = 0; i < dummyBytesPerRow.Length; i++)
{
dummyBytesPerRow[i] = (byte)0xFF;
}
}
//an array to receive the pixels from the source image
int[] pixels = new int[width * height];
//the number of bytes used in the file to store raw image data (excluding file headers)
int imageSize = (rowWidthInBytes + (hasDummy ? dummyBytesPerRow.Length : 0)) * height;
//file headers size
int imageDataOffset = 0x36;
//final size of the file
int fileSize = imageSize + imageDataOffset;
//Android Bitmap Image Data
orgBitmap.GetPixels(pixels, 0, width, 0, 0, width, height);
//ByteArrayOutputStream baos = new ByteArrayOutputStream(fileSize);
ByteBuffer buffer = ByteBuffer.Allocate(fileSize);
/**
* BITMAP FILE HEADER Write Start
**/
buffer.Put((sbyte)0x42);
buffer.Put((sbyte)0x4D);
//size
buffer.Put(writeInt(fileSize));
//reserved
buffer.Put(writeShort((short)0));
buffer.Put(writeShort((short)0));
//image data start offset
buffer.Put(writeInt(imageDataOffset));
/** BITMAP FILE HEADER Write End */
//*******************************************
/** BITMAP INFO HEADER Write Start */
//size
buffer.Put(writeInt(0x28));
//width, height
//if we add 3 dummy bytes per row : it means we add a pixel (and the image width is modified.
buffer.Put(writeInt(width + (hasDummy ? (dummyBytesPerRow.Length == 3 ? 1 : 0) : 0)));
buffer.Put(writeInt(height));
//planes
buffer.Put(writeShort((short)1));
//bit count
buffer.Put(writeShort((short)24));
//bit compression
buffer.Put(writeInt(0));
//image data size
buffer.Put(writeInt(imageSize));
//horizontal resolution in pixels per meter
buffer.Put(writeInt(0));
//vertical resolution in pixels per meter (unreliable)
buffer.Put(writeInt(0));
buffer.Put(writeInt(0));
buffer.Put(writeInt(0));
/** BITMAP INFO HEADER Write End */
int row = height;
int col = width;
int startPosition = (row - 1) * col;
int endPosition = row * col;
// This while loop is a lengthy process
// Puts take a while so only do one by creating a big array called final
byte[] final = new byte[0];
while (row > 0)
{
// This array is also used to cut down on time of puts
byte[] b = new byte[(endPosition - startPosition)*3];
int counter = 0;
for (int i = startPosition; i < endPosition; i++)
{
b[counter] = (byte)((pixels[i] & 0x000000FF));
b[counter + 1] = (byte)((pixels[i] & 0x0000FF00) >> 8);
b[counter + 2] = (byte)((pixels[i] & 0x00FF0000) >> 16);
counter += 3;
}
int finalPriorLength = final.Length;
Array.Resize<byte>(ref final, finalPriorLength + b.Length);
Array.Copy(b, 0, final, finalPriorLength, b.Length);
if (hasDummy)
{
finalPriorLength = final.Length;
Array.Resize<byte>(ref final, finalPriorLength + dummyBytesPerRow.Length);
Array.Copy(dummyBytesPerRow, 0, final, finalPriorLength, dummyBytesPerRow.Length);
}
row--;
endPosition = startPosition;
startPosition = startPosition - col;
}
buffer.Put(final);
buffer.Rewind();
IntPtr classHandle = JNIEnv.FindClass("java/nio/ByteBuffer");
IntPtr methodId = JNIEnv.GetMethodID(classHandle, "array", "()[B");
IntPtr resultHandle = JNIEnv.CallObjectMethod(buffer.Handle, methodId);
byte[] result = JNIEnv.GetArray<byte>(resultHandle);
JNIEnv.DeleteLocalRef(resultHandle);
return result;
}
/**
* Write integer to little-endian
* @param value
* @return
* @throws IOException
*/
private static byte[] writeInt(int value)
{
byte[] b = new byte[4];
b[0] = (byte)(value & 0x000000FF);
b[1] = (byte)((value & 0x0000FF00) >> 8);
b[2] = (byte)((value & 0x00FF0000) >> 16);
b[3] = (byte)((value & 0xFF000000) >> 24);
return b;
}
/**
* Write short to little-endian byte array
* @param value
* @return
* @throws IOException
*/
private static byte[] writeShort(short value)
{
byte[] b = new byte[2];
b[0] = (byte)(value & 0x00FF);
b[1] = (byte)((value & 0xFF00) >> 8);
return b;
}
}
}
To use it here's the calling code:
using (FileStream outStream = new FileStream(@yourFilePath, FileMode.Create))
{
Bitmap Signature = Bitmap.CreateBitmap(user defined values...);
byte[] buffer = AndroidBmpUtil.ConvertAndroidBitmapByteArray(Signature, @yourFilePath);
// Actually create the file
outStream.Write(buffer, 0, buffer.Length);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With