简介:PHP二维码在线生成工具本地版v1.0是一份基于PHP源码的二维码生成方案,主要面向需要在自己网站空间或本地环境生成二维码的开发者,解决线上生成服务依赖外部接口、无法自定义部署的问题。程序采用当前时间与随机数组合的方式生成PNG图片路径,可有效避免文件重复,生成的图片保存在根目录,单张体积约1K~4K,图片宽高会随文字数量自动变化,实测生成包含200个汉字的二维码也能正常运行,整体逻辑简洁,适合内网环境、临时工具站或教学演示场景,也便于按需修改样式或扩展功能。压缩包共7个文件,以两个PHP核心脚本为主,配以txt使用说明与URL参考链接,另有png示例图,包体仅25KB,部署非常轻量;上传至PHP网站空间后通过首页index.php即可使用,子目录部署同样支持。目前已有280人学习下载,适合具备基础PHP环境、希望快速搭建本地二维码生成功能的开发者参考。 做这个工具的直接原因很简单:我给客户做小程序后台时,每天要给几十个商品生成带渠道参数的二维码,原来一直用在线二维码网站,但用着用着问题就来了。最让我没法忍的是,商品编号、渠道码这些内部数据都要先传到别人的服务器,免费版还有各种限制,一不注意就弹广告。后来我干脆自己用 PHP 写了一个本地版二维码在线生成工具,v1.0 从开发到现在稳定跑了两个多月,今天把完整实现思路、核心代码和踩坑记录都整理出来,给同样需要本地生成二维码的朋友参考。这里的“在线”指的是浏览器访问本地服务实时生成,不是 SaaS 服务。
1. 为什么我弃用在线二维码网站,自己写了个本地版
1.1 在线生成器的四个痛点
用在线网站生成二维码,表面上省事,实际用起来难受的地方不少。先说数据安全问题,这是最核心的。我在给客户做商品渠道追踪时,二维码内容里通常带着商品编号、内部物料编码、渠道参数,有时候还有客户手机号,这些东西一旦提交到第三方网站,数据就脱离你控制了。虽然大多数网站声称不做存储,但你没法验证,出了问题就是自己的锅。
第二个痛点是功能限制。免费版在线生成器基本只能改改内容、尺寸,容错级别固定,想加 logo、改颜色、批量生成,全部要付费。我曾经为了一个项目连续开了三个月会员,一个月几十块钱不算多,但用起来总觉得亏。
第三个痛点是稳定性不可控。在线服务哪天改版、域名变更、图片 CDN 挂了,你历史生成的二维码图片就可能失效。二维码一旦打印出去,用户扫码直接就打不开,这种问题在线工具厂家不会替你负责。
第四个痛点是效率。一次要生成一两百个二维码时,用在线工具要么手动复制粘贴到网页,要么一个个点下载,效率非常低。有时还有验证码、登录、等待队列之类的东西,批量场景下根本没法用。
1.2 本地版的核心价值:数据安全、离线可用、可二次开发
自己写本地版之后,以上四个问题基本都消失了。数据全程在本地 PHP 进程里处理,不经过任何第三方服务器,商品信息、渠道参数这些敏感数据可以放心用。服务跑在内网或者本机,断网也能正常生成,不用看别人脸色。
另外最重要的是可二次开发。我后来把生成接口接进了内部商品管理后台,商品入库时自动生成带渠道参数的二维码,文件名按商品编码命名,直接归档到服务器。在线工具根本做不到这种深度集成。
1.3 v1.0 的功能边界与技术选型
v1.0 我规划了这几个功能:单张二维码生成、批量生成、自定义容错级别、自定义尺寸和留白、logo 合成、前端实时预览、批量打包下载。技术栈没有追新,用了原生 PHP + jQuery + phpqrcode。phpqrcode 是网上很老牌的单文件 PHP 二维码库,不需要 Composer,适合本地小工具。如果你项目里用的是 ThinkPHP 3.2.3 这类老框架,也可以直接把 qrlib.php 引入,改成控制器方法即可,代码思路完全一致。
2. 二维码生成原理与 PHP 库选型
2.1 黑白格子背后的数据编码与容错级别
写生成工具之前,我建议先花十分钟理解二维码的基本原理,不然遇到“扫不出来”的问题会很被动。二维码本质上是一个二维矩阵,把字符串按一定规则转成二进制,再通过 Reed-Solomon 纠错算法加上冗余数据,最终分布到矩阵的黑色和白色模块里。识别端扫码时,先通过三个角上的回字定位图形确定方向和坐标系,再读取矩阵里的数据,反向还原出原始字符串。
这里最需要关注的是容错级别。二维码分 L、M、Q、H 四个级别,分别能容忍大约 7%、15%、25%、30% 的污损或遮挡。容错级别越高,二维码图案越密,识别越稳。给商品打码这种场景,我默认用 M 级,既能保证一定的抗污损能力,图案密度也适中。如果要在二维码中间贴 logo,就得用 Q 或者 H 级,不然中央遮挡区域容易导致扫码失败。
2.2 phpqrcode 与 endroid/qr-code 怎么选
PHP 生成二维码的库,主流就是 phpqrcode 和 endroid/qr-code 两个。我当时的选型对比是这样的:
| 对比项 | phpqrcode | endroid/qr-code |
|---|---|---|
| 安装方式 | 手动 require 单文件 | Composer 安装 |
| PHP 版本要求 | PHP 5.6+ 即可 | 要求 PHP 7.2+,部分版本更高 |
| 依赖扩展 | GD 库 | GD、fileinfo 等 |
| 输出格式 | PNG、SVG | PNG、SVG、EPS、PDF 等 |
| Logo 合成 | 需要自己用 GD 处理 | 官方支持,自带接口 |
| 维护状态 | 多年未更新,功能稳定 | 持续维护 |
| 上手成本 | 极低 | 需要熟悉 Composer |
最终我选了 phpqrcode,原因有三个:一是本地服务器是 Windows 环境,跑着老项目,PHP 版本还停留在 5.6,endroid 新版本直接装不上;二是这个工具不需要复杂导出格式,PNG 就够用;三是 phpqrcode 单文件引入,代码逻辑一目了然,出问题好排查。
2.3 本地环境准备:PHP 版本、GD 扩展、目录权限
开始前先确认环境。Windows 上我建议用 phpstudy 或 XAMPP,Linux 直接用包管理器安装 php-gd。打开命令行输入php -m,如果输出列表里没有 gd,就需要安装或开启扩展。
php -m | grep gd如果没有输出,说明 GD 扩展没装好。我在 Windows 的 phpstudy 里遇到过一次,原因很简单,php.ini 里没有去掉extension=gd2前面的分号。如果你用 Docker 拉官方镜像,比如php:7.4-apache,也需要自己执行docker-php-ext-install gd安装扩展。
另外要注意 output 目录的写权限。批量生成二维码时要写文件,apache 或 php-fpm 运行用户必须对目标目录有写权限,不然会报 “failed to open stream: Permission denied”。
3. 核心实现:单张生成、批量导出、Logo 合成、前端预览
3.1 单张二维码接口:20 行代码搞定
phpqrcode 的核心方法只有一行QRcode::png(),但直接调用会遇到一个输出问题:这个方法默认把图片数据直接打印到浏览器,不方便作为接口返回。我用输出缓冲把它截获,再统一返回图片流。单张生成接口的完整代码:
<?php require_once __DIR__ . '/lib/phpqrcode/qrlib.php'; $data = $_GET['data'] ?? 'hello'; $size = max(1, min((int)($_GET['size'] ?? 10), 20)); $margin = (int)($_GET['margin'] ?? 2); $ec = in_array($_GET['ec'] ?? 'M', ['L', 'M', 'Q', 'H']) ? $_GET['ec'] : 'M'; ob_start(); QRcode::png($data, false, $ec, $size, $margin); $png = ob_get_clean(); header('Content-Type: image/png'); header('Content-Length: ' . strlen($png)); echo $png;这段代码里几个参数要注意:size 是每个模块的像素数,取值范围在 1 到 20 之间,实际图片边长大约是size * 模块数 + 2 * margin,并不是说 size 越大内容越多。margin 是二维码四周白边的像素宽度,这个值不能设成 0,扫码器需要靠白边来确认边界,一般保持 2 以上。
3.2 批量生成:一次导入几百条并打包下载
批量生成是本地工具相对在线网站最明显的优势。我的实现是从 textarea 里读文本,一行一条内容,然后循环调用QRcode::png输出到指定目录,最后用 ZipArchive 打包下载。核心代码:
<?php require_once __DIR__ . '/lib/phpqrcode/qrlib.php'; $raw = $_POST['data'] ?? ''; $lines = explode("\n", $raw); $outDir = __DIR__ . '/output/' . date('YmdHis'); if (!is_dir($outDir)) { mkdir($outDir, 0755, true); } foreach ($lines as $i => $line) { $text = trim($line); if ($text === '') { continue; } $filename = $outDir . '/' . $i . '_' . md5($text) . '.png'; QRcode::png($text, $filename, QR_ECLEVEL_M, 10, 2); } $zip = new ZipArchive(); $zipFile = $outDir . '.zip'; $zip->open($zipFile, ZipArchive::CREATE | ZipArchive::OVERWRITE); foreach (glob($outDir . '/*.png') as $file) { $zip->addFile($file, basename($file)); } $zip->close();这里我踩过一个坑:如果直接用序号$i做文件名,有两个内容不同的二维码在循环里都会生成同样名称的文件,后生成的文件会覆盖先前的。后来加了md5($text)做指纹,再加上序号前缀,基本不会冲突。批量导出时如果一次几万条,建议用set_time_limit(0)避免执行超时,同时每处理完一个 unset 释放内存。
3.3 Logo 与彩色二维码:扫得出来才是关键
默认生成的二维码是黑白的,放在产品包装上比较丑,所以 v1.0 加了 logo 合成和颜色自定义。logo 合成的原理很简单:先生成二维码 PNG,再用 GD 库把 logo 图片缩放后贴到二维码中央。
<?php $qr = imagecreatefrompng($qrFile); $logo = imagecreatefrompng($logoFile); $qrW = imagesx($qr); $logoW = (int)($qrW * 0.25); // logo边长控制在二维码的25% $logoH = (int)(imagesy($logo) * $logoW / imagesx($logo)); $dstX = (int)(($qrW - $logoW) / 2); $dstY = (int)(($qrW - $logoH) / 2); imagecopyresampled($qr, $logo, $dstX, $dstY, 0, 0, $logoW, $logoH, imagesx($logo), imagesy($logo)); imagepng($qr, $qrFile);logo 大小一定要控制好,边长不要超过二维码总尺寸的 30%。二维码的容错机制能容忍中央区域被遮挡,但也有限度,我实测过,logo 如果超过 30%,手机扫码时识别率会明显下降,尤其是环境光线不好时,基本就是报废状态。
彩色二维码我最初想直接改 phpqrcode 源码,后来发现不值得。phpqrcode 的QRcode::png()本身就支持$saveToFile参数,但不支持自定义前景色。简单做法是生成灰度二维码后,再用 GD 遍历像素替换颜色,但性能一般。经验是:二维码的三个定位角尽量保持深色,不要做反色处理。很多人做“反色二维码”觉得酷,把黑底白格,或者用亮色当前景,结果就是扫码器识别不了,日常使用千万别这么干。
3.4 前端不刷新页面实时出码
v1.0 的前端用 jQuery 做实时预览,绑定 input 的 input 事件,防抖后请求 3.1 里的接口,拿回 base64 图片直接替换 img 标签。这里关键是防抖,不然用户每敲一个字符就请求一次,本地服务压力不大,但浏览器网络请求会乱掉。
另外分享一个扩展场景:有人想在背景图上拖拽二维码、调整大小后保存成一张新图片。这个需求本质上已经不是生成二维码,而是图片合成。前端用 html2canvas 可以把 DOM 截图,但高清输出比较麻烦,字号和图片清晰度都不稳定。我更推荐的做法是后端接收坐标、尺寸、背景图参数,用 GD 成图,这样图片质量可控,还能保存高清版本。v1.0 我把这个功能留到 v1.1 再做成独立模块。
4. 完整实操:把 v1.0 跑起来并集成到业务系统
4.1 目录结构与部署步骤
我最终的目录结构是这样的:
qr_tool/ ├── index.php ├── api.php ├── lib/ │ └── phpqrcode/ │ ├── qrlib.php │ └── ... ├── assets/ │ └── jquery.min.js └── output/部署步骤很简单:把整个目录放到本地 Web 服务的站点根目录,比如 phpstudy 的 www 目录或 Nginx 的 html 目录,然后访问http://localhost/qr_tool/index.php。output 目录需要提前建好并确保有写权限。
4.2 核心代码逐段解析
index.php 核心是表单和实时预览,我贴一段关键代码:
<div class="form-item"> <label>二维码内容</label> <input type="text" id="qr-data" value="https://example.com" /> </div> <div class="form-item"> <label>容错级别</label> <select id="qr-ec"> <option value="L">L(7%)</option> <option value="M" selected>M(15%)</option> <option value="Q">Q(25%)</option> <option value="H">H(30%)</option> </select> </div> <img id="qr-preview" src="" alt="二维码预览" /> <script> var timer = null; $('#qr-data, #qr-ec, #qr-size').on('input change', function () { clearTimeout(timer); timer = setTimeout(loadQr, 300); }); function loadQr() { var data = $('#qr-data').val(); var ec = $('#qr-ec').val(); var size = $('#qr-size').val(); $('#qr-preview').attr('src', 'api.php?data=' + encodeURIComponent(data) + '&ec=' + ec + '&size=' + size); } </script>几个细节:内容输入框一定要用encodeURIComponent编码,如果内容里带了&、?这样的参数,不编码会导致请求参数断裂。容错级别下拉框选中后立刻触发预览,用户交互会比较顺手。
4.3 性能测试与内存调优记录
我本地压过一次,生成一张纯文本二维码,接口响应约 10ms,肉眼感知就是瞬间。批量生成 100 张普通二维码,写入文件方式总耗时大概 0.8 秒,这个速度完全满足日常需求。
批量场景下内存增长比较明显,主要出现在 logo 合成阶段,因为imagecreatefrompng会把整张图片载入内存,如果大量并发,内存很容易顶满。我的处理办法是每处理完一张就imagedestroy()释放资源。生成大量文件时,建议分批处理,比如每 500 张打包一个 zip,避免单个 zip 文件过大导致下载超时。
5. 常见问题与排查技巧实录
5.1 扫不出来:从容错级别、颜色对比度、白边三个方面排查
这个问题的出现频率最高。我整理了一个排查顺序,从概率高的原因开始:
| 现象 | 原因 | 解决方案 |
|---|---|---|
| 时好时坏,尤其在光线差时识别失败 | 容错级别太低 | 至少用 M 级,有遮挡用 Q/H 级 |
| 深色背景、亮色模块,或反色二维码 | 扫描器对反色支持差 | 尽量采用深色模块浅色背景 |
| 二维码四周白边太小 | 扫码器无法确认边界 | margin 参数加到 2 以上 |
| 打印出来扫码失败 | 打印分辨率不足或纸张纹路干扰 | 提高打印分辨率,至少 300dpi |
| 屏幕显示时扫不出来 | 屏幕亮度、反射 | 调亮屏幕并减少环境光反射 |
还有一个“二维码缺口怎么调整”的问题,其实是打印场景里经常遇到的。生成时数据没错,但打印后出现白色断点或黑点,这不是二维码本身的 bug,而是打印机脏了或纸张受潮。可以先打一张测试页,确认打印机状态。如果边缘锯齿明显,检查打印设置里的图片缩放,不要勾选“适应页面”,否则会改变长宽比例。
5.2 中文和特殊字符乱码怎么处理
PHP 生成二维码时,内容字符串必须是 UTF-8 编码,否则生成出来的东西扫码会显示乱码。我在 Windows 下踩过坑:PHP 文件本身有时是 GBK 编码,字符串拼接出来直接就是 GBK,二维码扫出来就是乱码。解决办法是编辑器统一把 PHP 文件保存为 UTF-8 无 BOM 格式。
如果内容是 URL 地址,建议生成前用urlencode处理参数部分。比如https://example.com/?uid=1001&from=qr,直接把整段内容放进二维码,扫码是能识别的,但有些扫码器会忽略部分参数,稳妥的做法是对参数值做 URL 编码,或者把参数内容编码成短链再生成二维码。
5.3 批量生成时超时和内存耗尽
批量生成几百张还好,超过 1000 张时我遇到过Maximum execution time of 30 seconds exceeded和Allowed memory size exhausted。原因和解决办法都很明确:一是 PHP 默认执行时间太短,在脚本开头加set_time_limit(0);二是 GD 图像资源没有及时释放,循环里加imagedestroy($qr)。
另外批量提交数据时,max_input_vars默认只有 1000 个变量,如果你用多个 input 字段提交一批数据,很容易被这个限制截断。我的方案是用 textarea 一次提交全部内容,换行分隔,完美避开这个坑。
5.4 PHP 环境报错速查表
| 报错信息 | 原因 | 解决 |
|---|---|---|
Call to undefined function imagecreate() | GD 扩展未安装/未启用 | 开启 php.ini 里的extension=gd2,或重新编译安装 |
QRcode::png(): Argument #4 ($size) must be ... | size 参数类型不合法 | 确保传入整数,且范围在 1 到 20 之间 |
mbstring is already loaded | php.ini 中重复加载模块 | 检查 php.ini 和命令行配置是否重复,保留一处 |
mkdir(): Permission denied | 输出目录无写权限 | 给目录配置写入权限,程序内用is_dir()检查后创建 |
最后再分享一个我自己一直在用的小技巧:批量导出的文件名里加上日期、时间戳和内容哈希,比如20260503_123000_1f3872a.png,这样既能避免覆盖,又方便追溯到是哪一批生成的数据。管理内部物料码时,这个习惯能省不少事。
做这个工具最大的体会是:本地工具真正的价值不是省那几十块钱会员费,而是数据安全可控、逻辑随时能改,还能和内部系统无缝对接。现在这个 v1.0 已经从工具升级成了我内部系统里的一个模块,商品录入时自动调用,生成完直接归档。后面如果有时间,我打算把前端摄像头扫码识别也加进去,让生成和验证形成闭环。
本文还有配套的精品资源,点击获取