BMP解析的详细讲解,是新手就来看看吧!

  本人参考了一些人的关于BMP的代码,最后终于搞懂了,针对网上的很多代码对于新手来说有的看其实还是比较费力的,我就给我的代码尽量做了详细的解释和注解和提出两个问题,希望这能帮助你理解以下代码能。

 

新手为什么对那些代码看起来比较费力,我想应该是一下几个知识点不清楚。这是我作为一个新手的体会,如果你把下面这两个知识点搞懂了,我想BMP的解析对你来说应该是可以轻轻松松的搞下来。

 

1.BMPWindows里面的储存格式是什么样子的?

 

BMPWindows操作系统中的标准图像文件格式,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类型的,0xff16进制它的二进制是

 

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,前面80在进行与位运算的时候不管怎么样都是0,所以是得到t的次8位的数据,就这样将一个int值的t按照88位的一次取出数据。

 

那么肯定会有人这样想,你一个int你拆成4int值,那不就将数据扩大了吗?当然是不会了,再看我们下面是每次写一个字节,abcd虽然都是int值,但是我们write()是写一个字节,一个字节是8位,那么只会将这个int值的后面8位数据写进去。

 

如果你把我上面说的都理解了,好了关于BMP你已经掌握了一半了。因为下面代码里面方法里面涉及的位运算,我想你肯定也可以自己看懂了。

 

写到这里突然感觉BMP的解析和哈夫曼的压缩和解压道理差不多,知道原理了,再一一写进去就可以了,读取的时候就是你读什么就写什么就可以了

好我们继续说BMPBMP的解析,其实它的原理很简单:按照格式去写进去,按照格式来读就可以了。其实大多数人不懂都是那个关于二进制的位运算。下面我给出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值是被拆成4byte写进去的,但是位图数据虽然是一个int值,但是它只有三个字节的大小,所以写进的时候是把它拆成三个byte写进去的。这样字我们在读取的时候前面54个字节里面的int需要去读4个字节来拼成源数据,后面读取位图数据的时候只需要三个字节就可以拼成源数据。

你可能感兴趣的:(Java语言基础)