qrcode 0.0.1 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,318 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2008-2021, by Duncan Robertson.
5
+ # Copyright, 2011, by Gioele Barabucci.
6
+ # Copyright, 2015, by Björn Blomqvist.
7
+ # Copyright, 2025, by Samuel Williams.
8
+
9
+ require_relative "constants"
10
+
11
+ module QRCode
12
+ module Encoder
13
+ class ErrorCorrectionBlock
14
+ attr_reader :data_count, :total_count
15
+
16
+ def initialize(total_count, data_count)
17
+ @total_count = total_count
18
+ @data_count = data_count
19
+ end
20
+
21
+ # http://www.thonky.com/qr-code-tutorial/error-correction-table/
22
+ # Table of [block_count, total_codewords, data_codewords] for each version and error level
23
+ TABLE = [
24
+ # The order within each group is:
25
+ # L
26
+ # M
27
+ # Q
28
+ # H
29
+
30
+ # 1
31
+ [1, 26, 19],
32
+ [1, 26, 16],
33
+ [1, 26, 13],
34
+ [1, 26, 9],
35
+
36
+ # 2
37
+ [1, 44, 34],
38
+ [1, 44, 28],
39
+ [1, 44, 22],
40
+ [1, 44, 16],
41
+
42
+ # 3
43
+ [1, 70, 55],
44
+ [1, 70, 44],
45
+ [2, 35, 17],
46
+ [2, 35, 13],
47
+
48
+ # 4
49
+ [1, 100, 80],
50
+ [2, 50, 32],
51
+ [2, 50, 24],
52
+ [4, 25, 9],
53
+
54
+ # 5
55
+ [1, 134, 108],
56
+ [2, 67, 43],
57
+ [2, 33, 15, 2, 34, 16],
58
+ [2, 33, 11, 2, 34, 12],
59
+
60
+ # 6
61
+ [2, 86, 68],
62
+ [4, 43, 27],
63
+ [4, 43, 19],
64
+ [4, 43, 15],
65
+
66
+ # 7
67
+ [2, 98, 78],
68
+ [4, 49, 31],
69
+ [2, 32, 14, 4, 33, 15],
70
+ [4, 39, 13, 1, 40, 14],
71
+
72
+ # 8
73
+ [2, 121, 97],
74
+ [2, 60, 38, 2, 61, 39],
75
+ [4, 40, 18, 2, 41, 19],
76
+ [4, 40, 14, 2, 41, 15],
77
+
78
+ # 9
79
+ [2, 146, 116],
80
+ [3, 58, 36, 2, 59, 37],
81
+ [4, 36, 16, 4, 37, 17],
82
+ [4, 36, 12, 4, 37, 13],
83
+
84
+ # 10
85
+ [2, 86, 68, 2, 87, 69],
86
+ [4, 69, 43, 1, 70, 44],
87
+ [6, 43, 19, 2, 44, 20],
88
+ [6, 43, 15, 2, 44, 16],
89
+
90
+ # 11
91
+ [4, 101, 81],
92
+ [1, 80, 50, 4, 81, 51],
93
+ [4, 50, 22, 4, 51, 23],
94
+ [3, 36, 12, 8, 37, 13],
95
+
96
+ # 12
97
+ [2, 116, 92, 2, 117, 93],
98
+ [6, 58, 36, 2, 59, 37],
99
+ [4, 46, 20, 6, 47, 21],
100
+ [7, 42, 14, 4, 43, 15],
101
+
102
+ # 13
103
+ [4, 133, 107],
104
+ [8, 59, 37, 1, 60, 38],
105
+ [8, 44, 20, 4, 45, 21],
106
+ [12, 33, 11, 4, 34, 12],
107
+
108
+ # 14
109
+ [3, 145, 115, 1, 146, 116],
110
+ [4, 64, 40, 5, 65, 41],
111
+ [11, 36, 16, 5, 37, 17],
112
+ [11, 36, 12, 5, 37, 13],
113
+
114
+ # 15
115
+ [5, 109, 87, 1, 110, 88],
116
+ [5, 65, 41, 5, 66, 42],
117
+ [5, 54, 24, 7, 55, 25],
118
+ [11, 36, 12, 7, 37, 13],
119
+
120
+ # 16
121
+ [5, 122, 98, 1, 123, 99],
122
+ [7, 73, 45, 3, 74, 46],
123
+ [15, 43, 19, 2, 44, 20],
124
+ [3, 45, 15, 13, 46, 16],
125
+
126
+ # 17
127
+ [1, 135, 107, 5, 136, 108],
128
+ [10, 74, 46, 1, 75, 47],
129
+ [1, 50, 22, 15, 51, 23],
130
+ [2, 42, 14, 17, 43, 15],
131
+
132
+ # 18
133
+ [5, 150, 120, 1, 151, 121],
134
+ [9, 69, 43, 4, 70, 44],
135
+ [17, 50, 22, 1, 51, 23],
136
+ [2, 42, 14, 19, 43, 15],
137
+
138
+ # 19
139
+ [3, 141, 113, 4, 142, 114],
140
+ [3, 70, 44, 11, 71, 45],
141
+ [17, 47, 21, 4, 48, 22],
142
+ [9, 39, 13, 16, 40, 14],
143
+
144
+ # 20
145
+ [3, 135, 107, 5, 136, 108],
146
+ [3, 67, 41, 13, 68, 42],
147
+ [15, 54, 24, 5, 55, 25],
148
+ [15, 43, 15, 10, 44, 16],
149
+
150
+ # 21
151
+ [4, 144, 116, 4, 145, 117],
152
+ [17, 68, 42],
153
+ [17, 50, 22, 6, 51, 23],
154
+ [19, 46, 16, 6, 47, 17],
155
+
156
+ # 22
157
+ [2, 139, 111, 7, 140, 112],
158
+ [17, 74, 46],
159
+ [7, 54, 24, 16, 55, 25],
160
+ [34, 37, 13],
161
+
162
+ # 23
163
+ [4, 151, 121, 5, 152, 122],
164
+ [4, 75, 47, 14, 76, 48],
165
+ [11, 54, 24, 14, 55, 25],
166
+ [16, 45, 15, 14, 46, 16],
167
+
168
+ # 24
169
+ [6, 147, 117, 4, 148, 118],
170
+ [6, 73, 45, 14, 74, 46],
171
+ [11, 54, 24, 16, 55, 25],
172
+ [30, 46, 16, 2, 47, 17],
173
+
174
+ # 25
175
+ [8, 132, 106, 4, 133, 107],
176
+ [8, 75, 47, 13, 76, 48],
177
+ [7, 54, 24, 22, 55, 25],
178
+ [22, 45, 15, 13, 46, 16],
179
+
180
+ # 26
181
+ [10, 142, 114, 2, 143, 115],
182
+ [19, 74, 46, 4, 75, 47],
183
+ [28, 50, 22, 6, 51, 23],
184
+ [33, 46, 16, 4, 47, 17],
185
+
186
+ # 27
187
+ [8, 152, 122, 4, 153, 123],
188
+ [22, 73, 45, 3, 74, 46],
189
+ [8, 53, 23, 26, 54, 24],
190
+ [12, 45, 15, 28, 46, 16],
191
+
192
+ # 28
193
+ [3, 147, 117, 10, 148, 118],
194
+ [3, 73, 45, 23, 74, 46],
195
+ [4, 54, 24, 31, 55, 25],
196
+ [11, 45, 15, 31, 46, 16],
197
+
198
+ # 29
199
+ [7, 146, 116, 7, 147, 117],
200
+ [21, 73, 45, 7, 74, 46],
201
+ [1, 53, 23, 37, 54, 24],
202
+ [19, 45, 15, 26, 46, 16],
203
+
204
+ # 30
205
+ [5, 145, 115, 10, 146, 116],
206
+ [19, 75, 47, 10, 76, 48],
207
+ [15, 54, 24, 25, 55, 25],
208
+ [23, 45, 15, 25, 46, 16],
209
+
210
+ # 31
211
+ [13, 145, 115, 3, 146, 116],
212
+ [2, 74, 46, 29, 75, 47],
213
+ [42, 54, 24, 1, 55, 25],
214
+ [23, 45, 15, 28, 46, 16],
215
+
216
+ # 32
217
+ [17, 145, 115],
218
+ [10, 74, 46, 23, 75, 47],
219
+ [10, 54, 24, 35, 55, 25],
220
+ [19, 45, 15, 35, 46, 16],
221
+
222
+ # 33
223
+ [17, 145, 115, 1, 146, 116],
224
+ [14, 74, 46, 21, 75, 47],
225
+ [29, 54, 24, 19, 55, 25],
226
+ [11, 45, 15, 46, 46, 16],
227
+
228
+ # 34
229
+ [13, 145, 115, 6, 146, 116],
230
+ [14, 74, 46, 23, 75, 47],
231
+ [44, 54, 24, 7, 55, 25],
232
+ [59, 46, 16, 1, 47, 17],
233
+
234
+ # 35
235
+ [12, 151, 121, 7, 152, 122],
236
+ [12, 75, 47, 26, 76, 48],
237
+ [39, 54, 24, 14, 55, 25],
238
+ [22, 45, 15, 41, 46, 16],
239
+
240
+ # 36
241
+ [6, 151, 121, 14, 152, 122],
242
+ [6, 75, 47, 34, 76, 48],
243
+ [46, 54, 24, 10, 55, 25],
244
+ [2, 45, 15, 64, 46, 16],
245
+
246
+ # 37
247
+ [17, 152, 122, 4, 153, 123],
248
+ [29, 74, 46, 14, 75, 47],
249
+ [49, 54, 24, 10, 55, 25],
250
+ [24, 45, 15, 46, 46, 16],
251
+
252
+ # 38
253
+ [4, 152, 122, 18, 153, 123],
254
+ [13, 74, 46, 32, 75, 47],
255
+ [48, 54, 24, 14, 55, 25],
256
+ [42, 45, 15, 32, 46, 16],
257
+
258
+ # 39
259
+ [20, 147, 117, 4, 148, 118],
260
+ [40, 75, 47, 7, 76, 48],
261
+ [43, 54, 24, 22, 55, 25],
262
+ [10, 45, 15, 67, 46, 16],
263
+
264
+ # 40
265
+ [19, 148, 118, 6, 149, 119],
266
+ [18, 75, 47, 31, 76, 48],
267
+ [34, 54, 24, 34, 55, 25],
268
+ [20, 45, 15, 61, 46, 16]
269
+ ].freeze
270
+
271
+ # Create error correction blocks for given version and level
272
+ # @parameter version [Integer] QR code version (1-40)
273
+ # @parameter level [Integer] Error correction level
274
+ # @return [Array<ErrorCorrectionBlock>] Array of error correction blocks
275
+ def self.for(version, level)
276
+ entry = table_entry_for(version, level)
277
+
278
+ if entry.nil?
279
+ raise RuntimeError, "Invalid error correction configuration: version=#{version}, level=#{level}"
280
+ end
281
+
282
+ blocks = []
283
+ groups = entry.size / 3
284
+
285
+ (0...groups).each do |i|
286
+ count = entry[i * 3 + 0]
287
+ total_count = entry[i * 3 + 1]
288
+ data_count = entry[i * 3 + 2]
289
+
290
+ count.times do
291
+ blocks << ErrorCorrectionBlock.new(total_count, data_count)
292
+ end
293
+ end
294
+
295
+ blocks
296
+ end
297
+
298
+ # Get table entry for specific version and error correction level
299
+ # @parameter version [Integer] QR code version (1-40)
300
+ # @parameter level [Integer] Error correction level
301
+ # @return [Array] Table entry [block_count, total_codewords, data_codewords, ...]
302
+ def self.table_entry_for(version, level)
303
+ return nil unless version.between?(1, 40)
304
+
305
+ case level
306
+ when ERROR_CORRECTION_LEVEL[:l]
307
+ TABLE[(version - 1) * 4 + 0]
308
+ when ERROR_CORRECTION_LEVEL[:m]
309
+ TABLE[(version - 1) * 4 + 1]
310
+ when ERROR_CORRECTION_LEVEL[:q]
311
+ TABLE[(version - 1) * 4 + 2]
312
+ when ERROR_CORRECTION_LEVEL[:h]
313
+ TABLE[(version - 1) * 4 + 3]
314
+ end
315
+ end
316
+ end
317
+ end
318
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2008-2025, by Duncan Robertson.
5
+ # Copyright, 2025, by Samuel Williams.
6
+
7
+ module QRCode
8
+ module Encoder
9
+ class Math
10
+ module_eval do
11
+ exp_table = Array.new(256)
12
+ log_table = Array.new(256)
13
+
14
+ 8.times do |i|
15
+ exp_table[i] = 1 << i
16
+ end
17
+
18
+ (8...256).each do |i|
19
+ exp_table[i] = exp_table[i - 4] ^ exp_table[i - 5] ^ exp_table[i - 6] ^ exp_table[i - 8]
20
+ end
21
+
22
+ 255.times do |i|
23
+ log_table[exp_table[i]] = i
24
+ end
25
+
26
+ const_set(:EXP_TABLE, exp_table).freeze
27
+ const_set(:LOG_TABLE, log_table).freeze
28
+ end
29
+
30
+ class << self
31
+ def glog(n)
32
+ raise RuntimeError, "glog(#{n})" if n < 1
33
+ LOG_TABLE[n]
34
+ end
35
+
36
+ def gexp(n)
37
+ while n < 0
38
+ n += 255
39
+ end
40
+
41
+ while n >= 256
42
+ n -= 255
43
+ end
44
+
45
+ EXP_TABLE[n]
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2008-2021, by Duncan Robertson.
5
+ # Copyright, 2025, by Samuel Williams.
6
+
7
+ require_relative "math"
8
+
9
+ module QRCode
10
+ module Encoder
11
+ class Polynomial
12
+ def initialize(num, shift)
13
+ raise RuntimeError, "#{num.size}/#{shift}" if num.empty?
14
+ offset = 0
15
+
16
+ while offset < num.size && num[offset] == 0
17
+ offset += 1
18
+ end
19
+
20
+ @num = Array.new(num.size - offset + shift)
21
+
22
+ (0...num.size - offset).each do |i|
23
+ @num[i] = num[i + offset]
24
+ end
25
+ end
26
+
27
+ def get(index)
28
+ @num[index]
29
+ end
30
+
31
+ def get_length
32
+ @num.size
33
+ end
34
+
35
+ def multiply(e)
36
+ num = Array.new(get_length + e.get_length - 1)
37
+
38
+ (0...get_length).each do |i|
39
+ (0...e.get_length).each do |j|
40
+ tmp = num[i + j].nil? ? 0 : num[i + j]
41
+ num[i + j] = tmp ^ Encoder::Math.gexp(Encoder::Math.glog(get(i)) + Encoder::Math.glog(e.get(j)))
42
+ end
43
+ end
44
+
45
+ Encoder::Polynomial.new(num, 0)
46
+ end
47
+
48
+ def mod(e)
49
+ if get_length - e.get_length < 0
50
+ return self
51
+ end
52
+
53
+ ratio = Encoder::Math.glog(get(0)) - Encoder::Math.glog(e.get(0))
54
+ num = Array.new(get_length)
55
+
56
+ (0...get_length).each do |i|
57
+ num[i] = get(i)
58
+ end
59
+
60
+ (0...e.get_length).each do |i|
61
+ tmp = num[i].nil? ? 0 : num[i]
62
+ num[i] = tmp ^ Encoder::Math.gexp(Encoder::Math.glog(e.get(i)) + ratio)
63
+ end
64
+
65
+ Encoder::Polynomial.new(num, 0).mod(e)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2021, by Sam Sayer.
5
+ # Copyright, 2021-2025, by Duncan Robertson.
6
+ # Copyright, 2025, by Samuel Williams.
7
+
8
+ require_relative "constants"
9
+ require_relative "util"
10
+
11
+ module QRCode
12
+ module Encoder
13
+ # Base segment class - defaults to binary (8-bit byte) encoding
14
+ class Segment
15
+ attr_reader :data
16
+
17
+ def initialize(data)
18
+ @data = data.to_s
19
+ end
20
+
21
+ def mode
22
+ :mode_8bit_byte
23
+ end
24
+
25
+ def bit_size(version)
26
+ 4 + header_size(version) + content_size
27
+ end
28
+
29
+ def header_size(version)
30
+ Encoder::Util.get_length_in_bits(MODE[mode], version)
31
+ end
32
+
33
+ def content_size
34
+ @data.bytesize * 8
35
+ end
36
+
37
+ def write(buffer)
38
+ buffer.byte_encoding_start(@data.bytesize)
39
+
40
+ @data.each_byte do |b|
41
+ buffer.put(b, 8)
42
+ end
43
+ end
44
+
45
+ # Factory method to build segments from various data types
46
+ # @parameter data [String, Array, Segment] The data to encode
47
+ # @parameter mode [Symbol] Encoding mode (:auto, :number, :alphanumeric, :byte_8bit)
48
+ # @return [Array<Segment>] Array of segments
49
+ def self.build(data, mode: :auto)
50
+ case data
51
+ when String
52
+ [create_segment_for_data(data, mode)]
53
+ when Array
54
+ data.map do |item|
55
+ case item
56
+ when Hash
57
+ create_segment_for_data(item[:data], item[:mode] || :auto)
58
+ when String
59
+ create_segment_for_data(item, mode)
60
+ when Segment
61
+ item
62
+ else
63
+ raise ArgumentError, "Array elements must be Strings, Hashes with :data key, or Segments"
64
+ end
65
+ end
66
+ when Segment
67
+ [data]
68
+ else
69
+ raise ArgumentError, "data must be a String, Segment, or Array"
70
+ end
71
+ end
72
+
73
+ private_class_method def self.create_segment_for_data(data, mode)
74
+ case mode
75
+ when :auto
76
+ detect_optimal_segment_class(data).new(data)
77
+ when :number, :numeric
78
+ NumericSegment.new(data)
79
+ when :alphanumeric, :alpha_numk
80
+ AlphanumericSegment.new(data)
81
+ when :byte_8bit, :binary
82
+ Segment.new(data)
83
+ else
84
+ raise ArgumentError, "Unknown mode: #{mode}"
85
+ end
86
+ end
87
+
88
+ private_class_method def self.detect_optimal_segment_class(data)
89
+ if NumericSegment.valid_data?(data)
90
+ NumericSegment
91
+ elsif AlphanumericSegment.valid_data?(data)
92
+ AlphanumericSegment
93
+ else
94
+ Segment
95
+ end
96
+ end
97
+ end
98
+
99
+ # Numeric segment - optimized for numeric data (0-9)
100
+ class NumericSegment < Segment
101
+ def initialize(data)
102
+ data_str = data.to_s
103
+ unless self.class.valid_data?(data_str)
104
+ raise ArgumentError, "Not a numeric string `#{data_str}`"
105
+ end
106
+ super(data_str)
107
+ end
108
+
109
+ def self.valid_data?(data)
110
+ data.to_s.match?(/\A\d+\z/)
111
+ end
112
+
113
+ def mode
114
+ :mode_number
115
+ end
116
+
117
+ def content_size
118
+ data_length = @data.length
119
+ case data_length % 3
120
+ when 0
121
+ (data_length / 3) * 10
122
+ when 1
123
+ ((data_length / 3) * 10) + 4
124
+ when 2
125
+ ((data_length / 3) * 10) + 7
126
+ end
127
+ end
128
+
129
+ def write(buffer)
130
+ buffer.numeric_encoding_start(@data.size)
131
+
132
+ @data.scan(/\d{1,3}/).each do |group|
133
+ case group.length
134
+ when 3
135
+ buffer.put(group.to_i, 10)
136
+ when 2
137
+ buffer.put(group.to_i, 7)
138
+ when 1
139
+ buffer.put(group.to_i, 4)
140
+ end
141
+ end
142
+ end
143
+ end
144
+
145
+ # Alphanumeric segment - optimized for alphanumeric data
146
+ class AlphanumericSegment < Segment
147
+ ALPHANUMERIC = [
148
+ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I",
149
+ "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", " ", "$",
150
+ "%", "*", "+", "-", ".", "/", ":"
151
+ ].freeze
152
+
153
+ def initialize(data)
154
+ data_str = data.to_s
155
+ unless self.class.valid_data?(data_str)
156
+ raise ArgumentError, "Not an alphanumeric string `#{data_str}`"
157
+ end
158
+ super(data_str)
159
+ end
160
+
161
+ def self.valid_data?(data)
162
+ (data.to_s.chars - ALPHANUMERIC).empty?
163
+ end
164
+
165
+ def mode
166
+ :mode_alpha_numk
167
+ end
168
+
169
+ def content_size
170
+ (@data.size / 2.0).ceil * 11
171
+ end
172
+
173
+ def write(buffer)
174
+ buffer.alphanumeric_encoding_start(@data.size)
175
+
176
+ @data.size.times do |i|
177
+ if i % 2 == 0
178
+ if i == (@data.size - 1)
179
+ value = ALPHANUMERIC.index(@data[i])
180
+ buffer.put(value, 6)
181
+ else
182
+ value = (ALPHANUMERIC.index(@data[i]) * 45) + ALPHANUMERIC.index(@data[i + 1])
183
+ buffer.put(value, 11)
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end