1use std::fmt;
2
3use crate::qrcode::error::QrError;
4
5#[derive(Debug, Clone, PartialEq)]
10pub enum ModuleShape {
11 Square,
13 RoundedSquare {
17 radius: f32,
19 },
20 Circle,
22 Diamond,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum FinderShape {
31 Square,
33 Rounded,
35 Circle,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum Color {
44 Hex(String),
46 Rgb(u8, u8, u8),
48}
49
50impl Color {
51 pub fn to_hex(&self) -> Result<String, QrError> {
59 match self {
60 Color::Rgb(r, g, b) => Ok(format!("#{r:02x}{g:02x}{b:02x}")),
61 Color::Hex(s) => {
62 let s = s.trim();
63 if !s.starts_with('#') {
64 return Err(QrError::InvalidColor(s.to_string()));
65 }
66 let hex = &s[1..];
67 match hex.len() {
68 3 => {
69 if !hex.chars().all(|c| c.is_ascii_hexdigit()) {
70 return Err(QrError::InvalidColor(s.to_string()));
71 }
72 let expanded: String = hex.chars().flat_map(|c| [c, c]).collect();
73 Ok(format!("#{expanded}"))
74 }
75 6 => {
76 if !hex.chars().all(|c| c.is_ascii_hexdigit()) {
77 return Err(QrError::InvalidColor(s.to_string()));
78 }
79 Ok(s.to_lowercase())
80 }
81 _ => Err(QrError::InvalidColor(s.to_string())),
82 }
83 }
84 }
85 }
86}
87
88impl fmt::Display for Color {
89 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90 match self.to_hex() {
91 Ok(hex) => write!(f, "{hex}"),
92 Err(_) => write!(f, "(invalid)"),
93 }
94 }
95}
96
97#[derive(Debug, Clone, PartialEq)]
123pub struct QrStyle {
124 pub module_shape: ModuleShape,
126 pub finder_shape: FinderShape,
128 pub fg_color: Color,
130 pub bg_color: Color,
132 pub module_size: u32,
134 pub quiet_zone: u32,
137}
138
139impl Default for QrStyle {
140 fn default() -> Self {
141 Self {
142 module_shape: ModuleShape::RoundedSquare { radius: 0.3 },
143 finder_shape: FinderShape::Rounded,
144 fg_color: Color::Hex("#000000".into()),
145 bg_color: Color::Hex("#ffffff".into()),
146 module_size: 10,
147 quiet_zone: 4,
148 }
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
159 fn rgb_to_hex() {
160 let c = Color::Rgb(26, 26, 46);
161 assert_eq!(c.to_hex().unwrap(), "#1a1a2e");
162 }
163
164 #[test]
165 fn rgb_black() {
166 let c = Color::Rgb(0, 0, 0);
167 assert_eq!(c.to_hex().unwrap(), "#000000");
168 }
169
170 #[test]
171 fn rgb_white() {
172 let c = Color::Rgb(255, 255, 255);
173 assert_eq!(c.to_hex().unwrap(), "#ffffff");
174 }
175
176 #[test]
177 fn hex_six_char_valid() {
178 let c = Color::Hex("#1a1a2e".into());
179 assert_eq!(c.to_hex().unwrap(), "#1a1a2e");
180 }
181
182 #[test]
183 fn hex_six_char_uppercase_normalized() {
184 let c = Color::Hex("#FF00AA".into());
185 assert_eq!(c.to_hex().unwrap(), "#ff00aa");
186 }
187
188 #[test]
189 fn hex_three_char_expanded() {
190 let c = Color::Hex("#fff".into());
191 assert_eq!(c.to_hex().unwrap(), "#ffffff");
192 }
193
194 #[test]
195 fn hex_three_char_color() {
196 let c = Color::Hex("#f0a".into());
197 assert_eq!(c.to_hex().unwrap(), "#ff00aa");
198 }
199
200 #[test]
201 fn hex_missing_hash_is_error() {
202 let c = Color::Hex("000000".into());
203 assert!(c.to_hex().is_err());
204 }
205
206 #[test]
207 fn hex_invalid_chars_is_error() {
208 let c = Color::Hex("#gggggg".into());
209 assert!(c.to_hex().is_err());
210 }
211
212 #[test]
213 fn hex_wrong_length_is_error() {
214 let c = Color::Hex("#12345".into());
215 assert!(c.to_hex().is_err());
216 }
217
218 #[test]
219 fn hex_named_color_is_error() {
220 let c = Color::Hex("red".into());
221 assert!(c.to_hex().is_err());
222 }
223
224 #[test]
225 fn hex_three_char_invalid_chars_is_error() {
226 let c = Color::Hex("#ggg".into());
227 assert!(c.to_hex().is_err());
228 }
229
230 #[test]
233 fn default_style_has_rounded_module_shape() {
234 let s = QrStyle::default();
235 assert_eq!(s.module_shape, ModuleShape::RoundedSquare { radius: 0.3 });
236 }
237
238 #[test]
239 fn default_style_has_rounded_finder_shape() {
240 let s = QrStyle::default();
241 assert_eq!(s.finder_shape, FinderShape::Rounded);
242 }
243
244 #[test]
245 fn default_style_colors() {
246 let s = QrStyle::default();
247 assert_eq!(s.fg_color.to_hex().unwrap(), "#000000");
248 assert_eq!(s.bg_color.to_hex().unwrap(), "#ffffff");
249 }
250
251 #[test]
252 fn default_style_sizes() {
253 let s = QrStyle::default();
254 assert_eq!(s.module_size, 10);
255 assert_eq!(s.quiet_zone, 4);
256 }
257}