博客

用 JSON 生成 0.1 mm 精度的 GS1-128 条码

GS1-128 看起来简单,直到 240 dpi 下扫描枪开始拒读。本文解释总长度精度、X-dimension、静区,以及 HTML/CSS 为什么很难稳定做到。

只要业务会发出实体货物,迟早都会遇到 GS1-128 条码:它必须在真实仓库灯光、真实距离、真实手持扫描枪上读得出来。这个问题看起来很无聊,但它经常是 PDF 生成里最吵、最难复现的故障之一。

这篇文章讲清楚“0.1 mm 精度”在 GS1-128 里到底指什么,为什么基于 HTML/CSS 的渲染链很难稳定交付这种几何精度,以及哪些设计规则能让 DHL、FedEx、USPS、Amazon 入库扫描都更少出错。

“条码精度”到底是什么

GS1-128(以前叫 UCC/EAN-128)靠一组严格比例的条宽和空隙宽度编码。最小单位是 X-dimension,也就是最窄条/最窄空隙的宽度。其他宽度都是 X 的倍数(Code 128 内部通常是 1X、2X、3X、4X)。

扫描枪看的不是“像不像条码”,而是相对宽度是否一致。生产里最常见的两个失败模式是:

  1. 同一个符号内部 X-dimension 不一致:渲染器对相邻条做了不同的亚像素取整,前几条是 8 px,中间突然出现 7 px,扫描枪看到的就是不一致的模式。
  2. 总长度或缩放错误:条码先被渲染,再被页面或打印链缩放,X-dimension 被压到 GS1 下限以下(1.0× 放大倍率下通常是 0.495 mm)。

症状往往一样:单张样品能扫,批量生产却有 1/30 被拒读。开发机上的扫描枪或手机相机会比仓库设备宽容得多,QA 很容易漏掉。

0.1 mm 规则

这里的精度不是“每一条都是 0.1 mm 宽”。条本身通常是 0.495 mm 或更宽。真正相关的是:条码整体长度相对规格目标的误差要在 0.1 mm 以内。

一个承载 18 位数字的典型 GS1-128:

  • 符号大约包含 120 个条和空隙
  • 1.0× 放大倍率下总长约 58 mm
  • 0.1 mm 总误差约等于整体 0.17% 精度
  • 平均到每条,预算大约是 0.001 mm,远小于单条宽度

所以,“应该是 7.4 px 的条被画成 7 px”会出问题。亚像素取整会在 120 个条/空隙上累积,通常到第 50 到第 80 个条之间,整体误差就已经超界。

HTML/CSS 为什么难

很多团队的路径是:把 GS1-128 数据编码成字符串,生成 SVG(甚至生成一堆 <div> 条),嵌进 HTML,再用 Puppeteer 或 Prince 输出 PDF。每一环都可能引入漂移。

1. 浏览器栅格化会取整

SVG 放在 HTML 里也会被浏览器绘制器做亚像素取整,除非 shape-rendering="crispEdges"、外层容器刚好落在整数像素边界、PDF DPI 又能整除条宽。这三个条件都很容易被一次普通改版打破。

2. CSS 布局会悄悄缩放

样式表里某个半年前为了解决别的布局问题加的 transform: scale(0.95),会无声地扭曲页面上所有条码。PDF 看起来没问题,扫描枪才会报错。

3. PDF 输出器还有自己的量化

浏览器把绘制结果写成 PDF 绘图指令时,可能会对内部坐标网格做吸附。条码坐标如果没有对齐这个网格,结果会“几乎正确”,但误差会累积。

4. 字体编码更危险

用 Code 128 字体直接打数据也不安全。字体虽然是矢量,但小尺寸 hinting 的目标是让人眼觉得更好看,它会移动宽度,刚好和扫描枪需要的几何一致性相反。

结构化渲染方式

gPdf 接收数据,根据 GS1-128 规格计算条/空隙模式,然后直接输出 PDF 矢量图元:不经过 HTML,不做 SVG 翻译,也不依赖字体 hinting。

