feat: 顶层 API + PNG/SVG/ASCII 渲染器
This commit is contained in:
+197
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user