feat: Logo 嵌入 — QR 码中央叠加自定义图片

- PNG 渲染:RgbaImage 上使用 imageops::overlay 叠加 logo
- SVG 渲染:base64 编码 logo 嵌入 <image> 标签
- QrCode::to_png_bytes / to_svg 新增 Option<logo_bytes> 参数
- Logo 默认占 QR 区域 25%,建议配合 H 级纠错使用
This commit is contained in:
2026-06-19 21:12:44 +08:00
parent 23ccb37b52
commit 38be82973e
10 changed files with 112 additions and 40 deletions
+43 -13
View File
@@ -1,9 +1,8 @@
use crate::qr::QrCode;
use image::{ImageBuffer, Rgba};
use image::{imageops, ImageBuffer, Rgba, RgbaImage};
/// 将单个模块填充到图像缓冲区(module_size × module_size 像素块)
fn fill_module(
img: &mut ImageBuffer<Rgba<u8>, Vec<u8>>,
img: &mut RgbaImage,
x: u32,
y: u32,
module_size: u32,
@@ -25,7 +24,40 @@ fn fill_module(
}
}
pub fn render_png(qr: &QrCode, module_size: u8) -> Result<Vec<u8>, image::ImageError> {
/// 在 QR 码 PNG 缓冲区中央叠加 logo
fn overlay_logo(img: &mut RgbaImage, logo_bytes: &[u8], logo_size_pct: f32) -> Result<(), String> {
let logo =
image::load_from_memory(logo_bytes).map_err(|e| format!("Logo 加载失败: {e}"))?;
let logo = logo.to_rgba8();
let img_w = img.width();
let img_h = img.height();
// Logo 边长 = min(图像边长 * pct, 实际 QR 区域 * pct)
let logo_size = (img_w.min(img_h) as f32 * logo_size_pct) as u32;
if logo_size < 4 {
return Ok(()); // 太小,跳过
}
let resized = imageops::resize(
&logo,
logo_size,
logo_size,
imageops::FilterType::Lanczos3,
);
let x = (img_w - logo_size) / 2;
let y = (img_h - logo_size) / 2;
imageops::overlay(img, &resized, x as i64, y as i64);
Ok(())
}
pub fn render_png(
qr: &QrCode,
module_size: u8,
logo: Option<&[u8]>,
) -> Result<Vec<u8>, image::ImageError> {
let matrix_size = qr.size() as u32;
let margin = qr.margin as u32;
let total_size = matrix_size + 2 * margin;
@@ -47,18 +79,16 @@ pub fn render_png(qr: &QrCode, module_size: u8) -> Result<Vec<u8>, image::ImageE
false
};
fill_module(
&mut img,
x,
y,
module_size as u32,
is_dark,
&qr.fg_color,
&qr.bg_color,
);
fill_module(&mut img, x, y, module_size as u32, is_dark, &qr.fg_color, &qr.bg_color);
}
}
// Logo 叠加
if let Some(logo_data) = logo {
// 忽略 logo 叠加错误(logo 有损不影响 QR 主体)
let _ = overlay_logo(&mut img, logo_data, 0.25);
}
let mut buf = Vec::new();
img.write_to(&mut std::io::Cursor::new(&mut buf), image::ImageFormat::Png)?;
Ok(buf)
+39 -3
View File
@@ -1,6 +1,6 @@
use crate::qr::QrCode;
pub fn render_svg(qr: &QrCode) -> String {
pub fn render_svg(qr: &QrCode, logo: Option<&[u8]>) -> String {
let matrix_size = qr.size() as u32;
let margin = qr.margin as u32;
let total = matrix_size + 2 * margin;
@@ -20,9 +20,9 @@ pub fn render_svg(qr: &QrCode) -> String {
.flat_map(|row| row.iter())
.filter(|&&m| m)
.count();
let mut svg = String::with_capacity(200 + dark_count * 50);
let mut svg = String::with_capacity(300 + dark_count * 50);
svg.push_str(&format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" width="{total}" height="{total}" viewBox="0 0 {total} {total}">"#
r#"<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{total}" height="{total}" viewBox="0 0 {total} {total}">"#
));
svg.push_str(&format!(
r#"<rect width="{total}" height="{total}" fill="{bg}"/>"#
@@ -40,6 +40,42 @@ pub fn render_svg(qr: &QrCode) -> String {
}
}
// Logo 嵌入:base64 PNG data URL
if let Some(logo_bytes) = logo {
let b64 = base64_encode(logo_bytes);
let logo_size = total as f32 * 0.25;
let logo_x = (total as f32 - logo_size) / 2.0;
let logo_y = (total as f32 - logo_size) / 2.0;
svg.push_str(&format!(
r#"<image x="{logo_x}" y="{logo_y}" width="{logo_size}" height="{logo_size}" xlink:href="data:image/png;base64,{b64}"/>"#
));
}
svg.push_str("</svg>");
svg
}
/// 简易 base64 编码(无外部依赖)
fn base64_encode(data: &[u8]) -> String {
const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut result = String::with_capacity((data.len() + 2) / 3 * 4);
for chunk in data.chunks(3) {
let b0 = chunk[0] as u32;
let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
let triple = (b0 << 16) | (b1 << 8) | b2;
result.push(CHARS[((triple >> 18) & 0x3F) as usize] as char);
result.push(CHARS[((triple >> 12) & 0x3F) as usize] as char);
if chunk.len() > 1 {
result.push(CHARS[((triple >> 6) & 0x3F) as usize] as char);
} else {
result.push('=');
}
if chunk.len() > 2 {
result.push(CHARS[(triple & 0x3F) as usize] as char);
} else {
result.push('=');
}
}
result
}