feat: 彩色 QR 码 — 自定义前景色/背景色

- QrConfig 新增 fg_color/bg_color 字段(CSS 十六进制格式)
- QrCode 存储解析后的 [u8;3] RGB
- PNG 渲染 Luma→Rgba,支持 RGBA 颜色
- SVG 渲染使用 QrCode 颜色字段
- CLI 新增 --fg/--bg 参数
- 新增 parse_hex_color 支持 #RRGGBB 和 #RGB
- 新增 2 个颜色测试(74 tests total)
This commit is contained in:
2026-06-19 21:10:56 +08:00
parent 3d11ba2446
commit 23ccb37b52
6 changed files with 119 additions and 14 deletions
+20 -7
View File
@@ -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<Luma<u8>, Vec<u8>>,
img: &mut ImageBuffer<Rgba<u8>, Vec<u8>>,
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<Vec<u8>, 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<Vec<u8>, 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,
);
}
}
+12 -6
View File
@@ -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#"<svg xmlns="http://www.w3.org/2000/svg" width="{}" height="{}" viewBox="0 0 {} {}">"#,
total, total, total, total
r#"<svg xmlns="http://www.w3.org/2000/svg" width="{total}" height="{total}" viewBox="0 0 {total} {total}">"#
));
svg.push_str(&format!(
r#"<rect width="{}" height="{}" fill="white"/>"#,
total, total
r#"<rect width="{total}" height="{total}" fill="{bg}"/>"#
));
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="black"/>"#,
r#"<rect x="{}" y="{}" width="1" height="1" fill="{fg}"/>"#,
x + margin,
y + margin
));