feat: QR 码解码器 — 从零手写的完整解码流水线

新增 core/src/decoder/ 模块(9 文件,~1500 行):
- bch.rs: BCH(15,5)+BCH(18,6) 查表解码(32+64 有效码字,t≤3)
- format.rs: 从矩阵读取格式信息(EC+掩码)+版本信息(2 副本容错)
- extract.rs: 逆向蛇形排列提取数据码字
- deinterleave.rs: 逆向 RS 数据交错
- rs_decode.rs: RS 纠错流水线(伴随式→BM→Chien→Forney)
- mode_decode.rs: 逆向 4 种编码模式(数字/字母/字节/汉字 Shift JIS)
- detect.rs: 定位图案检测(1:1:3:1:1 比例+交叉验证+聚类)
- image.rs: 图像加载+灰度二值化(PNG/JPEG/WebP)
- mod.rs: 顶层 API(decode_image + decode_matrix)

修改已有文件:
- core: galois.rs 表 pub(crate), 新增 poly_eval(); reed_solomon 公开内部函数
- cli: 新增 --decode <file> 解码模式
- web: 新增 POST /api/decode(multipart file upload)

测试: 72 passed (58 原有 + 14 新增 decoder 测试)
This commit is contained in:
2026-06-19 20:36:12 +08:00
parent 87aa3f4574
commit effc88c6d7
20 changed files with 1832 additions and 31 deletions
+93
View File
@@ -0,0 +1,93 @@
//! 从 QR 矩阵数据区域蛇形读取码字比特
//!
//! 读取顺序严格对应 `placement.rs::place_data` 的写入顺序:
//! 从右下角开始,两列一组上下交替扫描,跳过保留模块。
use crate::matrix::grid::Matrix;
/// 从矩阵非保留区域蛇形读取码字比特
fn extract_bits(matrix: &Matrix, total_codewords: usize) -> Vec<bool> {
let size = matrix.size as usize;
let target_bits = total_codewords * 8;
let mut bits: Vec<bool> = Vec::with_capacity(target_bits);
let mut col = (size - 1) as i16;
let mut going_up = true;
while col >= 0 && bits.len() < target_bits {
let actual_col = col as usize;
if going_up {
for row in (0..size).rev() {
read_module(matrix, &mut bits, actual_col, row);
if actual_col > 0 {
read_module(matrix, &mut bits, actual_col - 1, row);
}
}
} else {
for row in 0..size {
read_module(matrix, &mut bits, actual_col, row);
if actual_col > 0 {
read_module(matrix, &mut bits, actual_col - 1, row);
}
}
}
col -= 2;
going_up = !going_up;
// 跳过垂直时序图案列(col 6)
if col == 6 {
col -= 1;
}
}
bits.truncate(target_bits);
bits
}
/// 读取单个非保留模块,追加 bool 到 bits
fn read_module(matrix: &Matrix, bits: &mut Vec<bool>, x: usize, y: usize) {
let xu = x as u8;
let yu = y as u8;
if xu < matrix.size && yu < matrix.size && !matrix.is_reserved(xu, yu) {
bits.push(matrix.get(xu, yu));
}
}
/// 将布尔比特打包为 u8 码字(MSB 优先)
fn bits_to_bytes(bits: &[bool]) -> Vec<u8> {
bits.chunks(8)
.map(|chunk| {
let mut byte = 0u8;
for &b in chunk {
byte = (byte << 1) | (b as u8);
}
// 不足 8 位的 chunk 左对齐
byte <<= 8 - chunk.len();
byte
})
.collect()
}
/// 从矩阵提取数据码字
///
/// *`total_codewords` 为数据码字总数(来自 EcInfo.total_codewords*
pub(crate) fn extract_codewords(matrix: &Matrix, total_codewords: usize) -> Vec<u8> {
let bits = extract_bits(matrix, total_codewords);
bits_to_bytes(&bits)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::qr::{QrCode, QrConfig};
use crate::version::get_data_capacity;
#[test]
fn test_extract_roundtrip_simple() {
let qr = QrCode::encode("HELLO WORLD", QrConfig::default()).unwrap();
let cap = get_data_capacity(qr.version, qr.level) as usize;
let codewords = extract_codewords(qr.matrix(), cap);
assert!(!codewords.is_empty());
// 前 11 字节编码 "HELLO WORLD"byte 模式 4b+8b+88b=100bits≈13字节)
assert!(codewords.len() >= 5); // 至少有数据
}
}