3.x二维码信息解析使用示例

一灰灰blogQuickMediaqrcode-plugin约 2150 字大约 7 分钟

前面几篇介绍的都是二维码的"生成",本篇来看一下它的另一半功能——二维码的"解析",也就是把一张二维码图片反解出里面塞的内容。

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_SETutf-8按照什么编码去还原内容,与生成时的编码必须匹配
TRY_HARDERtrue让解码器花更多精力去找二维码,提升识别成功率
PURE_BARCODEtrue声明图片是一张"纯净"的二维码,不做额外的容错识别

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 的 QrCodeGenWrapper Builder 提供了 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/背景/前置图、矢量二维码与动态二维码。

Loading...