File Coverage

blib/lib/QRCode/Encoder/Matrix.pm
Criterion Covered Total %
statement 314 342 91.8
branch 46 48 95.8
condition 23 23 100.0
subroutine 35 38 92.1
pod 22 22 100.0
total 440 473 93.0


line stmt bran cond sub pod time code
1             package QRCode::Encoder::Matrix;
2 4     4   54 use v5.24;
  4         16  
3 4     4   21 use experimental qw< signatures >;
  4         8  
  4         22  
4 4     4   548 use List::Util qw< sum >;
  4         7  
  4         388  
5 4         285 use QRCode::Encoder::QRSpec qw<
6             qrspec_version_pattern
7             qrspec_format_pattern
8             qrspec_alignment_patterns
9 4     4   25 >;
  4         18  
10              
11 4     4   22 use Exporter qw< import >;
  4         8  
  4         22886  
12             our @EXPORT_OK = qw< add_matrix >;
13              
14             # Parts liberally taken from libqrencode/qrspec.c, which is distributed
15             # with LGPL license
16              
17 4     4 1 9 sub add_matrix ($data) {
  4         10  
  4         7  
18 4         20 add_base_matrix($data);
19 4         27 add_quiet($data);
20 4         33 add_finders($data);
21 4         19 add_format_reservations($data); # MUST: before add_timing
22 4         19 add_version($data);
23 4         20 add_timing($data);
24 4         16 add_alignments($data);
25 4         16 add_codewords($data);
26 4         24 add_mask($data);
27 4         23 return $data;
28             }
29              
30 0     0 1 0 sub stringify_matrix ($data) {
  0         0  
  0         0  
31 0         0 my @chunks;
32 0         0 my $matrix = $data->{matrix};
33 0         0 for my $row ($matrix->@*) {
34 0         0 push @chunks, join '', map { chr($_) } $row->@*;
  0         0  
35             }
36 0         0 return join "\n", @chunks;
37             }
38              
39 0     0 1 0 sub stringify_matrix_2 ($data) {
  0         0  
  0         0  
40 0         0 my @chunks;
41 0         0 my $matrix = $data->{matrix};
42 0         0 for my $row ($matrix->@*) {
43 0 0       0 push @chunks, join '', map { ($_ & 0x1) ? '#' : ' ' } $row->@*;
  0         0  
44             }
45 0         0 return join "\n", @chunks;
46             }
47              
48 4     4 1 8 sub add_base_matrix ($data) {
  4         8  
  4         8  
49 4         71 my $side = $data->{side_size} = 17 + 4 * $data->{version};
50 4         20 my $eside = $data->{eside_size} = $side + 8;
51 4         19 $data->{matrix} = [ map { [ ( 0x38 ) x $eside ] } 1 .. $eside ];
  144         739  
52 4         21 return $data;
53             }
54              
55 4     4 1 11 sub add_finders ($data) {
  4         9  
  4         8  
56 4         12 my $eside_size = $data->{eside_size};
57 4         21 add_finder($data, 4 - 1, 4 - 1);
58 4         70 add_finder($data, 4 - 1, $eside_size - 8 - 4);
59 4         56 add_finder($data, $eside_size - 8 - 4, 4 - 1);
60 4         11 return $data;
61             }
62              
63 4     4 1 15 sub add_quiet ($data) {
  4         10  
  4         10  
64 4         10 my $es = $data->{eside_size};
65 4         11 my $matrix = $data->{matrix};
66 4         19 for my $i (0 .. 3) {
67 16         43 for my $j (0 .. $es - 1) {
68 576         1385 $matrix->[$i][$j] =
69             $matrix->[$es - 1 - $i][$j] =
70             $matrix->[$j][$i] =
71             $matrix->[$j][$es - 1 - $i] = 0x30;
72             }
73             }
74 4         40 return $data;
75             }
76              
77 12     12 1 22 sub add_finder ($data, $x, $y) {
  12         31  
  12         22  
  12         19  
  12         19  
78 12         146 state $shape = [
79             [ 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30 ],
80             [ 0x30, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x30 ],
81             [ 0x30, 0x31, 0x30, 0x30, 0x30, 0x30, 0x30, 0x31, 0x30 ],
82             [ 0x30, 0x31, 0x30, 0x31, 0x31, 0x31, 0x30, 0x31, 0x30 ],
83             [ 0x30, 0x31, 0x30, 0x31, 0x31, 0x31, 0x30, 0x31, 0x30 ],
84             [ 0x30, 0x31, 0x30, 0x31, 0x31, 0x31, 0x30, 0x31, 0x30 ],
85             [ 0x30, 0x31, 0x30, 0x30, 0x30, 0x30, 0x30, 0x31, 0x30 ],
86             [ 0x30, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x30 ],
87             [ 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30 ],
88             ];
89 12         42 my $matrix = $data->{matrix};
90 12         32 for my $yoff (0 .. 8) {
91 108         178 my $Y = $y + $yoff;
92 108         212 for my $xoff (0 .. 8) {
93 972         1425 my $X = $x + $xoff;
94 972         1707 $matrix->[$Y][$X] = $shape->[$yoff][$xoff];
95             }
96             }
97 12         72 return $data;
98             }
99              
100 4     4 1 8 sub add_format_reservations ($data) {
  4         9  
  4         8  
101 4         9 my $matrix = $data->{matrix};
102 4         10 my $es = $data->{eside_size};
103 4         30 for my $i (0 .. 7) {
104 32         92 $matrix->[12][$i + 4] =
105             $matrix->[12][$i + $es - 4 - 8] =
106             $matrix->[$i + $es - 4 - 8][12] =
107             $matrix->[$i + 4][12] = 0x32;
108             }
109 4         10 $matrix->[12][12] = 0x32;
110 4         11 $matrix->[$es - 4 - 8][12] = 0x31;
111 4         11 return $data;
112             }
113              
114 0     0 1 0 sub add_version_reservations ($data) {
  0         0  
  0         0  
115 0         0 return $data->{version} <= 6;
116              
117 0         0 my $matrix = $data->{matrix};
118 0         0 my $ecstart = $data->{eside_size} - 4 - 7 - 4;
119 0         0 for my $i (4 .. 9) {
120 0         0 for my $j ($ecstart .. ($ecstart + 2)) {
121 0         0 $matrix->[$i][$j] = $matrix->[$j][$i] = 0x32;
122             }
123             }
124              
125 0         0 return $data;
126             }
127              
128 4     4 1 7 sub add_timing ($data) {
  4         9  
  4         7  
129 4         9 my $matrix = $data->{matrix};
130 4         10 my $es = $data->{eside_size};
131 4         32 for my $i (12 .. ($es - 4 - 8 - 1)) {
132 48         103 $matrix->[$i][10] = $matrix->[10][$i] = 0x35 ^ ($i & 1);
133             }
134 4         10 return $data;
135             }
136              
137 13     13 1 16 sub try_add_alignment_pattern ($data, $x, $y) {
  13         18  
  13         19  
  13         17  
  13         19  
138 13         27 state $shape = [
139             [ 0x31, 0x31, 0x31, 0x31, 0x31 ],
140             [ 0x31, 0x30, 0x30, 0x30, 0x31 ],
141             [ 0x31, 0x30, 0x31, 0x30, 0x31 ],
142             [ 0x31, 0x30, 0x30, 0x30, 0x31 ],
143             [ 0x31, 0x31, 0x31, 0x31, 0x31 ],
144             ];
145 13         18 $x += 4; # offset by quiet zone
146 13         20 $y += 4; # offset by quiet zone
147 13         19 my $matrix = $data->{matrix};
148 13 100 100     120 return if $matrix->[$y][$x] < 0x34
      100        
149             || $matrix->[$y][$x + 4] < 0x34
150             || $matrix->[$y + 4][$x] < 0x34;
151 7         16 for my $i (0 .. 4) {
152 35         76 for my $j (0 .. 4) {
153 175         344 $matrix->[$y + $i][$x + $j] = $shape->[$i][$j];
154             }
155             }
156             }
157              
158 4     4 1 7 sub add_alignments ($data) {
  4         7  
  4         10  
159 4         63 my @offset = qrspec_alignment_patterns($data->{version});
160 4         12 for my $y_center (@offset) {
161 5         10 for my $x_center (@offset) {
162 13         35 try_add_alignment_pattern($data, $x_center - 2, $y_center - 2);
163             }
164             }
165 4         9 return $data;
166             }
167              
168 4     4 1 7 sub bits_iterator ($data) {
  4         24  
  4         6  
169 4         11 my $n_expanded = length($data->{expanded});
170 4         10 my $rem = $data->{remainder};
171 4         7 my $i = 0;
172 4         9 my @queue;
173             return sub {
174 2347 100   2347   4486 if (! @queue) {
175 297 100       513 if ($i < $n_expanded) {
176 292         1963 push @queue, split m{}mxs, unpack 'B*', substr($data->{expanded}, $i++, 1);
177             }
178             else {
179 5         17 push @queue, ('0') x $rem;
180 5         9 $rem = 0;
181             }
182             }
183 2347         5421 return shift(@queue);
184 4         37 };
185             }
186              
187 4     4 1 8 sub add_codewords ($data) {
  4         16  
  4         6  
188 4         14 my $it = bits_iterator($data);
189 4         11 my $matrix = $data->{matrix};
190 4         9 my $side_size = $data->{side_size};
191              
192             # start from a fake position that would be the last bit of a
193             # hypothetical "-1" codeword
194 4         10 my $x = $side_size - 2;
195 4         6 my $y = $side_size;
196 4         8 my $left = 1;
197 4         11 my $d = -1; # direction
198 4         21 while (defined(my $bit = $it->())) {
199 2343         3378 while ('necessary') {
200 3350 100       6031 if ($x % 2 == $left) {
201 1675         2334 ++$x;
202 1675         2547 $y += $d;
203             }
204             else {
205 1675         2460 --$x;
206             }
207 3350 100 100     12782 if ($d < 0 && $y < 0) { # reset condition
    100 100        
208 27         66 $x -= 2;
209 27         37 $y = 0;
210 27         61 $d = 1;
211             }
212             elsif ($d > 0 && $y >= $side_size) { # other reset condition
213 23         49 $x -= 2;
214 23         38 $y = $side_size - 1;
215 23         37 $d = -1;
216             }
217 3350 100       6402 if ($x == 6) { # left timing column, skip a column entirely
218 4         13 $x = 5;
219 4         9 $left = 0;
220             }
221 3350 100       7282 last if $matrix->[$y + 4][$x + 4] > 0x37; # found suitable position
222             }
223 2343 100       5674 $matrix->[$y + 4][$x + 4] = $bit ? 0x37 : 0x36;
224             }
225 4         51 return $data;
226             }
227              
228 32     32 1 58 sub evaluate_matrix ($matrix) {
  32         52  
  32         52  
229 32         103 return 0
230             + evaluate_matrix_adjacents_and_11311($matrix)
231             + evaluate_matrix_blocks($matrix)
232             + evaluate_matrix_proportion($matrix);
233             }
234              
235 896     896   1229 sub __row ($matrix, $i) {
  896         1135  
  896         1088  
  896         1039  
236 896         1415 my $max_idx = $matrix->[0]->$#* - 4;
237 896 100       1925 join('', map { $matrix->[$i + 4][$_] & 0x01 ? 1 : 0 } 4 .. $max_idx);
  28256         55201  
238             }
239              
240 896     896   1175 sub __col ($matrix, $i) {
  896         1061  
  896         1112  
  896         1029  
241 896         1269 my $max_idx = $matrix->[0]->$#* - 4;
242 896 100       1796 join('', map { $matrix->[$_][$i + 4] & 0x01 ? 1 : 0 } 4 .. $max_idx);
  28256         54234  
243             }
244              
245 32     32 1 48 sub evaluate_matrix_adjacents_and_11311 ($matrix) {
  32         83  
  32         47  
246 32         87 my $side_size = $matrix->[0]->@* - 8;
247 32         61 my $penalty = 0;
248 32         55 my $penalty2 = 0;
249 32         105 for my $i (0 .. ($side_size - 1)) {
250 896         1680 for my $seq (__row($matrix, $i), __col($matrix, $i)) {
251              
252             # adjacences
253             my @contributions =
254 2273         3689 map { $_ - 2 }
255 28633         39031 grep { $_ >= 5 }
256 1792         13442 map { length }
  28633         36642  
257             split m{(0+)}mxs, $seq;
258 1792 100       6685 $penalty += sum(@contributions) if @contributions;
259              
260             # 000011311 | 113110000
261 1792         13195 my @matches = $seq =~ m{
262             (
263             (?: (?<=0000) 1011101 ) # look behind...
264             | (?: 1011101 (?=0000) ) # or look ahead
265             )
266             }gmxs;
267 1792         4684 $penalty2 += 40 * scalar(@matches);
268              
269             }
270             }
271 32         124 return $penalty + $penalty2;
272             }
273              
274 32     32 1 48 sub evaluate_matrix_blocks ($matrix) {
  32         46  
  32         43  
275 32         72 my $side_size = $matrix->[0]->@* - 8;
276 32         61 my $penalty = 0;
277 32         125 for my $i (0 .. ($side_size - 2)) {
278 864         1759 for my $j (0 .. ($side_size - 2)) {
279 26496         31087 my $count = 0;
280 26496         48802 for my $offset ([0, 0], [0, 1], [1, 0], [1, 1]) {
281 105984         142339 my ($oi, $oj) = $offset->@*;
282 105984 100       198911 $count++ if $matrix->[$i + $oi + 4][$j + $oj + 4] & 1;
283             }
284 26496 100 100     77479 $penalty += 3 if ($count == 0) || ($count == 4);
285             }
286             }
287 32         223 return $penalty;
288             }
289              
290 32     32 1 66 sub evaluate_matrix_proportion ($matrix) {
  32         68  
  32         56  
291 32 100       93 my $count = sum( map { map { $_ & 0x1 ? 1 : 0 } $_->@* } $matrix->@* );
  1152         1828  
  44640         71358  
292 32         843 my $side_size = $matrix->[0]->@* - 8;
293 32         76 my $total = $side_size * $side_size;
294 32         85 my $percentage = 100 * $count / $total;
295 32         88 my $deviation = abs($percentage - 50);
296 32         129 my $penalty = 10 * int($deviation / 5);
297 32         191 return $penalty;
298             }
299              
300 32     32 1 59 sub masked_matrix ($data, $mask_id) {
  32         74  
  32         75  
  32         50  
301 2343         3267 state $mask_for = {
302 2343     2343   6510 0 => sub ($i, $j) { (($i + $j) % 2) == 0 },
  2343         3353  
  2343         3310  
  2343         3157  
303 2343     2343   4069 1 => sub ($i, $j) { ($i % 2) == 0 },
  2343         2336  
  2343         2270  
  2343         2220  
  2343         2145  
304 2343     2343   4146 2 => sub ($i, $j) { ($j % 3) == 0 },
  2343         2353  
  2343         2397  
  2343         2258  
  2343         2244  
305 2343     2343   4320 3 => sub ($i, $j) { (($i + $j) % 3) == 0 },
  2343         2368  
  2343         2365  
  2343         2320  
  2343         2284  
306 2343     2343   5488 4 => sub ($i, $j) { ((int($i / 2) + int($j / 3)) % 2) == 0 },
  2343         2539  
  2343         2513  
  2343         2519  
  2343         2492  
307 2343     2343   5471 5 => sub ($i, $j) { ((($i * $j) % 2) + (($i * $j) % 3)) == 0 },
  2343         2835  
  2343         2925  
  2343         2967  
  2343         2697  
308 2343     2343   5869 6 => sub ($i, $j) { (((($i * $j) % 2) + (($i * $j) % 3)) % 2) == 0 },
  2343         2840  
  2343         2872  
  2343         2872  
  2343         2696  
309 2343     2343   5707 7 => sub ($i, $j) { (((($i + $j) % 2) + (($i * $j) % 3)) % 2) == 0 },
  2343         2808  
  2343         2931  
  2343         2810  
  2343         2746  
310 32         183 };
311 32         111 my $matrix = $data->{matrix};
312 32         52 my @masked;
313 32         95 my $eside_size = $data->{eside_size};
314 32         122 my $mask = $mask_for->{$mask_id};
315 32         103 for my $i (0 .. ($eside_size - 1)) {
316 1152         2076 for my $j (0 .. ($eside_size - 1)) {
317 44640 100 100     83942 if (($matrix->[$i][$j] >= 0x36) && $mask->($i - 4, $j - 4)) {
318 8680         15749 $masked[$i][$j] = $matrix->[$i][$j] ^ 0x01;
319             }
320             else {
321 35960         59493 $masked[$i][$j] = $matrix->[$i][$j];
322             }
323             }
324             }
325 32         146 return \@masked;
326             }
327              
328 4     4 1 8 sub add_mask ($data) {
  4         8  
  4         21  
329 4         10 my ($best_mask_id, $best_matrix, $best_penalty);
330 4         20 $data->{masked} = \my @masked;
331 4         17 for my $mask_id (0 .. 7) {
332 32         113 my $matrix = masked_matrix($data, $mask_id);
333 32         200 add_format($matrix, $data->{level}, $mask_id);
334 32         71 push @masked, $matrix;
335 32         112 my $penalty = evaluate_matrix($matrix);
336 32 100 100     248 ($best_mask_id, $best_matrix, $best_penalty) = ($mask_id, $matrix, $penalty)
337             if (! $best_matrix) || $penalty < $best_penalty;
338             }
339 4         20 $data->{original_matrix} = delete($data->{matrix});
340 4         16 $data->{matrix} = $best_matrix;
341 4         12 $data->{mask_id} = $best_mask_id;
342 4         12 return $data;
343             }
344              
345 32     32 1 66 sub add_format ($matrix, $level, $mask_id) {
  32         63  
  32         65  
  32         52  
  32         47  
346 32         218 my $fmt = qrspec_format_pattern($level, $mask_id);
347 32         71 my $es = $matrix->[0]->@*;
348              
349             # 1st copy
350 32         81 my $format = $fmt;
351 32         90 for my $i (0 .. 7) {
352 256 100       515 $matrix->[12][$es - 1 - 4 - $i] = $format & 0x01 ? 0x31 : 0x30;
353 256         407 $format >>= 1;
354             }
355 32         72 for my $i (8 .. 14) {
356 224 100       454 $matrix->[$es - 1 - 4 - 14 + $i][12] = $format & 0x01 ? 0x31 : 0x30;
357 224         348 $format >>= 1;
358             }
359              
360             # 2nd copy
361 32         54 $format = $fmt;
362 32         78 for my $i (0 .. 5) {
363 192 100       412 $matrix->[4 + $i][12] = $format & 0x01 ? 0x31 : 0x30;
364 192         265 $format >>= 1;
365             }
366 32         70 for my $i (6, 7) {
367 64 100       154 $matrix->[4 + 1 + $i][12] = $format & 0x01 ? 0x31 : 0x30;
368 64         103 $format >>= 1;
369             }
370             # 8
371 32 100       99 $matrix->[4 + 1 + 7][11] = $format & 0x01 ? 0x31 : 0x30;
372 32         56 $format >>= 1;
373 32         75 for my $i (9 .. 14) {
374 192 100       357 $matrix->[4 + 1 + 7][9 + 9 - $i] = $format & 0x01 ? 0x31 : 0x30;
375 192         256 $format >>= 1;
376             }
377              
378 32         68 return $matrix;
379             }
380              
381 4     4 1 8 sub add_version ($data) {
  4         11  
  4         11  
382 4   100     28 my $vp = qrspec_version_pattern($data->{version}) // return;
383 1         3 my $matrix = $data->{matrix};
384 1         4 my $ecstart = $data->{eside_size} - 4 - 7 - 4;
385 1         4 for my $i (4 .. 9) {
386 6         11 for my $j ($ecstart .. ($ecstart + 2)) {
387 18         34 $matrix->[$i][$j] = $matrix->[$j][$i] = 0x30 ^ ($vp & 1);
388 18         33 $vp >>= 1;
389             }
390             }
391 1         42 return $data;
392             }
393              
394             1;