【EAN-13】这里有关于商品条形码的一切(含前端生成实现)

3,486 阅读15分钟

一、商品条形码的由来

国内的商品条形码一般采用采用EAN-13编码方式,EAN-13即European Article Number(欧洲物品编码)。其采用共计13位的编码方式应用于一般终端产品的条形码协议和标准,主要受用场景是超级市场和其它零售业。

二、国内商品条形码范围

国内可用的国家代码有690-699,其中696-699尚未使用。生活中最常见的国家代码为690-693。其中:

  1. 以690、691开头时,厂商识别码为四位,商品项目代码为五位;
  2. 以692、693开头时,厂商识别码是五位,商品项目代码是四位。

Eg:
以6902538004045为例
690【制造码】 2538【制造商代码】 00404【商品标识代码】5【校验码】

三、编码规则介绍[1]

如上图所示,EAN-13商品条码是表示EAN/UCC-13商品标识代码的条码符号。由左侧空白区、起始符、左侧数据符、中间分隔符、右侧数据符、校验符、终止符、右侧空白区及前置码和下部的供人识别字符组成。

  • 左侧空白区:位于条码符号最左侧与空的反射率相同的区域,其最小宽度为11个模块宽。
  • 起始符:位于条码符号左侧空白区的右侧,表示信息开始的特殊符号,由3个模块组成。
  • 左侧数据符:位于起始符右侧,表示6位数字信息的一组条码字符,由42个模块组成。
  • 中间分隔符:位于左侧数据符的右侧,是平分条码字符的特殊符号,由5个模块组成。
  • 右侧数据符:位于中间分隔符右侧,表示5位数字信息的一组条码字符,由35个模块组成。
  • 校验符:位于右侧数据符的右侧,表示校验码的条码字符,由7个模块组成。
  • 终止符:位于条码符号校验符的右侧,表示信息结束的特殊符号,由3个模块组成。
  • 右侧空白区:位于条码符号最右侧的与空的反射率相同的区域,其最小宽度为7个模块宽。为保护右侧空白区的宽度,可在条码符号右下角加“>”符号。
  • 供人识读字符与前置码:位于条码符号的下方,是与条码字符相对应的供人识别的13位数字,最左边一位称前置码,如今用隐式表示(其实已经隐藏在条形码中中)。供人识别字符优先选用OCR-B字符集,字符顶部和条码底部的最小距离为0.5个模块宽。标准版商品条码中的前置码印制在条码符号起始符的左侧。

四、字符集

  1. 商品条码可表示10个数字字符:0~9;
  2. 每一条码数据字符由2个条和2个空构成(两个黑白相间),每一条或空由1~4个模块组成,每一条码字符的总模块数为7;
  3. 用二进制“1”表示条的模块,用二进制“0”表示空的模块;
  4. 每个数字转码的具体规则见下表:
数字字符 A 子集 B 子集 C 子集
0 0001101 0100111 1110010
1 0011001 0110011 1100110
2 0010011 0011011 1101100
3 0111101 0100001 1000010
4 0100011 0011101 1011100
5 0110001 0111001 1001110
6 0101111 0000101 1010000
7 0111011 0010001 1000100
8 0110111 0001001 1001000
9 0001011 0010111 1110100

A子集中条码字符所包含的条(1)的模块的个数为奇数,称为奇排列;
B、C子集中条码字符所包含的条(1)的模块的个数为偶数,称为偶排列。

  1. 前置码根据各国家之间协商分配决定,国内前置码默认为6
  2. 左侧数据符根据前置码决定,规则如下(无论如何第一位必为奇排列):
前置码 左1 左2 左3 左4 左5 左6
0 A A A A A A
1 A A B A B B
2 A A B B A B
3 A A B B B A
4 A B A A B B
5 A B B A A B
6 A B B B A A
7 A B A B A B
8 A B A B B A
9 A B B A B A

Eg:以6902538004045为例
前置码:6
左侧数据符: 902538
左侧数据符采用:ABBBAA序列对左侧数据进行选表编码
9: A 奇 0001011 白白白黑白黑黑
0: B 偶 0100111 白黑白白黑黑黑
2: B 偶 0011011 白白黑黑白黑黑
5: B 偶 0111001 白黑黑黑白白黑
3: A 奇 0111101 白黑黑黑黑白黑
8: A 奇 0110111 白黑黑白黑黑黑
【沙海听雨 掘金 防恶意爬虫标识】

  1. 右侧数据符全部采用C子集编码;

