Files
QRGen/core/src/render/image.rs
T
Serendipity b41f6ee7df feat: 格式扩展 — 支持 BMP/JPEG/WebP 输出
- png.rs 重命名为 image.rs,新增 OutputFormat 枚举
- QrCode::to_image_bytes 支持 PNG/BMP/JPEG/WebP
- CLI 新增 -f/--format 参数(png/bmp/jpeg/webp)
- Web API fmt 参数扩展至全部 4 种图像格式
- core/Cargo.toml: image crate 新增 bmp feature
2026-06-19 21:34:21 +08:00

149 lines
4.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! QR 码图像渲染(支持 PNG/BMP/JPEG/WebP
//!
//! 使用 `image` crate 将 QR 模块矩阵渲染为像素缓冲区,可选叠加 Logo。
use crate::qr::QrCode;
use image::{imageops, ImageBuffer, Rgba, RgbaImage};
/// 输出图像格式
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputFormat {
Png,
Bmp,
Jpeg,
WebP,
}
impl OutputFormat {
/// 转为 `image` crate 的格式枚举
fn to_image_format(self) -> image::ImageFormat {
match self {
Self::Png => image::ImageFormat::Png,
Self::Bmp => image::ImageFormat::Bmp,
Self::Jpeg => image::ImageFormat::Jpeg,
Self::WebP => image::ImageFormat::WebP,
}
}
/// 文件扩展名(不含点)
pub fn extension(self) -> &'static str {
match self {
Self::Png => "png",
Self::Bmp => "bmp",
Self::Jpeg => "jpeg",
Self::WebP => "webp",
}
}
/// MIME 类型
pub fn mime(self) -> &'static str {
match self {
Self::Png => "image/png",
Self::Bmp => "image/bmp",
Self::Jpeg => "image/jpeg",
Self::WebP => "image/webp",
}
}
/// 从扩展名解析
pub fn from_ext(ext: &str) -> Option<Self> {
match ext.to_lowercase().as_str() {
"png" => Some(Self::Png),
"bmp" => Some(Self::Bmp),
"jpeg" | "jpg" => Some(Self::Jpeg),
"webp" => Some(Self::WebP),
_ => None,
}
}
}
fn fill_module(
img: &mut RgbaImage,
x: u32,
y: u32,
module_size: u32,
is_dark: bool,
fg: &[u8; 3],
bg: &[u8; 3],
) {
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, color);
}
}
}
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();
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(())
}
/// 渲染 QR 码到图像字节(支持 PNG/BMP/JPEG/WebP
pub fn render_image(
qr: &QrCode,
module_size: u8,
format: OutputFormat,
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;
let img_size = total_size * module_size as u32;
let mut img = ImageBuffer::new(img_size, img_size);
for y in 0..total_size {
for x in 0..total_size {
let is_dark = if x >= margin
&& x < margin + matrix_size
&& y >= margin
&& y < margin + matrix_size
{
let mx = (x - margin) as usize;
let my = (y - margin) as usize;
qr.modules()[my][mx]
} else {
false
};
fill_module(
&mut img,
x,
y,
module_size as u32,
is_dark,
&qr.fg_color,
&qr.bg_color,
);
}
}
if let Some(logo_data) = logo {
let _ = overlay_logo(&mut img, logo_data, 0.25);
}
let mut buf = Vec::new();
img.write_to(
&mut std::io::Cursor::new(&mut buf),
format.to_image_format(),
)?;
Ok(buf)
}