手把手 bmp 格式编解码(三)—— 手搓 libbmp 源码

git 传送门:https://gitee.com/louistark/lib_bmp

1. 手搓流程简述

  前两篇文章我们已经对 BMP 图像格式的内容和细节有了较为深入的了解,并且通过手动解析两张 BMP 图片,对 BMP 格式的编解码方式有了直观认识。所以,在这篇文章中,我们可以尝试手搓一下 BMP 图片的编解码源码。

1.1 格式约束

BMP 的格式和用法比较多样,此处我们只讲 最常用的 BMP 类型,因此做以下几点约束:

  1. 图像格式:Windows V1 BMP 格式
  2. 像素位深:24 bit
  3. 图像压缩:不进行压缩
  4. 调色板:不使用调色板

1.2 数据结构

手搓源码第一步,我们需要定义一些 BMP 图像相关的 数据结构

  1. BMP 文件头数据结构
  2. RGB 图像数据结构
  3. BMP 图像在内存中的数据结构

1.3 处理函数

手搓源码第二步,我们需要写一些 BMP 编解码需要用到的几类 处理函数

  1. BMP 图像内存管理函数(分配、释放内存)
  2. BMP 图像文件头读写函数(读取、解析、写入文件头)
  3. BMP 图像数据读写函数(读取、写入 rgb 图像数据)
  4. BMP 文件读写函数(读取、写入 bmp 文件)

2. 数据结构

2.1 BMP 文件头数据结构

  根据我们之前列出的 BMP 文件头数据结构表,我们可以非常轻松的写出 位图文件头文件信息头 数据结构。

序号字节数变量名变量类型字段名
12bfTypeunsigned short文件类型
24bfSizeunsigned int文件大小
32bfReserved1unsigned short保留字段 1
42bfReserved2unsigned short保留字段 2
54bfOffBitsunsigned int数据偏移量
64biSizeunsigned intDIB 头大小
74biWidthint图像宽度
84biHeightint图像高度
92biPlanesunsigned short颜色平面数
102biBitCountunsigned short每个像素的位数
114biCompressionunsigned int压缩类型
124biSizeImagesunsigned int图像数据大小
134biXPelsPerMeterint水平分辨率
144biYPelsPerMeterint垂直分辨率
154biClrUsedunsigned int调色板大小
164biClrImportantunsigned int重要颜色数
typedef struct __bmp_file_header
{
    unsigned short          bfType;
    unsigned int            bfSize;
    unsigned short          bfReserved1;
    unsigned short          bfReserved2;
    unsigned int            bfOffBits;

} bmp_file_header_t;

typedef struct __bmp_info_header
{
    unsigned int            biSize;
    int                     biWidth;
    int                     biHeight;
    unsigned short          biPlanes;
    unsigned short          biBitCount;
    unsigned int            biCompression;
    unsigned int            biSizeImage;
    int                     biXPelsPerMeter;
    int                     biYPelsPerMeter;
    unsigned int            biClrUsed;
    unsigned int            biClrImportant;

} bmp_info_header_t;

2.2 RGB 图像数据结构

  此处定义了两种图像像素数据结构:

  1. bmp_pixel_t: BMP 文件图像像素数据在内存中存在时的数据格式,注意 B G R 通道顺序,他一定是与我们架构中所使用的 BMP 图像格式类型保持一致的,但未必会与整体架构中的数据流格式保持一致;定义这么一层数据结构有利于和图像处理架构中的数据流对接。

  2. rgb8_pixel_t: 定义了一种在整个图像处理架构中通用的数据结构,采用较为普遍的 RGB888 格式;这样做是模拟整体架构中的数据流,我们需要将输入的 BMP 图像内容读取到该数据流中去,或是将数据流中的数据写到 BMP 文件中去。

  注:大部分情况下这两个数据格式可能是不同的,此处写成类似的形式,只是便于演示。

typedef struct __bmp_pixel
{
    unsigned char           blue;
    unsigned char           green;
    unsigned char           red;

} bmp_pixel_t;

