3.x二维码信息解析使用示例
前面几篇介绍的都是二维码的"生成",本篇来看一下它的另一半功能——二维码的"解析",也就是把一张二维码图片反解出里面塞的内容。
1. 场景介绍
二维码解析并不是一个可有可无的功能,实际业务里用得相当多:
- 用户上传了一张二维码截图,后端需要判断这张图里到底是一个支付码、一个 URL 还是一段文本;
- 生成二维码之后,做一道"生成 -> 解析"的闭环校验,确认生成的二维码确实可扫、内容没被样式折腾丢;
- 对第三方提供的二维码图片做内容预检,拦截不合规的跳转链接。
quick-media 的 qrcode-plugin 底层依赖 zxing,生成用它的编码能力,解析用的就是它的解码能力(MultiFormatReader),插件只是把整个解析流程包装成了一个开箱即用的工具类:QrCodeDeWrapper。
2. 依赖引入
与生成篇完全一致,同一个依赖里既包含生成也包含解析:
<!-- https://mvnrepository.com/artifact/com.github.liuyueyi.media/qrcode-plugin -->
<dependency>
<groupId>com.github.liuyueyi.media</groupId>
<artifactId>qrcode-plugin</artifactId>
<version>3.0.1</version>
</dependency>
gradle 用户:
// https://mvnrepository.com/artifact/com.github.liuyueyi.media/qrcode-plugin
implementation 'com.github.liuyueyi.media:qrcode-plugin:3.0.0'
相关的测试用例可以在源码中找到:
3. 解析接口说明
解析的入口是 QrCodeDeWrapper(De 即 Decode),整个类只有两个静态方法,非常简单:
// 方式一:传图片地址,支持本地路径(相对/绝对)与网络地址
public static String decode(String qrcodeImg) throws IOException, NotFoundException
// 方式二:直接传加载好的 BufferedImage 对象
public static String decode(BufferedImage image) throws NotFoundException
- 入参
qrcodeImg走的是ImageLoadUtil.getImageByPath(path),因此支持三种姿势:本地绝对路径、classpath 相对路径(测试目录中的logo.jpg就是这么加载的)、http开头的网络地址 - 返回值就是二维码中的原始文本内容
IOException对应图片加载失败;NotFoundException对应图片中"找不到"可识别的二维码,这是解析失败最常见的异常,调用时务必处理
内部实现一眼就能看穿,走的是 zxing 的标准解码流程:
LuminanceSource source = new BufferedImageLuminanceSource(image);
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
Map<DecodeHintType, Object> hints = new LinkedHashMap<>();
// 解码设置编码方式为:utf-8
hints.put(DecodeHintType.CHARACTER_SET, "utf-8");
// 优化精度
hints.put(DecodeHintType.TRY_HARDER, Boolean.TRUE);
// 复杂模式,开启PURE_BARCODE模式
hints.put(DecodeHintType.PURE_BARCODE, Boolean.TRUE);
Result result = new MultiFormatReader().decode(bitmap, hints);
return result.getText();
这里有三个解码提示(hints),后面排查问题全靠它们,先混个脸熟:
| Hint | 值 | 作用 |
|---|---|---|
| CHARACTER_SET | utf-8 | 按照什么编码去还原内容,与生成时的编码必须匹配 |
| TRY_HARDER | true | 让解码器花更多精力去找二维码,提升识别成功率 |
| PURE_BARCODE | true | 声明图片是一张"纯净"的二维码,不做额外的容错识别 |
4. 可运行示例
4.1 解析本地二维码图片
来自测试类 QrDetectTest#testReadQrCode,加载一张本地图片并反解内容:
@Test
public void testReadQrCode() throws Exception {
BufferedImage img = ImageLoadUtil.getImageByPath("d://tmp/qq.jpg");
String ans = QrCodeDeWrapper.decode(img);
System.out.println(ans);
}
等价的简写姿势(一步到位,连图片加载都不用自己写):
public void testReadQrCodeV2() throws Exception {
String ans = QrCodeDeWrapper.decode("d://tmp/qq.jpg");
System.out.println(ans);
}
4.2 解析网络二维码图片
来自测试类 QrCodeWrapperTest#testDepaseQrCode,直接传网络图片地址:
@Test
public void testDepaseQrCode() throws FormatException, ChecksumException, NotFoundException, IOException {
String img = "https://static.oschina.net/uploads/img/201707/09205944_0PzS.jpg";
String ans = QrCodeDeWrapper.decode(img);
System.out.println(ans);
}
网络图片的解析姿势与本地没有区别,底层会自动下载再解码。不过要注意网络失败、图床失效这类问题,生产环境里更稳妥的做法是自己下载好图片再传 BufferedImage 进去。
4.3 生成 -> 解析闭环验证
很多小伙伴会在生成二维码之后怀疑"我生成的二维码到底能不能扫",其实完全可以自己写个闭环校验:生成之后直接反解,看内容是否与预期一致。
插件自带的 Base64Util 可以在 base64 与图片之间互转,配合使用如下:
public class QrDetectTest {
@Test
public void testBase642Ima() throws IOException {
// 图片转 base64
BufferedImage bfimg = ImageLoadUtil.getImageByPath("logo.jpg");
String str = Base64Util.encode(bfimg, "png");
// base64 转回图片
BufferedImage img = Base64Util.decode2Img(str);
System.out.println("img");
}
}
把这条链路串起来,就是完整的"生成-解析"闭环:
public void genAndParse() throws Exception {
String msg = "https://liuyueyi.github.io/hexblog/";
// 第一步:生成二维码,直接拿 BufferedImage
BufferedImage qrImg = QrCodeGenWrapper.of(msg)
.setW(300)
.setPadding(1)
.asBufferedImage();
// 第二步:解析回内容
String ans = QrCodeDeWrapper.decode(qrImg);
System.out.println(ans);
// 输出:https://liuyueyi.github.io/hexblog/
}
- 2.x 的
QrCodeGenWrapperBuilder 提供了asBufferedImage()方法,直接返回BufferedImage,不需要落盘再读盘 - 3.x 的
QrCodeGenV3则对应asImg(),同样返回BufferedImage - 解析出来的内容与输入
msg一致,说明这张二维码至少在 zxing 眼里是"可扫"的
4.4 异常处理姿势
解析与生成不同,"失败"是一种正常输入(比如用户随手传了张风景照),所以异常处理要当成业务逻辑的一部分来写:
public void parseWithError(String qrPath) {
try {
String content = QrCodeDeWrapper.decode(qrPath);
System.out.println("解析结果: " + content);
} catch (IOException e) {
// 图片加载失败:路径错误、网络异常、格式不支持等
System.out.println("图片加载失败: " + e.getMessage());
} catch (NotFoundException e) {
// 图片中识别不到二维码:可能根本没有二维码,也可能样式太花哨
System.out.println("未识别到二维码");
}
}
提示:
QrCodeDeWrapper#decode(BufferedImage)的方法签名只声明了NotFoundException,NotFoundException本身是 zxing 的受检异常;而decode(String)额外抛IOException。按上面的姿势两个都接住最稳妥。
5. 注意点
解析虽然接口简单,但"能不能解出来"受很多因素影响,下面几个点是实际使用中的高频坑:
- 编码要对上:解码器内部固定按
utf-8解析。如果二维码生成时通过setCode()指定过其他编码(比如 gbk),解析出来就是乱码甚至直接解析失败。所以自家生成的二维码请保持默认 utf-8 - PURE_BARCODE 模式对"花哨"二维码不友好:解码 hints 中开启了
PURE_BARCODE,它假设图片是一张干净的二维码。对于带大面积背景图、艺术化码元、深色滤镜的二维码,识别成功率会下降。如果失败,可以尝试先把图片裁剪出二维码所在区域再解析 - 遮挡与对比度:中间的 logo 太大(超过二维码约 1/5~1/3 的区域)、码元与背景色对比度太低(比如浅灰配浅蓝),都会导致识别失败。生成侧的规避方式:logo 别超过
setLogoRate的合理范围、容错级别用 H - 图片质量:图片过小、模糊、有噪点都会影响解码,建议二维码输出尺寸不小于 200x200;用户上传的截图常见"压得特别小"的情况,必要时先放大再解析
- gif 动图只取第一帧:
decode(String)内部走的是ImageIO.read(),对于 gif 只会读取第一帧。动态二维码的第一帧通常就是完整的二维码,所以一般没问题,但如果是自定义素材请保证第一帧可扫 - 解析接口与生成接口解耦:
QrCodeDeWrapper只依赖图片本身,不关心二维码是谁生成的。所以它既能解析本插件生成的二维码,也能解析微信、支付宝等任何标准二维码 - 只返回文本:
decode返回的是Result#getText(),即二维码的字符串内容。如果需要拿到二维码的纠错级别、版本号等元信息,就得自己基于 zxing 的MultiFormatReader扩展了
6. 小结
本篇介绍了 qrcode-plugin 中二维码信息解析的使用姿势:核心入口是 QrCodeDeWrapper#decode,支持本地图片、网络图片与 BufferedImage 三种输入,底层由 zxing 的 MultiFormatReader 完成,解码时固定 utf-8 + TRY_HARDER + PURE_BARCODE 三个提示。
配合生成侧的 asBufferedImage(),可以低成本实现"生成 -> 解析"的闭环自测;配合 Base64Util,也能处理 base64 形式的二维码图片。解析失败在业务上是常态而非异常,记得把 NotFoundException 当成一种正常返回来处理。
至此,二维码的生成与解析就都介绍完了,完整的生成侧定制能力请回顾前面几篇:基本姿势、logo/背景/前置图、矢量二维码与动态二维码。