File: //usr/share/webmin/vendor_perl/QRCode/Encoder.pm
package QRCode::Encoder;
use v5.24;
use warnings;
use experimental qw< signatures >;
{ our $VERSION = '0.005' }
use Math::ReedSolomon::Encoder qw< rs_correction_string >;
use QRCode::Encoder::QRSpec qw< :all >;
use QRCode::Encoder::Matrix qw< add_matrix >;
use Exporter qw< import >;
our @EXPORT_OK = qw<
qr_best_params
qr_encode
qr_mode
>;
our %EXPORT_TAGS = (all => [ @EXPORT_OK ]);
sub qr_mode ($octets) {
return 'numeric' if $octets =~ m{\A \d* \z}mxs;
return 'alphanumeric'
if $octets =~ m{\A [0-9A-Z\x20\x24\x25\x2a\x2b\x2d-\x2f\x3a]+ \z}mxs;
return 'kanji'
if $octets =~ m{\A
(?: # start of a pair
(?: [\x81-\x9f\xe0-\xea] [\x40-\x7e\x80-\xfc])
| (?: \xeb [\x40-\x7e\x80\xbf])
)+
\z}mxs;
return 'byte';
}
sub qr_best_params (@args) {
state $rank_for = { L => 1, M => 2, Q => 3, H => 4 };
my %args = scalar(@args) % 2 ? (octets => @args) : @args;
my $size = length($args{octets});
my $mode = $args{mode} // qr_mode($args{octets});
my $version = $args{version};
my $min_version = $args{'min-version'} // 1;
$version //= 40 if $min_version eq 40;
my $level = $args{level};
my $min_level = $args{min_level} // 'L';
$level //= 'H' if $min_level eq 'H';
if (defined($level)) {
my $minv = qrspec_min_version_for($mode, $size, $level)
or die "no suitable version for $mode/$size/$level";
if (defined($version)) { # just check
die "version $version insufficient for $mode/$size/$level"
if $version < $minv;
}
else {
my $req = $args{min_version} // 1;
$version = $minv < $req ? $req : $minv;
}
}
elsif (defined($version)) {
my $min_rank = $rank_for->{$min_level};
for my $candidate (qw< H Q M L >) {
last if $rank_for->{$candidate} < $min_rank;
my $minv = qrspec_min_version_for($mode, $size, $candidate);
if ($minv <= $version) {
$level = $candidate;
last;
}
}
die "no level for $mode/$size/$version (min rank: $min_rank)"
unless defined $level;
}
else { # nothing is defined, go for the smaller size
$level = $min_level;
my $min_rank = $rank_for->{$level};
my $minv = qrspec_min_version_for($mode, $size, $level);
my $req = $args{'min-version'} // 1;
$version = $minv <= $req ? $req : $minv;
for my $candidate (qw< L M Q H >) {
next if $rank_for->{$candidate} <= $min_rank;
my $altv = qrspec_min_version_for($mode, $size, $candidate);
last if $altv > $minv;
$level = $candidate;
}
}
return (
%args,
mode => $mode,
level => $level,
version => $version,
);
}
sub qr_encode (@args) {
my %args = qr_best_params(@args);
my $mode = $args{mode};
my $level = $args{level};
my $size = length($args{octets});
$args{version} //= qrspec_min_version_for($mode, $size, $level);
_add_encoded(\%args);
_add_codewords(\%args);
_add_error_correction(\%args);
add_matrix(\%args);
_add_plot(\%args);
return \%args;
}
sub _add_plot ($args) {
$args->{plot} = [
map { [ map { $_ & 0x01 ? '*' : ' ' } $_->@* ] } $args->{matrix}->@*
];
return $args;
}
sub _add_encoded ($args) {
state $encoder_for = {
numeric => \&_qr_encode_numeric,
alphanumeric => \&_qr_encode_alphanumeric,
byte => \&_qr_encode_byte,
kanji => \&_qr_encode_kanji,
};
my $mode = $args->{mode};
my $encoder = $encoder_for->{$mode} or die "missing mode <$mode>\n";
my $mi = qrspec_mode_indicator($mode);
my $version = $args->{version};
my $size = length($args->{octets});
my $lis = qrspec_length_indicator($mode, $args->{version});
my $li = _dec2bin(length($args->{octets}), $lis);
$args->{encoded} = $mi . $li . $encoder->($args->{octets});
return $args;
}
sub _add_codewords ($args) {
my $bit_stream = $args->{encoded};
my $data_size = qrspec_data_size($args->@{qw< version level >});
my $needed_bits = length($bit_stream);
my $residual_bits = 8 * $data_size - $needed_bits;
die "not enough bits, wrong version?\n" if $residual_bits < 0;
my $terminator_size = $residual_bits >= 4 ? 4 : $residual_bits;
$bit_stream .= '0' x $terminator_size;
$residual_bits -= $terminator_size;
if (my $pad1 = $residual_bits % 8) {
$bit_stream .= '0' x $pad1;
$residual_bits -= $pad1;
}
while ($residual_bits > 0) {
$bit_stream .= '11101100';
last if $residual_bits == 8;
$bit_stream .= '00010001';
$residual_bits -= 16;
}
$args->{bit_stream} = $bit_stream;
$args->{codewords} = pack 'B*', $bit_stream;
return $args;
}
sub _add_error_correction ($args) {
my @blocks = qrspec_ecc_spec($args->@{qw< version level >});
$args->{ecc} = \@blocks;
my $expanded = '';
my $codewords = $args->{codewords};
my $i = 0;
my (@codewords, @eccs);
for my $block (@blocks) {
my ($ecc, $data, $count) = $block->@{qw< ecc data count >};
while ($count-- > 0) {
my $cw = substr($codewords, $i, $data);
push @codewords, $cw;
push @eccs, rs_correction_string($cw, $ecc);
$i += $data;
}
}
$args->{expanded} = _linearize(\@codewords) . _linearize(\@eccs);
$args->{remainder} = qrspec_remainder($args->{version});
return $args;
}
sub _linearize ($strings) {
return $strings->[0] if $strings->@* == 1;
my $retval = '';
my $i = 0;
my $n = length($strings->[-1]);
while ($i < $n) {
for my $string ($strings->@*) {
next if $i >= length($string);
$retval .= substr($string, $i, 1);
}
++$i;
}
return $retval;
}
sub _dec2bin ($v, $n) { substr(unpack('B*', pack('N', $v)), -$n, $n) }
sub _qr_encode_numeric ($octets) {
state $n_bits_for = [ 4, 7, 10 ];
my $i = 0; # index of start of substr, advanced each iteration
my $r = length($octets); # number of residual octets to take
my $bits = '';
while ($r > 0) {
my $l = $r >= 3 ? 3 : $r;
$bits .= _dec2bin(substr($octets, $i, $l), $n_bits_for->[$l - 1]);
$r -= $l;
$i += $l;
}
return $bits;
}
sub _qr_encode_alphanumeric ($octets) {
state $chars = [ 0 .. 9, 'A' .. 'Z', split //, ' $%*+-./:' ];
state $value_for = { map { $chars->[$_] => $_ } 0 .. $chars->$#* };
my $i = 0; # index of start of substr, advanced each iteration
my $r = length($octets); # number of residual octets to take
my $bits = '';
while ($r > 0) {
if ($r == 1) {
$bits .= _dec2bin($value_for->{substr($octets, $i, 1)}, 6);
$r = 0;
}
else {
my $value = $value_for->{substr($octets, $i++, 1)} * 45;
$value += $value_for->{substr($octets, $i++, 1)};
$bits .= _dec2bin($value, 11);
$r -= 2;
}
}
return $bits;
}
sub _qr_encode_kanji ($octets) {
my $i = 0;
my $r = length($octets);
my $bits = '';
while ($r > 0) {
my $v = unpack('n', substr($octets, $i, 2));
$v -= ($v <= 0x9FFC) ? 0x8140 : 0xC140;
$v = ($v >> 8) * 0xC0 + ($v & 0xFF);
$bits .= _dec2bin($v, 13);
$r -= 2;
$i += 2;
}
return $bits;
}
sub _qr_encode_byte ($octets) { unpack 'B*', $octets }
1;