
简介创建位图并生成BMP文件的完整示例工程面向需要掌握图像文件格式与底层写入逻辑的开发者尤其适合C/MFC初学者理解BMP文件头、DIB头、像素阵列、颜色深度与行填充等关键概念。压缩包共32个文件以Visual C 6.0工程为核心既包含头文件.h、源文件.cpp、对话框资源.rc/ico等可读代码也有编译生成的exe可执行程序obj、pdb等调试中间文件则方便直接对照二进制输出进行学习。代码演示了从构造文件头、计算像素数据偏移、按自下而上逐行写入RGB像素到补齐行填充字节并最终生成BMP的完整流程同时涉及8位、24位、32位颜色深度的差异。整体包体约1.81MB已有1122人学习。通过运行示例并跟踪源码可快速掌握BMP文件生成规范为后续图像处理、格式转换或图像分析工具开发打下扎实基础。 最近有个做嵌入式的朋友问我给一个小屏幕做开机画面手里没有现成的图片处理库能不能自己拼一个BMP文件出来我的回答是不仅能而且我强烈建议每个写代码的人都亲手生成一次位图。这个“创建位图并生成BMP文件”的小任务看起来只是写几个字节的事但它能把图片文件的底层约定、内存布局、字节序这些知识一次性串起来。搞懂它之后你再去碰PNG、JPEG甚至视频编码都会觉得地基稳了很多。这篇文章我打算用Python和C两种方式从零生成一张合法的24位BMP图片。不调图像库、不套框架纯手写文件字节然后验证、排错、扩展。适合刚接触图像处理、想做底层原理补课或者需要在资源受限环境下手动生成图片的朋友。我不保证这是最快的写法但保证每一步都讲清楚为什么。1. 这个任务到底在解决什么问题位图和BMP的关系1.1 位图不是“一张图”而是一种数据约定很多新手一听“位图”下意识觉得它就是一张照片或一张插画这其实把概念用窄了。位图bitmap本质上是一套“把图像拆成像素点、每个像素点用数字记录颜色”的数据组织方式。你手机拍的照片、电脑截图、网上看的图片绝大多数都是位图只是它们通常被压缩编码成了JPEG、PNG等格式而不是直接暴露原始像素。BMP就是位图最朴素、最“不设防”的载体。它几乎没有有损压缩像素是什么颜色文件里就存什么颜色。Windows画图保存的.bmp、老式游戏里的素材图很多就是这个格式。也正因为结构简单BMP特别适合用来学习“图片文件到底是怎么被组织出来的”。1.2 为什么还要自己动手生成一次BMP你可能会说OpenCV一行代码就能存图Python的PIL也可以为什么非要手写我的原因有三个。第一调库的时候你不清楚图片文件头里那几十个字节是干什么的一旦遇到“图片打不开”“颜色全不对”“图像上下颠倒”这类问题排查起来全靠猜。第二在单片机、嵌入式、裸机环境里很多场景根本没有图像库可用你要给LCD屏写驱动、生成开机logo就不得不自己拼BMP。第三BMP是理解其他图片格式的跳板它把“文件头-信息头-像素数据”这个三段式结构演绎得特别标准你把这个结构刻进脑子往后看PNG的chunk、JPEG的segment都会觉得似曾相识。所以我建议你哪怕只用一次也要亲手生成一张BMP。这个半小时换来的底层认知后面能帮你省下乱七八糟的排查时间。2. 动手前必须搞懂的BMP文件结构2.1 文件头和信息头前54个字节的约定一个最经典的24位BMP文件布局非常固定文件头BITMAPFILEHEADER14字节信息头BITMAPINFOHEADER40字节然后是像素数据。加起来就是很多人常说的“BMP前54字节是头信息”。14字节文件头里核心是这几项第0-1字节固定为0x42 0x4D也就是ASCII字符“BM”用来标识这是一个BMP文件。看图软件打开文件后第一件事就是检查这两字节不对就直接拒绝。第2-5字节整个文件的大小小端存储。小端的意思是低字节在前比如文件大小是5000字节那么字节序就是0x88 0x13 0x00 0x00。第10-13字节像素数据从文件第几个字节开始。无压缩、无调色板的24位BMP这个值固定是54。40字节信息头里最不能写错的是这几项第14-17字节信息头长度固定为40。第18-21字节图像宽度单位是像素。第22-25字节图像高度。这里有个很坑的设计——正数表示自底向上存储负数表示自顶向下存储后面细说。第26-27字节颜色平面数必须为1。第28-29字节每个像素的位数24位真彩色就是24。第30-33字节压缩方式无压缩填0。第34-37字节像素数据大小。2.2 像素数据BGR顺序、自底向上、行对齐一个都不能错24位BMP的每个像素用3字节表示但顺序不是RGB而是BGR。也就是说一个纯红色像素在文件里存的是0x00 0x00 0xFF蓝色是0xFF 0x00 0x00。我第一次写BMP的时候就在这里栽过跟头图片生成出来后红色和蓝色完全对调。第二个容易踩的坑是行对齐。BMP规定每行像素数据的字节数必须是4的倍数。如果宽度×3不是4的倍数就要在行尾补0。比如宽度是21像素21×363字节不是4的倍数最近的是64字节所以每行末尾要补1字节。这个对齐规则来自早期32位系统按4字节块读写数据的习惯放到现在纯粹是历史包袱但你不遵守就是打不开。第三个坑是像素顺序。高度为正时文件里先存的是图像最下面一行再往上存。你可以理解为BMP的行顺序是从底到顶。很多新手把像素按从上到下顺序写入图片生成后就是倒过来的。2.3 文件大小的计算公式与手算示例文件总大小 54字节文件信息 每行字节数 × 高度。其中每行字节数 ceil(宽度 × 3 / 4) × 4这个ceil是向上取整。举个例子生成一张宽200、高100的24位BMP。每行像素原始数据是200×3600字节600刚好是4的倍数不需要补齐。像素数据总大小就是600×10060000字节文件大小就是546000060054字节。如果改成宽201像素201×3603字节不是4的倍数向上取整后是604字节所以每行要补1字节像素数据区就是604×10060400字节文件总大小为60454字节。你看只差1个像素文件大小就多了400字节。注意这个公式只针对24位、无压缩、无调色板的标准BMP。8位、16位、32位BMP的情况略有不同但思路一样。3. 从零生成一张BMP完整代码与实操步骤3.1 Python版快速实现20行代码生成合法图片先上最简单的Python实现。我建议你用Python先跑通全流程理解清楚之后再去看C版本。import struct width, height 200, 100 row_size ((width * 3 3) // 4) * 4 # 每行字节数4字节对齐 data_size row_size * height file_size 54 data_size with open(output.bmp, wb) as f: # BITMAPFILEHEADER14字节 f.write(bBM) f.write(struct.pack(IHHI, file_size, 0, 0, 54)) # BITMAPINFOHEADER40字节 f.write(struct.pack(IiiHHIIiiII, 40, width, height, 1, 24, 0, data_size, 2835, 2835, 0, 0)) # 像素数据自底向上写入 for y in range(height - 1, -1, -1): row bytearray(row_size) for x in range(width): blue int(255 * x / width) # 从左到右蓝色渐变 green int(255 * y / height) # 从下到上绿色渐变 red 128 # 红色固定为中间值 row[x * 3] blue row[x * 3 1] green row[x * 3 2] red f.write(row)这里struct.pack(IHHI, ...)表示按小端格式打包4个字段I对应文件大小HH对应两个保留字段I对应像素数据偏移54。信息头那一行里的IiiHHIIiiII依次对应宽度、高度、平面数、位数等顺序必须和BMP规范一致写错一个整个文件就废了。这段代码生成的BMP左边偏蓝、右边偏暗、下半部分偏绿、上半部分偏深红颜色渐变能非常直观地验证BGR顺序和自底向上规则。3.2 C语言版底层拆解不靠库也能把字节写对Python写起来方便但如果你想彻底看清字节是怎么落到文件里的我建议再写一遍C语言版。核心思路是定义两个紧凑结构体然后直接往文件里写。#include stdio.h #include stdint.h #pragma pack(push, 1) typedef struct { uint16_t bfType; // 固定 0x4D42即 BM uint32_t bfSize; // 文件总大小 uint16_t bfReserved1; uint16_t bfReserved2; uint32_t bfOffBits; // 像素数据偏移固定54 } BITMAPFILEHEADER; typedef struct { uint32_t biSize; // 本结构体大小固定40 int32_t biWidth; int32_t biHeight; uint16_t biPlanes; uint16_t biBitCount; uint32_t biCompression; uint32_t biSizeImage; int32_t biXPelsPerMeter; int32_t biYPelsPerMeter; uint32_t biClrUsed; uint32_t biClrImportant; } BITMAPINFOHEADER; #pragma pack(pop) int main() { int width 200, height 100; int row_size ((width * 3 3) / 4) * 4; int data_size row_size * height; FILE *fp fopen(output_c.bmp, wb); if (!fp) return 1; BITMAPFILEHEADER fh {0x4D42, 54 data_size, 0, 0, 54}; BITMAPINFOHEADER ih {40, width, height, 1, 24, 0, data_size, 2835, 2835, 0, 0}; fwrite(fh, sizeof(fh), 1, fp); fwrite(ih, sizeof(ih), 1, fp); for (int y height - 1; y 0; y--) { unsigned char *row calloc(1, row_size); for (int x 0; x width; x) { row[x * 3] (unsigned char)(255 * x / width); row[x * 3 1] (unsigned char)(255 * y / height); row[x * 3 2] 128; } fwrite(row, 1, row_size, fp); free(row); } fclose(fp); return 0; }#pragma pack(push, 1)是必须的它告诉编译器不要对结构体做内存对齐。如果不加BITMAPFILEHEADER里的两个uint16_t后面很可能被填充出多余字节导致文件头长度不是14字节生成的BMP直接打不开。这个坑非常隐蔽我当年排查了半小时才意识到。3.3 生成之后的验证方法用Hexdump和看图工具双重确认代码跑完之后先别急着双击打开。先用十六进制工具看一眼文件开头确认文件头没写错。命令很简单hexdump -C output.bmp | head -n 5正常输出长这样00000000 42 4d 36 ea 00 00 00 00 00 00 36 00 00 00 28 00 |BM6.......6...(.| 00000010 00 00 c8 00 00 00 64 00 00 00 01 00 18 00 00 00 |......d.........|我来带你读一下。开头两个字节是42 4d正是“BM”。第2-5字节是36 ea 00 00小端读出来是0x0000ea36也就是59958字节这正好是546000060054的十六进制表示。至于为什么命令里显示“36 ea 00 00”而不是我写的60054你可以自己算一下这个细节搞懂就说明小端学明白了。确认文件头之后再用看图软件打开或者用Python的PIL读一下尺寸和像素from PIL import Image img Image.open(output.bmp) print(img.size, img.mode) print(img.getpixel((0, 0))) # 左下角颜色 print(img.getpixel((199, 99))) # 右上角颜色这一步能确认像素区写入正确颜色渐变符合预期。4. 常见问题与排查技巧实录4.1 打不开、报格式错误先查这3个字节如果生成的BMP双击报错第一时间用Hexdump看文件开头。我最常遇到的问题是第一个字节被写反了比如写成了4d 42而不是42 4d。原因通常是结构体没加紧凑对齐或者有人手误把0x4D42的字节序搞混。还有一次朋友写的是BM两个ASCII字符但后面用fwrite时写文件头的大小估计错了导致文件头整体偏移图片也打不开。排查思路就一条对照规范逐字节核对前54字节。别偷懒这个格式总共才54个字节从头对一遍比看十篇教程都管用。4.2 颜色不对、图像颠倒RGB顺序和高度符号的坑颜色不对基本就是RGB和BGR搞混了。把像素区第一组3个字节读出来如果原计划是红色实际显示成蓝色那基本可以断定写成了FF 00 00而不是00 00 FF调换顺序即可。图像上下颠倒的根因我之前提过BMP高度为正时要求自底向上存储像素行。如果你生成的是height正数但代码里用for y in range(height)正向写入那图片肯定倒过来了。解决办法有两种一种是把循环改成从height-1往下倒着写另一种更省事的做法是把高度写成负数负高度表示自顶向下存储很多图像库会按现代习惯读取。4.3 “检查卷位图时发现损坏”和图像位图不是一回事搜索“位图”相关话题时你可能会看到“检查卷位图时发现损坏”这种提示千万别和这里说的BMP搞混。文件系统里的“位图”是用于记录存储空间分配状态的另一种bitmap它不是一个图片文件而是磁盘上几十上百个连续的位每一位表示某个数据块是否被占用。如果它损坏了系统会提示磁盘检查但它跟你要生成的BMP图片没有任何关系。这个混淆非常常见因为英文都是bitmap。我在给别人讲BMP格式时至少有三次被追问“那磁盘的位图损坏怎么办”。这里统一澄清一下做图像开发时说的位图是像素点阵做存储开发时说的位图是分配表两者除了都用二进制位表达信息之外完全不是一个世界的东西。4.4 真实开发中容易忽略的边界情况首先是宽度或高度为0。BMP规范里宽度和高度不能为0但代码里如果没做校验生成的BMP文件大小会变成54字节打开肯定报错。我在做数据校验工具时就踩过这个坑。其次是异常大的尺寸。假设你要生成一张100000×100000的图按24位算像素数据需要约30GB但文件头里bfSize是uint32_t最大只能表示4GB-1字节直接溢出。真实场景里虽然很少生成这么大的图但做工具类程序时一定要考虑上限否则总有一天会被用户拿异常参数撞出bug。最后是补零问题。行尾补齐的字节必须是0不能是任意值。有的图像库对补零字节不敏感但读取时会把它当像素数据处理导致画面右侧出现杂色条纹。所以写像素赋值逻辑时最好先用calloc或者bytearray初始化整行再往固定位置填颜色。5. 从这里还能往哪走个人经验与扩展建议5.1 从BMP到其他图片格式的认知迁移BMP搞明白之后再看PNG格式会轻松很多。PNG本质上是把像素数据按行拆成压缩块每行用滤波方式预处理再套zlib压缩。核心概念还是“文件头-块结构-像素数据”只是增加了压缩层。JPEG更复杂一些但底层也逃不开颜色空间转换、分块变换、量化和熵编码这套东西。你手写过BMP后至少能理解“原始像素”和“编码后的文件”中间隔了多少层转换。5.2 手写像素画工具时的几点心得如果你继续往这个方向做我建议你扩展三个小功能按照色值生成纯色填充图、画水平渐变和垂直渐变、输出带文字或简单几何形状的图。这些功能看似基础但组合起来就是一个小型图片生成器。我做嵌入式开机logo的时候就是这么干的用脚本生成不同尺寸和色彩的BMP再批量转成C语言数组烧进屏幕驱动里整个过程完全脱离图像库运行起来特别可控。另外生成完BMP之后再研究一下“位图转矢量”的算法比如vector magic这类工具的实现思路会很好玩。BMP是纯像素数据而矢量图存的是路径和曲线从位图到矢量相当于从点阵反推几何轮廓。这已经是另一个方向了但起点正是你手写BMP时积累下来的像素读写能力。说实话BMP格式又老又繁琐它不像现代格式那样有很多酷炫的压缩算法可聊但它是那种“只要亲手做一次就再也不会忘”的底层基本功。你现在花一晚上搞懂这54个字节和BGR顺序之后不管是做图像处理、搞嵌入式显示还是写自己的小工具都会感谢今天这个决定的。本文还有配套的精品资源点击获取