typedef struct __rgb8_pixel
{
    unsigned char           red;
    unsigned char           green;
    unsigned char           blue;

} rgb8_pixel_t;

2.3 BMP 图像在内存中的数据结构

  一个 BMP 图像文件数据包含了 位图文件头文件信息头图像像素数据 三部分;此处图像像素数据用一个二维指针去表示,其本质上就是一个二维数组,相比内存中常见的一维数组可读性高,处理方便。

typedef struct __bmp_img
{
    bmp_file_header_t       img_file_header;
    bmp_info_header_t       img_info_header;
    bmp_pixel_t           **img_pixels;

} bmp_img_t;

3. 处理函数

3.1 BMP 图像内存管理函数

  1. bmp_img_size:获取 bmp 文件尺寸,便于在分配内存或创建文件时知道需要的空间大小;
  2. bmp_img_pixels_alloc:根据图像的尺寸,来分配 RGB 数据所需占用的内存空间;
  3. bmp_img_pixels_free:根据图像尺寸,释放 RGB 数据所占用的内存空间;
  4. bmp_init:根据图像的尺寸,对传入的 bmp_img_t 空指针进行初始化,分配内存;
  5. bmp_free:释放 bmp_img_t 指针指向的 bmp 图片数据内存;
/// Acquire bmp image file size
unsigned int
bmp_img_size(bmp_img_t *img)
{
    return img->img_file_header.bfSize;
}

/// Allocate memory for bmp image pixels
void
bmp_img_pixels_alloc(bmp_img_t *img,
                     const int  width,
                     const int  height)
{
    img->img_pixels = malloc(sizeof(bmp_pixel_t *) * height);
    memset(img->img_pixels, 0, sizeof(bmp_pixel_t *) * height);

    for (size_t j = 0; j < height; j++)
    {
        img->img_pixels[j] = malloc(sizeof(bmp_pixel_t) * width);
        memset(img->img_pixels[j], 0, sizeof(bmp_pixel_t) * width);
    }
}

/// Free memory of bmp image pixels
void
bmp_img_pixels_free(bmp_img_t *img)
{
    const size_t height = abs(img->img_info_header.biHeight);

    for (size_t y = 0; y < height; y++)
    {
        free(img->img_pixels[y]);
    }
    free(img->img_pixels);
}

/// Initialize bmp image struct
void
bmp_init(bmp_img_t **img,
         const int   width,
         const int   height)
{
    *img = malloc(sizeof(bmp_img_t));

    memset(*img, 0, sizeof(bmp_img_t));
    (*img)->img_info_header.biWidth = width;
    (*img)->img_info_header.biHeight = height;

    bmp_img_pixels_alloc(*img, width, height);
}

/// Free bmp image struct
void
bmp_free(bmp_img_t **img)
{
    bmp_img_pixels_free(*img);
    free(*img);
    *img = NULL;
}

3.2 BMP 图像文件头读写函数

  1. bmp_header_init_df:对 bmp 图像的文件头进行默认初始化(位深 24 bit,无调色板,不压缩);
  2. bmp_header_read_buf:从内存中的 bmp 图像数据中解码出 bmp header 信息;
  3. bmp_header_write_buf:将 bmp header 信息编码后写入指定的 bmp 图像数据内存地址;

注:为避免图像库直接操作文件 IO,库中所有的编解码对象都是一块指定内存,而非文件。

/// Set bmp image header to default value
void
bmp_header_init_df(bmp_img_t   *img)
{
    bmp_file_header_t  *file_header = &img->img_file_header;
    bmp_info_header_t  *info_header = &img->img_info_header;
    const int           width       = info_header->biWidth;
    const int           height      = info_header->biHeight;

    /// Set default bmp file header
    file_header->bfType = BMP_MAGIC;
    file_header->bfSize = (sizeof(bmp_pixel_t) * abs(width) + GET_PADDING_BYTES(abs(width)))
                               * abs(height) + BMP_FILE_HEADER_SIZE + BMP_DIB_HEADER_SIZE;
    file_header->bfReserved1 = 0;
    file_header->bfReserved2 = 0;
    file_header->bfOffBits   = BMP_FILE_HEADER_SIZE + BMP_DIB_HEADER_SIZE;

    /// Set default bmp info header
    info_header->biSize             = BMP_DIB_HEADER_SIZE;
    info_header->biPlanes           = 1;
    info_header->biBitCount         = 24;
    info_header->biCompression      = 0;
    info_header->biSizeImage        = 0;
    info_header->biXPelsPerMeter    = 0;
    info_header->biYPelsPerMeter    = 0;
    info_header->biClrUsed          = 0;
    info_header->biClrImportant     = 0;
}

