什么叫 BMP 到 HTML 呢……?就是生成一个网页,里面用不同颜色的字符拼出那个图片……很无聊的功能嗯,而且原理上说,生成的 HTML 文件如果要表现整个 BMP 的所有细节,大小肯定大大超过原 BMP 文件……
为什么会做这个呢?主要是受到我们 C++ 老师的启发,尝试去做的。不过我没有用 C++ 写,而是选用了寒假学的 Erlang,这也是我写的第一个 Erlang 程序。
为什么会选用 Erlang 呢?主要是基于两点原因:1、寒假学了半天,一点都没有练过,就拿这个来练练;2、看中了 Erlang 强大的模式匹配和比特语法。比特语法在 Erlang 里面原来是用来解决网络传输协议中的二进制数据的,不过这里拿来处理二进制文件着实是一个很好的选择~不过其实 Erlang 真正最重要的特性:面向并发,我完全没有用到,而是继续使用了顺序编程。主要是,BMP to HTML 没什么可以并发化的,而且就算并发了,也是大传输小计算,并没有什么很大的优势。因为是第一次写 Erlang 程序,如果有 Erlang 高手路过,还请多多指点咯~
另外一点,为什么选用 BMP 这种几乎被人抛弃的格式呢?因为最容易呗……而且我这里还用了 BMP 中最简单的一种:真彩色无压缩格式。这是最直接的图形表示方式了,就是一个点一个点的,每个点三个字节表示一种颜色。所以还是很简单的。
先看看最后的效果:
下面说干就干。
BMP 文件结构
首先要知道的就是 BMP 的格式。在维基百科里的相关条目有很详细的记载,甚至于文件头的 C 结构都已经直接给出来了。不过这个当然对我没有很大的用途,因为用的是 Erlang 嘛~识别文件头要比 C 方便多了~下面我先说说 BMP 的文件结构:
BMP 文件头
一个 BMP 文件最开始是 BMP 文件头。这个文件头的大小是固定的14字节,包含 BMP 文件的 Magic Number (魔术数字,即文件识别码) 2字节、整个文件大小4字节、保留区4字节、图像数据在文件中的起始位置4字节。
其中 Magic Number 我们最常见的是 Windows BMP 文件的 BM
,除此之外,还有 BA
、CI
、CP
、IC
、PT
这几种,应该是不太常见吧。
整个文件的大小其实我觉得在这里是没有什么用途的,而整整4字节的保留区也是令我很难理解的。整个14字节的头当中,最重要的应该就是图像数据的偏移。如果这个偏移量是相对于整个文件而言的,因此直接把读取指针指向偏移量的位置就是图像数据。
DIB 头
这个信息其实是关于图像描述最有意义的部分。这里面将会描述这张图片几乎一切你需要的信息。
DIB 头最开始的32位,也就是4个字节,描述了 DIB 头的长度。根据 DIB 头的长度不同,共有4种 BMP 图片:
- 12字节 – OS/2 V1 格式,OS/2 和 Windows 3.0 以后所有版本支持
- 64字节 – OS/2 V2 格式
- 40字节 – Windows V3,Windows 3.0 以后所有版本支持
- 108字节 – Windows V4,Windows 95/NT4 以后所有版本支持
- 124字节 – Windows V5,Windows 98/2000 以后所有版本支持
鉴于 Windows V4、V5 都与 V3 的格式兼容,我在程序里面事实上仅仅实现了 V3 的头识别。
V3 头的40字节分布,我觉得已经很复杂了……分别是:
- DIB 头大小,4字节,这里为40
- 图像宽度,4字节
- 图像高度,4字节
- Color planes (不知道是什么东西),2字节,必须为1
- 颜色位数,2字节,可以为1、4、8、16、24、32,我的程序中只识别24位色
- 压缩方式,4字节,我的程序里只能识别不压缩的……值为0
- 图像信息大小,4字节,与整个文件大小不一样,这里表示的是图像信息那块数据的大小
- 横向分辨率,4字节,单位像素/米
- 纵向分辨率,4字节,单位像素/米
- 调色板颜色数,4字节,如果为0则默认为2n
- 重要颜色数量,4字节,不知道干什么用的,直接忽略好了……
介绍完了这个 DIB 头,其实很很没意思就是了。真正有用的,也就这么几个:长度、宽度。如果要识别能力强一点,识别一下调色板相关的,也就差不多了。
调色板
因为我没有用到调色板,所以也没有细看这块内容,直接跳过……
图像数据
显然对于我的最基本选择要求:24位色、无压缩,图像信息是很简单的,就是每个像素占3个字节。
不过仍然有两点需要注意:1、颜色储存依次是是蓝、绿、红,而非红、绿、蓝;2、每行的字节数必须是4的整倍数。也就是 BMP 文件每行最后会有1-3个字节用于补足,这个补足用字节数正是宽度除以4的余数。
Erlang 程序
BMP 图片的基本信息探究清楚了,下面就可以开始写咯~
导出函数
第一部分就是模块的声明和导出函数
-module(bmp).
-export([bmp_to_html/2]).
-define(ULI, unsigned-little-integer).
bmp_to_html(Bmp, Html) ->
case bmp_to_html(Bmp) of
{ok, Bin} ->
file:write_file(Html, Bin);
Error ->
{Bmp, Error}
end.
bmp_to_html(File) ->
case file:open(File, [read, binary, raw]) of
{ok, F} ->
{ok, ["<span style=\"font-size: 1px; line-height: 1px;\">",
convert_bmp(F),"</span>"]};
Error ->
{File, Error}
end.
我在 bmp
模块内导出了一个 bmp_to_html
的函数,他读取一个 bmp
文件,转换并写入一个 html
文件。中间定义了一个宏 ULI
,很容易理解的~其实定义这个的主要原因是那个 little……由于 Erlang 最初是为网络协议做的这个比特语法,因此默认是 Big endian 的,而 x86 系的 CPU 却总是使用 Little endian 的……
读取 BMP 文件信息
convert_bmp(F) ->
{Offset, {Size, Width, Height}} = read_header(F),
{ok, Bin} = file:pread(F, Offset, Size),
L = binary_to_list(Bin),
each_pixel(Width, Height, L).
read_header(F) ->
{ok, Bin} = file:pread(F, 0, 14),
case Bin of
<<"BM", _FileSize: 32/?ULI,
_Creator: 32/?ULI,
Offset: 32/?ULI
>> ->
{Offset, read_dib_header(F)};
_Other ->
{error, unsupported, _Other}
end.
read_dib_header(F) ->
{ok, Bin} = file:pread(F, 14, 4),
case Bin of
<<40:32/?ULI>> ->
{ok, << Width: 32/?ULI,
Height: 32/?ULI,
_: 16/?ULI,
_: 16/?ULI,
0: 32/?ULI,
Size: 32/?ULI
>>} = file:pread(F, 18, 20),
{Size, Width, Height};
_Other ->
{error, unsupported, _Other}
end.
很明显,为了简化程序,BMP 文件头和 DIB 头的识别都只写了一点点,够用就行~
转换图像数据
这是最后的部分了,将图像数据转换为 HTML 代码~
each_pixel(Width, Height, Bin) ->
each_pixel(Width, Height, Width, Height, Bin, []).
each_pixel(0, 0, _, _, _, Output) ->
Output;
each_pixel(0, Y, Width, Height, Bin, Output) ->
NewBin = drop_begin(Bin, Width rem 4),
NewOutput = [new_line() | Output],
each_pixel(Width, Y-1, Width, Height, NewBin, NewOutput);
each_pixel(_, 0, _, _, _, Output) ->
Output;
each_pixel(X, Y, Width, Height, Bin, Output) ->
[B,G,R|NewBin] = Bin,
NewOutput = [to_html(R, G, B) | Output],
each_pixel(X-1, Y, Width, Height, NewBin, NewOutput).
drop_begin(Bin, 0) ->
Bin;
drop_begin([_|Bin], Num) ->
drop_begin(Bin, Num-1).
new_line() ->
["<br>"].
to_html(R, G, B) ->
io_lib:format("<font color=\"#~.16B~.16B~.16B\">█</font>", [R, G, B]).
这是最后的部分了~其中字符选用了一个非常黑的字符“█”嗯~
至于为什么用 font
这个不推荐的标签,其实是我觉得这个文件本身就很冗长了,再变成样式就……所以……
使用
差不多了,整个程序全部拼在一起,就出来了~
最后,在 Erlang 的命令行中输入:
c(bmp).
bmp:bmp_to_html("BMP 文件", "HTML 文件").
就好了~
我用了一个 2.6KB 的 BMP 文件,转换出了一个 26.3KB 的 HTML 文件……真够大……
参考资料
There are comments.