qr_code_maker 0.1.0
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.
- checksums.yaml +7 -0
- data/README.md +78 -0
- data/lib/qr_code_maker/encoder.rb +1031 -0
- data/lib/qr_code_maker/renderer.rb +56 -0
- data/lib/qr_code_maker.rb +16 -0
- metadata +45 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 883092da5fc7735a8babef3fe6bb57b630147a3ed7c4ce7c21e3e095b829c9e8
|
|
4
|
+
data.tar.gz: 26869583a0e07a791bd552a2675000e5b33cf00c4217e08e82cca4208f7bc196
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 44d9ea7bd6708381e7445a180956273309269b50d5b9ff54ce8c01736c8c48de030fb4ff6a3a154ad195f3c27d73ca8a488d0d6ccf58c861fe5e2a316b90d9da
|
|
7
|
+
data.tar.gz: 2b9e34abdbae056cf89512be3ebbb9216832db62af0a9d2f9b063bc019e63e864607cf02ea287c10ec968c08052cec14ce97b6eca3be1757d91dd08b80a53e53
|
data/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# qr_code_maker
|
|
2
|
+
|
|
3
|
+
_A simple, zero-dependency, standard-library-only QR code generator for Ruby._
|
|
4
|
+
|
|
5
|
+
`qr_code_maker` is written in pure Ruby, with no external gem dependencies. It supports the full QR Code specification (Versions 1-40, ECC levels L, M, Q, H) and generates vector SVG strings or terminal ASCII art.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add this line to your application's Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem 'qr_code_maker'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
And then execute:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
$ bundle install
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or install it yourself as:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
$ gem install qr_code_maker
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
### High-Level API (Quickest)
|
|
30
|
+
|
|
31
|
+
You can generate QR code SVG strings or ASCII prints directly from a URL or text string:
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
require 'qr_code_maker'
|
|
35
|
+
|
|
36
|
+
# Generate a raw SVG string
|
|
37
|
+
svg = QrCodeMaker.generate_svg("https://github.com")
|
|
38
|
+
|
|
39
|
+
# Generate an ASCII terminal representation
|
|
40
|
+
ascii = QrCodeMaker.generate_ascii("https://github.com")
|
|
41
|
+
puts ascii
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Object-Oriented API
|
|
45
|
+
|
|
46
|
+
If you need more control, you can instantiate a `QRCode` object:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
require 'qr_code_maker'
|
|
50
|
+
|
|
51
|
+
# Create the QR Code object with custom Error Correction Level (:l, :m, :q, or :h)
|
|
52
|
+
qr = QrCodeMaker::QRCode.new("https://github.com", level: :m)
|
|
53
|
+
|
|
54
|
+
# Access metadata
|
|
55
|
+
puts qr.version # The auto-chosen QR version (1-40)
|
|
56
|
+
puts qr.module_count # Dimension of the grid (width/height)
|
|
57
|
+
puts qr.checked?(0, 0) # Check if the module at (row, col) is dark (true) or light (false)
|
|
58
|
+
|
|
59
|
+
# Render to SVG with options
|
|
60
|
+
svg = qr.as_svg(
|
|
61
|
+
color: "black", # Foreground color of the modules
|
|
62
|
+
bg_color: "white", # Background color (set to nil or false for transparent)
|
|
63
|
+
module_size: 10, # Size of each module in pixels
|
|
64
|
+
margin: 4 # Number of modules for quiet zone margin
|
|
65
|
+
)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Running Tests
|
|
69
|
+
|
|
70
|
+
Run the automated test suite with:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
ruby -Ilib -Itest test/test_qr_code_maker.rb
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -0,0 +1,1031 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QrCodeMaker
|
|
4
|
+
QRMODE = {
|
|
5
|
+
mode_number: 1 << 0, # 1
|
|
6
|
+
mode_alpha_numk: 1 << 1, # 2
|
|
7
|
+
mode_8bit_byte: 1 << 2 # 4
|
|
8
|
+
}.freeze
|
|
9
|
+
|
|
10
|
+
QRMODE_NAME = {
|
|
11
|
+
number: :mode_number,
|
|
12
|
+
alphanumeric: :mode_alpha_numk,
|
|
13
|
+
byte_8bit: :mode_8bit_byte,
|
|
14
|
+
multi: :mode_multi
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
QRERRORCORRECTLEVEL = {
|
|
18
|
+
l: 1,
|
|
19
|
+
m: 0,
|
|
20
|
+
q: 3,
|
|
21
|
+
h: 2
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
QRMASKPATTERN = {
|
|
25
|
+
pattern000: 0,
|
|
26
|
+
pattern001: 1,
|
|
27
|
+
pattern010: 2,
|
|
28
|
+
pattern011: 3,
|
|
29
|
+
pattern100: 4,
|
|
30
|
+
pattern101: 5,
|
|
31
|
+
pattern110: 6,
|
|
32
|
+
pattern111: 7
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
QRMASKCOMPUTATIONS = [
|
|
36
|
+
proc { |i, j| (i + j) % 2 == 0 },
|
|
37
|
+
proc { |i, j| i % 2 == 0 },
|
|
38
|
+
proc { |i, j| j % 3 == 0 },
|
|
39
|
+
proc { |i, j| (i + j) % 3 == 0 },
|
|
40
|
+
proc { |i, j| ((i / 2).floor + (j / 3).floor) % 2 == 0 },
|
|
41
|
+
proc { |i, j| (i * j) % 2 + (i * j) % 3 == 0 },
|
|
42
|
+
proc { |i, j| ((i * j) % 2 + (i * j) % 3) % 2 == 0 },
|
|
43
|
+
proc { |i, j| ((i * j) % 3 + (i + j) % 2) % 2 == 0 }
|
|
44
|
+
].freeze
|
|
45
|
+
|
|
46
|
+
QRPOSITIONPATTERNLENGTH = (7 + 1) * 2 + 1
|
|
47
|
+
QRFORMATINFOLENGTH = 15
|
|
48
|
+
|
|
49
|
+
QRMAXBITS = {
|
|
50
|
+
l: [152, 272, 440, 640, 864, 1088, 1248, 1552, 1856, 2192, 2592, 2960, 3424, 3688, 4184,
|
|
51
|
+
4712, 5176, 5768, 6360, 6888, 7456, 8048, 8752, 9392, 10_208, 10_960, 11_744, 12_248,
|
|
52
|
+
13_048, 13_880, 14_744, 15_640, 16_568, 17_528, 18_448, 19_472, 20_528, 21_616, 22_496, 23_648],
|
|
53
|
+
m: [128, 224, 352, 512, 688, 864, 992, 1232, 1456, 1728, 2032, 2320, 2672, 2920, 3320, 3624,
|
|
54
|
+
4056, 4504, 5016, 5352, 5712, 6256, 6880, 7312, 8000, 8496, 9024, 9544, 10_136, 10_984,
|
|
55
|
+
11_640, 12_328, 13_048, 13_800, 14_496, 15_312, 15_936, 16_816, 17_728, 18_672],
|
|
56
|
+
q: [104, 176, 272, 384, 496, 608, 704, 880, 1056, 1232, 1440, 1648, 1952, 2088, 2360, 2600, 2936,
|
|
57
|
+
3176, 3560, 3880, 4096, 4544, 4912, 5312, 5744, 6032, 6464, 6968, 7288, 7880, 8264, 8920, 9368,
|
|
58
|
+
9848, 10288, 10832, 11408, 12016, 12656, 13328],
|
|
59
|
+
h: [72, 128, 208, 288, 368, 480, 528, 688, 800, 976, 1120, 1264, 1440, 1576, 1784,
|
|
60
|
+
2024, 2264, 2504, 2728, 3080, 3248, 3536, 3712, 4112, 4304, 4768, 5024, 5288, 5608, 5960,
|
|
61
|
+
6344, 6760, 7208, 7688, 7888, 8432, 8768, 9136, 9776, 10_208]
|
|
62
|
+
}.freeze
|
|
63
|
+
|
|
64
|
+
class QRCodeArgumentError < ArgumentError; end
|
|
65
|
+
class QRCodeRunTimeError < RuntimeError; end
|
|
66
|
+
|
|
67
|
+
class QRMath
|
|
68
|
+
module_eval {
|
|
69
|
+
exp_table = Array.new(256)
|
|
70
|
+
log_table = Array.new(256)
|
|
71
|
+
|
|
72
|
+
8.times do |i|
|
|
73
|
+
exp_table[i] = 1 << i
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
(8...256).each do |i|
|
|
77
|
+
exp_table[i] = exp_table[i - 4] \
|
|
78
|
+
^ exp_table[i - 5] \
|
|
79
|
+
^ exp_table[i - 6] \
|
|
80
|
+
^ exp_table[i - 8]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
255.times do |i|
|
|
84
|
+
log_table[exp_table[i]] = i
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
const_set(:EXP_TABLE, exp_table).freeze
|
|
88
|
+
const_set(:LOG_TABLE, log_table).freeze
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
class << self
|
|
92
|
+
def glog(n)
|
|
93
|
+
raise QRCodeRunTimeError, "glog(#{n})" if n < 1
|
|
94
|
+
LOG_TABLE[n]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def gexp(n)
|
|
98
|
+
while n < 0
|
|
99
|
+
n += 255
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
while n >= 256
|
|
103
|
+
n -= 255
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
EXP_TABLE[n]
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
class QRPolynomial
|
|
112
|
+
def initialize(num, shift)
|
|
113
|
+
raise QRCodeRunTimeError, "#{num.size}/#{shift}" if num.empty?
|
|
114
|
+
offset = 0
|
|
115
|
+
|
|
116
|
+
while offset < num.size && num[offset] == 0
|
|
117
|
+
offset += 1
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
@num = Array.new(num.size - offset + shift)
|
|
121
|
+
|
|
122
|
+
(0...num.size - offset).each do |i|
|
|
123
|
+
@num[i] = num[i + offset]
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def get(index)
|
|
128
|
+
@num[index]
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def get_length
|
|
132
|
+
@num.size
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def multiply(e)
|
|
136
|
+
num = Array.new(get_length + e.get_length - 1)
|
|
137
|
+
|
|
138
|
+
(0...get_length).each do |i|
|
|
139
|
+
(0...e.get_length).each do |j|
|
|
140
|
+
tmp = num[i + j].nil? ? 0 : num[i + j]
|
|
141
|
+
num[i + j] = tmp ^ QRMath.gexp(QRMath.glog(get(i)) + QRMath.glog(e.get(j)))
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
QRPolynomial.new(num, 0)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def mod(e)
|
|
149
|
+
if get_length - e.get_length < 0
|
|
150
|
+
return self
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
ratio = QRMath.glog(get(0)) - QRMath.glog(e.get(0))
|
|
154
|
+
num = Array.new(get_length)
|
|
155
|
+
|
|
156
|
+
(0...get_length).each do |i|
|
|
157
|
+
num[i] = get(i)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
(0...e.get_length).each do |i|
|
|
161
|
+
tmp = num[i].nil? ? 0 : num[i]
|
|
162
|
+
num[i] = tmp ^ QRMath.gexp(QRMath.glog(e.get(i)) + ratio)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
QRPolynomial.new(num, 0).mod(e)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
class QRRSBlock
|
|
170
|
+
attr_reader :data_count, :total_count
|
|
171
|
+
|
|
172
|
+
def initialize(total_count, data_count)
|
|
173
|
+
@total_count = total_count
|
|
174
|
+
@data_count = data_count
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
RS_BLOCK_TABLE = [
|
|
178
|
+
[1, 26, 19], [1, 26, 16], [1, 26, 13], [1, 26, 9],
|
|
179
|
+
[1, 44, 34], [1, 44, 28], [1, 44, 22], [1, 44, 16],
|
|
180
|
+
[1, 70, 55], [1, 70, 44], [2, 35, 17], [2, 35, 13],
|
|
181
|
+
[1, 100, 80], [2, 50, 32], [2, 50, 24], [4, 25, 9],
|
|
182
|
+
[1, 134, 108], [2, 67, 43], [2, 33, 15, 2, 34, 16], [2, 33, 11, 2, 34, 12],
|
|
183
|
+
[2, 86, 68], [4, 43, 27], [4, 43, 19], [4, 43, 15],
|
|
184
|
+
[2, 98, 78], [4, 49, 31], [2, 32, 14, 4, 33, 15], [4, 39, 13, 1, 40, 14],
|
|
185
|
+
[2, 121, 97], [2, 60, 38, 2, 61, 39], [4, 40, 18, 2, 41, 19], [4, 40, 14, 2, 41, 15],
|
|
186
|
+
[2, 146, 116], [3, 58, 36, 2, 59, 37], [4, 36, 16, 4, 37, 17], [4, 36, 12, 4, 37, 13],
|
|
187
|
+
[2, 86, 68, 2, 87, 69], [4, 69, 43, 1, 70, 44], [6, 43, 19, 2, 44, 20], [6, 43, 15, 2, 44, 16],
|
|
188
|
+
[4, 101, 81], [1, 80, 50, 4, 81, 51], [4, 50, 22, 4, 51, 23], [3, 36, 12, 8, 37, 13],
|
|
189
|
+
[2, 116, 92, 2, 117, 93], [6, 58, 36, 2, 59, 37], [4, 46, 20, 6, 47, 21], [7, 42, 14, 4, 43, 15],
|
|
190
|
+
[4, 133, 107], [8, 59, 37, 1, 60, 38], [8, 44, 20, 4, 45, 21], [12, 33, 11, 4, 34, 12],
|
|
191
|
+
[3, 145, 115, 1, 146, 116], [4, 64, 40, 5, 65, 41], [11, 36, 16, 5, 37, 17], [11, 36, 12, 5, 37, 13],
|
|
192
|
+
[5, 109, 87, 1, 110, 88], [5, 65, 41, 5, 66, 42], [5, 54, 24, 7, 55, 25], [11, 36, 12, 7, 37, 13],
|
|
193
|
+
[5, 122, 98, 1, 123, 99], [7, 73, 45, 3, 74, 46], [15, 43, 19, 2, 44, 20], [3, 45, 15, 13, 46, 16],
|
|
194
|
+
[1, 135, 107, 5, 136, 108], [10, 74, 46, 1, 75, 47], [1, 50, 22, 15, 51, 23], [2, 42, 14, 17, 43, 15],
|
|
195
|
+
[5, 150, 120, 1, 151, 121], [9, 69, 43, 4, 70, 44], [17, 50, 22, 1, 51, 23], [2, 42, 14, 19, 43, 15],
|
|
196
|
+
[3, 141, 113, 4, 142, 114], [3, 70, 44, 11, 71, 45], [17, 47, 21, 4, 48, 22], [9, 39, 13, 16, 40, 14],
|
|
197
|
+
[3, 135, 107, 5, 136, 108], [3, 67, 41, 13, 68, 42], [15, 54, 24, 5, 55, 25], [15, 43, 15, 10, 44, 16],
|
|
198
|
+
[4, 144, 116, 4, 145, 117], [17, 68, 42], [17, 50, 22, 6, 51, 23], [19, 46, 16, 6, 47, 17],
|
|
199
|
+
[2, 139, 111, 7, 140, 112], [17, 74, 46], [7, 54, 24, 16, 55, 25], [34, 37, 13],
|
|
200
|
+
[4, 151, 121, 5, 152, 122], [4, 75, 47, 14, 76, 48], [11, 54, 24, 14, 55, 25], [16, 45, 15, 14, 46, 16],
|
|
201
|
+
[6, 147, 117, 4, 148, 118], [6, 73, 45, 14, 74, 46], [11, 54, 24, 16, 55, 25], [30, 46, 16, 2, 47, 17],
|
|
202
|
+
[8, 132, 106, 4, 133, 107], [8, 75, 47, 13, 76, 48], [7, 54, 24, 22, 55, 25], [22, 45, 15, 13, 46, 16],
|
|
203
|
+
[10, 142, 114, 2, 143, 115], [19, 74, 46, 4, 75, 47], [28, 50, 22, 6, 51, 23], [33, 46, 16, 4, 47, 17],
|
|
204
|
+
[8, 152, 122, 4, 153, 123], [22, 73, 45, 3, 74, 46], [8, 53, 23, 26, 54, 24], [12, 45, 15, 28, 46, 16],
|
|
205
|
+
[3, 147, 117, 10, 148, 118], [3, 73, 45, 23, 74, 46], [4, 54, 24, 31, 55, 25], [11, 45, 15, 31, 46, 16],
|
|
206
|
+
[7, 146, 116, 7, 147, 117], [21, 73, 45, 7, 74, 46], [1, 53, 23, 37, 54, 24], [19, 45, 15, 26, 46, 16],
|
|
207
|
+
[5, 145, 115, 10, 146, 116], [19, 75, 47, 10, 76, 48], [15, 54, 24, 25, 55, 25], [23, 45, 15, 25, 46, 16],
|
|
208
|
+
[13, 145, 115, 3, 146, 116], [2, 74, 46, 29, 75, 47], [42, 54, 24, 1, 55, 25], [23, 45, 15, 28, 46, 16],
|
|
209
|
+
[17, 145, 115], [10, 74, 46, 23, 75, 47], [10, 54, 24, 35, 55, 25], [19, 45, 15, 35, 46, 16],
|
|
210
|
+
[17, 145, 115, 1, 146, 116], [14, 74, 46, 21, 75, 47], [29, 54, 24, 19, 55, 25], [11, 45, 15, 46, 46, 16],
|
|
211
|
+
[13, 145, 115, 6, 146, 116], [14, 74, 46, 23, 75, 47], [44, 54, 24, 7, 55, 25], [59, 46, 16, 1, 47, 17],
|
|
212
|
+
[12, 151, 121, 7, 152, 122], [12, 75, 47, 26, 76, 48], [39, 54, 24, 14, 55, 25], [22, 45, 15, 41, 46, 16],
|
|
213
|
+
[6, 151, 121, 14, 152, 122], [6, 75, 47, 34, 76, 48], [46, 54, 24, 10, 55, 25], [2, 45, 15, 64, 46, 16],
|
|
214
|
+
[17, 152, 122, 4, 153, 123], [29, 74, 46, 14, 75, 47], [49, 54, 24, 10, 55, 25], [24, 45, 15, 46, 46, 16],
|
|
215
|
+
[4, 152, 122, 18, 153, 123], [13, 74, 46, 32, 75, 47], [48, 54, 24, 14, 55, 25], [42, 45, 15, 32, 46, 16],
|
|
216
|
+
[20, 147, 117, 4, 148, 118], [40, 75, 47, 7, 76, 48], [43, 54, 24, 22, 55, 25], [10, 45, 15, 67, 46, 16],
|
|
217
|
+
[19, 148, 118, 6, 149, 119], [18, 75, 47, 31, 76, 48], [34, 54, 24, 34, 55, 25], [20, 45, 15, 61, 46, 16]
|
|
218
|
+
].freeze
|
|
219
|
+
|
|
220
|
+
def self.get_rs_blocks(version, error_correct_level)
|
|
221
|
+
rs_block = get_rs_block_table(version, error_correct_level)
|
|
222
|
+
raise QRCodeRunTimeError, "bad rsblock @ version: #{version}/error_correct_level:#{error_correct_level}" if rs_block.nil?
|
|
223
|
+
|
|
224
|
+
length = rs_block.size / 3
|
|
225
|
+
list = []
|
|
226
|
+
|
|
227
|
+
(0...length).each do |i|
|
|
228
|
+
count = rs_block[i * 3 + 0]
|
|
229
|
+
total_count = rs_block[i * 3 + 1]
|
|
230
|
+
data_count = rs_block[i * 3 + 2]
|
|
231
|
+
|
|
232
|
+
(0...count).each do
|
|
233
|
+
list << QRRSBlock.new(total_count, data_count)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
list
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def self.get_rs_block_table(version, error_correct_level)
|
|
240
|
+
case error_correct_level
|
|
241
|
+
when QRERRORCORRECTLEVEL[:l]
|
|
242
|
+
RS_BLOCK_TABLE[(version - 1) * 4 + 0]
|
|
243
|
+
when QRERRORCORRECTLEVEL[:m]
|
|
244
|
+
RS_BLOCK_TABLE[(version - 1) * 4 + 1]
|
|
245
|
+
when QRERRORCORRECTLEVEL[:q]
|
|
246
|
+
RS_BLOCK_TABLE[(version - 1) * 4 + 2]
|
|
247
|
+
when QRERRORCORRECTLEVEL[:h]
|
|
248
|
+
RS_BLOCK_TABLE[(version - 1) * 4 + 3]
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
class QRUtil
|
|
254
|
+
PATTERN_POSITION_TABLE = [
|
|
255
|
+
[],
|
|
256
|
+
[6, 18], [6, 22], [6, 26], [6, 30], [6, 34], [6, 22, 38], [6, 24, 42], [6, 26, 46], [6, 28, 50], [6, 30, 54],
|
|
257
|
+
[6, 32, 58], [6, 34, 62], [6, 26, 46, 66], [6, 26, 48, 70], [6, 26, 50, 74], [6, 30, 54, 78], [6, 30, 56, 82],
|
|
258
|
+
[6, 30, 58, 86], [6, 34, 62, 90], [6, 28, 50, 72, 94], [6, 26, 50, 74, 98], [6, 30, 54, 78, 102],
|
|
259
|
+
[6, 28, 54, 80, 106], [6, 32, 58, 84, 110], [6, 30, 58, 86, 114], [6, 34, 62, 90, 118],
|
|
260
|
+
[6, 26, 50, 74, 98, 122], [6, 30, 54, 78, 102, 126], [6, 26, 52, 78, 104, 130], [6, 30, 56, 82, 108, 134],
|
|
261
|
+
[6, 34, 60, 86, 112, 138], [6, 30, 58, 86, 114, 142], [6, 34, 62, 90, 118, 146], [6, 30, 54, 78, 102, 126, 150],
|
|
262
|
+
[6, 24, 50, 76, 102, 128, 154], [6, 28, 54, 80, 106, 132, 158], [6, 32, 58, 84, 110, 136, 162],
|
|
263
|
+
[6, 26, 54, 82, 110, 138, 166], [6, 30, 58, 86, 114, 142, 170]
|
|
264
|
+
].freeze
|
|
265
|
+
|
|
266
|
+
G15 = 1 << 10 | 1 << 8 | 1 << 5 | 1 << 4 | 1 << 2 | 1 << 1 | 1 << 0
|
|
267
|
+
G18 = 1 << 12 | 1 << 11 | 1 << 10 | 1 << 9 | 1 << 8 | 1 << 5 | 1 << 2 | 1 << 0
|
|
268
|
+
G15_MASK = 1 << 14 | 1 << 12 | 1 << 10 | 1 << 4 | 1 << 1
|
|
269
|
+
|
|
270
|
+
DEMERIT_POINTS_1 = 3
|
|
271
|
+
DEMERIT_POINTS_2 = 3
|
|
272
|
+
DEMERIT_POINTS_3 = 40
|
|
273
|
+
DEMERIT_POINTS_4 = 10
|
|
274
|
+
|
|
275
|
+
BITS_FOR_MODE = {
|
|
276
|
+
QRMODE[:mode_number] => [10, 12, 14],
|
|
277
|
+
QRMODE[:mode_alpha_numk] => [9, 11, 13],
|
|
278
|
+
QRMODE[:mode_8bit_byte] => [8, 16, 16]
|
|
279
|
+
}.freeze
|
|
280
|
+
|
|
281
|
+
ARCH_BITS = ENV.fetch("RQRCODE_CORE_ARCH_BITS", nil)&.to_i || 1.size * 8
|
|
282
|
+
|
|
283
|
+
def self.max_size
|
|
284
|
+
PATTERN_POSITION_TABLE.count
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def self.get_bch_format_info(data)
|
|
288
|
+
d = data << 10
|
|
289
|
+
while get_bch_digit(d) - get_bch_digit(G15) >= 0
|
|
290
|
+
d ^= (G15 << (get_bch_digit(d) - get_bch_digit(G15)))
|
|
291
|
+
end
|
|
292
|
+
((data << 10) | d) ^ G15_MASK
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def self.rszf(num, count)
|
|
296
|
+
(num >> count) & ((1 << (ARCH_BITS - count)) - 1)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def self.get_bch_version(data)
|
|
300
|
+
d = data << 12
|
|
301
|
+
while get_bch_digit(d) - get_bch_digit(G18) >= 0
|
|
302
|
+
d ^= (G18 << (get_bch_digit(d) - get_bch_digit(G18)))
|
|
303
|
+
end
|
|
304
|
+
(data << 12) | d
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def self.get_bch_digit(data)
|
|
308
|
+
digit = 0
|
|
309
|
+
while data != 0
|
|
310
|
+
digit += 1
|
|
311
|
+
data = rszf(data, 1)
|
|
312
|
+
end
|
|
313
|
+
digit
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def self.get_pattern_positions(version)
|
|
317
|
+
PATTERN_POSITION_TABLE[version - 1]
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def self.get_mask(mask_pattern, i, j)
|
|
321
|
+
raise QRCodeRunTimeError, "bad mask_pattern: #{mask_pattern}" if mask_pattern > QRMASKCOMPUTATIONS.size
|
|
322
|
+
QRMASKCOMPUTATIONS[mask_pattern].call(i, j)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def self.get_error_correct_polynomial(error_correct_length)
|
|
326
|
+
a = QRPolynomial.new([1], 0)
|
|
327
|
+
(0...error_correct_length).each do |i|
|
|
328
|
+
a = a.multiply(QRPolynomial.new([1, QRMath.gexp(i)], 0))
|
|
329
|
+
end
|
|
330
|
+
a
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def self.get_length_in_bits(mode, version)
|
|
334
|
+
raise QRCodeRunTimeError, "Unknown mode: #{mode}" unless QRMODE.value?(mode)
|
|
335
|
+
raise QRCodeRunTimeError, "Unknown version: #{version}" if version > 40
|
|
336
|
+
|
|
337
|
+
if version.between?(1, 9)
|
|
338
|
+
macro_version = 0
|
|
339
|
+
elsif version <= 26
|
|
340
|
+
macro_version = 1
|
|
341
|
+
elsif version <= 40
|
|
342
|
+
macro_version = 2
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
BITS_FOR_MODE[mode][macro_version]
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def self.get_lost_points(modules)
|
|
349
|
+
demerit_points = 0
|
|
350
|
+
demerit_points += demerit_points_1_same_color(modules)
|
|
351
|
+
demerit_points += demerit_points_2_full_blocks(modules)
|
|
352
|
+
demerit_points += demerit_points_3_dangerous_patterns(modules)
|
|
353
|
+
demerit_points += demerit_points_4_dark_ratio(modules)
|
|
354
|
+
demerit_points
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def self.demerit_points_1_same_color(modules)
|
|
358
|
+
demerit_points = 0
|
|
359
|
+
module_count = modules.size
|
|
360
|
+
max_index = module_count - 1
|
|
361
|
+
|
|
362
|
+
module_count.times do |row|
|
|
363
|
+
modules_row = modules[row]
|
|
364
|
+
module_count.times do |col|
|
|
365
|
+
same_count = 0
|
|
366
|
+
dark = modules_row[col]
|
|
367
|
+
|
|
368
|
+
if row > 0
|
|
369
|
+
row_above = modules[row - 1]
|
|
370
|
+
same_count += 1 if col > 0 && dark == row_above[col - 1]
|
|
371
|
+
same_count += 1 if dark == row_above[col]
|
|
372
|
+
same_count += 1 if col < max_index && dark == row_above[col + 1]
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
same_count += 1 if col > 0 && dark == modules_row[col - 1]
|
|
376
|
+
same_count += 1 if col < max_index && dark == modules_row[col + 1]
|
|
377
|
+
|
|
378
|
+
if row < max_index
|
|
379
|
+
row_below = modules[row + 1]
|
|
380
|
+
same_count += 1 if col > 0 && dark == row_below[col - 1]
|
|
381
|
+
same_count += 1 if dark == row_below[col]
|
|
382
|
+
same_count += 1 if col < max_index && dark == row_below[col + 1]
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
demerit_points += (DEMERIT_POINTS_1 + same_count - 5) if same_count > 5
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
demerit_points
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def self.demerit_points_2_full_blocks(modules)
|
|
392
|
+
demerit_points = 0
|
|
393
|
+
module_count = modules.size
|
|
394
|
+
max_row = module_count - 1
|
|
395
|
+
|
|
396
|
+
max_row.times do |row|
|
|
397
|
+
row_curr = modules[row]
|
|
398
|
+
row_next = modules[row + 1]
|
|
399
|
+
|
|
400
|
+
max_row.times do |col|
|
|
401
|
+
val = row_curr[col]
|
|
402
|
+
if val == row_next[col] && val == row_curr[col + 1] && val == row_next[col + 1]
|
|
403
|
+
demerit_points += DEMERIT_POINTS_2
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
demerit_points
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def self.demerit_points_3_dangerous_patterns(modules)
|
|
411
|
+
demerit_points = 0
|
|
412
|
+
module_count = modules.size
|
|
413
|
+
pattern_len = 7
|
|
414
|
+
max_start = module_count - pattern_len + 1
|
|
415
|
+
|
|
416
|
+
modules.each do |row|
|
|
417
|
+
max_start.times do |col|
|
|
418
|
+
if row[col] && !row[col + 1] && row[col + 2] &&
|
|
419
|
+
row[col + 3] && row[col + 4] && !row[col + 5] && row[col + 6]
|
|
420
|
+
demerit_points += DEMERIT_POINTS_3
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
module_count.times do |col|
|
|
426
|
+
max_start.times do |row|
|
|
427
|
+
if modules[row][col] && !modules[row + 1][col] && modules[row + 2][col] &&
|
|
428
|
+
modules[row + 3][col] && modules[row + 4][col] && !modules[row + 5][col] && modules[row + 6][col]
|
|
429
|
+
demerit_points += DEMERIT_POINTS_3
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
demerit_points
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def self.demerit_points_4_dark_ratio(modules)
|
|
437
|
+
dark_count = modules.reduce(0) do |sum, col|
|
|
438
|
+
sum + col.count(true)
|
|
439
|
+
end
|
|
440
|
+
ratio = dark_count.to_f / (modules.size * modules.size)
|
|
441
|
+
ratio_delta = (100 * ratio - 50).abs / 5
|
|
442
|
+
ratio_delta * DEMERIT_POINTS_4
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
class QRBitBuffer
|
|
447
|
+
attr_reader :buffer
|
|
448
|
+
|
|
449
|
+
PAD0 = 0xEC
|
|
450
|
+
PAD1 = 0x11
|
|
451
|
+
|
|
452
|
+
def initialize(version)
|
|
453
|
+
@version = version
|
|
454
|
+
@buffer = []
|
|
455
|
+
@length = 0
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def get(index)
|
|
459
|
+
buf_index = (index / 8).floor
|
|
460
|
+
(QRUtil.rszf(@buffer[buf_index], 7 - index % 8) & 1) == 1
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def put(num, length)
|
|
464
|
+
(0...length).each do |i|
|
|
465
|
+
put_bit((QRUtil.rszf(num, length - i - 1) & 1) == 1)
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def get_length_in_bits
|
|
470
|
+
@length
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def put_bit(bit)
|
|
474
|
+
buf_index = (@length / 8).floor
|
|
475
|
+
if @buffer.size <= buf_index
|
|
476
|
+
@buffer << 0
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
if bit
|
|
480
|
+
@buffer[buf_index] |= QRUtil.rszf(0x80, @length % 8)
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
@length += 1
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def byte_encoding_start(length)
|
|
487
|
+
put(QRMODE[:mode_8bit_byte], 4)
|
|
488
|
+
put(length, QRUtil.get_length_in_bits(QRMODE[:mode_8bit_byte], @version))
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def alphanumeric_encoding_start(length)
|
|
492
|
+
put(QRMODE[:mode_alpha_numk], 4)
|
|
493
|
+
put(length, QRUtil.get_length_in_bits(QRMODE[:mode_alpha_numk], @version))
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def numeric_encoding_start(length)
|
|
497
|
+
put(QRMODE[:mode_number], 4)
|
|
498
|
+
put(length, QRUtil.get_length_in_bits(QRMODE[:mode_number], @version))
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
def pad_until(prefered_size)
|
|
502
|
+
while get_length_in_bits % 8 != 0
|
|
503
|
+
put_bit(false)
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
while get_length_in_bits < prefered_size
|
|
507
|
+
put(PAD0, 8)
|
|
508
|
+
put(PAD1, 8) if get_length_in_bits < prefered_size
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
def end_of_message(max_data_bits)
|
|
513
|
+
put(0, 4) unless get_length_in_bits + 4 > max_data_bits
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
NUMERIC = %w[0 1 2 3 4 5 6 7 8 9].freeze
|
|
518
|
+
|
|
519
|
+
class QRNumeric
|
|
520
|
+
def initialize(data)
|
|
521
|
+
raise QRCodeArgumentError, "Not a numeric string `#{data}`" unless QRNumeric.valid_data?(data)
|
|
522
|
+
@data = data
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
def self.valid_data?(data)
|
|
526
|
+
(data.chars - NUMERIC).empty?
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
def write(buffer)
|
|
530
|
+
buffer.numeric_encoding_start(@data.size)
|
|
531
|
+
@data.size.times do |i|
|
|
532
|
+
if i % 3 == 0
|
|
533
|
+
chars = @data[i, 3]
|
|
534
|
+
bit_length = get_bit_length(chars.length)
|
|
535
|
+
buffer.put(chars.to_i, bit_length)
|
|
536
|
+
end
|
|
537
|
+
end
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
private
|
|
541
|
+
|
|
542
|
+
NUMBER_LENGTH = {
|
|
543
|
+
3 => 10,
|
|
544
|
+
2 => 7,
|
|
545
|
+
1 => 4
|
|
546
|
+
}.freeze
|
|
547
|
+
|
|
548
|
+
def get_bit_length(length)
|
|
549
|
+
NUMBER_LENGTH[length]
|
|
550
|
+
end
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
ALPHANUMERIC = [
|
|
554
|
+
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I",
|
|
555
|
+
"J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", " ", "$",
|
|
556
|
+
"%", "*", "+", "-", ".", "/", ":"
|
|
557
|
+
].freeze
|
|
558
|
+
|
|
559
|
+
class QRAlphanumeric
|
|
560
|
+
def initialize(data)
|
|
561
|
+
raise QRCodeArgumentError, "Not a alpha numeric uppercase string `#{data}`" unless QRAlphanumeric.valid_data?(data)
|
|
562
|
+
@data = data
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def self.valid_data?(data)
|
|
566
|
+
(data.chars - ALPHANUMERIC).empty?
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
def write(buffer)
|
|
570
|
+
buffer.alphanumeric_encoding_start(@data.size)
|
|
571
|
+
@data.size.times do |i|
|
|
572
|
+
if i % 2 == 0
|
|
573
|
+
if i == (@data.size - 1)
|
|
574
|
+
value = ALPHANUMERIC.index(@data[i])
|
|
575
|
+
buffer.put(value, 6)
|
|
576
|
+
else
|
|
577
|
+
value = (ALPHANUMERIC.index(@data[i]) * 45) + ALPHANUMERIC.index(@data[i + 1])
|
|
578
|
+
buffer.put(value, 11)
|
|
579
|
+
end
|
|
580
|
+
end
|
|
581
|
+
end
|
|
582
|
+
end
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
class QR8bitByte
|
|
586
|
+
def initialize(data)
|
|
587
|
+
@data = data
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
def write(buffer)
|
|
591
|
+
buffer.byte_encoding_start(@data.bytesize)
|
|
592
|
+
@data.each_byte do |b|
|
|
593
|
+
buffer.put(b, 8)
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
class QRMulti
|
|
599
|
+
def initialize(data)
|
|
600
|
+
@data = data
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
def write(buffer)
|
|
604
|
+
@data.each { |seg| seg.writer.write(buffer) }
|
|
605
|
+
end
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
class QRSegment
|
|
609
|
+
attr_reader :data, :mode
|
|
610
|
+
|
|
611
|
+
def initialize(data:, mode: nil)
|
|
612
|
+
@data = data
|
|
613
|
+
@mode = QRMODE_NAME.dig(mode&.to_sym)
|
|
614
|
+
|
|
615
|
+
@mode ||= if QRNumeric.valid_data?(@data)
|
|
616
|
+
QRMODE_NAME[:number]
|
|
617
|
+
elsif QRAlphanumeric.valid_data?(@data)
|
|
618
|
+
QRMODE_NAME[:alphanumeric]
|
|
619
|
+
else
|
|
620
|
+
QRMODE_NAME[:byte_8bit]
|
|
621
|
+
end
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
def size(version)
|
|
625
|
+
4 + header_size(version) + content_size
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
def header_size(version)
|
|
629
|
+
QRUtil.get_length_in_bits(QRMODE[mode], version)
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
def content_size
|
|
633
|
+
chunk_size, bit_length, extra = case mode
|
|
634
|
+
when :mode_number
|
|
635
|
+
[3, QRNumeric.const_get(:NUMBER_LENGTH)[3], QRNumeric.const_get(:NUMBER_LENGTH)[data_length % 3] || 0]
|
|
636
|
+
when :mode_alpha_numk
|
|
637
|
+
[2, 11, 6]
|
|
638
|
+
when :mode_8bit_byte
|
|
639
|
+
[1, 8, 0]
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
(data_length / chunk_size) * bit_length + (((data_length % chunk_size) == 0) ? 0 : extra)
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
def writer
|
|
646
|
+
case mode
|
|
647
|
+
when :mode_number
|
|
648
|
+
QRNumeric.new(data)
|
|
649
|
+
when :mode_alpha_numk
|
|
650
|
+
QRAlphanumeric.new(data)
|
|
651
|
+
when :mode_multi
|
|
652
|
+
QRMulti.new(data)
|
|
653
|
+
else
|
|
654
|
+
QR8bitByte.new(data)
|
|
655
|
+
end
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
private
|
|
659
|
+
|
|
660
|
+
def data_length
|
|
661
|
+
data.bytesize
|
|
662
|
+
end
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
class QRCode
|
|
666
|
+
attr_reader :modules, :module_count, :version
|
|
667
|
+
|
|
668
|
+
def initialize(data, *args)
|
|
669
|
+
options = extract_options!(args)
|
|
670
|
+
|
|
671
|
+
level = (options[:level] || :h).to_sym
|
|
672
|
+
max_size = options[:max_size] || QRUtil.max_size
|
|
673
|
+
|
|
674
|
+
@data = case data
|
|
675
|
+
when String
|
|
676
|
+
QRSegment.new(data: data, mode: options[:mode])
|
|
677
|
+
when Array
|
|
678
|
+
raise QRCodeArgumentError, "Array must contain Hashes with :data and :mode keys" unless data.all? { |seg| seg.is_a?(Hash) && %i[data mode].all? { |s| seg.key? s } }
|
|
679
|
+
data.map { |seg| QRSegment.new(**seg) }
|
|
680
|
+
when QRSegment
|
|
681
|
+
data
|
|
682
|
+
else
|
|
683
|
+
raise QRCodeArgumentError, "data must be a String, QRSegment, or an Array"
|
|
684
|
+
end
|
|
685
|
+
@error_correct_level = QRERRORCORRECTLEVEL[level]
|
|
686
|
+
|
|
687
|
+
unless @error_correct_level
|
|
688
|
+
raise QRCodeArgumentError, "Unknown error correction level `#{level.inspect}`"
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
size = options[:size] || minimum_version(limit: max_size)
|
|
692
|
+
|
|
693
|
+
if size > max_size
|
|
694
|
+
raise QRCodeArgumentError, "Given size greater than maximum possible size of #{QRUtil.max_size}"
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
@version = size
|
|
698
|
+
@module_count = @version * 4 + QRPOSITIONPATTERNLENGTH
|
|
699
|
+
@modules = Array.new(@module_count)
|
|
700
|
+
@data_list = multi_segment? ? QRMulti.new(@data) : @data.writer
|
|
701
|
+
@data_cache = nil
|
|
702
|
+
make
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
def checked?(row, col)
|
|
706
|
+
if !row.between?(0, @module_count - 1) || !col.between?(0, @module_count - 1)
|
|
707
|
+
raise QRCodeRunTimeError, "Invalid row/column pair: #{row}, #{col}"
|
|
708
|
+
end
|
|
709
|
+
@modules[row][col]
|
|
710
|
+
end
|
|
711
|
+
alias_method :dark?, :checked?
|
|
712
|
+
|
|
713
|
+
def to_s(*args)
|
|
714
|
+
options = extract_options!(args)
|
|
715
|
+
dark = options[:dark] || "x"
|
|
716
|
+
light = options[:light] || " "
|
|
717
|
+
quiet_zone_size = options[:quiet_zone_size] || 0
|
|
718
|
+
|
|
719
|
+
rows = []
|
|
720
|
+
@modules.each do |row|
|
|
721
|
+
cols = light * quiet_zone_size
|
|
722
|
+
row.each do |col|
|
|
723
|
+
cols += (col ? dark : light)
|
|
724
|
+
end
|
|
725
|
+
rows << cols
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
quiet_zone_size.times do
|
|
729
|
+
rows.unshift(light * (rows.first.length / light.size))
|
|
730
|
+
rows << light * (rows.first.length / light.size)
|
|
731
|
+
end
|
|
732
|
+
rows.join("\n")
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
def inspect
|
|
736
|
+
"QRCodeMaker: @data='#{@data}', @error_correct_level=#{@error_correct_level}, @version=#{@version}, @module_count=#{@module_count}"
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
def error_correction_level
|
|
740
|
+
QRERRORCORRECTLEVEL.invert[@error_correct_level]
|
|
741
|
+
end
|
|
742
|
+
|
|
743
|
+
def multi_segment?
|
|
744
|
+
@data.is_a?(Array)
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
def mode
|
|
748
|
+
case @data_list
|
|
749
|
+
when QRNumeric
|
|
750
|
+
:mode_number
|
|
751
|
+
when QRAlphanumeric
|
|
752
|
+
:mode_alpha_numk
|
|
753
|
+
else
|
|
754
|
+
:mode_8bit_byte
|
|
755
|
+
end
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
protected
|
|
759
|
+
|
|
760
|
+
def make
|
|
761
|
+
prepare_common_patterns
|
|
762
|
+
make_impl(false, get_best_mask_pattern)
|
|
763
|
+
end
|
|
764
|
+
|
|
765
|
+
private
|
|
766
|
+
|
|
767
|
+
def prepare_common_patterns
|
|
768
|
+
@modules.map! { Array.new(@module_count) }
|
|
769
|
+
|
|
770
|
+
place_position_probe_pattern(0, 0)
|
|
771
|
+
place_position_probe_pattern(@module_count - 7, 0)
|
|
772
|
+
place_position_probe_pattern(0, @module_count - 7)
|
|
773
|
+
place_position_adjust_pattern
|
|
774
|
+
place_timing_pattern
|
|
775
|
+
|
|
776
|
+
@common_patterns = @modules.map(&:clone)
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
def make_impl(test, mask_pattern)
|
|
780
|
+
@modules = @common_patterns.map(&:clone)
|
|
781
|
+
|
|
782
|
+
place_format_info(test, mask_pattern)
|
|
783
|
+
place_version_info(test) if @version >= 7
|
|
784
|
+
|
|
785
|
+
if @data_cache.nil?
|
|
786
|
+
@data_cache = QRCode.create_data(
|
|
787
|
+
@version, @error_correct_level, @data_list
|
|
788
|
+
)
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
map_data(@data_cache, mask_pattern)
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
def place_position_probe_pattern(row, col)
|
|
795
|
+
(-1..7).each do |r|
|
|
796
|
+
next unless (row + r).between?(0, @module_count - 1)
|
|
797
|
+
|
|
798
|
+
(-1..7).each do |c|
|
|
799
|
+
next unless (col + c).between?(0, @module_count - 1)
|
|
800
|
+
|
|
801
|
+
is_vert_line = r.between?(0, 6) && (c == 0 || c == 6)
|
|
802
|
+
is_horiz_line = c.between?(0, 6) && (r == 0 || r == 6)
|
|
803
|
+
is_square = r.between?(2, 4) && c.between?(2, 4)
|
|
804
|
+
|
|
805
|
+
is_part_of_probe = is_vert_line || is_horiz_line || is_square
|
|
806
|
+
@modules[row + r][col + c] = is_part_of_probe
|
|
807
|
+
end
|
|
808
|
+
end
|
|
809
|
+
end
|
|
810
|
+
|
|
811
|
+
def get_best_mask_pattern
|
|
812
|
+
min_lost_point = 0
|
|
813
|
+
pattern = 0
|
|
814
|
+
|
|
815
|
+
8.times do |i|
|
|
816
|
+
make_impl(true, i)
|
|
817
|
+
lost_point = QRUtil.get_lost_points(modules)
|
|
818
|
+
|
|
819
|
+
if i == 0 || min_lost_point > lost_point
|
|
820
|
+
min_lost_point = lost_point
|
|
821
|
+
pattern = i
|
|
822
|
+
end
|
|
823
|
+
end
|
|
824
|
+
pattern
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
def place_timing_pattern
|
|
828
|
+
(8...@module_count - 8).each do |i|
|
|
829
|
+
@modules[i][6] = @modules[6][i] = i % 2 == 0
|
|
830
|
+
end
|
|
831
|
+
end
|
|
832
|
+
|
|
833
|
+
def place_position_adjust_pattern
|
|
834
|
+
positions = QRUtil.get_pattern_positions(@version)
|
|
835
|
+
|
|
836
|
+
positions.each do |row|
|
|
837
|
+
positions.each do |col|
|
|
838
|
+
next unless @modules[row][col].nil?
|
|
839
|
+
|
|
840
|
+
(-2..2).each do |r|
|
|
841
|
+
(-2..2).each do |c|
|
|
842
|
+
is_part_of_pattern = r.abs == 2 || c.abs == 2 || (r == 0 && c == 0)
|
|
843
|
+
@modules[row + r][col + c] = is_part_of_pattern
|
|
844
|
+
end
|
|
845
|
+
end
|
|
846
|
+
end
|
|
847
|
+
end
|
|
848
|
+
end
|
|
849
|
+
|
|
850
|
+
def place_version_info(test)
|
|
851
|
+
bits = QRUtil.get_bch_version(@version)
|
|
852
|
+
|
|
853
|
+
18.times do |i|
|
|
854
|
+
mod = !test && ((bits >> i) & 1) == 1
|
|
855
|
+
@modules[(i / 3).floor][ i % 3 + @module_count - 8 - 3 ] = mod
|
|
856
|
+
@modules[i % 3 + @module_count - 8 - 3][ (i / 3).floor ] = mod
|
|
857
|
+
end
|
|
858
|
+
end
|
|
859
|
+
|
|
860
|
+
def place_format_info(test, mask_pattern)
|
|
861
|
+
data = (@error_correct_level << 3 | mask_pattern)
|
|
862
|
+
bits = QRUtil.get_bch_format_info(data)
|
|
863
|
+
|
|
864
|
+
QRFORMATINFOLENGTH.times do |i|
|
|
865
|
+
mod = !test && ((bits >> i) & 1) == 1
|
|
866
|
+
|
|
867
|
+
# vertical
|
|
868
|
+
row = if i < 6
|
|
869
|
+
i
|
|
870
|
+
elsif i < 8
|
|
871
|
+
i + 1
|
|
872
|
+
else
|
|
873
|
+
@module_count - 15 + i
|
|
874
|
+
end
|
|
875
|
+
@modules[row][8] = mod
|
|
876
|
+
|
|
877
|
+
# horizontal
|
|
878
|
+
col = if i < 8
|
|
879
|
+
@module_count - i - 1
|
|
880
|
+
elsif i < 9
|
|
881
|
+
15 - i - 1 + 1
|
|
882
|
+
else
|
|
883
|
+
15 - i - 1
|
|
884
|
+
end
|
|
885
|
+
@modules[8][col] = mod
|
|
886
|
+
end
|
|
887
|
+
|
|
888
|
+
# fixed module
|
|
889
|
+
@modules[@module_count - 8][8] = !test
|
|
890
|
+
end
|
|
891
|
+
|
|
892
|
+
def map_data(data, mask_pattern)
|
|
893
|
+
inc = -1
|
|
894
|
+
row = @module_count - 1
|
|
895
|
+
bit_index = 7
|
|
896
|
+
byte_index = 0
|
|
897
|
+
|
|
898
|
+
(@module_count - 1).step(1, -2) do |col|
|
|
899
|
+
col -= 1 if col <= 6
|
|
900
|
+
|
|
901
|
+
loop do
|
|
902
|
+
2.times do |c|
|
|
903
|
+
if @modules[row][col - c].nil?
|
|
904
|
+
dark = false
|
|
905
|
+
if byte_index < data.size && !data[byte_index].nil?
|
|
906
|
+
dark = ((QRUtil.rszf(data[byte_index], bit_index) & 1) == 1)
|
|
907
|
+
end
|
|
908
|
+
mask = QRUtil.get_mask(mask_pattern, row, col - c)
|
|
909
|
+
dark = !dark if mask
|
|
910
|
+
@modules[row][ col - c ] = dark
|
|
911
|
+
bit_index -= 1
|
|
912
|
+
|
|
913
|
+
if bit_index == -1
|
|
914
|
+
byte_index += 1
|
|
915
|
+
bit_index = 7
|
|
916
|
+
end
|
|
917
|
+
end
|
|
918
|
+
end
|
|
919
|
+
|
|
920
|
+
row += inc
|
|
921
|
+
|
|
922
|
+
if row < 0 || @module_count <= row
|
|
923
|
+
row -= inc
|
|
924
|
+
inc = -inc
|
|
925
|
+
break
|
|
926
|
+
end
|
|
927
|
+
end
|
|
928
|
+
end
|
|
929
|
+
end
|
|
930
|
+
|
|
931
|
+
def minimum_version(limit: QRUtil.max_size, version: 1)
|
|
932
|
+
raise QRCodeRunTimeError, "Data length exceed maximum capacity of version #{limit}" if version > limit
|
|
933
|
+
|
|
934
|
+
max_size_bits = QRMAXBITS[error_correction_level][version - 1]
|
|
935
|
+
size_bits = multi_segment? ? @data.sum { |seg| seg.size(version) } : @data.size(version)
|
|
936
|
+
|
|
937
|
+
return version if size_bits < max_size_bits
|
|
938
|
+
|
|
939
|
+
minimum_version(limit: limit, version: version + 1)
|
|
940
|
+
end
|
|
941
|
+
|
|
942
|
+
def extract_options!(arr)
|
|
943
|
+
arr.last.is_a?(::Hash) ? arr.pop : {}
|
|
944
|
+
end
|
|
945
|
+
|
|
946
|
+
class << self
|
|
947
|
+
def count_max_data_bits(rs_blocks)
|
|
948
|
+
max_data_bytes = rs_blocks.reduce(0) do |sum, rs_block|
|
|
949
|
+
sum + rs_block.data_count
|
|
950
|
+
end
|
|
951
|
+
max_data_bytes * 8
|
|
952
|
+
end
|
|
953
|
+
|
|
954
|
+
def create_data(version, error_correct_level, data_list)
|
|
955
|
+
rs_blocks = QRRSBlock.get_rs_blocks(version, error_correct_level)
|
|
956
|
+
max_data_bits = count_max_data_bits(rs_blocks)
|
|
957
|
+
buffer = QRBitBuffer.new(version)
|
|
958
|
+
|
|
959
|
+
data_list.write(buffer)
|
|
960
|
+
buffer.end_of_message(max_data_bits)
|
|
961
|
+
|
|
962
|
+
if buffer.get_length_in_bits > max_data_bits
|
|
963
|
+
raise QRCodeRunTimeError, "code length overflow. (#{buffer.get_length_in_bits}>#{max_data_bits}). (Try a larger size!)"
|
|
964
|
+
end
|
|
965
|
+
|
|
966
|
+
buffer.pad_until(max_data_bits)
|
|
967
|
+
create_bytes(buffer, rs_blocks)
|
|
968
|
+
end
|
|
969
|
+
|
|
970
|
+
def create_bytes(buffer, rs_blocks)
|
|
971
|
+
offset = 0
|
|
972
|
+
max_dc_count = 0
|
|
973
|
+
max_ec_count = 0
|
|
974
|
+
dcdata = Array.new(rs_blocks.size)
|
|
975
|
+
ecdata = Array.new(rs_blocks.size)
|
|
976
|
+
|
|
977
|
+
rs_blocks.each_with_index do |rs_block, r|
|
|
978
|
+
dc_count = rs_block.data_count
|
|
979
|
+
ec_count = rs_block.total_count - dc_count
|
|
980
|
+
max_dc_count = [max_dc_count, dc_count].max
|
|
981
|
+
max_ec_count = [max_ec_count, ec_count].max
|
|
982
|
+
|
|
983
|
+
dcdata_block = Array.new(dc_count)
|
|
984
|
+
dcdata_block.size.times do |i|
|
|
985
|
+
dcdata_block[i] = 0xff & buffer.buffer[i + offset]
|
|
986
|
+
end
|
|
987
|
+
dcdata[r] = dcdata_block
|
|
988
|
+
|
|
989
|
+
offset += dc_count
|
|
990
|
+
rs_poly = QRUtil.get_error_correct_polynomial(ec_count)
|
|
991
|
+
raw_poly = QRPolynomial.new(dcdata[r], rs_poly.get_length - 1)
|
|
992
|
+
mod_poly = raw_poly.mod(rs_poly)
|
|
993
|
+
|
|
994
|
+
ecdata_block = Array.new(rs_poly.get_length - 1)
|
|
995
|
+
ecdata_block.size.times do |i|
|
|
996
|
+
mod_index = i + mod_poly.get_length - ecdata_block.size
|
|
997
|
+
ecdata_block[i] = (mod_index >= 0) ? mod_poly.get(mod_index) : 0
|
|
998
|
+
end
|
|
999
|
+
ecdata[r] = ecdata_block
|
|
1000
|
+
end
|
|
1001
|
+
|
|
1002
|
+
total_code_count = rs_blocks.reduce(0) do |sum, rs_block|
|
|
1003
|
+
sum + rs_block.total_count
|
|
1004
|
+
end
|
|
1005
|
+
|
|
1006
|
+
data = Array.new(total_code_count)
|
|
1007
|
+
index = 0
|
|
1008
|
+
|
|
1009
|
+
max_dc_count.times do |i|
|
|
1010
|
+
rs_blocks.size.times do |r|
|
|
1011
|
+
if i < dcdata[r].size
|
|
1012
|
+
data[index] = dcdata[r][i]
|
|
1013
|
+
index += 1
|
|
1014
|
+
end
|
|
1015
|
+
end
|
|
1016
|
+
end
|
|
1017
|
+
|
|
1018
|
+
max_ec_count.times do |i|
|
|
1019
|
+
rs_blocks.size.times do |r|
|
|
1020
|
+
if i < ecdata[r].size
|
|
1021
|
+
data[index] = ecdata[r][i]
|
|
1022
|
+
index += 1
|
|
1023
|
+
end
|
|
1024
|
+
end
|
|
1025
|
+
end
|
|
1026
|
+
|
|
1027
|
+
data
|
|
1028
|
+
end
|
|
1029
|
+
end
|
|
1030
|
+
end
|
|
1031
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QrCodeMaker
|
|
4
|
+
class QRCode
|
|
5
|
+
def as_svg(options = {})
|
|
6
|
+
color = options[:color] || "black"
|
|
7
|
+
bg_color = options[:bg_color]
|
|
8
|
+
bg_color = "white" if bg_color.nil?
|
|
9
|
+
|
|
10
|
+
module_size = options[:module_size] || 10
|
|
11
|
+
margin = options[:margin] || 4
|
|
12
|
+
|
|
13
|
+
view_box_size = @module_count + 2 * margin
|
|
14
|
+
total_size = view_box_size * module_size
|
|
15
|
+
|
|
16
|
+
path_data = +""
|
|
17
|
+
@modules.each_with_index do |row, r|
|
|
18
|
+
row.each_with_index do |col, c|
|
|
19
|
+
if col
|
|
20
|
+
path_data << "M#{c + margin},#{r + margin}h1v1h-1z"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
svg = []
|
|
26
|
+
svg << %(<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 #{view_box_size} #{view_box_size}" width="#{total_size}" height="#{total_size}">)
|
|
27
|
+
if bg_color && bg_color != "transparent" && bg_color != false
|
|
28
|
+
svg << %( <rect width="100%" height="100%" fill="#{bg_color}"/>)
|
|
29
|
+
end
|
|
30
|
+
svg << %( <path d="#{path_data}" fill="#{color}" shape-rendering="crispEdges"/>)
|
|
31
|
+
svg << %(</svg>)
|
|
32
|
+
svg.join("\n")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def as_ascii(options = {})
|
|
36
|
+
dark = options[:dark] || "██"
|
|
37
|
+
light = options[:light] || " "
|
|
38
|
+
margin = options[:margin] || 2
|
|
39
|
+
|
|
40
|
+
rows = []
|
|
41
|
+
margin.times { rows << (light * (@module_count + 2 * margin)) }
|
|
42
|
+
|
|
43
|
+
@modules.each do |row|
|
|
44
|
+
row_str = light * margin
|
|
45
|
+
row.each do |col|
|
|
46
|
+
row_str << (col ? dark : light)
|
|
47
|
+
end
|
|
48
|
+
row_str << (light * margin)
|
|
49
|
+
rows << row_str
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
margin.times { rows << (light * (@module_count + 2 * margin)) }
|
|
53
|
+
rows.join("\n")
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "qr_code_maker/encoder"
|
|
4
|
+
require_relative "qr_code_maker/renderer"
|
|
5
|
+
|
|
6
|
+
module QrCodeMaker
|
|
7
|
+
# High-level helper to generate SVG in a single call
|
|
8
|
+
def self.generate_svg(data, options = {})
|
|
9
|
+
QRCode.new(data, options).as_svg(options)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# High-level helper to generate ASCII in a single call
|
|
13
|
+
def self.generate_ascii(data, options = {})
|
|
14
|
+
QRCode.new(data, options).as_ascii(options)
|
|
15
|
+
end
|
|
16
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: qr_code_maker
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- swlkr
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Generate QR codes in SVG and ASCII format with a super simple API, relying
|
|
13
|
+
only on standard Ruby.
|
|
14
|
+
email:
|
|
15
|
+
- from@swlkr.com
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- README.md
|
|
21
|
+
- lib/qr_code_maker.rb
|
|
22
|
+
- lib/qr_code_maker/encoder.rb
|
|
23
|
+
- lib/qr_code_maker/renderer.rb
|
|
24
|
+
homepage: https://github.com/swlkr/qr_code_generator
|
|
25
|
+
licenses:
|
|
26
|
+
- MIT
|
|
27
|
+
metadata: {}
|
|
28
|
+
rdoc_options: []
|
|
29
|
+
require_paths:
|
|
30
|
+
- lib
|
|
31
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
32
|
+
requirements:
|
|
33
|
+
- - ">="
|
|
34
|
+
- !ruby/object:Gem::Version
|
|
35
|
+
version: 2.5.0
|
|
36
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0'
|
|
41
|
+
requirements: []
|
|
42
|
+
rubygems_version: 3.7.2
|
|
43
|
+
specification_version: 4
|
|
44
|
+
summary: A zero-dependency standard-library-only QR code maker.
|
|
45
|
+
test_files: []
|