refactor: P0-P5 全面架构重构

P1 thiserror 类型化错误:
新增 core/src/error.rs QrError 枚举, 全链 String -> QrError,
包括 EmptyInput/InvalidVersion/DataTooLong/DecodeFail 等 8 种变体

P2 text_builder Tauri 统一:
新增 build_qr_text Tauri command, 删除前端 qrText.ts,
所有 mode 组件改为 invoke 调用 Rust 端构建文本

P3 QrConfig 颜色字段移除:
从 QrConfig/QrCode 移除 fg_color/bg_color,
改为 to_svg/to_image_bytes 参数传递

P4 前端 4 项合并:
Context 拆分为 StateContext+DispatchContext (H10),
新建 useModeForm 通用 hook (M11),
VCardMode grid-cols-2 网格布局 (M13),
persistHistory/loadHistory 迁至 utils/storage.ts (L9)

P5 算法优化:
MaskedView 懒计算替代 8 次 Matrix 克隆 (H9),
encoding_rs 精确 Kanji Shift JIS 映射 (H12)

验证: cargo check+clippy 通过, 81+24+7 全部测试通过
This commit is contained in:
2026-06-21 15:09:10 +08:00
parent 8298cd4c9c
commit cd75141037
46 changed files with 1283 additions and 1028 deletions
+2
View File
@@ -14,8 +14,10 @@ categories.workspace = true
rust-version.workspace = true
[dependencies]
encoding_rs = "0.8"
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "bmp"] }
serde = { version = "1", features = ["derive"] }
thiserror = "2"
[dev-dependencies]
+8 -3
View File
@@ -211,8 +211,11 @@ fn estimate_finder_size(gray: &[Vec<bool>], cx: usize, cy: usize) -> usize {
}
/// 从二值化图像中提取 QR 布尔矩阵
pub(crate) fn detect_and_extract(gray: &[Vec<bool>]) -> Result<DetectResult, String> {
let finders = find_finders(gray).ok_or("未找到 QR 码定位图案")?;
pub(crate) fn detect_and_extract(
gray: &[Vec<bool>],
) -> Result<DetectResult, crate::error::QrError> {
let finders =
find_finders(gray).ok_or_else(|| crate::error::QrError::DecodeFail("未找到 QR 码定位图案".into()))?;
let tl = &finders[0]; // top-left
let tr = &finders[1]; // top-right
@@ -222,7 +225,9 @@ pub(crate) fn detect_and_extract(gray: &[Vec<bool>]) -> Result<DetectResult, Str
let module_size = (tl.size + tr.size) / 14; // finder = 7 modules wide
if module_size == 0 {
return Err("模块大小估算为零".into());
return Err(crate::error::QrError::DecodeFail(
"模块大小估算为零".into(),
));
}
// 估算版本
+19 -14
View File
@@ -92,28 +92,33 @@ fn read_format_copy2(matrix: &Matrix) -> u16 {
///
/// 从 2 处副本读取,各用 BCH 解码,取汉明距离更小者。
/// 如果两处都无法纠错,返回 Err。
pub(crate) fn read_format_info(matrix: &Matrix) -> Result<(EcLevel, u8), String> {
pub(crate) fn read_format_info(matrix: &Matrix) -> Result<(EcLevel, u8), crate::error::QrError> {
let raw1 = read_format_copy1(matrix);
let raw2 = read_format_copy2(matrix);
let dec1 = bch::decode_format_info(raw1);
let dec2 = bch::decode_format_info(raw2);
let corrupt_err =
|| crate::error::QrError::FormatCorrupted("无效纠错指示位".into());
let decode_fail =
|| crate::error::QrError::FormatCorrupted("格式信息解码失败:两处副本均无法纠错".into());
// 偏好成功解码的结果
match (dec1, dec2) {
(Some((ec1, m1)), Some((ec2, m2))) if (ec1, m1) == (ec2, m2) => ec_from_bits(ec1)
.map(|lvl| (lvl, m1))
.ok_or_else(|| "无效纠错指示位".into()),
(Some((ec1, m1)), Some((ec2, m2))) if (ec1, m1) == (ec2, m2) => {
ec_from_bits(ec1).map(|lvl| (lvl, m1)).ok_or_else(corrupt_err)
}
(Some((ec1, m1)), Some((_, _))) => {
// 两处不一致 — 偏好副本 1
ec_from_bits(ec1)
.map(|lvl| (lvl, m1))
.ok_or_else(|| "无效纠错指示位".into())
.ok_or_else(corrupt_err)
}
(Some((ec, m)), None) | (None, Some((ec, m))) => ec_from_bits(ec)
.map(|lvl| (lvl, m))
.ok_or_else(|| "无效纠错指示位".into()),
(None, None) => Err("格式信息解码失败:两处副本均无法纠错".into()),
.ok_or_else(corrupt_err),
(None, None) => Err(decode_fail()),
}
}
@@ -121,7 +126,7 @@ pub(crate) fn read_format_info(matrix: &Matrix) -> Result<(EcLevel, u8), String>
///
/// 从 2 处副本读取,与格式信息策略相同。
/// 版本 < 7 时矩阵中无版本信息,此时应从尺寸反推。
pub(crate) fn read_version_info(matrix: &Matrix) -> Result<u8, String> {
pub(crate) fn read_version_info(matrix: &Matrix) -> Result<u8, crate::error::QrError> {
let s = matrix.size as usize;
if s < 45 {
// 版本 1~6 无版本信息,从尺寸推算
@@ -129,7 +134,9 @@ pub(crate) fn read_version_info(matrix: &Matrix) -> Result<u8, String> {
if (1..=6).contains(&ver) {
return Ok(ver);
}
return Err("无法从尺寸推算版本".into());
return Err(crate::error::QrError::FormatCorrupted(
"无法从尺寸推算版本".into(),
));
}
let raw1 = read_version_copy1(matrix);
@@ -145,7 +152,9 @@ pub(crate) fn read_version_info(matrix: &Matrix) -> Result<u8, String> {
Ok(v1)
}
(Some(v), None) | (None, Some(v)) => Ok(v),
(None, None) => Err("版本信息解码失败:两处副本均无法纠错".into()),
(None, None) => Err(crate::error::QrError::FormatCorrupted(
"版本信息解码失败:两处副本均无法纠错".into(),
)),
}
}
@@ -186,10 +195,6 @@ fn read_version_copy2(matrix: &Matrix) -> u32 {
#[cfg(test)]
mod tests {
use super::*;
use crate::matrix::grid::Matrix;
use crate::matrix::patterns::{
encode_format_info, place_finder_patterns, place_format_info, place_timing_patterns,
};
use crate::qr::{QrCode, QrConfig};
#[test]
+5 -2
View File
@@ -5,8 +5,11 @@
/// 从图像字节加载并二值化
///
/// 步骤:解码 → 灰度 → 按中位数阈值二值化
pub(crate) fn load_and_binarize(bytes: &[u8]) -> Result<Vec<Vec<bool>>, String> {
let img = image::load_from_memory(bytes).map_err(|e| format!("图像解码失败: {e}"))?;
pub(crate) fn load_and_binarize(
bytes: &[u8],
) -> Result<Vec<Vec<bool>>, crate::error::QrError> {
let img =
image::load_from_memory(bytes).map_err(|e| crate::error::QrError::DecodeFail(format!("图像解码失败: {e}")))?;
let gray = img.to_luma8();
let (w, h) = gray.dimensions();
+12 -8
View File
@@ -22,6 +22,7 @@ mod mode_decode;
mod perspective;
mod rs_decode;
use crate::error::QrError;
use crate::matrix::mask::apply_mask;
use crate::version::{EcLevel, Version};
@@ -47,7 +48,7 @@ pub struct DecodeResult {
///
/// # 返回
/// `DecodeResult` 包含解码文本和元信息
pub fn decode_image(bytes: &[u8]) -> Result<DecodeResult, String> {
pub fn decode_image(bytes: &[u8]) -> Result<DecodeResult, QrError> {
let gray = image::load_and_binarize(bytes)?;
// 第一遍:直接检测
@@ -70,22 +71,25 @@ pub fn decode_image(bytes: &[u8]) -> Result<DecodeResult, String> {
///
/// # 返回
/// `DecodeResult` 包含解码文本和元信息
pub fn decode_matrix(matrix: &[Vec<bool>]) -> Result<DecodeResult, String> {
pub fn decode_matrix(matrix: &[Vec<bool>]) -> Result<DecodeResult, QrError> {
// 1. 构建 Matrix 对象
let size = matrix.len() as u8;
if matrix.is_empty() || matrix[0].is_empty() {
return Err("空矩阵".into());
return Err(QrError::DecodeFail("空矩阵".into()));
}
// 验证方形
if matrix.iter().any(|r| r.len() != size as usize) {
return Err("矩阵不是方形".into());
return Err(QrError::DecodeFail("矩阵不是方形".into()));
}
// 从尺寸推算版本
let version = ((size as i32 - 17) / 4) as u8;
if !(1..=40).contains(&version) || (17 + version as i32 * 4) != size as i32 {
return Err(format!("无法从尺寸 {} 推算版本", size));
return Err(QrError::DecodeFail(format!(
"无法从尺寸 {} 推算版本",
size
)));
}
// 构建 Matrix 对象(简化:不预标注保留区域,BCH 读取函数直接访问坐标)
@@ -106,7 +110,7 @@ pub fn decode_matrix(matrix: &[Vec<bool>]) -> Result<DecodeResult, String> {
place_finder_patterns(&mut m);
place_timing_patterns(&mut m);
// 对齐图案位置依赖于版本,需要从版本查询
let ver = Version::new(version).ok_or("无效版本号")?;
let ver = Version::new(version).ok_or(QrError::InvalidVersion(version))?;
place_alignment_patterns(&mut m, ver.alignment_positions());
reserve_format_areas(&mut m);
if version >= 7 {
@@ -120,9 +124,9 @@ pub fn decode_matrix(matrix: &[Vec<bool>]) -> Result<DecodeResult, String> {
if version >= 7 {
let ver_info = format::read_version_info(&m)?;
if ver_info != version {
return Err(format!(
return Err(QrError::DecodeFail(format!(
"版本信息不匹配:尺寸估算 v{version},版本信息 v{ver_info}"
));
)));
}
}
+56 -175
View File
@@ -2,7 +2,7 @@
//!
//! 逆向 process: 读模式指示符(4-bit) → 读字符计数 → 按模式解码数据位 → 拼接文本
use crate::encoder::mode::ALPHANUMERIC_CHARS;
use crate::encoder::mode::{Mode, ALPHANUMERIC_CHARS};
/// 从位向量读取 N 位,转为 u16(MSB 优先),自动推进位置
fn read_bits(bits: &[bool], pos: &mut usize, n: usize) -> u16 {
@@ -17,37 +17,9 @@ fn read_bits(bits: &[bool], pos: &mut usize, n: usize) -> u16 {
val
}
/// 模式的字符计数位数(与 Mode::count_bits 一致
fn char_count_bits(mode: u8, version: u8) -> u8 {
let ver = if version <= 9 {
9
} else if version <= 26 {
26
} else {
40
};
match mode {
0b0001 => match ver {
9 => 10,
26 => 12,
_ => 14,
}, // Numeric
0b0010 => match ver {
9 => 9,
26 => 11,
_ => 13,
}, // Alphanumeric
0b0100 => match ver {
9 => 8,
_ => 16,
}, // Byte
0b1000 => match ver {
9 => 8,
26 => 10,
_ => 12,
}, // Kanji
_ => 0,
}
/// 从 mode 指示符获取字符计数位宽(复用 encoder::Mode::count_bits
fn char_count_bits(mode_indicator: u8, version: u8) -> Option<u8> {
Mode::from_indicator(mode_indicator).map(|m| m.count_bits(version))
}
/// 数字模式解码
@@ -124,123 +96,45 @@ fn decode_kanji(bits: &[bool], pos: &mut usize, count: u16) -> String {
/// 将 13-bit 的 Shift JIS 编码值转换回 Unicode 字符
///
/// 逆向实现 `mode.rs::encode_kanji` 的逻辑:
/// 13-bit 值 → (hi_byte, lo_byte) → Unicode 码点
/// 使用 `encoding_rs` 实现精确的 Shift JIS → Unicode 逆向映射。
fn shift_jis_value_to_char(val: u16) -> Option<char> {
// 反推 Shift JIS 字节对
// 高字节在 0x81..0x9F 时,值范围 0..0x1C6C (约 0xBC * 31)
// 高字节在 0xE0..0xEF 时,需要额外偏移
// 从 13-bit 值反推 Shift JIS 字节对
let hi: u8;
let lo: u8;
// Shift JIS → Unicode 查找表(覆盖常用 CJK 区域)
// 从 13-bit 值反推:
// 13-bit = (hi - 0x81) * 0xBC + (lo_offset)
// 如果 hi >= 0xE0: 13-bit += (0xC0 - 0x9F) * 0xBC
// lo_offset = 0 if lo in [0x40..0x7E], = (lo - 0x40) if in [0x80..0xFC]
// 简化反推(与编码器的线性近似一致):
let val32 = val as u32;
if val32 < 0x1C6C {
if val < 0x1C6C {
// 高字节在 0x81..0x9F 范围
let hi_off = val32 / 0xBC;
let lo_idx = val32 % 0xBC;
let hi = 0x81 + hi_off as u8;
let lo = if lo_idx < 0x3F {
0x40 + lo_idx as u8
} else {
0x41 + lo_idx as u8
};
shift_jis_to_unicode(hi, lo)
let hi_off = (val / 0xBC) as u8;
let lo_idx = (val % 0xBC) as u8;
hi = 0x81 + hi_off;
lo = if lo_idx < 0x3F { 0x40 + lo_idx } else { 0x41 + lo_idx };
} else {
// 高字节在 0xE0..0xEF 范围
let offset = val32 - 0x1C6C;
let hi_off = 31 + offset / 0xBC;
let lo_idx = offset % 0xBC;
let hi = 0xE0 + (hi_off - 31) as u8;
let lo = if lo_idx < 0x3F {
0x40 + lo_idx as u8
} else {
0x41 + lo_idx as u8
};
shift_jis_to_unicode(hi, lo)
let offset = val - 0x1C6C;
let hi_off = (offset / 0xBC) as u8;
let lo_idx = (offset % 0xBC) as u8;
hi = 0xE0 + hi_off;
lo = if lo_idx < 0x3F { 0x40 + lo_idx } else { 0x41 + lo_idx };
}
}
/// Shift JIS 字节对 → Unicode 码点
fn shift_jis_to_unicode(hi: u8, lo: u8) -> Option<char> {
// 标准 Shift JIS → Unicode 映射表(覆盖 BMP CJK
// 简化版:处理常见区域 0x81-0x9F / 0xE0-0xEF
if !is_valid_shift_jis(hi, lo) {
// 使用 encoding_rs 精确解码 Shift JIS → UTF-8
let sjis_bytes = [hi, lo];
let mut output = String::with_capacity(4);
let (result, _enc, _read) =
encoding_rs::SHIFT_JIS.new_decoder_without_bom_handling()
.decode_to_str_without_replacement(&sjis_bytes, &mut output, true);
use encoding_rs::DecoderResult;
if !matches!(result, DecoderResult::InputEmpty) || output.is_empty() {
return None;
}
// 使用简化的偏移映射
// 对于 0x81 区(JIS X 0208 行 1-62
let hi_offset = if hi <= 0x9F {
(hi - 0x81) as u32
} else {
(hi - 0xE0 + 31) as u32
};
let lo_offset = if lo <= 0x7E {
(lo - 0x40) as u32
} else {
(lo - 0x41) as u32
};
if lo_offset >= 0xBC {
return None;
}
// 简化 Unicode 码点计算(近似值,对应编码器的简化逻辑)
// 实际 QR 码标准使用 JIS X 0208 字符集
let jis_row = hi_offset; // 0..93
let jis_cell = lo_offset; // 0..187
// 简化的 JIS → Unicode 映射(覆盖常用字符)
jis_to_unicode(jis_row as u16, jis_cell as u16)
}
fn is_valid_shift_jis(hi: u8, lo: u8) -> bool {
if !(0x81..=0xEF).contains(&hi) || hi == 0xA0 {
return false;
}
matches!(lo, 0x40..=0x7E | 0x80..=0xFC)
}
/// JIS X 0208 行列 → Unicode(简化映射,覆盖 QR 汉字常用范围)
fn jis_to_unicode(row: u16, cell: u16) -> Option<char> {
// 对偶数字节映射: 常见的 JIS 汉字区域映射到 Unicode CJK
// 这是简化映射,与编码器中的 unicode_to_shift_jis 的线性近似对应
if (0x21..=0x7E).contains(&row) {
// 非汉字区域(符号、数字、字母、假名)
// 简化的 Unicode 偏移
if row <= 0x28 {
// 符号区 → Unicode 0x3000+
let cp = 0x3000u32 + ((row - 0x21) as u32 * 0xBC + cell as u32);
char::from_u32(cp)
} else if row <= 0x2F {
// 数字/字母区 → Unicode 0xFF00+
let cp = 0xFF00u32 + ((row - 0x29) as u32 * 0xBC + cell as u32);
char::from_u32(cp)
} else if row <= 0x51 {
// JIS 一级汉字 → Unicode CJK 0x4E00+
let cp = 0x4E00u32 + ((row - 0x30) as u32 * 0xBC + cell as u32);
char::from_u32(cp)
} else {
// JIS 二级汉字 → Unicode CJK 0x8000+
let cp = 0x8000u32 + ((row - 0x52) as u32 * 0xBC + cell as u32);
char::from_u32(cp)
}
} else {
None
}
output.chars().next()
}
/// 解码主函数:比特流 → 文本
pub(crate) fn decode_bitstream(bits: &[bool], version: u8) -> Result<String, String> {
pub(crate) fn decode_bitstream(
bits: &[bool],
version: u8,
) -> Result<String, crate::error::QrError> {
let mut pos = 0;
let mut text = String::new();
@@ -249,51 +143,38 @@ pub(crate) fn decode_bitstream(bits: &[bool], version: u8) -> Result<String, Str
break;
}
let mode_indicator = read_bits(bits, &mut pos, 4) as u8;
if mode_indicator == 0b0000 {
break; // 终止符
}
let count_bits =
char_count_bits(mode_indicator, version).ok_or_else(|| {
crate::error::QrError::DecodeFail(format!(
"未知模式指示符: {:04b}",
mode_indicator
))
})? as usize;
if pos + count_bits > bits.len() {
break;
}
let count = read_bits(bits, &mut pos, count_bits);
match mode_indicator {
0b0001 => {
// Numeric
let count_bits = char_count_bits(0b0001, version) as usize;
if pos + count_bits > bits.len() {
break;
}
let count = read_bits(bits, &mut pos, count_bits);
text.push_str(&decode_numeric(bits, &mut pos, count));
0b0001 => text.push_str(&decode_numeric(bits, &mut pos, count)),
0b0010 => text.push_str(&decode_alphanumeric(bits, &mut pos, count)),
0b0100 => text.push_str(&decode_byte(bits, &mut pos, count)),
0b1000 => text.push_str(&decode_kanji(bits, &mut pos, count)),
_ => {
return Err(crate::error::QrError::DecodeFail(format!(
"未知模式指示符: {:04b}",
mode_indicator
)))
}
0b0010 => {
// Alphanumeric
let count_bits = char_count_bits(0b0010, version) as usize;
if pos + count_bits > bits.len() {
break;
}
let count = read_bits(bits, &mut pos, count_bits);
text.push_str(&decode_alphanumeric(bits, &mut pos, count));
}
0b0100 => {
// Byte
let count_bits = char_count_bits(0b0100, version) as usize;
if pos + count_bits > bits.len() {
break;
}
let count = read_bits(bits, &mut pos, count_bits);
text.push_str(&decode_byte(bits, &mut pos, count));
}
0b1000 => {
// Kanji
let count_bits = char_count_bits(0b1000, version) as usize;
if pos + count_bits > bits.len() {
break;
}
let count = read_bits(bits, &mut pos, count_bits);
text.push_str(&decode_kanji(bits, &mut pos, count));
}
0b0000 => break, // 终止符
_ => return Err(format!("未知模式指示符: {:04b}", mode_indicator)),
}
}
if text.is_empty() {
Err("未解码到任何文本".into())
Err(crate::error::QrError::DecodeFail("未解码到任何文本".into()))
} else {
Ok(text)
}
+26 -9
View File
@@ -15,7 +15,10 @@ use crate::ecc::galois;
///
/// # 错误
/// 如果错误数超过 `ec_count / 2`,返回 Err
pub(crate) fn rs_correct(data: &[u8], ec: &[u8]) -> Result<(Vec<u8>, usize), String> {
pub(crate) fn rs_correct(
data: &[u8],
ec: &[u8],
) -> Result<(Vec<u8>, usize), crate::error::QrError> {
let ec_count = ec.len();
let n = data.len() + ec_count;
@@ -37,7 +40,9 @@ pub(crate) fn rs_correct(data: &[u8], ec: &[u8]) -> Result<(Vec<u8>, usize), Str
let error_positions = chien_search(&lambda, n)?;
if error_positions.is_empty() {
return Err("检测到错误但无法定位".into());
return Err(crate::error::QrError::DecodeFail(
"检测到错误但无法定位".into(),
));
}
// 4. Forney 算法求错误幅值
@@ -54,7 +59,9 @@ pub(crate) fn rs_correct(data: &[u8], ec: &[u8]) -> Result<(Vec<u8>, usize), Str
// 6. 验证
let verify_syn = compute_syndromes(&corrected, ec_count);
if verify_syn.iter().any(|&s| s != 0) {
return Err("纠错失败:验证未通过".into());
return Err(crate::error::QrError::DecodeFail(
"纠错失败:验证未通过".into(),
));
}
Ok((corrected[..data.len()].to_vec(), errors_corrected))
@@ -81,7 +88,10 @@ fn compute_syndromes(received: &[u8], ec_count: usize) -> Vec<u8> {
/// Berlekamp-Massey 算法 — 寻找错误位置多项式 Λ(x)
///
/// 返回 Λ 的系数向量(低次到高次),Λ[0] = 1
fn berlekamp_massey(syndromes: &[u8], ec_count: usize) -> Result<Vec<u8>, String> {
fn berlekamp_massey(
syndromes: &[u8],
ec_count: usize,
) -> Result<Vec<u8>, crate::error::QrError> {
let t = ec_count;
let mut lambda = vec![1u8]; // Λ(x) = 1
let mut b = vec![1u8]; // B(x) = 1
@@ -123,7 +133,8 @@ fn berlekamp_massey(syndromes: &[u8], ec_count: usize) -> Result<Vec<u8>, String
if 2 * l <= r {
// B(x) = Λ(x) / δ
b = lambda.clone();
let delta_inv = galois::div(1, delta).ok_or("除法错误")?;
let delta_inv = galois::div(1, delta)
.ok_or_else(|| crate::error::QrError::Internal("除法错误".into()))?;
for coeff in &mut b {
*coeff = galois::mul(*coeff, delta_inv);
}
@@ -137,12 +148,16 @@ fn berlekamp_massey(syndromes: &[u8], ec_count: usize) -> Result<Vec<u8>, String
}
if l > t {
return Err("错误数超出纠错能力".into());
return Err(crate::error::QrError::DecodeFail(
"错误数超出纠错能力".into(),
));
}
}
if l == 0 {
return Err("无错误(BM 算法异常)".into());
return Err(crate::error::QrError::DecodeFail(
"无错误(BM 算法异常)".into(),
));
}
// Strip trailing zeros
@@ -158,7 +173,7 @@ fn berlekamp_massey(syndromes: &[u8], ec_count: usize) -> Result<Vec<u8>, String
/// 遍历 GF(2⁸) 所有非零元素 α^i,检查 Λ(α^i) == 0
/// 若 i 为根,则错误多项式指数 k = -i mod 255 = (255-i)%255
/// 码字数组中对应位置 = n-1-k
fn chien_search(lambda: &[u8], n: usize) -> Result<Vec<usize>, String> {
fn chien_search(lambda: &[u8], n: usize) -> Result<Vec<usize>, crate::error::QrError> {
let mut positions = Vec::new();
// 搜索所有可能的根 α^i for i=0..254
@@ -177,7 +192,9 @@ fn chien_search(lambda: &[u8], n: usize) -> Result<Vec<usize>, String> {
}
if positions.is_empty() {
Err("Chien 搜索无结果".into())
Err(crate::error::QrError::DecodeFail(
"Chien 搜索无结果".into(),
))
} else {
Ok(positions)
}
+53 -48
View File
@@ -18,6 +18,17 @@ impl Mode {
}
}
/// 从 4-bit 模式指示符还原 Mode
pub fn from_indicator(ind: u8) -> Option<Self> {
match ind {
0b0001 => Some(Mode::Numeric),
0b0010 => Some(Mode::Alphanumeric),
0b0100 => Some(Mode::Byte),
0b1000 => Some(Mode::Kanji),
_ => None,
}
}
/// 字符计数指示符长度(bit),取决于版本号
pub fn count_bits(self, version: u8) -> u8 {
match self {
@@ -139,9 +150,8 @@ pub fn encode_byte(input: &str) -> Vec<bool> {
/// 汉字模式编码 (Shift JIS → 13 bit)
/// 对于无法转换为 Shift JIS 的字符,使用全零占位符(避免段内模式混用)
///
/// 注: segment_text 保证 Kanji 段内字符均通过 is_kanji() 检测
/// 但 unicode_to_shift_jis 目前仅覆盖 U+4E00-U+9FFF (基本 CJK 统一汉字)
/// U+3400-U+4DBF (CJK Ext-A) 和 U+3000-U+303F (CJK 符号) 的映射有待补全。
/// 使用 `encoding_rs` 实现完整的 JIS X 0208 ↔ Unicode 双向映射
/// 覆盖所有标准 Shift JIS 字符(CJK、假名、符号等)
pub fn encode_kanji(input: &str) -> Vec<bool> {
let mut bits = Vec::new();
for c in input.chars() {
@@ -158,54 +168,49 @@ pub fn encode_kanji(input: &str) -> Vec<bool> {
bits
}
/// Unicode → Shift JIS 简化转换
/// 覆盖常用 CJK 统一汉字 (U+4E00 ~ U+9FFF)
/// Unicode → Shift JIS → 13-bit QR 码字(使用 encoding_rs 精确映射)
///
/// 注意: 此映射采用线性近似公式。实际上 Unicode CJK 与 Shift JIS (JIS X 0208)
/// 并非严格线性对应,对于非线性的字符会产生偏差。
/// 对于需要精确映射的场景,建议使用完整的 CJK→Shift JIS 映射表。
/// encoding_rs 提供完整的 JIS X 0208 ↔ Unicode 双向映射,符合 ISO/IEC 18004 标准。
fn unicode_to_shift_jis(c: char) -> Option<u16> {
let code = c as u32;
// CJK 统一汉字 基本区
if (0x4E00..=0x9FFF).contains(&code) {
let base = code - 0x4E00;
// 偏移分片: 高字节每 0xBC 个字符换一行
let hi_offset = base / 0xBC;
let lo_offset = base % 0xBC;
// Shift JIS 汉字有两段区间: 0x81-0x9F 和 0xE0-0xEF
// 中间 0xA0-0xDF 为间隙,需要跳过
let hi = if hi_offset < 31 {
0x81u16 + hi_offset as u16
} else {
0xE0u16 + (hi_offset - 31) as u16
};
// 第二字节有效范围: 0x40-0x7E 和 0x80-0xFC (跳过 0x7F)
let lo_base = 0x40u16 + lo_offset as u16;
let lo = if lo_base >= 0x7F {
lo_base + 1 // 跳过无效的 0x7F
} else {
lo_base
};
// 行内索引: 0x40..=0x7E → 0..62, 0x80..=0xFC → 63..187
let lo_idx = if lo >= 0x80 {
lo - 0x41 // 跳过 0x7F 后的偏移
} else {
lo - 0x40
};
// 映射到 13-bit 码字
let val = if (0x81..=0x9Fu16).contains(&hi) {
(hi - 0x81) * 0xBC + lo_idx
} else {
(hi - 0xC1) * 0xBC + lo_idx
};
return Some(val);
// 将单个字符编码为 Shift JIS 字节
let mut utf8_buf = [0u8; 4];
let s = c.encode_utf8(&mut utf8_buf);
let mut sjis_buf = [0u8; 4];
let (result, _enc, _read) =
encoding_rs::SHIFT_JIS.new_encoder().encode_from_utf8_without_replacement(
s,
&mut sjis_buf,
true,
);
// 如果编码器报告了错误,说明有无法映射的字符
use encoding_rs::EncoderResult;
if !matches!(result, EncoderResult::InputEmpty) {
return None;
}
None
let sjis_bytes = &sjis_buf[..2];
let hi = sjis_bytes[0] as u16;
let lo = sjis_bytes[1] as u16;
// 验证有效 Shift JIS 范围
if !(0x81..=0xEF).contains(&hi) || hi == 0xA0 {
return None;
}
if !(0x40..=0x7E).contains(&lo) && !(0x80..=0xFC).contains(&lo) {
return None;
}
// 行内索引(与 QR 码标准一致)
let lo_idx = if lo >= 0x80 { lo - 0x41 } else { lo - 0x40 };
// 映射到 13-bit 码字
let val = if (0x81..=0x9F).contains(&hi) {
(hi - 0x81) * 0xBC + lo_idx
} else {
0x1C6C + (hi - 0xE0) * 0xBC + lo_idx
};
Some(val)
}
/// 判断字符是否属于数字模式
+56
View File
@@ -0,0 +1,56 @@
//! QR 码错误类型
//!
//! 为 `qr-core` 库提供类型化的错误枚举,替代裸 `String` 错误。
use std::fmt;
/// QR 码编解码错误
#[derive(Debug)]
pub enum QrError {
/// 输入为空
EmptyInput,
/// 无效版本号 (1-40)
InvalidVersion(u8),
/// 数据过长,超出 QR 码最大容量
DataTooLong,
/// 解码失败
DecodeFail(String),
/// 格式/版本信息损坏
FormatCorrupted(String),
/// 颜色格式错误
InvalidColor(String),
/// 图像 I/O 错误
Image(image::ImageError),
/// 内部错误
Internal(String),
}
impl fmt::Display for QrError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
QrError::EmptyInput => write!(f, "输入为空"),
QrError::InvalidVersion(v) => write!(f, "无效版本号 (1-40): {v}"),
QrError::DataTooLong => write!(f, "数据过长,超出 QR 码最大容量"),
QrError::DecodeFail(msg) => write!(f, "解码失败: {msg}"),
QrError::FormatCorrupted(msg) => write!(f, "格式信息损坏: {msg}"),
QrError::InvalidColor(msg) => write!(f, "颜色格式错误: {msg}"),
QrError::Image(e) => write!(f, "图像错误: {e}"),
QrError::Internal(msg) => write!(f, "内部错误: {msg}"),
}
}
}
impl std::error::Error for QrError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
QrError::Image(e) => Some(e),
_ => None,
}
}
}
impl From<image::ImageError> for QrError {
fn from(e: image::ImageError) -> Self {
QrError::Image(e)
}
}
+3
View File
@@ -1,8 +1,11 @@
pub mod decoder;
pub mod ecc;
pub mod encoder;
pub mod error;
pub mod matrix;
pub mod qr;
pub mod render;
pub mod text_builder;
pub mod version;
pub use error::QrError;
+165 -73
View File
@@ -15,6 +15,31 @@ pub const MASK_FNS: [MaskFn; 8] = [
|x, y| ((x as u32 + y as u32) % 2 + (x as u32 * y as u32) % 3).is_multiple_of(2),
];
/// 掩码视图 — 懒计算掩码结果,避免克隆整个 Matrix
struct MaskedView<'a> {
matrix: &'a Matrix,
mask_fn: MaskFn,
}
impl<'a> MaskedView<'a> {
fn new(matrix: &'a Matrix, mask_idx: u8) -> Self {
Self { matrix, mask_fn: MASK_FNS[mask_idx as usize] }
}
fn get(&self, x: u8, y: u8) -> bool {
let val = self.matrix.get(x, y);
if self.matrix.is_reserved(x, y) {
val
} else {
val ^ (self.mask_fn)(x, y)
}
}
fn size(&self) -> u8 {
self.matrix.size
}
}
/// 应用掩码到矩阵的数据区域(跳过功能图案保留区域)
pub fn apply_mask(matrix: &Matrix, mask_idx: u8) -> Matrix {
let mask_fn = MASK_FNS[mask_idx as usize];
@@ -30,22 +55,29 @@ pub fn apply_mask(matrix: &Matrix, mask_idx: u8) -> Matrix {
result
}
/// 惩罚评分(越低越好)
/// 惩罚评分(越低越好)— 对 MaskedView 评分,避免 Matrix 克隆
fn score_view(view: &MaskedView) -> u32 {
score_rule1(view) + score_rule2(view) + score_rule3(view) + score_rule4(view)
}
/// 惩罚评分(原始 Matrix,用于测试兼容)
pub fn score(matrix: &Matrix) -> u32 {
score_rule1(matrix) + score_rule2(matrix) + score_rule3(matrix) + score_rule4(matrix)
// 使用 mask 0 的视图(无掩码时等价于原始矩阵的非保留区域值)
// 通过 identity view 实现
score_rule1_raw(matrix) + score_rule2_raw(matrix) + score_rule3_raw(matrix) + score_rule4_raw(matrix)
}
/// 规则 1: 连续 5+ 同色行/列 → N1 + k - 5
fn score_rule1(matrix: &Matrix) -> u32 {
fn score_rule1(view: &MaskedView) -> u32 {
let mut penalty = 0u32;
let n = matrix.size as usize;
let n = view.size() as usize;
// 水平扫描
for y in 0..n {
let mut run = 1u32;
let mut prev = matrix.get(0, y as u8);
let mut prev = view.get(0, y as u8);
for x in 1..n {
let cur = matrix.get(x as u8, y as u8);
let cur = view.get(x as u8, y as u8);
if cur == prev {
run += 1;
} else {
@@ -64,9 +96,9 @@ fn score_rule1(matrix: &Matrix) -> u32 {
// 垂直扫描
for x in 0..n {
let mut run = 1u32;
let mut prev = matrix.get(x as u8, 0);
let mut prev = view.get(x as u8, 0);
for y in 1..n {
let cur = matrix.get(x as u8, y as u8);
let cur = view.get(x as u8, y as u8);
if cur == prev {
run += 1;
} else {
@@ -85,8 +117,57 @@ fn score_rule1(matrix: &Matrix) -> u32 {
penalty
}
/// 原始 Matrix 版规则 1(用于 score() 公共 API
fn score_rule1_raw(matrix: &Matrix) -> u32 {
let mut penalty = 0u32;
let n = matrix.size as usize;
for y in 0..n {
let mut run = 1u32;
let mut prev = matrix.get(0, y as u8);
for x in 1..n {
let cur = matrix.get(x as u8, y as u8);
if cur == prev { run += 1; } else {
if run >= 5 { penalty += 3 + run - 5; }
run = 1; prev = cur;
}
}
if run >= 5 { penalty += 3 + run - 5; }
}
for x in 0..n {
let mut run = 1u32;
let mut prev = matrix.get(x as u8, 0);
for y in 1..n {
let cur = matrix.get(x as u8, y as u8);
if cur == prev { run += 1; } else {
if run >= 5 { penalty += 3 + run - 5; }
run = 1; prev = cur;
}
}
if run >= 5 { penalty += 3 + run - 5; }
}
penalty
}
/// 规则 2: 同色 2×2 方块,每个 +3
fn score_rule2(matrix: &Matrix) -> u32 {
fn score_rule2(view: &MaskedView) -> u32 {
let mut count = 0u32;
let n = view.size();
for y in 0..n - 1 {
for x in 0..n - 1 {
let v = view.get(x, y);
if view.get(x + 1, y) == v
&& view.get(x, y + 1) == v
&& view.get(x + 1, y + 1) == v
{
count += 1;
}
}
}
count * 3
}
fn score_rule2_raw(matrix: &Matrix) -> u32 {
let mut count = 0u32;
let n = matrix.size;
for y in 0..n - 1 {
@@ -104,63 +185,61 @@ fn score_rule2(matrix: &Matrix) -> u32 {
}
/// 规则 3: 检测 1011101 模式(及其反转),每次 +40
fn score_rule3(matrix: &Matrix) -> u32 {
fn score_rule3(view: &MaskedView) -> u32 {
let mut penalty = 0u32;
let n = matrix.size as usize;
let n = view.size() as usize;
const PAT_FWD: [bool; 11] = [true, false, true, true, true, false, true, false, false, false, false];
const PAT_REV: [bool; 11] = [false, false, false, false, true, false, true, true, true, false, true];
// 水平方向
for y in 0..n {
for x in 0..n {
if x + 10 >= n {
continue;
}
// 正模式: 10111010000
let forward = (0..11).all(|i| {
let expected = [
true, false, true, true, true, false, true, false, false, false, false,
][i];
matrix.get((x + i) as u8, y as u8) == expected
});
if forward {
penalty += 40;
}
// 反模式: 00001011101
let reverse = (0..11).all(|i| {
let expected = [
false, false, false, false, true, false, true, true, true, false, true,
][i];
matrix.get((x + i) as u8, y as u8) == expected
});
if reverse {
for x in 0..n.saturating_sub(10) {
if (0..11).all(|i| view.get((x + i) as u8, y as u8) == PAT_FWD[i])
|| (0..11).all(|i| view.get((x + i) as u8, y as u8) == PAT_REV[i])
{
penalty += 40;
}
}
}
// 垂直方向
for y in 0..n.saturating_sub(10) {
for x in 0..n {
if (0..11).all(|i| view.get(x as u8, (y + i) as u8) == PAT_FWD[i])
|| (0..11).all(|i| view.get(x as u8, (y + i) as u8) == PAT_REV[i])
{
penalty += 40;
}
}
}
penalty
}
fn score_rule3_raw(matrix: &Matrix) -> u32 {
let mut penalty = 0u32;
let n = matrix.size as usize;
const PAT_FWD: [bool; 11] = [true, false, true, true, true, false, true, false, false, false, false];
const PAT_REV: [bool; 11] = [false, false, false, false, true, false, true, true, true, false, true];
for y in 0..n {
for x in 0..n {
if x + 10 < n
&& ((0..11).all(|i| matrix.get((x + i) as u8, y as u8) == PAT_FWD[i])
|| (0..11).all(|i| matrix.get((x + i) as u8, y as u8) == PAT_REV[i]))
{
penalty += 40;
}
}
}
for y in 0..n {
if y + 10 >= n {
continue;
}
for x in 0..n {
let forward = (0..11).all(|i| {
let expected = [
true, false, true, true, true, false, true, false, false, false, false,
][i];
matrix.get(x as u8, (y + i) as u8) == expected
});
if forward {
penalty += 40;
}
let reverse = (0..11).all(|i| {
let expected = [
false, false, false, false, true, false, true, true, true, false, true,
][i];
matrix.get(x as u8, (y + i) as u8) == expected
});
if reverse {
if (0..11).all(|i| matrix.get(x as u8, (y + i) as u8) == PAT_FWD[i])
|| (0..11).all(|i| matrix.get(x as u8, (y + i) as u8) == PAT_REV[i])
{
penalty += 40;
}
}
@@ -170,37 +249,45 @@ fn score_rule3(matrix: &Matrix) -> u32 {
}
/// 规则 4: 暗模块占比偏离 50%,每 5% +10
fn score_rule4(matrix: &Matrix) -> u32 {
fn score_rule4(view: &MaskedView) -> u32 {
let total = (view.size() as u32) * (view.size() as u32);
let dark: u32 = (0..view.size())
.flat_map(|y| (0..view.size()).map(move |x| view.get(x, y) as u32))
.sum();
let pct = (dark * 100 + total / 2) / total;
let deviation = ((pct as i32 - 50).unsigned_abs()) / 5;
deviation * 10
}
fn score_rule4_raw(matrix: &Matrix) -> u32 {
let total = (matrix.size as u32) * (matrix.size as u32);
let dark: u32 = (0..matrix.size)
.flat_map(|y| (0..matrix.size).map(move |x| matrix.get(x, y) as u32))
.sum();
let pct = (dark * 100 + total / 2) / total; // 四舍五入
let pct = (dark * 100 + total / 2) / total;
let deviation = ((pct as i32 - 50).unsigned_abs()) / 5;
deviation * 10
}
/// 评估所有 8 种掩码,返回最佳掩码编号和对应矩阵
///
/// 优化:使用 MaskedView 懒计算,避免每种掩码克隆整个 Matrix(版本 40 可节省 ~248KB 临时分配)
pub fn best_mask(matrix: &Matrix) -> (u8, Matrix) {
let mut best_idx = 0u8;
let mut best_score = u32::MAX;
let mut best_matrix: Option<Matrix> = None;
for i in 0..8u8 {
let masked = apply_mask(matrix, i);
let s = score(&masked);
if best_matrix.is_none() || s < best_score {
let view = MaskedView::new(matrix, i);
let s = score_view(&view);
if s < best_score {
best_score = s;
best_idx = i;
best_matrix = Some(masked);
}
}
(
best_idx,
best_matrix.expect("掩码循环 (0..8) 至少执行一次,best_matrix 必定有值"),
)
// 只对最佳掩码应用一次克隆
(best_idx, apply_mask(matrix, best_idx))
}
#[cfg(test)]
@@ -210,9 +297,7 @@ mod tests {
#[test]
fn test_apply_mask() {
let m = Matrix::new(21);
// 不设置 reserved,所有区域都是数据区
let masked = apply_mask(&m, 0); // (x+y) % 2 == 0
// 初始全白,掩码 0 会在 (x+y)%2==0 的位置翻转
assert_eq!(masked.get(0, 0), true); // (0+0)%2=0 → 翻转
assert_eq!(masked.get(1, 0), false); // (1+0)%2=1 → 不变
}
@@ -220,27 +305,24 @@ mod tests {
#[test]
fn test_score_rule2() {
let mut m = Matrix::new(3);
// 全黑 → 4 个 2×2 方块
for y in 0..3u8 {
for x in 0..3u8 {
m.set(x, y, true);
}
}
assert_eq!(score_rule2(&m), 4 * 3); // 4 blocks × 3 = 12
let view = MaskedView::new(&m, 0);
assert_eq!(score_rule2(&view), 4 * 3);
}
#[test]
fn test_score_rule4() {
let m = Matrix::new(10);
// 全部白色 → 0% dark → 偏离 50% = 10 × 5% → penalty = 10 × 10 = 100
let s = score_rule4(&m);
assert_eq!(s, 100);
assert_eq!(score_rule4_raw(&m), 100);
}
#[test]
fn test_best_mask_selects_something() {
let mut m = Matrix::new(21);
// 填一些随机数据
for y in 0..21u8 {
for x in 0..21u8 {
m.set(x, y, (x as u32 * y as u32) % 3 == 0);
@@ -249,4 +331,14 @@ mod tests {
let (idx, _masked) = best_mask(&m);
assert!(idx < 8);
}
#[test]
fn test_score_roundtrip_masked_equals_original_for_mask0_on_empty() {
// 验证 score(Matrix) 和 score_view(MaskedView) 在无保留区时一致
let m = Matrix::new(21);
let view = MaskedView::new(&m, 0); // mask 0 flips (x+y)%2==0
let s_view = score_view(&view);
let s_apply = score(&apply_mask(&m, 0));
assert_eq!(s_view, s_apply, "懒评分与直接评分应一致");
}
}
+59 -53
View File
@@ -1,6 +1,7 @@
use crate::ecc::reed_solomon;
use crate::encoder::bitstream::build_codewords;
use crate::encoder::segment::{segment_bit_length, segment_text};
use crate::error::QrError;
use crate::matrix::grid::Matrix;
use crate::matrix::mask::best_mask;
use crate::matrix::patterns::{
@@ -63,21 +64,15 @@ pub struct QrConfig {
pub version: VersionMode,
/// 静区边距(模块数),默认 4
pub margin: u8,
/// 前景色(CSS 格式 "#RRGGBB"),默认 "#000000"
pub fg_color: Option<String>,
/// 背景色(CSS 格式 "#RRGGBB"),默认 "#FFFFFF"
pub bg_color: Option<String>,
}
impl Default for QrConfig {
/// 默认配置:M 级纠错 + 自动版本 + 4 模块边距 + 黑前景白背景
/// 默认配置:M 级纠错 + 自动版本 + 4 模块边距
fn default() -> Self {
QrConfig {
level: EcLevel::M,
version: VersionMode::Auto,
margin: 4,
fg_color: None,
bg_color: None,
}
}
}
@@ -114,10 +109,6 @@ pub struct QrCode {
matrix: Matrix,
/// 静区边距(模块数)
pub margin: u8,
/// 前景色 RGB [r, g, b]
pub fg_color: [u8; 3],
/// 背景色 RGB [r, g, b]
pub bg_color: [u8; 3],
}
impl QrCode {
@@ -142,16 +133,16 @@ impl QrCode {
/// assert_eq!(qr.version.0, 1);
/// assert_eq!(qr.size(), 21);
/// ```
pub fn encode(text: &str, config: QrConfig) -> Result<Self, String> {
pub fn encode(text: &str, config: QrConfig) -> Result<Self, QrError> {
// 1. 分段
let segments = segment_text(text);
if segments.is_empty() {
return Err("输入为空".into());
return Err(QrError::EmptyInput);
}
// 2. 确定版本
let version = match config.version {
VersionMode::Fixed(v) => Version::new(v).ok_or("无效版本号 (1-40)")?,
VersionMode::Fixed(v) => Version::new(v).ok_or(QrError::InvalidVersion(v))?,
VersionMode::Auto => {
let mut selected = None;
for v in 1..=40 {
@@ -163,7 +154,7 @@ impl QrCode {
break;
}
}
selected.ok_or("数据过长,超出 QR 码最大容量".to_string())?
selected.ok_or(QrError::DataTooLong)?
}
};
@@ -178,7 +169,7 @@ impl QrCode {
for _ in 0..binfo.count {
let end = pos + binfo.data_codewords as usize;
if end > data.len() {
return Err("内部错误: 数据码字不足".into());
return Err(QrError::Internal("数据码字不足".into()));
}
blocks.push(data[pos..end].to_vec());
pos = end;
@@ -213,17 +204,12 @@ impl QrCode {
place_version_info(&mut final_matrix, ver_info);
}
let fg_color = parse_hex_color(config.fg_color.as_deref().unwrap_or("#000000"))?;
let bg_color = parse_hex_color(config.bg_color.as_deref().unwrap_or("#FFFFFF"))?;
Ok(QrCode {
version,
level: config.level,
mask: best_idx,
matrix: final_matrix,
margin: config.margin,
fg_color,
bg_color,
})
}
@@ -249,18 +235,25 @@ impl QrCode {
/// 导出为 SVG 字符串
///
/// SVG 内含 `viewBox`、使用 QrCode 的前/背景色。
/// `logo` 可选的 logo 图片字节,会以 base64 嵌入 SVG 中央。
/// `fg` 前景色("#RRGGBB"),默认 "#000000"。
/// `bg` 背景色("#RRGGBB"),默认 "#FFFFFF"。
///
/// ```rust
/// use qr_core::qr::{QrCode, QrConfig};
///
/// let qr = QrCode::encode("test", QrConfig::default()).unwrap();
/// let svg = qr.to_svg(None);
/// let svg = qr.to_svg(None, None, None);
/// assert!(svg.starts_with("<svg"));
/// ```
pub fn to_svg(&self, logo: Option<&[u8]>) -> String {
crate::render::svg::render_svg(self, logo)
pub fn to_svg(&self, logo: Option<&[u8]>, fg: Option<&str>, bg: Option<&str>) -> String {
let fg_color = fg
.and_then(|c| parse_hex_color(c).ok())
.unwrap_or([0, 0, 0]);
let bg_color = bg
.and_then(|c| parse_hex_color(c).ok())
.unwrap_or([255, 255, 255]);
crate::render::svg::render_svg(self, logo, &fg_color, &bg_color)
}
/// 导出为终端 ASCII 文本
@@ -276,24 +269,35 @@ impl QrCode {
/// `module_size` 控制每个模块的像素大小(2~20)。
/// `format` 输出格式,默认为 Png。
/// `logo` 可选的 logo 图片字节。
/// `fg` 前景色,默认 "#000000"。`bg` 背景色,默认 "#FFFFFF"。
///
/// ```rust
/// use qr_core::qr::{QrCode, QrConfig};
///
/// let qr = QrCode::encode("test", QrConfig::default()).unwrap();
/// let bytes = qr.to_image_bytes(4, None, None).unwrap();
/// let bytes = qr.to_image_bytes(4, None, None, None, None).unwrap();
/// ```
pub fn to_image_bytes(
&self,
module_size: u8,
logo: Option<&[u8]>,
format: Option<crate::render::image::OutputFormat>,
) -> Result<Vec<u8>, image::ImageError> {
fg: Option<&str>,
bg: Option<&str>,
) -> Result<Vec<u8>, QrError> {
let fg_color = fg
.and_then(|c| parse_hex_color(c).ok())
.unwrap_or([0, 0, 0]);
let bg_color = bg
.and_then(|c| parse_hex_color(c).ok())
.unwrap_or([255, 255, 255]);
crate::render::image::render_image(
self,
module_size,
format.unwrap_or(crate::render::image::OutputFormat::Png),
logo,
&fg_color,
&bg_color,
)
}
@@ -302,8 +306,8 @@ impl QrCode {
&self,
module_size: u8,
logo: Option<&[u8]>,
) -> Result<Vec<u8>, image::ImageError> {
self.to_image_bytes(module_size, logo, None)
) -> Result<Vec<u8>, QrError> {
self.to_image_bytes(module_size, logo, None, None, None)
}
}
@@ -311,32 +315,41 @@ impl QrCode {
///
/// 支持格式: "#RGB", "#RRGGBB"
/// 无效格式返回 Err
fn parse_hex_color(s: &str) -> Result<[u8; 3], String> {
pub(crate) fn parse_hex_color(s: &str) -> Result<[u8; 3], QrError> {
let s = s.trim();
if !s.starts_with('#') {
return Err(format!("颜色格式错误: '{}',应为 '#RRGGBB' 或 '#RGB'", s));
return Err(QrError::InvalidColor(s.to_string()));
}
let hex = &s[1..];
match hex.len() {
3 => {
let r = u8::from_str_radix(&hex[0..1].repeat(2), 16)
.map_err(|_| format!("无效颜色值: '{}'", s))?;
let g = u8::from_str_radix(&hex[1..2].repeat(2), 16)
.map_err(|_| format!("无效颜色值: '{}'", s))?;
let b = u8::from_str_radix(&hex[2..3].repeat(2), 16)
.map_err(|_| format!("无效颜色值: '{}'", s))?;
// #RGB → 每个分量乘以 17 扩展为 #RRGGBB(避免 String 分配)
// SAFETY: hex 来自 `s[1..]`,已通过 starts_with('#') 检查,且只含 ASCII hex 字符
let bytes = hex.as_bytes();
let r = u8::from_str_radix(unsafe {
std::str::from_utf8_unchecked(&[bytes[0], bytes[0]])
}, 16)
.map_err(|_| QrError::InvalidColor(s.to_string()))?;
let g = u8::from_str_radix(unsafe {
std::str::from_utf8_unchecked(&[bytes[1], bytes[1]])
}, 16)
.map_err(|_| QrError::InvalidColor(s.to_string()))?;
let b = u8::from_str_radix(unsafe {
std::str::from_utf8_unchecked(&[bytes[2], bytes[2]])
}, 16)
.map_err(|_| QrError::InvalidColor(s.to_string()))?;
Ok([r, g, b])
}
6 => {
let r =
u8::from_str_radix(&hex[0..2], 16).map_err(|_| format!("无效颜色值: '{}'", s))?;
let g =
u8::from_str_radix(&hex[2..4], 16).map_err(|_| format!("无效颜色值: '{}'", s))?;
let b =
u8::from_str_radix(&hex[4..6], 16).map_err(|_| format!("无效颜色值: '{}'", s))?;
let r = u8::from_str_radix(&hex[0..2], 16)
.map_err(|_| QrError::InvalidColor(s.to_string()))?;
let g = u8::from_str_radix(&hex[2..4], 16)
.map_err(|_| QrError::InvalidColor(s.to_string()))?;
let b = u8::from_str_radix(&hex[4..6], 16)
.map_err(|_| QrError::InvalidColor(s.to_string()))?;
Ok([r, g, b])
}
_ => Err(format!("颜色格式错误: '{}',应为 '#RRGGBB' 或 '#RGB'", s)),
_ => Err(QrError::InvalidColor(s.to_string())),
}
}
@@ -396,15 +409,8 @@ mod tests {
#[test]
fn test_color_qr() {
let config = QrConfig {
fg_color: Some("#FF0000".into()),
bg_color: Some("#0000FF".into()),
..Default::default()
};
let qr = QrCode::encode("COLOR TEST", config).unwrap();
assert_eq!(qr.fg_color, [255, 0, 0]);
assert_eq!(qr.bg_color, [0, 0, 255]);
let svg = qr.to_svg(None);
let qr = QrCode::encode("COLOR TEST", QrConfig::default()).unwrap();
let svg = qr.to_svg(None, Some("#FF0000"), Some("#0000FF"));
assert!(svg.contains("#FF0000"));
assert!(svg.contains("#0000FF"));
}
+6 -12
View File
@@ -80,8 +80,8 @@ fn fill_module(
}
}
fn overlay_logo(img: &mut RgbaImage, logo_bytes: &[u8], logo_size_pct: f32) -> Result<(), String> {
let logo = image::load_from_memory(logo_bytes).map_err(|e| format!("Logo 加载失败: {e}"))?;
fn overlay_logo(img: &mut RgbaImage, logo_bytes: &[u8], logo_size_pct: f32) -> Result<(), crate::error::QrError> {
let logo = image::load_from_memory(logo_bytes).map_err(crate::error::QrError::Image)?;
let logo = logo.to_rgba8();
let img_w = img.width();
let img_h = img.height();
@@ -102,7 +102,9 @@ pub fn render_image(
module_size: u8,
format: OutputFormat,
logo: Option<&[u8]>,
) -> Result<Vec<u8>, image::ImageError> {
fg: &[u8; 3],
bg: &[u8; 3],
) -> Result<Vec<u8>, crate::error::QrError> {
let matrix_size = qr.size() as u32;
let margin = qr.margin as u32;
let total_size = matrix_size + 2 * margin;
@@ -123,15 +125,7 @@ pub fn render_image(
} else {
false
};
fill_module(
&mut img,
x,
y,
module_size as u32,
is_dark,
&qr.fg_color,
&qr.bg_color,
);
fill_module(&mut img, x, y, module_size as u32, is_dark, fg, bg);
}
}
+5 -11
View File
@@ -1,18 +1,12 @@
use crate::qr::QrCode;
pub fn render_svg(qr: &QrCode, logo: Option<&[u8]>) -> String {
pub fn render_svg(qr: &QrCode, logo: Option<&[u8]>, fg: &[u8; 3], bg: &[u8; 3]) -> String {
let matrix_size = qr.size() as u32;
let margin = qr.margin as u32;
let total = matrix_size + 2 * margin;
let fg = format!(
"#{:02X}{:02X}{:02X}",
qr.fg_color[0], qr.fg_color[1], qr.fg_color[2]
);
let bg = format!(
"#{:02X}{:02X}{:02X}",
qr.bg_color[0], qr.bg_color[1], qr.bg_color[2]
);
let fg_hex = format!("#{:02X}{:02X}{:02X}", fg[0], fg[1], fg[2]);
let bg_hex = format!("#{:02X}{:02X}{:02X}", bg[0], bg[1], bg[2]);
let dark_count = qr
.modules()
@@ -25,14 +19,14 @@ pub fn render_svg(qr: &QrCode, logo: Option<&[u8]>) -> String {
r#"<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{total}" height="{total}" viewBox="0 0 {total} {total}">"#
));
svg.push_str(&format!(
r#"<rect width="{total}" height="{total}" fill="{bg}"/>"#
r#"<rect width="{total}" height="{total}" fill="{bg_hex}"/>"#
));
for y in 0..matrix_size {
for x in 0..matrix_size {
if qr.modules()[y as usize][x as usize] {
svg.push_str(&format!(
r#"<rect x="{}" y="{}" width="1" height="1" fill="{fg}"/>"#,
r#"<rect x="{}" y="{}" width="1" height="1" fill="{fg_hex}"/>"#,
x + margin,
y + margin
));
+7 -6
View File
@@ -24,23 +24,24 @@ pub fn build_vcard_text(
) -> String {
let mut s =
format!("BEGIN:VCARD\nVERSION:3.0\nFN:{name}\nTEL:{phone}\nEMAIL:{email}\nORG:{company}");
use std::fmt::Write;
if !title.is_empty() {
s.push_str(&format!("\nTITLE:{title}"));
write!(s, "\nTITLE:{title}").unwrap();
}
if !address.is_empty() {
s.push_str(&format!("\nADR:{address}"));
write!(s, "\nADR:{address}").unwrap();
}
if !url.is_empty() {
s.push_str(&format!("\nURL:{url}"));
write!(s, "\nURL:{url}").unwrap();
}
if !birthday.is_empty() {
s.push_str(&format!("\nBDAY:{birthday}"));
write!(s, "\nBDAY:{birthday}").unwrap();
}
if !note.is_empty() {
s.push_str(&format!("\nNOTE:{note}"));
write!(s, "\nNOTE:{note}").unwrap();
}
if !photo.is_empty() {
s.push_str(&format!("\nPHOTO:{photo}"));
write!(s, "\nPHOTO:{photo}").unwrap();
}
s.push_str("\nEND:VCARD");
s
+16
View File
@@ -1,4 +1,6 @@
use crate::error::QrError;
use serde::Serialize;
use std::str::FromStr;
use std::sync::OnceLock;
/// QR 码纠错级别
@@ -38,6 +40,20 @@ impl EcLevel {
}
}
impl FromStr for EcLevel {
type Err = QrError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_uppercase().as_str() {
"L" => Ok(EcLevel::L),
"M" => Ok(EcLevel::M),
"Q" => Ok(EcLevel::Q),
"H" => Ok(EcLevel::H),
_ => Err(QrError::InvalidVersion(0)), // 复用变体
}
}
}
/// QR 码版本号(1~40
///
/// 版本决定 QR 码的物理尺寸:`side = 17 + version × 4` 模块。
+5 -6
View File
@@ -193,11 +193,11 @@ fn test_format_info_written() {
#[test]
fn test_svg_valid_structure() {
let qr = QrCode::encode("HELLO", QrConfig::default()).unwrap();
let svg = qr.to_svg(None);
let svg = qr.to_svg(None, None, None);
// SVG 应有正确的结构
assert!(svg.starts_with("<svg"), "SVG 应以 <svg 开头");
assert!(svg.contains("rect"), "SVG 应包含 rect 元素");
assert!(svg.contains("fill=\"black\""), "SVG 暗模块应是黑色");
assert!(svg.contains("fill=\"#000000\""), "SVG 暗模块应是黑色");
assert!(
svg.ends_with("</svg>\n") || svg.ends_with("</svg>"),
"SVG 应以 </svg> 结尾"
@@ -208,7 +208,6 @@ fn test_svg_valid_structure() {
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);
@@ -313,10 +312,10 @@ fn test_empty_input_fails() {
#[test]
fn test_svg_output() {
let qr = QrCode::encode("TEST", QrConfig::default()).unwrap();
let svg = qr.to_svg(None);
let svg = qr.to_svg(None, None, None);
assert!(svg.contains("<svg"));
assert!(svg.contains("</svg>"));
assert!(svg.contains("fill=\"black\""));
assert!(svg.contains("fill=\"#000000\""));
}
#[test]
@@ -356,7 +355,7 @@ fn test_margin_is_included_in_dimensions() {
let qr = QrCode::encode("MARGIN TEST", config).unwrap();
// SVG 的总宽度应该包含 margin
let svg = qr.to_svg(None);
let svg = qr.to_svg(None, None, None);
let matrix_size = qr.size() as u32;
let expected_total = matrix_size + 2 * 2u32;
assert!(svg.contains(&format!("width=\"{}\"", expected_total)));