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:
+20
-13
@@ -55,6 +55,10 @@ struct Args {
|
||||
#[arg(long)]
|
||||
logo: Option<String>,
|
||||
|
||||
/// 输出图像格式 [png/bmp/jpeg/webp] [default: png]
|
||||
#[arg(short = 'f', long, default_value = "png")]
|
||||
format: String,
|
||||
|
||||
// ---- 编码模式参数 ----
|
||||
/// 编码模式 [text/url/wifi/vcard/email/phone/sms/batch]
|
||||
#[arg(long)]
|
||||
@@ -199,24 +203,27 @@ fn main() -> anyhow::Result<()> {
|
||||
.to_lowercase();
|
||||
|
||||
match ext.as_str() {
|
||||
"png" => {
|
||||
let bytes = qr.to_png_bytes(args.size, logo_bytes.as_deref())?;
|
||||
fs::write(path, bytes)?;
|
||||
println!(
|
||||
"已生成: {} (版本 {}, {}×{} 模块, {:?} 级纠错)",
|
||||
path,
|
||||
qr.version.0,
|
||||
qr.size(),
|
||||
qr.size(),
|
||||
qr.level
|
||||
);
|
||||
}
|
||||
"svg" => {
|
||||
let svg = qr.to_svg(logo_bytes.as_deref());
|
||||
fs::write(path, svg)?;
|
||||
println!("已生成: {} (版本 {}, SVG 格式)", path, qr.version.0);
|
||||
}
|
||||
_ => anyhow::bail!("不支持的文件格式: .{}。支持 .png / .svg", ext),
|
||||
_ => {
|
||||
let fmt = qr_core::render::image::OutputFormat::from_ext(&ext)
|
||||
.or_else(|| qr_core::render::image::OutputFormat::from_ext(&args.format))
|
||||
.unwrap_or(qr_core::render::image::OutputFormat::Png);
|
||||
let bytes = qr.to_image_bytes(args.size, logo_bytes.as_deref(), Some(fmt))?;
|
||||
fs::write(path, bytes)?;
|
||||
println!(
|
||||
"已生成: {} (版本 {}, {}×{} 模块, {:?} 级纠错, {})",
|
||||
path,
|
||||
qr.version.0,
|
||||
qr.size(),
|
||||
qr.size(),
|
||||
qr.level,
|
||||
fmt.extension()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
|
||||
+1
-1
@@ -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
@@ -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,3 +1,3 @@
|
||||
pub mod ascii;
|
||||
pub mod png;
|
||||
pub mod image;
|
||||
pub mod svg;
|
||||
|
||||
+7
-6
@@ -70,12 +70,13 @@ async fn generate_qr(Query(params): Query<QrParams>) -> impl IntoResponse {
|
||||
|
||||
if params.fmt == "svg" {
|
||||
let svg = qr.to_svg(None);
|
||||
([(header::CONTENT_TYPE, "image/svg+xml")], svg).into_response()
|
||||
} else {
|
||||
match qr.to_png_bytes(params.size, None) {
|
||||
Ok(b) => ([(header::CONTENT_TYPE, "image/png")], b).into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
return ([(header::CONTENT_TYPE, "image/svg+xml")], svg).into_response();
|
||||
}
|
||||
let fmt = qr_core::render::image::OutputFormat::from_ext(¶ms.fmt)
|
||||
.unwrap_or(qr_core::render::image::OutputFormat::Png);
|
||||
match qr.to_image_bytes(params.size, None, Some(fmt)) {
|
||||
Ok(b) => ([(header::CONTENT_TYPE, fmt.mime())], b).into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user