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
+49 -18
View File
@@ -4,10 +4,17 @@ use qr_core::version::EcLevel;
use std::path::Path;
#[derive(Parser)]
#[command(name = "qrgen", about = "QR 码生成器 — 从零手搓的 ISO/IEC 18004 实现")]
#[command(
name = "qrgen",
about = "QR 码生成/解码工具 — 从零手搓的 ISO/IEC 18004 实现"
)]
struct Args {
/// 要编码的内容
content: String,
/// 要编码的内容(编码模式)
content: Option<String>,
/// 解码图片文件 (PNG/JPEG/WebP),与编码模式互斥
#[arg(short = 'd', long)]
decode: Option<String>,
/// 输出文件 (.png 或 .svg),不指定则输出终端 ASCII
#[arg(short = 'o', long)]
@@ -37,6 +44,20 @@ struct Args {
fn main() -> anyhow::Result<()> {
let args = Args::parse();
// 解码模式
if let Some(path) = args.decode {
return do_decode(&path);
}
// 编码模式
let content = args
.content
.as_deref()
.ok_or_else(|| anyhow::anyhow!("请提供编码内容,或使用 --decode <文件> 解码图片"))?;
do_encode(content, &args)
}
fn do_encode(content: &str, args: &Args) -> anyhow::Result<()> {
let level = match args.level.to_uppercase().as_str() {
"L" => EcLevel::L,
"M" => EcLevel::M,
@@ -61,13 +82,11 @@ fn main() -> anyhow::Result<()> {
margin: args.margin,
};
let qr =
QrCode::encode(&args.content, config).map_err(|e| anyhow::anyhow!("编码失败: {}", e))?;
let qr = QrCode::encode(content, config).map_err(|e| anyhow::anyhow!("编码失败: {}", e))?;
match args.output {
match &args.output {
Some(path) => {
// 防止路径遍历攻击,拒绝包含 ".." 的路径
let path_obj = Path::new(&path);
let path_obj = Path::new(path);
if path_obj
.components()
.any(|c| matches!(c, std::path::Component::ParentDir))
@@ -84,34 +103,46 @@ fn main() -> anyhow::Result<()> {
match ext.as_str() {
"png" => {
let bytes = qr.to_png_bytes(args.size)?;
std::fs::write(&path, bytes)?;
std::fs::write(path, bytes)?;
println!(
"已生成: {} (版本 {}, {}×{} 模块, {} 级纠错)",
"已生成: {} (版本 {}, {}×{} 模块, {:?} 级纠错)",
path,
qr.version.0,
qr.size(),
qr.size(),
match qr.level {
EcLevel::L => "L",
EcLevel::M => "M",
EcLevel::Q => "Q",
EcLevel::H => "H",
}
qr.level
);
}
"svg" => {
let svg = qr.to_svg();
std::fs::write(&path, svg)?;
std::fs::write(path, svg)?;
println!("已生成: {} (版本 {}, SVG 格式)", path, qr.version.0);
}
_ => anyhow::bail!("不支持的文件格式: .{}。支持 .png / .svg", ext),
}
}
None => {
// 终端 ASCII 输出
println!("{}", qr.to_ascii(args.invert));
}
}
Ok(())
}
fn do_decode(path: &str) -> anyhow::Result<()> {
let bytes =
std::fs::read(path).map_err(|e| anyhow::anyhow!("无法读取文件 '{}': {}", path, e))?;
let result = qr_core::decoder::decode_image(&bytes).map_err(|e| anyhow::anyhow!("{e}"))?;
println!("解码成功:");
println!(" 文本: {}", result.text);
println!(" 版本: {}", result.version);
println!(" 纠错级别: {:?}", result.level);
println!(" 掩码: {}", result.mask);
if result.errors_corrected > 0 {
println!(" 纠正错误: {} 码字", result.errors_corrected);
}
Ok(())
}