Files
QRGen/core/src/text_builder.rs
T
Serendipity 86d788e57c feat: vCard 扩展 + 格式扩展 + 解码透视矫正 — v0.3.0
Phase 1: 格式扩展
- png.rs → image.rs,OutputFormat 枚举 (PNG/BMP/JPEG/WebP)
- CLI -f/--format,Web fmt 参数扩展,image crate +bmp feature

Phase 2: 解码增强
- 新增 decoder/perspective.rs — 旋转矫正(MVP)
- auto_correct: finder 检测→计算旋转角→仿射变换→再解码
- decode_image 自动重试矫正流水线

Phase 3: vCard 扩展
- 新增 5 字段:TITLE/URL/BDAY/NOTE/PHOTO
- Rust text_builder + TS qrText + VCardMode UI 同步
- CLI 新增 --title --vcard-url --birthday --note --photo
- 中/英 i18n 翻译

测试: 81 Rust + 19 前端全部通过
2026-06-19 21:38:58 +08:00

158 lines
4.2 KiB
Rust

//! QR 编码文本构造工具
//!
//! 集中管理各模式的文本格式(与 gui 前端 `utils/qrText.ts` 功能一致)
/// 构造 WiFi 连接字符串
pub fn build_wifi_text(ssid: &str, password: &str, encryption: &str, hidden: bool) -> String {
let h = if hidden { "H:true;" } else { "" };
format!("WIFI:T:{encryption};S:{ssid};P:{password};{h};")
}
/// 构造 vCard 3.0 字符串(含扩展字段)
#[allow(clippy::too_many_arguments)]
pub fn build_vcard_text(
name: &str,
phone: &str,
email: &str,
company: &str,
address: &str,
title: &str,
url: &str,
birthday: &str,
note: &str,
photo: &str,
) -> String {
let mut s =
format!("BEGIN:VCARD\nVERSION:3.0\nFN:{name}\nTEL:{phone}\nEMAIL:{email}\nORG:{company}");
if !title.is_empty() {
s.push_str(&format!("\nTITLE:{title}"));
}
if !address.is_empty() {
s.push_str(&format!("\nADR:{address}"));
}
if !url.is_empty() {
s.push_str(&format!("\nURL:{url}"));
}
if !birthday.is_empty() {
s.push_str(&format!("\nBDAY:{birthday}"));
}
if !note.is_empty() {
s.push_str(&format!("\nNOTE:{note}"));
}
if !photo.is_empty() {
s.push_str(&format!("\nPHOTO:{photo}"));
}
s.push_str("\nEND:VCARD");
s
}
/// 构造 mailto 链接
pub fn build_email_text(to: &str, subject: &str, body: &str) -> String {
let subject_enc = urlencoding(subject);
let body_enc = urlencoding(body);
format!("mailto:{to}?subject={subject_enc}&body={body_enc}")
}
/// 构造电话链接
pub fn build_phone_text(number: &str) -> String {
format!("tel:{number}")
}
/// 构造短信链接
pub fn build_sms_text(number: &str, message: &str) -> String {
format!("smsto:{number}:{message}")
}
/// 简易 URL 编码(仅编码特殊字符)
fn urlencoding(s: &str) -> String {
s.chars()
.map(|c| match c {
' ' => "%20".into(),
'&' => "%26".into(),
'=' => "%3D".into(),
'#' => "%23".into(),
'%' => "%25".into(),
'+' => "%2B".into(),
'\n' => "%0A".into(),
'\r' => "%0D".into(),
_ => c.to_string(),
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_wifi_text() {
let text = build_wifi_text("MyWiFi", "pass123", "WPA", false);
assert!(text.contains("WIFI:T:WPA;S:MyWiFi;P:pass123;"));
}
#[test]
fn test_build_wifi_hidden() {
let text = build_wifi_text("HiddenNet", "secret", "WPA2", true);
assert!(text.contains("H:true;"));
}
#[test]
fn test_build_vcard_text() {
let text = build_vcard_text(
"张三",
"13800138000",
"a@b.com",
"公司",
"北京",
"",
"",
"",
"",
"",
);
assert!(text.contains("BEGIN:VCARD"));
assert!(text.contains("FN:张三"));
assert!(text.contains("END:VCARD"));
assert!(!text.contains("TITLE:")); // 空字段不输出
}
#[test]
fn test_build_vcard_full() {
let text = build_vcard_text(
"张三",
"13800138000",
"a@b.com",
"公司",
"北京",
"工程师",
"https://z.com",
"1990-01-01",
"备注",
"https://z.com/p.jpg",
);
assert!(text.contains("TITLE:工程师"));
assert!(text.contains("URL:https://z.com"));
assert!(text.contains("BDAY:1990-01-01"));
assert!(text.contains("NOTE:备注"));
assert!(text.contains("PHOTO:https://z.com/p.jpg"));
}
#[test]
fn test_build_email_text() {
let text = build_email_text("a@b.com", "Hello World", "Test body");
assert!(text.starts_with("mailto:a@b.com"));
assert!(text.contains("Hello%20World"));
}
#[test]
fn test_build_phone_text() {
assert_eq!(build_phone_text("13800138000"), "tel:13800138000");
}
#[test]
fn test_build_sms_text() {
let text = build_sms_text("13800138000", "Hi");
assert_eq!(text, "smsto:13800138000:Hi");
}
}