/// Read bmp header from buffer
bmp_ret_t
bmp_header_read_buf(bmp_file_header_t   *file_header,
                	bmp_info_header_t   *info_header,
                	const unsigned char *buf)
{
    bmp_ret_t res = BMP_OK;

    if (NULL == file_header || NULL == info_header || NULL == buf)
    {
        res = BMP_BUFFER_EMPTY;
    }
    else
    {
        memcpy(&file_header->bfType, (void *)((char *)buf + 0), 2);
        memcpy(&file_header->bfSize, (void *)((char *)buf + 2), 4);
        memcpy(&file_header->bfReserved1, (void *)((char *)buf + 6), 2);
        memcpy(&file_header->bfReserved2, (void *)((char *)buf + 8), 2);
        memcpy(&file_header->bfOffBits, (void *)((char *)buf + 10), 4);
        memcpy(info_header, (void *)((char *)buf + 14), 40);
    }

    return res;
}

/// Write bmp header to buffer
bmp_ret_t
bmp_header_write_buf(bmp_file_header_t  *file_header,
                     bmp_info_header_t  *info_header,
                     unsigned char      *buf)
{
    bmp_ret_t res = BMP_OK;

    if (NULL == file_header || NULL == info_header || NULL == buf)
    {
        res = BMP_BUFFER_EMPTY;
    }
    else
    {
        memcpy((void *)((char *)buf + 0), &file_header->bfType, 2);
        memcpy((void *)((char *)buf + 2), &file_header->bfSize, 4);
        memcpy((void *)((char *)buf + 6), &file_header->bfReserved1, 2);
        memcpy((void *)((char *)buf + 8), &file_header->bfReserved2, 2);
        memcpy((void *)((char *)buf + 10), &file_header->bfOffBits, 4);
        memcpy(buf + BMP_FILE_HEADER_SIZE, info_header, BMP_DIB_HEADER_SIZE);
    }

    return res;
}

3.3 BMP 图像数据读写函数

  1. bmp_image_export:将 bmp 数据结构中的像素 RGB 数据转化为 c-model 数据流所使用的一维数组;
  2. bmp_image_import:将 c-model 数据流所使用的一维数组转化为 bmp 数据结构中的像素 RGB 数据;

注:这两个函数是提供 bmp 数据结构中的像素数据与 c-model 数据流转换的接口。

/// Export bmp image pixels data to RGB888 struct
bmp_ret_t
bmp_image_export(bmp_img_t	  *img,
                 rgb8_pixel_t *pRgb8)
{
    bmp_ret_t res = BMP_OK;

    if (NULL == img || NULL == pRgb8)
    {
        res = BMP_BUFFER_EMPTY;
    }
    else
    {
        const size_t width  = abs(img->img_info_header.biWidth);
        const size_t height = abs(img->img_info_header.biHeight);

        for (size_t row = 0; row < height; row++)
        {
            for (size_t col = 0; col < width; col++)
            {
                pRgb8[row * width + col].blue   = img->img_pixels[row][col].blue;
                pRgb8[row * width + col].green  = img->img_pixels[row][col].green;
                pRgb8[row * width + col].red    = img->img_pixels[row][col].red;
            }
        }
    }

    return res;
}

