找回密码
立即注册
搜索
发回帖 发新帖

6699

积分

0

好友

846

主题
发表于 6 小时前 | 查看: 1| 回复: 0

快递面单、门票、商品价签,还有登录页上的二维码,背后经常是同一套技术库。Java 生态里用得最多的是 ZXing,全称 Zebra Crossing,也就是“斑马线”。它出自 Google,采用 Apache 2.0 协议,至今仍是很多服务端生成和识别二维码、条码的常见方案。这次就来聊聊 ZXing 3.5 这个版本。

ZXing 3.5 Java 版技术封面:二维码生成、识别与 3.5.4 改动

目录

  • ZXing 拿到手里是什么样
  • 日常能碰到的码,它基本都覆盖
  • 一张图是怎么被读出来的
  • 3.5.4 这次到底改了什么
  • 先把依赖接上
  • 案例:生成一张门票二维码
  • 案例:把图里的内容读回来

ZXing 拿到手里是什么样

平时真正要引入的是两个包。

core 只负责码本身:把文字编成矩阵,或者把一块黑白图解析成字符串。它不认识 JPG、PNG,也不依赖 AWT。javase 则负责对接 BufferedImage,命令行小工具也在这个包里。如果你只是在内存里摆矩阵、自己负责画图,第二个包完全可以不引入。

所以 ZXing 的典型工作分成两头。一头是输入一段文字,输出二维码或条码;另一头是你已经有一张图,它告诉你图里写了什么、属于哪种码、码在哪个位置。批量打印门票、对账时读快递面单、测试时先编码再解码做闭环,都属于这类路子。

手机镜头实时扫码,系统相机扫码和 ML Kit 通常更省事,因为对焦、旋转、帧率都需要额外处理。ZXing 的 Java 版更适合“图已经在手里”的场景。官方这些年也淡化了 Android 示例的持续集成,服务端和普通 Java 程序才是 3.5 的主场。

ZXing 支持的条码与二维码演进示例:从条形码到二维码芯片

日常能碰到的码,它基本都覆盖

大家提到 ZXing,第一反应通常是二维码,但它能处理的码制其实宽得多。

方块和层叠类有 QR Code、Aztec、Data Matrix、PDF417、MaxiCode。商品包装上常见 EAN-8、EAN-13、UPC-A、UPC-E。物流和工业场景里还有 Code 39、Code 93、Code 128、ITF(交叉二五码)、Codabar,以及 RSS-14、RSS Expanded。

3.5.0 于 2022 年 5 月发布,Aztec、PDF417、Data Matrix 的解码都修过一轮,还加入了更省容量的 minimal 编码模式。后续 3.5.1 到 3.5.3 继续补边界:二维码在两类极端图片上不再长时间空转、Code 128 去掉了过严的长度限制、GS1 应用标识按 2023 年规范更新、RSS Expanded 减少了部分误识别。3.5.4 则是这条线的最新版本,整体方向仍是把旧坑填平。

ZXing 支持的一维条形码与二维码示例

上图只是帮你留下形状印象。真正的码以库实际绘制结果为准,静区、模块大小和纠错等级都会影响能否被读回。

一张图是怎么被读出来的

业务代码里基本就是四步。记住流程后,排错也有明确方向。

ZXing 二维码解码流程图:亮度图、二值化、MultiFormatReader

亮度图会把彩色或灰度图收成 0 到 255 的亮度值,0 是黑,255 是白。二值化再决定每个点最终算黑还是算白。照片类图片用 HybridBinarizer 比较稳;如果图片特别干净、几乎就是一张纯码,可以尝试 GlobalHistogramBinarizer,有时会更快。

最后一步不要什么码都试。面单场景里大概率是 Code 128 和 QR,用 POSSIBLE_FORMATS 收窄后,识别速度和误判率都会改善。TRY_HARDER 会花更多时间寻找难图,适合模糊、偏转的照片;干净的电子码不必每次都打开。

ZXing 纸质二维码扫描识别过程示意图

3.5.4 这次到底改了什么

相对 3.5.3,它没有新增码制。发布说明看着琐碎,实际都是“以前会踩、现在少踩”的修改。

灰度图可以直接送进去。 新增 GrayscaleLuminanceSource,构造方法是 GrayscaleLuminanceSource(width, height, pixels),像素按行排列。如果图片本来就是灰度,就不必再包一层 RGB。RGBLuminanceSource 改为它的子类,并补上了逆时针旋转 90 度。手机横拍的图可以在亮度图上旋转,少写一遍图像旋转逻辑。彩色像素还会先校验数组长度是否等于宽乘高,对不上就尽早失败,避免后面抛数组越界。

一张图里有多个二维码时,结果更完整。 QRCodeMultiReader 以前会丢掉符号标识 SYMBOLOGY_IDENTIFIER,与单个 QRCodeReader 的结果对不上。3.5.4 把这份元数据留了下来。对接 GS1,或者需要区分同一张图里不同码的标识时,这个字段会很有用。

PDF417 少了两个闷坑。 文本压缩模式 Compaction.TEXT 只接受码点 0 到 127 的字符,超出会抛出明确异常。这在以前可能生成一张看起来正常、内容却不对的码。另一个问题是,指定行列范围时内容明明放得下,却报 Unable to fit message in columns。issue 1831 中那种“把最小列数调大就突然能画”的情况,尺寸推算已经修正。

