文档扫描与图像处理

GS1 条码生成器:按场景拼出合规元素串,并给出应有的扫描结果

在线 GS1 条码生成器界面

GS1 标签做错的方式通常很隐蔽。校验位差一位,扫描器还是能读出字符串;FNC1 少了一个,批次号会把序列号一起吞掉;在 DataBar 上多挂一个 AI,编码器直接拒绝或者生成一个解出来和预期不同的符号。这些问题的共同点是“结果看起来正常”。在线 GS1 条码生成器的思路是:把校验规则前置到生成阶段,并且把“符合规范的扫描器应该返回什么”一并算出来,让一张标签图成为一个可核对的测试用例。

在线演示

在线 GS1 条码生成器(英文名 Online GS1 Barcode Generator):打开就能用,不需要注册,也不需要安装;元素串拼装、校验位计算和符号渲染都在浏览器内完成。

配合本文验证时可以这样走一遍:挑一个业务场景生成标签,先把右侧「扫描器应该返回什么」的对照表读一遍,再把导出的标签 PNG 拿到 GS1 扫描器里读回来,逐项比对元素串、AI 表和校验位判定。

关键要点

  • 唯一的第三方依赖是 bwip-js@4.11.4,没有识别 SDK、没有许可证、没有上传;生成器本身就是校验器。
  • 覆盖 12 种符号:DataBar 全向 / 截断 / 堆叠 / 堆叠全向 / 受限、DataBar Expanded、DataBar Expanded Stacked、GS1 DataMatrix、GS1 QR Code、GS1-128、ITF-14、EAN-13。
  • AI 表含 39 个条目,按用途分组(零售、追溯、日期、计量、物流、参与方、资产、内部),并以数据而非代码的方式生成 310394 计量家族与 391x/393x 货币家族。
  • 校验位自动计算:从校验位左边一位起按 3、1 交替加权(最右侧数据位权重 3),取 (10 − sum mod 10) mod 10
  • 每条诊断信息都带一键修正:+ 修正校验位+ 改用 GS1 DataMatrix+ 添加 AI 01(GTIN),从不静默改动输入。
  • 11 个真实场景在两轮随机化载荷下均通过 11/11 往返验证(生成 → 打印/导出 → 扫描 → 解析比对)。

从场景开始,而不是从 AI 开始

页面的第一个控件是场景下拉框,第二个是符号类型,两者联动。11 个场景与默认符号、默认 AI 组合如下:

场景 默认符号 默认 AI
零售单品——只带 GTIN GS1 DataBar 全向 01
生鲜计重——GTIN + 重量 + 价格 DataBar Expanded Stacked 01, 3103, 3922
医药单元——GTIN + 有效期 + 批次 + 序列号 GS1 DataMatrix 01, 17, 10, 21
物流集货箱——SSCC GS1-128 00
ITF-14 纸箱——只带 GTIN ITF-14 01
订单配送——GTIN、批次、订单、收货方 GS1-128 01, 10, 400, 410, 422
组合装——GTIN + 件数 + 保质期 DataBar Expanded 01, 30, 15
周转资产——GRAI GS1 DataMatrix 8003
标价包装——GTIN + 重量 + 欧元标价 DataBar Expanded 01, 3103, 3932
面向消费者码——GTIN + 批次 + 序列号 GS1 QR Code 01, 10, 21
自定义——自己拼 GS1 DataMatrix 01, 17, 10

注意医药单元与面向消费者码的 AI 组合完全相同,只是符号不同:前者用 DataMatrix(尺寸小、适合单支药盒),后者用 QR Code(普通手机相机可读)。这正说明“选哪种符号”往往取决于使用场景而不只是数据。

符号与数据的兼容规则

12 种符号里有 7 种是只能装 GTIN 的:DataBar 全向 / 截断 / 堆叠 / 堆叠全向 / 受限,以及 ITF-14 和 EAN-13。在上面挂第二个 AI 会直接报错:

DataBar、ITF-14 或 EAN-13 符号只能承载 GTIN,没有别的内容。多出的 1 个元素无法编码。

