Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第六章 条形码

条形码(Barcode)是一种将数据编码为机器可读图形符号的技术。它通过不同宽度的条(bar)和空(space)的组合来表示信息,广泛应用于零售、物流、图书管理、医疗等领域。本章将深入探讨条形码的编码原理、常见标准,并使用 Rust 实现条形码的生成与校验。

6.1 条形码的定义与历史

6.1.1 什么是条形码

条形码是一种光学机器可读的数据表示形式。它将字符集(通常是数字和字母)转换为一系列具有不同反射率的平行线条或图案。扫描设备通过检测条(暗色,低反射率)和空(亮色,高反射率)的宽度差异来解码信息。

从信息论的角度看,条形码是一种信道编码方案,它将源数据映射到适合光学传输的物理信号。编码过程需要满足以下约束:

  • 自同步性:扫描器能够从任意位置开始正确解码
  • 检错能力:能够检测常见的读取错误
  • 密度与可靠性平衡:在有限空间内存储尽可能多的信息,同时保证扫描成功率

6.1.2 发展历程

条形码的发展可追溯至20世纪中期:

  • 1948年:Norman Joseph Woodland 和 Bernard Silver 基于摩尔斯电码发明了最早的条形码概念,使用同心圆图案(牛眼码)
  • 1973年:美国统一代码委员会(UCC)采纳了 George Laurer 设计的 UPC(Universal Product Code) 标准,标志着现代条形码时代的开始
  • 1977年:欧洲成立了欧洲物品编码协会(EAN),推出了与 UPC 兼容的 EAN 编码体系
  • 1994年:日本 Denso Wave 公司发明了 QR Code,将条形码从一维扩展到二维,显著提升了信息容量

如今,条形码已成为全球供应链和零售行业的基础设施,每天被扫描数十亿次。

6.2 常见条形码类型

条形码主要分为一维码(线性条形码)和二维码两大类。它们在信息容量、编码字符集和应用场景上各有侧重。

6.2.1 EAN/UPC 系列

UPC-AEAN-13 是最常见的零售商品条形码。

  • UPC-A:12位数字,主要用于美国和加拿大
  • EAN-13:13位数字,是国际通用的商品条码标准
  • EAN-8:8位数字,用于小型包装

EAN-13 的结构如下:

| 前缀码 (2-3位) | 厂商代码 (4-5位) | 产品代码 (5位) | 校验码 (1位) |

例如,中国的前缀码为 690-699

6.2.2 Code 128

Code 128 是一种高密度的一维码,可以编码全部 128 个 ASCII 字符。它通过三种不同的字符集(Code A、Code B、Code C)灵活切换,在物流和工业领域广泛应用。

Code 128 的特点:

  • 支持数字、字母、标点符号和控制字符
  • 编码密度高,相同长度可存储更多字符
  • 包含校验码,可靠性好

6.2.3 Code 39

Code 39(又称 Code 3 of 9)是一种较早的条形码标准,每个字符由 9 个元素组成(5 条 + 4 空),其中 3 个是宽元素。

特点:

  • 支持 43 个字符:数字 0-9、大写字母 A-Z 和若干符号
  • 不需要校验码(但通常添加一位校验字符以增强可靠性)
  • 编码密度较低,但实现简单

6.2.4 QR Code

QR Code(Quick Response Code)是一种矩阵式二维码,与一维码有本质区别:

特性一维码QR Code
信息方向水平方向水平 + 垂直方向
容量约 20-30 个字符数千个字符
容错能力四个等级(L/M/Q/H)
中文支持不支持支持
读取方向需对齐360° 任意角度

QR Code 通过 Reed-Solomon 纠错码实现了即使部分图案损坏仍可读取的能力。其容量随版本号增加而增大,最高版本(Version 40)可存储约 7,089 个数字或 4,296 个字母数字字符。

6.3 条形码编码原理

6.3.1 条空宽度与二进制的对应

