Skip to main content

qr_code_styling/core/
qr_matrix.rs

1//! QR code matrix wrapper providing neighbor lookup functionality.
2
3use crate::config::QROptions;
4use crate::error::{QRError, Result};
5use qrcode::{QrCode, Version};
6
7/// Wrapper around the QR code matrix providing efficient module access.
8#[derive(Debug, Clone)]
9pub struct QRMatrix {
10    /// Flat array of module values (true = dark, false = light).
11    modules: Vec<bool>,
12    /// Size of the QR code (number of modules per side).
13    size: usize,
14}
15
16impl QRMatrix {
17    /// Create a new QR matrix from data with the specified options.
18    pub fn new(data: &str, options: &QROptions) -> Result<Self> {
19        let ec_level = options.error_correction_level.to_qrcode_level();
20
21        // Determine the version
22        let version = if options.type_number == 0 {
23            None // Auto-detect
24        } else {
25            Some(Version::Normal(options.type_number as i16))
26        };
27
28        // Build the QR code
29        let qr = if let Some(v) = version {
30            QrCode::with_version(data.as_bytes(), v, ec_level)
31                .map_err(|e| QRError::QRGenerationError(e.to_string()))?
32        } else {
33            QrCode::with_error_correction_level(data.as_bytes(), ec_level)
34                .map_err(|e| QRError::QRGenerationError(e.to_string()))?
35        };
36
37        let size = qr.width() as usize;
38        let mut modules = Vec::with_capacity(size * size);
39
40        // Convert to flat array for O(1) access
41        for y in 0..size {
42            for x in 0..size {
43                let color = qr[(x, y)];
44                modules.push(color == qrcode::Color::Dark);
45            }
46        }
47
48        Ok(Self { modules, size })
49    }
50
51    /// Get the size (width/height) of the QR code in modules.
52    #[inline]
53    pub fn size(&self) -> usize {
54        self.size
55    }
56
57    /// Get the module count (same as size for compatibility).
58    #[inline]
59    pub fn module_count(&self) -> usize {
60        self.size
61    }
62
63    /// Check if a module at (row, col) is dark.
64    #[inline]
65    pub fn is_dark(&self, row: usize, col: usize) -> bool {
66        if row >= self.size || col >= self.size {
67            return false;
68        }
69        self.modules[row * self.size + col]
70    }
71
72    /// Check if a module at (row, col) is dark, with signed coordinates.
73    /// Returns false for out-of-bounds coordinates.
74    #[inline]
75    pub fn is_dark_signed(&self, row: i32, col: i32) -> bool {
76        if row < 0 || col < 0 {
77            return false;
78        }
79        self.is_dark(row as usize, col as usize)
80    }
81
82    /// Get neighbor state relative to a position.
83    #[inline]
84    pub fn get_neighbor(&self, row: i32, col: i32, offset_x: i32, offset_y: i32) -> bool {
85        self.is_dark_signed(row + offset_y, col + offset_x)
86    }
87
88    /// Check if a position is part of a finder pattern (corner square).
89    /// Finder patterns are 7x7 and located at:
90    /// - Top-left: (0, 0)
91    /// - Top-right: (0, size-7)
92    /// - Bottom-left: (size-7, 0)
93    pub fn is_finder_pattern(&self, row: usize, col: usize) -> bool {
94        let size = self.size;
95
96        // Top-left finder pattern
97        if row < 7 && col < 7 {
98            return true;
99        }
100
101        // Top-right finder pattern
102        if row < 7 && col >= size - 7 {
103            return true;
104        }
105
106        // Bottom-left finder pattern
107        if row >= size - 7 && col < 7 {
108            return true;
109        }
110
111        false
112    }
113
114    /// Check if a position is part of a finder pattern's outer square (7x7 border).
115    pub fn is_finder_pattern_outer(&self, row: usize, col: usize) -> bool {
116        if !self.is_finder_pattern(row, col) {
117            return false;
118        }
119
120        let size = self.size;
121
122        // Check if on the border of any finder pattern
123        let check_border = |r: usize, c: usize, start_r: usize, start_c: usize| -> bool {
124            let local_r = r - start_r;
125            let local_c = c - start_c;
126            local_r == 0 || local_r == 6 || local_c == 0 || local_c == 6
127        };
128
129        // Top-left
130        if row < 7 && col < 7 {
131            return check_border(row, col, 0, 0);
132        }
133
134        // Top-right
135        if row < 7 && col >= size - 7 {
136            return check_border(row, col, 0, size - 7);
137        }
138
139        // Bottom-left
140        if row >= size - 7 && col < 7 {
141            return check_border(row, col, size - 7, 0);
142        }
143
144        false
145    }
146
147    /// Check if a position is part of a finder pattern's inner dot (3x3 center).
148    pub fn is_finder_pattern_inner(&self, row: usize, col: usize) -> bool {
149        if !self.is_finder_pattern(row, col) {
150            return false;
151        }
152
153        let size = self.size;
154
155        let check_inner = |r: usize, c: usize, start_r: usize, start_c: usize| -> bool {
156            let local_r = r - start_r;
157            let local_c = c - start_c;
158            local_r >= 2 && local_r <= 4 && local_c >= 2 && local_c <= 4
159        };
160
161        // Top-left
162        if row < 7 && col < 7 {
163            return check_inner(row, col, 0, 0);
164        }
165
166        // Top-right
167        if row < 7 && col >= size - 7 {
168            return check_inner(row, col, 0, size - 7);
169        }
170
171        // Bottom-left
172        if row >= size - 7 && col < 7 {
173            return check_inner(row, col, size - 7, 0);
174        }
175
176        false
177    }
178}
179
180/// Square mask for corner squares (7x7 pattern).
181/// 1 = part of outer square border, 0 = not part of border
182#[allow(dead_code)]
183pub const SQUARE_MASK: [[u8; 7]; 7] = [
184    [1, 1, 1, 1, 1, 1, 1],
185    [1, 0, 0, 0, 0, 0, 1],
186    [1, 0, 0, 0, 0, 0, 1],
187    [1, 0, 0, 0, 0, 0, 1],
188    [1, 0, 0, 0, 0, 0, 1],
189    [1, 0, 0, 0, 0, 0, 1],
190    [1, 1, 1, 1, 1, 1, 1],
191];
192
193/// Dot mask for corner dots (7x7 pattern).
194/// 1 = part of inner 3x3 dot, 0 = not part of dot
195#[allow(dead_code)]
196pub const DOT_MASK: [[u8; 7]; 7] = [
197    [0, 0, 0, 0, 0, 0, 0],
198    [0, 0, 0, 0, 0, 0, 0],
199    [0, 0, 1, 1, 1, 0, 0],
200    [0, 0, 1, 1, 1, 0, 0],
201    [0, 0, 1, 1, 1, 0, 0],
202    [0, 0, 0, 0, 0, 0, 0],
203    [0, 0, 0, 0, 0, 0, 0],
204];
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn test_qr_matrix_creation() {
212        let options = QROptions::default();
213        let matrix = QRMatrix::new("Hello", &options).unwrap();
214        assert!(matrix.size() >= 21); // Minimum QR code size
215    }
216
217    #[test]
218    fn test_is_dark() {
219        let options = QROptions::default();
220        let matrix = QRMatrix::new("Test", &options).unwrap();
221
222        // Finder pattern top-left corner should be dark
223        assert!(matrix.is_dark(0, 0));
224    }
225
226    #[test]
227    fn test_neighbor_lookup() {
228        let options = QROptions::default();
229        let matrix = QRMatrix::new("Test", &options).unwrap();
230
231        // Test that neighbor lookup works
232        let dark = matrix.is_dark(0, 0);
233        let neighbor = matrix.get_neighbor(0, 1, -1, 0);
234        assert_eq!(dark, neighbor);
235    }
236
237    #[test]
238    fn test_finder_pattern_detection() {
239        let options = QROptions::default();
240        let matrix = QRMatrix::new("Test", &options).unwrap();
241
242        // Top-left corner should be finder pattern
243        assert!(matrix.is_finder_pattern(0, 0));
244        assert!(matrix.is_finder_pattern(3, 3));
245        assert!(matrix.is_finder_pattern(6, 6));
246
247        // Middle of QR code should not be finder pattern
248        let mid = matrix.size() / 2;
249        assert!(!matrix.is_finder_pattern(mid, mid));
250    }
251}