Files
QRGen/core/tests/integration_test.rs
T
Serendipity 38be82973e feat: Logo 嵌入 — QR 码中央叠加自定义图片
- PNG 渲染:RgbaImage 上使用 imageops::overlay 叠加 logo
- SVG 渲染:base64 编码 logo 嵌入 <image> 标签
- QrCode::to_png_bytes / to_svg 新增 Option<logo_bytes> 参数
- Logo 默认占 QR 区域 25%,建议配合 H 级纠错使用
2026-06-19 21:12:44 +08:00

400 lines
11 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use qr_core::qr::{QrCode, QrConfig, VersionMode};
use qr_core::version::EcLevel;
/// 诊断: 验证格式信息编码 + 解码是否正确
#[test]
fn test_format_info_roundtrip() {
use qr_core::matrix::patterns::{encode_format_info, encode_version_info};
// M 级 (00) + mask 0 (000): data = 00000 = 0
let fmt0 = encode_format_info(0b00, 0);
// L 级 (01) + mask 3 (011): data = 01011 = 11
let fmt1 = encode_format_info(0b01, 3);
// H 级 (10) + mask 7 (111): data = 10111 = 23
let fmt2 = encode_format_info(0b10, 7);
// 不同输入应产生不同输出
assert_ne!(fmt0, fmt1);
assert_ne!(fmt1, fmt2);
assert_ne!(fmt0, fmt2);
// 应该在 15-bit 范围内
assert!(fmt0 < 0x8000);
assert!(fmt1 < 0x8000);
println!("格式信息 M+mask0: 0x{:04X}", fmt0);
println!("格式信息 L+mask3: 0x{:04X}", fmt1);
println!("格式信息 H+mask7: 0x{:04X}", fmt2);
// 版本信息编码
let v7 = encode_version_info(7);
println!("版本信息 v7: 0x{:06X}", v7);
// 前 6 bit 是版本号
assert_eq!((v7 >> 12) & 0x3F, 7);
}
/// 诊断: 打印 QR 码的格式信息比特
#[test]
fn test_dump_format_info() {
let qr = QrCode::encode("HELLO", QrConfig::default()).unwrap();
let m = qr.modules();
println!("\n=== 格式信息 15 bit (bit14 到 bit0) ===");
println!("EC 级别: {:?}, 掩码: {}", qr.level, qr.mask);
// 格式信息位置 (按标准顺序 bit14→bit0)
let coords = [
(0, 8),
(1, 8),
(2, 8),
(3, 8),
(4, 8),
(5, 8),
(7, 8),
(8, 8),
(8, 7),
(8, 5),
(8, 4),
(8, 3),
(8, 2),
(8, 1),
(8, 0),
];
let mut fmt_bits = 0u16;
for (i, &(x, y)) in coords.iter().enumerate() {
let bit = if m[y as usize][x as usize] {
1u16
} else {
0u16
};
fmt_bits = (fmt_bits << 1) | bit;
print!(
"{} ",
if m[y as usize][x as usize] {
'█'
} else {
'_'
}
);
}
println!();
println!(
"读取的格式信息 (原始, 含 XOR mask 0x5412): 0x{:04X}",
fmt_bits
);
// 去掉 XOR mask
let unmasked = fmt_bits ^ 0x5412;
println!("去 XOR mask 后: 0x{:04X}", unmasked);
let ec_bits = (unmasked >> 13) & 0x03;
let mask_bits = (unmasked >> 10) & 0x07;
let bch = unmasked & 0x3FF;
println!(
"EC bits: {:02b} 掩码 bits: {:03b} BCH: 0x{:03X}",
ec_bits, mask_bits, bch
);
// 期望值
let expected = {
use qr_core::matrix::patterns::encode_format_info;
encode_format_info(qr.level.indicator_bits(), qr.mask)
};
println!("期望的格式信息: 0x{:04X}", expected);
println!(
"匹配: {}",
if fmt_bits == expected {
"✅"
} else {
"❌ 不匹配!"
}
);
assert_eq!(fmt_bits, expected, "格式信息不匹配!");
}
#[test]
fn test_finder_patterns_present() {
let qr = QrCode::encode("HELLO", QrConfig::default()).unwrap();
let size = qr.size() as usize;
let m = qr.modules();
// 三个定位图案: (0,0), (size-7,0), (0,size-7)
let s = size as u8;
let finders: [(u8, u8); 3] = [(0, 0), (s - 7, 0), (0, s - 7)];
for (fx, fy) in finders {
assert!(
m[fy as usize][fx as usize],
"定位({},{}): 左上角应为暗",
fx, fy
);
assert!(
m[fy as usize][(fx + 6) as usize],
"定位({},{}): 右上角应为暗",
fx,
fy
);
assert!(
m[(fy + 6) as usize][fx as usize],
"定位({},{}): 左下角应为暗",
fx,
fy
);
// 内部 3×3 是暗色
assert!(
m[(fy + 2) as usize][(fx + 2) as usize],
"定位({},{}): 中心3×3应为暗",
fx,
fy
);
}
}
#[test]
fn test_timing_pattern_alternates() {
let qr = QrCode::encode("HELLO", QrConfig::default()).unwrap();
let m = qr.modules();
let s = qr.size() as usize;
// 行6: 从列8到列s-8-1 应交替
for x in (8..s - 8).step_by(2) {
assert!(m[6][x], "时序 y=6 x={}: 偶数应为暗", x);
if x + 1 < s - 8 {
assert!(!m[6][x + 1], "时序 y=6 x={}: 奇数应为亮", x + 1);
}
}
// 列6: 从行8到行s-8-1 应交替
for y in (8..s - 8).step_by(2) {
assert!(m[y][6], "时序 x=6 y={}: 偶数应为暗", y);
if y + 1 < s - 8 {
assert!(!m[y + 1][6], "时序 x=6 y={}: 奇数应为亮", y + 1);
}
}
}
#[test]
fn test_dark_module_present() {
let qr = QrCode::encode("HELLO", QrConfig::default()).unwrap();
let m = qr.modules();
let s = qr.size() as usize;
// 暗模块总是位于 (8, size-8)
assert!(m[s - 8][8], "暗模块 (8,{}) 缺失!", s - 8);
}
#[test]
fn test_format_info_written() {
let qr = QrCode::encode("HELLO", QrConfig::default()).unwrap();
let m = qr.modules();
// 格式信息在定位图案旁,检查几个位置不是全亮
assert!(
m[8][0] || m[8][1] || m[8][2] || !m[8][0],
"格式信息应已写入"
);
}
#[test]
fn test_svg_valid_structure() {
let qr = QrCode::encode("HELLO", QrConfig::default()).unwrap();
let svg = qr.to_svg(None);
// SVG 应有正确的结构
assert!(svg.starts_with("<svg"), "SVG 应以 <svg 开头");
assert!(svg.contains("rect"), "SVG 应包含 rect 元素");
assert!(svg.contains("fill=\"black\""), "SVG 暗模块应是黑色");
assert!(
svg.ends_with("</svg>\n") || svg.ends_with("</svg>"),
"SVG 应以 </svg> 结尾"
);
}
#[test]
fn test_quiet_zone_is_white() {
let qr = QrCode::encode("HELLO", QrConfig::default()).unwrap();
let m = qr.modules();
let s = qr.size() as usize;
// 左上角分隔符区域 (7,0..7) 和 (0..7,7) 应为白色
for i in 0..8usize {
assert!(!m[7][i], "定位分隔符 (7,{}) 应为白色", i);
assert!(!m[i][7], "定位分隔符 ({},7) 应为白色", i);
}
}
#[test]
fn test_qr_structure_dump() {
// 打印矩阵到 stdout(用于调试)
let qr = QrCode::encode("HELLO", QrConfig::default()).unwrap();
let size = qr.size() as usize;
let m = qr.modules();
println!(
"\n=== QR Matrix {}x{} v{} mask{} ===",
size, size, qr.version.0, qr.mask
);
for y in 0..size {
for x in 0..size {
print!("{}", if m[y][x] { "##" } else { " " });
}
println!();
}
// 统计
let dark: usize = m.iter().flatten().filter(|&&x| x).count();
let total = size * size;
println!(
"\n暗/总: {}/{} = {:.1}%",
dark,
total,
dark as f64 / total as f64 * 100.0
);
println!("尺寸: {}×{}", size, size);
println!(
"版本: {} 掩码: {} 纠错: {:?}",
qr.version.0, qr.mask, qr.level
);
}
#[test]
fn test_encode_simple_text() {
let config = QrConfig::default();
let qr = QrCode::encode("HELLO WORLD", config).unwrap();
assert_eq!(qr.version.0, 1);
assert_eq!(qr.size(), 21);
}
#[test]
fn test_all_levels() {
for level in [EcLevel::L, EcLevel::M, EcLevel::Q, EcLevel::H] {
let config = QrConfig {
level,
..Default::default()
};
let qr = QrCode::encode("TEST", config).unwrap();
assert!(qr.size() >= 21);
assert!(qr.size() <= 177);
}
}
#[test]
fn test_chinese_text() {
let config = QrConfig::default();
let qr = QrCode::encode("你好世界", config).unwrap();
assert!(qr.size() >= 21);
}
#[test]
fn test_url_encoding() {
let config = QrConfig::default();
let qr = QrCode::encode("https://example.com/path?q=1", config).unwrap();
assert!(qr.size() >= 21);
}
#[test]
fn test_numeric_only_small_version() {
let mut config = QrConfig::default();
config.version = VersionMode::Fixed(1);
let qr = QrCode::encode("12345678901234567890", config).unwrap();
assert_eq!(qr.version.0, 1);
}
#[test]
fn test_fixed_version() {
let config = QrConfig {
version: VersionMode::Fixed(5),
..Default::default()
};
let qr = QrCode::encode("FIXED VERSION TEST", config).unwrap();
assert_eq!(qr.version.0, 5);
}
#[test]
fn test_empty_input_fails() {
let config = QrConfig::default();
let result = QrCode::encode("", config);
assert!(result.is_err());
}
#[test]
fn test_svg_output() {
let qr = QrCode::encode("TEST", QrConfig::default()).unwrap();
let svg = qr.to_svg(None);
assert!(svg.contains("<svg"));
assert!(svg.contains("</svg>"));
assert!(svg.contains("fill=\"black\""));
}
#[test]
fn test_ascii_output() {
let qr = QrCode::encode("TEST", QrConfig::default()).unwrap();
let ascii = qr.to_ascii(false);
assert!(!ascii.is_empty());
assert!(ascii.contains('\n'));
// 应该有暗模块
assert!(ascii.contains("██"));
}
#[test]
fn test_png_output() {
let qr = QrCode::encode("TEST", QrConfig::default()).unwrap();
let png = qr.to_png_bytes(4, None).unwrap();
assert!(!png.is_empty());
// PNG 文件应以 8 字节魔术签名开头
assert_eq!(&png[..8], &[137, 80, 78, 71, 13, 10, 26, 10]);
}
#[test]
fn test_modules_matrix() {
let qr = QrCode::encode("QR", QrConfig::default()).unwrap();
let modules = qr.modules();
assert_eq!(modules.len(), qr.size() as usize);
assert_eq!(modules[0].len(), qr.size() as usize);
// 至少有一些暗模块
let has_dark = modules.iter().any(|row| row.iter().any(|&m| m));
assert!(has_dark, "QR 码应该包含暗模块");
}
#[test]
fn test_margin_is_included_in_dimensions() {
let mut config = QrConfig::default();
config.margin = 2;
let qr = QrCode::encode("MARGIN TEST", config).unwrap();
// SVG 的总宽度应该包含 margin
let svg = qr.to_svg(None);
let matrix_size = qr.size() as u32;
let expected_total = matrix_size + 2 * 2u32;
assert!(svg.contains(&format!("width=\"{}\"", expected_total)));
// ASCII 输出行应该包含 margin 列
let ascii = qr.to_ascii(false);
let first_line = ascii.lines().next().unwrap();
let chars_per_module = 2; // ██ 是两个字符
assert_eq!(
first_line.chars().count(),
expected_total as usize * chars_per_module
);
}
#[test]
fn test_long_text_auto_version() {
// 长文本应该自动选择更高的版本
let long_text = "A".repeat(200);
let qr = QrCode::encode(&long_text, QrConfig::default()).unwrap();
// 至少要 Version 2 以上
assert!(qr.version.0 >= 2);
}
#[test]
fn test_special_chars() {
let config = QrConfig::default();
let qr = QrCode::encode("$%*+-./: SPACE", config).unwrap();
assert!(qr.size() >= 21);
}
#[test]
fn test_numeric_mode_efficiency() {
// 纯数字在 Version 1 L 级最多 41 位(约 7089 位数字)
let digits = "1".repeat(20);
let mut config = QrConfig::default();
config.version = VersionMode::Fixed(1);
config.level = EcLevel::L;
let qr = QrCode::encode(&digits, config).unwrap();
assert_eq!(qr.version.0, 1);
}