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 @@ -3,9 +3,11 @@
require "rqrcode_core/qrcode/qr_8bit_byte"
require "rqrcode_core/qrcode/qr_alphanumeric"
require "rqrcode_core/qrcode/qr_bit_buffer"
require "rqrcode_core/qrcode/qr_segment"
require "rqrcode_core/qrcode/qr_code"
require "rqrcode_core/qrcode/qr_math"
require "rqrcode_core/qrcode/qr_numeric"
require "rqrcode_core/qrcode/qr_polynomial"
require "rqrcode_core/qrcode/qr_rs_block"
require "rqrcode_core/qrcode/qr_util"
require "rqrcode_core/qrcode/qr_multi"
104 changes: 68 additions & 36 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 @@ -134,6 +135,21 @@ module RQRCodeCore
}
}.freeze

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, 10_208, 10_960, 11_744, 12_248,
13_048, 13_880, 14_744, 15_640, 16_568, 17_528, 18_448, 19_472, 20_528, 21_616, 22_496, 23_648],
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, 10_136, 10_984,
11_640, 12_328, 13_048, 13_800, 14_496, 15_312, 15_936, 16_816, 17_728, 18_672],
q: [104, 176, 272, 384, 496, 608, 704, 880, 1056, 1232, 1440, 1648, 1952, 2088, 2360, 2600, 2936,
3176, 3560, 3880, 4096, 4544, 4912, 5312, 5744, 6032, 6464, 6968, 7288, 7880, 8264, 8920, 9368,
9848, 10288, 10832, 11408, 12016, 12656, 13328],
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, 10_208]
}.freeze

# StandardErrors

class QRCodeArgumentError < ArgumentError; end
Expand All @@ -152,69 +168,59 @@ class QRCodeRunTimeError < RuntimeError; end
class QRCode
attr_reader :modules, :module_count, :version

# Expects a string to be parsed in, other args are optional
# Expects a string or array (for multi-segment encoding) to be parsed in, other args are optional
#
# # string - the string you wish to encode
# # size - the size (Integer) of the qrcode (defaults to smallest size needed to encode the string)
# # data - the string, QRSegment or array of QRSegments you wish to encode
# # size - the size (Integer) of the qrcode (defaults to smallest size needed to encode the data)
# # level - the error correction level, can be:
# * Level :l 7% of code can be restored
# * Level :m 15% of code can be restored
# * Level :q 25% of code can be restored
# * Level :h 30% of code can be restored (default :h)
# # mode - the mode of the qrcode (defaults to alphanumeric or byte_8bit, depending on the input data):
# # mode - the mode of the qrcode (defaults to alphanumeric or byte_8bit, depending on the input data, only used when data is a string):
# * :number
# * :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
raise QRCodeArgumentError, "The passed data is #{string.class}, not String"
end
# segment_qr = QRCodeCore::QRCode.new(QRSegment.new(data: 'foo', mode: :byte_8bit))
# multi_qr = RQRCodeCore::QRCode.new([QRSegment.new(data: 'foo', mode: :byte_8bit), QRSegment.new(data: 'bar1', mode: :alphanumeric)])

def initialize(data, *args)
options = extract_options!(args)

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

if !QRERRORCORRECTLEVEL.has_key?(level)
raise QRCodeArgumentError, "Unknown error correction level `#{level.inspect}`"
@data = case data
when String
QRSegment.new(data: data, mode: options[:mode])
when Array
raise QRCodeArgumentError, "Array must contain QRSegments" if data.find { |seg| !seg.is_a?(QRSegment) }
data
ssayer marked this conversation as resolved.
Show resolved Hide resolved
when QRSegment
data
else
raise QRCodeArgumentError, "data must be a String, QRSegment, or Array of QRSegments"
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]
elsif QRAlphanumeric.valid_data?(@data)
QRMODE_NAME[:alphanumeric]
else
QRMODE_NAME[:byte_8bit]
if !QRERRORCORRECTLEVEL.has_key?(level)
ssayer marked this conversation as resolved.
Show resolved Hide resolved
raise QRCodeArgumentError, "Unknown error correction level `#{level.inspect}`"
end

max_size_array = QRMAXDIGITS[level][mode]
size = options[:size] || smallest_size_for(string, max_size_array)
size = options[:size] || (multi_segment? && smallest_size_for_multi(data: @data, level: level, max_version: max_size)) || smallest_size_for(@data.data, QRMAXDIGITS[level][@data.mode])

