diff --git a/README.md b/README.md index e075f7b..f61be27 100644 --- a/README.md +++ b/README.md @@ -108,4 +108,40 @@ fn main() { Generates this SVG: -[![Output](src/test_annex_i_micro_qr_as_svg.svg)](src/test_annex_i_micro_qr_as_svg.svg) \ No newline at end of file +[![Output](src/test_annex_i_micro_qr_as_svg.svg)](src/test_annex_i_micro_qr_as_svg.svg) + +## Unicode string generation + +```rust +use qrcode::QrCode; +use qrcode::render::unicode; + +fn main() { + let code = QrCode::new("mow mow").unwrap(); + let image = code.render::() + .dark_color(unicode::Dense1x2::Light) + .light_color(unicode::Dense1x2::Dark) + .build(); + println!("{}", image); +} +``` + +Generates this output: + +```text +█████████████████████████████ +█████████████████████████████ +████ ▄▄▄▄▄ █ ▀▀▀▄█ ▄▄▄▄▄ ████ +████ █ █ █▀ ▀ ▀█ █ █ ████ +████ █▄▄▄█ ██▄ ▀█ █▄▄▄█ ████ +████▄▄▄▄▄▄▄█ ▀▄▀ █▄▄▄▄▄▄▄████ +████▄▀ ▄▀ ▄ █▄█ ▀ ▀█ █▄ ████ +████▄██▄▄▀▄▄▀█▄ ██▀▀█▀▄▄▄████ +█████▄▄▄█▄▄█ ▀▀▄█▀▀▀▄█▄▄████ +████ ▄▄▄▄▄ █ ▄▄██▄ ▄ ▀▀████ +████ █ █ █▀▄▄▀▄▄ ▄▄▄▄ ▄████ +████ █▄▄▄█ █▄ █▄▀▄▀██▄█▀████ +████▄▄▄▄▄▄▄█▄████▄█▄██▄██████ +█████████████████████████████ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +``` diff --git a/src/render/mod.rs b/src/render/mod.rs index ab5e71c..32fff6e 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -7,6 +7,7 @@ use std::cmp::max; pub mod image; pub mod string; pub mod svg; +pub mod unicode; //------------------------------------------------------------------------------ //{{{ Pixel trait diff --git a/src/render/unicode.rs b/src/render/unicode.rs new file mode 100644 index 0000000..066a75d --- /dev/null +++ b/src/render/unicode.rs @@ -0,0 +1,140 @@ +//! UTF-8 rendering, with 2 pixels per symbol. + +use crate::render::{Canvas as RenderCanvas, Pixel, Color}; + +const CODEPAGE: [&str; 4] = [" ","\u{2584}","\u{2580}","\u{2588}"]; + +#[derive(Copy, Clone, PartialEq)] +pub enum Dense1x2 { + Dark, Light +} + +impl Pixel for Dense1x2 { + type Image = String; + type Canvas = Canvas1x2; + fn default_color(color: Color) -> Dense1x2 { color.select(Dense1x2::Dark, Dense1x2::Light) } + fn default_unit_size() -> (u32, u32) { (1, 1) } +} + +impl Dense1x2 { + fn value(&self) -> u8 { + match self { + Dense1x2::Dark => {1} + Dense1x2::Light => {0} + } + } + fn parse_2_bits(sym: &u8) -> &'static str { + CODEPAGE[*sym as usize] + } +} + +pub struct Canvas1x2 { + canvas: Vec, + width: u32, + dark_pixel: u8 +} + +impl RenderCanvas for Canvas1x2 { + + type Pixel = Dense1x2; + type Image = String; + + + fn new(width: u32, height: u32, dark_pixel: Dense1x2, light_pixel: Dense1x2) -> Self { + let a = vec![light_pixel.value(); (width * height) as usize]; + Canvas1x2 { + width: width, + canvas: a, + dark_pixel: dark_pixel.value() + } + } + + fn draw_dark_pixel(&mut self, x: u32, y: u32) { + self.canvas[(x + y * self.width) as usize] = self.dark_pixel; + } + + fn into_image(self) -> String { + self.canvas + // Chopping array into 1-line sized fragments + .chunks_exact(self.width as usize) + .collect::>() + // And then glueing every 2 lines. + .chunks(2) + .map(|rows| + { + // Then zipping those 2 lines together into a single 2-bit number list. + if rows.len() == 2 { + rows[0].iter().zip(rows[1]).map(|(top,bot)| (top * 2 + bot)).collect::>() + } else { + rows[0].iter().map(|top| (top * 2)).collect::>() + } + } + .iter() + // Mapping those 2-bit numbers to corresponding pixels. + .map(Dense1x2::parse_2_bits) + .collect::>() + .concat() + ) + .collect::>() + .join("\n") + } +} + +#[test] +fn test_render_to_utf8_string() { + use crate::render::Renderer; + let colors = &[Color::Dark, Color::Light, Color::Light, Color::Dark]; + let image: String = Renderer::::new(colors, 2, 1).build(); + + assert_eq!(&image, " ▄ \n ▀ "); + + let image2 = Renderer::::new(colors, 2, 1).module_dimensions(2, 2).build(); + + assert_eq!(&image2, " \n ██ \n ██ \n "); +} + +#[test] +fn integration_render_utf8_1x2() { + use crate::{QrCode, Version, EcLevel}; + use crate::render::unicode::Dense1x2; + + let code = QrCode::with_version(b"09876542", Version::Micro(2), EcLevel::L).unwrap(); + let image = code.render::() + .module_dimensions(1, 1) + .build(); + assert_eq!(image, + " \n".to_owned() + + " █▀▀▀▀▀█ ▀ █ ▀ \n" + + " █ ███ █ ▀ █ \n" + + " █ ▀▀▀ █ ▀█ █ \n" + + " ▀▀▀▀▀▀▀ ▄▀▀ █ \n" + + " ▀█ ▀▀▀▀▀██▀▀▄ \n" + + " ▀███▄ ▀▀ █ ██ \n" + + " ▀▀▀ ▀ ▀▀ ▀ ▀ \n" + + " ") + +} + +#[test] +fn integration_render_utf8_1x2_inverted() { + use crate::{QrCode, Version, EcLevel}; + use crate::render::unicode::Dense1x2; + + let code = QrCode::with_version(b"12345678", Version::Micro(2), EcLevel::L).unwrap(); + let image = code.render::() + .dark_color(Dense1x2::Light) + .light_color(Dense1x2::Dark) + .module_dimensions(1, 1) + .build(); + assert_eq!(image, + "█████████████████\n\ + ██ ▄▄▄▄▄ █▄▀▄█▄██\n\ + ██ █ █ █ █ ██\n\ + ██ █▄▄▄█ █▄▄██▀██\n\ + ██▄▄▄▄▄▄▄█▄▄▄▀ ██\n\ + ██▄ ▀ ▀ ▀▄▄ ████\n\ + ██▄▄▀▄█ ▀▀▀ ▀▄▄██\n\ + ██▄▄▄█▄▄█▄██▄█▄██\n\ + ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀"); + +} \ No newline at end of file