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 前端全部通过
This commit is contained in:
2026-06-19 21:38:58 +08:00
parent b41f6ee7df
commit 86d788e57c
8 changed files with 294 additions and 9 deletions
+63 -3
View File
@@ -8,15 +8,42 @@ pub fn build_wifi_text(ssid: &str, password: &str, encryption: &str, hidden: boo
format!("WIFI:T:{encryption};S:{ssid};P:{password};{h};")
}
/// 构造 vCard 字符串
/// 构造 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 {
format!("BEGIN:VCARD\nVERSION:3.0\nFN:{name}\nTEL:{phone}\nEMAIL:{email}\nORG:{company}\nADR:{address}\nEND:VCARD")
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 链接
@@ -71,10 +98,43 @@ mod tests {
#[test]
fn test_build_vcard_text() {
let text = build_vcard_text("张三", "13800138000", "a@b.com", "公司", "北京");
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]