diff --git a/cli/src/main.rs b/cli/src/main.rs index cc01d40..bf36dac 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -39,6 +39,14 @@ struct Args { /// 反色(黑底白码) #[arg(long)] invert: bool, + + /// 前景色 "#RRGGBB" [default: "#000000"] + #[arg(long)] + fg: Option, + + /// 背景色 "#RRGGBB" [default: "#FFFFFF"] + #[arg(long)] + bg: Option, } fn main() -> anyhow::Result<()> { @@ -80,6 +88,8 @@ fn do_encode(content: &str, args: &Args) -> anyhow::Result<()> { level, version, margin: args.margin, + fg_color: args.fg.clone(), + bg_color: args.bg.clone(), }; let qr = QrCode::encode(content, config).map_err(|e| anyhow::anyhow!("编码失败: {}", e))?; diff --git a/core/src/qr.rs b/core/src/qr.rs index 845bb77..9b2e875 100644 --- a/core/src/qr.rs +++ b/core/src/qr.rs @@ -63,15 +63,21 @@ pub struct QrConfig { pub version: VersionMode, /// 静区边距(模块数),默认 4 pub margin: u8, + /// 前景色(CSS 格式 "#RRGGBB"),默认 "#000000" + pub fg_color: Option, + /// 背景色(CSS 格式 "#RRGGBB"),默认 "#FFFFFF" + pub bg_color: Option, } 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, } } } @@ -108,6 +114,10 @@ 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 { @@ -203,12 +213,17 @@ 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, }) } @@ -271,6 +286,39 @@ impl QrCode { } } +/// 解析 CSS 十六进制颜色 → [R, G, B] +/// +/// 支持格式: "#RGB", "#RRGGBB" +/// 无效格式返回 Err +fn parse_hex_color(s: &str) -> Result<[u8; 3], String> { + let s = s.trim(); + if !s.starts_with('#') { + return Err(format!("颜色格式错误: '{}',应为 '#RRGGBB' 或 '#RGB'", s)); + } + 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))?; + 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))?; + Ok([r, g, b]) + } + _ => Err(format!("颜色格式错误: '{}',应为 '#RRGGBB' 或 '#RGB'", s)), + } +} + #[cfg(test)] mod tests { use super::*; @@ -314,4 +362,29 @@ mod tests { assert!(qr.size() >= 21); } } + + #[test] + fn test_parse_hex_color() { + assert_eq!(parse_hex_color("#000000").unwrap(), [0, 0, 0]); + assert_eq!(parse_hex_color("#FFFFFF").unwrap(), [255, 255, 255]); + assert_eq!(parse_hex_color("#FF5733").unwrap(), [255, 87, 51]); + assert_eq!(parse_hex_color("#F53").unwrap(), [255, 85, 51]); + assert!(parse_hex_color("invalid").is_err()); + assert!(parse_hex_color("#GGGGGG").is_err()); + } + + #[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(); + assert!(svg.contains("#FF0000")); + assert!(svg.contains("#0000FF")); + } } diff --git a/core/src/render/png.rs b/core/src/render/png.rs index 02112bb..d89d736 100644 --- a/core/src/render/png.rs +++ b/core/src/render/png.rs @@ -1,20 +1,26 @@ use crate::qr::QrCode; -use image::{ImageBuffer, Luma}; +use image::{ImageBuffer, Rgba}; /// 将单个模块填充到图像缓冲区(module_size × module_size 像素块) fn fill_module( - img: &mut ImageBuffer, Vec>, + img: &mut ImageBuffer, Vec>, x: u32, y: u32, module_size: u32, is_dark: bool, + fg: &[u8; 3], + bg: &[u8; 3], ) { - let px_val = if is_dark { 0u8 } else { 255u8 }; + let color = if is_dark { + Rgba([fg[0], fg[1], fg[2], 255]) + } else { + Rgba([bg[0], bg[1], bg[2], 255]) + }; let x0 = x * module_size; let y0 = y * module_size; for dy in 0..module_size { for dx in 0..module_size { - img.put_pixel(x0 + dx, y0 + dy, Luma([px_val])); + img.put_pixel(x0 + dx, y0 + dy, color); } } } @@ -29,7 +35,6 @@ pub fn render_png(qr: &QrCode, module_size: u8) -> Result, image::ImageE for y in 0..total_size { for x in 0..total_size { - // 直接比较坐标与 margin 边界,避免 saturating_sub 在边界处回绕到 0 let is_dark = if x >= margin && x < margin + matrix_size && y >= margin @@ -39,10 +44,18 @@ pub fn render_png(qr: &QrCode, module_size: u8) -> Result, image::ImageE let my = (y - margin) as usize; qr.modules()[my][mx] } else { - false // 白边 (quiet zone) + false }; - fill_module(&mut img, x, y, module_size as u32, is_dark); + fill_module( + &mut img, + x, + y, + module_size as u32, + is_dark, + &qr.fg_color, + &qr.bg_color, + ); } } diff --git a/core/src/render/svg.rs b/core/src/render/svg.rs index 43b4b27..b8216a6 100644 --- a/core/src/render/svg.rs +++ b/core/src/render/svg.rs @@ -5,7 +5,15 @@ pub fn render_svg(qr: &QrCode) -> String { let margin = qr.margin as u32; let total = matrix_size + 2 * margin; - // 预估 SVG 大小: 固定头部 + 每个暗模块约 48 字节 + 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 dark_count = qr .modules() .iter() @@ -14,19 +22,17 @@ pub fn render_svg(qr: &QrCode) -> String { .count(); let mut svg = String::with_capacity(200 + dark_count * 50); svg.push_str(&format!( - r#""#, - total, total, total, total + r#""# )); svg.push_str(&format!( - r#""#, - total, total + r#""# )); 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#""#, + r#""#, x + margin, y + margin )); diff --git a/gui/src/lib.rs b/gui/src/lib.rs index c6ca74e..fb69b82 100644 --- a/gui/src/lib.rs +++ b/gui/src/lib.rs @@ -45,6 +45,7 @@ fn encode_qr(text: String, level: String, margin: u8) -> Result Resul level: ec_level, version: VersionMode::Auto, margin, + ..Default::default() }; let qr = QrCode::encode(&text, config).map_err(|e| format!("编码失败: {}", e))?; diff --git a/web/src/main.rs b/web/src/main.rs index e225182..eee19bc 100644 --- a/web/src/main.rs +++ b/web/src/main.rs @@ -60,6 +60,7 @@ async fn generate_qr(Query(params): Query) -> impl IntoResponse { level, version: VersionMode::Auto, margin: params.margin, + ..Default::default() }; let qr = match QrCode::encode(¶ms.text, config) {