开发者

利用Java实现读写bmp文件的示例代码

开发者 https://www.devze.com 2025-05-27 10:32 出处:网络 作者: 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/DataOut编程putStream 等 API 的使用。

本项目旨在用纯 Java 从零实现一个轻量级的 BMP 文件读写库,支持以下功能:

  • 读取常见的 24 位真彩色 BMP 文件,解析文件头、信息头、像素数据;

  • 将内存中的像素数据(ARGB 或 RGB 数组)写出为标准 BMP 文件;

  • 支持带调色板的 8 位灰度 BMP 读写;

  • 支持行字节对齐与填充;

  • 提供简单易用的 API:BmpImage read(File)void write(BmpImage, File)

  • 包含单元测试与示例,便于学习和集成。

通过本项目,您将掌握二进制文件解析、内存与磁盘数据映射、图像像素处理、文件 I/O、字节序与对齐等核心技术,既可用于图像算法学习,也可在不依赖第三方库的情况下完成基础图像处理需求。

二、项目需求详细介绍

  1. 核心功能

    • BMP 读取

      • 解析 BMP 文件头(14 字节),获取文件大小、像素数据偏移;

      • 解析 DIB 信息头(至少 BITMAPINFOHEADER,40 字节),获取宽度、高度、位深、压缩方式、像素数据大小;

      • 支持 24 位(无调色板)和 8 位(带调色板)两种常见格式;

      • 读取调色板数据(8 位 BMP);

      • 读取像素数据,并根据行对齐规则计算实际字节长度,转换为 int[][] 或 byte[][] 数组表示。

    • BMP 写入

      • 将内存中像素数据构造 BMP 文件头和 DIB 头,计算文件大小与偏移;

      • 支持将 int[][](24 位真彩)或 byte[][](8 位灰度)像素数据写入文件;

      • 自动填充行尾对齐字节(行长度必须是 4 的倍数);

      • 写入调色板(灰度表),写入像素数据。

  2. API 设计

    • class BmpImage:封装宽度、高度、位深、调色板(可选)、像素数据;

    • class BmpReader:静态方法 BmpImage read(File) throws IOException, BmpParseException

    • class BmpWriter:静态方法 void write(BmpImage, File) throws IOException

    • 自定义异常 BmpParseException 用于格式错误。

  3. 扩展需求

    • 支持 32 位带 Alpha 通道 BMP(可选扩展);

    • 支持 RLE 压缩的 8 位 BMP(高阶扩展);

    • 提供 BmpImage toBufferedImage() 方法转换为 java.awt.image.BufferedImage

    • 提供从 BufferedImage 构建 BmpImage 的工厂方法;

  4. 性能与健壮性

    • 使用 BufferedInputStreamBufferedOutputStream 或 NIO FileChannel 进行高效 I/O;

    • 对所有读取步骤进行合法性校验,格式不符时抛出 BmpParseException

    • 单元测试覆盖宽高、位深、对齐、调色板、异常路径。

  5. 文档与测试

    • 完整 Javadoc 注释;

    • JUnit 5 单元测试,测试案例包括小尺寸 BMP、大尺寸 BMP、无效文件、非 BMP 文件;

    • 示例主程序演示读取 BMP 文件并保存为另一个 BMP。

