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