Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multi Mode Support #25

Merged
merged 15 commits into from Aug 12, 2021
2 changes: 2 additions & 0 deletions lib/rqrcode_core/qrcode.rb
Expand Up @@ -9,3 +9,5 @@
require "rqrcode_core/qrcode/qr_polynomial"
require "rqrcode_core/qrcode/qr_rs_block"
require "rqrcode_core/qrcode/qr_util"
require "rqrcode_core/qrcode/multi_util"
require "rqrcode_core/qrcode/qr_multi"
44 changes: 44 additions & 0 deletions lib/rqrcode_core/qrcode/multi_util.rb
@@ -0,0 +1,44 @@
module RQRCodeCore
class MultiUtil

# http://web.archive.org/web/20110710094955/http://www.denso-wave.com/qrcode/vertable1-e.html
# http://web.archive.org/web/20110710094955/http://www.denso-wave.com/qrcode/vertable2-e.html
# http://web.archive.org/web/20110710094955/http://www.denso-wave.com/qrcode/vertable3-e.html
# http://web.archive.org/web/20110710094955/http://www.denso-wave.com/qrcode/vertable4-e.html
# Each array contains levels max bits from level 1 to level 40
QRMAXBITS = {
l: [152, 272, 440, 640, 864, 1088, 1248, 1552, 1856, 2192, 2592, 2960, 3424, 3688, 4184,
4712, 5176, 5768, 6360, 6888, 7456, 8048, 8752, 9392, 10208, 10960, 11744, 12248,
13048, 13880, 14744, 15640, 16568, 17528, 18448, 19472, 20528, 21616, 22496, 23648],
m: [128, 224, 352, 512, 688, 864, 992, 1232, 1456, 1728, 2032, 2320, 2672, 2920, 3320, 3624,
4056, 4504, 5016, 5352, 5712, 6256, 6880, 7312, 8000, 8496, 9024, 9544, 10136, 10984,
11640, 12328, 13048, 13800, 14496, 15312, 15936, 16816, 17728, 18672],
h: [72, 128, 208, 288, 368, 480, 528, 688, 800, 976, 1120, 1264, 1440, 1576, 1784,
2024, 2264, 2504, 2728, 3080, 3248, 3536, 3712, 4112, 4304, 4768, 5024, 5288, 5608, 5960,
6344, 6760, 7208, 7688, 7888, 8432, 8768, 9136, 9776, 10208]
}

def self.smallest_size_for_multi(data, level, max_version=40, min_version=1)
raise QRCodeArgumentError, 'Data too long for QR Code' if min_version > max_version

# Manually calculate max size
# rs_blocks = QRRSBlock.get_rs_blocks(min_version, QRERRORCORRECTLEVEL[level])
# max_size_bits = QRCode.count_max_data_bits(rs_blocks)

# Max size table
max_size_bits = QRMAXBITS[level][min_version - 1]

size_bits = data.reduce(0) do |total, segment|
ssayer marked this conversation as resolved.
Show resolved Hide resolved
mode = QRMODE[QRMODE_NAME[segment[:mode]]]
head_len = QRUtil.get_length_in_bits(mode, min_version)
data_len = segment[:data].bytesize * 8
total += (4 + head_len + data_len)
end

return min_version if size_bits < max_size_bits

smallest_size_for_multi(data, level, max_version, min_version+1)
end

end
end
47 changes: 29 additions & 18 deletions lib/rqrcode_core/qrcode/qr_code.rb
Expand Up @@ -10,7 +10,8 @@ module RQRCodeCore
QRMODE_NAME = {
number: :mode_number,
alphanumeric: :mode_alpha_numk,
byte_8bit: :mode_8bit_byte
byte_8bit: :mode_8bit_byte,
multi: :mode_multi
}.freeze

