目录
- 一、项目背景详细介绍
- 二、项目需求详细介绍
- 三、相关技术详细介绍
- 四、实现思路详细介绍
- 五、完整实现代码
- 六、代码详细解读
- 七、项目详细总结
- 八、项目常见问题及解答
- 九、扩展方向与性能优化
一、项目背景详细介绍
位图(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/DataOut编程putStream 等 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 !=python 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 intgSPIhsspG[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); // 分辨率 javascript 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()
两个静态方法,简洁明了;性能可控:使用缓冲流和按行处理,内存占用可控;
可扩展:后续可加入 3python2 位 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文件的资料请关注编程客栈(www.devze.com)其它相关文章!
精彩评论