/// Import bmp image pixels data from RGB888 struct
bmp_ret_t
bmp_image_import(bmp_img_t    *img,
                 rgb8_pixel_t *pRgb8)
{
    bmp_ret_t res = BMP_OK;

    if (NULL == img || NULL == pRgb8)
    {
        res = BMP_BUFFER_EMPTY;
    }
    else
    {
        const size_t width  = abs(img->img_info_header.biWidth);
        const size_t height = abs(img->img_info_header.biHeight);

        for (size_t row = 0; row < height; row++)
        {
            for (size_t col = 0; col < width; col++)
            {
                img->img_pixels[row][col].blue  = pRgb8[row * width + col].blue;
                img->img_pixels[row][col].green = pRgb8[row * width + col].green;
                img->img_pixels[row][col].red   = pRgb8[row * width + col].red;
            }
        }
    }

    return res;
}

3.4 BMP 文件读写函数

  1. bmp_read_buffer:从内存中读取 bmp 图像数据,并转化为我们设计的 bmp_img_t 数据结构;
  2. bmp_write_buffer:将 bmp_img_t 数据结构中的内容编码成连续的数据写进内存中去;

注:为避免图像库直接操作文件 IO,库中所有的编解码对象都是一块指定内存,而非文件。

/// Read bmp image from buffer
bmp_ret_t
bmp_read_buffer(bmp_img_t            *img,
                const unsigned char  *buf)
{
    bmp_ret_t res = BMP_OK;

    if (NULL == img || NULL == buf)
    {
        res = BMP_BUFFER_EMPTY;
    }

    /// 1. Read bmp header
    res = bmp_header_read_buf(&img->img_file_header, &img->img_info_header, (void *)buf);

    /// 2. Read image pixels
    if (BMP_OK == res)
    {
        const size_t offset     = img->img_file_header.bfOffBits;
        const size_t width      = abs(img->img_info_header.biWidth);
        const size_t height     = abs(img->img_info_header.biHeight);
        const size_t padding    = GET_PADDING_BYTES(width);

        for (size_t y = 0; y < height; y++)
        {
            size_t row = img->img_info_header.biHeight > 0 ? height - y - 1 : y;
            for (size_t x = 0; x < width; x++)
            {
                size_t col = img->img_info_header.biWidth > 0 ? x : width - x - 1;
                img->img_pixels[y][x].blue
                    = buf[offset + row * (width * 3 + padding) + col * 3 + 0];
                img->img_pixels[y][x].green
                    = buf[offset + row * (width * 3 + padding) + col * 3 + 1];
                img->img_pixels[y][x].red
                    = buf[offset + row * (width * 3 + padding) + col * 3 + 2];
            }
        }
    }

    return res;
}

/// Write bmp image to buffer
bmp_ret_t
bmp_write_buffer(bmp_img_t     *img,
                 unsigned char *buf)
{
    bmp_ret_t res = BMP_OK;

    if (NULL == img || NULL == buf)
    {
        res = BMP_BUFFER_EMPTY;
    }

    /// 1. Initialize target buffer
    size_t file_size = img->img_file_header.bfSize;
    memset(buf, 0, file_size);

    /// 2. Write bmp header
    res = bmp_header_write_buf(&img->img_file_header, &img->img_info_header, buf);

    /// 3. Write image pixels
    if (BMP_OK == res)
    {
        const size_t offset     = img->img_file_header.bfOffBits;
        const size_t width      = abs(img->img_info_header.biWidth);
        const size_t height     = abs(img->img_info_header.biHeight);
        const size_t padding    = GET_PADDING_BYTES(width);

        for (size_t y = 0; y < height; y++)
        {
            size_t row = img->img_info_header.biHeight > 0 ? height - y - 1 : y;
            for (size_t x = 0; x < width; x++)
            {
                size_t col = img->img_info_header.biWidth > 0 ? x : width - x - 1;

                buf[offset + row * (width * 3 + padding) + col * 3 + 0]
                    = img->img_pixels[y][x].blue;
                buf[offset + row * (width * 3 + padding) + col * 3 + 1]
                    = img->img_pixels[y][x].green;
                buf[offset + row * (width * 3 + padding) + col * 3 + 2]
                    = img->img_pixels[y][x].red;
            }
        }
    }

    return res;
}

4. 测试代码

4.1 BMP 文件读取测试

