From 3b168b43438a73e50e1556ccf3ea4061d20a9e53 Mon Sep 17 00:00:00 2001 From: Erwan Vivien Date: Tue, 26 Sep 2023 15:23:35 +0200 Subject: [PATCH] wip: Add different kind of Eye --- examples/svg.rs | 3 +- src/convert/eye_shape.rs | 189 +++++++++++++++++++++++++++++++++++++++ src/convert/image.rs | 14 +++ src/convert/mod.rs | 13 +++ src/convert/svg.rs | 71 ++++++++++++++- 5 files changed, 287 insertions(+), 3 deletions(-) create mode 100644 src/convert/eye_shape.rs diff --git a/examples/svg.rs b/examples/svg.rs index 2626e93..ddf772e 100644 --- a/examples/svg.rs +++ b/examples/svg.rs @@ -1,6 +1,6 @@ fn main() { use fast_qr::{ - convert::{svg::SvgBuilder, Builder, ModuleShape}, + convert::{svg::SvgBuilder, Builder, EyeFrameShape, ModuleShape}, QRBuilder, Version, ECL, }; @@ -12,5 +12,6 @@ fn main() { let _svg = SvgBuilder::default() .module_shape(ModuleShape::RoundedSquare) + .eye_frame_shape(EyeFrameShape::Square) .to_file(&qrcode, "svg.svg"); } diff --git a/src/convert/eye_shape.rs b/src/convert/eye_shape.rs new file mode 100644 index 0000000..9d3898f --- /dev/null +++ b/src/convert/eye_shape.rs @@ -0,0 +1,189 @@ +/// 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, + /// Square shape with a dot in the middle + DottedSquare, + /// 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::DottedSquare => 7, + EyeFrameShape::EyeLash => 8, + #[cfg(not(feature = "wasm-bindgen"))] + EyeFrameShape::Command(_) => 9, + } + } +} + +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_side_1" => Self::RoundedSquaredOuterCorner, + "rounded_squared_side_2" => Self::Leaf, + "rounded_squared_side_3" => Self::RoundedSquaredInnerCorner, + "dotted_square" => Self::DottedSquare, + "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_side_1", + EyeFrameShape::Leaf => "rounded_squared_side_2", + EyeFrameShape::RoundedSquaredInnerCorner => "rounded_squared_side_3", + EyeFrameShape::DottedSquare => "dotted_square", + EyeFrameShape::EyeLash => "eye_lash", + + #[cfg(not(feature = "wasm-bindgen"))] + EyeFrameShape::Command(_) => "command", + } + } +} + +impl EyeFrameShape { + 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; + + 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") + } + + const FUNCTIONS: [EyeFunction; 2] = [EyeFrameShape::empty, EyeFrameShape::square]; +} + +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 7b71a46..5403884 100644 --- a/src/convert/image.rs +++ b/src/convert/image.rs @@ -118,6 +118,20 @@ impl Builder for ImageBuilder { 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 + } } impl ImageBuilder { diff --git a/src/convert/mod.rs b/src/convert/mod.rs index 7afd4de..96b74e9 100644 --- a/src/convert/mod.rs +++ b/src/convert/mod.rs @@ -19,6 +19,9 @@ pub use color::{rgba2hex, Color}; 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::*; @@ -86,6 +89,16 @@ pub trait Builder { /// Add a shape to the shapes list with a specific color 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 /// Provides the image path or an base64 encoded image diff --git a/src/convert/svg.rs b/src/convert/svg.rs index 33fa759..2843b18 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::{module_shape::ModuleFunction, Builder, Color, ImageBackgroundShape, ModuleShape}; +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(), @@ -139,9 +150,40 @@ 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; + + dbg!((margin, offset)); + + match eye_position { + EyePosition::TopLeft => (margin, margin), + EyePosition::TopRight => (offset, margin), + EyePosition::BottomLeft => (margin, offset), + } + } + fn image_placement( image_background_shape: ImageBackgroundShape, margin: usize, @@ -275,6 +317,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)); } @@ -295,6 +343,25 @@ impl SvgBuilder { paths[i].push_str(&format!(r#"" fill="{}"/>"#, command_color.to_str())); } + let mut finder_pattern_path = String::with_capacity(100); + if self.eye_frame_shape != EyeFrameShape::Empty { + finder_pattern_path.push_str(r#""#, self.eye_frame_color.to_str())); + + paths.push(finder_pattern_path); + } + paths.join("") }