当前位置: 代码网 > it编程>编程语言>Java > 利用Java实现读写bmp文件的示例代码

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

2025年05月27日 Java 我要评论
一、项目背景详细介绍位图(bitmap,bmp)是一种最原始、最简单的图像文件格式,由微软和 ibm 在 1980 年代联合制定,用于 windows 操作系统。与 jpeg、png 等压缩格式相比,

一、项目背景详细介绍

位图(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、字节序与对齐等核心技术,既可用于图像算法学习,也可在不依赖第三方库的情况下完成基础图像处理需求。

二、项目需求详细介绍

  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 != 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 的互操作。

七、项目详细总结

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

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

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

  4. 易用 api:提供 bmpreader.read()bmpwriter.write() 两个静态方法,简洁明了;

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

  6. 可扩展:后续可加入 32 位 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文件的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com