关于Java读取和编写BMP文件的总结
BMP到底是何方神圣?
BMP文件格式,又称为Bitmap(位图)或是DIB(Device-Independent Device, 设备无关位图),是Windows系统中广泛使用的图像文件格式。由于它可以不作任何变换 地保存图像像素域的数据,因此成为我们取得RAW数据的重要来源。Windows的图形用户界面(graphical user interfaces)也在它的内建图像子系统GDI中对BMP格式提供了支持。
那么BMP的格式到底什么?
BMP文件的数据按照从文件头开始的先后顺序分为四个部分:
Ø bmp文件头(bmp file header):提供文件的格式、大小等信息
Ø 位图信息头(bitmap information):提供图像数据的尺寸、位平面数、压 缩方式、颜色索引等信息
Ø 调色板(color palette):可选,如使用索引来表示图像,调色板就是索引 与其对应的颜色的映射表
Ø 位图数据(bitmap data):就是图像数据啦^_^
下面结合Windows结构体的定义,通过一个表来分析这四个部分。
我们一般见到的图像以24位图像为主,即R、G、B三种颜色各用8个bit来表示,这样的图像我们称为真彩色,这种情况下是不需要调色板的,也就是所位图信息头后面紧跟的就是位图数据了。因此,我们常常见到有这样一种说法:位图文件从文件头开始偏移54个字节就是位图数据了,这其实说的是24或32位图的情况。这也就解释了我们按照这种程序写出来的程序为什么对某些位图文件没用了。
下面针对一幅特定的图像进行分析,来看看在位图文件中这四个数据段的排布以及组成。
我们使用的图像显示如下:
int biSize // 信息头所需的字节数(14-17字节)
int biWidth // 位图的宽(18-21字节)
int biHeight // 位图的高(22-25字节)
int biPlanes // 目标设备的级别,必须是1(26-27字节)
int biBitcount // 每个像素所需的位数(28-29字节),必须是1位(双色)、4位(16色)、8位(256色)或者24位(真彩色)之一。
int biCompression // 位图压缩类型,必须是0(不压缩)(30-33字节)、1(BI_RLEB压缩类型)或2(BI_RLE4压缩类型)之一。
int biSizeImage // 实际位图图像的大小,即整个实际绘制的图像大小(34-37字节)
int biXPelsPerMeter // 位图水平分辨率,每米像素数(38-41字节)这个数是系统默认值
int biYPelsPerMeter // 位图垂直分辨率,每米像素数(42-45字节)这个数是系统默认值
int biClrUsed // 位图实际使用的颜色表中的颜色数(46-49字节),如果为0的话,说明全部使用了
int biClrImportant // 位图显示过程中重要的颜色数(50-53字节),如果为0的话,说明全部重要
咱们再根据解析图来重点说明下biSize、biHeight、biBitCount。
<!--EndFragment-->
根据上面我列出的信息可知,
在信息头的第一个信息就是biSize,即这个信息头所占的总共的字节空间为40个。
那么我们再结合上面所说的文件头的总字节空间为14个,那么我们相加一下是不是就是54呢!那么偏移量bfOffBits 就是54咯!不过你可能就会问了,为什么这样就是54了?怎么来的?是不是所有的bmp文件的偏移量bfOffBits 都是54呢?呵呵,不是的,不是所有的bmp文件的偏移量bfOffBits 都是54,对于24位的位图来说是的,对于其他的bmp文件就不一定了!这个偏移量很重要,大家在读取和编写的时候一定要注意它的准确性。
咱们再返回上面biSize,根据前面文件头是占14个字节,那么biSize的地址就是第14-17字节,那么对应上图,那么也就是28 00 00 00.而根据我们上面所说的机器的存储是小端存储,所以正确的读数是00 00 00 28。
而在读写后面的位图数据的时候,一定要注意,这个height到底是正数还是负数。如果是正数,那么位图数据读写的时候顺序就是从左到右,从下到上。否则就是从左到右,从上到下。因为我们这个图中height是正数,所以下面讨论位图数据的时候,就按照从左到右,从下到上的顺序来描述了。
咱们看完了bmp文件的第二块信息头,接下来的第三块颜色表就要根据第二块信息头来了,什么意思?因为如果bmp文件是24位或者32位的图像,那么就没有颜色表这一块,只有当位数少于24位的时候,才需要考虑这一块。
三.颜色表
颜色表其实是一张映射表,标识颜色索引号与其代表的颜色的对应关系。它在文件中的布局就像一个二维数组palette[N][4],其中N表示总的颜色索引数,每行的四个元素分别表示该索引对应的B、G、R和Alpha的值,每个分量占一个字节。如不设透明通道时,Alpha为0。具体的信息,大家可以google或者度娘一下。
四.位图数据
好了,接下来就是我们最重要的一块了,写入或者读取bmp文件的实际数据了,也就是我们画的图像的每个像素。我们知道一个像素由三个int值来构成,即RGB,红色分量,绿色分量,蓝色分量。而一个像素在在内存中是占三个字节的,即每个分量占据一个字节。那么我们就需要得到每个像素的分量,然后按小端存储的方式按BGR顺序存入内存中。是否这样就可以了?No,No,No。不是的,因为我们必须还要注意Windows默认的扫描的最小单位是4字节,如果数据对齐满足这个值的话对于数据的获取速度等都是有很大的增益的。因此,BMP图像顺应了这个要求,要求每行的数据的长度必须是4的倍数,如果不够需要进行比特填充(以0填充),这样可以达到按行的快速存取。这时,位图数据区的大小就未必是 图片宽×每像素字节数×图片高能表示的了,因为每行可能还需要进行比特填充。具体的实现在我下面的代码中:
BMP的读取:
package pcm24; import java.awt.Color; import java.awt.Graphics; import java.io.IOException; /** * * 类说明: * * @author 彭晨明 E-mail:[email protected] * * @version 创建时间:2012-2-3下午8:48:27 * */ public class BmpRead24 extends javax.swing.JFrame { /** * */ private static final long serialVersionUID = 1L; /** * 位图的宽 */ private static int width; /** * 位图的高 */ private static int height; /** * 位图数据数组,即一个像素的三个分量的数据数组 */ private static int[][] red, green, blue; Graphics g; public static void main(String args[]) { BmpRead24 bmp = new BmpRead24(); bmp.init(); } public void init() { try { // 通过bmp文件地址创建文件输入流对象 java.io.FileInputStream fin = new java.io.FileInputStream( "C:\\Documents and Settings\\专属于我\\桌面\\未命名1.bmp"); // 根据文件输入流对象创建原始数据输入对象 // 这里既可以用原始数据输入流来读取数据,也可以用缓冲输入流来读取,后者速度相比较快点。 // java.io.DataInputStream bis = new java.io.DataInputStream(fin); java.io.BufferedInputStream bis = new java.io.BufferedInputStream( fin); // 建立两个字节数组来得到文件头和信息头的数据 byte[] array1 = new byte[14]; bis.read(array1, 0, 14); byte[] array2 = new byte[40]; bis.read(array2, 0, 40); // 翻译bmp文件的数据,即将字节数据转化为int数据 // 通过翻译得到位图数据的宽和高 width = ChangeInt(array2, 7); height = ChangeInt(array2, 11); // 调用可以将整个位图数据读取成byte数组的方法 getInf(bis); fin.close(); bis.close(); // 创建BMP对象来显示图画 showUI(); } catch (Exception e) { e.printStackTrace(); } } /** * 实现可将四个字节翻译int数据的方法 * * @param array2 * 存储字节的字节数组 * @param start * 起始字节 * @return 返回翻译后的int数据 */ public int ChangeInt(byte[] array2, int start) { // 因为char,byte,short这些数据类型经过运算符后会自动转为成int数据类, // 所以array2[start]&0xff的实际意思就是通过&0xff将字符数据转化为正int数据,然后在进行位运算。 // 这里需要注意的是<<的优先级别比&高,所以必须加上括号。 int i = (int) ((array2[start] & 0xff) << 24) | ((array2[start - 1] & 0xff) << 16) | ((array2[start - 2] & 0xff) << 8) | (array2[start - 3] & 0xff); return i; } /** * 得到位图数据的int数组 * * @param dis * 数据输入流对象 */ public void getInf(java.io.BufferedInputStream bis) { // 给数组开辟空间 red = new int[height][width]; green = new int[height][width]; blue = new int[height][width]; // 通过计算得到每行计算机需要填充的字符数。 // 为什么要填充?这是因为windows系统在扫描数据的时候,每行都是按照4个字节的倍数来读取的。 // 因为图片是由每个像素点组成。而每个像素点都是由3个颜色分量来构成的,而每个分量占据1个字节。 // 因此在内存存储中实际图片数据每行的长度是width*3。 int skip_width = 0; int m = width * 3 % 4; if (m != 0) { skip_width = 4 - m; } // 通过遍历给数组填值 // 这里需要注意,因为根据bmp的保存格式。 // 位图数据中height的值如果是正数的话: // 那么数据就是按从下到上,从左到右的顺序来保存。这个称之为倒向位图。 // 反之就是按从上到下,从左到右的顺序来保存。这个则称之为正向位图。 for (int i = height - 1; i >= 0; i--) { for (int j = 0; j < width; j++) { try { // 这里遍历的时候,一定要注意本来像素是有RGB来表示, // 但是在存储的时候由于windows是小段存储,所以在内存中是BGR顺序。 blue[i][j] = bis.read(); green[i][j] = bis.read(); red[i][j] = bis.read(); // 这里一定要知道,其实系统在给位图数据中添加填充0的时候,都是加在每行的最后。 // 但是我们在使用dis.skipBytes()这个方法的时候,却不一定要在最后一列。 // 系统在填充数据的时候,在数据上加了标记。 // 所以dis.skipBytes()这个方法只要调用了,那么系统就会自动不读取填充数据。 if (j == 0) { bis.skip(skip_width); } } catch (IOException e) { e.printStackTrace(); } } } } public void showUI() { // 对窗体的属性进行设置 this.setTitle("BMP解析");//设置标题 this.setSize(width, height);//设置窗体大小 this.setDefaultCloseOperation(3);//点击关闭,程序自动退出。 this.setResizable(false);//设置窗体大小不可以调节 this.setLocationRelativeTo(null);//设置窗体出现在屏幕中间 //创建自己的panel,用其来显示图形。 //因为如果将图片设置到窗体上显示时,因为jframe是一个复合组件,上面的组件有多个paint方法,所以在paint的时候会画两次, //而panel是只需画一次。 MyPanel panel = new MyPanel(); java.awt.Dimension di = new java.awt.Dimension(width, height);//设置panel大小 panel.setPreferredSize(di); this.add(panel);//窗体添加panel this.setVisible(true);//使窗体可见。 } public class MyPanel extends javax.swing.JPanel { /** * */ private static final long serialVersionUID = 1L; /** * 重写paint方法 */ public void paint(Graphics g) { // 这句话可写可不写,因为这句话是用来画jframe的contentPane的。 // 而这里我们已经在下面定义了contentPane的方法了 super.paint(g); for (int i = 0; i < height; i++) { for (int j = 0; j < width; j++) { g.setColor(new Color(red[i][j], green[i][j], blue[i][j])); // 如果这里画点的话,是不能使用下面注释掉的方法的,不行的话,亲,自己试试吧 // 因为系统在画椭圆的时候,是先画出椭圆的外切矩形。而矩形的边框刚好是占据一个像素点。 // 因此也就出现了,jdk api中说g.drawOval的像素点是width+1,height+1。 // 如果亲,你有更好的理解,请告诉我们。欢迎交流!!! // g.fillOval(j, i, 1, 1); g.fillRect(j, i, 1, 1);// 这里可以使用画点的任何方法,除了上面那种特例。 } } } } }
BMP文件的编写:
package pcm24; import java.awt.Color; /** * * 类说明:实现BMP文件格式的保存 * * @author 彭晨明 E-mail:[email protected] * * @version 创建时间:2012-2-5下午9:06:28 * */ public class BmpWrite24 { /** * 图形数据数组 */ private Color[][] pointArray; /** * 图形的宽 */ int width; /** * 图形的高 */ int height; /** * BMPWrite构造器的重载,传入图形数据数组 * * @param pointArray */ public BmpWrite24(Color[][] pointArray) { this.pointArray = pointArray; this.width = pointArray.length; this.height = pointArray[0].length; this.write(); } /** * 将数据传入内存 */ public void write() { try { // 创建输出流文件对象 java.io.FileOutputStream fos = new java.io.FileOutputStream( "C:\\Documents and Settings\\专属于我\\桌面\\未命名1.bmp"); // 创建原始数据输出流对象 java.io.DataOutputStream dos = new java.io.DataOutputStream(fos); // 给文件头的变量赋值 int bfType = 0x424d; // 位图文件类型(0—1字节) int bfSize = 54 + width * height * 3;// bmp文件的大小(2—5字节) int bfReserved1 = 0;// 位图文件保留字,必须为0(6-7字节) int bfReserved2 = 0;// 位图文件保留字,必须为0(8-9字节) int bfOffBits = 54;// 文件头开始到位图实际数据之间的字节的偏移量(10-13字节) // 输入数据的时候要注意输入的数据在内存中要占几个字节, // 然后再选择相应的写入方法,而不是它自己本身的数据类型 // 输入文件头数据 dos.writeShort(bfType); // 输入位图文件类型'BM' dos.write(changeByte(bfSize),0,4); // 输入位图文件大小 dos.write(changeByte(bfReserved1),0,2);// 输入位图文件保留字 dos.write(changeByte(bfReserved2),0,2);// 输入位图文件保留字 dos.write(changeByte(bfOffBits),0,4);// 输入位图文件偏移量 // 给信息头的变量赋值 int biSize = 40;// 信息头所需的字节数(14-17字节) int biWidth = width;// 位图的宽(18-21字节) int biHeight = height;// 位图的高(22-25字节) int biPlanes = 1; // 目标设备的级别,必须是1(26-27字节) int biBitcount = 24;// 每个像素所需的位数(28-29字节),必须是1位(双色)、4位(16色)、8位(256色)或者24位(真彩色)之一。 int biCompression = 0;// 位图压缩类型,必须是0(不压缩)(30-33字节)、1(BI_RLEB压缩类型)或2(BI_RLE4压缩类型)之一。 int biSizeImage = width * height;// 实际位图图像的大小,即整个实际绘制的图像大小(34-37字节) int biXPelsPerMeter = 0;// 位图水平分辨率,每米像素数(38-41字节)这个数是系统默认值 int biYPelsPerMeter = 0;// 位图垂直分辨率,每米像素数(42-45字节)这个数是系统默认值 int biClrUsed = 0;// 位图实际使用的颜色表中的颜色数(46-49字节),如果为0的话,说明全部使用了 int biClrImportant = 0;// 位图显示过程中重要的颜色数(50-53字节),如果为0的话,说明全部重要 // 因为java是大端存储,那么也就是说同样会大端输出。 // 但计算机是按小端读取,如果我们不改变多字节数据的顺序的话,那么机器就不能正常读取。 // 所以首先调用方法将int数据转变为多个byte数据,并且按小端存储的顺序。 // 输入信息头数据 dos.write(changeByte(biSize),0,4);// 输入信息头数据的总字节数 dos.write(changeByte(biWidth),0,4);// 输入位图的宽 dos.write(changeByte(biHeight),0,4);// 输入位图的高 dos.write(changeByte(biPlanes),0,2);// 输入位图的目标设备级别 dos.write(changeByte(biBitcount),0,2);// 输入每个像素占据的字节数 dos.write(changeByte(biCompression),0,4);// 输入位图的压缩类型 dos.write(changeByte(biSizeImage),0,4);// 输入位图的实际大小 dos.write(changeByte(biXPelsPerMeter),0,4);// 输入位图的水平分辨率 dos.write(changeByte(biYPelsPerMeter),0,4);// 输入位图的垂直分辨率 dos.write(changeByte(biClrUsed),0,4);// 输入位图使用的总颜色数 dos.write(changeByte(biClrImportant),0,4);// 输入位图使用过程中重要的颜色数 // 因为是24位图,所以没有颜色表 // 通过遍历输入位图数据 // 这里遍历的时候注意,在计算机内存中位图数据是从左到右,从下到上来保存的, // 也就是说实际图像的第一行的点在内存是最后一行 for (int i = height - 1; i >= 0; i--) { for (int j = 0; j < width; j++) { // 这里还需要注意的是,每个像素是有三个RGB颜色分量组成的, // 而数据在windows操作系统下是小端存储,对多字节数据有用。 int red = pointArray[i][j].getRed();// 得到位图点的红色分量 int green = pointArray[i][j].getGreen();// 得到位图点的绿色分量 int blue = pointArray[i][j].getBlue();// 得到位图点的蓝色分量 byte[] red1 = changeByte(red); byte[] green1 = changeByte(green); byte[] blue1 = changeByte(blue); dos.write(blue1,0,1); dos.write(green1,0,1); dos.write(red1,0,1); } } //关闭数据的传输 dos.flush(); dos.close(); fos.close(); System.out.println("success!!!"); } catch (Exception e) { e.printStackTrace(); } } /** * 将一个int数据转为按小端顺序排列的字节数组 * @param data int数据 * @return 按小端顺序排列的字节数组 */ public byte[] changeByte(int data){ byte b4 = (byte)((data)>>24); byte b3 = (byte)(((data)<<8)>>24); byte b2= (byte)(((data)<<16)>>24); byte b1 = (byte)(((data)<<24)>>24); byte[] bytes = {b1,b2,b3,b4}; return bytes; } }
部分参考:http://www.cnblogs.com/Jason_Yao/archive/2009/12/02/1615295.html