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)]
|
#[arg(long)]
|
||||||
logo: Option<String>,
|
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]
|
/// 编码模式 [text/url/wifi/vcard/email/phone/sms/batch]
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
@@ -199,24 +203,27 @@ fn main() -> anyhow::Result<()> {
|
|||||||
.to_lowercase();
|
.to_lowercase();
|
||||||
|
|
||||||
match ext.as_str() {
|
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" => {
|
"svg" => {
|
||||||
let svg = qr.to_svg(logo_bytes.as_deref());
|
let svg = qr.to_svg(logo_bytes.as_deref());
|
||||||
fs::write(path, svg)?;
|
fs::write(path, svg)?;
|
||||||
println!("已生成: {} (版本 {}, SVG 格式)", path, qr.version.0);
|
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 => {
|
None => {
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@ categories.workspace = true
|
|||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[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"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
+22
-7
@@ -271,24 +271,39 @@ impl QrCode {
|
|||||||
crate::render::ascii::render_ascii(self, invert)
|
crate::render::ascii::render_ascii(self, invert)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 导出为 PNG 字节数据
|
/// 导出为图像字节数据(支持 PNG/BMP/JPEG/WebP)
|
||||||
///
|
///
|
||||||
/// `module_size` 控制每个模块的像素大小(2~20),越大文件越大。
|
/// `module_size` 控制每个模块的像素大小(2~20)。
|
||||||
/// `logo` 可选的 logo 图片字节,会在 QR 码中央叠加(建议搭配 H 级纠错)
|
/// `format` 输出格式,默认为 Png。
|
||||||
|
/// `logo` 可选的 logo 图片字节。
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// use qr_core::qr::{QrCode, QrConfig};
|
/// use qr_core::qr::{QrCode, QrConfig};
|
||||||
///
|
///
|
||||||
/// let qr = QrCode::encode("PNG test", QrConfig::default()).unwrap();
|
/// let qr = QrCode::encode("test", QrConfig::default()).unwrap();
|
||||||
/// let bytes = qr.to_png_bytes(4, None).unwrap();
|
/// let bytes = qr.to_image_bytes(4, None, None).unwrap();
|
||||||
/// std::fs::write("test.png", &bytes).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(
|
pub fn to_png_bytes(
|
||||||
&self,
|
&self,
|
||||||
module_size: u8,
|
module_size: u8,
|
||||||
logo: Option<&[u8]>,
|
logo: Option<&[u8]>,
|
||||||
) -> Result<Vec<u8>, image::ImageError> {
|
) -> 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 crate::qr::QrCode;
|
||||||
use image::{imageops, ImageBuffer, Rgba, RgbaImage};
|
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(
|
fn fill_module(
|
||||||
img: &mut RgbaImage,
|
img: &mut RgbaImage,
|
||||||
x: u32,
|
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> {
|
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 = image::load_from_memory(logo_bytes).map_err(|e| format!("Logo 加载失败: {e}"))?;
|
||||||
let logo = logo.to_rgba8();
|
let logo = logo.to_rgba8();
|
||||||
|
|
||||||
let img_w = img.width();
|
let img_w = img.width();
|
||||||
let img_h = img.height();
|
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;
|
let logo_size = (img_w.min(img_h) as f32 * logo_size_pct) as u32;
|
||||||
if logo_size < 4 {
|
if logo_size < 4 {
|
||||||
return Ok(()); // 太小,跳过
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let resized = imageops::resize(&logo, logo_size, logo_size, imageops::FilterType::Lanczos3);
|
let resized = imageops::resize(&logo, logo_size, logo_size, imageops::FilterType::Lanczos3);
|
||||||
|
|
||||||
let x = (img_w - logo_size) / 2;
|
let x = (img_w - logo_size) / 2;
|
||||||
let y = (img_h - logo_size) / 2;
|
let y = (img_h - logo_size) / 2;
|
||||||
imageops::overlay(img, &resized, x as i64, y as i64);
|
imageops::overlay(img, &resized, x as i64, y as i64);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_png(
|
/// 渲染 QR 码到图像字节(支持 PNG/BMP/JPEG/WebP)
|
||||||
|
pub fn render_image(
|
||||||
qr: &QrCode,
|
qr: &QrCode,
|
||||||
module_size: u8,
|
module_size: u8,
|
||||||
|
format: OutputFormat,
|
||||||
logo: Option<&[u8]>,
|
logo: Option<&[u8]>,
|
||||||
) -> Result<Vec<u8>, image::ImageError> {
|
) -> Result<Vec<u8>, image::ImageError> {
|
||||||
let matrix_size = qr.size() as u32;
|
let matrix_size = qr.size() as u32;
|
||||||
@@ -72,7 +123,6 @@ pub fn render_png(
|
|||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
};
|
};
|
||||||
|
|
||||||
fill_module(
|
fill_module(
|
||||||
&mut img,
|
&mut img,
|
||||||
x,
|
x,
|
||||||
@@ -85,13 +135,14 @@ pub fn render_png(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logo 叠加
|
|
||||||
if let Some(logo_data) = logo {
|
if let Some(logo_data) = logo {
|
||||||
// 忽略 logo 叠加错误(logo 有损不影响 QR 主体)
|
|
||||||
let _ = overlay_logo(&mut img, logo_data, 0.25);
|
let _ = overlay_logo(&mut img, logo_data, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut buf = Vec::new();
|
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)
|
Ok(buf)
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
pub mod ascii;
|
pub mod ascii;
|
||||||
pub mod png;
|
pub mod image;
|
||||||
pub mod svg;
|
pub mod svg;
|
||||||
|
|||||||
+6
-5
@@ -70,12 +70,13 @@ async fn generate_qr(Query(params): Query<QrParams>) -> impl IntoResponse {
|
|||||||
|
|
||||||
if params.fmt == "svg" {
|
if params.fmt == "svg" {
|
||||||
let svg = qr.to_svg(None);
|
let svg = qr.to_svg(None);
|
||||||
([(header::CONTENT_TYPE, "image/svg+xml")], svg).into_response()
|
return ([(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(),
|
|
||||||
}
|
}
|
||||||
|
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