这段测试代码允许用户传入一张 bmp 图片,程序将图片数据解码完成后,会打印出这张 bmp 图片的所有信息。

#include <stdio.h>
#include <stdlib.h>
#include "libbmp.h"

#ifndef DATA_SET_DIR
#define DATA_SET_DIR    "./"
#endif

#ifndef OUTPUT_DIR
#define OUTPUT_DIR      "./"
#endif

void print_bmp_header(bmp_img_t *img)
{
    printf("BMP file header:\n");
    printf("Type: %#x\n", img->img_file_header.bfType);
    printf("Size: %u\n", img->img_file_header.bfSize);
    printf("Reserved1: %#x\n", img->img_file_header.bfReserved1);
    printf("Reserved2: %#x\n", img->img_file_header.bfReserved2);
    printf("Offset: %u\n", img->img_file_header.bfOffBits);

    printf("BMP info header:\n");
    printf("Size: %u\n", img->img_info_header.biSize);
    printf("Width: %d\n", img->img_info_header.biWidth);
    printf("Height: %d\n", img->img_info_header.biHeight);
    printf("Planes: %#x\n", img->img_info_header.biPlanes);
    printf("Bits: %#x\n", img->img_info_header.biBitCount);
    printf("Compression: %u\n", img->img_info_header.biCompression);
    printf("Image size: %u\n", img->img_info_header.biSizeImage);
    printf("X pixels per meter: %d\n", img->img_info_header.biXPelsPerMeter);
    printf("Y pixels per meter: %d\n", img->img_info_header.biYPelsPerMeter);
    printf("Colors used: %u\n", img->img_info_header.biClrUsed);
    printf("Colors important: %u\n", img->img_info_header.biClrImportant);
}

int main(int argc, char *argv[])
{
    char            input_filename[511];
    FILE           *input_file;
    unsigned char  *input_buf;

    if (argc != 2)
    {
        printf("\nUsage: %s <input_file>\n\n", argv[0]);
        return 1;
    }

    sprintf(input_filename, "%s/%s", DATA_SET_DIR, argv[1]);
    input_file = fopen(input_filename, "rb");
    if (input_file == NULL)
    {
        printf("Error: could not open %s\n", input_filename);
        return 1;
    }

    /// Acquire file size
    fseek(input_file, 0, SEEK_END);
    int fileSize = ftell(input_file);
    fseek(input_file, 0, SEEK_SET);
    input_buf = (unsigned char *)malloc(fileSize);
    fread(input_buf, fileSize, 1, input_file);
    fclose(input_file);

    /// Read bmp image from buffer
    bmp_ret_t       res     = BMP_OK;
    int             width   = 512;
    int             height  = 512;
    bmp_img_t      *pImage;

    bmp_init(&pImage, width, height);
    res = bmp_read_buffer(pImage, input_buf);
    if (res != BMP_OK)
    {
        printf("Error: read bmp fail! res = %d.\n", res);
        bmp_free(&pImage);
        free(input_buf);
        return 1;
    }

    print_bmp_header(pImage);

    bmp_free(&pImage);
    free(input_buf);

    return 0;
}

测试输出结果:

root@DESKTOP-GQBECKU:/project/gitee/lib_bmp/build# ./out/image_access  Lena.bmp
BMP file header:
Type: 0x4d42
Size: 786486
Reserved1: 0
Reserved2: 0
Offset: 54
BMP info header:
Size: 40
Width: 512
Height: 512
Planes: 0x1
Bits: 0x18
Compression: 0
Image size: 786432
X pixels per meter: 0
Y pixels per meter: 0
Colors used: 0
Colors important: 0

4.2 BMP 文件写入测试

这段代码会输出一张 512 x 512 的 bmp 图片,图片内容为 8 x 8 的黑白棋盘格。

#include <stdio.h>
#include <stdlib.h>
#include "libbmp.h"

#ifndef DATA_SET_DIR
#define DATA_SET_DIR    "./"
#endif

#ifndef OUTPUT_DIR
#define OUTPUT_DIR      "./"
#endif

