diff --git a/core/src/qr.rs b/core/src/qr.rs index 2c7895e..26ea9c6 100644 --- a/core/src/qr.rs +++ b/core/src/qr.rs @@ -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 { + // 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::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] { + &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 { + 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); + } + } +} diff --git a/core/src/render/ascii.rs b/core/src/render/ascii.rs index 9b94bfd..9d2f2f4 100644 --- a/core/src/render/ascii.rs +++ b/core/src/render/ascii.rs @@ -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 +} diff --git a/core/src/render/png.rs b/core/src/render/png.rs index 0a28a65..65c1ae7 100644 --- a/core/src/render/png.rs +++ b/core/src/render/png.rs @@ -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 { + 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 +} diff --git a/core/src/render/svg.rs b/core/src/render/svg.rs index adb3da8..49b9377 100644 --- a/core/src/render/svg.rs +++ b/core/src/render/svg.rs @@ -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#""#, + total, total, total, total + )); + svg.push_str(&format!( + r#""#, + 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#""#, + x + margin, + y + margin + )); + } + } + } + + svg.push_str(""); + svg +}