File: //usr/share/webmin/vendor_perl/QRCode/Encoder/Matrix.pm
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;