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
This commit is contained in:
@@ -1,6 +1,62 @@
|
||||
//! 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,
|
||||
@@ -24,32 +80,27 @@ fn fill_module(
|
||||
}
|
||||
}
|
||||
|
||||
/// 在 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(()); // 太小,跳过
|
||||
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 码到图像字节(支持 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;
|
||||
@@ -72,7 +123,6 @@ pub fn render_png(
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
fill_module(
|
||||
&mut img,
|
||||
x,
|
||||
@@ -85,13 +135,14 @@ pub fn render_png(
|
||||
}
|
||||
}
|
||||
|
||||
// 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)?;
|
||||
img.write_to(
|
||||
&mut std::io::Cursor::new(&mut buf),
|
||||
format.to_image_format(),
|
||||
)?;
|
||||
Ok(buf)
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
pub mod ascii;
|
||||
pub mod png;
|
||||
pub mod image;
|
||||
pub mod svg;
|
||||
|
||||
Reference in New Issue
Block a user