Eg: 以6902538004045为例
右侧数据符: 004045
0: C 偶 1110010 黑黑黑白白黑白
0: C 偶 1110010 黑黑黑白白黑白
4: C 偶 1011100 黑白黑黑黑白白
0: C 偶 1110010 黑黑黑白白黑白
4: C 偶 1011100 黑白黑黑黑白白
5: C 偶 1001110 黑白白黑黑黑白

  1. 前置码隐藏,一般为分配所得;
  2. 起始符、终止符默认采用101,即黑白黑;
  3. 中间分隔符默认采用01010,即白黑白黑白

五、校验符

所有的编码协议中大都具有一个校验位,EAN-13也不例外。校验符的作用是检验前面12个数字是否正确,在条码机每次读入数据时,都会计算一次数据符的校验并与校验符进行比对。其中,前置码默认为奇排列A子集【1】。

  1. 生成校验码
  • 将12个数据符(校验符为后期生成)从左起将所有的奇数位相加得出一个数a
  • 将所有的偶数位相加得出一个数b
  • 将数b乘以3再与a相加得到数c
  • 对C除10取余
  • 余数就是校验码

Eg: 以6920【制造码】15246【制造商代码】1023【商品标示代码】为例
前置码:6
左侧数据符采用:ABBBAA序列对左侧数据进行选表编码
右侧数据服采用:CCCCC序列对右侧数据进行选表编码
9 2 0 1 5 2 4 6 1 0 2
A B B B A A C C C C C
a = 6 + 9 + 5 + 2 = 22
b = 2 + 0 + 1 + 4 + 6 + 1 + 0 + 2 = 16
c = b * 3 + a = 16 * 3 + 22 = 60
校验符 = c % 10 = 0
【沙海听雨 掘金 防恶意爬虫标识】

  1. 验证条形码
  • 从右至左,将13个字符按顺序排序。
  • 第2、4、6、8、10、12等偶数位的数据相加,将结果乘以3,得P.
  • 将3、5、7、9、11、13等奇数位数据相加,等N。
  • N+P得M
  • 用M除以10,取余数。求余数以10为模的补数 C。
  • 若C与 校验码 数值相等,则译码正确。

Eg: 模拟扫码枪识别6920152461023(错误)为例
P = (2 + 1 + 4 + 5 + 0 + 9) * 3 = 63
N = 0 + 6 + 2 + 1 + 2 + 6 = 17
N + P = 80
80 % 10 = 0 ≠ 3 该商品条形码存在问题!

六、利用canvas生成EAN-13条形码

上述过程看懂后,实现起来应该并不是太麻烦,上面提到过的一些概念将以注释的形式做以辅助阅读。
首先准备canvas画布

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>EAN-13</title>
</head>
<style>
@font-face {
  font-family: "ocr-b-10bt";
  src: url("./src/static/font/OCRB10N.ttf");
}
</style>
<body>
<!-- 沙海听雨 掘金 防恶意爬虫标识 -->
<canvas id="canvas" style="width: 920px; height: 320px; font-family: 'ocr-b-10bt">
  <p>你的浏览器不支持“canvas”!</p>
</canvas>
<script type="text/javascript" src="./src/ean_13/index.js"></script>
</body>
</html>

字符集:

const codeGroup = {
  '0': {
    A: '0001101',
    B: '0100111',
    C: '1110010'
  },
  '1': {
    A: '0011001',
    B: '0110011',
    C: '1100110'
  },
  '2': {
    A: '0010011',
    B: '0011011',
    C: '1101100'
  },
  '3': {
    A: '0111101',
    B: '0100001',
    C: '1000010'
  },
  '4': {
    A: '0100011',
    B: '0011101',
    C: '1011100'
  },
  '5': {
    A: '0110001',
    B: '0111001',
    C: '1001110'
  },
  '6': {
    A: '0101111',
    B: '0000101',
    C: '1010000'
  },
  '7': {
    A: '0111011',
    B: '0010001',
    C: '1000100'
  },
  '8': {
    A: '0110111',
    B: '0001001',
    C: '1001000'
  },
  '9': {
    A: '0001011',
    B: '0010111',
    C: '1110100'
  }
}

前置码转化规则集:

