通过分析bmp图片的格式,可以完成BMP图片的打开和保存
一、bmp格式:
典型的BMP 图像文件由四部分组成:
1 .位图文件头数据结构,它包含BMP 图像文件的类型、显示内容等信息;
2 .位图信息数据结构,它包含有BMP 图像的宽、高、压缩方法,以及定义颜色等信息;
3.调色板,这个部分是可选的,有些位图需要调色板,有些位图,比如真彩色图(24 位的 BMP )就不需要调色板;
4.位图数据,这部分的内容根据BMP 位图使用的位数不同而不同,在 24 位图中直接使用 RGB ,而其他的小于 24 位的使用调色板中颜色索引值。
① BMP文件头 (14 字节 )
BMP文件头数据结构含有 BMP 文件的类型、文件大小和位图起始位置等信息。其结构定义如下:
intbfType; // 位图文件的类型,必须为 ' B '' M '两个字母 (0-1字节 ) intbfSize; // 位图文件的大小,以字节为单位 (2-5 字节 ) intbfReserved1; // 位图文件保留字,必须为 0(6-7 字节 ) intbfReserved2; // 位图文件保留字,必须为 0(8-9 字节 ) intbfOffBits; // 位图数据的起始位置,以相对于位图 (10-13 字节 ) |
② 位图信息头(40 字节 )
BMP 位图信息头数据用于说明位图的尺寸等信息。
intSize; // 本结构所占用字节数 (14-17 字节 ) intimage_width; // 位图的宽度,以像素为单位 (18-21 字节 ) intimage_heigh; // 位图的高度,以像素为单位 (22-25 字节 ) intPlanes; // 目标设备的级别,必须为 1(26-27 字节 ) intbiBitCount;// 每个像素所需的位数,必须是或1,4,8 24(// 真彩色 ) 之一 (28-29 字节) intbiCompression; // 位图压缩类型,必须是 0( 不压缩 ), 1(BI_RLE8 压缩类型 ) 或// 2(BI_RLE4 压缩类型 ) 之一 (30-33 字节 ) intSizeImage; // 位图数据的大小,以字节为单位 (34-37 字节 ) intbiXPelsPerMeter; // 位图水平分辨率,每米像素数 (38-41 字节 ) intbiYPelsPerMeter; // 位图垂直分辨率,每米像素数 (42-45 字节 ) intbiClrUsed;// 位图实际使用的颜色表中的颜色数 (46-49 字节 ) intbiClrImportant;// 位图显示过程中重要的颜色数 (50-53 字节 ) |
③ 颜色表
颜色表中的个数有 biBitCount 来确定。当biBitCount=1,4,8 时,分别有 2,16,256 个颜色 ; 当biBitCount=24 时,没有颜色表项。
在windows中每个颜色是 b g r a 四个字节保存,a代表透明度,如果是1位位图,那么颜色表一共站八个字节,如果是4位位图,颜色表站84个字节,如果是8位位图,需要表示256中颜色,每种颜色站四个字节,所以颜色表一共站1024个字节。
256色的时候,windows中,位图数据的大小就是0-255,代表着调色板中的数据,每次读八个为就是一个像素信息,其他位图类似。
④ 位图数据
位图数据记录了位图的每一个像素值,记录顺序是在扫描行内是从左到右, 扫描行之间是从下到上。位图的一个像素值所占的字节数:
当biBitCount=1 时, 8 个像素占 1 个字节 ;
当biBitCount=4 时, 2 个像素占 1 个字节 ;
当biBitCount=8 时, 1 个像素占 1 个字节 ;
当biBitCount=24 时, 1 个像素占 3 个字节 ;
Windows规定一个扫描行所占的字节数必须是4 的倍数 ( 即以 long 为单位 ), 不足的以 0 填充,具体数据举例:
也就是说,写入图片一行的像素信息的时候,每一行的字节数都必须是4的倍数,不足的在后面补0,然后才又开始写入下一行的像素信息。
位图数据,其实就是在描述图片上每一个点的颜色,在windos中,是先写入该图片的最有一排像素点,从下往上,从左往右,没写完一排,如果写入的字节数不是4的倍数,就会补0,比如写入了3个字节,就不要再补一个字节的0,然后开始写下一排的数据。
对于单色位图,只需要一个位就可以表示其所有的颜色可能,黑白,对于16色位图,也就是有16中颜色的可能,那么需要四个位就可以表示16中可能,对于256色位图,那么需要8个位才能表示256中情况,对于真彩色,也就是表示所有的颜色,那么需要三个字节才可以表示所有情况(透明度不算),对于单色,16色,256色位图来说,位图数据读到的是调色板上的索引,通过这个索引在调试版中找到对应的颜色,然后显示在屏幕上。所以对于单色位图,他的调色板只有黑白两种颜色,16色位图,他的调色板有16种颜色,256色的调色板有256中颜色,如果是24位位图,他的位图信息,就是保存的真实颜色,每读三个字节就表示了一个像素点的颜色,不需要再进行调色板查找对应的颜色。
前言:windows中存入基本数据,是先存入了低位,再存入高位。
******************************************************************************************
首先完成打开一张bmp24位位图图片:
1、利用系统画图板画一张图片,保存的时候选择24位位图,由于24为位图没有调试板,位图数据区保存的是每个像素点的颜色,不需要我们进行调试板的转换,所有有利于初学者对于bmp格式的理解,完成了24位的再来看其他位图的。
2、利用swing写一个图形界面,中间留一个panel区域用于显示图片。将图片的每一个颜色保存到二维数组中,然后画在面板上,就打开了一张图片。
3、读取到图片的主要信息:
- File file = new File("d:\\8bit.bmp");
- FileInputStream fis = new FileInputStream(file);
- // 首先得到宽度和高度
- fis.skip(18);
- int width = myReadInt(fis);
- int height = myReadInt(fis);
- //由于24位位图没有调试板数据,所以现在已经已经读取了26个字节,再跳过28个字节就到了位图
- //数据区
- fis.skip(28);
- //再开始读取就是位图数据了,现在是24位位图,每读取三个字节就代表一个像素点的颜色,注意
- //windows在保存的时候是先保存的最后一排的数据,从下往上从左往右保存的,所以我们取出数
- //据只有需要保存的数组正确的位置。每次读取三个字节我们就转成颜色保存起来
- // 现在读取位图数据
- // 判断是否补零
- Color[][] colors = new Color[height][width];
- int sk = 0;
- if (width % 4 > 0) {
- sk = 4 - width % 4;
- }
- for (int h = image_heigh - 1; h >= 0; h--) {
- for (int w = 0; w < image_width; w++) {
- // dis.skipBytes(bfOffBits-60);
- // 读入三原色
- int blue = dis.read();
- int green = dis.read();
- int red = dis.read();
- Color color = new Color(red,green,blue);
- colors[h][w]=color;
- }
- // 跳过补0项,每次读取第一行就判断需要跳过的字节
- dis.skipBytes(sk);
- }
File file = new File("d:\\8bit.bmp"); FileInputStream fis = new FileInputStream(file); // 首先得到宽度和高度 fis.skip(18); int width = myReadInt(fis); int height = myReadInt(fis); //由于24位位图没有调试板数据,所以现在已经已经读取了26个字节,再跳过28个字节就到了位图 //数据区 fis.skip(28); //再开始读取就是位图数据了,现在是24位位图,每读取三个字节就代表一个像素点的颜色,注意 //windows在保存的时候是先保存的最后一排的数据,从下往上从左往右保存的,所以我们取出数 //据只有需要保存的数组正确的位置。每次读取三个字节我们就转成颜色保存起来 // 现在读取位图数据 // 判断是否补零 Color[][] colors = new Color[height][width]; int sk = 0; if (width % 4 > 0) { sk = 4 - width % 4; } for (int h = image_heigh - 1; h >= 0; h--) { for (int w = 0; w < image_width; w++) { // dis.skipBytes(bfOffBits-60); // 读入三原色 int blue = dis.read(); int green = dis.read(); int red = dis.read(); Color color = new Color(red,green,blue); colors[h][w]=color; } // 跳过补0项,每次读取第一行就判断需要跳过的字节 dis.skipBytes(sk); }
保存下来所有像素的颜色,就可以画在面板上了,面板的大小应该是图片的大小。
如果理解了文件格式含义,那么就可以尝试完成256色位图的打开和保存。
保存的时候,我们需要严格按照格式规定,写入正确的字节,如果是256色,需要进行颜色的转换:
下面是256色位图的打开:
说明:我先根据系统画图板的图片,保存一张256色图片,读取出了调调色板的数据,放在一个文件中
便于我在进行打开256色图片的时候,把位图数据读到的信息转成调色板对应的颜色,所以要完成下面的操作,请先自己
读取256色的调色板数据。把由于系统画图板,位图信息是0,代表的是第一个颜色,如果是255代表最后一个颜色,所以
我们可以把调色板数据放在一维数组中,利用下标来进行查找。
- // 打开256色,也就是8位表示一个像素点的颜色
- File file = new File("d:\\8bit.bmp");
- FileInputStream fis = new FileInputStream(file);
- // 首先得到宽度和高度
- fis.skip(18);
- int width = myReadInt(fis);
- int height = myReadInt(fis);
- // 现在流的位置,26
- // 再跳过12个字节,到达需要测试的数据区
- fis.skip(12);
- int biXPelsPerMeter = myReadInt(fis); // 位图水平分辨率,每米像素数 (38-41 字节
- // )
- int biYPelsPerMeter = myReadInt(fis); // 位图垂直分辨率,每米像素数 (42-45 字节
- // )
- int biClrUsed = myReadInt(fis);// 位图实际使用的颜色表中的颜色数 (46-49 字节 )
- int biClrImportant = myReadInt(fis);// 位图显示过程中重要的颜色数 (50-53 字节 )
- System.out.println("" + biXPelsPerMeter + " " + biYPelsPerMeter
- + " " + biClrUsed + " " + biClrImportant);
- // 由于是256色,所以颜色表是[256][4]大小的数组,每个分量占1个字节
- // 定义一个256的数组,保存下颜色表的数据
- Color[] colors256 = new Color[256];
- for (int i = 0; i < 256; i++) {
- colors256[i] = myReadColor(fis);
- System.out.println("颜色表" + colors256[i]);
- }
- // 现在读取位图数据
- // 判断是否补零
- int sk = 0;
- if (width % 4 > 0) {
- sk = 4 - width % 4;
- }
- DrawUI.array = newint[height][width];
- System.out.println("宽度" + width + "高度" + height);
- for (int i = height - 1; i >= 0; i--) {
- for (int j = 0; j < width; j++) {
- int index = fis.read();
- DrawUI.array[i][j] = colors256[index].getRGB();
- System.out.println("保存的值"+DrawUI.array[i][j]);
- }
- fis.skip(sk);
- }
- // 设置画布大小
- panel.setPreferredSize(new Dimension(width, height));
- // 重新绘制panel,如果是repaint,只会重新绘制原来panel中的内容,不会将panel更新大小
- SwingUtilities.updateComponentTreeUI(panel);
// 打开256色,也就是8位表示一个像素点的颜色 File file = new File("d:\\8bit.bmp"); FileInputStream fis = new FileInputStream(file); // 首先得到宽度和高度 fis.skip(18); int width = myReadInt(fis); int height = myReadInt(fis); // 现在流的位置,26 // 再跳过12个字节,到达需要测试的数据区 fis.skip(12); int biXPelsPerMeter = myReadInt(fis); // 位图水平分辨率,每米像素数 (38-41 字节 // ) int biYPelsPerMeter = myReadInt(fis); // 位图垂直分辨率,每米像素数 (42-45 字节 // ) int biClrUsed = myReadInt(fis);// 位图实际使用的颜色表中的颜色数 (46-49 字节 ) int biClrImportant = myReadInt(fis);// 位图显示过程中重要的颜色数 (50-53 字节 ) System.out.println("" + biXPelsPerMeter + " " + biYPelsPerMeter + " " + biClrUsed + " " + biClrImportant); // 由于是256色,所以颜色表是[256][4]大小的数组,每个分量占1个字节 // 定义一个256的数组,保存下颜色表的数据 Color[] colors256 = new Color[256]; for (int i = 0; i < 256; i++) { colors256[i] = myReadColor(fis); System.out.println("颜色表" + colors256[i]); } // 现在读取位图数据 // 判断是否补零 int sk = 0; if (width % 4 > 0) { sk = 4 - width % 4; } DrawUI.array = new int[height][width]; System.out.println("宽度" + width + "高度" + height); for (int i = height - 1; i >= 0; i--) { for (int j = 0; j < width; j++) { int index = fis.read(); DrawUI.array[i][j] = colors256[index].getRGB(); System.out.println("保存的值"+DrawUI.array[i][j]); } fis.skip(sk); } // 设置画布大小 panel.setPreferredSize(new Dimension(width, height)); // 重新绘制panel,如果是repaint,只会重新绘制原来panel中的内容,不会将panel更新大小 SwingUtilities.updateComponentTreeUI(panel);
下面的代码,保存256色位图的bmp图片,注意需要自己写入调色板,调色板的信息是按照b g r a的顺序写入,调色板字节
先要准备好。
- // 将图片保存为bmp格式
- File mysave256 = new File("d:\\mysave256bmp.bmp");
- FileOutputStream fos = new FileOutputStream(mysave256);
- // 先写bm
- fos.write('B');
- fos.write('M');
- int width = DrawUI.array[0].length;
- int height = DrawUI.array.length;
- int bu0 = 0;
- if (width * bit % 4 > 0) {
- bu0 = 4 - width * bit % 4;
- }
- int size = 14 + 40 + width * height * bit + bu0 * height;
- // 写入文图文件大小
- myWriteInt(fos, size);
- // 写入文件保留字
- myWriteShort(fos, (short) 0);
- myWriteShort(fos, (short) 0);
- // 位图数据起始位置,54 + 颜色表
- int beginweitu = 54 + 256 * 4;
- myWriteInt(fos, beginweitu);
- // 位图信息头
- // 写入写入信息大小
- myWriteInt(fos, 40);
- // 写入宽度和高度
- myWriteInt(fos, width);
- myWriteInt(fos, height);
- // 写入级别
- myWriteShort(fos, (short) 1);
- // 每个像素所需的位数
- myWriteShort(fos, (short) 8);
- // 位图压缩类型
- myWriteInt(fos, 0);
- // 位图数据的大小
- int wsize = width * height + bu0 * height;
- myWriteInt(fos, wsize);
- // 保留的字节
- myWriteInt(fos, 0);
- myWriteInt(fos, 0);
- myWriteInt(fos, 0);
- myWriteInt(fos, 0);
- // 开始写入调色板
- FileInputStream fiscolor = new FileInputStream(fcolor256);
- // 得到调试板数组,用于转换
- HashMap hm = new HashMap();
- for (int i = 0; i < 256; i++) {
- int b = fiscolor.read() & 0xff;
- int g = fiscolor.read() & 0xff;
- int r = fiscolor.read() & 0xff;
- int a = fiscolor.read() & 0xff;
- Color colora =new Color(r,g,b) ;
- hm.put(colora.getRGB(),i );
- fos.write(b);
- fos.write(g);
- fos.write(r);
- fos.write(a);
- }
- fiscolor.close();
- // 开始写入位图数据,从最后一行从下往上输入
- int b0 = 0;
- if (width % 4 > 0) {
- b0 = 4 - width % 4;
- }
- int yuan = 0;
- int zhuan = 0;
- for (int i = height - 1; i >= 0; i--) {
- for (int j = 0; j < width; j++) {
- if(DrawUI.array[i][j]!=-1){
- System.out.print(DrawUI.array[i][j]+" ");
- }
- yuan = DrawUI.array[i][j];
- try{
- zhuan =(Integer) hm.get(yuan);
- }catch (Exception e2){
- zhuan=0;
- }
- fos.write(zhuan);
- }
- System.out.println();
- for (int k = 0; k < b0; k++) {
- fos.write(0);
- }
- }
- fos.flush();
- fos.close();
- publicvoid myWriteInt(OutputStream os, int num) throws IOException {
- int a = num & 0xff;
- int b = (num >> 8) & 0xff;
- int c = (num >> 16) & 0xff;
- int d = (num >> 24) & 0xff;
- os.write(a);
- os.write(b);
- os.write(c);
- os.write(d);
- os.flush();
- }
- publicvoid myWriteShort(OutputStream os, short num) throws IOException {
- int a = num & 0xff;
- int b = (num >> 8) & 0xff;
- os.write(a);
- os.write(b);
- os.flush();
- }