一维条形码本质上是一种**脉冲宽度调制(PWM)**信号。条和空的宽度对应不同的二进制序列。以 EAN-13 为例,每个数字由 7 个模块(module)组成,每个模块宽度相同,条表示二进制的 1,空表示二进制的 0

每个数字有两种编码模式:

  • 左侧奇校验(L-code):以空开始,以条结束
  • 左侧偶校验(G-code):以空开始,以条结束,但模式与 L-code 不同
  • 右侧(R-code):以条开始,以空结束,是 L-code 的反码

EAN-13 的数字编码表如下:

数字L-codeG-codeR-code
0000110101001111110010
1001100101100111100110
2001001100110111101100
3011110101000011000010
4010001100111011011100
5011000101110011001110
6010111100001011010000
7011101100100011000100
8011011100010011001000
9000101100101111110100

注意:这里的 10 分别表示条和空,实际绘制时需要考虑条空交替。

前缀第一位数字决定了左侧 6 位数字使用 L-code 还是 G-code 的组合,这一设计使得扫描器可以自动判断条码方向。

6.3.2 起始符、终止符与中间分隔符

EAN-13 条码还包含以下固定模式:

  • 起始符(Guard Bar)101,位于条码左侧
  • 中间分隔符(Center Guard)01010,位于条码中间
  • 终止符(Guard Bar)101,位于条码右侧

这些守卫条帮助扫描器校准读取位置,确保解码的准确性。

6.3.3 Code 128 的编码结构

Code 128 的每个字符由 11 个模块组成(终止符为 13 个),结构为:

| 起始符 | 数据字符1 | 数据字符2 | ... | 校验字符 | 终止符 |

Code 128 使用三种字符集:

  • Code A:ASCII 控制字符和大写字母
  • Code B:大写、小写字母和标点
  • Code C:仅数字,每两个数字用一个字符编码,密度翻倍

字符集切换通过特殊的切换字符(如 CODE BCODE C)实现,这类似于数据压缩中的模式切换。

6.4 校验码计算

6.4.1 EAN-13 校验码算法

EAN-13 的最后一位是校验码,用于检测输入或扫描错误。其计算步骤如下:

  1. 取前 12 位数字
  2. 从右向左,奇数位(第1、3、5…位)数字相加,乘以 3
  3. 从右向左,偶数位(第2、4、6…位)数字相加
  4. 将两个和相加,取结果的个位数
  5. 校验码 = $(10 - \text{个位数}) \bmod 10$

用数学公式表示:

$$ c = \left(10 - \left(\sum_{i=1}^{6} d_{2i-1} \times 3 + \sum_{i=1}^{6} d_{2i}\right) \bmod 10\right) \bmod 10 $$

其中 $d_1, d_2, \ldots, d_{12}$ 为前 12 位数字。

6.4.2 Rust 实现 EAN-13 校验码

fn calculate_ean13_check_digit(digits: &[u8; 12]) -> u8 {
    let mut sum = 0u32;
    for (i, &d) in digits.iter().enumerate() {
        // 从右向左,所以索引 0 对应最右边(奇数位)
        if i % 2 == 0 {
            sum += (d as u32) * 3;
        } else {
            sum += d as u32;
        }
    }
    let remainder = sum % 10;
    ((10 - remainder) % 10) as u8
}

fn validate_ean13(code: &str) -> bool {
    if code.len() != 13 || !code.chars().all(|c| c.is_ascii_digit()) {
        return false;
    }
    let digits: Vec<u8> = code.bytes().map(|b| b - b'0').collect();
    let input_check = digits[12];
    let computed = calculate_ean13_check_digit(
        &digits[..12].try_into().unwrap()
    );
    input_check == computed
}

