You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

BMP文件读写异常:像素写入错误致图像显示异常

BMP文件读写异常排查与修复

问题现象

使用C语言实现BMP读写时,读入自定义结构体后重新写出的图像呈现全黑状态,且列出现重复压缩现象;文件头部数据正常,但像素写入错误,对比十六进制文件发现头部之后的数据存在差异且长度明显更短。

用户代码实现

结构体定义

typedef uint16_t ImageType;

typedef struct
{
    uint32_t size;
    uint16_t additionalFeature;
    uint16_t copy;
    uint32_t offset;

} ImageHeader;

typedef struct
{
    uint32_t headerSize;
    int width;
    int height;
    uint16_t colorSpaces;
    uint16_t bitsPerPixel;
    uint32_t compression;
    uint32_t size;
    int verticalResolution;
    int horizontalResolution;
    uint32_t totalColors;
    uint32_t importantColors;
} ImageMetadata;

typedef struct {
    uint8_t blue;
    uint8_t green;
    uint8_t red;
} ImageColors;

typedef struct {
    ImageType type;
    ImageHeader header;
    ImageMetadata metadata;
    ImageColors **pixels;
} Image;

头文件引用

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdbool.h>

像素读取函数

void readImage(char *filename, Image *image)
{
    FILE *imageFile = fopen(filename, "rb");
    if (!imageFile)
    {
        perror("ReadImageFileException");
        fclose(imageFile);
        exit(EXIT_FAILURE);
    }

    fread(&(image->type), sizeof(ImageType), 1, imageFile);
    //validate for correct image type
    if (image->type != BMP_IMAGE_TYPE)
    {
        fprintf(stderr, "%hu", image->type);
        perror("ReadImageTypeException");
        fclose(imageFile);
        exit(EXIT_FAILURE);
    }

    fread(&(image->header), sizeof(ImageHeader), 1, imageFile);
    fread(&(image->metadata), sizeof(ImageMetadata), 1, imageFile);
    
    // Allocate space for the pixels.
    image->pixels = malloc( image->metadata.height * sizeof(ImageColors *) );
    for(int i = 0; i < image->metadata.height; i++){
        image->pixels[i] = malloc(image->metadata.width * sizeof(ImageColors));
    }
    
    // Read in each pixel
    for (int i = 0; i < image->metadata.height; i++){
        for(int j = 0; j < image->metadata.width; j++){
            ImageColors px;
            if( fread(&px, sizeof(ImageColors), 1, imageFile) < 1 ) {
                printf("Error while reading bmp pixel.\n");
                return;
            }

            image->pixels[i][j] = px;
        }
    }

    fclose(imageFile);
}

像素写入函数

void writeImage(char *filename, Image *image)
{
    FILE *imageFile = fopen(filename, "wb+");
    if (!imageFile)
    {
        perror("WriteImageFileException");
        fclose(imageFile);
        exit(EXIT_FAILURE);
    }

    size_t typeWritten = fwrite(&(image->type), sizeof(image->type), 1, imageFile);
    if (typeWritten == 0)
    {
        perror("WriteImageTypeException");
        fclose(imageFile);
        exit(EXIT_FAILURE);
    }

    size_t headerWritten = fwrite(&(image->header), sizeof(image->header), 1, imageFile);
    if (headerWritten == 0)
    {
        perror("WriteImageHeaderException");
        fclose(imageFile);
        exit(EXIT_FAILURE);
    }

    size_t metadataWritten = fwrite(&(image->metadata), sizeof(image->metadata), 1, imageFile);
    if (metadataWritten == 0)
    {
        perror("WriteImageMetadataException");
        fclose(imageFile);
        exit(EXIT_FAILURE);
    }

    fseek(imageFile, image->header.offset, SEEK_SET);
    // Read in each pixel
    for (int i = 0; i < image->metadata.height; i++){
        for(int j = 0; j < image->metadata.width; j++){
            if( fwrite(&image->pixels[i][j], sizeof(ImageColors), 1, imageFile) < 1 ) {
                printf("Error while wr bmp pixel.\n");
                fclose(imageFile);
                exit(EXIT_FAILURE);
            }

        }
    }

    fclose(imageFile);
}

