ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

二维码(QRcode)生成算法:C语言/C++源码实现解析

二维码(QRcode)生成算法:C语言/C++源码实现解析 简介二维码生成算法C语言/C源码是一份面向C/C开发者的完整参考实现适合希望深入理解QRcode编码原理、数据码字与纠错码构造的读者。它完整覆盖二维码生成全流程先根据输入字符串自动识别编码模式再按字符串长度选择合适QRcode版本随后将编码转为二进制位流作为数据码字并通过多项式生成纠错码接着将数据码与纠错码排列到矩阵中加入定位符号、时序符号与格式信息最终在命令行直接输出二维码字符样式便于逐环节对照原理验证结果。压缩包内含8个文件以2个C源文件、1个头文件为核心附带Visual Studio工程文件sln/vcxproj及一个可直接运行的exe整体大小仅41KB结构清晰便于快速编译与调试。已有4489人学习适合嵌入式开发、图像处理及C/C算法入门者参考也可作为课程设计或二维码功能二次开发的基础工具。1. 二维码QRcode生成算法到底在生成什么火车站闸机前、外卖包装袋上、网银转账页面里二维码已经成了比条形码更常见的信息载体。你拿设备去扫零点几秒内对方返回一段文本。很多人第一次接触二维码时以为这是某种加密图片其实不是——QRcode 本质上是一套把二进制数据铺到二维平面上的编码规则。C语言/C源码里真正要解决的问题不是“画图”而是把字符串、字节流按规格切块、补位、纠错、掩码最后落成黑白矩阵。这就引出本篇文章的主题二维码QRcode生成算法 C语言/C源码。它适合两类人。一类是嵌入式或桌面端开发产品里要离线生成二维码不能依赖云服务或第三方库另一类是正在看条形码、QR码知识想把协议层弄懂而不是只调用一条qrcode_gen()就完事。QRcode 不是刚需技术里的顶流但凡是设备端、内网环境、自定义扫码业务它几乎是绕不开的硬骨头。一个容易误导新手的反直觉点QRcode 的图片尺寸和内容长短没有线性关系。短网址可能用 21×21 的 Version 1 就够一长串 JSON 塞进去可能直接跳到 57×57。这就是“版本”和“容量”的关系。把这块逻辑吃透比复制一段能跑的代码重要得多。下文从编码流程开始一路走完掩码、纠错、矩阵输出再给出一个可直接编译的 C 语言框架最后拆几个实际踩过的坑。2. QRcode 版本选择与数据编码先把比特流拼对生成一张二维码第一步不是画格子而是把你要放的文本变成一串比特bit。这一阶段决定了最终二维码的容量和密度。QRcode 的规格里定义了 40 个版本Version 1 到 40每个版本对应一个固定的模块尺寸——Version 1 是 21×21Version 2 是 25×25每升一版横向增加 4 个模块。这个增长规律很机械但数据容量不是简单的平方增长因为纠错码、功能图形也要占地方。版本选择上常见做法是先按误差校正级别和输入内容长度查表得出最小满足容量要求的版本。如果你自己写源码不需要把 40 个版本的容量表全部背下来但至少要把字节Byte模式下各版本的最大数据容量表放进代码里。表结构大致像这样Version模块尺寸L 级纠错容量字节H 级纠错容量字节121×21177225×253214329×295324433×3378341057×57271130这张表是硬编码的不需要现场计算。高纠错级别H容量低因为冗余占了空间低纠错L容量最大。类似票据、物流标签这种容易弄脏的场景一般选 Q 或 H如果是屏幕上展示、扫码距离固定L 就够。2.1 四种数据模式数字、字母数字、字节、日文QRcode 规范定义了四种数据编码模式实现时要根据输入内容自动选择最节约空间的模式数字模式Numeric0-9每 3 位数字编成 10 个比特字母数字模式Alphanumeric0-9、大写字母 A-Z、空格以及$%*-./:共 45 个字符每 2 个字符编成 11 个比特字节模式ByteUTF-8 或 ISO-8859-1 编码每字节 8 个比特日文模式KanjiShift JIS 双字节每字符 13 个比特C/C 实现里判断逻辑很简单内容全数字就选 Numeric否则检查是否属于 Alphanumeric 字符集再不行就用 Byte。字节模式最省事也最通用。中文如果走 UTF-8一个汉字 3 字节容量上不划算如果想要容量更优可以自己实现 Shift JIS 转换走 Kanji 模式——不过工程上很少这么干因为绝大多数扫码设备都认 UTF-8没必要为了一点容量引入编码转换成本。选择模式后写一段函数来计算最小版本static int choose_version(int data_mode, size_t data_len, int ecc_level) { const int capacity_table[40][4] { // L, M, Q, H 最大字节容量这里只列 Version 1-5 示意图 {17, 14, 11, 7}, // v1 {32, 26, 20, 14}, // v2 {53, 42, 32, 24}, // v3 {78, 62, 46, 34}, // v4 {106, 84, 60, 44}, // v5 // ... 后续版本实际项目中查全量表 }; int v; int col (ecc_level 0) ? 0 : (ecc_level 1) ? 1 : (ecc_level 2) ? 2 : 3; for (v 1; v 5; v) { if (capacity_table[v - 1][col] (int)data_len) { return v; } } return 5; }代码逻辑说明capacity_table是版本与容量的对应表第一维是版本号减一第二维是纠错级别L/M/Q/H。函数从 Version 1 开始试第一个装得下全部数据字节的版本就是结果。注意这里的容量是“数据字节数”不包含模式指示符和长度计数位的额外开销——完整实现时要在外层预留额外空间否则贴边选版本容易爆容量。2.2 位流拼接模式指示符、长度、数据、终止符选定模式后进入核心的比特流拼接阶段。这一步最容易出错因为 QRcode 规范对“每一段写多少比特”有严格限制[模式指示符 4bit] [长度计数 8/16bit] [数据位] [终止符 0000] [补位到字节边界] [填充字节]长度计数位的宽度和版本有关Version 1-9 的 Byte 模式长度是 8 位Version 10-26 是 16 位Version 27-40 是 16 位。数字模式对应是 10、12、14 位。这个差异非常容易写错尤其是做动态版本选择时算出来的长度位宽不对整个二维码就废了。数据位之后要追加终止符0000但总量不超过容量限制。终止符后面如果还没到字节边界补 0 对齐。所有空间填完后如果还有剩余容量按固定规则循环填入填充字节0xEC和0x11。这两步看起来机械实际上决定了二维码能不能被标准扫码头解析。这是拼接代码的逻辑骨架// 把比特写入缓冲区bit_count 是已经写过的位数 static void write_bits(BitBuffer *buf, unsigned int val, int len) { for (int i len - 1; i 0; i--) { int bit (val i) 1; buf-data[buf-bit_count / 8] | (bit (7 - buf-bit_count % 8)); buf-bit_count; } }参数说明val是要写入的数值len是这个值占几位。写入顺序是从高位到低位bit_count是全局位游标。因为 QRcode 数据编码是逐比特拼接所以用位游标而不是字节游标来控制。这一步写出来后后面所有编码步骤都依赖这个基础工具函数。3. 纠错码与掩码从比特流到黑白色块的秘密比特流拼好之后理论上已经可以画矩阵了但直接画出来有两个问题。一是数据太规整容易被扫码头误识别为功能图形二是如果二维码局部脏污、破损数据恢复能力差。QRcode 的解决方式是先加里德-所罗门Reed-Solomon纠错码再做掩码处理。这一章是实现二维码生成算法的分水岭。很多“为啥我的二维码扫不出来”的 case最后都定位在纠错码生成错误或者掩码序号不对上。如果你只调库不写底层可能永远碰不到这些问题但一旦你进入 C语言/C源码层面这两个就是主战场。3.1 Reed-Solomon 纠错编码生成多项式与除法QRcode 的纠错原理是把数据位当作多项式系数用生成多项式去除余数就是校正码字ECC codewords。每个版本、每个纠错级别定义了固定的纠错码字数比如 Version 1-L 需要 7 个 ECC 码字Version 1-H 需要 17 个。这些码字是伽罗华域 GF(256) 上的运算结果。简化实现里我们不需要自己构造伽罗华域表——QRcode 规范附录里直接给出了不可约多项式0x11D对应的指数表和对数表。你可以在源码里硬编码两个 256 长度的数组gf_exp和gf_log然后基于查表实现乘法。static int gf_mul(int x, int y) { if (x 0 || y 0) return 0; return gf_exp[(gf_log[x] gf_log[y]) % 255]; }这段的逻辑是把乘法转换成对数域的加法再查指数表还原。GF(256) 里加法和减法都是异或运算所以除法就是对数相减后再走指数表。实际工程中不需要每次都查表可以预计算乘法表但对现代 CPU 来说这两个查表操作成本完全可以忽略。生成纠错码核心代码如下void rs_generate(const uint8_t *data, size_t data_len, uint8_t *ecc, size_t ecc_len) { uint8_t gen[68]; // 最多支持的纠错码字个数 rs_init_gen_poly(gen, ecc_len); uint8_t res[256]; memset(res, 0, ecc_len); for (size_t i 0; i data_len; i) { uint8_t factor data[i] ^ res[0]; for (size_t j 0; j ecc_len - 1; j) { res[j] res[j 1] ^ gf_mul(gen[j], factor); } res[ecc_len - 1] gf_mul(gen[ecc_len - 1], factor); } memcpy(ecc, res, ecc_len); }参数说明data是数据码字也就是上一步拼好的比特流按 8 位切分的结果ecc_len是纠错码字数由版本和纠错级别查表决定。gen是生成多项式系数按 QRcode 规范预先生成。核心循环里factor是当前数据字节异或上一个余数首字节的结果然后逐位更新余数寄存器。最终余数就是附加的纠错码字。3.2 掩码规则与选择8 种图案的评估打分纠错码附加完成后码字序列就要被放置到矩阵里。放置顺序是规定的“之字形”路径从右下角开始两列一组从下往上、再从上往下交替布局。功能图形区旁边需要跳过。这里有一个很多人忘记的细节Version 2 时有 18 块齐位图案Alignment Pattern这些位置在数据填充时也要跳过。数据放置完毕后整体矩阵要与 8 种预设掩码图案做 XOR。掩码图案由特定公式生成本质是决定哪些坐标翻转。8 种掩码要全部尝试一次然后按规范里四个惩罚规则评分得分最低的那个胜出。这四个规则包括相邻同色块惩罚、同色区域块惩罚、行列相似性惩罚、以及黑白比例失衡惩罚。自己实现时最简单的策略是全部试完 8 种算分取最低。掩码应用核心代码// mask_id: 0-7根据规范中的公式判断该坐标是否翻转 static bool should_mask(int i, int j, int mask_id) { switch (mask_id) { case 0: return (i j) % 2 0; case 1: return i % 2 0; case 2: return j % 3 0; case 3: return (i j) % 3 0; case 4: return (i / 2 j / 3) % 2 0; case 5: return (i * j) % 2 (i * j) % 3 0; case 6: return ((i * j) % 2 (i * j) % 3) % 2 0; case 7: return ((i j) % 2 (i * j) % 3) % 2 0; } return false; }这段公式不是我们臆造的而是 QRcode 规范 ISO/IEC 18004 里定义的标准掩码公式。i是行索引j是列索引。函数返回true时表示原始数据模块要翻转颜色。掩码最终的输出就是扫码设备看到的黑白矩阵。3.3 放置功能图形定位、校正、时序图案QRcode 能保证任意角度被识别靠的是三个较大的“回”字形定位框Finder Pattern分布在左上、右上、左下三处。每个定位框是 7×7 的模块外圈黑色、内圈白色、中心黑色。除此之外左上和右上、左上和左下之间各有一条时序图案Timing Pattern由黑白交替的模块组成用于确定码元的尺寸。版本 2 时还要放置对齐图案Alignment Pattern。对齐图案的坐标呈规律性分布比如 Version 7 的坐标是 (6, 22, 38)。这个规律可以总结为一个公式构建坐标列表后去掉首尾的 6因为该位置已被定位框占用剩下的两两组合就是对齐图案的中心坐标。放置功能图形的顺序很关键先放定位框和对齐图案再放时序图案最后再填入数据码字。顺序反了数据区会被功能图形覆盖结果就是二维码肉眼看着是“花”的但扫不出来。源码实现里建议用一个独立的set_module()函数来写矩阵这样可以在任何阶段覆盖数据但不要对功能图形区域再次写数据。4. C语言实现 QRcode 编码器的完整框架从矩阵到位图输出前两章讲清了编码的理论路径这一章把它拼成一个可以编译运行的 C 语言程序。在 vscode 配置 c/c环境 时很多人习惯一上来就写图形界面其实生成二维码的核心算法不需要依赖任何外部库纯标准 C 就能完成。实现这个最小版本时我一般建议按四个模块拆分代码数据编码、纠错码计算、矩阵构建、位图输出。顺序不能乱模块之间通过结构体传数据。下面给出每个模块的骨架和关键决策。4.1 结构体定义与状态管理一个可维护的 QRcode 生成器至少需要这些数据结构typedef struct { uint8_t *data; // 数据码字 size_t len; // 数据码字长度 uint8_t *ecc; // 纠错码字 size_t ecc_len;// 纠错码字长度 int version; int ecc_level; // 0L,1M,2Q,3H int mask_id; uint8_t *matrix; // version*version 大小的矩阵值为 0/1 } QRCode; typedef struct { uint8_t bytes[512]; int bit_count; } BitBuffer;需要说明的是matrix数组的大小在运行时动态分配因为版本号直接决定矩阵尺寸。建议使用uint8_t*而非char*避免符号位混淆。C 语言里没有类的约束因此把状态集中在一个结构体里传递比函数之间用全局变量可靠得多。4.2 矩阵填充数据码字与纠错码字的交织放置规范要求数据和纠错码字按块Block交错排列。Version 1-2 的块数很少甚至只有一组Version 5 以上会有多组块每组各自有独立纠错码。这个交织逻辑是排行榜里最容易写错的部分。简单做法是先把所有数据码字按规范切成若干块每块单独计算纠错码然后按块间轮转的方式填充矩阵。分块规则需要查版本表——每个版本的 (数据码字数, 纠错码字数) 组合都是硬编码的。你不需要推导公式但要在源代码里准备 40 项的分组表。矩阵填充的路径是之字形从右下角开始向左移动每两列一组。规则是从右向左列坐标以 2 为步长递减在当前两列内先下后上或先上后下交替遇到功能图形区域跳过。实现时用一个状态机控制方向比递归实现直观。4.3 用最小代码生成 PNG 位图可选 PPM 方案二维码最终要落成图片最轻量的方式是输出 PPM 格式P6。PPM 是一种未压缩的位图格式写起来只需要一个文件头和 RGB 像素数组。消耗内存小调试容易配合 ImageMagick 可以快速转 PNG 或 JPEG。void write_ppm(QRCode *qr, const char *filename, int scale) { int size qr-version * 4 17; FILE *fp fopen(filename, wb); fprintf(fp, P6\n%d %d\n255\n, size * scale, size * scale); for (int y 0; y size * scale; y) { for (int x 0; x size * scale; x) { int mx x / scale, my y / scale; int is_black qr-matrix[my * size mx] 1; uint8_t color is_black ? 0 : 255; fputc(color, fp); fputc(color, fp); fputc(color, fp); } } fclose(fp); }参数说明scale是每个二维码模块的像素大小。如果scale8一个 Version 1 的 21×21 矩阵输出为 168×168 像素。size计算方式是version*417对应版本公式。这段代码没有引入第三方数据库直接写文件流在任何 C 编译器上都能跑。真正的产品代码里可以换成 libpng 或 stb_image_write核心矩阵部分不用改。推荐流程是先输出 PPM确认格式正确再做格式转换。如果用 vscode 调试可以用十六进制查看器对比文件头部是否有P6标记以及像素数据长度是否符合预期。5. C 方向QRcode 源码的封装、性能与工程化落点C 语言版的编码器通常以函数库形态存在C 工程里直接调用 C 接口也可以但长期维护下来会暴露两个问题资源管理要手动处理错误信息不够结构化。更好的方案是用 C 类封装把底层 API 包成 RAII 风格的接口。5.1 C 封装的可读性与内存安全可以做的事是定义QRCodeEncoder类构造函数传入纠错级别成员函数返回一个矩阵对象或 bufferclass QRCodeEncoder { public: QRCodeEncoder(int ecc_level) : level_(ecc_level) {} bool encode(const std::string text, std::vectoruint8_t out_matrix, int version, int mask_id); private: int level_; };封装不是目的内存安全才是。C 语言版里QRCode.matrix需要手动 free一旦漏掉就内存泄漏。C 封装可以用std::vector来管理二进制缓冲区异常安全性和代码可读性都更好。encode()内部调用底层 C 函数把矩阵数据拷贝到传入的 vector 里。如果你的项目里已经有 mbedtls 这类加密库把二维码生成和签名数据放在一起做也是个合理需求——二维码内容可以是加密包但二维码本身无所谓加密只是编码。5.2 性能考虑与内存优化嵌入式环境里生成二维码内存是个硬指标。Version 40 的矩阵尺寸是 177×177即 31329 个模块每个模块 1 bit 存储的话不到 4KB但如果你用一个uint8_t表示一个模块就膨胀到 31KB 内存。对于动不动只有几十 KB RAM 的 MCU这是个不小的开销。优化的手段有两个方向一是位压缩矩阵按位存储而不是按字节存储读写时增加位移操作二是原地修改不要拷贝多个中间缓冲区。数据码字 纠错码字的总量在 Version 40-H 时约为 3706 字节所以整体内存控制在 10KB 以内是可行的。// 位访问的矩阵封装 class BitMatrix { public: explicit BitMatrix(int size) : size_(size), bits_((size * size 7) / 8, 0) {} bool get(int x, int y) const { int idx y * size_ x; return (bits_[idx / 8] (idx % 8)) 1; } void set(int x, int y, bool val) { int idx y * size_ x; if (val) bits_[idx / 8] | (1 (idx % 8)); else bits_[idx / 8] ~(1 (idx % 8)); } private: int size_; std::vectoruint8_t bits_; };接口设计说明get/set都接受逻辑坐标(x, y)内部自行计算位偏移。这样上层图形绘制时不必关心位存储细节底层位压缩的改动不会影响调用方。对于 jetson 这类 Linux 设备还可以考虑用 SIMD 优化掩码运算——不过没有性能瓶颈时不必过早优化。5.3 扫码失败时的验证技巧用行扫描确认时序图案代码写完生成了一张二维码但手机扫不出来。这时最常见的排查方法是用命令行工具解码测试。可以参考的工具包括zbarimg、zxing-cpp的命令行样例它们能直接告诉你解码失败的阶段比如“数据块纠错失败”还是“没有找到定位图案”。另一个不依赖外部库的办法是在生成代码里加一个自检函数手动扫描矩阵的时序图案行检查是否按黑白交替排列。时序图案的坐标是固定的例如 Version 1 的第 6 行和第 6 列。如果自检函数发现连续两个相同颜色的模块说明功能图形被数据覆盖或掩码应用到了非数据区这就是扫描失败的直接原因。加上这个自检函数后编码流程就完整了。生成二维码的核心不是图形库而是规格的精确实现。C 语言和 C 源码的差距主要体现在资源管理和接口组织上底层算法一带共通——把 ISO/IEC 18004 的细节各自吃透没有捷径。本文还有配套的精品资源点击获取
返回列表