三、相关技术详细介绍

  1. BMP 文件结构

    • Bitmap File Header(BITMAPFILEHEADER):14 字节

      • bfType (2 bytes): 固定为 “BM” (0x42 0x4D);

      • bfSize (4 bytes): 文件总大小(字节);

      • bfReserved1bfReserved2 (各 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;

      • biXPelsPerMeterbiYPelsPerMeter (各 4 bytes):水平/垂直分辨率;

      • biClrUsed (4 bytes):调色板中颜色数,0 表示默认;

      • biClrImportant (4 bytes):重要颜色数,0 表示全部重要。

    • Color Table(可选):当 biBitCount ≤ 8 时存在,每条 4 字节(B, G, R, Reserved)

    • Pixel Array:按行从下到上(BMP 默认),每行左到右;每行长度需填充到 4 字节对齐。

  2. Java I/O 与 NIO

    • DataInputStream / DataOutputStream:方便读取/写入基本类型大端或小端;

    • ByteBuffer:调整字节序(order(ByteOrder.LITTLE_ENDIAN));

    • FileChannel + MappedByteBuffer:可选内存映射加速;

    • BufferedInputStream / BufferedOutputStream:缓冲字节流提高效率。

  3. 字节对齐

    • BMP 每行像素数据占用字节数 = ((width * bitsPerPixel + 31) / 32) * 4

    • 对齐后每行末尾填充 0x00。

  4. 错误处理

    • 当 bfType 不是 “BM” 或 biBitCount 不支持时,抛出 BmpParseException

    • 当文件过短、偏移超出或数据不完整时,抛出异常。

  5. 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

  1. 写入流程(BmpWriter)

    • 根据 BmpImage 字段,计算 rowBytes 与 pixelDataSize = rowBytes * abs(height)

    • bfSize = 14 + dibSize + paletteSize + pixelDataSize

    • 使用 DataOutputStream,按小端顺序写入 BITMAPFILEHEADER;

    • 写入 BITMAPINFOHEADER 各字段;

    • 若 8 位,写入调色板;

    • 按行填充写入像素数据,注意 4 字节对齐,写入行尾填充字节;

  2. 辅助方法

    • readUnsignedShortLE()readIntLE():读取小端数;

    • writeShortLE()writeIntLE():写入小端;

    • padzeros(int count):写入指定数量的 0;

  3. 与 BufferedImage 互操作

    • BmpImage toBufferedImage():构造 BufferedImage 并填充像素;

    • static BmpImage fromBufferedImage(BufferedImage img, boolean usePalette)

  4. 异常与校验

    • 在各读取阶段检查可用字节数;

    • 对不支持的格式或参数,立即抛 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 的互操作。

七、项目详细总结

  1. 功能完整:支持读取和写入最常见的 8 位带调色板 BMP 和 24 位真彩色 BMP;

  2. 纯 Java 实现:无第三方依赖,便于集成到任意 Java 项目;

  3. 对齐与字节序处理:正确实现行对齐及小端字节序,确保跨平台一致性;

  4. 易用 API:提供 BmpReader.read()BmpWriter.write() 两个静态方法,简洁明了;

  5. 性能可控:使用缓冲流和按行处理,内存占用可控;

  6. 可扩展:后续可加入 3python2 位 Alpha 通道、RLE 压缩、性能优化的 NIO 实现。

八、项目常见问题及解答

  1. Q:为何 BMP 读取时要按行倒序?

    A:BMP 默认自下而上存储像素,高度字段若为正值表示倒序;

  2. Q:如何支持其它 DIB 头格式?

    A:在解析时根据 biSize 分支处理不同头结构,如 BITMAPV2INFOHEADER(52 字节);

  3. Q:写入 32 位带 Alpha BMP?

    A:将 bitCount 设为 32,写入 BGRA 顺序像素,DIB 头中的位域需设置 BI_BITFIELDS;

  4. Q:如何提高大文件读写性能?

    A:可使用 NIO FileChannel 与 MappedByteBuffer,一次映射全部或部分文件;

  5. Q:写入时如何生成灰度调色板?

    A:调色板数组 palette[i] = (i << 16)|(i<<8)|i,0-255 等级灰度。

九、扩展方向与性能优化

  1. NIO 内存映射:使用 FileChannel.map() 将文件映射到内存,使用 ByteBuffer 直接读取写入,减少复制与方法调用;

  2. 并行读取与处理:对大图分块并行读取和像素处理,提高多核利用率;

  3. 支持更多格式:扩展到 RLE 压缩的 8 位 BMP、16 位 RGB565、32 位 BI_BITFIELDS;

  4. 动态调色板生成:支持自定义调色板或从图像均衡化生成伪彩色;

  5. 与 Java2D 整合:提供直接在 Graphics2D 上绘制 BMP 数据的优化方法;

  6. 流式 API:提供从 InputStream 和 OutputStream 读取写入的重载方法,方便网络传输;

  7. 内存优化:使用压缩存储结构、按需加载行数据,处理超大图像防止 OOM;

  8. 测试与基准:使用 JMH 对比 ImageIO 与本实现的读写性能差异,并进行调优;

  9. 工具类集成:将项目打包为 Maven 依赖,提供 CLI 工具快速转换 BMP 格式。

以上就是利用Java实现读写bmp文件的示例代码的详细内容,更多关于Java读写bmp文件的资料请关注编程客栈(www.devze.com)其它相关文章!

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号