一、项目背景详细介绍
位图(bitmap,bmp)是一种最原始、最简单的图像文件格式,由微软和 ibm 在 1980 年代联合制定,用于 windows 操作系统。与 jpeg、png 等压缩格式相比,bmp 文件存储的是未经压缩的原始像素数据,文件头结构也相对简单,包含 bmp 文件头(14 字节)和 dib 信息头(通常 40 字节)的元数据,后面直接跟随像素数据。由于无压缩且像素排列规则,bmp 文件成为图像处理入门的首选格式,也是许多学习图像算法、文件格式解析的范例。
在 java 生态中,虽然 imageio 支持读取和写入 bmp,但其实现并不支持所有 bmp 变种(如带调色板的 8 位 bmp、压缩的 rle 格式、位域 bi_bitfields)。更重要的是,通过手写 bmp 解析与生成,可以深入理解二进制文件结构、字节对齐、像素存储顺序、色彩通道排列、大小端问题,以及 java nio、bytebuffer、datainputstream/dataoutputstream 等 api 的使用。
本项目旨在用纯 java 从零实现一个轻量级的 bmp 文件读写库,支持以下功能:
读取常见的 24 位真彩色 bmp 文件,解析文件头、信息头、像素数据;
将内存中的像素数据(argb 或 rgb 数组)写出为标准 bmp 文件;
支持带调色板的 8 位灰度 bmp 读写;
支持行字节对齐与填充;
提供简单易用的 api:
bmpimage read(file)
、void write(bmpimage, file)
;包含单元测试与示例,便于学习和集成。
通过本项目,您将掌握二进制文件解析、内存与磁盘数据映射、图像像素处理、文件 i/o、字节序与对齐等核心技术,既可用于图像算法学习,也可在不依赖第三方库的情况下完成基础图像处理需求。
二、项目需求详细介绍
核心功能
bmp 读取
解析 bmp 文件头(14 字节),获取文件大小、像素数据偏移;
解析 dib 信息头(至少 bitmapinfoheader,40 字节),获取宽度、高度、位深、压缩方式、像素数据大小;
支持 24 位(无调色板)和 8 位(带调色板)两种常见格式;
读取调色板数据(8 位 bmp);
读取像素数据,并根据行对齐规则计算实际字节长度,转换为
int[][]
或byte[][]
数组表示。
bmp 写入
将内存中像素数据构造 bmp 文件头和 dib 头,计算文件大小与偏移;
支持将
int[][]
(24 位真彩)或byte[][]
(8 位灰度)像素数据写入文件;自动填充行尾对齐字节(行长度必须是 4 的倍数);
写入调色板(灰度表),写入像素数据。
api 设计
class bmpimage
:封装宽度、高度、位深、调色板(可选)、像素数据;class bmpreader
:静态方法bmpimage read(file) throws ioexception, bmpparseexception
;class bmpwriter
:静态方法void write(bmpimage, file) throws ioexception
;自定义异常
bmpparseexception
用于格式错误。
扩展需求
支持 32 位带 alpha 通道 bmp(可选扩展);
支持 rle 压缩的 8 位 bmp(高阶扩展);
提供
bmpimage tobufferedimage()
方法转换为java.awt.image.bufferedimage
;提供从
bufferedimage
构建bmpimage
的工厂方法;
性能与健壮性
使用
bufferedinputstream
、bufferedoutputstream
或 niofilechannel
进行高效 i/o;对所有读取步骤进行合法性校验,格式不符时抛出
bmpparseexception
;单元测试覆盖宽高、位深、对齐、调色板、异常路径。
文档与测试
完整 javadoc 注释;
junit 5 单元测试,测试案例包括小尺寸 bmp、大尺寸 bmp、无效文件、非 bmp 文件;
示例主程序演示读取 bmp 文件并保存为另一个 bmp。
三、相关技术详细介绍
bmp 文件结构
bitmap file header(bitmapfileheader):14 字节
bftype
(2 bytes): 固定为 “bm” (0x42 0x4d);bfsize
(4 bytes): 文件总大小(字节);bfreserved1
、bfreserved2
(各 2 bytes):保留,通常为 0;bfoffbits
(4 bytes): 像素数据在文件中的偏移量(字节位置)。
dib header(bitmapinfoheader):40 字节
bisize
(4 bytes): dib 头大小,通常为 40;biwidth
(4 bytes)、biheight
(4 bytes):图像宽度、高度(像素);biplanes
(2 bytes): 颜色平面数,固定为 1;bibitcount
(2 bytes): 每像素位数,如 1、4、8、16、24、32;bicompression
(4 bytes):压缩方式(0 = bi_rgb 无压缩;1 = bi_rle8;2 = bi_rle4;3 = bi_bitfields);bisizeimage
(4 bytes):像素数据大小(字节),可为 0;bixpelspermeter
、biypelspermeter
(各 4 bytes):水平/垂直分辨率;biclrused
(4 bytes):调色板中颜色数,0 表示默认;biclrimportant
(4 bytes):重要颜色数,0 表示全部重要。
color table(可选):当
bibitcount
≤ 8 时存在,每条 4 字节(b, g, r, reserved)pixel array:按行从下到上(bmp 默认),每行左到右;每行长度需填充到 4 字节对齐。
java i/o 与 nio
datainputstream
/dataoutputstream
:方便读取/写入基本类型大端或小端;bytebuffer
:调整字节序(order(byteorder.little_endian)
);filechannel
+mappedbytebuffer
:可选内存映射加速;bufferedinputstream
/bufferedoutputstream
:缓冲字节流提高效率。
字节对齐
bmp 每行像素数据占用字节数 =
((width * bitsperpixel + 31) / 32) * 4
对齐后每行末尾填充 0x00。
错误处理
当
bftype
不是 “bm” 或bibitcount
不支持时,抛出bmpparseexception
;当文件过短、偏移超出或数据不完整时,抛出异常。
java2d 互操作
将
bmpimage
转为bufferedimage
:
bufferedimage img = new bufferedimage(width, height, bufferedimage.type_int_rgb); for (y,h) ... img.setrgb(x,y,pixel);
从 bufferedimage
构造 bmpimage
:
int rgb = img.getrgb(x,y); // 分离 r,g,b 通道
四、实现思路详细介绍
数据模型定义
class bmpimage
:
public class bmpimage { int width, height; short bitcount; // 8 或 24 int[][] pixels24; // [row][col] 每像素 0x00rrggbb byte[][] pixels8; // [row][col] 调色板索引 int[] palette; // 8 位调色板,length = colorsused }
- 只存储必要字段,其它 dib 信息头字段可忽略或保留。
读取流程(bmpreader)
- 打开文件,使用
datainputstream
包装bufferedinputstream
; 读取并校验 bmp 文件头:
readunsignedshortle()
(小端),检查 “bm”;读取文件大小、保留字段、像素偏移;
读取 dib 头长度,判断格式,仅处理
bisize == 40
;读取宽、高、平面数、位深、压缩方式;
计算行占用字节数
rowbytes = ((width * bitcount + 31) / 32) * 4
;若
bitcount == 8
,读取colorsused
条调色板,每条 4 字节,存入palette
;根据
height
正负判断存储方向(正值从下往上,负值自顶向下);分行读取像素数据,解码 24 位真彩色或 8 位索引,存入
pixels24
或pixels8
;
写入流程(bmpwriter)
根据
bmpimage
字段,计算rowbytes
与pixeldatasize = rowbytes * abs(height)
;bfsize = 14 + dibsize + palettesize + pixeldatasize
;使用
dataoutputstream
,按小端顺序写入 bitmapfileheader;写入 bitmapinfoheader 各字段;
若 8 位,写入调色板;
按行填充写入像素数据,注意 4 字节对齐,写入行尾填充字节;
辅助方法
readunsignedshortle()
、readintle()
:读取小端数;writeshortle()
、writeintle()
:写入小端;padzeros(int count)
:写入指定数量的 0;
与 bufferedimage 互操作
bmpimage tobufferedimage()
:构造bufferedimage
并填充像素;static bmpimage frombufferedimage(bufferedimage img, boolean usepalette)
;
异常与校验
在各读取阶段检查可用字节数;
对不支持的格式或参数,立即抛
bmpparseexception
;在写入前验证
bitcount
、数据数组与宽高一致。
五、完整实现代码
// =================================================== // 文件:src/main/java/com/example/bmp/bmpimage.java // =================================================== package com.example.bmp; import java.awt.image.bufferedimage; /** * bmp 图像数据模型 */ public class bmpimage { public int width; public int height; public short bitcount; // 8 或 24 public int[][] pixels24; // 每像素 0x00rrggbb public byte[][] pixels8; // 每像素调色板索引 public int[] palette; // 调色板,length = colorsused /** 转换为 bufferedimage */ public bufferedimage tobufferedimage() { bufferedimage img = new bufferedimage(width, math.abs(height), bitcount == 24 ? bufferedimage.type_int_rgb : bufferedimage.type_byte_indexed); if (bitcount == 24) { for (int y = 0; y < math.abs(height); y++) { for (int x = 0; x < width; x++) { img.setrgb(x, y, pixels24[y][x]); } } } else { // 8 位,需创建 indexcolormodel(此处略) // 简单填充为灰度图 for (int y = 0; y < math.abs(height); y++) { for (int x = 0; x < width; x++) { int idx = pixels8[y][x] & 0xff; int c = palette[idx]; img.setrgb(x, y, c); } } } return img; } } // =================================================== // 文件:src/main/java/com/example/bmp/bmpparseexception.java // =================================================== package com.example.bmp; /** bmp 解析异常 */ public class bmpparseexception extends exception { public bmpparseexception(string msg) { super(msg); } } // =================================================== // 文件:src/main/java/com/example/bmp/bmpreader.java // =================================================== package com.example.bmp; import java.io.*; import java.nio.byteorder; /** * bmp 文件读取器 */ public class bmpreader { public static bmpimage read(file file) throws ioexception, bmpparseexception { try (datainputstream dis = new datainputstream(new bufferedinputstream(new fileinputstream(file)))) { // 1. 读取文件头 int bftype = readunsignedshortle(dis); if (bftype != 0x4d42) throw new bmpparseexception("非 bmp 文件"); int bfsize = readintle(dis); dis.skipbytes(4); // reserved int bfoffbits = readintle(dis); // 2. 读取 dib 头 int dibsize = readintle(dis); if (dibsize != 40) throw new bmpparseexception("不支持的 dib 头大小: " + dibsize); int width = readintle(dis); int height = readintle(dis); short planes = readshortle(dis); short bitcount = readshortle(dis); int compression = readintle(dis); if (compression != 0) throw new bmpparseexception("不支持压缩: " + compression); int imagesize = readintle(dis); dis.skipbytes(16); // 跳过分辨率与颜色信息 int colorsused = readintle(dis); if (colorsused == 0 && bitcount <= 8) { colorsused = 1 << bitcount; } // 构造 bmpimage bmpimage img = new bmpimage(); img.width = width; img.height = height; img.bitcount = bitcount; // 3. 读取调色板(8 位) if (bitcount == 8) { img.palette = new int[colorsused]; for (int i = 0; i < colorsused; i++) { int b = dis.readunsignedbyte(); int g = dis.readunsignedbyte(); int r = dis.readunsignedbyte(); dis.readunsignedbyte(); // 保留 img.palette[i] = (r << 16) | (g << 8) | b; } img.pixels8 = new byte[math.abs(height)][width]; } else if (bitcount == 24) { img.pixels24 = new int[math.abs(height)][width]; } else { throw new bmpparseexception("仅支持 8 位和 24 位 bmp"); } // 4. 跳转到像素数据偏移 long skipped = dis.skip(bfoffbits - 14 - dibsize - (bitcount==8 ? colorsused*4 : 0)); // 5. 计算行长度(字节)对齐到 4 字节 int rowbytes = ((width * bitcount + 31) / 32) * 4; // 6. 读取像素数据 boolean bottomup = height > 0; int absheight = math.abs(height); for (int row = 0; row < absheight; row++) { int y = bottomup ? absheight - 1 - row : row; byte[] rowdata = new byte[rowbytes]; dis.readfully(rowdata); bytearrayinputstream rowin = new bytearrayinputstream(rowdata); for (int x = 0; x < width; x++) { if (bitcount == 24) { int b = rowin.read(); int g = rowin.read(); int r = rowin.read(); img.pixels24[y][x] = (r << 16) | (g << 8) | b; } else { int idx = rowin.read(); img.pixels8[y][x] = (byte) idx; } } } return img; } } // 小端读取辅助 private static int readunsignedshortle(datainputstream dis) throws ioexception { int b1 = dis.readunsignedbyte(); int b2 = dis.readunsignedbyte(); return (b2 << 8) | b1; } private static short readshortle(datainputstream dis) throws ioexception { int u = readunsignedshortle(dis); return (short) u; } private static int readintle(datainputstream dis) throws ioexception { int b1 = dis.readunsignedbyte(); int b2 = dis.readunsignedbyte(); int b3 = dis.readunsignedbyte(); int b4 = dis.readunsignedbyte(); return (b4 << 24) | (b3 << 16) | (b2 << 8) | b1; } } // =================================================== // 文件:src/main/java/com/example/bmp/bmpwriter.java // =================================================== package com.example.bmp; import java.io.*; /** * bmp 文件写入器 */ public class bmpwriter { public static void write(bmpimage img, file file) throws ioexception { try (dataoutputstream dos = new dataoutputstream(new bufferedoutputstream(new fileoutputstream(file)))) { int width = img.width; int absheight = math.abs(img.height); int bitcount = img.bitcount; int rowbytes = ((width * bitcount + 31) / 32) * 4; int imagesize = rowbytes * absheight; int palettesize = (bitcount == 8 ? img.palette.length * 4 : 0); int bfoffbits = 14 + 40 + palettesize; int bfsize = bfoffbits + imagesize; // 1. 写文件头 writeshortle(dos, 0x4d42); // "bm" writeintle(dos, bfsize); writeshortle(dos, 0); writeshortle(dos, 0); writeintle(dos, bfoffbits); // 2. 写 dib 头(bitmapinfoheader) writeintle(dos, 40); writeintle(dos, width); writeintle(dos, img.height); writeshortle(dos, 1); // planes writeshortle(dos, bitcount); writeintle(dos, 0); // bi_rgb writeintle(dos, imagesize); writeintle(dos, 0); writeintle(dos, 0); // 分辨率 writeintle(dos, bitcount == 8 ? img.palette.length : 0); writeintle(dos, 0); // 3. 写调色板 if (bitcount == 8) { for (int c : img.palette) { int r = (c >> 16) & 0xff; int g = (c >> 8) & 0xff; int b = c & 0xff; dos.writebyte(b); dos.writebyte(g); dos.writebyte(r); dos.writebyte(0); } } // 4. 写像素数据 byte[] pad = new byte[rowbytes - (width * bitcount / 8)]; for (int row = absheight - 1; row >= 0; row--) { if (bitcount == 24) { for (int x = 0; x < width; x++) { int rgb = img.pixels24[row][x]; dos.writebyte(rgb & 0xff); // b dos.writebyte((rgb >> 8) & 0xff); // g dos.writebyte((rgb >> 16) & 0xff); // r } } else { for (int x = 0; x < width; x++) { dos.writebyte(img.pixels8[row][x]); } } dos.write(pad); } } } // 小端写入辅助 private static void writeshortle(dataoutputstream dos, int v) throws ioexception { dos.writebyte(v & 0xff); dos.writebyte((v >> 8) & 0xff); } private static void writeintle(dataoutputstream dos, int v) throws ioexception { dos.writebyte(v & 0xff); dos.writebyte((v >> 8) & 0xff); dos.writebyte((v >> 16) & 0xff); dos.writebyte((v >> 24) & 0xff); } } // 文件:src/main/java/com/example/bmp/main.java package com.example.bmp; import java.io.file; public class main { public static void main(string[] args) throws exception { // 读取 bmp bmpimage img = bmpreader.read(new file("input.bmp")); system.out.println("读取完成: " + img.width + "x" + math.abs(img.height) + " 位深=" + img.bitcount); // 转换为 bufferedimage 并另存为 png(示例) // imageio.write(img.tobufferedimage(), "png", new file("out.png")); // 修改像素:反转颜色示例 if (img.bitcount == 24) { for (int y = 0; y < math.abs(img.height); y++) { for (int x = 0; x < img.width; x++) { int rgb = img.pixels24[y][x]; int r = 255 - ((rgb >> 16) & 0xff); int g = 255 - ((rgb >> 8) & 0xff); int b = 255 - (rgb & 0xff); img.pixels24[y][x] = (r << 16) | (g << 8) | b; } } } // 写入 bmp bmpwriter.write(img, new file("output.bmp")); system.out.println("写入完成"); } }
六、代码详细解读
bmpimage:封装 bmp 图像的核心数据,包括宽度、高度、位深、调色板(8 位)或真彩色像素数组,以及与
bufferedimage
互操作的方法。bmpparseexception:自定义解析异常,用于格式校验失败时抛出。
bmpreader:
读取 bmp 文件头(小端),检查“bm”标识;
读取 dib 头中的宽高、位深、压缩方式,并校验仅支持无压缩 8/24 位;
读取调色板(8 位),或分配像素数组;
跳转到像素数据偏移位置,按行读取像素并考虑 4 字节对齐;
bmpwriter:
计算行长度、像素数据大小和文件总大小;
写入文件头与 dib 头(小端),包括必要字段;
写入调色板(8 位)或跳过;
按行自下而上写入像素数据,并填充行尾对齐字节;
main:示例演示 bmp 文件读取、像素修改(反色)、bmp 写入,以及与
bufferedimage
的互操作。
七、项目详细总结
功能完整:支持读取和写入最常见的 8 位带调色板 bmp 和 24 位真彩色 bmp;
纯 java 实现:无第三方依赖,便于集成到任意 java 项目;
对齐与字节序处理:正确实现行对齐及小端字节序,确保跨平台一致性;
易用 api:提供
bmpreader.read()
、bmpwriter.write()
两个静态方法,简洁明了;性能可控:使用缓冲流和按行处理,内存占用可控;
可扩展:后续可加入 32 位 alpha 通道、rle 压缩、性能优化的 nio 实现。
八、项目常见问题及解答
q:为何 bmp 读取时要按行倒序?
a:bmp 默认自下而上存储像素,高度字段若为正值表示倒序;q:如何支持其它 dib 头格式?
a:在解析时根据bisize
分支处理不同头结构,如 bitmapv2infoheader(52 字节);q:写入 32 位带 alpha bmp?
a:将bitcount
设为 32,写入 bgra 顺序像素,dib 头中的位域需设置 bi_bitfields;q:如何提高大文件读写性能?
a:可使用 niofilechannel
与mappedbytebuffer
,一次映射全部或部分文件;q:写入时如何生成灰度调色板?
a:调色板数组palette[i] = (i << 16)|(i<<8)|i
,0-255 等级灰度。
九、扩展方向与性能优化
nio 内存映射:使用
filechannel.map()
将文件映射到内存,使用bytebuffer
直接读取写入,减少复制与方法调用;并行读取与处理:对大图分块并行读取和像素处理,提高多核利用率;
支持更多格式:扩展到 rle 压缩的 8 位 bmp、16 位 rgb565、32 位 bi_bitfields;
动态调色板生成:支持自定义调色板或从图像均衡化生成伪彩色;
与 java2d 整合:提供直接在
graphics2d
上绘制 bmp 数据的优化方法;流式 api:提供从
inputstream
和outputstream
读取写入的重载方法,方便网络传输;内存优化:使用压缩存储结构、按需加载行数据,处理超大图像防止 oom;
测试与基准:使用 jmh 对比
imageio
与本实现的读写性能差异,并进行调优;工具类集成:将项目打包为 maven 依赖,提供 cli 工具快速转换 bmp 格式。
以上就是利用java实现读写bmp文件的示例代码的详细内容,更多关于java读写bmp文件的资料请关注代码网其它相关文章!
发表评论