Compare commits

...

3 Commits

Author SHA1 Message Date
Serendipity ce9c8b1b6e fix: 解码器3个bug修复 — 编码→解码往返恢复 + 新增往返测试
1. detect.rs: remove()索引bug — 元素前移后索引未更新,改用每次remove(0)
2. detect.rs: 版本估算公式修正 — (dist-14)/4 → (dist-10)/4,符合ZXing公式
3. extract.rs: 移除显式col 6跳过 — read_module已自动跳过保留区,显式skip导致列配对错位/行序反转

新增 test_roundtrip_png: 矩阵往返 + PNG往返双验证
2026-06-27 14:53:48 +08:00
Serendipity 309c9429ea refactor: ZXing对齐 — 容差50pct + 几何验证 + end-based中心 + 废弃代码清理
- detect.rs: 容差从40%提升到50% (对齐ZXing的moduleSize/2)
- detect.rs: 中心点改用ZXing end-based公式 (浮点精度, end - r4 - r3 - r2/2)
- detect.rs: 新增validate_finder_geometry()几何验证
  module_size一致性<10% + 勾股定理偏差<15%
- detect.rs: compute_finder_deviation改用base=total/7
- detect.rs: 删除废弃的estimate_finder_size函数
- perspective.rs: count_finder_hits容差对齐 (base=total/7 + 50pct)
- web/main.rs + cli/main.rs: 修复Rust 1.96新版clippy规则
2026-06-21 23:01:46 +08:00
Serendipity a03ab95ce5 fix: 解码器核心bug修复 — 容差计算+积分图像+Sauvola下溢
- detect.rs: finder比例容差改用 base=total/7 替代 avg=total/5
  修复 1:1:3:1:1 模式匹配对任意module_size均正确工作
- detect.rs: finder尺寸估算改为5段总长度(完整28px)替代外圈暗环(4px)
  修复版本号误判(原本将version 1误判为version 10)
- detect.rs: scan_row/scan_col 返回类型改为 (cx,cy,total_size) 三元组
- image.rs: 积分图像公式修正为 I(y+1,x+1)=I(y,x+1)+I(y+1,x)-I(y,x)+pixel
  替换错误的 cumulative row_sum 公式
