本人参考了一些人的关于BMP的代码,最后终于搞懂了,针对网上的很多代码对于新手来说有的看其实还是比较费力的,我就给我的代码尽量做了详细的解释和注解和提出两个问题,希望这能帮助你理解以下代码能。
新手为什么对那些代码看起来比较费力,我想应该是一下几个知识点不清楚。这是我作为一个新手的体会,如果你把下面这两个知识点搞懂了,我想BMP的解析对你来说应该是可以轻轻松松的搞下来。
1.BMP在Windows里面的储存格式是什么样子的?
BMP是Windows操作系统中的标准图像文件格式,Windows规定一个扫描行所占的字节数必须是4的倍数,不足的以0补充。关键就是要搞清楚Windows里面数据是怎么存的?Windows储存数据是从地位到高位来储存的,我们一般都是从高位到地位进行保存。所以就需要我们进行位运算将数据来转化成Windows储存数据的方式。
2.对于位运算你懂了多少?
我们上面也说了要将数据转化之后来储存,所以要搞清楚位运算。我就拿代码中的位运算来说一下位运算。
int a = (t >> 24) & 0xff;
int b = (t >> 16) & 0xff;
int c = (t >> 8) & 0xff;
int d = t & 0xff;
ops.write(d);
ops.write(c);
ops.write(b);
ops.write(a);
要理解我下面的说的,那你要懂二进制的位运算,在这里至少你要知道&(与)的位运算(关于这个知识我就不说,这需要你自己去百度,不是特别难,如果你不知道,也不去百度继续看下去,那懂不懂就看你的造化了)
上面的t是一个int类型的,0xff是16进制它的二进制是
1111 1111 t>>24表示t的二进制左移24位与1111 1111做位运算,得到t的二进制的前8位,同理t>>16 左移16得到t的次8位,这里右移16,还剩下16位怎么会是得到t的次8位呢,注意啊0xff的二进制是1111 1111 只有8位,在计算的b的时候它会把0xff前面用0补成16位就是0000 0000 1111 1111,前面8个0在进行与位运算的时候不管怎么样都是0,所以是得到t的次8位的数据,就这样将一个int值的t按照8位8位的一次取出数据。
那么肯定会有人这样想,你一个int你拆成4个int值,那不就将数据扩大了吗?当然是不会了,再看我们下面是每次写一个字节,a,b,c,d虽然都是int值,但是我们write()是写一个字节,一个字节是8位,那么只会将这个int值的后面8位数据写进去。
如果你把我上面说的都理解了,好了关于BMP你已经掌握了一半了。因为下面代码里面方法里面涉及的位运算,我想你肯定也可以自己看懂了。
写到这里突然感觉BMP的解析和哈夫曼的压缩和解压道理差不多,知道原理了,再一一写进去就可以了,读取的时候就是你读什么就写什么就可以了
好我们继续说BMP,BMP的解析,其实它的原理很简单:按照格式去写进去,按照格式来读就可以了。其实大多数人不懂都是那个关于二进制的位运算。下面我给出BMP前面54个字节的格式。
(一下格式参考湖南大学周圣韬的,自己只是做了一点点的修改)
① BMP文件头(14)字节
byte bfType1;//位图文件的类型,必须位’B’’1个字母(’B ‘ 1个字节)
byte bfType2;//位图文件的类型,必须位’B’1个个字母(’M ‘ 1个字节)
int bfSize;//位图文件的大小,以字节位单位(4个字节)
short usignedshort bfReserved1;//位图文件保留字,必须为0(2个字节)
short usignedshort bfReserved2;//位图文件保留字,必须为0(2个字节)
int bfOffBits;//位图数据的起始位置,以相对于位图(文件头的偏移量表示,以字节为单位)(4个字节)
① BMP位图信息头(40)字节
BMP位图信息头数据用于说明位图的尺寸等信息
int Size;//本结构所占用字节数(4个字节)
int image_width;//位图的宽度,以像素为单位(4个字节)
int image_heigh;//位图的高度,以像素位单位(4个字节)
short Planes;//目标设备的级别,必须位1(2个字节)
short biBitCount;//每个像素所需的位数,必须是1(双色),4(16色),
8(256色)或24(//真彩色)之一(2个字节)
int biCompression;//位图压缩类型,必须是0(不压缩),1(BI_RLE8 压缩类型)或2(BI_RLE4)之一(4个字节)
int SizeImage;//位图的大小,以字节位单位(4个字节)
int biXPelsPerMeter;//位图水平分辨率,每米像素数(4个字节)
int biYPelsPerMeter;//位图垂直分辨率,每米像素数(4个字节)
int biClrUsed;//位图实际使用的颜色表中的颜色数(4个字节)
int biClrImportant;//位图显示过程中重要的颜色数(4个字节)
我们就先从保存说起。按照上面的格式写完之后,然后就开始写文件的数据了。
下面我给出代码,这里我需呀说明一下,我的这个BMP的代码是由何旭同学提供的,我只是拿出来给大家讲解。这个代码是属于画图板的一部分的,画图板的所有代码我以附件给出。
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
/**
* 菜单处理监听器
*
* @author kowloon
*
*/
public class MenuListener implements ActionListener {
private MyPanel panel;
public MenuListener(MyPanel panel) {
this.panel = panel;
}
// bmp文件头
public void savebmpTop(OutputStream ops) throws Exception {
ops.write('B');
ops.write('M');
int height = DrawListener.array.length;
int width = DrawListener.array[0].length;
//注意这里的宽和高是像素的宽和高,24位图的话是一个像素3个//字节,所以一行的字节数应该是width*3
int size = 54+ height * width * 3 + (4 - width * 3 % 4) * height;
writeInt(ops, size);
// 保留字节,必须为零
writeShort(ops, (short) 0);
writeShort(ops, (short) 0);
// 位图偏移量
writeInt(ops, 54);
}
// 位图信息头
public void savebmpInfo(OutputStream ops) throws Exception {
int height = DrawListener.array.length;
int width = DrawListener.array[0].length;
// 位图信息头长度,本结构所占的字节
writeInt(ops, 40);
// 位图宽
writeInt(ops, width);
// 位图高
writeInt(ops, height);
// 位图位面数总是为1
writeShort(ops, (short) 1);
// 位图24位像素
writeShort(ops, (short) 24);
// 位图是否被压缩,0为不压缩
writeInt(ops, 0);
// 字节数代表位图大小
writeInt(ops, height * width * 3 + (4 - width * 3 % 4) * height);
// 水平分辨率
writeInt(ops, 0);
// 垂直分辨率
writeInt(ops, 0);
// 颜色索引,0为所有调色板
writeInt(ops, 0);
// 对图象显示有重要影响的颜色索引的数目。如果是0表示都重要
writeInt(ops, 0);
}
// 图像数据阵列
public void savebmpDate(OutputStream ops) throws Exception {
int height = DrawListener.array.length;
int width = DrawListener.array[0].length;
int m = 0;
// 进行补0
if (width * 3 % 4 > 0) {
m = 4 - width * 3 % 4;
}
//其实每一个i代表一行数据,j=0说明是从左往右,
//正好符合window的储存数据的格式
for (int i = height - 1; i >= 0; i--) {
for (int j = 0; j < width; j++) {
int t = DrawListener.array[i][j];
writeColor(ops, t);
}
for (int k = 0; k < m; k++) {
ops.write(0);
}
}
}
public void writeInt(OutputStream ops, int t) throws Exception {
int a = (t >> 24) & 0xff;
//window里面储存数据是从高位到地位进行储存的,因为每次写一//个字节,所以是8位
//这里的0xff是可以变化的如果是一个字节那么它就是1111 1111 //如果是一个int那么它就是一个00000000 00000000 00000000 //11111111
int b = (t >> 16) & 0xff;
int c = (t >> 8) & 0xff;
int d = t & 0xff;
//a是取前8位,一次每次取得t的8位,d是最后8位,然后反着//储存数据。
ops.write(d);
ops.write(c);
ops.write(b);
ops.write(a);
}
public void writeColor(OutputStream ops, int t) throws Exception {
int b = (t >> 16) & 0xff;
int c = (t >> 8) & 0xff;
int d = t & 0xff;
ops.write(d);
ops.write(c);
ops.write(b);
}
public void writeShort(OutputStream ops, short t) throws Exception {
int c = (t >> 8) & 0xff;
int d = t & 0xff;
ops.write(d);
ops.write(c);
}
// 由于读取的是字节,把读取到的4个byte转化成1个int
public int changeInt(InputStream ips) throws IOException {
int t1 = ips.read() & 0xff;
int t2 = ips.read() & 0xff;
int t3 = ips.read() & 0xff;
int t4 = ips.read() & 0xff;
int num = (t4 << 24) + (t3 << 16) + (t2 << 8) + t1;
System.out.println(num);
return num;
}
// 24位的图片是1个像素3个字节。
public int readColor(InputStream ips) throws IOException {
int b = ips.read() & 0xff;
int g = ips.read() & 0xff;
int r = ips.read() & 0xff;
int c = (r << 16) + (g << 8) + b;
return c;
//这里是读取,第一读取做位运算得到的这个数据的最低8位,
//在读取得到原来数据的次8位,最后一次
//读取得到原来数据的最高8位,通过位运算把原来数据还原。
}
public void actionPerformed(ActionEvent e) {
// 获得被点击的组件的动作命令
String command = e.getActionCommand();
JFileChooser chooser = new JFileChooser();
if (command.equals("保存")) {
int t = chooser.showSaveDialog(null);
if (t == JFileChooser.APPROVE_OPTION) {
String path = chooser.getSelectedFile().getAbsolutePath();
try {
FileOutputStream fos = new FileOutputStream(path);
DataOutputStream dos = new DataOutputStream(fos);
savebmpTop(dos);
savebmpInfo(dos);
savebmpDate(dos);
fos.flush();
fos.close();
} catch (Exception ef) {
JOptionPane.showMessageDialog(null, "文件保存失败!!");
ef.printStackTrace();
}
}
} else if (command.equals("打开")) {
int t = chooser.showOpenDialog(null);
if (t == JFileChooser.APPROVE_OPTION) {
String path = chooser.getSelectedFile().getAbsolutePath();
try {
FileInputStream fis = new FileInputStream(path);
DataInputStream dis = new DataInputStream(fis);
dis.skip(18);
//skip(n)从输入流中跳过并丢弃 n个字节的数据
int width = changeInt(dis);
// 跳过不需要的,读取宽度和高度
int height = changeInt(dis);
dis.skip(28);
// 跳过,直接读取位图数据。
DrawListener.array = new int[height][width];
int w = 0;
//注意上面的width是源图的宽,是补零之前的宽度
if (width * 3 % 4 > 0) {
t = 4 - width * 3 % 4;
}
for (int i = height - 1; i >= 0; i--) {
for (int j = 0; j < width; j++) {
// 调用自定义方法,得到图片的像素点并保存到int数组中
int c = readColor(dis);
DrawListener.array[i][j] = c;
}
dis.skip(w);
}
fis.close();
// 刷新界面
panel.repaint();
} catch (Exception ef) {
JOptionPane.showMessageDialog(null, "文件打开失败!!");
ef.printStackTrace();
}
}
}
}
}
看完上面的代码可能你还有一些不懂的,我说一下我之前的问题。
DrawListener.array中的array是一个二维数组,array数组的长度表示源图数据的高和宽,array中的每一个值表示的是一个表示颜色的RGB值,array是其他代码中得到的,这里直接用了,所以感觉还是不太懂的,可以下载全部代码,看一看array是怎么来的。
1.
说一下文件大小的计算,上面的采用24位图的,也就是一个像素占3个字节,54+height*width*3,54是前面BMP格式所占用的54个字节,height*width*3总共有height*width个像素,一个像素占用3个字节所以源文件的总共有height*width*3个字节,注意这是源文件的字节数,并不是我们得到BMP图片的大小,因为Windows规定一个扫描行所占的字节数必须是4的倍数,不足的以0补充,所以4 - width * 3 % 4这是每一行需要补0的字节,(4 - width * 3 % 4) * height这是总共需要补零的字节个数。这里应该理解很多地方是*3%4了吧,这样子就得出写出后BMP文件的大小是int size = 54+ height * width * 3 + (4 - width * 3 % 4) * height;
2.写完BMP头文件和位图信息头就开始写位图数据,再写的过程中进行了补零操作。
3.同理在读取数据的时候就按照你写什么就读什么就可以了,要注意的是我们存的时候是把int拆开写进去的,但是我们每次读取只是读取一个字节,并且顺序和源数据顺序相反,这就需要我们把把顺序调好再拼接成一个int类型,得出源数据。
这里你需要搞清楚的是之前54字节int值是被拆成4个byte写进去的,但是位图数据虽然是一个int值,但是它只有三个字节的大小,所以写进的时候是把它拆成三个byte写进去的。这样字我们在读取的时候前面54个字节里面的int需要去读4个字节来拼成源数据,后面读取位图数据的时候只需要三个字节就可以拼成源数据。