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
+48 -5
View File
@@ -1,13 +1,13 @@
use axum::{
extract::Query,
extract::{Multipart, Query},
http::{header, StatusCode},
response::{Html, IntoResponse},
routing::get,
response::{Html, IntoResponse, Json},
routing::{get, post},
Router,
};
use qr_core::qr::{QrCode, QrConfig, VersionMode};
use qr_core::version::EcLevel;
use serde::Deserialize;
use serde::{Deserialize, Serialize};
/// GET /api/qr 查询参数
#[derive(Deserialize)]
@@ -78,11 +78,54 @@ async fn generate_qr(Query(params): Query<QrParams>) -> impl IntoResponse {
}
}
/// 解码结果 JSON 响应
#[derive(Serialize)]
struct DecodeResponse {
text: String,
version: u8,
level: String,
mask: u8,
errors_corrected: usize,
}
/// POST /api/decode — 解码上传的 QR 码图片
async fn decode_qr(mut multipart: Multipart) -> impl IntoResponse {
while let Ok(Some(field)) = multipart.next_field().await {
if field.name() == Some("file") {
match field.bytes().await {
Ok(data) => match qr_core::decoder::decode_image(&data) {
Ok(result) => {
return (
StatusCode::OK,
Json(DecodeResponse {
text: result.text,
version: result.version,
level: format!("{:?}", result.level),
mask: result.mask,
errors_corrected: result.errors_corrected,
}),
)
.into_response();
}
Err(e) => {
return (StatusCode::BAD_REQUEST, e).into_response();
}
},
Err(e) => {
return (StatusCode::BAD_REQUEST, e.to_string()).into_response();
}
}
}
}
(StatusCode::BAD_REQUEST, "未找到上传文件(字段名: file").into_response()
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(index))
.route("/api/qr", get(generate_qr));
.route("/api/qr", get(generate_qr))
.route("/api/decode", post(decode_qr));
let addr = "0.0.0.0:3000";
println!("QRGen Web → http://{}", addr);