前几天突发奇想,不知道如果把我们整天写的程序代码变成图片显示的话,看起来会是什么样子?
经过一番研究,编写出了这个小程序,如下图所示:
原来我们的程序看起来是如此地杂乱无章!^ ^
下面来说说这个小程序的实现过程。
我们知道,无论是文本文件还是二进制文件,都是由一个个字节组成的。如果把这些字节的值当作像素的颜色值来显示,不就能把程序变成图片了吗?
注:Java有BufferedImage类,可以在运行时修改图像的属性以及每个象素,但这里我想直接“写”一个位图,而不是使用BufferedImage这样的现有的类。
籍由此思路,我开始想到的是将Java程序转化为JPG格式,但一番考察之后,发现JPG格式很复杂。它有很多段,且各段之间有着紧密的关联,最重要的是,其中还有哈夫曼表这种难以逆推的数据结构。
因此我转而想到较为简单的BMP格式。在最简单的情况下,它仅由一个文件头和像素的颜色数据两部分组成,且保存的像素数据与显示的像素一一对应:
BMP格式的头部很简单,仅仅由54个字节组成,其中只有部分字节需要改变:
public static final int BITMAP_HEADER_LENGTH = 0x36;
public static final int[] HEADER = {
0x42, 0x4D, -1, -1, -1, -1, 0x00, 0x00,
0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x28, 0x00,
0x00, 0x00, -1, -1, -1, -1, -1, -1,
-1, -1, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00,
0x00, 0x00, -1, -1, -1, -1, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
注意:BMP支持更复杂的特性(例如透明度、颜色表等),这里只是采用了最简单的一种BMP格式。
标为-1的字节是需要动态改变的,它们的含义如下:
- 第一行中的4个字节表示BMP文件的总字节数(包括文件头);
- 第三行的前4个字节表示图片的宽度,后面两个字节和第四行的两个字节表示图片的高;
- 第五行的4个字节则表示BMP文件的数据部分的字节数。
注意:这些数据都是采用“小端方式”存储的。即0x12345678依次存储为78 56 34 12四个字节。而如果采用相反的“大端方式”,则存储为12 34 56 78。
因此,最重要的就是要确定这些字段的值。相关代码如下:
public BitMapInputStream(File file) throws FileNotFoundException {
long size = file.length();
long pixelNum = size / PIXEL_BYTES;
long widthHeight = (long) Math.sqrt(pixelNum);
long rowBytesLength = widthHeight * PIXEL_BYTES / BYTE_ALIGNING * BYTE_ALIGNING;
width = rowBytesLength / PIXEL_BYTES;
height = widthHeight;
totalBytesLength = width * height * PIXEL_BYTES;
bitmapSize = totalBytesLength + BITMAP_HEADER_LENGTH;
reader = new FileInputStream(file);
}
程序首先根据文件的长度初步计算出像素数(每个像素3个字节,RGB),然后再进一步粗略地计算出宽高。为什么说是粗略地呢?因为在BMP中,每一行像素的字节数必须是4字节的整数倍。上面代码中的:
long rowBytesLength = widthHeight * PIXEL_BYTES / BYTE_ALIGNING * BYTE_ALIGNING;
这一句就是用来将行字节凑成4的整数倍的。
构造方法的中的其余代码就较为简单了。
接下来是BitMapInputStream的read()方法:
@Override
public int read() throws IOException {
++pointer;
if (pointer >= BITMAP_HEADER_LENGTH)
return reader.read();
switch (pointer) {
case 0x02: case 0x03: case 0x04: case 0x05: // bitmap total size
return ((int)bitmapSize>>(8*(pointer-0x02))) & 0xFF;
case 0x12: case 0x13: case 0x14: case 0x15: // width
return ((int)width>>(8*(pointer-0x12))) & 0xFF;
case 0x16: case 0x17: case 0x18: case 0x19: // height
return ((int)height>>(8*(pointer-0x16))) & 0xFF;
case 0x22: case 0x23: case 0x24: case 0x25: // data total length
return ((int)totalBytesLength>>(8*(pointer-0x22))) & 0xFF;
default:
return HEADER[pointer];
}
}
该方法依次返回BMP的每一个字节,供ImageIO使用,以构造一个BufferedImage。它根据要读取的字节在BMP文件中的偏移值来判断需要返回什么:如果偏移大于等于54(头部的长度),则返回所选择的文件中的字节;如果偏移是头部中需要动态计算的值,则用上面计算出的值代替(注意每个数值都要转化为小端方式存储的4个字节);否则就返回保存在HEADER数组中相应的字节。
下面附上全部代码:
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import javax.imageio.ImageIO;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.filechooser.FileFilter;
/**
* @author Terry
*/
public class ProgramToBitmap {
/**
* @param args
*/
public static void main(String[] args) {
new ShowPic();
}
}
class ShowPic extends JFrame {
private static final long serialVersionUID = -8283712863041322524L;
//
public static final int PIXEL_BYTES = 3;
public static final int BYTE_ALIGNING = 4;
public static final int BITMAP_HEADER_LENGTH = 0x36;
public static final int[] HEADER = {
0x42, 0x4D, -1, -1, -1, -1, 0x00, 0x00,
0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x28, 0x00,
0x00, 0x00, -1, -1, -1, -1, -1, -1,
-1, -1, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00,
0x00, 0x00, -1, -1, -1, -1, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
private Canvas canvas;
public ShowPic() {
super("Choose your file");
setLayout(null);
setBounds(200, 200, 600, 560);
canvas = new Canvas();
canvas.setBounds(0, 0, 600, 480);
add(canvas);
JButton ChooseFileBtn = new JButton("Choose your file");
ChooseFileBtn.setBounds(200, 485, 180, 30);
ChooseFileBtn.addActionListener(new ChooseFileActionListenser());
add(ChooseFileBtn);
JButton helpBtn = new JButton("?");
Font font = helpBtn.getFont();
helpBtn.setFont(new Font(font.getName(), font.getStyle(), font.getSize()-4));
helpBtn.setBounds(500, 490, 40, 25);
helpBtn.addActionListener(new HelpActionListenser());
add(helpBtn);
setDefaultCloseOperation(EXIT_ON_CLOSE);
setVisible(true);
}
private void showBitMap(File file) throws IOException {
BufferedImage image = ImageIO.read(new BitMapInputStream(file));
canvas.paintBMP(image);
setTitle(file.getName());
}
private class BitMapInputStream extends InputStream {
private long width;
private long height;
private long totalBytesLength;
private long bitmapSize;
private FileInputStream reader;
private int pointer = -1;
public BitMapInputStream(File file) throws FileNotFoundException {
long size = file.length();
long pixelNum = size / PIXEL_BYTES;
long widthHeight = (long) Math.sqrt(pixelNum);
long rowBytesLength = widthHeight * PIXEL_BYTES / BYTE_ALIGNING * BYTE_ALIGNING;
width = rowBytesLength / PIXEL_BYTES;
height = widthHeight;
totalBytesLength = width * height * PIXEL_BYTES;
bitmapSize = totalBytesLength + BITMAP_HEADER_LENGTH;
reader = new FileInputStream(file);
}
@Override
public int read() throws IOException {
++pointer;
if (pointer >= BITMAP_HEADER_LENGTH)
return reader.read();
switch (pointer) {
case 0x02: case 0x03: case 0x04: case 0x05: // bitmap total size
return ((int)bitmapSize>>(8*(pointer-0x02))) & 0xFF;
case 0x12: case 0x13: case 0x14: case 0x15: // width
return ((int)width>>(8*(pointer-0x12))) & 0xFF;
case 0x16: case 0x17: case 0x18: case 0x19: // height
return ((int)height>>(8*(pointer-0x16))) & 0xFF;
case 0x22: case 0x23: case 0x24: case 0x25: // data total length
return ((int)totalBytesLength>>(8*(pointer-0x22))) & 0xFF;
default:
return HEADER[pointer];
}
}
}
private class ChooseFileActionListenser implements ActionListener {
private File currentDir;
@Override
public void actionPerformed(ActionEvent event) {
try {
JFileChooser fileChooser = new JFileChooser();
if (currentDir != null)
fileChooser.setCurrentDirectory(currentDir);
fileChooser.setFileFilter(new FileFilter() {
@Override
public String getDescription() {
return "Java Files (.java, .class, .jar)";
}
@Override
public boolean accept(File f) {
if (f == null) return false;
if (f.isDirectory()) return true;
String name = f.getName().toLowerCase();
if (name.endsWith(".java")
|| name.endsWith(".class")
|| name.endsWith(".jar"))
return true;
return false;
}
});
int selection = fileChooser.showOpenDialog(ShowPic.this);
if (selection == JFileChooser.APPROVE_OPTION) {
File file = fileChooser.getSelectedFile();
currentDir = file.getParentFile();
showBitMap(file);
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
private class HelpActionListenser implements ActionListener {
public static final String USAGE =
"Choose a Java file, and then the program will show a picture by the file.";
@Override
public void actionPerformed(ActionEvent e) {
JOptionPane.showMessageDialog(
ShowPic.this, USAGE, "Help", JOptionPane.INFORMATION_MESSAGE);
}
}
}
class Canvas extends JPanel {
private static final long serialVersionUID = 4177361418259831798L;
private Image image;
public void paintBMP(Image bmp) {
image = bmp;
repaint();
}
@Override
public void paint(Graphics g) {
super.paint(g);
if (image != null) {
g.drawImage(image, 0, 0, getWidth(), getHeight(), null);
}
}
}