use clap::Parser; use qr_core::qr::{QrCode, QrConfig, VersionMode}; use qr_core::text_builder; use qr_core::version::EcLevel; use serde::Deserialize; use std::fs; use std::path::Path; #[derive(Parser)] #[command( name = "qrgen", about = "QR 码生成/解码工具 — 从零手搓的 ISO/IEC 18004 实现" )] struct Args { /// 快捷编码内容 content: Option, /// 解码图片文件 (PNG/JPEG/WebP) #[arg(short = 'd', long)] decode: Option, /// 输出文件 (.png 或 .svg),不指定则输出终端 ASCII #[arg(short = 'o', long)] output: Option, /// 纠错级别 [L/M/Q/H] [default: M] #[arg(short = 'l', long, default_value = "M")] level: String, /// 手动指定版本 (1-40),不指定则自动选择 #[arg(short = 'v', long)] version: Option, /// 模块像素大小(仅 PNG)[default: 4] #[arg(short = 's', long, default_value = "4")] size: u8, /// 白边模块数 [default: 4] #[arg(short = 'm', long, default_value = "4")] margin: u8, /// 反色(黑底白码) #[arg(long)] invert: bool, /// 前景色 "#RRGGBB" #[arg(long)] fg: Option, /// 背景色 "#RRGGBB" #[arg(long)] bg: Option, /// Logo 图片文件 #[arg(long)] logo: Option, // ---- 编码模式参数 ---- /// 编码模式 [text/url/wifi/vcard/email/phone/sms/batch] #[arg(long)] mode: Option, /// WiFi SSID #[arg(long)] ssid: Option, /// WiFi 密码 #[arg(long)] password: Option, /// WiFi 加密方式 [default: WPA] #[arg(long, default_value = "WPA")] encryption: String, /// 隐藏 WiFi 网络 #[arg(long)] hidden: bool, /// 姓名 (vCard) #[arg(long)] name: Option, /// 电话 (vCard) #[arg(long)] phone: Option, /// 邮箱 (vCard) #[arg(long)] email: Option, /// 公司 (vCard) #[arg(long)] company: Option, /// 地址 (vCard) #[arg(long)] address: Option, /// 收件人 (Email) #[arg(long)] to: Option, /// 主题 (Email) #[arg(long)] subject: Option, /// 正文 (Email) #[arg(long)] body: Option, /// 电话号码 (Phone/SMS) #[arg(long)] number: Option, /// 短信内容 (SMS) #[arg(long)] message: Option, /// URL 链接 #[arg(long)] url: Option, /// 批量输入文件 (JSON/CSV) #[arg(long)] batch: Option, /// 批量输出目录 #[arg(long)] output_dir: Option, } #[derive(Deserialize)] struct BatchEntry { content: Option, level: Option, ssid: Option, password: Option, encryption: Option, #[serde(default)] hidden: Option, name: Option, phone: Option, email: Option, company: Option, address: Option, to: Option, subject: Option, body: Option, number: Option, message: Option, url: Option, } fn main() -> anyhow::Result<()> { let args = Args::parse(); if let Some(path) = args.decode { return do_decode(&path); } if let Some(ref batch_file) = args.batch { return do_batch(batch_file, &args); } let text = build_text_from_args(&args)?; let level = parse_level(&args.level)?; let logo_bytes = args .logo .as_ref() .map(fs::read) .transpose() .map_err(|e| anyhow::anyhow!("无法读取 logo 文件: {e}"))?; let config = QrConfig { level, version: match args.version { Some(v) => { if !(1..=40).contains(&v) { anyhow::bail!("无效版本号: {}。支持 1-40", v); } VersionMode::Fixed(v) } None => VersionMode::Auto, }, margin: args.margin, fg_color: args.fg.clone(), bg_color: args.bg.clone(), }; let qr = QrCode::encode(&text, config).map_err(|e| anyhow::anyhow!("编码失败: {}", e))?; match &args.output { Some(path) => { check_path(path)?; let ext = Path::new(path) .extension() .and_then(|e| e.to_str()) .unwrap_or("") .to_lowercase(); match ext.as_str() { "png" => { let bytes = qr.to_png_bytes(args.size, logo_bytes.as_deref())?; fs::write(path, bytes)?; println!( "已生成: {} (版本 {}, {}×{} 模块, {:?} 级纠错)", path, qr.version.0, qr.size(), qr.size(), qr.level ); } "svg" => { let svg = qr.to_svg(logo_bytes.as_deref()); fs::write(path, svg)?; println!("已生成: {} (版本 {}, SVG 格式)", path, qr.version.0); } _ => anyhow::bail!("不支持的文件格式: .{}。支持 .png / .svg", ext), } } None => { println!("{}", qr.to_ascii(args.invert)); } } Ok(()) } fn build_text_from_args(args: &Args) -> anyhow::Result { match args.mode.as_deref() { Some("wifi") => { let ssid = args .ssid .as_deref() .ok_or_else(|| anyhow::anyhow!("WiFi 模式需要 --ssid"))?; let pwd = args.password.as_deref().unwrap_or(""); Ok(text_builder::build_wifi_text( ssid, pwd, &args.encryption, args.hidden, )) } Some("vcard") => Ok(text_builder::build_vcard_text( args.name.as_deref().unwrap_or(""), args.phone.as_deref().unwrap_or(""), args.email.as_deref().unwrap_or(""), args.company.as_deref().unwrap_or(""), args.address.as_deref().unwrap_or(""), )), Some("email") => { let to = args .to .as_deref() .ok_or_else(|| anyhow::anyhow!("Email 模式需要 --to"))?; Ok(text_builder::build_email_text( to, args.subject.as_deref().unwrap_or(""), args.body.as_deref().unwrap_or(""), )) } Some("phone") => { let num = args .number .as_deref() .ok_or_else(|| anyhow::anyhow!("电话模式需要 --number"))?; Ok(text_builder::build_phone_text(num)) } Some("sms") => { let num = args .number .as_deref() .ok_or_else(|| anyhow::anyhow!("短信模式需要 --number"))?; Ok(text_builder::build_sms_text( num, args.message.as_deref().unwrap_or(""), )) } Some("url") => args .url .clone() .ok_or_else(|| anyhow::anyhow!("URL 模式需要 --url")), Some(m) => anyhow::bail!("未知模式: {m}。支持 text/url/wifi/vcard/email/phone/sms/batch"), None => args .content .clone() .ok_or_else(|| anyhow::anyhow!("请提供编码内容或使用 --mode 指定模式")), } } fn do_decode(path: &str) -> anyhow::Result<()> { let bytes = fs::read(path).map_err(|e| anyhow::anyhow!("无法读取文件 '{}': {}", path, e))?; let result = qr_core::decoder::decode_image(&bytes).map_err(|e| anyhow::anyhow!("{e}"))?; println!("解码成功:"); println!(" 文本: {}", result.text); println!(" 版本: {}", result.version); println!(" 纠错级别: {:?}", result.level); println!(" 掩码: {}", result.mask); if result.errors_corrected > 0 { println!(" 纠正错误: {} 码字", result.errors_corrected); } Ok(()) } fn do_batch(file: &str, args: &Args) -> anyhow::Result<()> { let input = fs::read_to_string(file) .map_err(|e| anyhow::anyhow!("无法读取批量文件 '{}': {}", file, e))?; let entries: Vec = serde_json::from_str(&input) .or_else(|_| parse_csv(&input)) .map_err(|e| anyhow::anyhow!("无法解析输入: {e}\n支持 JSON 数组或 CSV 格式"))?; let out_dir = args.output_dir.as_deref().unwrap_or("batch_output"); fs::create_dir_all(out_dir)?; for (i, entry) in entries.iter().enumerate() { let text = batch_entry_to_text(entry)?; let level = entry .level .as_deref() .map(parse_level) .unwrap_or(Ok(EcLevel::M))?; let config = QrConfig { level, version: VersionMode::Auto, margin: args.margin, fg_color: args.fg.clone(), bg_color: args.bg.clone(), }; let qr = QrCode::encode(&text, config).map_err(|e| anyhow::anyhow!("{e}"))?; let path = format!("{}/qr_{:04}.png", out_dir, i + 1); let bytes = qr.to_png_bytes(args.size, None)?; fs::write(&path, bytes)?; println!("[{}/{}] {}", i + 1, entries.len(), path); } println!("批量生成完成: {} 个 QR 码 → {}", entries.len(), out_dir); Ok(()) } fn batch_entry_to_text(entry: &BatchEntry) -> anyhow::Result { if let Some(c) = &entry.content { return Ok(c.clone()); } if let Some(u) = &entry.url { return Ok(u.clone()); } if let Some(s) = &entry.ssid { let p = entry.password.as_deref().unwrap_or(""); let e = entry.encryption.as_deref().unwrap_or("WPA"); let h = entry.hidden.unwrap_or(false); return Ok(text_builder::build_wifi_text(s, p, e, h)); } if let Some(n) = &entry.name { let ph = entry.phone.as_deref().unwrap_or(""); let em = entry.email.as_deref().unwrap_or(""); let co = entry.company.as_deref().unwrap_or(""); let ad = entry.address.as_deref().unwrap_or(""); return Ok(text_builder::build_vcard_text(n, ph, em, co, ad)); } if let Some(t) = &entry.to { let s = entry.subject.as_deref().unwrap_or(""); let b = entry.body.as_deref().unwrap_or(""); return Ok(text_builder::build_email_text(t, s, b)); } if let Some(n) = &entry.number { if let Some(m) = &entry.message { return Ok(text_builder::build_sms_text(n, m)); } return Ok(text_builder::build_phone_text(n)); } anyhow::bail!("无法识别的条目格式") } fn parse_csv(input: &str) -> Result, String> { let mut lines = input.lines(); let header = lines.next().ok_or("CSV 为空")?; let columns: Vec<&str> = header.split(',').map(|s| s.trim()).collect(); let mut entries = Vec::new(); for line in lines { if line.trim().is_empty() { continue; } let values: Vec = line .split(',') .map(|s| s.trim().trim_matches('"').to_string()) .collect(); let mut content = None; let mut level = None; let mut ssid = None; let mut password = None; let mut encryption = None; let mut hidden = None; let mut name = None; let mut phone = None; let mut email = None; let mut company = None; let mut address = None; let mut to = None; let mut subject = None; let mut body = None; let mut number = None; let mut message = None; let mut url = None; for (i, col) in columns.iter().enumerate() { let val = values.get(i).cloned(); match *col { "content" => content = val, "level" => level = val, "ssid" => ssid = val, "password" => password = val, "encryption" => encryption = val, "hidden" => hidden = val.map(|v| v == "true"), "name" => name = val, "phone" => phone = val, "email" => email = val, "company" => company = val, "address" => address = val, "to" => to = val, "subject" => subject = val, "body" => body = val, "number" => number = val, "message" => message = val, "url" => url = val, _ => {} } } entries.push(BatchEntry { content, level, ssid, password, encryption, hidden, name, phone, email, company, address, to, subject, body, number, message, url, }); } Ok(entries) } fn parse_level(s: &str) -> anyhow::Result { match s.to_uppercase().as_str() { "L" => Ok(EcLevel::L), "M" => Ok(EcLevel::M), "Q" => Ok(EcLevel::Q), "H" => Ok(EcLevel::H), _ => anyhow::bail!("无效纠错级别: {}。支持 L/M/Q/H", s), } } fn check_path(path: &str) -> anyhow::Result<()> { let path_obj = Path::new(path); if path_obj .components() .any(|c| matches!(c, std::path::Component::ParentDir)) { anyhow::bail!("不允许包含 '..' 的路径,请使用当前目录下的文件名"); } Ok(()) }