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

第七 二维码

二维码源于日本,在中国发扬光大。

二维码相较于条形码,能够存储的信息更多,同时也能快速完成短信息的识别输入。从移动支付到健康码,从商品溯源到电子名片,二维码已经深入到我们生活的方方面面。

7.1 二维码的历史

7.1.1 QR码的诞生

1994年,日本 Denso Wave 公司的工程师原昌宏(Masahiro Hara)发明了 QR Code(Quick Response Code)。当时,丰田汽车旗下的零部件供应商需要一种能够快速扫描并存储大量信息的条码系统来追踪汽车零部件。传统的条形码只能存储少量数字信息,且只能单向扫描,无法满足工业需求。

原昌宏从围棋棋盘的网格结构中获得灵感,设计了这种可以在水平和垂直两个方向同时存储信息的矩阵式二维码。QR Code 的“Quick Response“(快速响应)之名,正是强调了其高速读取的特性。

QR Code 的关键创新:

  • 高速读取:通过定位图案实现 360 度任意方向扫描
  • 大容量:相比一维条码,信息容量提升数十倍
  • 容错能力:即使部分损坏仍可正确读取
  • 支持多种数据类型:数字、字母、汉字、二进制数据

7.1.2 在中国的发扬光大

QR Code 在日本发明后,最初主要用于工业领域。然而,真正让二维码“火遍全球“的是中国。

  • 2011年:支付宝推出二维码支付,开创了移动支付的先河
  • 2014年:微信支付全面接入二维码,“扫码支付“迅速普及
  • 2020年:健康码系统使用二维码进行疫情防控,几乎全民使用
  • 如今,中国已成为全球二维码应用最广泛的国家,日均扫码量超过百亿次

7.2 QR码原理详解

7.2.1 QR码的结构

一个标准的 QR 码由以下功能区域组成:

┌─────────────────────────────┐
│ ■ ■ ■ ■ ■ ■ ■   ■         │  定位图案(左上)
│ ■       ■         ■         │
│ ■       ■         ■         │  定位图案(右上)
│ ■ ■ ■ ■ ■ ■ ■   ■ ■ ■ ■ ■ ■ ■ ■
│                     ■       │
│ ■ ■ ■ ■ ■ ■ ■     ■       │  定位图案(左下)
│ ■       ■           ■       │
│ ■       ■           ■       │
│ ■ ■ ■ ■ ■ ■ ■   ■         │
│                     ■       │
│   ■   ■   ■   ■   ■   ■   ■  ← 定时图案(水平和垂直)
│                     ■       │
│   [格式信息]  [版本信息]     │
│   [数据和纠错码字]           │
│   [剩余位]                  │
└─────────────────────────────┘

各区域说明

区域位置功能
定位图案 (Finder Pattern)三个角落7x7 的嵌套正方形,帮助扫描器定位 QR 码的位置和方向
定时图案 (Timing Pattern)连接定位图案交替的黑白模块,帮助确定模块坐标
格式信息 (Format Info)定位图案旁纠错等级和掩码图案编号
版本信息 (Version Info)较大版本中QR 码版本号(1-40)
数据和纠错码字主体区域编码后的数据和纠错码
静区 (Quiet Zone)四周空白边框,宽度至少 4 个模块

7.2.2 编码模式

QR 码支持四种编码模式,根据数据内容自动或手动选择最优模式:

模式名称适用字符编码效率
0001数字模式 (Numeric)0-9最高(3个数字→10bit)
0010字母数字模式 (Alphanumeric)0-9, A-Z, 空格, $%*+-./:较高(2个字符→11bit)
0100字节模式 (Byte)ISO 8859-1 字符集(含中文UTF-8)中等(1字节→8bit)
1000汉字模式 (Kanji)Shift-JIS 编码的日文汉字较高(1个汉字→13bit)

数字模式的编码方式:将数字分为 3 位一组,每组转换为 10 位二进制数。例如数字 123456

$$123 \rightarrow 0001111011_2$$ $$456 \rightarrow 0111001000_2$$

字母数字模式的字符集包含 45 个字符:

