diff --git a/examples/custom.rs b/examples/custom.rs index ebd5f03..47581bd 100644 --- a/examples/custom.rs +++ b/examples/custom.rs @@ -1,6 +1,6 @@ fn main() { use fast_qr::{ - convert::{image::ImageBuilder, Builder, Shape}, + convert::{image::ImageBuilder, Builder, ModuleShape}, ModuleType, QRBuilder, Version, ECL, }; @@ -12,21 +12,21 @@ fn main() { let mut _img = ImageBuilder::default() // Can have many shapes and custom shapes - .shape(Shape::Command(|y, x, cell| { + .module_shape(ModuleShape::Command(|y, x, cell| { match cell.module_type() { ModuleType::FinderPattern | ModuleType::Alignment => String::new(), _ => { // Works thanks to Deref - Shape::Square(y, x, cell) + ModuleShape::Square(y, x, cell) } } })) - .shape_color( - Shape::Command(|y, x, cell| { + .module_shape_color( + ModuleShape::Command(|y, x, cell| { match cell.module_type() { ModuleType::FinderPattern | ModuleType::Alignment => { // Works thanks to Deref - Shape::Circle(y, x, cell) + ModuleShape::Circle(y, x, cell) } _ => String::new(), } diff --git a/examples/embed.rs b/examples/embed.rs index cb1fb35..20e3dad 100644 --- a/examples/embed.rs +++ b/examples/embed.rs @@ -1,6 +1,6 @@ fn main() { use fast_qr::{ - convert::{image::ImageBuilder, Builder, ImageBackgroundShape, Shape}, + convert::{image::ImageBuilder, Builder, ImageBackgroundShape, ModuleShape}, QRBuilder, Version, ECL, }; @@ -11,7 +11,7 @@ fn main() { .unwrap(); let mut _img = ImageBuilder::default() - .shape(Shape::Square) + .module_shape(ModuleShape::Square) .fit_width(600) .background_color([255, 255, 255, 255]) // New: embed an image diff --git a/examples/image.rs b/examples/image.rs index d81b9e2..742c63e 100644 --- a/examples/image.rs +++ b/examples/image.rs @@ -1,6 +1,6 @@ fn main() { use fast_qr::{ - convert::{image::ImageBuilder, Builder, Shape}, + convert::{image::ImageBuilder, Builder, ModuleShape}, QRBuilder, Version, ECL, }; @@ -11,14 +11,14 @@ fn main() { .unwrap(); let _image = ImageBuilder::default() - .shape(Shape::RoundedSquare) + .module_shape(ModuleShape::RoundedSquare) .fit_width(600) .background_color([255, 255, 255, 0]) // transparency .to_file(&qrcode, "image.png"); // Or maybe as bytes. let _image_as_bytes = ImageBuilder::default() - .shape(Shape::RoundedSquare) + .module_shape(ModuleShape::RoundedSquare) .fit_width(512) .background_color([255, 255, 255, 255]) // opaque .to_bytes(&qrcode); diff --git a/examples/svg.rs b/examples/svg.rs index c8ca145..ddf772e 100644 --- a/examples/svg.rs +++ b/examples/svg.rs @@ -1,6 +1,6 @@ fn main() { use fast_qr::{ - convert::{svg::SvgBuilder, Builder, Shape}, + convert::{svg::SvgBuilder, Builder, EyeFrameShape, ModuleShape}, QRBuilder, Version, ECL, }; @@ -11,6 +11,7 @@ fn main() { .unwrap(); let _svg = SvgBuilder::default() - .shape(Shape::RoundedSquare) + .module_shape(ModuleShape::RoundedSquare) + .eye_frame_shape(EyeFrameShape::Square) .to_file(&qrcode, "svg.svg"); } diff --git a/src/convert/color.rs b/src/convert/color.rs new file mode 100644 index 0000000..7c4bc9c --- /dev/null +++ b/src/convert/color.rs @@ -0,0 +1,76 @@ +//! Contains functions to convert colors from one format to another + +/// Converts an array of pixel color to it's hexadecimal representation +/// # Example +/// ```rust +/// # use fast_qr::convert::rgba2hex; +/// let color = [0, 0, 0, 255]; +/// assert_eq!(&rgba2hex(color), "#000000"); +/// ``` +#[must_use] +pub fn rgba2hex(color: [u8; 4]) -> String { + let mut hex = String::with_capacity(9); + + hex.push('#'); + hex.push_str(&format!("{:02x}", color[0])); + hex.push_str(&format!("{:02x}", color[1])); + hex.push_str(&format!("{:02x}", color[2])); + if color[3] != 255 { + hex.push_str(&format!("{:02x}", color[3])); + } + + hex +} + +/// Allows to take String, string slices, arrays or slices of u8 (3 or 4) to create a [Color] +pub struct Color(pub String); + +impl Color { + /// Returns the contained color + #[must_use] + pub fn to_str(&self) -> &str { + &self.0 + } +} + +impl From for Color { + fn from(color: String) -> Self { + Self(color) + } +} + +impl From<&str> for Color { + fn from(color: &str) -> Self { + Self(color.to_string()) + } +} + +impl From<[u8; 4]> for Color { + fn from(color: [u8; 4]) -> Self { + Self(rgba2hex(color)) + } +} + +impl From<[u8; 3]> for Color { + fn from(color: [u8; 3]) -> Self { + Self::from([color[0], color[1], color[2], 255]) + } +} + +impl From<&[u8]> for Color { + fn from(color: &[u8]) -> Self { + if color.len() == 3 { + Self::from([color[0], color[1], color[2]]) + } else if color.len() == 4 { + Self::from([color[0], color[1], color[2], color[3]]) + } else { + panic!("Invalid color length"); + } + } +} + +impl From> for Color { + fn from(color: Vec) -> Self { + Self::from(&color[..]) + } +} diff --git a/src/convert/eye_shape.rs b/src/convert/eye_shape.rs new file mode 100644 index 0000000..c951c1b --- /dev/null +++ b/src/convert/eye_shape.rs @@ -0,0 +1,363 @@ +/// Corresponds to the position of the Eye +/// - TopLeft +/// - TopRight +/// - BottomRight +#[derive(Debug, Clone, Copy)] +pub enum EyePosition { + /// Top left eye + TopLeft, + /// Top right eye + TopRight, + /// Bottom left eye + BottomLeft, +} + +impl EyePosition { + /// Iterates over all possible eye positions + /// + /// TopLeft, TopRight, BottomLeft + pub const ALL: [Self; 3] = [Self::TopLeft, Self::TopRight, Self::BottomLeft]; +} + +/// Converts an eye position to a custom svg +/// +/// # Example +/// +/// For the fully squared shape, the svg is `M{x},{y}h7v7h-7` +/// +/// The eye function for the eye frame should be max of 7x7. \ +/// The eye function for the eye ball should be max of 3x3. +/// +/// ```rust +/// fn square(y: usize, x: usize, _: EyePosition) -> String { +/// format!("h7v7h-7") +/// } +/// ``` +pub type EyeFunction = fn(usize, usize, EyePosition) -> String; + +// TODO: Find a way to use the same enum for wasm and not wasm +// Current bug being that wasm_bindgen & #[cfg(not(target_arch = "wasm32"))] are not compatible(?) +/// Different possible Shapes to represent modules in a [`crate::QRCode`] +#[repr(C)] +#[wasm_bindgen] +#[cfg(feature = "wasm-bindgen")] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)] +pub enum EyeFrameShape { + /// Square shape + Square, + /// Rounded square shape + Rounded, + /// Circle shape + Circle, + /// Rounded square shape with the outer corner rounded + RoundedSquaredOuterCorner, + /// Leaf shape + Leaf, + /// Rounded square shape with all but the inner corner rounded + RoundedSquaredInnerCorner, + /// Square shape with a dot in the middle + DottedSquare, + /// Eye lash shape + EyeLash, +} + +/// Different possible Shapes to represent modules in a [`crate::QRCode`] +#[cfg(not(feature = "wasm-bindgen"))] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)] +pub enum EyeFrameShape { + /// Empty shape, most often used with [`ModuleShape::Custom`] + Empty, + /// Square shape + Square, + /// Rounded square shape + Rounded, + /// Circle shape + Circle, + /// Rounded square shape with the outer corner rounded + RoundedSquaredOuterCorner, + /// Leaf shape + Leaf, + /// Rounded square shape with all but the inner corner rounded + RoundedSquaredInnerCorner, + /// Eye lash shape + EyeLash, + /// Custom Shape with a function / closure + /// # Example + /// ```rust + /// use fast_qr::convert::EyeFrameShape; + /// let command_function = |eye_position| { + /// match eye_position { + /// EyePosition::TopLeft => String::from("..."), + /// _ => String::from("..."), + /// } + /// }; + /// let command = EyeFrameShape::Command(command_function); + /// ``` + Command(EyeFunction), +} + +impl From for usize { + fn from(shape: EyeFrameShape) -> Self { + match shape { + EyeFrameShape::Empty => 0, + EyeFrameShape::Square => 1, + EyeFrameShape::Rounded => 2, + EyeFrameShape::Circle => 3, + EyeFrameShape::RoundedSquaredOuterCorner => 4, + EyeFrameShape::Leaf => 5, + EyeFrameShape::RoundedSquaredInnerCorner => 6, + EyeFrameShape::EyeLash => 7, + #[cfg(not(feature = "wasm-bindgen"))] + EyeFrameShape::Command(_) => 8, + } + } +} + +impl From for EyeFrameShape { + #[allow(clippy::match_same_arms)] + fn from(shape: String) -> Self { + match shape.as_ref() { + "empty" => Self::Empty, + "square" => Self::Square, + "rounded" => Self::Rounded, + "circle" => Self::Circle, + "rounded_squared_outer_corner" => Self::RoundedSquaredOuterCorner, + "leaf" => Self::Leaf, + "rounded_squared_side_3" => Self::RoundedSquaredInnerCorner, + "eye_lash" => Self::EyeLash, + + _ => Self::Square, + } + } +} + +impl From for &str { + fn from(shape: EyeFrameShape) -> Self { + match shape { + EyeFrameShape::Empty => "empty", + EyeFrameShape::Square => "square", + EyeFrameShape::Rounded => "rounded", + EyeFrameShape::Circle => "circle", + EyeFrameShape::RoundedSquaredOuterCorner => "rounded_squared_outer_corner", + EyeFrameShape::Leaf => "leaf", + EyeFrameShape::RoundedSquaredInnerCorner => "rounded_squared_side_3", + EyeFrameShape::EyeLash => "eye_lash", + + #[cfg(not(feature = "wasm-bindgen"))] + EyeFrameShape::Command(_) => "command", + } + } +} + +impl EyeFrameShape { + fn inner_black_rect(y: usize, x: usize) -> String { + let x_rect_offset = x + 2; + let y_rect_offset = y + 2; + + let black_rect_path = format!("M{x_rect_offset},{y_rect_offset}h3v3h-3z"); + + format!(r#""#) + } + + pub(crate) const fn empty(_: usize, _: usize, _: EyePosition) -> String { + String::new() + } + + pub(crate) fn square(y: usize, x: usize, _: EyePosition) -> String { + let offset_x = x + 6; + let offset_y = y + 6; + + let x_1 = x + 1; + let y_1 = y + 1; + + let x_2 = x + 2; + let y_2 = y + 2; + + let d_path = format!("M{x_1},{y}h6v1h-6zM{x},{y}v7h1v-7zM{offset_x},{y_1}v6h1v-6zM{x_1},{offset_y}h5v1h-5zM{x_2},{y_2}h3v3h-3z"); + format!(r#""#) + } + + pub(crate) fn rounded(y: usize, x: usize, _: EyePosition) -> String { + const TRANSPARENT: &str = "#0000"; + + let stroked_rect = format!( + r#""# + ); + let black_rect_path = EyeFrameShape::inner_black_rect(y, x); + + format!(r"{stroked_rect}{black_rect_path}") + } + + pub(crate) fn circle(y: usize, x: usize, _: EyePosition) -> String { + let x_circle_center = x + 3; + let y_circle_center = y + 3; + + let stroked_circle = format!( + r#""# + ); + let black_rect_path = EyeFrameShape::inner_black_rect(y, x); + + format!(r"{stroked_circle}{black_rect_path}") + } + + pub(crate) fn rounded_squared_outer_corner( + y: usize, + x: usize, + eye_position: EyePosition, + ) -> String { + const WHITE: &str = "#fff"; + + let x_inner = x + 1; + let y_inner = y + 1; + + let outer_shape_path = match eye_position { + EyePosition::TopLeft => format!(r#"M{x},{}v-5q0,-2 2,-2h5v7z"#, y + 7), + EyePosition::TopRight => format!(r#"M{x},{y}h5q2,0 2,2v5h-7z"#), + EyePosition::BottomLeft => format!(r#"M{x},{y}v5q0,2 2,2h5v-7z"#), + }; + + let inner_shape_path = match eye_position { + EyePosition::TopLeft => format!(r#"M{x_inner},{}v-4q0,-1 1,-1h4v5z"#, y + 6), + EyePosition::TopRight => format!(r#"M{x_inner},{y_inner}h4q1,0 1,1v4h-5z"#), + EyePosition::BottomLeft => format!(r#"M{x_inner},{y_inner}v4q0,1 1,1h4v-5z"#), + }; + + let black_rect_path = EyeFrameShape::inner_black_rect(y, x); + + format!( + r#"{black_rect_path}"#, + ) + } + + pub(crate) fn leaf(y: usize, x: usize, eye_position: EyePosition) -> String { + const WHITE: &str = "#fff"; + + let x_inner = x + 1; + let y_inner = y + 1; + + let outer_shape_path = match eye_position { + EyePosition::TopLeft => format!(r#"M{x},{}v-5q0,-2 2,-2h5v5q0,2 -2,2z"#, y + 7), + EyePosition::TopRight | EyePosition::BottomLeft => { + format!(r#"M{x},{y}h5q2,0 2,2v5h-5q-2,0 -2,-2z"#) + } + }; + + let inner_shape_path = match eye_position { + EyePosition::TopLeft => format!(r#"M{x_inner},{}v-4q0,-1 1,-1h4v4q0,1 -1,1z"#, y + 6), + EyePosition::TopRight | EyePosition::BottomLeft => { + format!(r#"M{x_inner},{y_inner}h4q1,0 1,1v4h-4q-1,0 -1,-1z"#) + } + }; + + let black_rect_path = EyeFrameShape::inner_black_rect(y, x); + + format!( + r#"{black_rect_path}"#, + ) + } + + pub(crate) fn rounded_squared_inner_corner( + y: usize, + x: usize, + eye_position: EyePosition, + ) -> String { + const WHITE: &str = "#fff"; + + let x_6 = x + 6; + let y_6 = y + 6; + let x_7 = x + 7; + let y_7 = y + 7; + + let x_inner = x + 1; + let y_inner = y + 1; + + let outer_shape_path = match eye_position { + EyePosition::TopLeft => { + format!(r#"M{x_7},{y_7}h-5q-2,0 -2,-2v-3q0,-2 2,-2h3q2,0 2,2"#) + } + EyePosition::TopRight => { + format!(r#"M{x},{y_7}v-5q0,-2 2,-2h3q2,0 2,2v3q0,2 -2,2"#) + } + + EyePosition::BottomLeft => { + format!(r#"M{x_7},{y}v5q0,2 -2,2h-3q-2,0 -2,-2v-3q0,-2 2,-2"#) + } + }; + + let inner_shape_path = match eye_position { + EyePosition::TopLeft => { + format!(r#"M{x_6},{y_6}h-4q-1,0 -1,-1v-3q0,-1 1,-1h3q1,0 1,1"#) + } + EyePosition::TopRight => { + format!(r#"M{x_inner},{y_6}v-4q0,-1 1,-1h3q1,0 1,1v3q0,1 -1,1"#) + } + EyePosition::BottomLeft => { + format!(r#"M{x_6},{y_inner}v4q0,1 -1,1h-3q-1,0 -1,-1v-3q0,-1 1,-1"#) + } + }; + + let black_rect_path = EyeFrameShape::inner_black_rect(y, x); + + format!( + r#"{black_rect_path}"#, + ) + } + + pub(crate) fn eye_lash(y: usize, x: usize, eye_position: EyePosition) -> String { + const WHITE: &str = "#fff"; + + let x_inner = x + 1; + let y_inner = y + 1; + + let x_1 = x - 1; + let y_1 = y - 1; + + let outer_shape_path = match eye_position { + EyePosition::TopLeft => format!(r#"M{x_1}.5,{y_1}.5l5.5 .5q2,0 2,2v5h-5q-2,0 -2,-2z"#), + EyePosition::TopRight => { + format!(r#"M{x},{}v-5q0,-2 2,-2l5.5 -.5l-.5 5.5q0,2 -2,2z"#, y + 7) + } + EyePosition::BottomLeft => { + format!(r#"M{x_1}.5,{}.5l.5 -5.5q0,-2 2,-2h5v5q0,2 -2,2z"#, y + 7) + } + }; + + let inner_shape_path = match eye_position { + EyePosition::TopLeft => format!(r#"M{x_inner},{y_inner}h4q1,0 1,1v4h-4q-1,0 -1,-1z"#), + EyePosition::TopRight => format!(r#"M{x_inner},{}v-4q0,-1 1,-1h4v4q0,1 -1,1z"#, y + 6), + EyePosition::BottomLeft => { + format!(r#"M{x_inner},{}v-4q0,-1 1,-1h4v4q0,1 -1,1z"#, y + 6) + } + }; + + let black_rect_path = EyeFrameShape::inner_black_rect(y, x); + + format!( + r#"{black_rect_path}"#, + ) + } + + const FUNCTIONS: [EyeFunction; 8] = [ + EyeFrameShape::empty, + EyeFrameShape::square, + EyeFrameShape::rounded, + EyeFrameShape::circle, + EyeFrameShape::rounded_squared_outer_corner, + EyeFrameShape::leaf, + EyeFrameShape::rounded_squared_inner_corner, + EyeFrameShape::eye_lash, + ]; +} + +impl core::ops::Deref for EyeFrameShape { + type Target = EyeFunction; + + fn deref(&self) -> &Self::Target { + let index: usize = (*self).into(); + match self { + #[cfg(not(target_arch = "wasm32"))] + Self::Command(func) => func, + _ => &Self::FUNCTIONS[index], + } + } +} diff --git a/src/convert/image.rs b/src/convert/image.rs index 66ec259..5403884 100644 --- a/src/convert/image.rs +++ b/src/convert/image.rs @@ -27,7 +27,7 @@ use std::io; use crate::QRCode; use super::Color; -use super::{svg::SvgBuilder, Builder, Shape}; +use super::{svg::SvgBuilder, Builder, ModuleShape}; use resvg::tiny_skia::{self, Pixmap}; use resvg::usvg; @@ -79,8 +79,8 @@ impl Builder for ImageBuilder { self } - fn shape(&mut self, shape: Shape) -> &mut Self { - self.svg_builder.shape(shape); + fn module_shape(&mut self, shape: ModuleShape) -> &mut Self { + self.svg_builder.module_shape(shape); self } @@ -114,8 +114,22 @@ impl Builder for ImageBuilder { self } - fn shape_color>(&mut self, shape: Shape, color: C) -> &mut Self { - self.svg_builder.shape_color(shape, color); + fn module_shape_color>(&mut self, shape: ModuleShape, color: C) -> &mut Self { + self.svg_builder.module_shape_color(shape, color); + self + } + + fn eye_frame_shape(&mut self, shape: super::EyeFrameShape) -> &mut Self { + self.svg_builder.eye_frame_shape(shape); + self + } + + fn eye_frame_shape_color>( + &mut self, + shape: super::EyeFrameShape, + color: C, + ) -> &mut Self { + self.svg_builder.eye_frame_shape_color(shape, color); self } } diff --git a/src/convert/mod.rs b/src/convert/mod.rs index 0e608e0..96b74e9 100644 --- a/src/convert/mod.rs +++ b/src/convert/mod.rs @@ -3,7 +3,6 @@ #[cfg(feature = "svg")] #[cfg_attr(docsrs, doc(cfg(feature = "svg")))] pub mod svg; -use core::ops::Deref; #[cfg(feature = "svg")] use svg::SvgError; @@ -14,178 +13,18 @@ pub mod image; #[cfg(feature = "image")] use image::ImageError; -use crate::Module; +pub mod color; +pub use color::{rgba2hex, Color}; -/// Converts a position to a module svg -/// # Example -/// -/// For the square shape, the svg is `M{x},{y}h1v1h-1` -/// -/// ```rust -/// fn square(y: usize, x: usize) -> String { -/// format!("M{x},{y}h1v1h-1") -/// } -/// ``` -pub type ModuleFunction = fn(usize, usize, Module) -> String; +mod module_shape; +pub use module_shape::{ModuleFunction, ModuleShape}; + +mod eye_shape; +pub use eye_shape::{EyeFrameShape, EyeFunction, EyePosition}; #[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] use wasm_bindgen::prelude::*; -/// Different possible Shapes to represent modules in a [`crate::QRCode`] -#[repr(C)] -#[wasm_bindgen] -#[cfg(feature = "wasm-bindgen")] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)] -pub enum Shape { - /// Square Shape - Square, - /// Circle Shape - Circle, - /// RoundedSquare Shape - RoundedSquare, - /// Vertical Shape - Vertical, - /// Horizontal Shape - Horizontal, - /// Diamond Shape - Diamond, -} - -/// Different possible Shapes to represent modules in a [`crate::QRCode`] -#[cfg(not(feature = "wasm-bindgen"))] -#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)] -pub enum Shape { - /// Square Shape - Square, - /// Circle Shape - Circle, - /// RoundedSquare Shape - RoundedSquare, - /// Vertical Shape - Vertical, - /// Horizontal Shape - Horizontal, - /// Diamond Shape - Diamond, - /// Custom Shape with a function / closure - /// # Example - /// ```rust - /// use fast_qr::convert::Shape; - /// let command_function = |y, x, cell| { - /// if x % 2 == 0 { - /// // Works thanks to Deref - /// Shape::Square(y, x, cell) - /// } else { - /// // Rectangle - /// format!("M{x},{y}h1v.5h-1") - /// } - /// }; - /// let command = Shape::Command(command_function); - /// ``` - /// - /// - /// - /// - /// - Command(ModuleFunction), -} - -impl From for usize { - fn from(shape: Shape) -> Self { - match shape { - Shape::Square => 0, - Shape::Circle => 1, - Shape::RoundedSquare => 2, - Shape::Vertical => 3, - Shape::Horizontal => 4, - Shape::Diamond => 5, - #[cfg(not(feature = "wasm-bindgen"))] - Shape::Command(_) => 6, - } - } -} - -impl From for Shape { - #[allow(clippy::match_same_arms)] - fn from(shape: String) -> Self { - match shape.to_lowercase().as_str() { - "square" => Shape::Square, - "circle" => Shape::Circle, - "rounded_square" => Shape::RoundedSquare, - "vertical" => Shape::Vertical, - "horizontal" => Shape::Horizontal, - "diamond" => Shape::Diamond, - - _ => Shape::Square, - } - } -} - -impl From for &str { - fn from(shape: Shape) -> Self { - match shape { - Shape::Square => "square", - Shape::Circle => "circle", - Shape::RoundedSquare => "rounded_square", - Shape::Vertical => "vertical", - Shape::Horizontal => "horizontal", - Shape::Diamond => "diamond", - #[cfg(not(feature = "wasm-bindgen"))] - Shape::Command(_) => "command", - } - } -} - -impl Shape { - pub(crate) fn square(y: usize, x: usize, _: Module) -> String { - format!("M{x},{y}h1v1h-1") - } - - pub(crate) fn circle(y: usize, x: usize, _: Module) -> String { - format!("M{},{y}.5a.5,.5 0 1,1 0,-.1", x + 1) - } - - pub(crate) fn rounded_square(y: usize, x: usize, _: Module) -> String { - format!("M{x}.2,{y}.2 {x}.8,{y}.2 {x}.8,{y}.8 {x}.2,{y}.8z") - } - - pub(crate) fn horizontal(y: usize, x: usize, _: Module) -> String { - format!("M{x},{y}.1h1v.8h-1") - } - - pub(crate) fn vertical(y: usize, x: usize, _: Module) -> String { - format!("M{x}.1,{y}h.8v1h-.8") - } - - pub(crate) fn diamond(y: usize, x: usize, _: Module) -> String { - format!("M{x}.5,{y}l.5,.5l-.5,.5l-.5,-.5z") - } - - const FUNCTIONS: [ModuleFunction; 6] = [ - Shape::square, - Shape::circle, - Shape::rounded_square, - Shape::vertical, - Shape::horizontal, - Shape::diamond, - ]; -} - -impl Deref for Shape { - type Target = ModuleFunction; - - fn deref(&self) -> &Self::Target { - let index: usize = (*self).into(); - match self { - #[cfg(not(feature = "wasm-bindgen"))] - Self::Command(func) => func, - _ => &Self::FUNCTIONS[index], - } - } -} - /// Different possible image background shapes #[cfg_attr(feature = "wasm-bindgen", repr(C), wasm_bindgen)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)] @@ -237,81 +76,6 @@ impl From for ConvertError { } } -/// Converts an array of pixel color to it's hexadecimal representation -/// # Example -/// ```rust -/// # use fast_qr::convert::rgba2hex; -/// let color = [0, 0, 0, 255]; -/// assert_eq!(&rgba2hex(color), "#000000"); -/// ``` -#[must_use] -pub fn rgba2hex(color: [u8; 4]) -> String { - let mut hex = String::with_capacity(9); - - hex.push('#'); - hex.push_str(&format!("{:02x}", color[0])); - hex.push_str(&format!("{:02x}", color[1])); - hex.push_str(&format!("{:02x}", color[2])); - if color[3] != 255 { - hex.push_str(&format!("{:02x}", color[3])); - } - - hex -} - -/// Allows to take String, string slices, arrays or slices of u8 (3 or 4) to create a [Color] -pub struct Color(pub String); - -impl Color { - /// Returns the contained color - #[must_use] - pub fn to_str(&self) -> &str { - &self.0 - } -} - -impl From for Color { - fn from(color: String) -> Self { - Self(color) - } -} - -impl From<&str> for Color { - fn from(color: &str) -> Self { - Self(color.to_string()) - } -} - -impl From<[u8; 4]> for Color { - fn from(color: [u8; 4]) -> Self { - Self(rgba2hex(color)) - } -} - -impl From<[u8; 3]> for Color { - fn from(color: [u8; 3]) -> Self { - Self::from([color[0], color[1], color[2], 255]) - } -} - -impl From<&[u8]> for Color { - fn from(color: &[u8]) -> Self { - if color.len() == 3 { - Self::from([color[0], color[1], color[2]]) - } else if color.len() == 4 { - Self::from([color[0], color[1], color[2], color[3]]) - } else { - panic!("Invalid color length"); - } - } -} - -impl From> for Color { - fn from(color: Vec) -> Self { - Self::from(&color[..]) - } -} - /// Trait for `SvgBuilder` and `ImageBuilder` pub trait Builder { /// Updates margin (default: 4) @@ -321,9 +85,19 @@ pub trait Builder { /// Updates background color (default: #FFFFFF) fn background_color>(&mut self, background_color: C) -> &mut Self; /// Adds a shape to the shapes list - fn shape(&mut self, shape: Shape) -> &mut Self; + fn module_shape(&mut self, shape: ModuleShape) -> &mut Self; /// Add a shape to the shapes list with a specific color - fn shape_color>(&mut self, shape: Shape, color: C) -> &mut Self; + fn module_shape_color>(&mut self, shape: ModuleShape, color: C) -> &mut Self; + + // Manages the eye part + /// Adds a shape to the eye shapes list + fn eye_frame_shape(&mut self, shape: EyeFrameShape) -> &mut Self; + /// Add a shape to the eye shapes list with a specific color + fn eye_frame_shape_color>( + &mut self, + shape: EyeFrameShape, + color: C, + ) -> &mut Self; // Manages the image part diff --git a/src/convert/module_shape.rs b/src/convert/module_shape.rs new file mode 100644 index 0000000..b0af9f6 --- /dev/null +++ b/src/convert/module_shape.rs @@ -0,0 +1,170 @@ +use crate::Module; + +/// Converts a position to a module svg +/// # Example +/// +/// For the square shape, the svg is `M{x},{y}h1v1h-1` +/// +/// ```rust +/// fn square(y: usize, x: usize, _: Module) -> String { +/// format!("M{x},{y}h1v1h-1") +/// } +/// ``` +pub type ModuleFunction = fn(usize, usize, Module) -> String; + +// TODO: Find a way to use the same enum for wasm and not wasm +// Current bug being that wasm_bindgen & #[cfg(not(target_arch = "wasm32"))] are not compatible(?) +/// Different possible Shapes to represent modules in a [`crate::QRCode`] +/// /// Different possible Shapes to represent modules in a [`crate::QRCode`] +#[repr(C)] +#[wasm_bindgen] +#[cfg(feature = "wasm-bindgen")] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)] +pub enum ModuleShape { + /// Square Shape + Square, + /// Circle Shape + Circle, + /// RoundedSquare Shape + RoundedSquare, + /// Vertical Shape + Vertical, + /// Horizontal Shape + Horizontal, + /// Diamond Shape + Diamond, +} + +/// Different possible Shapes to represent modules in a [`crate::QRCode`] +#[cfg(not(feature = "wasm-bindgen"))] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)] +pub enum ModuleShape { + /// Square Shape + Square, + /// Circle Shape + Circle, + /// RoundedSquare Shape + RoundedSquare, + /// Vertical Shape + Vertical, + /// Horizontal Shape + Horizontal, + /// Diamond Shape + Diamond, + /// Custom Shape with a function / closure + /// # Example + /// ```rust + /// use fast_qr::convert::ModuleShape; + /// let command_function = |y, x, cell| { + /// if x % 2 == 0 { + /// // Works thanks to Deref + /// ModuleShape::Square(y, x, cell) + /// } else { + /// // Rectangle + /// format!("M{x},{y}h1v.5h-1") + /// } + /// }; + /// let command = ModuleShape::Command(command_function); + /// ``` + /// + /// + /// + /// + /// + Command(ModuleFunction), +} +impl From for usize { + fn from(shape: ModuleShape) -> Self { + match shape { + ModuleShape::Square => 0, + ModuleShape::Circle => 1, + ModuleShape::RoundedSquare => 2, + ModuleShape::Vertical => 3, + ModuleShape::Horizontal => 4, + ModuleShape::Diamond => 5, + #[cfg(not(feature = "wasm-bindgen"))] + ModuleShape::Command(_) => 6, + } + } +} + +impl From for ModuleShape { + #[allow(clippy::match_same_arms)] + fn from(shape: String) -> Self { + match shape.to_lowercase().as_str() { + "square" => ModuleShape::Square, + "circle" => ModuleShape::Circle, + "rounded_square" => ModuleShape::RoundedSquare, + "vertical" => ModuleShape::Vertical, + "horizontal" => ModuleShape::Horizontal, + "diamond" => ModuleShape::Diamond, + + _ => ModuleShape::Square, + } + } +} + +impl From for &str { + fn from(shape: ModuleShape) -> Self { + match shape { + ModuleShape::Square => "square", + ModuleShape::Circle => "circle", + ModuleShape::RoundedSquare => "rounded_square", + ModuleShape::Vertical => "vertical", + ModuleShape::Horizontal => "horizontal", + ModuleShape::Diamond => "diamond", + #[cfg(not(feature = "wasm-bindgen"))] + ModuleShape::Command(_) => "command", + } + } +} + +impl ModuleShape { + pub(crate) fn square(y: usize, x: usize, _: Module) -> String { + format!("M{x},{y}h1v1h-1") + } + + pub(crate) fn circle(y: usize, x: usize, _: Module) -> String { + format!("M{},{y}.5a.5,.5 0 1,1 0,-.1", x + 1) + } + + pub(crate) fn rounded_square(y: usize, x: usize, _: Module) -> String { + format!("M{x}.2,{y}.2 {x}.8,{y}.2 {x}.8,{y}.8 {x}.2,{y}.8z") + } + + pub(crate) fn horizontal(y: usize, x: usize, _: Module) -> String { + format!("M{x},{y}.1h1v.8h-1") + } + + pub(crate) fn vertical(y: usize, x: usize, _: Module) -> String { + format!("M{x}.1,{y}h.8v1h-.8") + } + + pub(crate) fn diamond(y: usize, x: usize, _: Module) -> String { + format!("M{x}.5,{y}l.5,.5l-.5,.5l-.5,-.5z") + } + + const FUNCTIONS: [ModuleFunction; 6] = [ + ModuleShape::square, + ModuleShape::circle, + ModuleShape::rounded_square, + ModuleShape::vertical, + ModuleShape::horizontal, + ModuleShape::diamond, + ]; +} + +impl core::ops::Deref for ModuleShape { + type Target = ModuleFunction; + + fn deref(&self) -> &Self::Target { + let index: usize = (*self).into(); + match self { + #[cfg(not(feature = "wasm-bindgen"))] + Self::Command(func) => func, + _ => &Self::FUNCTIONS[index], + } + } +} diff --git a/src/convert/svg.rs b/src/convert/svg.rs index 8d80ad4..4fc54b3 100644 --- a/src/convert/svg.rs +++ b/src/convert/svg.rs @@ -21,9 +21,11 @@ //! # } //! ``` -use crate::{QRCode, Version}; +use crate::{convert::EyePosition, ModuleType, QRCode, Version}; -use super::{Builder, Color, ImageBackgroundShape, ModuleFunction, Shape}; +use super::{ + module_shape::ModuleFunction, Builder, Color, EyeFrameShape, ImageBackgroundShape, ModuleShape, +}; /// Builder for svg, can set shape, margin, background_color, dot_color pub struct SvgBuilder { @@ -41,6 +43,12 @@ pub struct SvgBuilder { /// The color for each module, default is #000000 dot_color: Color, + // Eye Frame + /// Eye Frame Shape + eye_frame_shape: EyeFrameShape, + /// Eye Frame Color + eye_frame_color: Color, + // Image Embedding /// Image to embed in the svg, can be a path or a base64 string image: Option, @@ -74,6 +82,9 @@ impl Default for SvgBuilder { commands: Vec::new(), command_colors: Vec::new(), + eye_frame_shape: EyeFrameShape::Empty, + eye_frame_color: [0, 0, 0, 255].into(), + // Image Embedding image: None, image_background_color: [255; 4].into(), @@ -100,13 +111,13 @@ impl Builder for SvgBuilder { self } - fn shape(&mut self, shape: Shape) -> &mut Self { + fn module_shape(&mut self, shape: ModuleShape) -> &mut Self { self.commands.push(*shape); self.command_colors.push(None); self } - fn shape_color>(&mut self, shape: Shape, color: C) -> &mut Self { + fn module_shape_color>(&mut self, shape: ModuleShape, color: C) -> &mut Self { self.commands.push(*shape); self.command_colors.push(Some(color.into())); self @@ -139,9 +150,38 @@ impl Builder for SvgBuilder { self.image_position = Some((x, y)); self } + + fn eye_frame_shape(&mut self, shape: EyeFrameShape) -> &mut Self { + self.eye_frame_shape = shape; + self + } + + fn eye_frame_shape_color>( + &mut self, + shape: EyeFrameShape, + color: C, + ) -> &mut Self { + self.eye_frame_shape = shape; + self.eye_frame_color = color.into(); + self + } } impl SvgBuilder { + /// Return the coordinates of the eye according to the eye position + /// + /// Return (x, y) + fn eye_placement(&self, qr: &QRCode, eye_position: EyePosition) -> (usize, usize) { + let margin = self.margin; + let offset = qr.size + margin - 7; + + match eye_position { + EyePosition::TopLeft => (margin, margin), + EyePosition::TopRight => (offset, margin), + EyePosition::BottomLeft => (margin, offset), + } + } + fn image_placement( image_background_shape: ImageBackgroundShape, margin: usize, @@ -248,7 +288,7 @@ impl SvgBuilder { } fn path(&self, qr: &QRCode) -> String { - const DEFAULT_COMMAND: [ModuleFunction; 1] = [Shape::square]; + const DEFAULT_COMMAND: [ModuleFunction; 1] = [ModuleShape::square]; const DEFAULT_COMMAND_COLOR: [Option; 1] = [None]; // TODO: cleanup this basic logic @@ -275,6 +315,12 @@ impl SvgBuilder { continue; } + if self.eye_frame_shape != EyeFrameShape::Empty + && cell.module_type() == ModuleType::FinderPattern + { + continue; + } + for (i, command) in commands.iter().enumerate() { paths[i].push_str(&command(x + self.margin, y + self.margin, cell)); } @@ -285,7 +331,7 @@ impl SvgBuilder { let command_color = command_colors[i].as_ref().unwrap_or(&self.dot_color); // Allows to compare if two function pointers are the same // This works because there is no notion of Generics for `rounded_square` - if command as usize == Shape::rounded_square as usize { + if command as usize == ModuleShape::rounded_square as usize { paths[i].push_str(&format!( r##"" stroke-width=".3" stroke-linejoin="round" stroke="{}"##, command_color.to_str() @@ -295,6 +341,20 @@ impl SvgBuilder { paths[i].push_str(&format!(r#"" fill="{}"/>"#, command_color.to_str())); } + let mut finder_pattern_path = String::with_capacity(128); + if self.eye_frame_shape != EyeFrameShape::Empty { + for eye_position in EyePosition::ALL { + let (x, y) = self.eye_placement(qr, eye_position); + + let eye_frame_shape_function = self.eye_frame_shape; + let eye_shape_str = eye_frame_shape_function(y, x, eye_position); + + finder_pattern_path.push_str(&eye_shape_str); + } + + paths.push(finder_pattern_path); + } + paths.join("") } diff --git a/src/wasm.rs b/src/wasm.rs index 695bc79..3d266e9 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -25,7 +25,7 @@ pub fn qr(content: &str) -> Vec { #[cfg_attr(feature = "wasm-bindgen", wasm_bindgen)] #[derive(Debug, Clone)] pub struct SvgOptions { - shape: convert::Shape, + shape: convert::ModuleShape, module_color: Vec, margin: usize, @@ -59,7 +59,7 @@ impl SvgOptions { } /// Updates the shape of the QRCode modules. - pub fn shape(self, shape: convert::Shape) -> Self { + pub fn shape(self, shape: convert::ModuleShape) -> Self { Self { shape, ..self } } @@ -149,7 +149,7 @@ impl SvgOptions { #[cfg_attr(feature = "wasm-bindgen", wasm_bindgen(constructor))] pub fn new() -> Self { Self { - shape: convert::Shape::Square, + shape: convert::ModuleShape::Square, module_color: vec![0, 0, 0, 255], margin: 4, @@ -173,7 +173,7 @@ pub fn qr_svg(content: &str, options: SvgOptions) -> String { let qrcode = QRCode::new(content.as_bytes(), None, None, None); let mut builder = SvgBuilder::default(); - builder.shape(options.shape); + builder.module_shape(options.shape); builder.margin(options.margin); builder.background_color(options.background_color); builder.module_color(options.module_color);