feat: 顶层 API + PNG/SVG/ASCII 渲染器

This commit is contained in:
2026-06-16 23:51:55 +08:00
parent 778b0ee1fa
commit 41ef43f038
4 changed files with 299 additions and 4 deletions
+197 -1
View File
@@ -1 +1,197 @@
// FIXME: 顶层 QR 码 API — Task 10 use crate::version::{Version, EcLevel, get_data_capacity};
use crate::encoder::bitstream::build_codewords;
use crate::encoder::segment::{segment_text, segment_bit_length};
use crate::ecc::reed_solomon;
use crate::matrix::grid::Matrix;
use crate::matrix::patterns::{
place_finder_patterns, place_timing_patterns, place_alignment_patterns,
reserve_format_areas, reserve_version_areas,
encode_format_info, encode_version_info, place_format_info, place_version_info,
};
use crate::matrix::placement::place_data;
use crate::matrix::mask::best_mask;
/// 版本选择模式
#[derive(Debug, Clone)]
pub enum VersionMode {
Auto,
Fixed(u8),
}
/// QR 码配置
#[derive(Debug, Clone)]
pub struct QrConfig {
pub level: EcLevel,
pub version: VersionMode,
pub margin: u8,
}
impl Default for QrConfig {
fn default() -> Self {
QrConfig {
level: EcLevel::M,
version: VersionMode::Auto,
margin: 4,
}
}
}
/// 生成的 QR 码
pub struct QrCode {
pub version: Version,
pub level: EcLevel,
pub mask: u8,
matrix: Matrix,
pub margin: u8,
}
impl QrCode {
/// 编码字符串生成 QR 码
pub fn encode(text: &str, config: QrConfig) -> Result<Self, String> {
// 1. 分段
let segments = segment_text(text);
if segments.is_empty() {
return Err("输入为空".into());
}
// 2. 确定版本
let version = match config.version {
VersionMode::Fixed(v) => Version::new(v).ok_or("无效版本号 (1-40)")?,
VersionMode::Auto => {
let mut selected = None;
for v in 1..=40 {
let ver = Version(v);
let total_bits: u16 = segments
.iter()
.map(|s| segment_bit_length(s, v))
.sum();
let cap = get_data_capacity(ver, config.level) as u32 * 8;
if cap >= total_bits as u32 {
selected = Some(ver);
break;
}
}
selected.ok_or("数据过长,超出 QR 码最大容量".to_string())?
}
};
// 3. 构建数据码字
let data = build_codewords(text, version, config.level);
// 4. 纠错编码 — 分组并计算 RS 纠错码
let ec_info = version.ec_info(config.level);
let mut blocks: Vec<Vec<u8>> = Vec::new();
let mut pos = 0usize;
for binfo in &ec_info.blocks {
for _ in 0..binfo.count {
let end = pos + binfo.data_codewords as usize;
if end > data.len() {
return Err("内部错误: 数据码字不足".into());
}
blocks.push(data[pos..end].to_vec());
pos = end;
}
}
let final_codewords = reed_solomon::interleave(&blocks, ec_info.ec_per_block);
// 5. 构建矩阵 + 放置功能图案
let mut matrix = Matrix::new(version.size());
place_finder_patterns(&mut matrix);
place_timing_patterns(&mut matrix);
place_alignment_patterns(&mut matrix, version.alignment_positions());
reserve_format_areas(&mut matrix);
if version.0 >= 7 {
reserve_version_areas(&mut matrix);
}
// 6. 蛇形放置数据
place_data(&mut matrix, &final_codewords);
// 7. 掩码评分 → 选最佳
let (best_idx, best_matrix) = best_mask(&matrix);
// 8. 写入格式信息
let format = encode_format_info(config.level.indicator_bits(), best_idx);
let mut final_matrix = best_matrix;
place_format_info(&mut final_matrix, format);
// 9. 写入版本信息(版本 ≥ 7)
if version.0 >= 7 {
let ver_info = encode_version_info(version.0);
place_version_info(&mut final_matrix, ver_info);
}
Ok(QrCode {
version,
level: config.level,
mask: best_idx,
matrix: final_matrix,
margin: config.margin,
})
}
pub fn modules(&self) -> &[Vec<bool>] {
&self.matrix.modules
}
pub fn size(&self) -> u8 {
self.matrix.size
}
pub fn to_svg(&self) -> String {
crate::render::svg::render_svg(self)
}
pub fn to_ascii(&self, invert: bool) -> String {
crate::render::ascii::render_ascii(self, invert)
}
pub fn to_png_bytes(&self, module_size: u8) -> Vec<u8> {
crate::render::png::render_png(self, module_size)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encode_simple() {
let qr = QrCode::encode("HELLO", QrConfig::default()).unwrap();
assert_eq!(qr.version.0, 1);
assert_eq!(qr.size(), 21);
}
#[test]
fn test_encode_numeric() {
let qr = QrCode::encode("1234567890", QrConfig::default()).unwrap();
assert_eq!(qr.size(), 21);
}
#[test]
fn test_encode_empty_fails() {
assert!(QrCode::encode("", QrConfig::default()).is_err());
}
#[test]
fn test_fixed_version() {
let config = QrConfig {
version: VersionMode::Fixed(3),
..Default::default()
};
let qr = QrCode::encode("FIXED VERSION", config).unwrap();
assert_eq!(qr.version.0, 3);
}
#[test]
fn test_all_levels() {
for level in [EcLevel::L, EcLevel::M, EcLevel::Q, EcLevel::H] {
let config = QrConfig {
level,
..Default::default()
};
let qr = QrCode::encode("TEST", config).unwrap();
assert!(qr.size() >= 21);
}
}
}
+27 -1
View File
@@ -1 +1,27 @@
// FIXME: ASCII 渲染器 — Task 10 use crate::qr::QrCode;
/// 终端 ASCII 渲染:██ 表示暗模块," " 表示亮模块
pub fn render_ascii(qr: &QrCode, invert: bool) -> String {
let size = qr.size() as usize;
let margin = qr.margin as usize;
let total = size + 2 * margin;
let dark_char = if invert { " " } else { "██" };
let light_char = if invert { "██" } else { " " };
let mut result = String::new();
for y in 0..total {
for x in 0..total {
let mx = x.saturating_sub(margin);
let my = y.saturating_sub(margin);
let is_dark = if mx < size && my < size {
qr.modules()[my][mx]
} else {
false
};
result.push_str(if is_dark { dark_char } else { light_char });
}
result.push('\n');
}
result
}
+43 -1
View File
@@ -1 +1,43 @@
// FIXME: PNG 渲染器 — Task 10 use crate::qr::QrCode;
use image::{ImageBuffer, Luma};
pub fn render_png(qr: &QrCode, module_size: u8) -> Vec<u8> {
let matrix_size = qr.size() as u32;
let margin = qr.margin as u32;
let total_size = matrix_size + 2 * margin;
let img_size = total_size * module_size as u32;
let mut img = ImageBuffer::new(img_size, img_size);
for y in 0..total_size {
for x in 0..total_size {
let module_x = x.saturating_sub(margin);
let module_y = y.saturating_sub(margin);
let is_dark = if module_x < matrix_size && module_y < matrix_size {
qr.modules()[module_y as usize][module_x as usize]
} else {
false // 白边
};
let px_val = if is_dark { 0u8 } else { 255u8 };
for dy in 0..module_size as u32 {
for dx in 0..module_size as u32 {
img.put_pixel(
x * module_size as u32 + dx,
y * module_size as u32 + dy,
Luma([px_val]),
);
}
}
}
}
let mut buf = Vec::new();
img.write_to(
&mut std::io::Cursor::new(&mut buf),
image::ImageFormat::Png,
)
.expect("PNG 编码失败");
buf
}
+32 -1
View File
@@ -1 +1,32 @@
// FIXME: SVG 渲染器 — Task 10 use crate::qr::QrCode;
pub fn render_svg(qr: &QrCode) -> String {
let matrix_size = qr.size() as u32;
let margin = qr.margin as u32;
let total = matrix_size + 2 * margin;
let mut svg = String::new();
svg.push_str(&format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" width="{}" height="{}" viewBox="0 0 {} {}">"#,
total, total, total, total
));
svg.push_str(&format!(
r#"<rect width="{}" height="{}" fill="white"/>"#,
total, total
));
for y in 0..matrix_size {
for x in 0..matrix_size {
if qr.modules()[y as usize][x as usize] {
svg.push_str(&format!(
r#"<rect x="{}" y="{}" width="1" height="1" fill="black"/>"#,
x + margin,
y + margin
));
}
}
}
svg.push_str("</svg>");
svg
}