0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:

将字符分为 2 个一组,每组转换为 11 位二进制数:

$$\text{value} = c_1 \times 45 + c_2$$

7.2.3 纠错等级

QR 码使用 Reed-Solomon 纠错编码,提供四个纠错等级:

等级代号纠错容量可恢复数据比例适用场景
LLow约 7%数据的 7% 可被恢复数据密度优先,环境良好
MMedium约 15%数据的 15% 可被恢复通用场景(默认)
QQuartile约 25%数据的 25% 可被恢复需要较高容错
HHigh约 30%数据的 30% 可被恢复带Logo的二维码、恶劣环境

纠错等级越高,可用数据容量越小,但容错能力越强。例如,Version 1 的 QR 码在不同纠错等级下的数据容量:

纠错等级数字字母数字字节汉字
L41251710
M3220148
Q2716117
H171074

7.2.4 数据编码流程

QR 码的完整编码流程如下:

┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐
│ 1.数据分析 │ → │ 2.数据编码 │ → │ 3.纠错编码 │ → │ 4.数据排列 │ → │ 5.掩码处理 │
│          │    │          │    │          │    │          │    │          │
│ 确定编码  │    │ 将数据转换 │    │ 生成Reed- │    │ 将码字放入 │    │ 应用掩码  │
│ 模式和版本│    │ 为位流    │    │ Solomon码 │    │ QR码矩阵  │    │ 优化图案  │
└──────────┘    └──────────┘    └──────────┘    └──────────┘    └──────────┘

步骤详解

  1. 数据分析:确定最优编码模式、版本号和纠错等级
  2. 数据编码:添加模式指示符和字符计数指示符,将数据转换为位流,添加终止符和填充位
  3. 纠错编码:将数据分为若干块,对每块生成 Reed-Solomon 纠错码字
  4. 数据排列:按照蛇形路径将数据码字和纠错码字放入 QR 码矩阵
  5. 掩码处理:尝试 8 种掩码图案,选择使评分最优的一种

7.2.5 Reed-Solomon 纠错算法

Reed-Solomon(RS)编码是 QR 码容错能力的核心。它属于分组纠错码,基于有限域(Galois Field, GF)上的多项式运算。

基本原理

设数据码字为 $d_0, d_1, \ldots, d_{n-1}$,构造数据多项式:

$$D(x) = d_0 + d_1 x + d_2 x^2 + \cdots + d_{n-1} x^{n-1}$$

纠错码字通过对 $D(x)$ 在特定点求值得到:

$$c_i = D(\alpha^i) \mod p(x)$$

其中 $\alpha$ 是有限域 $GF(2^8)$ 的本原元,$p(x)$ 是不可约多项式:

$$p(x) = x^8 + x^4 + x^3 + x^2 + 1$$

解码过程

当 QR 码部分损坏时,接收端通过求解方程组恢复原始数据。设接收到的码字为 $r_0, r_1, \ldots, r_{n+k-1}$(含 $k$ 个纠错码字),定义错误多项式和伴随式(Syndrome),通过 Berlekamp-Massey 算法或 Euclidean 算法求解错误位置和错误值。

7.2.6 版本与容量

QR 码共有 40 个版本(Version 1 到 Version 40),版本号与模块数的关系为:

$$\text{模块数} = 4 \times \text{版本号} + 17$$

版本模块数数据容量(字节模式,纠错等级L)数据容量(数字模式,纠错等级L)
121 x 2117 字节41 位数字
537 x 3777 字节185 位数字
1057 x 57174 字节419 位数字
2097 x 97589 字节1,423 位数字
30137 x 1371,256 字节3,041 位数字
40177 x 1772,953 字节7,089 位数字

Version 40 的 QR 码最大可存储 7,089 个数字字符或 2,953 个字节(约 2.9 KB)。

7.3 其他二维码标准

除了 QR Code,还有多种二维码标准,各有特色:

7.3.1 Data Matrix

Data Matrix 由 International Data Matrix 于 1994 年开发,广泛应用于工业标记和电子元器件标识。