QRERRORCORRECTLEVEL = {
Expand Down Expand Up @@ -166,25 +167,35 @@ class QRCode
# * :alphanumeric
# * :byte_8bit
# * :kanji
# * :multi
#
# qr = RQRCodeCore::QRCode.new('hello world', size: 1, level: :m, mode: :alphanumeric)
#

def initialize(string, *args)
if !string.is_a? String

options = extract_options!(args)
mode = set_mode(options[:mode])

multi = mode == :mode_multi

if string.is_a?(Array) && multi
@data = string.map { |seg| seg.merge(mode: set_mode(seg[:mode])) }
elsif string.is_a?(Array)
raise QRCodeArgumentError, "Must explicitly declare {mode: :multi} when passed data is an Array"
elsif string.is_a? String
@data = string
else
raise QRCodeArgumentError, "The passed data is #{string.class}, not String"
end

options = extract_options!(args)
level = (options[:level] || :h).to_sym
max_size = options[:max_size] || QRUtil.max_size

if !QRERRORCORRECTLEVEL.has_key?(level)
ssayer marked this conversation as resolved.
Show resolved Hide resolved
raise QRCodeArgumentError, "Unknown error correction level `#{level.inspect}`"
end

@data = string

mode = QRMODE_NAME[(options[:mode] || "").to_sym]

# If mode is not explicitely given choose mode according to data type
mode ||= if RQRCodeCore::QRNumeric.valid_data?(@data)
QRMODE_NAME[:number]
Expand All @@ -195,30 +206,30 @@ def initialize(string, *args)
end

max_size_array = QRMAXDIGITS[level][mode]
size = options[:size] || smallest_size_for(string, max_size_array)

if size > QRUtil.max_size
#
size = options[:size] || (multi && MultiUtil.smallest_size_for_multi(string, level, max_size)) || smallest_size_for(@data, max_size_array)

if size > max_size
raise QRCodeArgumentError, "Given size greater than maximum possible size of #{QRUtil.max_size}"
end

@error_correct_level = QRERRORCORRECTLEVEL[level]
@version = size
@module_count = @version * 4 + QRPOSITIONPATTERNLENGTH
@modules = Array.new(@module_count)
@data_list =
case mode
when :mode_number
QRNumeric.new(@data)
when :mode_alpha_numk
QRAlphanumeric.new(@data)
else
QR8bitByte.new(@data)
end
@data_list = QRUtil.writer_for_mode(mode, @data)

@data_cache = nil
make
end

def set_mode(mode)
QRMODE_NAME[(mode || "").to_sym]
end



# <tt>checked?</tt> is called with a +col+ and +row+ parameter. This will
# return true or false based on whether that coordinate exists in the
# matrix returned. It would normally be called while iterating through
Expand Down
15 changes: 15 additions & 0 deletions lib/rqrcode_core/qrcode/qr_multi.rb
@@ -0,0 +1,15 @@
module RQRCodeCore

class QRMulti
def initialize(data)
@data = data
end

def write(buffer)
@data.each do |seg|
writer = QRUtil.writer_for_mode(seg[:mode], seg[:data])
writer.write(buffer)
end
end
end
end
13 changes: 13 additions & 0 deletions lib/rqrcode_core/qrcode/qr_util.rb
Expand Up @@ -65,6 +65,19 @@ def self.max_size
PATTERN_POSITION_TABLE.count
end

def self.writer_for_mode(mode, data)
case mode
when :mode_number
QRNumeric.new(data)
when :mode_alpha_numk
QRAlphanumeric.new(data)
when :mode_multi
QRMulti.new(data)
else
QR8bitByte.new(data)
end
end

def self.get_bch_format_info(data)
d = data << 10
while QRUtil.get_bch_digit(d) - QRUtil.get_bch_digit(G15) >= 0
Expand Down
25 changes: 25 additions & 0 deletions test/rqrcode_core/multi_test.rb
@@ -0,0 +1,25 @@
require "test_helper"

class RQRCodeCore::MultiTest < Minitest::Test

PAYLOAD = [{data: "byteencoded", mode: :byte_8bit}, {data: "1" * 500, mode: :number}, {data: "A1" * 100, mode: :alphanumeric}]

def test_multi_payloads
begin
RQRCodeCore::QRCode.new(PAYLOAD, mode: 'multi', level: :l)
RQRCodeCore::QRCode.new(PAYLOAD, mode: 'multi', level: :m)
RQRCodeCore::QRCode.new(PAYLOAD, mode: 'multi')
rescue => e
flunk(e)
ssayer marked this conversation as resolved.
Show resolved Hide resolved
end
end

def test_invalid_code_configs
assert_raises(RQRCodeCore::QRCodeArgumentError) {
RQRCodeCore::QRCode.new(PAYLOAD)
RQRCodeCore::QRCode.new("duncan", mode: 'multi')
RQRCodeCore::QRCode.new(:not_a_string_or_array)
}
end

end