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:
2026-06-19 21:34:21 +08:00
parent ef6b092eda
commit b41f6ee7df
6 changed files with 115 additions and 41 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ categories.workspace = true
rust-version.workspace = true
[dependencies]
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp"] }
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "bmp"] }
serde = { version = "1", features = ["derive"] }
[dev-dependencies]
+22 -7
View File
@@ -271,24 +271,39 @@ impl QrCode {
crate::render::ascii::render_ascii(self, invert)
}
/// 导出为 PNG 字节数据
/// 导出为图像字节数据(支持 PNG/BMP/JPEG/WebP
///
/// `module_size` 控制每个模块的像素大小(2~20),越大文件越大
/// `logo` 可选的 logo 图片字节,会在 QR 码中央叠加(建议搭配 H 级纠错)
/// `module_size` 控制每个模块的像素大小(2~20)。
/// `format` 输出格式,默认为 Png。
/// `logo` 可选的 logo 图片字节。
///
/// ```rust
/// use qr_core::qr::{QrCode, QrConfig};
///
/// let qr = QrCode::encode("PNG test", QrConfig::default()).unwrap();
/// let bytes = qr.to_png_bytes(4, None).unwrap();
/// std::fs::write("test.png", &bytes).unwrap();
/// let qr = QrCode::encode("test", QrConfig::default()).unwrap();
/// let bytes = qr.to_image_bytes(4, None, None).unwrap();
/// ```
pub fn to_image_bytes(
&self,
module_size: u8,
logo: Option<&[u8]>,
format: Option<crate::render::image::OutputFormat>,
) -> Result<Vec<u8>, image::ImageError> {
crate::render::image::render_image(
self,
module_size,
format.unwrap_or(crate::render::image::OutputFormat::Png),
logo,
)
}
/// 导出为 PNG 字节数据(便捷方法,兼容旧 API)
pub fn to_png_bytes(
&self,
module_size: u8,
logo: Option<&[u8]>,
) -> Result<Vec<u8>, image::ImageError> {
crate::render::png::render_png(self, module_size, logo)
self.to_image_bytes(module_size, logo, None)
}
}
@@ -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 -1
View File
@@ -1,3 +1,3 @@
pub mod ascii;
pub mod png;
pub mod image;
pub mod svg;