BMP格式解析与保存 画图板的重绘

实现画图板重绘的两种方法
1.抽象Shape类 每次画的时候 都保存在ArrayList中 当窗体改变 自动调用paint方法时 取出ArrayList中保存的Shape对象

public void paint(Graphics g) {
super.paint(g);// 将面板本身绘制在屏幕上
// 当面板发生改变的时候,
//将ArrayList中保存的形状对象取出来,重新绘制
for(int i=0;i<DrawListener.list.size();i++){
Shape shape = DrawListener.list.get(i);
shape.draw(g);
}
}


2.截屏 每次释放的时候 用二维数组保存屏幕上的点

// 释放一次,就重新保存一次
// 截屏
Point point= drawJPanel.getLocationOnScreen();
Dimension dim=drawJPanel.getPreferredSize();
Rectangle screenrect=new Rectangle(point,dim);
BufferedImage bufferedImg=robot.createScreenCapture(screenrect);
// 根据面板大小调整数组大小
array=new int[dim.height][dim.width];
for(int i=0;i<array.length;i++){
for(int j=0;j<array[i].length;j++){
array[i][j]=bufferedImg.getRGB(j, i);
}
}


先介绍一下有关BMP的基础知识:

BMP 是一种与硬件设备无关的图像文件格式,使用非常广。它采用位映射存储格式,除了图 像深度可选以外,不采用其他任何压缩,因此, BMP 文件所占用的空间很大。 BMP 文件的图 像深度可选 lbit 、 4bit 、 8bit 及 24bit 。 BMP 文件存储数据时,图像的扫描方式是按从左 到右、从下到上的顺序。由于 BMP 文件格式是 Windows 环境中交换与图有关的数据的一种标 准,因此在 Windows 环境中运行的图形图像软件都支持 BMP 图像格式。

要解析文件,就必须知道他的文件结构:

BMP 文件结构
典型的 BMP 图像文件由四部分组成:
1 . 位图文件 头数据结构 ,它包含 BMP 图像文件的类型、显示内容等信息;
2 .位图信息数据结构 ,它包含有 BMP 图像的宽、高、压缩方法,以及定义颜色等信息;
3. 调色板 ,这个部分是可选的,有些位图需要调色板,有些位图,比如真彩色图(24 位 的 BMP )就不需要调色板;
4. 位图数据 ,这部分的内容根据 BMP 位图使用的位数不同而不同,在 24 位图中直 接使用 RGB ,而其他的小于 24 位的使用调色板中颜色索引值。

对应的数据结构
① BMP 文件头 (14 字节 )
BMP 文件头数据结构含有 BMP 文件的类型、文件大小和位图起始位置等信息。其结构定 义如下:
int bfType; // 位图文件的类型,必须为 ' B '' M '两个字母 (0-1字节 )
int bfSize; // 位图文件的大小,以字节为单位 (2-5 字节 )
int usignedshort bfReserved1; // 位图文件保留字,必须为 0(6-7 字节 )
int usignedshort bfReserved2; // 位图文件保留字,必须为 0(8-9 字节 )
int bfOffBits; // 位图数据的起始位置,以相对于位图 (10-13 字节 )
int bfOffBits;// 文件头的偏移量表示,以字节为单位

② 位图信息头(40 字节 )
BMP 位图信息头数据用于说明位图的尺寸等信息。
int Size; // 本结构所占用字节数 (14-17 字节 )
int image_width; // 位图的宽度,以像素为单位 (18-21 字节 )
int image_heigh; // 位图的高度,以像素为单位 (22-25 字节 )

