Skip to main content

tuv/matrix/
function_patterns.rs

1//! Function patterns — finder markers, alignment patterns, timing patterns.
2//! 
3//! Function patterns are the fixed structural elements of a QR matrix.
4//! They are immune to masking (placed before masking, never XOR'd).
5//! 
6//! ## Finder Markers (3 total, always in corners)
7//! 
8//! Each is a 7×7 pattern with a distinctive "target" appearance:
9//! - Outer 7×7 border: all dark
10//! - Inner 5×5 border: all dark  
11//! - Core 3×3: dark corners, light center (forming a ring)
12//! - Plus 1-module white separator (all around)
13//! 
14//! ## Alignment Patterns (version ≥ 2)
15//! 
16//! Smaller 5×5 patterns used to help scanners correct for rotation.
17//! Positioned according to a version-specific alignment grid.
18//! 
19//! ## Timing Patterns
20//! 
21//! Alternating dark/light 1×N strips that help scanners sync their clock.
22//! Row 6 (0-indexed) connecting finders horizontally, column 6 vertically.
23
24// QRMatrix and Module are defined in this file — no need to import from super
25
26/// Module type — identifies what's stored at each matrix position.
27///
28/// Every variant that actually renders carries its dark/light value so that
29/// rendering, masking and scoring can all consult a single source of truth.
30/// Function-pattern variants are immune to data masking by construction —
31/// `apply_mask` only touches `Module::Data`.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum Module {
34    /// Free data cell. `true` = dark.
35    Data(bool),
36    /// Cell inside a 7×7 finder marker. `true` = dark.
37    Finder(bool),
38    /// 1-module-wide white border around each finder. Always light.
39    Separator,
40    /// Cell inside a 5×5 alignment pattern. `true` = dark.
41    Alignment(bool),
42    /// Cell on a timing strip. `true` = dark.
43    Timing(bool),
44    /// Reserved/encoded format-info bit. `true` = dark.
45    FormatInfo(bool),
46    /// Reserved/encoded version-info bit. `true` = dark.
47    VersionInfo(bool),
48}
49
50impl Module {
51    /// Returns the rendered dark/light value for this module.
52    pub fn is_dark(&self) -> bool {
53        match self {
54            Module::Data(v)
55            | Module::Finder(v)
56            | Module::Alignment(v)
57            | Module::Timing(v)
58            | Module::FormatInfo(v)
59            | Module::VersionInfo(v) => *v,
60            Module::Separator => false,
61        }
62    }
63
64    /// True if this cell is available for data placement / data masking.
65    pub fn is_data(&self) -> bool {
66        matches!(self, Module::Data(_))
67    }
68}
69
70/// The QR module matrix.
71#[derive(Debug, Clone)]
72pub struct QRMatrix {
73    pub version: u8,
74    pub size: usize,
75    pub modules: Vec<Vec<Module>>,
76}
77
78impl QRMatrix {
79    /// Create a new empty matrix for the given version.
80    /// All modules start as Data(false).
81    pub fn new(version: u8) -> Self {
82        let size = (version as usize) * 4 + 17;
83        let modules = vec![vec![Module::Data(false); size]; size];
84        Self { version, size, modules }
85    }
86
87    /// Whether this matrix uses Micro QR sizing (11×11 … 17×17).
88    pub fn is_micro(&self) -> bool {
89        self.size <= 17 && self.size == self.version as usize * 2 + 9
90    }
91
92    /// Set a module at (col, row).
93    pub fn set(&mut self, i: usize, j: usize, module: Module) {
94        if i >= self.size || j >= self.size {
95            return;
96        }
97        self.modules[j][i] = module;
98    }
99
100    /// Get the module at (i, j).
101    pub fn get(&self, i: usize, j: usize) -> Module {
102        if i >= self.size || j >= self.size {
103            return Module::Data(false);
104        }
105        self.modules[j][i]
106    }
107
108    /// Get the boolean value at (i, j) for data modules.
109    pub fn data_at(&self, i: usize, j: usize) -> Option<bool> {
110        match self.get(i, j) {
111            Module::Data(v) => Some(v),
112            _ => None,
113        }
114    }
115
116    /// Get size (modules per side).
117    pub fn size(&self) -> usize {
118        self.size
119    }
120
121    /// Place all function patterns for this version, plus reserve the
122    /// format-info (and, for v ≥ 7, version-info) areas so that data
123    /// placement skips over them.
124    pub fn place_function_patterns(&mut self) {
125        self.place_finder_marker(0, 0);
126        self.place_finder_marker(self.size - 7, 0);
127        self.place_finder_marker(0, self.size - 7);
128
129        self.place_separators();
130        self.place_timing_patterns();
131
132        if self.version >= 2 {
133            self.place_alignment_patterns();
134        }
135
136        self.reserve_format_info_area();
137
138        if self.version >= 7 {
139            self.reserve_version_info_area();
140        }
141    }
142
143    /// Mark only the cells that will receive format information (see
144    /// `format_info::all_format_info_positions`) so data placement skips them.
145    fn reserve_format_info_area(&mut self) {
146        for (c, r) in crate::matrix::format_info::all_format_info_positions(self.size) {
147            self.set(c, r, Module::FormatInfo(false));
148        }
149        let s = self.size;
150        // Same cell as `qrcode` `Canvas::draw_format_info_patterns_with_number` dark module.
151        self.set(8, s - 8, Module::FormatInfo(true));
152    }
153
154    /// For v ≥ 7, reserve the two 3×6 version-info blocks.
155    fn reserve_version_info_area(&mut self) {
156        let s = self.size;
157        for row in 0..6 {
158            for col in 0..3 {
159                self.set(s - 11 + col, row, Module::VersionInfo(false));
160                self.set(row, s - 11 + col, Module::VersionInfo(false));
161            }
162        }
163    }
164
165    fn place_finder_marker(&mut self, ox: usize, oy: usize) {
166        for dy in 0..7 {
167            for dx in 0..7 {
168                self.set(ox + dx, oy + dy, Module::Finder(FINDER_PATTERN[dy][dx]));
169            }
170        }
171    }
172
173    fn place_separators(&mut self) {
174        let s = self.size;
175        for i in 0..8 {
176            self.set(i, 7, Module::Separator);
177            self.set(7, i, Module::Separator);
178
179            self.set(s - 8 + i, 7, Module::Separator);
180            self.set(s - 8, i, Module::Separator);
181
182            self.set(i, s - 8, Module::Separator);
183            self.set(7, s - 8 + i, Module::Separator);
184        }
185    }
186
187    /// Timing patterns are alternating dark/light strips on row 6 and column 6,
188    /// running between the finder separators.
189    fn place_timing_patterns(&mut self) {
190        let s = self.size;
191        for i in 8..=s - 9 {
192            let dark = i % 2 == 0;
193            self.set(i, 6, Module::Timing(dark));
194            self.set(6, i, Module::Timing(dark));
195        }
196    }
197
198    fn place_alignment_patterns(&mut self) {
199        let positions = alignment_positions(self.version);
200        for &(cx, cy) in &positions {
201            // Skip any centre that overlaps a finder marker.
202            if matches!(self.get(cx, cy), Module::Finder(_)) {
203                continue;
204            }
205            let ox = cx - 2;
206            let oy = cy - 2;
207            for dy in 0..5 {
208                for dx in 0..5 {
209                    self.set(ox + dx, oy + dy, Module::Alignment(ALIGNMENT_PATTERN[dy][dx]));
210                }
211            }
212        }
213    }
214}
215
216/// Finder pattern — 7×7 grid. true=dark, false=white.
217const FINDER_PATTERN: [[bool; 7]; 7] = [
218    [true,  true,  true,  true,  true,  true,  true ],
219    [true,  false, false, false, false, false, true ],
220    [true,  false, true,  true,  true,  false, true ],
221    [true,  false, true,  true,  true,  false, true ],
222    [true,  false, true,  true,  true,  false, true ],
223    [true,  false, false, false, false, false, true ],
224    [true,  true,  true,  true,  true,  true,  true ],
225];
226
227/// Alignment pattern — 5×5 grid.
228const ALIGNMENT_PATTERN: [[bool; 5]; 5] = [
229    [true,  true,  true,  true,  true ],
230    [true,  false, false, false, true ],
231    [true,  false, true,  false, true ],
232    [true,  false, false, false, true ],
233    [true,  true,  true,  true,  true ],
234];
235
236/// Alignment pattern centre coordinates per ISO/IEC 18004 Annex E Table E.1.
237/// Index into `ALIGNMENT_CENTRES` is `version - 1`. Empty for version 1.
238const ALIGNMENT_CENTRES: [&[usize]; 40] = [
239    &[],
240    &[6, 18],
241    &[6, 22],
242    &[6, 26],
243    &[6, 30],
244    &[6, 34],
245    &[6, 22, 38],
246    &[6, 24, 42],
247    &[6, 26, 46],
248    &[6, 28, 50],
249    &[6, 30, 54],
250    &[6, 32, 58],
251    &[6, 34, 62],
252    &[6, 26, 46, 66],
253    &[6, 26, 48, 70],
254    &[6, 26, 50, 74],
255    &[6, 30, 54, 78],
256    &[6, 30, 56, 82],
257    &[6, 30, 58, 86],
258    &[6, 34, 62, 90],
259    &[6, 28, 50, 72, 94],
260    &[6, 26, 50, 74, 98],
261    &[6, 30, 54, 78, 102],
262    &[6, 28, 54, 80, 106],
263    &[6, 32, 58, 84, 110],
264    &[6, 30, 58, 86, 114],
265    &[6, 34, 62, 90, 118],
266    &[6, 26, 50, 74, 98, 122],
267    &[6, 30, 54, 78, 102, 126],
268    &[6, 26, 52, 78, 104, 130],
269    &[6, 30, 56, 82, 108, 134],
270    &[6, 34, 60, 86, 112, 138],
271    &[6, 30, 58, 86, 114, 142],
272    &[6, 34, 62, 90, 118, 146],
273    &[6, 30, 54, 78, 102, 126, 150],
274    &[6, 24, 50, 76, 102, 128, 154],
275    &[6, 28, 54, 80, 106, 132, 158],
276    &[6, 32, 58, 84, 110, 136, 162],
277    &[6, 26, 54, 82, 110, 138, 166],
278    &[6, 30, 58, 86, 114, 142, 170],
279];
280
281/// All alignment-pattern centres for a given version, excluding the three
282/// centres that would overlap a finder marker.
283fn alignment_positions(version: u8) -> Vec<(usize, usize)> {
284    let centres = ALIGNMENT_CENTRES[(version as usize) - 1];
285    if centres.is_empty() {
286        return Vec::new();
287    }
288    let last = *centres.last().unwrap();
289    let mut out = Vec::new();
290    for &cy in centres {
291        for &cx in centres {
292            // Skip the three centres that coincide with finder markers.
293            let on_finder = (cx == 6 && cy == 6)
294                || (cx == 6 && cy == last)
295                || (cx == last && cy == 6);
296            if !on_finder {
297                out.push((cx, cy));
298            }
299        }
300    }
301    out
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307
308    #[test]
309    fn test_matrix_size_version_1() {
310        let m = QRMatrix::new(1);
311        assert_eq!(m.size, 21);
312    }
313
314    #[test]
315    fn test_matrix_size_version_10() {
316        let m = QRMatrix::new(10);
317        assert_eq!(m.size, 57);
318    }
319
320    #[test]
321    fn test_finder_marker_at_topleft() {
322        let mut mat = QRMatrix::new(1);
323        mat.place_function_patterns();
324        // Centre of top-left finder is dark.
325        assert_eq!(mat.get(3, 3), Module::Finder(true));
326        // Inside the white ring of the finder.
327        assert_eq!(mat.get(1, 1), Module::Finder(false));
328    }
329
330    #[test]
331    fn test_separator_placed() {
332        let mut mat = QRMatrix::new(1);
333        mat.place_function_patterns();
334        assert_eq!(mat.get(0, 7), Module::Separator);
335        assert_eq!(mat.get(7, 0), Module::Separator);
336    }
337
338    #[test]
339    fn test_timing_alternates() {
340        let mut mat = QRMatrix::new(2);
341        mat.place_function_patterns();
342        // Row 6, columns 8..=size-9 should alternate dark/light.
343        assert_eq!(mat.get(8, 6), Module::Timing(true));
344        assert_eq!(mat.get(9, 6), Module::Timing(false));
345        assert_eq!(mat.get(10, 6), Module::Timing(true));
346    }
347
348    #[test]
349    fn format_reserve_matches_placement_count() {
350        let s = 25;
351        let cells = crate::matrix::format_info::all_format_info_positions(s);
352        assert_eq!(cells.len(), 30);
353    }
354
355    #[test]
356    fn is_micro_detects_micro_vs_normal_sizing() {
357        let normal = QRMatrix::new(1);
358        assert!(!normal.is_micro());
359        assert_eq!(normal.size, 21);
360
361        let micro = QRMatrix {
362            version: 1,
363            size: 11,
364            modules: vec![vec![Module::Data(false); 11]; 11],
365        };
366        assert!(micro.is_micro());
367    }
368}