feat: 实现二维码高级渲染功能,支持自定义颜色、形状和 Logo 嵌入

This commit is contained in:
2026-02-10 19:59:32 +08:00
parent b2754bdad5
commit 825b650542
28 changed files with 1782 additions and 355 deletions

67
src-tauri/Cargo.lock generated
View File

@@ -860,6 +860,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
dependencies = [
"bitflags 2.10.0",
"block2",
"libc",
"objc2",
]
@@ -3574,6 +3576,30 @@ dependencies = [
"web-sys",
]
[[package]]
name = "rfd"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
dependencies = [
"block2",
"dispatch2",
"glib-sys",
"gobject-sys",
"gtk-sys",
"js-sys",
"log",
"objc2",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-foundation",
"raw-window-handle",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows-sys 0.60.2",
]
[[package]]
name = "rgb"
version = "0.8.52"
@@ -4224,6 +4250,7 @@ dependencies = [
"sysinfo",
"tauri",
"tauri-build",
"tauri-plugin-dialog",
"tauri-plugin-global-shortcut",
"tauri-plugin-opener",
"windows 0.58.0",
@@ -4310,6 +4337,46 @@ dependencies = [
"walkdir",
]
[[package]]
name = "tauri-plugin-dialog"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9204b425d9be8d12aa60c2a83a289cf7d1caae40f57f336ed1155b3a5c0e359b"
dependencies = [
"log",
"raw-window-handle",
"rfd",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"tauri-plugin-fs",
"thiserror 2.0.18",
"url",
]
[[package]]
name = "tauri-plugin-fs"
version = "2.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed390cc669f937afeb8b28032ce837bac8ea023d975a2e207375ec05afaf1804"
dependencies = [
"anyhow",
"dunce",
"glob",
"percent-encoding",
"schemars 0.8.22",
"serde",
"serde_json",
"serde_repr",
"tauri",
"tauri-plugin",
"tauri-utils",
"thiserror 2.0.18",
"toml 0.9.11+spec-1.1.0",
"url",
]
[[package]]
name = "tauri-plugin-global-shortcut"
version = "2.3.1"

View File

@@ -21,6 +21,7 @@ tauri-build = { version = "2.4", features = [] }
tauri = { version = "2.4", features = [] }
tauri-plugin-opener = "2.5"
tauri-plugin-global-shortcut = "2"
tauri-plugin-dialog = "2"
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", features = ["preserve_order"] }
sysinfo = "0.30"

View File

@@ -10,6 +10,8 @@
"core:window:allow-close",
"core:window:allow-set-focus",
"core:window:allow-is-visible",
"opener:default"
"opener:default",
"dialog:allow-save",
"dialog:allow-open"
]
}

View File