int Planes; // 目标设备的级别,必须为 1(26-27 字节 )
int biBitCount;// 每个像素所需的位数,必须是 1(双色),(28-29 字节) 4(16
色 ) , 8(256 色 ) 或 24(// 真彩色 ) 之一
int biCompression; // 位图压缩类型,必须是 0( 不压缩 ),(30-33 字节 ) 1(BI_RLE8 压缩类型 ) 或// 2(BI_RLE4 压缩类型 ) 之一
int SizeImage; // 位图的大小,以字节为单位 (34-37 字节 )
int biXPelsPerMeter; // 位图水平分辨率,每米像素数 (38-41 字节 ) int biYPelsPerMeter; // 位图垂直分辨率,每米像素数 (42-45 字节 ) int biClrUsed;// 位图实际使用的颜色表中的颜色数 (46-49 字节 )
int biClrImportant;// 位图显示过程中重要的颜色数 (50-53 字节 )


③ 颜色表
颜色表用于说明位图中的颜色,它有若干个表项,每一个表项是一个 RGBQUAD 类型的结 构,定义一种颜色。
class RGBQUAD {
byte rgbBlue;// 蓝色的亮度 ( 值范围为 0-255)
byte rgbGreen; // 绿色的亮度 ( 值范围为 0-255)
byte rgbRed; // 红色的亮度 ( 值范围为 0-255)
byte rgbReserved;// 保留,必须为 0
}
颜色表中 RGBQUAD 结构数据的个数有 biBitCount 来确定。当 biBitCount=1,4,8 时,分别有 2,16,256 个表项 ; 当 biBitCount=24 时,没有颜色表项。 位图信息头和颜色表组成位图信息,BITMAPINFO 结构定义如下 :
class BITMAPINFO {
BITMAPINFOHEADER bmiHeader; // 位图信息头
RGBQUAD bmiColors[1]; // 颜色表
}

④ 位图数据
位图数据记录了位图的每一个像素值,记录顺序是在扫描行内是从左到右,扫描行之间是从 下到上。位图的一个像素值所占的字节数:
当 biBitCount=1 时, 8 个像素占 1 个字节 ; 当 biBitCount=4 时, 2 个像素占 1 个字节 ; 当 biBitCount=8 时, 1 个像素占 1 个字节 ; 当 biBitCount=24 时 ,1 个像素占 3 个字节 ;
Windows 规定一个扫描行所占的字节数必须是 4 的倍数 ( 即以 long 为单位 ), 不足的以 0填充


简单来说 文件格式保存 就是 怎么写进去 就怎么读出来 但是其中转换格式还是很麻烦的
通过网上查资料 BMP解析可以有两种方式实现 其实方法差不多 原理都一样

方法一:


package BMP_Color;

import java.awt.Color;
import java.io.DataOutputStream;
import java.io.FileOutputStream;

public class BMPWrite24 {


Color [][] color;
int width,height;
String path;
public BMPWrite24(Color [][] color,String path){
this.color=color;
this.height=color.length;
this.width=color[0].length;
this.path=path;
this.write();
}
public void write(){
try {
FileOutputStream fos=new FileOutputStream(path);
DataOutputStream dos=new DataOutputStream(fos);


int bfSize=54+width*height*3+(4 - width * 3 % 4) * height;
int bfReserved1=0;
int bfReserved2=0;
int bfOffBits=54;

dos.write('B');
dos.write('M');
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 size=40;
int image_width=width;
int image_height=height;
int Planes=1;
int biBitCount=24;
int biCompression=0;
int SizeImage=width*height;
int biXPelsPerMeter=0;
int biYPelsPerMeter=0;
int biClrUsed=0;
int biClrImportant=0;

// 因为java是大端存储,那么也就是说同样会大端输出。
// 但计算机是按小端读取,如果我们不改变多字节数据的顺序的话,那么机器就不能正常读取。
// 所以首先调用方法将int数据转变为多个byte数据,并且按小端存储的顺序。

dos.write(ChangeByte(size),0,4);
dos.write(ChangeByte(image_width),0,4);
dos.write(ChangeByte(image_height),0,4);
dos.write(ChangeByte(Planes),0,2);
dos.write(ChangeByte(biBitCount),0,2);
dos.write(ChangeByte(biCompression),0,4);
dos.write(ChangeByte(SizeImage),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);

int skip=0;
if(!(width*3%4==0)){
skip=4-width*3%4;
}

// 因为是24位图,所以没有颜色表
// 通过遍历输入位图数据
// 这里遍历的时候注意,在计算机内存中位图数据是从左到右,从下到上来保存的,
// 也就是说实际图像的第一行的点在内存是最后一行
for(int i=height-1;i>=0;i--){
for(int j=0;j<width;j++){

int red=color[i][j].getRed();
int green=color[i][j].getGreen();
int blue=color[i][j].getBlue();

byte[] r=ChangeByte(red);
byte[] g=ChangeByte(green);
byte[] b=ChangeByte(blue);
dos.write(b,0,1);
dos.write(g,0,1);
dos.write(r,0,1);

for (int k = 0; k < skip; k++) {
dos.write(0);
}
}

}
dos.flush();
dos.close();
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
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 [] b={b1,b2,b3,b4};
return b;
}

}



package BMP_Color;

import java.awt.Color;
import java.io.DataInputStream;
import java.io.FileInputStream;

import javax.swing.JOptionPane;

public class BMPReader24{
MyPanel panel;
String path;
int [][] red,green,blue;
public BMPReader24(MyPanel panel,String path){
this.panel=panel;
this.path=path;
this.reader();
}
public void reader(){
try {
FileInputStream fis=new FileInputStream(path);
DataInputStream dis=new DataInputStream(fis);

int biLen=14;
byte[] bi=new byte[biLen];
dis.read(bi, 0, biLen);

int bfLen=40;
byte[] bf=new byte[bfLen];
dis.read(bf, 0, bfLen);

int width=ChangeInt(bf, 7);
int height=ChangeInt(bf, 11);

red=new int [height][width];
green=new int[height][width];
blue=new int[height][width];


// 通过计算得到每行计算机需要填充的字符数。
// 为什么要填充?这是因为windows系统在扫描数据的时候,每行都是按照4个字节的倍数来读取的。
// 因为图片是由每个像素点组成。而每个像素点都是由3个颜色分量来构成的,而每个分量占据1个字节。
// 因此在内存存储中实际图片数据每行的长度是width*3。
int skip=0;
if(!(width*3%4==0)){
skip=4-width*3%4;
}
for(int i=height-1;i>=0;i--){
for(int j=0;j<width;j++){

blue[i][j]=dis.read();
green[i][j]=dis.read();
red[i][j]=dis.read();

DrawListener.color[i][j]=new Color(red[i][j],green[i][j],blue[i][j]);
if(j==0){
dis.skipBytes(skip);
}
}
}
panel.repaint();
} catch (Exception e) {
JOptionPane.showMessageDialog(null, "文件打开失败!!");
e.printStackTrace();
}
}
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;
}

}


方法二:



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;
int size = 14 + 40 + height * width * 3 + (4 - width * 3 % 4) * height;
// 位图文件的大小
size = 14 + 40 + height * width * 3 + (4 - width * 3 % 4) * 255;
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;
}
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;
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);
//System.out.println(d+" <>"+c+"<>"+b+"<>"+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;
}

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);
int width = changeInt(dis);// 跳过不需要的,读取宽度和高度
int height = changeInt(dis);
dis.skip(28);
// 跳过,直接读取位图数据。
DrawListener.array = new int[height][width];
int w = 0;
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();
}
}
}
}
}



比较两个方法 :
方法一比较麻烦 再定义一个Color[][]数组 保存屏幕上的点的颜色 BMP格式保存时
color[i][j].getRed()getGreen()getBlue()方法获取屏幕点的颜色分量
方法二 直接利用重绘时定义int[][]数组 32位 后24位是红绿蓝颜色分量

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);
}


1.byte转成int为何要&0xff?
第一,oxff默认为整形,二进制位最低8位是1111 1111,前面24位都是0;

第二,&运算: 如果2个bit都是1,则得1,否则得0;

第三,byte的8位和0xff进行&运算后,最低8位中,原来为1的还是1,原来为0的还是0,而0xff其他位都是0,所以&后仍然得0

2.writeInt 方法中传入int值 写出4个int值 依旧是4个字节
因为 write(int a) 是把a转换为byte 而不是byte[] a的高24位将被忽略 只有低8位转成byte写入流
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值