一、项目背景详细介绍
位图(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文件的资料请关注代码网其它相关文章!
发表评论