package QRCode::Encoder::Matrix; use v5.24; use experimental qw< signatures >; use List::Util qw< sum >; use QRCode::Encoder::QRSpec qw< qrspec_version_pattern qrspec_format_pattern qrspec_alignment_patterns >; use Exporter qw< import >; our @EXPORT_OK = qw< add_matrix >; # Parts liberally taken from libqrencode/qrspec.c, which is distributed # with LGPL license sub add_matrix ($data) { add_base_matrix($data); add_quiet($data); add_finders($data); add_format_reservations($data); # MUST: before add_timing add_version($data); add_timing($data); add_alignments($data); add_codewords($data); add_mask($data); return $data; } sub stringify_matrix ($data) { my @chunks; my $matrix = $data->{matrix}; for my $row ($matrix->@*) { push @chunks, join '', map { chr($_) } $row->@*; } return join "\n", @chunks; } sub stringify_matrix_2 ($data) { my @chunks; my $matrix = $data->{matrix}; for my $row ($matrix->@*) { push @chunks, join '', map { ($_ & 0x1) ? '#' : ' ' } $row->@*; } return join "\n", @chunks; } sub add_base_matrix ($data) { my $side = $data->{side_size} = 17 + 4 * $data->{version}; my $eside = $data->{eside_size} = $side + 8; $data->{matrix} = [ map { [ ( 0x38 ) x $eside ] } 1 .. $eside ]; return $data; } sub add_finders ($data) { my $eside_size = $data->{eside_size}; add_finder($data, 4 - 1, 4 - 1); add_finder($data, 4 - 1, $eside_size - 8 - 4); add_finder($data, $eside_size - 8 - 4, 4 - 1); return $data; } sub add_quiet ($data) { my $es = $data->{eside_size}; my $matrix = $data->{matrix}; for my $i (0 .. 3) { for my $j (0 .. $es - 1) { $matrix->[$i][$j] = $matrix->[$es - 1 - $i][$j] = $matrix->[$j][$i] = $matrix->[$j][$es - 1 - $i] = 0x30; } } return $data; } sub add_finder ($data, $x, $y) { state $shape = [ [ 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30 ], [ 0x30, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x30 ], [ 0x30, 0x31, 0x30, 0x30, 0x30, 0x30, 0x30, 0x31, 0x30 ], [ 0x30, 0x31, 0x30, 0x31, 0x31, 0x31, 0x30, 0x31, 0x30 ], [ 0x30, 0x31, 0x30, 0x31, 0x31, 0x31, 0x30, 0x31, 0x30 ], [ 0x30, 0x31, 0x30, 0x31, 0x31, 0x31, 0x30, 0x31, 0x30 ], [ 0x30, 0x31, 0x30, 0x30, 0x30, 0x30, 0x30, 0x31, 0x30 ], [ 0x30, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x30 ], [ 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30 ], ]; my $matrix = $data->{matrix}; for my $yoff (0 .. 8) { my $Y = $y + $yoff; for my $xoff (0 .. 8) { my $X = $x + $xoff; $matrix->[$Y][$X] = $shape->[$yoff][$xoff]; } } return $data; } sub add_format_reservations ($data) { my $matrix = $data->{matrix}; my $es = $data->{eside_size}; for my $i (0 .. 7) { $matrix->[12][$i + 4] = $matrix->[12][$i + $es - 4 - 8] = $matrix->[$i + $es - 4 - 8][12] = $matrix->[$i + 4][12] = 0x32; } $matrix->[12][12] = 0x32; $matrix->[$es - 4 - 8][12] = 0x31; return $data; } sub add_version_reservations ($data) { return $data->{version} <= 6; my $matrix = $data->{matrix}; my $ecstart = $data->{eside_size} - 4 - 7 - 4; for my $i (4 .. 9) { for my $j ($ecstart .. ($ecstart + 2)) { $matrix->[$i][$j] = $matrix->[$j][$i] = 0x32; } } return $data; } sub add_timing ($data) { my $matrix = $data->{matrix}; my $es = $data->{eside_size}; for my $i (12 .. ($es - 4 - 8 - 1)) { $matrix->[$i][10] = $matrix->[10][$i] = 0x35 ^ ($i & 1); } return $data; } sub try_add_alignment_pattern ($data, $x, $y) { state $shape = [ [ 0x31, 0x31, 0x31, 0x31, 0x31 ], [ 0x31, 0x30, 0x30, 0x30, 0x31 ], [ 0x31, 0x30, 0x31, 0x30, 0x31 ], [ 0x31, 0x30, 0x30, 0x30, 0x31 ], [ 0x31, 0x31, 0x31, 0x31, 0x31 ], ]; $x += 4; # offset by quiet zone $y += 4; # offset by quiet zone my $matrix = $data->{matrix}; return if $matrix->[$y][$x] < 0x34 || $matrix->[$y][$x + 4] < 0x34 || $matrix->[$y + 4][$x] < 0x34; for my $i (0 .. 4) { for my $j (0 .. 4) { $matrix->[$y + $i][$x + $j] = $shape->[$i][$j]; } } } sub add_alignments ($data) { my @offset = qrspec_alignment_patterns($data->{version}); for my $y_center (@offset) { for my $x_center (@offset) { try_add_alignment_pattern($data, $x_center - 2, $y_center - 2); } } return $data; } sub bits_iterator ($data) { my $n_expanded = length($data->{expanded}); my $rem = $data->{remainder}; my $i = 0; my @queue; return sub { if (! @queue) { if ($i < $n_expanded) { push @queue, split m{}mxs, unpack 'B*', substr($data->{expanded}, $i++, 1); } else { push @queue, ('0') x $rem; $rem = 0; } } return shift(@queue); }; } sub add_codewords ($data) { my $it = bits_iterator($data); my $matrix = $data->{matrix}; my $side_size = $data->{side_size}; # start from a fake position that would be the last bit of a # hypothetical "-1" codeword my $x = $side_size - 2; my $y = $side_size; my $left = 1; my $d = -1; # direction while (defined(my $bit = $it->())) { while ('necessary') { if ($x % 2 == $left) { ++$x; $y += $d; } else { --$x; } if ($d < 0 && $y < 0) { # reset condition $x -= 2; $y = 0; $d = 1; } elsif ($d > 0 && $y >= $side_size) { # other reset condition $x -= 2; $y = $side_size - 1; $d = -1; } if ($x == 6) { # left timing column, skip a column entirely $x = 5; $left = 0; } last if $matrix->[$y + 4][$x + 4] > 0x37; # found suitable position } $matrix->[$y + 4][$x + 4] = $bit ? 0x37 : 0x36; } return $data; } sub evaluate_matrix ($matrix) { return 0 + evaluate_matrix_adjacents_and_11311($matrix) + evaluate_matrix_blocks($matrix) + evaluate_matrix_proportion($matrix); } sub __row ($matrix, $i) { my $max_idx = $matrix->[0]->$#* - 4; join('', map { $matrix->[$i + 4][$_] & 0x01 ? 1 : 0 } 4 .. $max_idx); } sub __col ($matrix, $i) { my $max_idx = $matrix->[0]->$#* - 4; join('', map { $matrix->[$_][$i + 4] & 0x01 ? 1 : 0 } 4 .. $max_idx); } sub evaluate_matrix_adjacents_and_11311 ($matrix) { my $side_size = $matrix->[0]->@* - 8; my $penalty = 0; my $penalty2 = 0; for my $i (0 .. ($side_size - 1)) { for my $seq (__row($matrix, $i), __col($matrix, $i)) { # adjacences my @contributions = map { $_ - 2 } grep { $_ >= 5 } map { length } split m{(0+)}mxs, $seq; $penalty += sum(@contributions) if @contributions; # 000011311 | 113110000 my @matches = $seq =~ m{ ( (?: (?<=0000) 1011101 ) # look behind... | (?: 1011101 (?=0000) ) # or look ahead ) }gmxs; $penalty2 += 40 * scalar(@matches); } } return $penalty + $penalty2; } sub evaluate_matrix_blocks ($matrix) { my $side_size = $matrix->[0]->@* - 8; my $penalty = 0; for my $i (0 .. ($side_size - 2)) { for my $j (0 .. ($side_size - 2)) { my $count = 0; for my $offset ([0, 0], [0, 1], [1, 0], [1, 1]) { my ($oi, $oj) = $offset->@*; $count++ if $matrix->[$i + $oi + 4][$j + $oj + 4] & 1; } $penalty += 3 if ($count == 0) || ($count == 4); } } return $penalty; } sub evaluate_matrix_proportion ($matrix) { my $count = sum( map { map { $_ & 0x1 ? 1 : 0 } $_->@* } $matrix->@* ); my $side_size = $matrix->[0]->@* - 8; my $total = $side_size * $side_size; my $percentage = 100 * $count / $total; my $deviation = abs($percentage - 50); my $penalty = 10 * int($deviation / 5); return $penalty; } sub masked_matrix ($data, $mask_id) { state $mask_for = { 0 => sub ($i, $j) { (($i + $j) % 2) == 0 }, 1 => sub ($i, $j) { ($i % 2) == 0 }, 2 => sub ($i, $j) { ($j % 3) == 0 }, 3 => sub ($i, $j) { (($i + $j) % 3) == 0 }, 4 => sub ($i, $j) { ((int($i / 2) + int($j / 3)) % 2) == 0 }, 5 => sub ($i, $j) { ((($i * $j) % 2) + (($i * $j) % 3)) == 0 }, 6 => sub ($i, $j) { (((($i * $j) % 2) + (($i * $j) % 3)) % 2) == 0 }, 7 => sub ($i, $j) { (((($i + $j) % 2) + (($i * $j) % 3)) % 2) == 0 }, }; my $matrix = $data->{matrix}; my @masked; my $eside_size = $data->{eside_size}; my $mask = $mask_for->{$mask_id}; for my $i (0 .. ($eside_size - 1)) { for my $j (0 .. ($eside_size - 1)) { if (($matrix->[$i][$j] >= 0x36) && $mask->($i - 4, $j - 4)) { $masked[$i][$j] = $matrix->[$i][$j] ^ 0x01; } else { $masked[$i][$j] = $matrix->[$i][$j]; } } } return \@masked; } sub add_mask ($data) { my ($best_mask_id, $best_matrix, $best_penalty); $data->{masked} = \my @masked; for my $mask_id (0 .. 7) { my $matrix = masked_matrix($data, $mask_id); add_format($matrix, $data->{level}, $mask_id); push @masked, $matrix; my $penalty = evaluate_matrix($matrix); ($best_mask_id, $best_matrix, $best_penalty) = ($mask_id, $matrix, $penalty) if (! $best_matrix) || $penalty < $best_penalty; } $data->{original_matrix} = delete($data->{matrix}); $data->{matrix} = $best_matrix; $data->{mask_id} = $best_mask_id; return $data; } sub add_format ($matrix, $level, $mask_id) { my $fmt = qrspec_format_pattern($level, $mask_id); my $es = $matrix->[0]->@*; # 1st copy my $format = $fmt; for my $i (0 .. 7) { $matrix->[12][$es - 1 - 4 - $i] = $format & 0x01 ? 0x31 : 0x30; $format >>= 1; } for my $i (8 .. 14) { $matrix->[$es - 1 - 4 - 14 + $i][12] = $format & 0x01 ? 0x31 : 0x30; $format >>= 1; } # 2nd copy $format = $fmt; for my $i (0 .. 5) { $matrix->[4 + $i][12] = $format & 0x01 ? 0x31 : 0x30; $format >>= 1; } for my $i (6, 7) { $matrix->[4 + 1 + $i][12] = $format & 0x01 ? 0x31 : 0x30; $format >>= 1; } # 8 $matrix->[4 + 1 + 7][11] = $format & 0x01 ? 0x31 : 0x30; $format >>= 1; for my $i (9 .. 14) { $matrix->[4 + 1 + 7][9 + 9 - $i] = $format & 0x01 ? 0x31 : 0x30; $format >>= 1; } return $matrix; } sub add_version ($data) { my $vp = qrspec_version_pattern($data->{version}) // return; my $matrix = $data->{matrix}; my $ecstart = $data->{eside_size} - 4 - 7 - 4; for my $i (4 .. 9) { for my $j ($ecstart .. ($ecstart + 2)) { $matrix->[$i][$j] = $matrix->[$j][$i] = 0x30 ^ ($vp & 1); $vp >>= 1; } } return $data; } 1;