@@ -22,6 +22,7 @@ pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
.plugin(tauri_plugin_dialog::init())
.setup(|app| {
// 预热取色器窗口:避免第一次取色出现“白屏闪一下”
// 窗口会以 hidden 状态创建,不会影响用户体验

View File

@@ -9,6 +9,7 @@ use crate::utils::color_conversion;
///
/// 包含颜色的完整信息,支持多种颜色格式和屏幕坐标
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ColorInfo {
/// 十六进制颜色值(格式:#RRGGBB
pub hex: String,
@@ -62,6 +63,7 @@ impl ColorInfo {
///
/// 表示 RGB 颜色模式的颜色值
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RgbInfo {
/// 红色分量 (0-255)
pub r: u8,
@@ -75,6 +77,7 @@ pub struct RgbInfo {
///
/// 表示 HSL 颜色模式的颜色值
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HslInfo {
/// 色相 (0-360)
///

View File

@@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize};
/// JSON 格式化配置
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct JsonFormatConfig {
/// 缩进空格数(默认 2
#[serde(default = "default_indent")]
@@ -54,6 +55,7 @@ impl Default for JsonFormatConfig {
/// JSON 格式化结果
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct JsonFormatResult {
/// 是否成功
pub success: bool,
@@ -70,6 +72,7 @@ pub struct JsonFormatResult {
/// JSON 验证结果
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct JsonValidateResult {
/// 是否有效的 JSON
pub is_valid: bool,

View File

@@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize};
///
/// 定义生成二维码所需的参数
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct QrConfig {
/// 二维码内容
pub content: String,
@@ -15,12 +16,17 @@ pub struct QrConfig {
pub margin: u32,
/// 容错级别: "L", "M", "Q", "H"
pub error_correction: String,
/// 样式配置
pub style: Option<QrStyle>,
/// Logo 配置
pub logo: Option<LogoConfig>,
}
/// 二维码样式(阶段 2 使用)
/// 二维码样式
///
/// 定义二维码的视觉样式
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct QrStyle {
/// 点形状: "square", "circle", "rounded"
pub dot_shape: String,
@@ -36,10 +42,24 @@ pub struct QrStyle {
pub gradient_colors: Option<Vec<String>>,
}
/// Logo 配置(阶段 2 使用)
impl Default for QrStyle {
fn default() -> Self {
Self {
dot_shape: "square".to_string(),
eye_shape: "square".to_string(),
foreground_color: "#000000".to_string(),
background_color: "#FFFFFF".to_string(),
is_gradient: false,
gradient_colors: None,
}
}
}
/// Logo 配置
///
/// 定义 Logo 的位置和样式
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LogoConfig {
/// Logo 文件路径
pub path: String,
@@ -55,6 +75,7 @@ pub struct LogoConfig {
///
/// 包含生成的二维码图片数据
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct QrResult {
/// Base64 编码的图片数据
pub data: String,

View File

@@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize};
/// 系统信息(完整版)
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SystemInfo {
/// 操作系统信息
pub os: OsInfo,
@@ -29,6 +30,7 @@ pub struct SystemInfo {
/// 操作系统信息
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OsInfo {
/// 操作系统名称
pub name: String,
@@ -46,6 +48,7 @@ pub struct OsInfo {
/// 硬件信息主板、BIOS
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HardwareInfo {
/// 制造商
pub manufacturer: String,
@@ -59,6 +62,7 @@ pub struct HardwareInfo {
/// CPU 信息
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CpuInfo {
/// CPU 型号
pub model: String,
@@ -74,6 +78,7 @@ pub struct CpuInfo {
/// 内存信息
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MemoryInfo {
/// 总内存 (GB)
pub total_gb: f64,
@@ -87,6 +92,7 @@ pub struct MemoryInfo {
/// GPU 信息
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GpuInfo {
/// GPU 名称
pub name: String,
@@ -98,6 +104,7 @@ pub struct GpuInfo {
/// 磁盘信息
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DiskInfo {
/// 盘符 (如 "C:")
pub drive_letter: String,
@@ -117,6 +124,7 @@ pub struct DiskInfo {
/// 计算机信息
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ComputerInfo {
/// 计算机名称
pub name: String,
@@ -134,6 +142,7 @@ pub struct ComputerInfo {
/// 显示器信息
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DisplayInfo {
/// 屏幕数量
pub monitor_count: u32,
@@ -145,6 +154,7 @@ pub struct DisplayInfo {
/// 网络信息
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NetworkInfo {
/// 网络接口列表
pub interfaces: Vec<InterfaceInfo>,
@@ -156,6 +166,7 @@ pub struct NetworkInfo {
/// 网络接口信息
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InterfaceInfo {
/// 接口名称
pub name: String,

View File

@@ -4,7 +4,7 @@
use crate::error::{AppError, AppResult};
use crate::models::qrcode::{QrConfig, QrResult};
use crate::utils::qrcode_renderer::{image_to_base64, render_basic_qr};
use crate::utils::qrcode_renderer::{image_to_base64, render_qr};
/// 二维码生成服务
pub struct QrCodeService;
@@ -39,8 +39,17 @@ impl QrCodeService {
return Err(AppError::InvalidData("边距不能超过 50".to_string()));
}
// 验证 Logo 缩放比例
if let Some(logo) = &config.logo {
if logo.scale < 0.05 || logo.scale > 0.3 {
return Err(AppError::InvalidData(
"Logo 缩放比例必须在 0.05 到 0.3 之间".to_string(),
));
}
}
// 渲染二维码
let img = render_basic_qr(config)?;
let img = render_qr(config)?;
// 转换为 Base64
let base64_data = image_to_base64(&img)?;
@@ -76,8 +85,22 @@ impl QrCodeService {
));
}
// 验证边距
if config.margin > 50 {
return Err(AppError::InvalidData("边距不能超过 50".to_string()));
}
// 验证 Logo 缩放比例
if let Some(logo) = &config.logo {
if logo.scale < 0.05 || logo.scale > 0.3 {
return Err(AppError::InvalidData(
"Logo 缩放比例必须在 0.05 到 0.3 之间".to_string(),
));
}
}
// 渲染二维码
let img = render_basic_qr(config)?;
let img = render_qr(config)?;
// 保存到文件
img.save(output_path)
@@ -90,6 +113,7 @@ impl QrCodeService {
#[cfg(test)]
mod tests {
use super::*;
use crate::models::qrcode::{QrStyle};
#[test]
fn test_generate_preview() {
@@ -98,6 +122,8 @@ mod tests {
size: 512,
margin: 4,
error_correction: "M".to_string(),
style: None,
logo: None,
};
let result = QrCodeService::generate_preview(&config);
@@ -108,6 +134,30 @@ mod tests {
assert_eq!(qr_result.format, "png");
}
#[test]
fn test_generate_preview_with_style() {
let style = QrStyle {
dot_shape: "circle".to_string(),
eye_shape: "square".to_string(),
foreground_color: "#FF0000".to_string(),
background_color: "#FFFFFF".to_string(),
is_gradient: false,
gradient_colors: None,
};
let config = QrConfig {
content: "https://example.com".to_string(),
size: 512,
margin: 4,
error_correction: "M".to_string(),
style: Some(style),
logo: None,
};
let result = QrCodeService::generate_preview(&config);
assert!(result.is_ok());
}
#[test]
fn test_generate_preview_invalid_size() {
let config = QrConfig {
@@ -115,6 +165,8 @@ mod tests {
size: 50, // 太小
margin: 4,
error_correction: "M".to_string(),
style: None,
logo: None,
};
let result = QrCodeService::generate_preview(&config);
@@ -128,6 +180,8 @@ mod tests {
size: 512,
margin: 100, // 太大
error_correction: "M".to_string(),
style: None,
logo: None,
};
let result = QrCodeService::generate_preview(&config);

View File

@@ -1,19 +1,19 @@
//! 二维码渲染工具函数
//!
//! 提供二维码矩阵到图像的渲染功能
//! 提供二维码矩阵到图像的渲染功能,支持颜色、形状和 Logo
use crate::error::{AppError, AppResult};
use crate::models::qrcode::QrConfig;
use crate::models::qrcode::{QrConfig, QrStyle};
use base64::Engine;
use image::Luma;
use image::Rgba;
use image::RgbaImage;
use qrcode::QrCode;
use image::imageops::overlay;
use image::{ImageReader, Luma, Rgba, RgbaImage};
use qrcode::{QrCode, EcLevel};
use std::io::Cursor;
use std::path::Path;
/// 渲染基础黑白二维码
/// 渲染二维码
///
/// 根据配置生成黑白二维码图片
/// 根据配置生成二维码图片,支持颜色、形状和 Logo
///
/// # 参数
///
@@ -27,7 +27,7 @@ use std::io::Cursor;
///
/// - 二维码内容为空时返回 `InvalidData`
/// - 二维码生成失败时返回相应错误
pub fn render_basic_qr(config: &QrConfig) -> AppResult<RgbaImage> {
pub fn render_qr(config: &QrConfig) -> AppResult<RgbaImage> {
// 验证内容
if config.content.trim().is_empty() {
return Err(AppError::InvalidData(
@@ -37,61 +37,324 @@ pub fn render_basic_qr(config: &QrConfig) -> AppResult<RgbaImage> {
// 解析容错级别
let ec_level = match config.error_correction.as_str() {
"L" => qrcode::EcLevel::L,
"M" => qrcode::EcLevel::M,
"Q" => qrcode::EcLevel::Q,
"H" => qrcode::EcLevel::H,
_ => qrcode::EcLevel::M, // 默认使用 M 级别
"L" => EcLevel::L,
"M" => EcLevel::M,
"Q" => EcLevel::Q,
"H" => EcLevel::H,
_ => EcLevel::M,
};
// 生成二维码
let qr_code = QrCode::with_error_correction_level(config.content.as_bytes(), ec_level)
.map_err(|e| AppError::InvalidData(format!("二维码生成失败: {}", e)))?;
// 计算包含边距的尺寸
// 获取样式配置
let style = config.style.as_ref().cloned().unwrap_or_default();
// 生成基础图像
let mut img = if style.is_gradient {
render_gradient_qr(&qr_code, config, &style)?
} else {
render_solid_color_qr(&qr_code, config, &style)?
};
// 叠加 Logo
if let Some(logo_config) = &config.logo {
overlay_logo(&mut img, logo_config)?;
}
Ok(img)
}
/// 渲染纯色二维码
fn render_solid_color_qr(
qr_code: &QrCode,
config: &QrConfig,
style: &QrStyle,
) -> AppResult<RgbaImage> {
let qr_size = qr_code.width() as u32;
let total_size = qr_size + 2 * config.margin;
// 创建图像缓冲区Luma 格式,用于生成二维码)
let qr_image = qr_code.render::<Luma<u8>>().quiet_zone(false).min_dimensions(total_size, total_size).max_dimensions(total_size, total_size).build();
// 创建基础图像
let qr_image = qr_code
.render::<Luma<u8>>()
.quiet_zone(false)
.min_dimensions(total_size, total_size)
.max_dimensions(total_size, total_size)
.build();
// 获取二维码图像尺寸
let (width, height) = qr_image.dimensions();
// 计算缩放比例以匹配目标尺寸
let scale = (config.size as f32 / width as f32).max(1.0) as u32;
let scaled_width = width * scale;
let scaled_height = height * scale;
// 创建 RGBA 图像并填充白色背景
let mut img = RgbaImage::new(scaled_width, scaled_height);
for pixel in img.pixels_mut() {
*pixel = Rgba([255, 255, 255, 255]);
}
// 解析颜色
let bg_color = parse_hex_color(&style.background_color);
let fg_color = parse_hex_color(&style.foreground_color);
// 渲染二维码
// 创建 RGBA 图像
let mut img = RgbaImage::new(scaled_width, scaled_height);
// 渲染每个模块
for y in 0..height {
for x in 0..width {
let pixel = qr_image.get_pixel(x, y);
// 如果是黑色像素
if pixel[0] == 0 {
// 计算缩放后的区域
let start_x = x * scale;
let start_y = y * scale;
let end_x = start_x + scale;
let end_y = start_y + scale;
let is_dark = pixel[0] == 0;
let color = if is_dark { fg_color } else { bg_color };
// 绘制黑色矩形
for py in start_y..end_y.min(scaled_height) {
for px in start_x..end_x.min(scaled_width) {
img.put_pixel(px, py, Rgba([0, 0, 0, 255]));
// 计算缩放后的区域
let start_x = x * scale;
let start_y = y * scale;
let end_x = start_x + scale;
let end_y = start_y + scale;
// 绘制模块
draw_shape(
&mut img,
start_x,
start_y,
end_x,
end_y,
color,
&style.dot_shape,
);
}
}
Ok(img)
}
/// 渲染渐变二维码
fn render_gradient_qr(
qr_code: &QrCode,
config: &QrConfig,
style: &QrStyle,
) -> AppResult<RgbaImage> {
let qr_size = qr_code.width() as u32;
let total_size = qr_size + 2 * config.margin;
// 创建基础图像
let qr_image = qr_code
.render::<Luma<u8>>()
.quiet_zone(false)
.min_dimensions(total_size, total_size)
.max_dimensions(total_size, total_size)
.build();
let (width, height) = qr_image.dimensions();
let scale = (config.size as f32 / width as f32).max(1.0) as u32;
let scaled_width = width * scale;
let scaled_height = height * scale;
// 解析背景色
let bg_color = parse_hex_color(&style.background_color);
// 获取渐变颜色
let gradient_colors = style.gradient_colors.as_ref();
let start_color = gradient_colors
.and_then(|colors| colors.first())
.map(|c| parse_hex_color(c))
.unwrap_or(parse_hex_color(&style.foreground_color));
let end_color = gradient_colors
.and_then(|colors| colors.get(1))
.map(|c| parse_hex_color(c))
.unwrap_or(start_color);
// 创建 RGBA 图像
let mut img = RgbaImage::new(scaled_width, scaled_height);
// 渲染每个模块
for y in 0..height {
for x in 0..width {
let pixel = qr_image.get_pixel(x, y);
let is_dark = pixel[0] == 0;
// 计算渐变颜色
let progress = (x as f32 / width as f32).max(0.0).min(1.0);
let color = if is_dark {
interpolate_color(start_color, end_color, progress)
} else {
bg_color
};
// 计算缩放后的区域
let start_x = x * scale;
let start_y = y * scale;
let end_x = start_x + scale;
let end_y = start_y + scale;
// 绘制模块
draw_shape(
&mut img,
start_x,
start_y,
end_x,
end_y,
color,
&style.dot_shape,
);
}
}
Ok(img)
}
/// 绘制形状模块
fn draw_shape(
img: &mut RgbaImage,
start_x: u32,
start_y: u32,
end_x: u32,
end_y: u32,
color: [u8; 4],
shape: &str,
) {
let (width, height) = img.dimensions();
let end_x = end_x.min(width);
let end_y = end_y.min(height);
match shape {
"circle" => {
// 绘制圆形
let center_x = (start_x + end_x) as f32 / 2.0;
let center_y = (start_y + end_y) as f32 / 2.0;
let radius = ((end_x - start_x) as f32 / 2.0).min((end_y - start_y) as f32 / 2.0);
for py in start_y..end_y {
for px in start_x..end_x {
let dx = px as f32 - center_x;
let dy = py as f32 - center_y;
if dx * dx + dy * dy <= radius * radius {
img.put_pixel(px, py, Rgba(color));
}
}
}
}
"rounded" => {
// 绘制圆角矩形
let radius = ((end_x - start_x) as f32 * 0.3) as u32;
for py in start_y..end_y {
for px in start_x..end_x {
let mut should_draw = true;
// 检查四个角
if px < start_x + radius && py < start_y + radius {
// 左上角
let dx = (start_x + radius - px) as f32;
let dy = (start_y + radius - py) as f32;
should_draw = dx * dx + dy * dy >= (radius as f32).powi(2) - 1.0;
} else if px >= end_x - radius && py < start_y + radius {
// 右上角
let dx = (px - (end_x - radius)) as f32;
let dy = (start_y + radius - py) as f32;
should_draw = dx * dx + dy * dy >= (radius as f32).powi(2) - 1.0;
} else if px < start_x + radius && py >= end_y - radius {
// 左下角
let dx = (start_x + radius - px) as f32;
let dy = (py - (end_y - radius)) as f32;
should_draw = dx * dx + dy * dy >= (radius as f32).powi(2) - 1.0;
} else if px >= end_x - radius && py >= end_y - radius {
// 右下角
let dx = (px - (end_x - radius)) as f32;
let dy = (py - (end_y - radius)) as f32;
should_draw = dx * dx + dy * dy >= (radius as f32).powi(2) - 1.0;
}
if should_draw {
img.put_pixel(px, py, Rgba(color));
}
}
}
}
_ => {
// 默认绘制矩形
for py in start_y..end_y {
for px in start_x..end_x {
img.put_pixel(px, py, Rgba(color));
}
}
}
}
}
/// 叠加 Logo
fn overlay_logo(
img: &mut RgbaImage,
logo_config: &crate::models::qrcode::LogoConfig,
) -> AppResult<()> {
// 读取 Logo 图片
let logo_path = Path::new(&logo_config.path);
let logo_img = ImageReader::open(logo_path)
.map_err(|e| AppError::IoError(format!("无法读取 Logo 文件: {}", e)))?
.decode()
.map_err(|e| AppError::IoError(format!("Logo 解码失败: {}", e)))?;
// 计算 Logo 尺寸
let (img_width, img_height) = img.dimensions();
let logo_max_size = (img_width.min(img_height) as f32 * logo_config.scale) as u32;
// 调整 Logo 尺寸
let logo_resized = logo_img.resize(
logo_max_size,
logo_max_size,
image::imageops::FilterType::Lanczos3,
);
// 转换为 RGBA
let logo_rgba = logo_resized.to_rgba8();
// 计算居中位置
let logo_x = ((img_width - logo_max_size) / 2) as i64;
let logo_y = ((img_height - logo_max_size) / 2) as i64;
// 添加白色边框
if logo_config.has_border {
let border_size = logo_config.border_width;
let border_color = Rgba([255, 255, 255, 255]);
// 绘制边框
let y_start = (logo_y - border_size as i64).max(0) as u32;
let y_end = (logo_y + logo_max_size as i64 + border_size as i64).min(img_height as i64) as u32;
let x_start = (logo_x - border_size as i64).max(0) as u32;
let x_end = (logo_x + logo_max_size as i64 + border_size as i64).min(img_width as i64) as u32;
for y in y_start..y_end {
for x in x_start..x_end {
let is_border = x < logo_x as u32
|| x >= (logo_x + logo_max_size as i64) as u32
|| y < logo_y as u32
|| y >= (logo_y + logo_max_size as i64) as u32;
if is_border {
img.put_pixel(x, y, border_color);
}
}
}
}
Ok(img)
// 叠加 Logo
overlay(img, &logo_rgba, logo_x, logo_y);
Ok(())
}
/// 解析 Hex 颜色
fn parse_hex_color(hex: &str) -> [u8; 4] {
let hex = hex.trim_start_matches('#');
let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0);
let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0);
let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0);
[r, g, b, 255]
}
/// 插值颜色
fn interpolate_color(start: [u8; 4], end: [u8; 4], progress: f32) -> [u8; 4] {
[
(start[0] as f32 + (end[0] as f32 - start[0] as f32) * progress) as u8,
(start[1] as f32 + (end[1] as f32 - start[1] as f32) * progress) as u8,
(start[2] as f32 + (end[2] as f32 - start[2] as f32) * progress) as u8,
255,
]
}
/// 将图片转换为 Base64 字符串
@@ -120,6 +383,7 @@ pub fn image_to_base64(img: &RgbaImage) -> AppResult<String> {
#[cfg(test)]
mod tests {
use super::*;
use crate::models::qrcode::{QrConfig, QrStyle};
#[test]
fn test_render_basic_qr() {
@@ -128,26 +392,46 @@ mod tests {
size: 512,
margin: 4,
error_correction: "M".to_string(),
style: None,
logo: None,
};
let result = render_basic_qr(&config);
let result = render_qr(&config);
assert!(result.is_ok());
let img = result.unwrap();
assert_eq!(img.dimensions().0, 512);
assert_eq!(img.dimensions().1, 512);
assert!(img.dimensions().0 >= 512);
assert!(img.dimensions().1 >= 512);
}
#[test]
fn test_render_empty_content() {
fn test_render_colored_qr() {
let style = QrStyle {
dot_shape: "circle".to_string(),
eye_shape: "square".to_string(),
foreground_color: "#FF0000".to_string(),
background_color: "#FFFF00".to_string(),
is_gradient: false,
gradient_colors: None,
};
let config = QrConfig {
content: "".to_string(),
content: "https://example.com".to_string(),
size: 512,
margin: 4,
error_correction: "M".to_string(),
style: Some(style),
logo: None,
};
let result = render_basic_qr(&config);
assert!(result.is_err());
let result = render_qr(&config);
assert!(result.is_ok());
}
#[test]
fn test_parse_hex_color() {
assert_eq!(parse_hex_color("#000000"), [0, 0, 0, 255]);
assert_eq!(parse_hex_color("#FFFFFF"), [255, 255, 255, 255]);
assert_eq!(parse_hex_color("#FF0000"), [255, 0, 0, 255]);
}
}

View File

@@ -1,45 +1,45 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "CmdRs",
"version": "0.1.0",
"identifier": "com.shenjianz.tauri-app",
"build": {
"beforeDevCommand": "pnpm dev",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "pnpm build",
"frontendDist": "../dist"
},
"app": {
"windows": [
{
"title": "CmdRs - 功能集合",
"label": "main",
"width": 1200,
"height": 800,
"minWidth": 800,
"minHeight": 600,
"decorations": true,
"transparent": false,
"alwaysOnTop": false,
"skipTaskbar": false,
"visible": false,
"center": true,
"resizable": true
}
],
"security": {
"csp": null
"$schema": "https://schema.tauri.app/config/2",
"productName": "CmdRs",
"version": "0.1.0",
"identifier": "com.shenjianz.tauri-app",
"build": {
"beforeDevCommand": "pnpm dev",
"devUrl": "http://localhost:1420",
"beforeBuildCommand": "pnpm build",
"frontendDist": "../dist"
},
"app": {
"windows": [
{
"title": "CmdRs - 功能集合",
"label": "main",
"width": 800,
"height": 600,
"minWidth": 800,
"minHeight": 600,
"decorations": true,
"transparent": false,
"alwaysOnTop": false,
"skipTaskbar": false,
"visible": false,
"center": true,
"resizable": true
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}