use image::{DynamicImage, ImageBuffer, Rgba, RgbaImage};
use qrcode::render::svg;
use qrcode::types::Color as QrColor;
use qrcode::{EcLevel, QrCode};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum QrCodeError {
#[error("二维码生成失败: {0}")]
Generation(String),
#[error("数据编码失败: {0}")]
Encoding(String),
#[error("IO 错误: {0}")]
Io(String),
}
impl From<std::io::Error> for QrCodeError {
fn from(err: std::io::Error) -> Self {
QrCodeError::Io(err.to_string())
}
}
impl From<image::ImageError> for QrCodeError {
fn from(err: image::ImageError) -> Self {
QrCodeError::Io(err.to_string())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ErrorCorrectionLevel {
Low,
#[default]
Medium,
Quartile,
High,
}
impl ErrorCorrectionLevel {
fn to_ec_level(self) -> EcLevel {
match self {
Self::Low => EcLevel::L,
Self::Medium => EcLevel::M,
Self::Quartile => EcLevel::Q,
Self::High => EcLevel::H,
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::Low => "low",
Self::Medium => "medium",
Self::Quartile => "quartile",
Self::High => "high",
}
}
}
impl std::fmt::Display for ErrorCorrectionLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone)]
pub struct QrCodeConfig {
pub size: u32,
pub margin: u32,
pub foreground_color: [u8; 3],
pub background_color: [u8; 3],
pub error_correction_level: ErrorCorrectionLevel,
}
impl Default for QrCodeConfig {
fn default() -> Self {
Self {
size: 200,
margin: 10,
foreground_color: [0, 0, 0],
background_color: [255, 255, 255],
error_correction_level: ErrorCorrectionLevel::Medium,
}
}
}
impl QrCodeConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_size(mut self, size: u32) -> Self {
self.size = size;
self
}
pub fn with_margin(mut self, margin: u32) -> Self {
self.margin = margin;
self
}
pub fn with_foreground_color(mut self, color: [u8; 3]) -> Self {
self.foreground_color = color;
self
}
pub fn with_background_color(mut self, color: [u8; 3]) -> Self {
self.background_color = color;
self
}
pub fn with_error_correction_level(mut self, level: ErrorCorrectionLevel) -> Self {
self.error_correction_level = level;
self
}
fn color_to_hex(color: [u8; 3]) -> String {
format!("#{:02x}{:02x}{:02x}", color[0], color[1], color[2])
}
}
#[derive(Debug, Clone)]
pub struct QrCodeGenerator {
config: QrCodeConfig,
}
impl QrCodeGenerator {
pub fn new() -> Self {
Self {
config: QrCodeConfig::default(),
}
}
pub fn with_config(config: QrCodeConfig) -> Self {
Self { config }
}
pub fn config(&self) -> &QrCodeConfig {
&self.config
}
pub fn generate_matrix(&self, data: &str) -> Result<Vec<Vec<bool>>, QrCodeError> {
if data.is_empty() {
return Err(QrCodeError::Encoding("数据不能为空".to_string()));
}
let code = QrCode::with_error_correction_level(
data.as_bytes(),
self.config.error_correction_level.to_ec_level(),
)
.map_err(|e| QrCodeError::Generation(format!("二维码编码失败: {e}")))?;
let width = code.width();
let matrix = (0..width)
.map(|y| (0..width).map(|x| code[(x, y)] == QrColor::Dark).collect())
.collect();
Ok(matrix)
}
pub fn generate_png(&self, data: &str) -> Result<Vec<u8>, QrCodeError> {
let matrix = self.generate_matrix(data)?;
let png_bytes = self.render_matrix_to_png(&matrix)?;
Ok(png_bytes)
}
pub fn generate_svg(&self, data: &str) -> Result<String, QrCodeError> {
if data.is_empty() {
return Err(QrCodeError::Encoding("数据不能为空".to_string()));
}
let code = QrCode::with_error_correction_level(
data.as_bytes(),
self.config.error_correction_level.to_ec_level(),
)
.map_err(|e| QrCodeError::Generation(format!("二维码编码失败: {e}")))?;
let fg_hex = QrCodeConfig::color_to_hex(self.config.foreground_color);
let bg_hex = QrCodeConfig::color_to_hex(self.config.background_color);
let svg_string = code
.render::<svg::Color>()
.dark_color(svg::Color(&fg_hex))
.light_color(svg::Color(&bg_hex))
.quiet_zone(self.config.margin > 0)
.min_dimensions(self.config.size, self.config.size)
.build();
Ok(svg_string)
}
fn render_matrix_to_png(&self, matrix: &[Vec<bool>]) -> Result<Vec<u8>, QrCodeError> {
let matrix_width = matrix.len();
if matrix_width == 0 {
return Err(QrCodeError::Generation("矩阵为空".to_string()));
}
let total_size = self.config.size;
let margin = self.config.margin;
let qr_area = total_size
.checked_sub(margin.saturating_mul(2))
.filter(|&v| v > 0)
.ok_or_else(|| {
QrCodeError::Generation(format!(
"尺寸不足以容纳边距: size={total_size}, margin={margin}"
))
})?;
let module_size = (qr_area / matrix_width as u32).max(1);
let [fr, fg, fb] = self.config.foreground_color;
let [br, bg, bb] = self.config.background_color;
let fg_pixel = Rgba([fr, fg, fb, 255]);
let bg_pixel = Rgba([br, bg, bb, 255]);
let mut img: RgbaImage = ImageBuffer::from_pixel(total_size, total_size, bg_pixel);
for (y, row) in matrix.iter().enumerate() {
for (x, &is_dark) in row.iter().enumerate() {
if is_dark {
let start_x = margin + (x as u32) * module_size;
let start_y = margin + (y as u32) * module_size;
for dy in 0..module_size {
for dx in 0..module_size {
let px = start_x + dx;
let py = start_y + dy;
if px < total_size && py < total_size {
img.put_pixel(px, py, fg_pixel);
}
}
}
}
}
}
let dynamic = DynamicImage::ImageRgba8(img);
let mut bytes = Vec::new();
let mut cursor = std::io::Cursor::new(&mut bytes);
dynamic.write_to(&mut cursor, image::ImageFormat::Png)?;
Ok(bytes)
}
}
impl Default for QrCodeGenerator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_qr_code_config_default() {
let config = QrCodeConfig::default();
assert_eq!(config.size, 200);
assert_eq!(config.margin, 10);
assert_eq!(config.foreground_color, [0, 0, 0]);
assert_eq!(config.background_color, [255, 255, 255]);
assert_eq!(config.error_correction_level, ErrorCorrectionLevel::Medium);
}
#[test]
fn test_qr_code_config_builder() {
let config = QrCodeConfig::new()
.with_size(300)
.with_margin(20)
.with_foreground_color([255, 0, 0])
.with_background_color([0, 0, 255])
.with_error_correction_level(ErrorCorrectionLevel::High);
assert_eq!(config.size, 300);
assert_eq!(config.margin, 20);
assert_eq!(config.foreground_color, [255, 0, 0]);
assert_eq!(config.background_color, [0, 0, 255]);
assert_eq!(config.error_correction_level, ErrorCorrectionLevel::High);
}
#[test]
fn test_error_correction_level() {
assert_eq!(
ErrorCorrectionLevel::default(),
ErrorCorrectionLevel::Medium
);
assert_eq!(ErrorCorrectionLevel::Low.as_str(), "low");
assert_eq!(ErrorCorrectionLevel::Medium.as_str(), "medium");
assert_eq!(ErrorCorrectionLevel::Quartile.as_str(), "quartile");
assert_eq!(ErrorCorrectionLevel::High.as_str(), "high");
assert_eq!(format!("{}", ErrorCorrectionLevel::Low), "low");
assert_eq!(format!("{}", ErrorCorrectionLevel::High), "high");
assert_eq!(ErrorCorrectionLevel::Low.to_ec_level(), EcLevel::L);
assert_eq!(ErrorCorrectionLevel::Medium.to_ec_level(), EcLevel::M);
assert_eq!(ErrorCorrectionLevel::Quartile.to_ec_level(), EcLevel::Q);
assert_eq!(ErrorCorrectionLevel::High.to_ec_level(), EcLevel::H);
}
#[test]
fn test_qr_code_generator_default() {
let generator = QrCodeGenerator::new();
assert_eq!(generator.config().size, 200);
assert_eq!(generator.config().margin, 10);
assert_eq!(
generator.config().error_correction_level,
ErrorCorrectionLevel::Medium
);
}
#[test]
fn test_qr_code_generator_with_config() {
let config = QrCodeConfig::new()
.with_size(400)
.with_margin(15)
.with_error_correction_level(ErrorCorrectionLevel::Quartile);
let generator = QrCodeGenerator::with_config(config);
assert_eq!(generator.config().size, 400);
assert_eq!(generator.config().margin, 15);
assert_eq!(
generator.config().error_correction_level,
ErrorCorrectionLevel::Quartile
);
}
#[test]
fn test_generate_matrix_basic() {
let generator = QrCodeGenerator::new();
let matrix = generator.generate_matrix("Hello, World!").unwrap();
assert!(!matrix.is_empty(), "矩阵不能为空");
let width = matrix.len();
for row in &matrix {
assert_eq!(row.len(), width, "矩阵必须是正方形");
}
}
#[test]
fn test_generate_matrix_empty_data() {
let generator = QrCodeGenerator::new();
let result = generator.generate_matrix("");
assert!(result.is_err(), "空数据应返回错误");
match result {
Err(QrCodeError::Encoding(_)) => {}
other => panic!("期望 Encoding 错误,得到: {other:?}"),
}
}
#[test]
fn test_generate_png_basic() {
let generator = QrCodeGenerator::new();
let png = generator.generate_png("https://example.com").unwrap();
assert!(!png.is_empty(), "PNG 字节流不能为空");
assert_eq!(png[0], 0x89, "PNG magic byte 0");
assert_eq!(png[1], 0x50, "PNG magic byte 1 ('P')");
assert_eq!(png[2], 0x4E, "PNG magic byte 2 ('N')");
assert_eq!(png[3], 0x47, "PNG magic byte 3 ('G')");
assert_eq!(png[4], 0x0D, "PNG magic byte 4 (CR)");
assert_eq!(png[5], 0x0A, "PNG magic byte 5 (LF)");
assert_eq!(png[6], 0x1A, "PNG magic byte 6");
assert_eq!(png[7], 0x0A, "PNG magic byte 7 (LF)");
}
#[test]
fn test_generate_png_valid_output() {
let generator = QrCodeGenerator::new();
let png = generator.generate_png("test data 12345").unwrap();
let png_signature: [u8; 8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
assert_eq!(&png[..8], &png_signature, "PNG 头部签名必须匹配");
assert!(png.len() > 24, "PNG 数据长度应超过头部+IHDR");
}
#[test]
fn test_generate_svg_basic() {
let generator = QrCodeGenerator::new();
let svg = generator.generate_svg("Hello SVG").unwrap();
assert!(!svg.is_empty(), "SVG 字符串不能为空");
assert!(svg.contains("<svg"), "SVG 必须包含 <svg> 标签");
assert!(svg.contains("</svg>"), "SVG 必须包含 </svg> 闭合标签");
}
#[test]
fn test_generate_svg_contains_data() {
let generator = QrCodeGenerator::new();
let svg = generator.generate_svg("Data content test 12345").unwrap();
assert!(
svg.contains("<rect") || svg.contains("<path"),
"SVG 必须包含 rect 或 path 元素来表示二维码模块"
);
assert!(svg.contains("#000000"), "SVG 应包含默认前景色 #000000");
}
#[test]
fn test_generate_different_data_different_matrix() {
let generator = QrCodeGenerator::new();
let matrix1 = generator.generate_matrix("data one").unwrap();
let matrix2 = generator.generate_matrix("data two").unwrap();
assert_ne!(matrix1, matrix2, "不同数据应生成不同的二维码矩阵");
}
#[test]
fn test_generate_high_error_correction() {
let config = QrCodeConfig::new().with_error_correction_level(ErrorCorrectionLevel::High);
let generator = QrCodeGenerator::with_config(config);
let png = generator.generate_png("High EC test").unwrap();
assert!(!png.is_empty(), "高容错 PNG 不应为空");
let svg = generator.generate_svg("High EC test").unwrap();
assert!(svg.contains("<svg"), "高容错 SVG 应包含 <svg> 标签");
let matrix = generator.generate_matrix("High EC test").unwrap();
assert!(!matrix.is_empty(), "高容错矩阵不应为空");
assert_eq!(
generator.config().error_correction_level,
ErrorCorrectionLevel::High
);
}
}