特性说明
外观方形或矩形,由黑白模块组成
最大容量2,335 个数字字符
容错Reed-Solomon 纠错
特点无需静区(利用 L 形边界定位)
应用电子元器件、医疗器械标记

7.3.2 PDF417

PDF417(Portable Data File 417)由 Symbol Technologies 于 1991 年开发,是一种堆叠式二维码。

特性说明
外观多行堆叠的一维条码
最大容量1,850 个数字字符
容错两个纠错等级
特点可打印在证件上,美国驾照标准
应用身份证件、运输标签、不动产

7.3.3 Aztec Code

Aztec Code 由 Andrew Longacre 于 1995 年发明,中心有一个定位图案。

特性说明
外观方形,中心有定位图案(类似“牛眼“)
最大容量3,832 个数字字符
容错Reed-Solomon 纠错,可调比例
特点不需要静区,尺寸更紧凑
应用航空 boarding pass、交通票务

7.3.4 二维码标准对比

特性QR CodeData MatrixPDF417Aztec Code
类型矩阵式矩阵式堆叠式矩阵式
最大容量7,089 数字2,335 数字1,850 数字3,832 数字
容错等级4级(L/M/Q/H)可调2级可调比例
需要静区
读取速度最快中等
中文支持
主要应用移动支付、广告工业标记证件交通票务

7.4 QR码安全

7.4.1 恶意二维码

二维码本身不携带恶意代码,但可以被用来引导用户访问恶意网站。常见的攻击方式:

  • 钓鱼攻击:将二维码指向伪造的登录页面,窃取用户账号密码
  • 恶意软件下载:扫描后自动下载并安装恶意应用
  • 中间人攻击:通过二维码劫持通信会话
  • 虚假支付:将收款二维码替换为攻击者的收款码

7.4.2 防护建议

  1. 扫码前预览:使用支持 URL 预览的扫码工具,查看目标地址后再决定是否访问
  2. 使用安全扫码工具:选择带有安全检测功能的扫码应用
  3. 注意 URL 来源:确认链接域名是否为官方域名
  4. 不要随意扫码:来源不明的二维码不要扫描
  5. 开启支付验证:移动支付应设置密码或生物识别验证

7.5 Rust 二维码实战

7.5.1 环境准备

Cargo.toml 中添加依赖:

[dependencies]
qrcode = "0.14"
image = "0.25"

7.5.2 生成基本二维码

use qrcode::QrCode;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 生成二维码
    let code = QrCode::new("https://www.rust-lang.org")?;

    // 在终端打印二维码(ASCII 形式)
    println!("{}", code.render::<char>()
        .quiet_zone(false)
        .module_dimensions(2, 1)
        .build());

    // 输出二维码的基本信息
    println!("版本: {}", code.version());
    println!("宽度: {} 模块", code.width());

    Ok(())
}

7.5.3 生成二维码图片

#![allow(unused)]
fn main() {
use image::{GenericImage, ImageBuffer, Rgba};
use qrcode::QrCode;

fn generate_qr_image(data: &str, size: u32) -> ImageBuffer<Rgba<u8>, Vec<u8>> {
    // 1. 编码数据生成二维码
    let code = QrCode::new(data.as_bytes()).unwrap();

    // 2. 设置二维码的模块大小,并添加白色边框
    let border = 4;
    let module_size = (size as f32 / (code.width() as f32 + 2.0 * border as f32)).ceil() as u32;

    // 3. 将二维码渲染为图像
    code.render::<Rgba<u8>>()
        .min_dimensions(size, size) // 设置最小尺寸,确保清晰
        .quiet_zone(true) // 添加静区(白色边框)
        .module_dimensions(module_size, module_size)
        .build()
}
}

7.5.4 自定义颜色的二维码

use image::{ImageBuffer, Rgba, Rgb};
use qrcode::{QrCode, Color};