if size > QRUtil.max_size
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 = multi_segment? ? QRMulti.new(@data) : @data.writer
@data_cache = nil
make
end
Expand Down Expand Up @@ -293,6 +299,11 @@ def error_correction_level
QRERRORCORRECTLEVEL.invert[@error_correct_level]
end

# Return true if this QR code includes multiple encoded segments
def multi_segment?
ssayer marked this conversation as resolved.
Show resolved Hide resolved
@data.is_a?(Array)
end

# Return a symbol in QRMODE.keys for current mode used
def mode
case @data_list
Expand Down Expand Up @@ -485,6 +496,27 @@ def smallest_size_for(string, max_size_array) #:nodoc:
ver + 1
end

def 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)
ssayer marked this conversation as resolved.
Show resolved Hide resolved

whomwah marked this conversation as resolved.
Show resolved Hide resolved
# Max size table
max_size_bits = QRMAXBITS[level][min_version - 1]

size_bits = data.reduce(0) do |total, segment|
mode = QRMODE[segment.mode]

total + 4 + QRUtil.get_length_in_bits(mode, min_version) + segment.bit_size
end

return min_version if size_bits < max_size_bits

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

def extract_options!(arr) #:nodoc:
arr.last.is_a?(::Hash) ? arr.pop : {}
end
Expand Down
11 changes: 11 additions & 0 deletions lib/rqrcode_core/qrcode/qr_multi.rb
@@ -0,0 +1,11 @@
module RQRCodeCore
class QRMulti
def initialize(data)
@data = data
end

def write(buffer)
@data.each { |seg| seg.writer.write(buffer) }
end
end
end
2 changes: 1 addition & 1 deletion lib/rqrcode_core/qrcode/qr_numeric.rb
Expand Up @@ -26,7 +26,7 @@ def write(buffer)
end
end

private
protected

ssayer marked this conversation as resolved.
Show resolved Hide resolved
NUMBER_LENGTH = {
3 => 10,
Expand Down
53 changes: 53 additions & 0 deletions lib/rqrcode_core/qrcode/qr_segment.rb
@@ -0,0 +1,53 @@
module RQRCodeCore
class QRSegment
attr_reader :data, :mode

def initialize(data:, mode: nil)
@data = data
if mode
@mode = QRMODE_NAME[(mode || "").to_sym]
else
# If mode is not explicitely given choose mode according to data type
@mode ||= if RQRCodeCore::QRNumeric.valid_data?(@data)
QRMODE_NAME[:number]
elsif QRAlphanumeric.valid_data?(@data)
QRMODE_NAME[:alphanumeric]
else
QRMODE_NAME[:byte_8bit]
end
end
end

ssayer marked this conversation as resolved.
Show resolved Hide resolved
def bit_size
chunk_size, bit_length, extra = case mode
when :mode_number
[3, QRNumeric::NUMBER_LENGTH[3], QRNumeric::NUMBER_LENGTH[data_length % 3] || 0]
when :mode_alpha_numk
[2, 11, 6]
when :mode_8bit_byte
[1, 8, 0]
end

(data_length / chunk_size) * bit_length + ((data_length % chunk_size) == 0 ? 0 : extra)
end

def writer
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

private

def data_length
data.length
end
end
end
26 changes: 26 additions & 0 deletions test/rqrcode_core/qr_segment_test.rb
@@ -0,0 +1,26 @@
# frozen_string_literal: true

ssayer marked this conversation as resolved.
Show resolved Hide resolved
require "test_helper"

class RQRCodeCore::QRSegmentTest < Minitest::Test
PAYLOAD = [{data: "byteencoded", mode: :byte_8bit}, {data: "A1" * 107, mode: :alphanumeric}, {data: "1" * 498, mode: :number}].map do |seg|
RQRCodeCore::QRSegment.new(**seg)
end

def test_multi_payloads
RQRCodeCore::QRCode.new(PAYLOAD, level: :l)
RQRCodeCore::QRCode.new(PAYLOAD, level: :m)
RQRCodeCore::QRCode.new(PAYLOAD, level: :q)
RQRCodeCore::QRCode.new(PAYLOAD)
RQRCodeCore::QRCode.new(PAYLOAD, level: :l, max_size: 22)
# rescue => e
# flunk(e)
end
ssayer marked this conversation as resolved.
Show resolved Hide resolved

def test_invalid_code_configs
assert_raises(RQRCodeCore::QRCodeArgumentError) {
RQRCodeCore::QRCode.new(:not_a_string_or_array)
RQRCodeCore::QRCode.new(PAYLOAD << :not_a_segment)
}
end
end