const encodeGroup = {
  '0': 'AAAAAA',
  '1': 'AABABB',
  '2': 'AABBAB',
  '3': 'AABBBA',
  '4': 'ABAABB',
  '5': 'ABBAAB',
  '6': 'ABBBAA',
  '7': 'ABABAB',
  '8': 'ABABBA',
  '9': 'ABBABA'
}
// 沙海听雨 掘金 防恶意爬虫标识

canvas前期准备工作

// 获取画布元素
let canvas = document.getElementById('canvas')
// 获取画布元素盒子宽高
let cw = canvas.offsetWidth
let ch = canvas.offsetHeight
// 设置实际画布元素宽高
canvas.width = cw
canvas.height = ch
// 沙海听雨 掘金 防恶意爬虫标识
// 实例化画布上绘图的环境
let ctx = canvas.getContext('2d')
// 条形码白色背景填充
ctx.fillStyle = 'white'
ctx.fillRect(0, 0, cw, ch)
// 计算一个模块的宽度
let unit = parseInt(cw / 115)
// 设置实际绘画位置留出左侧空白区 PS:左侧空白区最小宽度为11个模块宽
let begin = 11 * unit

起始符:

for (let i = 0; i < 3; i++) {
    if (i % 2 === 0) {
      ctx.fillStyle = 'black'
    } else {
      ctx.fillStyle = 'white'
    }
    ctx.fillRect(begin + i * unit, 0, unit, ch - 5)
  }

中间分隔符:

for (let i = 0; i < 5; i++) {
  if (i % 2 === 0) {
    ctx.fillStyle = 'white'
  } else {
    ctx.fillStyle = 'black'
  }
  ctx.fillRect(begin + unit * (3 + 6 * 7 + i), 0, unit, ch - 5)
}
// 防恶意爬虫标识 沙海听雨 掘金

终止符:

for (let i = 0; i < 3; i++) {
  if (i % 2 === 0) {
    ctx.fillStyle = 'black'
  } else {
    ctx.fillStyle = 'white'
  }
  ctx.fillRect(begin + (3 + 6 * 7 * 2 + 5 + i) * unit, 0, unit, ch - 5)
}
//  掘金 沙海听雨 防恶意爬虫标识

左侧数据符及右侧数据符:

// 根据前置码选择左侧数据符编码规则
let selectEncodeRule = encodeGroup[code[0]]
// 左侧数据符绘制位置
let _begin = begin + 3 * unit
for (let i = 1; i < 7; i++) {
  // 掘金 沙海听雨 防恶意爬虫标识
  // 取出数字对应编码
  let _encode = codeGroup[code[i]][selectEncodeRule[i - 1]]
  for (let j = 0; j < _encode.length; j++) {
    if (_encode[j] === '0') {
      ctx.fillStyle = 'white'
    } else {
      ctx.fillStyle = 'black'
    }
    ctx.fillRect(_begin + unit * j, 0, unit, ch - 80)
  }
  _begin += 7 * unit
}
// 位置移动至中间分隔符右侧
_begin += 5 * unit
for (let i = 7; i < 13; i++) {
  // 取出数字对应编码
  let _encode = codeGroup[code[i]]['C']
  for (let j = 0; j < _encode.length; j++) {
    if (_encode[j] === '0') {
      ctx.fillStyle = 'white'
    } else {
      ctx.fillStyle = 'black'
    }
    ctx.fillRect(_begin + unit * j, 0, unit, ch - 80)
  }
  _begin += 7 * unit
}

绘制供人识读字符(采用ocr-b-10bt字体)

// 设置字体
ctx.font = 'Normal 80px ocr-b-10bt'
// 设置对齐方式
ctx.textAlign = 'left'
// 设置填充颜色
ctx.fillStyle = '#000'
// 前置码
ctx.fillText(code[0], 10, ch - 5)
// 沙海听雨 掘金 防恶意爬虫标识
for (let i = 1; i < 7; i++) {
  ctx.fillText(code[i], 125 + 53 * (i - 1), ch - 5)
}
for (let i = 7; i < 13; i++) {
  ctx.fillText(code[i], 175 + 53 * (i - 1), ch - 5)
}