{
  "pages": [{
    "size": "label_100_150",
    "elements": [
      {
        "type": "barcode",
        "format": "gs1128",
        "content": "(00)123456789012345678",
        "x": 4,
        "y": 8,
        "width": 58.0,
        "height": 18.0,
        "barcode_text": { "enabled": true, "position": "bottom" }
      }
    ]
  }]
}

barcode 元素来说,width整个符号的总长度,单位是 mm,也就是你能用卡尺在打印标签上量出来的长度。width: 58.0 带来的保证是:

  • 渲染器根据目标长度和符号条数计算 X-dimension。
  • 每个条都用完全相同的 X-dimension 绘制。
  • 宽度作为浮点坐标写入 PDF(PDF 单位是 1/72 inch,扫描分辨率下已经足够细)。
  • 没有字体 hinting、CSS 像素取整或布局缩放。

结果是在打印机不额外缩放的前提下,整体长度可以落在目标值 0.1 mm 以内。

实际应该怎么打印

三条规则可以挡掉大部分生产扫码故障。

规则 1:指定总长度,不要只指定 X-dimension

width 是正确旋钮,因为它能测量。用卡尺量打印标签就能直接验证。只指定 X-dimension 会让符号总长随编码数据长度变化,不同 SKU 的条码宽度不同,QA 更难。

  • 4×6 in 面单:页面宽约 100 mm,GS1-128 通常约 58–72 mm
  • 4×4 in 合规标签:约 45–58 mm
  • 2×1 in 箱标(Amazon UPC):不适合 GS1-128,应使用 UPC-A

规则 2:静区永远要留

GS1-128 两侧需要 ≥ 10X 静区。1.0× 放大倍率下(X = 0.495 mm),就是至少 4.95 mm 的纯白空间。经典错误是为了挤版面把条码放在 x: 0,扫描枪找不到起点。渲染器最好自动保留静区(gPdf 会这样做),但上线前仍要复核。

规则 3:用目标扫描枪测试

手机相机比 Honeywell 或 Zebra 工业扫描枪宽容。标准 QA 路径是:用真实生产打印机按生产速度打 50 张标签,用真实扫描枪按真实传送速度跑一遍。如果读码率低于 99%,通常就是 X-dimension 一致性有问题。

多格式现实

标签通常不只有 GS1-128:

Symbol 用途 规格来源
GS1-128 物流单元、GTIN + 序列号 + 批次 GS1 General Specifications
QR with FNC1 可移动端扫描的电商场景 ISO/IEC 18004
Data Matrix 药品(DSCSA / EU FMD) ISO/IEC 16022
PDF417 驾照、登机牌 ISO/IEC 15438
Aztec 交通票据 ISO/IEC 24778
MaxiCode UPS 专用 ISO/IEC 16023

只支持 GS1-128 的渲染器迟早会把你推向第二套工具。物流工作流几乎总会同时需要两种以上码制,所以 gPdf 把这些格式放在同一个渲染器里。

生产中遇到拒读时怎么排查

  1. 拿到失败样本:不要只看聚合指标,要拿到那张真实标签。
  2. 用卡尺测量:量总长度和 X-dimension,不在规格容差内就是渲染或打印链的问题。
  3. 看下方可读文本:有些扫描系统会尝试 OCR fallback;如果文字也不对,符号本身就坏了。
  4. 复核静区:量两侧白边,logo、分隔线或另一个条码太近都会出问题。
  5. 换扫描枪型号:A 型号能读、B 型号不能读时,问题是互操作,不只是渲染器。
  6. 对照已知正确标签:供应商通常会给精确规格样张,差异从视觉上也能倒推。

TL;DR

GS1-128 精度不是“条能印得多细”,而是 X-dimension 能不能在整个符号范围内保持一致,并让整体长度落在毫米级分数以内。HTML/CSS 渲染链会在多个阶段引入亚像素漂移;直接输出 PDF 矢量图元的结构化渲染器可以绕开这些漂移源。

如果当前 PDF 栈有 1–5% 的扫码拒读率,这就是一个很强的信号。可以在 Playground 里把 GS1-128 示例的 width 设成标签规格,打印后用卡尺直接验证。