- image.rs: 积分和运算 u64→i64 避免 debug 模式溢出panic
- perspective.rs: count_finder_hits 应用相同容差修复
- mask.rs: test_score_rule2 改用 score_rule2_raw
- bch.rs: 测试适配 decode_format_info→3元组, decode_version_info→2元组
2026-06-21 22:19:00 +08:00
10 changed files with 136 additions and 90 deletions
+1 -1
View File
@@ -266,7 +266,7 @@ fn build_mode(mode: &str, opts: &EncodeOpts, fb: &str) -> Result<String> {
let pwd = opts let pwd = opts
.password .password
.as_deref() .as_deref()
.or_else(|| env_pwd.as_deref()) .or(env_pwd.as_deref())
.unwrap_or(""); .unwrap_or("");
Ok(text_builder::build_wifi_text( Ok(text_builder::build_wifi_text(
s, s,
+6 -5
View File
@@ -118,7 +118,7 @@ mod tests {
result.is_some(), result.is_some(),
"decode failed: ec_bits={ec_bits}, mask={mask}, encoded={encoded:#05X}" "decode failed: ec_bits={ec_bits}, mask={mask}, encoded={encoded:#05X}"
); );
let (dec_ec, dec_mask) = result.unwrap(); let (dec_ec, dec_mask, _d) = result.unwrap();
assert_eq!(ec_bits, dec_ec, "ec_bits mismatch"); assert_eq!(ec_bits, dec_ec, "ec_bits mismatch");
assert_eq!(mask, dec_mask, "mask mismatch"); assert_eq!(mask, dec_mask, "mask mismatch");
} }
@@ -134,7 +134,8 @@ mod tests {
result.is_some(), result.is_some(),
"decode failed: ver={ver}, encoded={encoded:#010X}" "decode failed: ver={ver}, encoded={encoded:#010X}"
); );
assert_eq!(ver, result.unwrap(), "version mismatch"); let (dec_ver, _d) = result.unwrap();
assert_eq!(ver, dec_ver, "version mismatch");
} }
} }
@@ -143,12 +144,11 @@ mod tests {
for ec_bits in 0u8..4 { for ec_bits in 0u8..4 {
for mask in 0u8..8 { for mask in 0u8..8 {
let original = encode_format_info(ec_bits, mask); let original = encode_format_info(ec_bits, mask);
// 翻转每个比特,验证能纠错
for bit in 0..15 { for bit in 0..15 {
let corrupted = original ^ (1 << bit); let corrupted = original ^ (1 << bit);
let result = decode_format_info(corrupted); let result = decode_format_info(corrupted);
assert!(result.is_some(), "1-bit error not corrected at bit {bit}"); assert!(result.is_some(), "1-bit error not corrected at bit {bit}");
let (dec_ec, dec_mask) = result.unwrap(); let (dec_ec, dec_mask, _d) = result.unwrap();
assert_eq!(ec_bits, dec_ec); assert_eq!(ec_bits, dec_ec);
assert_eq!(mask, dec_mask); assert_eq!(mask, dec_mask);
} }
@@ -164,7 +164,8 @@ mod tests {
let corrupted = original ^ (1 << bit); let corrupted = original ^ (1 << bit);
let result = decode_version_info(corrupted); let result = decode_version_info(corrupted);
assert!(result.is_some(), "1-bit error not corrected at bit {bit}"); assert!(result.is_some(), "1-bit error not corrected at bit {bit}");
assert_eq!(ver, result.unwrap()); let (dec_ver, _d) = result.unwrap();
assert_eq!(ver, dec_ver);
} }
} }
} }
+79 -52
View File
@@ -21,7 +21,7 @@ pub(crate) struct DetectResult {
} }
/// 水平扫描查找 1:1:3:1:1 比例 /// 水平扫描查找 1:1:3:1:1 比例
fn scan_row(gray: &[Vec<bool>], row: usize) -> Vec<(usize, usize)> { fn scan_row(gray: &[Vec<bool>], row: usize) -> Vec<(usize, usize, usize)> {
// (列号,运行长度) // (列号,运行长度)
let mut runs: Vec<(usize, usize)> = Vec::new(); let mut runs: Vec<(usize, usize)> = Vec::new();
let width = if gray.is_empty() { 0 } else { gray[0].len() }; let width = if gray.is_empty() { 0 } else { gray[0].len() };
@@ -37,8 +37,8 @@ fn scan_row(gray: &[Vec<bool>], row: usize) -> Vec<(usize, usize)> {
runs.push((col - run_len, run_len)); runs.push((col - run_len, run_len));
} }
// 找 5 连段符合 1:1:3:1:1 比例 // 找 5 连段符合 1:1:3:1:1 比例 — 返回 (cx, cy, total_size_px)
let mut centers: Vec<(usize, usize)> = Vec::new(); let mut centers: Vec<(usize, usize, usize)> = Vec::new();
for i in 0..runs.len().saturating_sub(4) { for i in 0..runs.len().saturating_sub(4) {
let r0 = runs[i].1 as f32; let r0 = runs[i].1 as f32;
let r1 = runs[i + 1].1 as f32; let r1 = runs[i + 1].1 as f32;
@@ -46,18 +46,19 @@ fn scan_row(gray: &[Vec<bool>], row: usize) -> Vec<(usize, usize)> {
let r3 = runs[i + 3].1 as f32; let r3 = runs[i + 3].1 as f32;
let r4 = runs[i + 4].1 as f32; let r4 = runs[i + 4].1 as f32;
let avg = (r0 + r1 + r2 + r3 + r4) / 5.0; let total = r0 + r1 + r2 + r3 + r4;
if avg < 2.0 { let base = total / 7.0;
if base < 2.0 {
continue; continue;
} }
let tolerance = 0.5; // ZXing 标准:moduleSize/2 容差
// 检查比例容差 ±40% let check = |v: f32, expected: f32| (v - expected * base).abs() < base * tolerance;
let tolerance = 0.4;
let check = |v: f32, expected: f32| (v - expected * avg).abs() < avg * tolerance;
if check(r0, 1.0) && check(r1, 1.0) && check(r2, 3.0) && check(r3, 1.0) && check(r4, 1.0) { if check(r0, 1.0) && check(r1, 1.0) && check(r2, 3.0) && check(r3, 1.0) && check(r4, 1.0) {
let cx = runs[i + 2].0 + runs[i + 2].1 / 2; // ZXing 的 end-based 中心公式: end - r4 - r3 - r2/2 (浮点精度更高)
centers.push((cx, row)); let end = runs[i + 4].0 + runs[i + 4].1;
let cx = end as f32 - r4 - r3 - r2 / 2.0;
centers.push((cx as usize, row, total as usize));
} }
} }
@@ -65,7 +66,7 @@ fn scan_row(gray: &[Vec<bool>], row: usize) -> Vec<(usize, usize)> {
} }
/// 垂直扫描查找 1:1:3:1:1 比例 /// 垂直扫描查找 1:1:3:1:1 比例
fn scan_col(gray: &[Vec<bool>], col: usize) -> Vec<(usize, usize)> { fn scan_col(gray: &[Vec<bool>], col: usize) -> Vec<(usize, usize, usize)> {
let height = gray.len(); let height = gray.len();
let mut runs: Vec<(usize, usize)> = Vec::new(); let mut runs: Vec<(usize, usize)> = Vec::new();
@@ -80,7 +81,7 @@ fn scan_col(gray: &[Vec<bool>], col: usize) -> Vec<(usize, usize)> {
runs.push((row - run_len, run_len)); runs.push((row - run_len, run_len));
} }
let mut centers: Vec<(usize, usize)> = Vec::new(); let mut centers: Vec<(usize, usize, usize)> = Vec::new();
for i in 0..runs.len().saturating_sub(4) { for i in 0..runs.len().saturating_sub(4) {
let r0 = runs[i].1 as f32; let r0 = runs[i].1 as f32;
let r1 = runs[i + 1].1 as f32; let r1 = runs[i + 1].1 as f32;
@@ -88,17 +89,19 @@ fn scan_col(gray: &[Vec<bool>], col: usize) -> Vec<(usize, usize)> {
let r3 = runs[i + 3].1 as f32; let r3 = runs[i + 3].1 as f32;
let r4 = runs[i + 4].1 as f32; let r4 = runs[i + 4].1 as f32;
let avg = (r0 + r1 + r2 + r3 + r4) / 5.0; let total = r0 + r1 + r2 + r3 + r4;
if avg < 2.0 { let base = total / 7.0;
if base < 2.0 {
continue; continue;
} }
let tolerance = 0.4; let tolerance = 0.5; // ZXing 标准:moduleSize/2 容差
let check = |v: f32, expected: f32| (v - expected * avg).abs() < avg * tolerance; let check = |v: f32, expected: f32| (v - expected * base).abs() < base * tolerance;
if check(r0, 1.0) && check(r1, 1.0) && check(r2, 3.0) && check(r3, 1.0) && check(r4, 1.0) { if check(r0, 1.0) && check(r1, 1.0) && check(r2, 3.0) && check(r3, 1.0) && check(r4, 1.0) {
let cy = runs[i + 2].0 + runs[i + 2].1 / 2; let end = runs[i + 4].0 + runs[i + 4].1;
centers.push((col, cy)); let cy = end as f32 - r4 - r3 - r2 / 2.0;
centers.push((col, cy as usize, total as usize));
} }
} }
@@ -142,7 +145,7 @@ pub(crate) fn estimate_version_from_tl_tr(tl: (f64, f64), tr: (f64, f64), module
let dy = tr.1 - tl.1; let dy = tr.1 - tl.1;
let dist_px = (dx * dx + dy * dy).sqrt() as f32; let dist_px = (dx * dx + dy * dy).sqrt() as f32;
let dist_modules = dist_px / module_size as f32; let dist_modules = dist_px / module_size as f32;
let ver = ((dist_modules as i32 - 14) / 4) as u8; let ver = ((dist_modules as i32 - 10) / 4) as u8;
ver.clamp(1, 40) ver.clamp(1, 40)
} }
@@ -154,18 +157,17 @@ fn find_finders(gray: &[Vec<bool>]) -> Option<[FinderMatch; 3]> {
return None; return None;
} }
// 水平扫描 // 水平扫描(含 finder 总尺寸)
let mut h_centers: Vec<(usize, usize, usize)> = Vec::new(); // (cx, cy, size) let mut h_centers: Vec<(usize, usize, usize)> = Vec::new(); // (cx, cy, size)
for row in (0..height).step_by(2) { for row in (0..height).step_by(2) {
for (cx, cy) in scan_row(gray, row) { for (cx, cy, total_size) in scan_row(gray, row) {
// 交叉验证:垂直扫描 // 交叉验证:垂直扫描
let v_matches = scan_col(gray, cx); let v_matches = scan_col(gray, cx);
if v_matches if v_matches
.iter() .iter()
.any(|&(_, vy)| (vy as i32 - cy as i32).abs() < 5) .any(|&(_, vy, _)| (vy as i32 - cy as i32).abs() < 5)
{ {
let size = estimate_finder_size(gray, cx, cy); h_centers.push((cx, cy, total_size));
h_centers.push((cx, cy, size));
} }
} }
} }
@@ -215,43 +217,66 @@ fn find_finders(gray: &[Vec<bool>]) -> Option<[FinderMatch; 3]> {
// 排序:左上、右上、左下 // 排序:左上、右上、左下
finders.sort_by(|a, b| { finders.sort_by(|a, b| {
let da = a.cx * a.cx + a.cy * a.cy; // 到原点的距离 let da = a.cx * a.cx + a.cy * a.cy;
let db = b.cx * b.cx + b.cy * b.cy; let db = b.cx * b.cx + b.cy * b.cy;
da.cmp(&db) da.cmp(&db)
}); });
// 区分右上(X 最大)和左下(Y 最大)
if finders[1].cx < finders[2].cx { if finders[1].cx < finders[2].cx {
finders.swap(1, 2); finders.swap(1, 2);
} }
// remove 会使后续元素前移,必须每次 remove(0)
let f0 = finders.remove(0); let f0 = finders.remove(0);
let f1 = finders.remove(0); let f1 = finders.remove(0);
let f2 = finders.remove(0); let f2 = finders.remove(0);
// ZXing 几何验证:module_size 一致性 + 勾股定理
if !validate_finder_geometry(&[&f0, &f1, &f2]) {
return None;
}
Some([f0, f1, f2]) Some([f0, f1, f2])
} }
/// 估算定位图案大小(像素) /// ZXing 风格几何验证:3 个定位图案的 module_size 必须一致且构成近似直角三角形
fn estimate_finder_size(gray: &[Vec<bool>], cx: usize, cy: usize) -> usize { fn validate_finder_geometry(finders: &[&FinderMatch; 3]) -> bool {
// 从中心点水平扫描连续暗像素 let s0 = finders[0].size as f64;
let mut left = cx; let s1 = finders[1].size as f64;
while left > 0 { let s2 = finders[2].size as f64;
if cy < gray.len() && left < gray[0].len() && gray[cy][left] {
left -= 1; // module_size 一致性: 最大偏差 < 10%
} else { let avg_size = (s0 + s1 + s2) / 3.0;
break; let max_dev = avg_size * 0.10;
} if (s0 - avg_size).abs() > max_dev
|| (s1 - avg_size).abs() > max_dev
|| (s2 - avg_size).abs() > max_dev
{
return false;
} }
let mut right = cx;
while right + 1 < gray[0].len() { // 勾股定理验证: |C - sqrt(A² + B²)| / min(C, sqrt(A² + B²)) < 15%
if cy < gray.len() && gray[cy][right] { let ax = finders[1].cx as f64 - finders[0].cx as f64;
right += 1; let ay = finders[1].cy as f64 - finders[0].cy as f64;
} else { let bx = finders[2].cx as f64 - finders[0].cx as f64;
break; let by = finders[2].cy as f64 - finders[0].cy as f64;
}
let a_len = (ax * ax + ay * ay).sqrt();
let b_len = (bx * bx + by * by).sqrt();
let c_len = {
let cx = finders[2].cx as f64 - finders[1].cx as f64;
let cy = finders[2].cy as f64 - finders[1].cy as f64;
(cx * cx + cy * cy).sqrt()
};
let hypotenuse = (a_len * a_len + b_len * b_len).sqrt();
let min = c_len.min(hypotenuse);
if min < 1.0 {
return true; // 太小,跳过检查
} }
right - left let deviation = (c_len - hypotenuse).abs() / min;
deviation < 0.15
} }
/// 从二值化图像中提取 QR 布尔矩阵 /// 从二值化图像中提取 QR 布尔矩阵
@@ -279,7 +304,9 @@ pub(crate) fn detect_and_extract(
let dy = tr.cy as f64 - tl.cy as f64; let dy = tr.cy as f64 - tl.cy as f64;
let dist_px = (dx * dx + dy * dy).sqrt() as f32; let dist_px = (dx * dx + dy * dy).sqrt() as f32;
let dist_modules = dist_px / module_size as f32; let dist_modules = dist_px / module_size as f32;
let ver = ((dist_modules as i32 - 14) / 4) as u8; // ZXing 公式: totalModules = dist/moduleSize + 7, version = (totalModules - 17) / 4
// 简化为: version = (dist_modules + 7 - 17) / 4 = (dist_modules - 10) / 4
let ver = ((dist_modules as i32 - 10) / 4) as u8;
let version = ver.clamp(1, 40); let version = ver.clamp(1, 40);
let size = 17 + version as usize * 4; let size = 17 + version as usize * 4;
@@ -360,7 +387,7 @@ fn check_quiet_zone(modules: &[Vec<bool>], size: usize) -> bool {
true true
} }
/// 计算定位图案比例偏差(与理想 1:1:3:1:1 的偏差,0=完美, 1=最大) /// 计算定位图案比例偏差(0=完美, 1=最大),使用 base=total/7 对齐 ZXing
fn compute_finder_deviation(gray: &[Vec<bool>], finder: &FinderMatch) -> f64 { fn compute_finder_deviation(gray: &[Vec<bool>], finder: &FinderMatch) -> f64 {
let row = finder.cy; let row = finder.cy;
let mut runs: Vec<usize> = Vec::new(); let mut runs: Vec<usize> = Vec::new();
@@ -382,14 +409,14 @@ fn compute_finder_deviation(gray: &[Vec<bool>], finder: &FinderMatch) -> f64 {
let mut best_dev = 1.0f64; let mut best_dev = 1.0f64;
for i in 0..runs.len().saturating_sub(4) { for i in 0..runs.len().saturating_sub(4) {
let avg = let total = (runs[i] + runs[i + 1] + runs[i + 2] + runs[i + 3] + runs[i + 4]) as f64;
(runs[i] + runs[i + 1] + runs[i + 2] + runs[i + 3] + runs[i + 4]) as f64 / 5.0; let base = total / 7.0;
if avg < 2.0 { if base < 2.0 {
continue; continue;
} }
let expected = [avg, avg, 3.0 * avg, avg, avg]; let expected = [base, base, 3.0 * base, base, base];
let dev: f64 = (0..5) let dev: f64 = (0..5)
.map(|j| (runs[j] as f64 - expected[j]).abs() / avg) .map(|j| (runs[i + j] as f64 - expected[j]).abs() / base)
.sum::<f64>() .sum::<f64>()
/ 5.0; / 5.0;
if dev < best_dev { if dev < best_dev {
+2 -5
View File
@@ -32,11 +32,8 @@ fn extract_bits(matrix: &Matrix, total_codewords: usize) -> Vec<bool> {
} }
col -= 2; col -= 2;
going_up = !going_up; going_up = !going_up;
// 垂直时序图案列(col 6)由 read_module 自动跳过保留区,
// 跳过垂直时序图案列(col 6 // 无需显式 skip,否则会导致列配对错位、行序反转
if col == 6 {
col -= 1;
}
} }
bits.truncate(target_bits); bits.truncate(target_bits);
+14 -16
View File
@@ -113,21 +113,19 @@ fn sauvola_binarize(gray: &GrayImage, w: u32, h: u32) -> Vec<Vec<bool>> {
let r = 128.0f64; let r = 128.0f64;
// 预计算积分图像以加速窗口和/平方和 // 预计算积分图像以加速窗口和/平方和
let mut integral = vec![0u64; (w as usize + 1) * (h as usize + 1)]; // 标准公式: I(y+1,x+1) = I(y,x+1) + I(y+1,x) - I(y,x) + pixel(y,x)
let mut sq_integral = vec![0u64; (w as usize + 1) * (h as usize + 1)]; let stride = w as usize + 1;
let mut integral = vec![0u64; stride * (h as usize + 1)];
let mut sq_integral = vec![0u64; stride * (h as usize + 1)];
for y in 0..h as usize { for y in 0..h as usize {
let mut row_sum = 0u64;
let mut row_sq = 0u64;
for x in 0..w as usize { for x in 0..w as usize {
let p = gray.get_pixel(x as u32, y as u32).0[0] as u64; let p = gray.get_pixel(x as u32, y as u32).0[0] as u64;
row_sum += p; let idx = (y + 1) * stride + (x + 1);
row_sq += p * p; let above = y * stride + (x + 1);
let idx = (y + 1) * (w as usize + 1) + (x + 1); let left = (y + 1) * stride + x;
let above = (y) * (w as usize + 1) + (x + 1); let above_left = y * stride + x;
let left = (y + 1) * (w as usize + 1) + (x); integral[idx] = integral[above] + integral[left] - integral[above_left] + p;
let above_left = (y) * (w as usize + 1) + (x); sq_integral[idx] = sq_integral[above] + sq_integral[left] - sq_integral[above_left] + p * p;
integral[idx] = integral[left] + integral[above] - integral[above_left] + row_sum;
sq_integral[idx] = sq_integral[left] + sq_integral[above] - sq_integral[above_left] + row_sq;
} }
} }
@@ -138,10 +136,10 @@ fn sauvola_binarize(gray: &GrayImage, w: u32, h: u32) -> Vec<Vec<bool>> {
let y2 = (y + half + 1).min(h as i32) as usize; let y2 = (y + half + 1).min(h as i32) as usize;
let idx = |xx: usize, yy: usize| yy * (w as usize + 1) + xx; let idx = |xx: usize, yy: usize| yy * (w as usize + 1) + xx;
let count = ((x2 - x1) * (y2 - y1)) as f64; let count = ((x2 - x1) * (y2 - y1)) as f64;
let sum = (integral[idx(x2, y2)] - integral[idx(x1, y2)] - integral[idx(x2, y1)] let sum = (integral[idx(x2, y2)] as i64 - integral[idx(x1, y2)] as i64
+ integral[idx(x1, y1)]) as f64; - integral[idx(x2, y1)] as i64 + integral[idx(x1, y1)] as i64) as f64;
let sq_sum = (sq_integral[idx(x2, y2)] - sq_integral[idx(x1, y2)] - sq_integral[idx(x2, y1)] let sq_sum = (sq_integral[idx(x2, y2)] as i64 - sq_integral[idx(x1, y2)] as i64
+ sq_integral[idx(x1, y1)]) as f64; - sq_integral[idx(x2, y1)] as i64 + sq_integral[idx(x1, y1)] as i64) as f64;
let mean = sum / count; let mean = sum / count;
let variance = (sq_sum / count - mean * mean).max(0.0); let variance = (sq_sum / count - mean * mean).max(0.0);
(mean, variance.sqrt()) (mean, variance.sqrt())
+4 -3
View File
@@ -143,9 +143,10 @@ fn count_finder_hits(gray: &[Vec<bool>]) -> usize {
let (mut col,mut runs)=(0,vec![]); let (mut col,mut runs)=(0,vec![]);
while col<gray[row].len() { let c=gray[row][col]; let mut l=0; while col<gray[row].len()&&gray[row][col]==c{l+=1;col+=1;} runs.push(l); } while col<gray[row].len() { let c=gray[row][col]; let mut l=0; while col<gray[row].len()&&gray[row][col]==c{l+=1;col+=1;} runs.push(l); }
for i in 0..runs.len().saturating_sub(4) { for i in 0..runs.len().saturating_sub(4) {
let avg=(runs[i]+runs[i+1]+runs[i+2]+runs[i+3]+runs[i+4])as f64/5.0; let total=(runs[i]+runs[i+1]+runs[i+2]+runs[i+3]+runs[i+4])as f64;
if avg<2.0{continue;} let base=total/7.0;
let ck=|v:f64,e:f64|(v-e*avg).abs()<avg*0.4; if base<2.0{continue;}
let ck=|v:f64,e:f64|(v-e*base).abs()<base*0.5;
if ck(runs[i]as f64,1.0)&&ck(runs[i+1]as f64,1.0)&&ck(runs[i+2]as f64,3.0)&&ck(runs[i+3]as f64,1.0)&&ck(runs[i+4]as f64,1.0){hits+=1;} if ck(runs[i]as f64,1.0)&&ck(runs[i+1]as f64,1.0)&&ck(runs[i+2]as f64,3.0)&&ck(runs[i+3]as f64,1.0)&&ck(runs[i+4]as f64,1.0){hits+=1;}
} }
} }
+4 -4
View File
@@ -324,10 +324,10 @@ mod tests {
#[test] #[test]
fn test_unicode_to_shift_jis_known() { fn test_unicode_to_shift_jis_known() {
// 基本汉字应返回 Some // 基本汉字应返回 Someencoding_rs 精确映射)
assert!(unicode_to_shift_jis('中').is_some()); assert!(unicode_to_shift_jis('中').is_some(), "中 should map");
assert!(unicode_to_shift_jis('文').is_some()); assert!(unicode_to_shift_jis('文').is_some(), "文 should map");
assert!(unicode_to_shift_jis('').is_some()); assert!(unicode_to_shift_jis('').is_some(), "日 should map");
} }
#[test] #[test]
+3 -2
View File
@@ -305,13 +305,14 @@ mod tests {
#[test] #[test]
fn test_score_rule2() { fn test_score_rule2() {
let mut m = Matrix::new(3); let mut m = Matrix::new(3);
// 全部设为 dark → 4 个 2×2 同色方块
for y in 0..3u8 { for y in 0..3u8 {
for x in 0..3u8 { for x in 0..3u8 {
m.set(x, y, true); m.set(x, y, true);
} }
} }
let view = MaskedView::new(&m, 0); // 使用 raw 版(不经过掩码)验证
assert_eq!(score_rule2(&view), 4 * 3); assert_eq!(score_rule2_raw(&m), 4 * 3);
} }
#[test] #[test]
+22
View File
@@ -419,4 +419,26 @@ mod tests {
assert!(svg.contains("#FF0000")); assert!(svg.contains("#FF0000"));
assert!(svg.contains("#0000FF")); assert!(svg.contains("#0000FF"));
} }
#[test]
fn test_roundtrip_png() {
let config = QrConfig {
level: EcLevel::M,
version: VersionMode::Auto,
margin: 4,
};
let qr = QrCode::encode("Hello World", config).unwrap();
// 直接从矩阵解码
let matrix: Vec<Vec<bool>> = qr.modules().to_vec();
let result = crate::decoder::decode_matrix(&matrix)
.expect("矩阵解码往返失败");
assert_eq!(result.text, "Hello World");
// PNG 往返
let png_bytes = qr.to_png_bytes(8, None).unwrap();
let result = crate::decoder::decode_image(&png_bytes)
.expect("PNG 解码往返失败");
assert_eq!(result.text, "Hello World");
}
} }
+1 -2
View File
@@ -69,7 +69,7 @@ fn generate_qr_inner(text: &str, level_str: &str, margin: u8, size: u8, fmt: &st
if margin > 20 { if margin > 20 {
return (StatusCode::BAD_REQUEST, "边距过大(最大 20").into_response(); return (StatusCode::BAD_REQUEST, "边距过大(最大 20").into_response();
} }
if size < 1 || size > 20 { if !(1..=20).contains(&size) {
return (StatusCode::BAD_REQUEST, "模块大小需在 1-20 之间").into_response(); return (StatusCode::BAD_REQUEST, "模块大小需在 1-20 之间").into_response();
} }
@@ -77,7 +77,6 @@ fn generate_qr_inner(text: &str, level_str: &str, margin: u8, size: u8, fmt: &st
level, level,
version: VersionMode::Auto, version: VersionMode::Auto,
margin, margin,
..Default::default()
}; };
let qr = match QrCode::encode(text, config) { let qr = match QrCode::encode(text, config) {