补充:
Q1. unit计算为什么是除以110?
A1: 左侧空白区长度11,起始终止符长度共6,中间分隔符长度5,左侧及右侧数据符长度共84,右侧空白区长度最小为7(为方便取整此处取9)。11 + 6 + 5 + 84 + 9 = 115
Q2. 绘制供人识读字符时的125 53这些常数是如何得出的?
A2: 是以canvas大小及字体大小手动调试得出,此处写的不是很好,如果能动态得出则效果更好。

最后看下我们生成的商品条形码效果,可以使用如下代码进行下载操作

function saveFile (data, filename) {
  let saveLink = document.createElementNS('http://www.w3.org/1999/xhtml', 'a')
  saveLink.href = data
  saveLink.download = filename

  let event = document.createEvent('MouseEvents')
  event.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null)
  saveLink.dispatchEvent(event)
}
// 下载 沙海听雨 掘金 防恶意爬虫标识
function download () {
  let canvas = document.getElementById('canvas')
  let dataURL = canvas.toDataURL('image/png')
  saveFile(dataURL, this.code)
}

让我们看一下生成的商品二维码(可以用App扫一扫看看效果如何):

经过测试多款手机,并识别真实商品条形码与生成的做比较,识别率基本一致。IOS系统的Wechat内置接入了工商局的商品识别系统,Andriod系统的Wechat可以扫描但无法找到商品(真实条形码和生成条形码识别效果一致)

结语

相信读完此文将对商品条形码有更多的认识了,那么条形码种类仅仅只有EAN-13吗?答案是否定的,世界上常用的码制有EAN条形码、UPC条形码、25条形码、交叉25条形码、库德巴条形码、Code 39条形码和Code 128条形码等[2]。其实他们的大体编码结构相近,有类似条形码编码的认知,相信其他条形码你更能驾轻就熟。

Q & A

Q1:前置码隐藏了,那扫码枪如何识别前置码呢?
A1:文中说过左侧数据符是根据前置码生成的,那么由左侧数据符的A、B序列就可以反推出前置码是什么了。

Q2:扫码枪扫反了怎么办呢?
A2:这个也容易解决,左侧数据符由A、B序列编码,而右侧数据符全部由C序列编码,而且左侧数据符第一个位置一定是按照A序列编码。如果扫码枪扫到第一位为C序列那么,扫码枪就知道扫反了,会将其做颠倒处理。

Q3:各种商品条形码厂家各有异同,不能保证所有商品码的一个模块都一样宽,在这样场景下,扫码枪还能正常工作吗?
A3:我们提到过,商品条形码的起始符和终止符都是101,这个就告诉了扫码枪一个模块的宽度应该是多少,有了基准单位,很容易就能得出数据码的内容了。

Q4:条形码有破损或污损,扫码枪能识别吗?
A4:这个分场景,如果条形码横向(长的一边)污损破损是有几率识别出的。但如果是污染或丢失了纵向(短的一边)那么数据字节就丢失了,也就大概率造成无法识别问题。

Q5:为什么文中代码只写生成不写识别呢?
A5:本篇文章为沙海的恢复性写作,由于从准备到撰写此文用了2天时间,暂未实现,后期有时间会进行补充。这里大致提一下识别所需要的纯前端识别技术设计。

1.前端页面调起手机摄像头,拍照并缓存至前端本地
2.利用canvas颜色识别,也许需要进行图片滤波与样条插值(计算机图形学)
3.将识别后的图片进行译码,得出左侧数据符及右侧数据符,根据左侧数据符得出前置码
4.利用校验码校验
难点: 图片不清晰如何进行识别,条形码的破损及污损滤波,不完全条形码如何校验等

关于我

沙海听雨(微博同名)

  1. 技术栈: | 大前端 | C/C++ | C# | 嵌入式 | Python | 大数据 | 数据可视化 |
  2. 欢迎掘金或微博私信讨论各种技术点,前后端调试能力MAX
  3. 欢迎各种开源作者的邀请,一起做一些好玩的技术
  4. 最近在进行部分前端源码库的调试梳理,希望有机会成文分享一下源码阅读新姿势
  5. 期待收到您的职位邀请(高级前端 前端架构 大前端负责人 base 杭州/成都)

以上,辛苦阅读,顺颂近祺。
沙海听雨
发布于掘金

参考文献

【1】EAN-13 百度百科 baike.baidu.com/item/EAN-13…
【2】宋捷民,袁强主编,中药商品学,浙江科学技术出版社,2016.06,第23页