HarmonyOS ArkTS 二维码生成器的编码原理与 Canvas 渲染

发布时间:2026/7/20 20:40:48
HarmonyOS ArkTS 二维码生成器的编码原理与 Canvas 渲染 引子二维码的黑白方块从哪来扫码支付、加好友、打开链接——二维码每天都在用。但大多数人不知道二维码是怎么生成的输入一段文字输出一堆黑白方块中间经历了什么这个二维码生成器是工具箱里技术含量最高的工具。代码不到 300 行但涉及了有限域运算、多项式除法、里德-所罗门纠错、模块矩阵铺设、Canvas 渲染等多个知识点。今天就从代码出发拆解二维码生成的完整流程。完整效果二维码的编码流程生成二维码分五步输入文字 → 字节编码 → 选择版本 → 计算纠错码 → 铺设模块矩阵每一步都有明确的输入和输出最终得到一个二维数组1 表示黑色0 表示白色。第一步字节编码constdatanewInt32Array(text.length);for(leti0;itext.length;i)data[i]text.charCodeAt(i);把每个字符转成 ASCII 码。比如 “HELLO” → [72, 69, 76, 76, 79]。为什么用字节模式QR 码有多种编码模式数字模式、字母数字模式、字节模式、汉字模式。字节模式最通用能编码任意字符虽然效率不是最高但实现最简单。对于工具箱这种输入内容不确定的场景字节模式是最佳选择。第二步构造比特流letbits:number[][];constpushBits(v:number,n:number){for(letin-1;i0;i--)bits.push((vi)1);};pushBits(4,4);// 模式指示器0100字节模式pushBits(data.length,8);// 字符计数8 位for(leti0;idata.length;i)pushBits(data[i],8);// 数据pushBits(0,4);// 终止符0000比特流的结构┌──────────┬──────────┬──────────┬──────────┐ │ 模式指示器 │ 字符计数 │ 数据内容 │ 终止符 │ │ 4 bit │ 8 bit │ N×8 bit │ 4 bit │ └──────────┴──────────┴──────────┴──────────┘模式指示器0100表示字节模式字符计数数据的字节数数据内容每个字符的 ASCII 码终止符0000标记结束pushBits 的位操作pushBits(v, n)把数字v用n位二进制表示逐位推入数组。比如pushBits(72, 4)把 7201001000的低 4 位1000推入。第三步选择版本letver0;constreqBits48data.length*816;for(letv0;v10;v){if(ECC_BLOCK[v][0]*8reqBits){verv;break;}}QR 码有 40 个版本1-40版本越大模块越多能存的数据越多。这个实现只支持版本 1-10。版本和模块数的关系版本号 × 4 17 模块数版本 1 是 21×21版本 2 是 25×25以此类推。版本选择的逻辑计算需要的总比特数reqBits然后找第一个能装下的版本。ECC_BLOCK[v][0] * 8是版本v的总数据容量比特。第四步计算纠错码这是整个算法最复杂的部分——里德-所罗门Reed-Solomon纠错码。什么是纠错码QR 码在使用中可能被遮挡、污损、模糊。纠错码的作用是即使部分数据损坏也能恢复原始内容。M 级纠错能纠正约 15% 的错误。比如一个 25×25 的二维码被遮住一小块扫码软件依然能正确识别。GF(256) 有限域functiongfLog():Int32Array{constlnewInt32Array(256);constanewInt32Array(256);letx1;for(leti0;i256;i){a[i]x;l[x]i;x1;if(x256)x^0x11D;}l[1]0;returnl;}QR 码的纠错码基于 GF(256)——一个有 256 个元素的有限域。在这个域里加法是异或XOR乘法是查表。为什么用 0x11D0x11D是 GF(256) 的不可约多项式x^8 x^4 x^3 x^2 1。当乘法结果超过 255 时用这个多项式取模保证结果还在 0-255 范围内。多项式类classPoly{terms:Int32Array;mul(other:Poly):Poly{...}mod(other:Poly):Poly{...}}Poly表示一个多项式terms是系数数组。mul做多项式乘法mod做多项式除法取余。生成多项式functionrsGenPoly(degree:number):Poly{constexpgfExp();letpnewPoly(newInt32Array([1]));for(leti0;idegree;i)pp.mul(newPoly(newInt32Array([1,exp[i]])));returnp;}生成degree阶的里德-所罗门生成多项式。比如 degree10 时生成(x-1)(x-2)...(x-10)。纠错码的计算constgenrsGenPoly(eccg);for(leti0;inumBlocks;i){constblockdc.slice(i*dcg,(i1)*dcg);constmsgnewInt32Array(block.lengtheccg);for(letj0;jblock.length;j)msg[j]block[j];constremnewPoly(msg).mod(gen);// rem 就是纠错码}把数据码字作为多项式的系数除以生成多项式余数就是纠错码。第五步铺设模块矩阵功能图案// 定位图案三个角constfp[[0,0],[0,size-7],[size-7,0]];for(letfi0;fifp.length;fi){constfrfp[fi][0];constfcfp[fi][1];rect(fr,fc,7,7,1);// 外圈黑色rect(fr1,fc1,5,5,0);// 中间白色rect(fr2,fc2,3,3,1);// 内圈黑色}// 时序图案第6行和第6列for(leti8;isize-8;i){set(6,i,i%20?1:0);set(i,6,i%20?1:0);}定位图案是二维码的锚点扫码软件通过它们确定二维码的位置和方向。时序图案是黑白交替的线用于校准模块大小。数据铺设letcolsize-1;while(col0){if(col6)col--;// 跳过时序图案列for(letrowsize-1;row0;row--){for(letccol;ccol-1;c--){if(reserved[row*sizec])continue;// 放置数据位}}col-2;}数据从右下角开始按之字形向上铺设。跳过功能图案区域。掩码constm0(r:number,c:number)((rc)%2)0;for(letr0;rsize;r)for(letc0;csize;c)if(!reserved[r*sizec])mods[r*sizec]^m0(r,c)?1:0;掩码的作用是打散数据中的连续黑白块让扫码软件更容易识别。掩码 0 的规则是行号列号为偶数时翻转。Canvas 渲染Canvas(this.ctx).onReady((){constcellMath.min(260,300)/size;// 白色背景this.ctx.fillStyle#FFFFFF;this.ctx.fillRect(0,0,total16,total16);// 绘制黑色模块for(letr0;rsize;r){for(letc0;csize;c){if(this.qrData![r*sizec]1){this.ctx.fillStyle#000000;this.ctx.fillRect(8c*cell,8r*cell,cell,cell);}}}})Canvas 的用法CanvasRenderingContext2D是 Canvas 的绘图上下文提供fillRect、fillStyle等方法。.onReady()在 Canvas 准备好后执行绘制。cell 的计算cell Math.min(260, 300) / size计算每个模块的像素大小。260是最大显示宽度除以模块数得到每个模块的大小。8px 的边距绘制时加了 8px 的边距8 c * cell这是 QR 码的静区quiet zone确保二维码周围有空白扫码软件能正确识别。交互流程输入和生成TextInput({placeholder:请输入文字、链接或任意内容…,text:this.inputText}).onChange((v:string){this.inputTextv;})Button(生成二维码).onClick(()this.generate())用户输入内容点击按钮生成。onChange实时更新输入值。错误处理if(text.length0){this.showToasttrue;this.showResultfalse;return;}输入为空时显示错误提示不执行生成。结果显示if(this.showResultthis.qrData!null){Column(){Canvas(this.ctx)// 渲染二维码Text(this.inputText)// 显示原始内容}}生成成功后显示二维码和原始文本。踩坑记录坑 1GF(256) 表的重复计算gfLog()和gfExp()在每次乘法和取模时都会重新计算。可以优化成全局缓存let_log:Int32Array|nullnull;let_exp:Int32Array|nullnull;functiongfLog():Int32Array{if(!_log){_loggfLogImpl();}return_log;}坑 2版本限制当前只支持版本 1-10最多容纳 174 个字节。如果输入超过这个长度ver会是 0导致错误。可以加版本上限判断。坑 3Canvas 的 onReady 时机Canvas 的绘制必须在.onReady()里执行否则上下文还没准备好。如果在build()里直接绘制会报错。坑 4静区大小当前静区是 8px。标准 QR 码要求静区至少 4 个模块宽。对于小版本21×218px 足够对于大版本可能不够。坑 5掩码的选择当前固定使用掩码 0。标准 QR 码应该根据数据选择最优掩码评估 8 种掩码选得分最低的。固定掩码可能导致某些数据的扫码效果不好。代码改进建议1. 缓存 GF(256) 表把gfLog()和gfExp()的结果缓存起来避免重复计算。2. 支持更多版本扩展ECC_BLOCK数组支持版本 11-40能编码更长的文本。3. 自动选择最优掩码实现 8 种掩码的评估函数自动选择最优掩码。4. 带Logo的二维码在二维码中间加一个 Logo需要先计算纠错码Logo会遮挡部分模块再叠加 Logo。5. 保存到相册生成的二维码可以保存到手机相册。可以用photoAccessHelper模块。总结二维码生成器的核心是编码流程——从文字到比特流从比特流到纠错码从纠错码到模块矩阵。每一步都有明确的数学原理字节编码用 ASCII纠错码用 GF(256) 有限域模块铺设用之字形扫描掩码用异或运算。适用边界这个部分适合用作 QR 码编码原理的学习案例涵盖了 GF(256) 运算、多项式除法、Reed-Solomon 纠错、模块矩阵铺设、Canvas 渲染等核心知识点。但如果要上架应用商店还需要补充更多版本支持、最优掩码选择、Logo 叠加、保存到相册、扫描功能等内容。