
前阵子接了个活动页需求要在鸿蒙App里生成带品牌Logo的二维码用户扫一下就能跳转到活动落地页。一开始我以为这事很简单真要动手才发现Harmony开发里并没有官方封装好的生成带图片的二维码接口网上搜出来的大多是Python、Java、后端生成方案想在端侧用ArkTS直接做还得把图片融进二维码里能直接抄的代码还真不多。这篇记录我就把自己从选型、编码、合成图片到实测识别率的完整过程整理一遍重点讲清楚二维码矩阵是怎么来的、Logo该怎么嵌、实测要注意哪些坑希望能帮到同样在Harmony开发里做二维码功能的同学。1. 需求拆解用户要的可不只是能扫的码1.1 一个邀请场景里的三个硬性要求业务方最初的需求描述只有一句话海报上要有个二维码中间放我们Logo用户扫了能进活动页。听起来简单但拆开看至少有三个硬性要求。第一是美观。二维码不能是干巴巴的黑白方块中间必须嵌入品牌Logo而且Logo要有干净的背景不能直接压在一堆黑模块上否则观感很差。第二是识别率。海报可能会被打印出来也可能被用户截图转发到微信群不同场景下二维码的清晰度、容错能力要求完全不一样。第三是动态性。活动链接里的参数比如用户ID、渠道ID是每次实时生成的不可能让服务端提前把所有二维码都做好必须支持端侧按需生成。所以这里说的带图片的二维码本质上是三个能力的组合二维码编码、Canvas绘制、图片合成缺一不可。1.2 为什么我一开始就没考虑后端生成网上大部分方案是后端生成。Node.js用qrcode库、Java用ZXing、Python用qrcode库生成完之后把图片URL返回给客户端客户端直接加载。这个方案成熟且实现成本低但对于我们这种活动页场景有个致命问题动态参数只能靠URL带参可如果每次生成的都是不同URL二维码图片就没有缓存价值每个用户都要单独请求一次后端并发一高服务和存储压力都上来了。另一个更现实的原因是部分活动链接是短链短链本身长度固定二维码的内容相对稳定区别只在于渠道参数。这类需求完全可以做模板二维码把URL模板和参数分开端侧拿到参数后拼接并自行生成压根不需要后端参与。当然这不是说后端方案一无是处。如果二维码里要放特别复杂的营销素材或者需要统一管理数据统计后端生成依然有优势。只是对于端侧实时生成、动态拼接这种场景本地生成明显更合适。2. 技术选型本地二维码矩阵 Canvas 绘制为什么可行2.1 两条技术路线的对比Harmony端侧生成二维码主流做法其实就两条路线一是找现成的原生SDK二是引入纯JS/TS版二维码编码库自己拿Canvas画。原生SDK的优势是封装完整、性能好但问题也很明显——不少SDK体积大接口绑定深想在里面加一个自定义Logo绘制反而绕。纯JS库的优势是轻量、跨端逻辑一致、可以完全掌控绘制过程比如中间那块Logo区域怎么留、圆角怎么裁都由自己代码说了算。我最终选了纯JS二维码编码库 Canvas绘制。这个方案的核心思路是编码库只负责计算二维码矩阵哪些格子是黑的、哪些是白的、纠错信息怎么排布矩阵拿到手之后绘制层完全自己做。2.2 二维码的纠错级别是图片嵌入的前提很多人没意识到二维码中间能放图片靠的是QR码自带的纠错机制。QR码有L、M、Q、H四个纠错级别分别能恢复约7%、15%、25%、30%的码字数据。中间放一个Logo本质上是故意毁掉一部分模块让解码器靠纠错能力把那些被遮挡的数据补回来。这意味着Logo能不能放、放多大完全由纠错级别决定。如果选L级Logo稍微大一点就扫不出来选H级理论上最多能容忍30%的区域损坏但也不是说Logo就能占30%面积就能随便放因为Logo是连续区域破坏不是均匀分布的实际可用比例要保守得多。我在项目里选的是H级Logo边长控制在二维码整体尺寸的25%以内同时保证Logo周围有一圈白边。从实测来看这个配比在手机扫码、微信扫一扫、系统相机等场景下都很稳。2.3 库的引入方式与工程配置Harmony工程里引入JS库我用的是qrcode-generator这个库本身是纯JavaScript实现没有依赖浏览器DOM移植到ArkTS环境完全没有问题。装起来也很简单通过ohpm执行ohpm install qrcode-generator安装完成后在页面里引入import qrcode from qrcode-generator;这里有个容易踩的点如果直接import报错优先检查工程tsconfig.json里有没有开启esModuleInterop选项。HarmonyOS的ArkTS编译器对模块导入检查比较严格部分npm包需要开启这个选项才能正常使用默认导出。改完配置后重新同步工程基本就能跑通。3. 从字符串到可扫描的二维码矩阵3.1 生成核心代码矩阵数据与绘制qrcode-generator的用法非常直接。先指定纠错级别然后塞入字符串数据调用make()生成矩阵之后就能通过isDark(row, col)逐个读取每个模块的颜色。我封装了一个绘制二维码矩阵的函数核心代码大概长这样// 生成二维码矩阵 const qr qrcode(0, H); // 0表示自动选择版本H级纠错 qr.addData(https://example.com/invite?uid1024channelposter); qr.make(); const moduleCount qr.getModuleCount(); const cellSize 8; // 每个模块的像素大小 const qrSize moduleCount * cellSize; // 准备画布 const ctx new CanvasRenderingContext2D(new RenderingContextSettings(true)); ctx.clearRect(0, 0, qrSize, qrSize); ctx.fillStyle #FFFFFF; ctx.fillRect(0, 0, qrSize, qrSize); // 逐个绘制黑色模块 ctx.fillStyle #000000; for (let row 0; row moduleCount; row) { for (let col 0; col moduleCount; col) { if (qr.isDark(row, col)) { ctx.fillRect(col * cellSize, row * cellSize, cellSize, cellSize); } } }这段代码把二维码画成一个moduleCount * cellSize像素的正方形。注意cellSize不是随便定的它决定了二维码的实际物理尺寸。如果只是手机屏幕上展示8像素已经够清晰但如果是做海报或者需要打印至少要放大到16甚至24否则打印出来模块边缘模糊扫码容易失败。3.2 第一次跑通后最容易翻车的三个细节第一次把二维码画出来屏幕上确实能显示一个完整图案但这种程度离能用还很远。我在这个阶段踩了三个坑每个都值得单独说一下。第一个坑是没有处理Canvas的清晰度缩放。HarmonyOS的Canvas默认单位是vp不同设备的物理像素密度不一样。同样100vp宽的二维码在2倍屏上实际只有200物理像素模块边缘会有明显锯齿。解决方式是先获取设备的像素密度对Canvas做一次整体缩放const dpr this.getContext().getPixelMap ? ... ctx.scale(dpr, dpr);这样画出来的二维码在Retina级别的屏幕上才会干净锐利。第二个坑是盲目用0版本自动选择带来的模块密度问题。qrcode(0, H)里的0表示自动选择二维码版本数据量不同生成的模块数量也不同。URL越长版本越高模块越密。如果URL里带了一大串参数二维码会变得特别密同样的物理尺寸下单个模块肉眼几乎看不清。所以URL尽量用短链或者把参数精简这不仅是为美观更是为了识别率。第三个坑是数据字符集问题。默认情况下addData方法对非ASCII字符可能处理不友好。如果链接里带了中文参数或者用户昵称等Unicode字符最好先确认库支持UTF-8否则生成的二维码扫出来内容会乱码。我在项目里统一对链接做了URL编码避免非ASCII字符直接进入二维码内容。4. 把图片合成到二维码中心Logo 处理实战4.1 获取 Bitmap / PixelMap二维码矩阵画好了下一步是在中心叠加品牌Logo。HarmonyOS里图片的呈现方式是PixelMap一般可以从资源文件读取也可以从网络下载。从资源读取的代码大概是这样的import { image } from kit.ImageKit; async loadLogo() { const buffer await getContext(this).resourceManager.getMediaContent($r(app.media.logo)); const imageSource image.createImageSource(buffer.buffer); this.logoPixelMap await imageSource.createPixelMap(); const imageInfo await this.logoPixelMap.getImageInfo(); this.logoWidth imageInfo.size.width; this.logoHeight imageInfo.size.height; }如果你的Logo是网络图片先下载成字节流再走同样的createImageSource链路即可。这里要注意网络图片下载回来一定要先校验图片格式和大小。我就遇到过一次测试环境返回的错误图片直接把createImageSource打崩了后续所有二维码都画不出来。4.2 圆角白底 Logo 的绘制写法拿到PixelMap之后直接在Canvas中心绘制还不够。直接画上去的话Logo的四边会紧贴二维码黑色模块边缘很突兀而且黑色模块紧贴Logo会对解码造成额外干扰。我的做法是先画一个圆角白底再把Logo裁进去最后加一圈白色描边让Logo和二维码之间有一层明显的呼吸区const logoSize qrSize * 0.25; // Logo边长不超过二维码的25% const logoX (qrSize - logoSize) / 2; const logoY (qrSize - logoSize) / 2; ctx.save(); ctx.beginPath(); ctx.roundRect(logoX, logoY, logoSize, logoSize, logoSize * 0.15); ctx.clip(); ctx.drawImage(this.logoPixelMap, logoX, logoY, logoSize, logoSize); ctx.restore(); // 白色描边形成隔离带 ctx.strokeStyle #FFFFFF; ctx.lineWidth 6; ctx.strokeRect(logoX, logoY, logoSize, logoSize);这段代码里roundRect的圆角弧度设成了logoSize * 0.15视觉上比较柔和。clip()和drawImage()配合把Logo裁进圆角矩形里避免原图直角破坏二维码的整体风格。白色描边那份6像素的宽度在实际测试中相当于给Logo加了一圈安全区对扫码识别率有实打实的帮助。4.3 图片占比和纠错等级的配合经验关于Logo能占多大我做了几轮真机测试结论比较明确纠错级别Logo占比识别表现M15%20%光线好时能识别阴影下偶尔失败Q25%25%大部分场景可识别打印后风险较高H30%20%手机扫码稳定打印后也可用H30%30%近距离扫码稳定远距离扫码变慢所以我在正式项目里用的是H级纠错25%Logo占比。如果你的Logo本身比较复杂、色块反差小建议再降到20%宁可Logo小一点也别让用户扫码失败。另外还有一个经验Logo区域不要覆盖二维码的三个定位角就是左上、右上、左下那个回字形图案。虽然中心位置和定位角有距离但如果二维码尺寸不大Logo稍微偏一点就可能压到定位角定位角一旦被破坏再高的纠错级别也救不回来。5. 识别率实测与典型问题排查5.1 我测过的几种扫码场景代码写完不代表功能完成。我把生成的二维码分别放到了手机屏幕、打印纸、微信群截图三种场景里测试。手机屏幕扫码是最宽松的系统相机基本秒识别。打印纸测试则需要把二维码放大到至少300像素以上并且不能用普通喷墨打印要用激光打印否则黑色模块会有飞白识别率直线下降。微信群截图是我个人最推荐的测试方式因为它模拟了图片经过压缩再被扫码的最差情况。微信传输图片会有压缩二维码密集的模块边界会模糊如果这种状态下都能稳定识别那真实用户手里的绝大多数场景都没问题。5.2 三个必须记录的坑这阶段我踩了几个坑其中一个特别隐蔽Canvas画完二维码后在部分机型上导出图片时出现了白边。原因是Canvas的绘制区域和图片导出分辨率不一致导出时多了一圈空白。解决方法是统一使用width*scale作为导出的目标尺寸不要在导出时再次乘一次缩放系数。另一个坑是Logo区域过大导致某个纠错块完全不可用。有次我把Logo占比调到30%在大部分手机上测试都能扫结果换了个老款Android手机就扫不出来。排查后发现那个手机的解码器对中心区域的校正能力弱Logo中心和二维码的校正图形重叠太多。最后的方案是把Logo尺寸改成可配置项运营后台可以根据渠道灵活调整。第三个坑是绘制顺序问题。如果你先画Logo再画二维码Logo会被黑色模块盖住如果先画二维码再画Logo又怕二维码信息被完全遮掉。实际项目中我调整为先画完整二维码、再画白底、最后画Logo。这样Logo区域的二维码模块即使被覆盖底下的白底也能保证画面整洁不露出半截黑点。5.3 批量生成的性能优化活动页面一次可能要生成几十张二维码如果每张都走一遍创建二维码矩阵-绘制-合成Logo的完整流程明显会卡。我的优化思路是分层缓存。第一层缓存矩阵数据。同一个URL模板同一纠错级别生成的模块矩阵是固定的如果只是参数值不同矩阵就得重新生成。不过很多活动链接用短链所有用户共用同一个短链矩阵是同一个完全可以把模块数组缓存下来生成图片时不用再跑一次二维码编码。第二层缓存绘制结果。如果连Logo都一样只是尺寸不同可以直接生成一张基准尺寸的二维码Logo合成图后续缩放加载即可。这样大批量导出时每张图只需要一次drawImage性能会好很多。第三层是用离屏Canvas。把合成过程放到离屏Canvas里做完再把最终结果一次性绘制到可见Canvas上而不是在可见Canvas上一笔一笔画这样既不会中间态闪现也能减少UI线程的绘制压力。6. 继续扩展动态短链、海报合图与分享6.1 动态二维码URL 不变内容可变有些业务场景对二维码不可变、跳转内容可变有需求比如同一个活动入口二维码要支持不同期次的活动页面。做法是后端维护一个场景ID二维码内容统一指向https://example.com/scene/{sceneId}这个短链场景ID对应的具体活动页由后端下发。这样的话端侧生成的二维码内容是固定的海报和物料可以长期使用活动迭代时只要改后端配置不需要重新生成二维码图片。这个方案对端侧还有一个好处二维码矩阵可以做成全局缓存生成一次之后所有同样式的海报可以直接复用连编码时间都省了。6.2 把二维码合成到营销海报里业务方后来还提了个进阶需求不要单独给一张二维码而是直接把二维码画到营销海报的固定位置。这种需求在Canvas体系下反而简单。先把海报背景图绘制完整在指定坐标区域调用之前封装好的drawQrWithLogo方法最后整体导出成图片发给用户。需要留意的是海报上的二维码尺寸和边距。二维码周围至少要留一个模块宽度的白色区域也就是静区。很多设计稿根本不会考虑这一点二维码直接贴到海报边缘结果扫码时边框和背景干扰识别成功率明显下降。我在和设计对稿时会专门标注二维码的留白区域这个比想象中重要得多。6.3 收尾建议与注意点最后说几个零碎但实用的建议。首先是二维码内容安全校验。生成二维码前一定要校验链接协议限制为https否则用户扫出来跳到一个乱七八糟的页面对产品是致命伤害。如果链接来自用户输入做一次正则校验再加白名单这步不能省。其次是图片导出格式选择。如果二维码是要发给用户保存的导出成PNG是正解别用JPEG。JPEG压缩会在黑白交界处产生大量噪点导致模块边缘模糊。我测试过同一张二维码PNG和JPEG格式下扫码延迟差距明显JPEG格式在低端机上经常要扫好几次。最后是真实的扫码环境测试。模拟器上的二维码看起来清晰不代表真实机器上没问题。我的习惯是画完之后立刻用自己手机扫一次然后丢到微信群里再从群里保存图片扫一次。两次都能过这功能我才敢放心交付。Harmony开发里生成带图片的二维码拆开来看就是矩阵数据、Canvas绘制、图片合成这三件事每件事都不复杂串在一起的水坑却不浅。按这个流程走一遍基本能避开我踩过的那些坑。