报错的同时会给出修复按钮“改用 GS1 DataBar Expanded Stacked”之类,而不只是告诉用户错了。DataBar Limited 还有一条额外约束:GTIN 必须以 01 开头(受限变体容量最小),不满足时提示改回全向变体。

DataBar Expanded 与 Expanded Stacked 不在“只能装 GTIN”之列,因为它们本身就能承载多个 AI。

编码器输入的两处特殊处理

对 10 种带 AI 语法的符号,交给 bwip-js 的 text带括号的人眼可读串 (01)…(10)…,由 BWIPP 自己决定 FNC1 的位置:

var options = {
  bcid: encoder.bcid,          // 例如 'gs1datamatrix'、'gs1_128'、'databarexpandedstacked'
  text: encoder.text,          // 带括号的 HRI 串
  scale: scale,
  includetext: false,
  paddingwidth: 4,
  paddingheight: 4
};
if (render.height) options.height = render.height;

EAN-13 与 ITF-14 是例外(aiSyntax: false)。EAN-13 接收 12 位数字让 BWIPP 算第 13 位校验;ITF-14 必须收到完整的 13 或 14 位 GTIN,绝不裁掉

// 裁成 13 位会让 BWIPP 对一段已经含校验位的数据再算一次校验位,
// 符号解出来的数字就和载荷声明的不一致了。

这是“测试素材生成器绝对不能出的那类错”——图上写着 A,扫出来是 B。

AI 长度与配对约束

每个 AI 在表里都声明了类型与长度,生成器的校验就建立在这上面:

类型 规则
gtin 14 位数字 + 校验位
sscc 18 位数字 + 校验位
gln 13 位数字 + 校验位
date 6 位数字,月份 01–12,日 00–31(00 表示当月最后一天)
decimal 定长数字,小数位数由 AI 第 4 位决定
country 3 位数字
text / number 不超过各自上限,且不含控制字符

除了逐项长度,还有两条配对约束,这两条是最容易被忽略、也最容易让扫描器报错的:

var PAIRING_RULES = [
    {
        match: function (ai) { return ai === '21'; },
        satisfiedBy: function (ais) {
            return ais.indexOf('01') !== -1 || ais.indexOf('03') !== -1 || ais.indexOf('8006') !== -1;
        },
        why: 'AI 21(序列号)标识的是某一个具体单品,因此必须带它所归属的贸易项目:AI 01、03 或 8006。',
        add: '01'
    }
];

规则里的 why 会直接显示给用户,add 则变成“添加 AI 01(GTIN)”按钮。第二条规则针对 391x/393x 这类带计价含义的 AI:它们必须和一个计件或计量 AI(3031nn/32nn/35nn/36nn)同时出现,否则价格没有分母。

FNC1 只加在该加的位置

元素串的显示形式用 | 标出 FNC1 应该出现的位置,而这个位置不是每个元素后面都有

function toDecodedText(rows) {
    var out = '';
    rows.forEach(function (item, index) {
        var entry = AI_BY_CODE[item.ai];
        out += item.ai + item.value;
        var isLast = index === rows.length - 1;
        if (!isLast && entry && !entry.fixed) out += FNC1;   // FNC1 = String.fromCharCode(29)
    });
    return out;
}

规则是:只有变长元素、且它后面还有元素时才需要分隔符。定长元素的长度是已知的,不需要分隔符;最后一个元素也不需要——读到结尾自然结束。页面上的说明写得很直接:“元素串里的 | 标出一个必须出现 FNC1 字节(0x1D)的位置,只有变长元素需要终止时才出现。”这是扫描器返回乱码最常见的单一原因,值得在生成阶段就讲明白。

“扫描器应该返回什么”对照表

每生成一个符号,右侧同步给出四组结果:

  1. 元素串(原始解码输出)——FNC1 位置以 | 标出。
  2. 人眼可读串(HRI)——(01)09506000123457(17)250430(10)LOT-4221 这种形式。
  3. 应用标识符表——四列:AI数据元扫描器读到的值校验位。变长字段会标注“(变长)”,校验位列显示“通过”“失败”或“—”(只有 GTIN、SSCC、GLN 参与校验,其它显示“—”)。
  4. GS1 Digital Link——以 018006 作主键,102122235400403 进路径,其余转查询参数;没有主键时明确写“没有 GTIN 或 ITIP,无法构造 Digital Link”。