几种条码的识别更稳一点。 RSS Expanded 的读取去掉了一处低效的容器操作,批量扫码时少做无用功。ITF 放宽了 3 倍模块宽度的误差上限,印刷略歪的交叉二五码更容易读出来。二维码估算模块数时,合法边长必须是“除以 4 余 1”。以前只自动修正差 1 的结果,差 2 就放弃;3.5.4 会向下减 2,拉回合法尺寸。三个定位角点构成等腰直角三角形时,畸变计算也做了修正。

邮箱解析更严。 结果被当作邮箱时,格式检查收紧了,怪字符串不再轻易被判定为邮件地址。

这些改动都不是首页功能。常年跑批的人会在意:少一次越界,少一次 PDF417 死活画不出来,多个二维码时元数据还在。

先把依赖接上

<dependency>
    <groupId>com.google.zxing</groupId>
    <artifactId>core</artifactId>
    <version>3.5.4</version>
</dependency>
<dependency>
    <groupId>com.google.zxing</groupId>
    <artifactId>javase</artifactId>
    <version>3.5.4</version>
</dependency>

这一代按 Java 8 来用就行。几个包的版本号保持一致,避免 core 和 javase 各写各的。

案例:生成一张门票二维码

下面这段代码会输出 ticket-qr.png。纠错等级用 M,即使票面脏一点、有褶皱,也还有机会读回来。边距留 1 个模块,打印和展示都更稳妥。字符集指定 UTF-8,中文不会在生成阶段乱掉。

import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.WriterException;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import com.google.zxing.MultiFormatWriter;

import java.io.IOException;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;

/**
 * 门票二维码生成示例。
 */
public class TicketQrWriter {

    /**
     * 把门票信息写成一张 QR 图。
     *
     * @param file 输出的 PNG 路径
     */
    public static void write(Path file) throws WriterException, IOException {
        Map<EncodeHintType, Object> hints = new HashMap<>();
        hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
        hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
        hints.put(EncodeHintType.MARGIN, 1);

        String content = "门票:A-1024,入场:2026-11-02 17:25:17";
        BitMatrix matrix = new MultiFormatWriter().encode(
                content,
                BarcodeFormat.QR_CODE,
                360,
                360,
                hints);
        MatrixToImageWriter.writeToPath(matrix, "PNG", file);
    }
}

如果把 BarcodeFormat.QR_CODE 换成 BarcodeFormat.CODE_128,宽高改成细长条,就是物流单上常见的一维码。PDF417 则使用 BarcodeFormat.PDF_417。如果明确指定了 Compaction.TEXT,而内容里出现中文或其他非 ASCII 字符,3.5.4 会直接提示不可用。这种情况应改用自动或字节压缩,或先确认内容是否纯英文和数字。

边距不要为了“图小一点”就设成 0。码四周那圈空白叫静区,裁掉后即便生成成功,识别也经常失败。

案例:把图里的内容读回来

识别时我习惯把码制写死。全码制来回试也能工作,但业务里码的种类通常是已知的。

import com.google.zxing.BarcodeFormat;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.DecodeHintType;
import com.google.zxing.GrayscaleLuminanceSource;
import com.google.zxing.LuminanceSource;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.ReaderException;
import com.google.zxing.Result;
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
import com.google.zxing.common.HybridBinarizer;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Collections;
import java.util.EnumMap;
import java.util.Map;

/**
 * 从图片中读取二维码。
 */
public class TicketQrReader {

    /**
     * 读取 PNG 里的二维码文本。读不到时返回 null。
     *
     * @param file 待识别的图片
     * @return 码内文本;图糊、静区被裁掉或码制不对时返回 null
     */
    public static String read(Path file) throws IOException {
        BufferedImage image = ImageIO.read(file.toFile());
        LuminanceSource source = new BufferedImageLuminanceSource(image);
        BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));

        Map<DecodeHintType, Object> hints = new EnumMap<>(DecodeHintType.class);
        hints.put(DecodeHintType.CHARACTER_SET, "UTF-8");
        hints.put(DecodeHintType.POSSIBLE_FORMATS,
                Collections.singletonList(BarcodeFormat.QR_CODE));
        hints.put(DecodeHintType.TRY_HARDER, Boolean.TRUE);

        try {
            Result result = new MultiFormatReader().decode(bitmap, hints);
            return result.getText();
        } catch (ReaderException ex) {
            // 没找到、校验失败、格式不对,都算这张图没有读出来
            return null;
        }
    }
}

二维码通常自带编码信息,CHARACTER_SET 对一维码更关键。多个二维码要一次拿全,可以改用 QRCodeMultiReader,在 3.5.4 中每个结果的 SYMBOLOGY_IDENTIFIER 会保留在 result.getResultMetadata() 中。

如果图片已经是灰度字节,走新类即可。数组长度必须等于宽乘高。

/**
 * 用灰度像素解码,横拍时先逆时针转 90 度。
 */
public class GrayTicketDecoder {

    /**
     * @param pixels 按行排列的灰度值,0 为黑,255 为白
     * @param rotate 为 true 时逆时针旋转 90 度后再识别
     */
    public static LuminanceSource toSource(byte[] pixels, int width, int height, boolean rotate) {
        LuminanceSource source = new GrayscaleLuminanceSource(width, height, pixels);
        if (rotate) {
            source = source.rotateCounterClockwise();
        }
        return source;
    }
}

读失败在业务里单独分支就好,多半是图糊、反光、码制没配对,或者静区没了。把它记成系统异常,后面会很难查。




上一篇:多Agent协作的下一场竞争不在模型,而在共享上下文
下一篇:Leak Canary 实战:Android 内存泄露检测、报告与引用链定位
您需要登录后才可以回帖 登录 | 立即注册

关闭

站长推荐上一条 /1 下一条

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-11-11 17:06 , Processed in 0.075012 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表