main函数

int main(int argc, char **argv) {
    int ch;
    char *inputFile = "sample.bmp";
    char *outputFile = "image-copy.bmp";
    Image image;
    
    // try to use port and password from parameter
    while ( (ch = getopt_long_only(argc, argv, "", long_options, NULL)) != -1 ) {
        switch (ch) {
            case 'i':
                inputFile = optarg;
                break;
            case 'o':
                outputFile = optarg;
                break;
            default:
                break;
        }
    }
    
    printf("input File: %s\n", inputFile);
    
    readImage(inputFile, &image);
    writeImage(outputFile, &image);
    return 0;
}

核心问题分析

  1. 结构体内存对齐偏差:编译器默认的内存对齐规则会在结构体成员间插入填充字节,导致自定义的ImageHeader、ImageMetadata与BMP文件的二进制结构不匹配,读写头部时数据错位,后续像素读取位置完全错误。
  2. 忽略BMP行对齐规则:BMP要求每行像素数据长度必须是4字节的倍数,不足时会填充空字节。当前代码直接按宽×高读取/写入像素,未处理这些填充字节,导致像素数据读取不完整、写入时缺失填充,最终图像数据错位。
  3. 文件指针定位错误:写入函数中fseek(imageFile, image->header.offset, SEEK_SET)属于多余操作,写完头部后文件指针已处于正确的像素起始位置,手动seek会导致像素写入位置偏移。
  4. 内存泄漏风险:读取函数中分配的二维像素数组未释放,属于潜在问题。

修复方案

1. 强制结构体1字节对齐

在结构体定义前后添加对齐指令,确保内存布局与BMP文件二进制结构完全一致:

#pragma pack(push, 1)
// 此处放置所有结构体定义
typedef uint16_t ImageType;
typedef struct {...} ImageHeader;
typedef struct {...} ImageMetadata;
typedef struct {...} ImageColors;
typedef struct {...} Image;
#pragma pack(pop)

2. 处理BMP行对齐填充

计算每行实际占用字节数(含填充),读写时跳过/写入填充字节:

修改读取函数的像素读取部分

// 计算每行实际字节数与填充字节数
int row_stride = (image->metadata.width * 3 + 3) / 4 * 4;
int padding = row_stride - image->metadata.width * 3;

for (int i = 0; i < image->metadata.height; i++){
    for(int j = 0; j < image->metadata.width; j++){
        ImageColors px;
        if( fread(&px, sizeof(ImageColors), 1, imageFile) < 1 ) {
            printf("Error while reading bmp pixel.\n");
            return;
        }
        image->pixels[i][j] = px;
    }
    // 跳过行尾填充字节
    fseek(imageFile, padding, SEEK_CUR);
}

修改写入函数的像素写入部分

// 计算每行实际字节数与填充字节数
int row_stride = (image->metadata.width * 3 + 3) / 4 * 4;
int padding = row_stride - image->metadata.width * 3;

// 移除多余的fseek操作
// fseek(imageFile, image->header.offset, SEEK_SET);

for (int i = 0; i < image->metadata.height; i++){
    for(int j = 0; j < image->metadata.width; j++){
        if( fwrite(&image->pixels[i][j], sizeof(ImageColors), 1, imageFile) < 1 ) {
            printf("Error while writing bmp pixel.\n");
            fclose(imageFile);
            exit(EXIT_FAILURE);
        }
    }
    // 写入行尾填充字节
    uint8_t pad[3] = {0};
    fwrite(pad, 1, padding, imageFile);
}

3. 补充内存释放(可选但推荐)

在main函数写完图像后添加内存释放代码,避免内存泄漏:

// 释放像素内存
for(int i = 0; i < image.metadata.height; i++){
    free(image.pixels[i]);
}
free(image.pixels);

验证修改

修改后重新编译运行,对比输入输出文件的十六进制数据,头部和像素数据应完全一致(若需要精准匹配文件大小,可同步更新image.header.size为实际生成的文件大小)。

内容的提问来源于stack exchange,提问作者Gonzo Gonzales

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 14:51:04