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;