cd75141037
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 全部测试通过
76 lines
2.6 KiB
Rust
76 lines
2.6 KiB
Rust
use crate::qr::QrCode;
|
|
|
|
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_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()
|
|
.iter()
|
|
.flat_map(|row| row.iter())
|
|
.filter(|&&m| m)
|
|
.count();
|
|
let mut svg = String::with_capacity(300 + dark_count * 50);
|
|
svg.push_str(&format!(
|
|
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_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_hex}"/>"#,
|
|
x + margin,
|
|
y + margin
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Logo 嵌入:base64 PNG data URL
|
|
if let Some(logo_bytes) = logo {
|
|
let b64 = base64_encode(logo_bytes);
|
|
let logo_size = total as f32 * 0.25;
|
|
let logo_x = (total as f32 - logo_size) / 2.0;
|
|
let logo_y = (total as f32 - logo_size) / 2.0;
|
|
svg.push_str(&format!(
|
|
r#"<image x="{logo_x}" y="{logo_y}" width="{logo_size}" height="{logo_size}" xlink:href="data:image/png;base64,{b64}"/>"#
|
|
));
|
|
}
|
|
|
|
svg.push_str("</svg>");
|
|
svg
|
|
}
|
|
|
|
/// 简易 base64 编码(无外部依赖)
|
|
fn base64_encode(data: &[u8]) -> String {
|
|
const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
let mut result = String::with_capacity(data.len().div_ceil(3) * 4);
|
|
for chunk in data.chunks(3) {
|
|
let b0 = chunk[0] as u32;
|
|
let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
|
|
let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
|
|
let triple = (b0 << 16) | (b1 << 8) | b2;
|
|
result.push(CHARS[((triple >> 18) & 0x3F) as usize] as char);
|
|
result.push(CHARS[((triple >> 12) & 0x3F) as usize] as char);
|
|
if chunk.len() > 1 {
|
|
result.push(CHARS[((triple >> 6) & 0x3F) as usize] as char);
|
|
} else {
|
|
result.push('=');
|
|
}
|
|
if chunk.len() > 2 {
|
|
result.push(CHARS[(triple & 0x3F) as usize] as char);
|
|
} else {
|
|
result.push('=');
|
|
}
|
|
}
|
|
result
|
|
}
|