fn main() {
    let digits = [6, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
    let check = calculate_ean13_check_digit(&digits);
    println!("校验码: {}", check); // 输出: 校验码: 2

    let full_code = "6901234567892";
    println!("{} 校验结果: {}", full_code, validate_ean13(full_code));
}

6.4.3 Code 128 校验码算法

Code 128 的校验码计算采用加权求和模 103 的方式:

  1. 起始符有一个数值(Code A = 103, Code B = 104, Code C = 105)
  2. 每个数据字符有一个对应的值(0-102)
  3. 校验码 = $(\text{起始符值} + \sum_{i=1}^{n} i \times \text{字符}_i\text{的值}) \bmod 103$
#![allow(unused)]
fn main() {
fn calculate_code128_check(start_value: u8, values: &[u8]) -> u8 {
    let mut sum = start_value as u32;
    for (i, &v) in values.iter().enumerate() {
        sum += ((i + 1) as u32) * (v as u32);
    }
    (sum % 103) as u8
}
}

6.5 使用 Rust 生成条形码

6.5.1 使用 barcode-rs 库

barcode crate 是 Rust 生态中成熟的条形码生成库,支持多种一维码格式。

Cargo.toml 中添加依赖:

[dependencies]
barcode = "0.1"
image = "0.24"

生成 Code 128 条形码的示例:

use barcode::sym::code128::Code128;
use barcode::generators::image::Image;
use barcode::generators::Generator;

fn main() {
    let data = "RUST-2024";
    let barcode = Code128::new(data).unwrap();
    let generator = Image::png();
    let bytes = generator.generate(&barcode).unwrap();
    std::fs::write("rust2024.png", bytes).unwrap();
    println!("条形码已生成: rust2024.png");
}

6.5.2 手动生成简单 Code 128

为了深入理解编码原理,下面手动实现一个简化的 Code 128 编码器,仅支持 Code B 字符集(ASCII 32-127)。

const CODE128_B_START: u8 = 104;
const CODE128_STOP: u8 = 106;

// Code B 字符值映射:空格(32) -> 0, '!'(33) -> 1, ..., 'Z'(90) -> 58, ...
fn char_to_code128_b(c: char) -> Option<u8> {
    let code = c as u8;
    if (32..=127).contains(&code) {
        Some(code - 32)
    } else {
        None
    }
}

fn code128_checksum(start: u8, values: &[u8]) -> u8 {
    let mut sum = start as u32;
    for (i, &v) in values.iter().enumerate() {
        sum += ((i + 1) as u32) * (v as u32);
    }
    (sum % 103) as u8
}

fn encode_code128_b(text: &str) -> Option<Vec<u8>> {
    let mut values = vec![CODE128_B_START];
    for c in text.chars() {
        values.push(char_to_code128_b(c)?);
    }
    let check = code128_checksum(CODE128_B_START, &values[1..]);
    values.push(check);
    values.push(CODE128_STOP);
    Some(values)
}

fn main() {
    let text = "HELLO";
    match encode_code128_b(text) {
        Some(encoded) => {
            println!("'{}' 的 Code 128 编码值: {:?}", text, encoded);
        }
        None => println!("包含不支持的字符"),
    }
}

6.5.3 将编码值渲染为位图

Code 128 每个字符由 11 个模块组成,条和空的模式由查找表确定。以下是将编码值渲染为简单文本表示的示例:

// Code 128 字符模式表(简化,仅展示部分)
// 每个字符由 11 个模块组成:1 表示条,0 表示空
const CODE128_PATTERNS: [&str; 107] = [
    "11011001100", "11001101100", "11001100110", // 0-2 (空格, !, ")
    // ... 省略中间条目
    "1100011101011", // STOP (106)
];

fn render_code128_bars(values: &[u8]) -> String {
    let mut result = String::new();
    for &v in values {
        let pattern = CODE128_PATTERNS.get(v as usize).unwrap_or(&"");
        result.push_str(pattern);
    }
    result
}

fn bars_to_ascii(bars: &str) -> String {
    bars.chars()
        .map(|c| if c == '1' { '█' } else { ' ' })
        .collect()
}

fn main() {
    let text = "HI";
    if let Some(values) = encode_code128_b(text) {
        let bars = render_code128_bars(&values);
        println!("{}", bars_to_ascii(&bars));
    }
}

实际应用中,应将条空模式渲染为 PNG、SVG 或位图格式,并确保条宽符合打印分辨率要求(通常最小条宽为 0.33mm)。

6.6 条形码的校验与识别

6.6.1 扫描解码流程

条形码扫描器的工作流程如下:

  1. 光学采样:激光或 LED 光源照射条码,光电传感器接收反射光
  2. 模拟-数字转换:将光强信号转换为电压波形
  3. 边缘检测:识别条与空之间的跳变沿
  4. 宽度测量:测量每个条和空的宽度(以模块为单位)
  5. 字符解码:根据编码表将模块序列映射为字符
  6. 校验验证:核对校验码,确认数据完整性

6.6.2 Rust 实现简单的条码校验

在实际系统中,接收到的条码数据应进行格式和校验码验证:

#[derive(Debug)]
enum BarcodeError {
    InvalidLength,
    InvalidCharacter,
    ChecksumMismatch,
}

struct Ean13(String);

impl Ean13 {
    fn new(code: &str) -> Result<Self, BarcodeError> {
        if code.len() != 13 {
            return Err(BarcodeError::InvalidLength);
        }
        if !code.chars().all(|c| c.is_ascii_digit()) {
            return Err(BarcodeError::InvalidCharacter);
        }
        let digits: Vec<u8> = code.bytes().map(|b| b - b'0').collect();
        let mut sum = 0u32;
        for (i, &d) in digits[..12].iter().enumerate() {
            if i % 2 == 0 {
                sum += (d as u32) * 3;
            } else {
                sum += d as u32;
            }
        }
        let check = ((10 - (sum % 10)) % 10) as u8;
        if digits[12] != check {
            return Err(BarcodeError::ChecksumMismatch);
        }
        Ok(Ean13(code.to_string()))
    }

    fn country_prefix(&self) -> &str {
        &self.0[..3]
    }

    fn as_str(&self) -> &str {
        &self.0
    }
}

fn main() {
    match Ean13::new("6901234567892") {
        Ok(ean) => {
            println!("有效的 EAN-13: {}", ean.as_str());
            println!("国家前缀: {}", ean.country_prefix());
        }
        Err(e) => println!("校验失败: {:?}", e),
    }
}

6.6.3 纠错与容错

一维码(如 EAN、Code 128)本身不具备纠错能力,仅能通过校验码检测错误。若校验失败,扫描器通常会要求重新扫描。

相比之下,QR Code 采用 Reed-Solomon 纠错码,可以在以下情况下恢复数据:

纠错等级最大恢复比例适用场景
L (Low)7%清洁环境
M (Medium)15%一般环境
Q (Quartile)25%较脏环境
H (High)30%极端环境

Reed-Solomon 码的数学基础是有限域(Galois Field)上的多项式运算。对于 QR Code,数据被分割为若干块,每块附加纠错码,使得即使部分模块损坏或遮挡,仍可通过解线性方程组恢复原始数据。

6.7 总结

本章系统介绍了条形码技术的核心概念与 Rust 实现方法。下表总结了各类条形码的关键特性:

特性EAN-13UPC-ACode 128Code 39QR Code
字符集数字数字全 ASCII43字符二进制数据
长度13位12位可变可变可变
校验模10模10模103可选Reed-Solomon
密度极高
方向敏感
纠错能力检错检错检错检错纠错
典型应用零售商品零售商品物流标签资产标签移动支付

条形码技术虽然看似简单,但其背后蕴含了信号处理、编码理论和纠错算法的深刻原理。从 UPC 到 QR Code,条形码的演进反映了信息存储密度与可靠性之间持续优化的过程。

练习

  1. 扩展 Ean13 结构体,添加解析厂商代码和产品代码的方法。
  2. 实现完整的 EAN-13 编码器,将 13 位数字转换为条空模块序列,并输出为 SVG 图形。
  3. 为 Code 128 实现 Code C 字符集支持,使连续数字对的编码密度提升一倍。
  4. 使用 image crate 将条形码模块序列渲染为 PNG 位图,支持配置条宽和高度。
  5. 研究 QR Code 的数据掩码(Data Masking)机制,理解其如何避免大面积同色块导致扫描困难。