int main(int argc, char *argv[])
{
    char            output_filename[511];
    FILE           *output_file;
    unsigned char  *output_buf;

    bmp_img_t  *img;
    int width   = 512;
    int height  = 512;

    bmp_init(&img, width, height);
    bmp_header_init_df(img);

    /// Draw a checkerboard pattern
    rgb8_pixel_t   *pRgb8 = malloc(sizeof(rgb8_pixel_t) * width * height);

    for (size_t y = 0; y < height; y++)
    {
        for (size_t x = 0; x < width; x++)
        {
            if ((y % 128 < 64 && x % 128 < 64) ||
                (y % 128 >= 64 && x % 128 >= 64))
            {
                pRgb8[y * width + x].red    = 250;
                pRgb8[y * width + x].green  = 250;
                pRgb8[y * width + x].blue   = 250;
            }
            else
            {
                pRgb8[y * width + x].red    = 0;
                pRgb8[y * width + x].green  = 0;
                pRgb8[y * width + x].blue   = 0;
            }
        }
    }

    bmp_image_import(img, pRgb8);
    output_buf = (unsigned char *)malloc(bmp_img_size(img));

    bmp_write_buffer(img, output_buf);

    sprintf(output_filename, "%s/%s", OUTPUT_DIR, "checkerboard.bmp");
    output_file = fopen(output_filename, "wb");
    fwrite(output_buf, bmp_img_size(img), 1, output_file);
    fclose(output_file);
    free(pRgb8);
    bmp_free(&img);

    return 0;
}

测试输出结果:
checkerboard.bmp

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
《大鱼吃小鱼》是一款简单而有趣的游戏,适合初学者学习Java游戏开发。下面我将手把手教你如何用Java开发这款游戏,并提供相应的源码。 步骤一:创建游戏窗口和游戏主类 首先,我们需要创建游戏的窗口,可以使用Java提供的Swing框架来实现。创建一个GameWindow类,继承JFrame类,并在构造方法中设置窗口的基本属性。 步骤二:添加游戏画布 在GameWindow类中,创建一个GameCanvas类,继承JPanel类,并重写paintComponent()方法,在此方法中实现游戏场景的绘制,包括大鱼、小鱼和其他游戏元素。 步骤:添加游戏逻辑 在GameWindow类中,添加游戏逻辑的处理方法,包括大鱼的移动、小鱼的生成和碰撞检测等。 步骤四:添加游戏控制 在GameWindow类中,添加游戏控制的方法,包括键盘事件的处理和游戏状态的切换等。 步骤五:运行游戏 在GameWindow类中,添加一个main()方法,创建游戏窗口对象,并启动游戏循环。 以上是《大鱼吃小鱼》游戏的基本开发步骤,下面提供相应的源码供参考: ```java import javax.swing.*; import java.awt.*; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; public class GameWindow extends JFrame implements KeyListener{ private GameCanvas gameCanvas; private boolean isRunning; public GameWindow(){ super("大鱼吃小鱼"); setSize(800, 600); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); addKeyListener(this); gameCanvas = new GameCanvas(); add(gameCanvas, BorderLayout.CENTER); isRunning = true; } public void start(){ while(isRunning){ update(); gameCanvas.repaint(); try{ Thread.sleep(10); }catch(InterruptedException e){ e.printStackTrace(); } } } public void update(){ // 游戏逻辑更新 } public void keyPressed(KeyEvent e){ // 键盘按下事件处理 } public void keyReleased(KeyEvent e){ // 键盘释放事件处理 } public void keyTyped(KeyEvent e){ // 键盘输入事件处理 } public static void main(String[] args){ GameWindow gameWindow = new GameWindow(); gameWindow.setVisible(true); gameWindow.start(); } } class GameCanvas extends JPanel{ protected void paintComponent(Graphics g){ super.paintComponent(g); // 游戏场景绘制 } } ``` 通过以上源码和步骤,我们可以实现《大鱼吃小鱼》游戏的基本开发,通过添加更多的游戏逻辑和个性化设计,可以进一步完善这款游戏。希望对您有所帮助!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值