日期在表里会换算成 ISO 形式(年窗口 yy <= 49 为 20xx,50–99 为 19xx,日 00 取当月最后一天),计量 AI 会补上单位或货币名称(内置 13 种货币代码)。

渲染:宁可重编码,也不缩小

符号宽度超出标签画框时,生成器用更小的整数倍重新编码,而不是把渲染结果等比缩小:

// 重新编码、而不是把成品图缩小——这才是重点。
// 按比例缩小线性符号会模糊模块边缘……

配套的还有画框留白:paddingwidth/paddingheight 设为 4 个模块。静区是规范的一部分而不是美观问题——符号贴着自己的边框,解码难度会明显上升。

如果为了塞进画框已经把模块尺寸压到 1 px,状态栏会给出明确警告:“模块尺寸已降到 1 px,请缩短载荷,给出一个相机能舒服读到的测试图。”另一个警告是符号被降采样的情况。所有这些状态都只影响预览与下载,不会让页面上的元素串和图片不一致——载荷有错时预览直接作废并标灰,而不是显示一张和载荷不匹配的图。

推荐的符号类型

页面内置了一套推荐逻辑,用 AI 组合反推符号:

条件 推荐符号
含 AI 00(SSCC) GS1-128
只有 GTIN,且是 13 位 EAN-13
只有 GTIN,其它情况 DataBar 全向
AI 少于 4 个,且不含 3103/3922/30 DataBar Expanded
3103/3922/30 DataBar Expanded Stacked
AI 不少于 4 个 GS1 DataMatrix
其它 GS1-128

当前选择与推荐不一致时,提示区会同时给出推荐项和理由,但不自动切换——符号选择是有业务含义的决定,工具只负责把取舍讲清楚。

用生成器验证扫描器

这两个工具是成对使用的:生成器按场景产出标签 PNG 与对照表,GS1 条码扫描器把图片读回来解析。11 个场景在两轮随机化载荷下都完成了往返验证,验证的是“生成器声明的结果”和“扫描器实际返回的结果”逐项一致——包括校验位判定和日期换算。

需要说清楚的是:生成的符号不构成真实性证明。GS1 载荷是明文,能被正确解析只说明数据格式合法。这些工具用于构造测试素材,不用于伪造或冒充真实商品标签。

常见问题

为什么生成器要同时给出“扫描器应该返回什么”?

因为一张标签图本身不是一个测试用例。把图片和“合规扫描器应返回的元素串、人眼可读串、AI 表、校验位判定”放在一起,才构成一个可核对、可回归的用例;否则你只能凭肉眼判断这次识别是不是对的。

DataBar 和 GS1-128 该选哪个?

只装 GTIN 的零售单元用 DataBar(全向、截断、堆叠、受限等变体);一个以上 AI 时用 GS1-128、GS1 DataMatrix 或 GS1 QR Code。生成器内置了推荐逻辑,选错符号时会给出“改用 XX”的一键修正,而不是只报一句错。

为什么代码里没有 GS1 Composite?

因为编码器 bwip-js 不提供 Composite 编码,而且 Composite 本质上是两个需要被读取器关联起来的符号,单张标签图无法完整表达它。生成器在说明里明确写出了这个缺口,而不是悄悄省略。

校验位是生成器算还是我自己填?

生成器自动计算并追加。GTIN-14、SSCC-18、GLN-13、GSRN-18 都按同一套 Mod-10 算法从右往左按 3、1 交替加权,你只需要填前面 12/17/12/17 位数字。

生成的条码能被哪些扫描器读取?

任何符合 GS1 规范的读取器都可以。数据本身是明文,所以“能被正确解析”只证明数据格式合法,不证明商品或标签真实——这个工具用于构造测试素材。

相关阅读