fn generate_colored_qr(
    data: &str,
    fg_color: [u8; 3],
    bg_color: [u8; 3],
    module_size: u32,
) -> ImageBuffer<Rgba<u8>, Vec<u8>> {
    let code = QrCode::new(data).unwrap();
    let border = 4;
    let width = code.width();
    let img_size = (width + border * 2) as u32 * module_size;

    let mut img = ImageBuffer::from_pixel(
        img_size,
        img_size,
        Rgba([bg_color[0], bg_color[1], bg_color[2], 255]),
    );

    for y in 0..width {
        for x in 0..width {
            let color = if code[(x, y)] == Color::Dark {
                Rgba([fg_color[0], fg_color[1], fg_color[2], 255])
            } else {
                Rgba([bg_color[0], bg_color[1], bg_color[2], 255])
            };

            // 绘制模块
            for dy in 0..module_size {
                for dx in 0..module_size {
                    let px = (x as u32 + border as u32) * module_size + dx;
                    let py = (y as u32 + border as u32) * module_size + dy;
                    img.put_pixel(px, py, color);
                }
            }
        }
    }

    img
}

fn main() {
    // 生成蓝色前景、白色背景的二维码
    let qr = generate_colored_qr(
        "https://www.rust-lang.org",
        [0, 100, 200],   // 蓝色前景
        [255, 255, 255],  // 白色背景
        10,
    );
    qr.save("qr_blue.png").unwrap();

    // 生成红色前景的二维码
    let qr_red = generate_colored_qr(
        "Hello, Rust!",
        [220, 50, 50],   // 红色前景
        [255, 255, 255],  // 白色背景
        10,
    );
    qr_red.save("qr_red.png").unwrap();
}

7.5.5 带Logo的二维码

利用 QR 码的纠错能力,可以在二维码中心放置 Logo 而不影响识别。需要使用较高的纠错等级(H 级,30% 容错)。

use image::{GenericImage, ImageBuffer, Rgba, imageops};
use qrcode::{QrCode, EcLevel};

fn generate_qr_with_logo(
    data: &str,
    logo_path: &str,
    size: u32,
    logo_ratio: f32,  // Logo 占二维码的比例(建议 0.2-0.3)
) -> Result<ImageBuffer<Rgba<u8>, Vec<u8>>, Box<dyn std::error::Error>> {
    // 1. 使用 H 级纠错生成二维码
    let code = QrCode::with_error_correction_level(
        data.as_bytes(),
        EcLevel::H,  // 30% 纠错容量
    )?;

    // 2. 渲染二维码
    let border = 4;
    let module_size = (size as f32 / (code.width() as f32 + 2.0 * border as f32)).ceil() as u32;
    let mut qr_img = code.render::<Rgba<u8>>()
        .min_dimensions(size, size)
        .quiet_zone(true)
        .module_dimensions(module_size, module_size)
        .build();

    // 3. 加载 Logo
    let logo = image::open(logo_path)?.to_rgba8();
    let (qr_w, qr_h) = qr_img.dimensions();

    // 4. 计算 Logo 尺寸和位置(居中)
    let logo_w = (qr_w as f32 * logo_ratio) as u32;
    let logo_h = (qr_h as f32 * logo_ratio) as u32;
    let resized_logo = logo.resize(logo_w, logo_h, imageops::FilterType::Lanczos3);

    // 5. 添加白色背景区域
    let padding = 10;
    let bg_w = logo_w + padding * 2;
    let bg_h = logo_h + padding * 2;
    let mut bg = ImageBuffer::from_pixel(bg_w, bg_h, Rgba([255, 255, 255, 255]));
    bg.copy_from(&resized_logo, padding, padding)?;

    // 6. 将 Logo 放置在二维码中心
    let x = (qr_w - bg_w) / 2;
    let y = (qr_h - bg_h) / 2;
    qr_img.copy_from(&bg, x, y)?;

    Ok(qr_img)
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let qr = generate_qr_with_logo(
        "https://www.rust-lang.org",
        "images/rust-logo-blk.svg",
        600,
        0.25,
    )?;
    qr.save("qr_with_logo.png")?;
    println!("带 Logo 的二维码已生成: qr_with_logo.png");
    Ok(())
}

7.5.6 二维码解码/识别

使用 bardecoder crate 进行二维码解码:

[dependencies]
bardecoder = "0.5"
image = "0.25"
use bardecoder::decode::Decode;
use image::DynamicImage;

fn decode_qr(image_path: &str) -> Result<String, Box<dyn std::error::Error>> {
    let img = image::open(image_path)?;
    let decoder = bardecoder::default_decoder();

    let results = decoder.decode(&img);
    for result in results {
        match result {
            Ok(content) => {
                println!("解码结果: {}", content);
                return Ok(content);
            }
            Err(e) => println!("解码失败: {:?}", e),
        }
    }

    Err("无法解码二维码".into())
}

fn main() {
    match decode_qr("qr_code.png") {
        Ok(content) => println!("二维码内容: {}", content),
        Err(e) => println!("错误: {}", e),
    }
}

7.5.7 批量生成二维码

use image::{GenericImage, ImageBuffer, Rgba};
use qrcode::QrCode;
use std::fs;

/// 批量生成二维码并保存
fn batch_generate_qr_codes(
    data_list: &[(&str, &str)],  // (数据, 输出文件名)
    size: u32,
) -> Result<(), Box<dyn std::error::Error>> {
    for (data, filename) in data_list {
        let code = QrCode::new(data.as_bytes())?;
        let border = 4;
        let module_size = (size as f32 / (code.width() as f32 + 2.0 * border as f32)).ceil() as u32;

        let img = code.render::<Rgba<u8>>()
            .min_dimensions(size, size)
            .quiet_zone(true)
            .module_dimensions(module_size, module_size)
            .build();

        img.save(filename)?;
        println!("已生成: {} -> {}", data, filename);
    }

    Ok(())
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let data_list = &[
        ("https://www.rust-lang.org", "qr_rust.png"),
        ("https://crates.io", "qr_crates.png"),
        ("https://docs.rs", "qr_docs.png"),
        ("https://github.com/rust-lang/rust", "qr_github.png"),
    ];

    // 创建输出目录
    fs::create_dir_all("output_qr")?;

    batch_generate_qr_codes(data_list, 300)?;

    Ok(())
}

7.5.8 合并多个二维码

以下代码将多个二维码横向合并为一张图片:

use image::{GenericImage, ImageBuffer, Rgba};
use qrcode::QrCode;

fn generate_qr_image(data: &str, size: u32) -> ImageBuffer<Rgba<u8>, Vec<u8>> {
    let code = QrCode::new(data.as_bytes()).unwrap();
    let border = 4;
    let module_size = (size as f32 / (code.width() as f32 + 2.0 * border as f32)).ceil() as u32;

    code.render::<Rgba<u8>>()
        .min_dimensions(size, size)
        .quiet_zone(true)
        .module_dimensions(module_size, module_size)
        .build()
}

fn combine_qr_codes_horizontally(
    img1: &ImageBuffer<Rgba<u8>, Vec<u8>>,
    img2: &ImageBuffer<Rgba<u8>, Vec<u8>>,
    gap: u32,
) -> ImageBuffer<Rgba<u8>, Vec<u8>> {
    // 1. 计算新图片的尺寸
    let (w1, h1) = img1.dimensions();
    let (w2, h2) = img2.dimensions();
    let final_width = w1 + gap + w2;
    let final_height = h1.max(h2);

    // 2. 创建新画布,并填充白色背景
    let mut combined =
        ImageBuffer::from_pixel(final_width, final_height, Rgba([255, 255, 255, 255]));

    // 3. 将第一张图复制到左侧 (0, 0) 位置
    combined.copy_from(img1, 0, 0).unwrap();

    // 4. 将第二张图复制到右侧 (w1 + gap, 0) 位置
    combined.copy_from(img2, w1 + gap, 0).unwrap();

    combined
}

fn main() {
    // 1. 生成两个二维码图像,尺寸设为 200x200
    let qr1 = generate_qr_image("https://qr.alipay.com/fkx18203840eqbo648wdo95", 200);
    let qr2 = generate_qr_image("wxp://f2f0QuWokq-0iWOJrBOVHz7vVLoOhrChnMI28COpdSmFzRs", 200);

    // 2. 横向合并,间隔 20 像素
    let combined = combine_qr_codes_horizontally(&qr1, &qr2, 20);

    // 3. 保存最终图片
    combined.save("/tmp/combined_qr_codes.png").unwrap();
}

