利用Java实现读写bmp文件的示例代码
作者:Katie。
一、项目背景详细介绍
位图(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文件的资料请关注脚本之家其它相关文章!