7.6 二维码的用途

二维码的应用已经渗透到生活的方方面面:

类别应用场景说明
移动支付付款码、收款码支付宝、微信支付
身份认证健康码、电子证照疫情防控、电子身份证
商品溯源商品编码、防伪标签食品安全、药品追溯
广告营销链接分享、优惠券扫码领红包、关注公众号
交通出行电子车票、登机牌高铁二维码、Aztec Code 登机牌
社交互动加好友、名片交换微信扫码加好友
文档管理文件分享、会议签到扫码下载文件、签到打卡
物联网设备配对、WiFi连接扫码连接WiFi、IoT设备配网
请作者喝杯奶茶呗 请作者喝杯奶茶呗

7.7 标准与规范

QR Code 的国际标准为 ISO/IEC 18004,由国际标准化组织(ISO)和国际电工委员会(IEC)联合发布。

QR Code 的官方信息:

技术原理参考:

7.8 相关 Rust 库

库名说明链接
qrcodeQR 码生成库,支持多种输出格式GitHub
bardecoder多格式条码/二维码解码库GitHub
rqrr高性能 QR 码解码库GitHub
quircsquirc QR 解码器的 Rust 绑定crates.io
image图像处理库(配合二维码库使用)GitHub
fast_qr高速 QR 码生成库(SIMD 加速)GitHub

7.9 总结

QR码核心参数对比

参数说明
版本范围Version 1 (21x21) 到 Version 40 (177x177)
模块数公式$4 \times \text{version} + 17$
编码模式数字、字母数字、字节、汉字
纠错等级L(7%), M(15%), Q(25%), H(30%)
最大容量7,089 数字 / 4,296 字母数字 / 2,953 字节
纠错算法Reed-Solomon 编码
有限域$GF(2^8)$,不可约多项式 $x^8 + x^4 + x^3 + x^2 + 1$

二维码标准对比

特性QR CodeData MatrixPDF417Aztec Code
类型矩阵式矩阵式堆叠式矩阵式
最大容量7,089 数字2,335 数字1,850 数字3,832 数字
容错等级4级可调2级可调
需要静区
读取速度最快中等
中国普及度最高中等

关键公式汇总

公式说明
$\text{模块数} = 4v + 17$QR 码版本 v 对应的模块数
$p(x) = x^8 + x^4 + x^3 + x^2 + 1$Reed-Solomon 有限域不可约多项式
$\text{数字容量} = \lfloor n/3 \rfloor \times 10 + r$数字模式位流计算

7.10 练习题

  1. 基础题:使用 qrcode crate 生成一个包含你名字的二维码,保存为 PNG 格式。

  2. 纠错等级对比:分别使用 L、M、Q、H 四种纠错等级生成相同内容的二维码,对比它们的大小差异,并用图像编辑工具遮挡部分区域后测试是否能正确解码。

  3. 带 Logo 二维码:选择一张你喜欢的 Logo 图片,使用 H 级纠错生成带 Logo 的二维码,并验证其可扫描性。

  4. 批量生成:编写一个 Rust 程序,读取 CSV 文件中的 URL 列表,为每个 URL 生成一个二维码,文件名为 URL 的哈希值。

  5. 编码模式分析:对于字符串 "Hello, World! 123",分析 QR 码会选择哪种编码模式?为什么?如果字符串是 "1234567890" 呢?

  6. 容量计算:Version 10 的 QR 码有多少个模块?在纠错等级 M 下,字节模式最多能存储多少个字节?

  7. 思考题:为什么二维码在中国如此普及,而在欧美国家的使用率相对较低?从技术、商业模式、社会习惯等角度分析。

  8. 安全实践:编写一个 Rust 程序,生成二维码前先验证 URL 的合法性(检查协议、域名等),拒绝生成指向已知恶意域名的二维码。