qrio 0.0.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.
- checksums.yaml +7 -0
- data/.gemtest +0 -0
- data/.gitignore +5 -0
- data/.rvmrc.example +1 -0
- data/Gemfile +3 -0
- data/README.md +66 -0
- data/Rakefile +8 -0
- data/examples/extract_qr +18 -0
- data/lib/qrio.rb +15 -0
- data/lib/qrio/finder_pattern_slice.rb +136 -0
- data/lib/qrio/horizontal_match.rb +31 -0
- data/lib/qrio/image_dumper.rb +97 -0
- data/lib/qrio/image_loader/png_image_loader.rb +18 -0
- data/lib/qrio/matrix.rb +72 -0
- data/lib/qrio/neighbor.rb +40 -0
- data/lib/qrio/qr.rb +207 -0
- data/lib/qrio/qr_matrix.rb +403 -0
- data/lib/qrio/region.rb +113 -0
- data/lib/qrio/sampling_grid.rb +152 -0
- data/lib/qrio/version.rb +3 -0
- data/lib/qrio/vertical_match.rb +31 -0
- data/qrio.gemspec +23 -0
- data/test/finder_pattern_slice_test.rb +121 -0
- data/test/fixtures/block_test.qr +25 -0
- data/test/fixtures/finder_pattern1.png +0 -0
- data/test/fixtures/finder_pattern2.png +0 -0
- data/test/fixtures/finder_pattern3.png +0 -0
- data/test/fixtures/finder_pattern4.png +0 -0
- data/test/fixtures/masked0.qr +25 -0
- data/test/fixtures/no_finder_pattern1.png +0 -0
- data/test/fixtures/qrio-codewords.txt +100 -0
- data/test/fixtures/qrio.qr +33 -0
- data/test/horizontal_match_test.rb +34 -0
- data/test/matrix_test.rb +66 -0
- data/test/qr_matrix_test.rb +164 -0
- data/test/qr_test.rb +107 -0
- data/test/region_test.rb +87 -0
- data/test/sampling_grid_test.rb +78 -0
- metadata +111 -0
@@ -0,0 +1,40 @@
|
|
1
|
+
module Qrio
|
2
|
+
# track angle and distance between two Finder Patterns
|
3
|
+
class Neighbor
|
4
|
+
attr_reader :source, :destination, :angle, :distance
|
5
|
+
|
6
|
+
ANGLE = Math::PI / 8
|
7
|
+
ZERO = 0..ANGLE
|
8
|
+
NINETY = (ANGLE * 3)..(ANGLE * 5)
|
9
|
+
ONEEIGHTY = (ANGLE * 7)..(ANGLE * 8)
|
10
|
+
|
11
|
+
def initialize(source, destination)
|
12
|
+
@source = source
|
13
|
+
@destination = destination
|
14
|
+
|
15
|
+
source.neighbors << self
|
16
|
+
|
17
|
+
dx = destination.center.first - source.center.first
|
18
|
+
# images are top down, geometry is bottom up. invert.
|
19
|
+
dy = source.center.last - destination.center.last
|
20
|
+
|
21
|
+
@angle = Math.atan2(dy, dx)
|
22
|
+
@distance = Math.sqrt(dx ** 2 + dy ** 2)
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_coordinates
|
26
|
+
[source.center, destination.center].flatten
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_s
|
30
|
+
"N[#{ to_coordinates * ',' }]"
|
31
|
+
end
|
32
|
+
|
33
|
+
def right_angle?
|
34
|
+
ZERO.include?(angle.abs) ||
|
35
|
+
NINETY.include?(angle.abs) ||
|
36
|
+
ONEEIGHTY.include?(angle.abs)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
data/lib/qrio/qr.rb
ADDED
@@ -0,0 +1,207 @@
|
|
1
|
+
module Qrio
|
2
|
+
class Qr
|
3
|
+
attr_reader :candidates, :matches, :finder_patterns, :qr_bounds, :qr
|
4
|
+
include ImageDumper
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
initialize_storage
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.load(filename)
|
11
|
+
instance = new
|
12
|
+
instance.load_image(filename)
|
13
|
+
|
14
|
+
instance.scan(:horizontal)
|
15
|
+
instance.scan(:vertical)
|
16
|
+
|
17
|
+
instance.filter_candidates
|
18
|
+
instance.find_intersections
|
19
|
+
instance.set_qr_bounds
|
20
|
+
|
21
|
+
instance.build_normalized_qr
|
22
|
+
instance.find_alignment_pattern
|
23
|
+
|
24
|
+
instance
|
25
|
+
end
|
26
|
+
|
27
|
+
def load_image(filename)
|
28
|
+
initialize_storage
|
29
|
+
|
30
|
+
image_type = File.extname(filename).upcase.gsub('.', '')
|
31
|
+
image_loader_class = "#{ image_type }ImageLoader"
|
32
|
+
image_loader_class = ImageLoader.const_get(image_loader_class) rescue nil
|
33
|
+
|
34
|
+
if image_loader_class.nil?
|
35
|
+
raise "Image type '#{ image_type }' not supported"
|
36
|
+
end
|
37
|
+
|
38
|
+
@input_matrix = image_loader_class.load(filename)
|
39
|
+
end
|
40
|
+
|
41
|
+
def scan(direction)
|
42
|
+
vectors = direction == :horizontal ? @input_matrix.rows : @input_matrix.columns
|
43
|
+
vectors.each_with_index do |vector, offset|
|
44
|
+
pattern = rle(vector)
|
45
|
+
|
46
|
+
if pattern.length >= 5
|
47
|
+
origin = 0
|
48
|
+
segment = pattern.slice!(0,4)
|
49
|
+
|
50
|
+
while next_length = pattern.shift
|
51
|
+
segment << next_length
|
52
|
+
|
53
|
+
if candidate = find_candidate(offset, origin, segment, direction)
|
54
|
+
add_candidate(candidate, direction)
|
55
|
+
end
|
56
|
+
|
57
|
+
origin += segment.shift
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def find_candidate(offset, origin, segment, direction)
|
64
|
+
FinderPatternSlice.build_matching(offset, origin, segment, direction)
|
65
|
+
end
|
66
|
+
|
67
|
+
def add_candidate(new_candidate, direction)
|
68
|
+
added = false
|
69
|
+
|
70
|
+
@candidates[direction].each_with_index do |existing, index|
|
71
|
+
if new_candidate.adjacent?(existing)
|
72
|
+
@candidates[direction][index] = existing.union(new_candidate)
|
73
|
+
added = true
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
@candidates[direction] << new_candidate unless added
|
78
|
+
end
|
79
|
+
|
80
|
+
def filter_candidates
|
81
|
+
[:horizontal, :vertical].each do |direction|
|
82
|
+
@candidates[direction].uniq.each do |candidate|
|
83
|
+
@matches[direction] << candidate if candidate.matches_aspect_ratio?
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# transform a vector of bits in to a run-length encoded vector of widths
|
89
|
+
# example: [1, 1, 1, 1, 0, 0, 1, 1, 1] => [4, 2, 3]
|
90
|
+
def rle(vector)
|
91
|
+
v = vector.dup
|
92
|
+
|
93
|
+
pattern = []
|
94
|
+
length = 1
|
95
|
+
last = v.shift
|
96
|
+
|
97
|
+
v.each do |current|
|
98
|
+
if current === last
|
99
|
+
length += 1
|
100
|
+
else
|
101
|
+
pattern << length
|
102
|
+
length = 1
|
103
|
+
last = current
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
pattern << length
|
108
|
+
end
|
109
|
+
|
110
|
+
# find intersections of horizontal and vertical slices, these
|
111
|
+
# are (likely) finder patterns
|
112
|
+
def find_intersections
|
113
|
+
@matches[:horizontal].each do |h|
|
114
|
+
@matches[:vertical].each do |v|
|
115
|
+
@finder_patterns << h.union(v) if h.intersects?(v)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def set_qr_bounds
|
121
|
+
if @finder_patterns.length >= 3
|
122
|
+
@sampling_grid = SamplingGrid.new(@input_matrix, @finder_patterns)
|
123
|
+
@qr_bounds = @sampling_grid.bounds
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# extract the qr into a smaller matrix and rotate to standard orientation
|
128
|
+
def build_normalized_qr
|
129
|
+
return false if @sampling_grid.nil?
|
130
|
+
|
131
|
+
original_orientation = @sampling_grid.orientation
|
132
|
+
|
133
|
+
@sampling_grid.normalize
|
134
|
+
@extracted_matrix = @sampling_grid.matrix
|
135
|
+
|
136
|
+
bits = []
|
137
|
+
@sampling_grid.extracted_pixels do |x, y|
|
138
|
+
bits << @extracted_matrix[x, y]
|
139
|
+
end
|
140
|
+
@qr = QrMatrix.new(bits, @sampling_grid.logical_width, @sampling_grid.logical_height)
|
141
|
+
|
142
|
+
@translated_matches = {
|
143
|
+
:horizontal => [],
|
144
|
+
:vertical => []
|
145
|
+
}
|
146
|
+
@translated_finder_patterns = []
|
147
|
+
@translated_neighbors = []
|
148
|
+
|
149
|
+
@matches[:horizontal].each do |m|
|
150
|
+
m = m.translate(*@qr_bounds.top_left)
|
151
|
+
if original_orientation > 0
|
152
|
+
(4 - original_orientation).times do
|
153
|
+
m = m.rotate(@qr_bounds.width, @qr_bounds.height)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
@translated_matches[:horizontal] << m
|
157
|
+
end
|
158
|
+
|
159
|
+
@matches[:vertical].each do |m|
|
160
|
+
m = m.translate(*@qr_bounds.top_left)
|
161
|
+
if original_orientation > 0
|
162
|
+
(4 - original_orientation).times do
|
163
|
+
m = m.rotate(@qr_bounds.width, @qr_bounds.height)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
@translated_matches[:vertical] << m
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def find_alignment_pattern
|
171
|
+
test_x = @sampling_grid.top_right.center.first - (@sampling_grid.block_width * 3)
|
172
|
+
test_y = @sampling_grid.bottom_left.center.last - (@sampling_grid.block_height * 3)
|
173
|
+
|
174
|
+
test_x = (test_x - @sampling_grid.block_width / 2.0).round
|
175
|
+
test_y = (test_y - @sampling_grid.block_height / 2.0).round
|
176
|
+
|
177
|
+
# TODO : this is where the AP *should* be, given no image skew.
|
178
|
+
# starting here, find center of nearest blob that "looks" like an AP.
|
179
|
+
# offset from predicted will be used in sampling grid to account for skew
|
180
|
+
@alignment_pattern = Qrio::Region.new(
|
181
|
+
test_x, test_y,
|
182
|
+
(test_x + @sampling_grid.block_width).round,
|
183
|
+
(test_y + @sampling_grid.block_height).round
|
184
|
+
)
|
185
|
+
end
|
186
|
+
|
187
|
+
private
|
188
|
+
|
189
|
+
def initialize_storage
|
190
|
+
@candidates = {
|
191
|
+
:horizontal => [],
|
192
|
+
:vertical => [],
|
193
|
+
}
|
194
|
+
@matches = {
|
195
|
+
:horizontal => [],
|
196
|
+
:vertical => [],
|
197
|
+
}
|
198
|
+
@finder_patterns = []
|
199
|
+
@input_matrix = @extracted_matrix = nil
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def self.decode(filename)
|
204
|
+
qr = Qr.load(filename)
|
205
|
+
qr.decoded? ? qr.text : qr
|
206
|
+
end
|
207
|
+
end
|
@@ -0,0 +1,403 @@
|
|
1
|
+
module Qrio
|
2
|
+
class QrMatrix < Matrix
|
3
|
+
def initialize(*args)
|
4
|
+
super
|
5
|
+
@unmasked = false
|
6
|
+
end
|
7
|
+
|
8
|
+
FORMAT_MASK = 0b101_0100_0001_0010
|
9
|
+
ERROR_CORRECTION_LEVEL = %w( M L H Q )
|
10
|
+
MODE = {
|
11
|
+
1 => :numeric,
|
12
|
+
2 => :alphanumeric,
|
13
|
+
4 => :ascii,
|
14
|
+
8 => :kanji
|
15
|
+
}
|
16
|
+
WORD_WIDTHS = {
|
17
|
+
:numeric => { 1..9 => 10, 10..26 => 12, 27..40 => 14 },
|
18
|
+
:alphanumeric => { 1..9 => 9, 10..26 => 11, 27..40 => 13 },
|
19
|
+
:ascii => { 1..9 => 8, 10..26 => 16, 27..40 => 16 },
|
20
|
+
:kanji => { 1..9 => 8, 10..26 => 10, 27..40 => 12 }
|
21
|
+
}
|
22
|
+
ALIGNMENT_CENTERS = [
|
23
|
+
[],
|
24
|
+
[6, 18],
|
25
|
+
[6, 22],
|
26
|
+
[6, 26],
|
27
|
+
[6, 30],
|
28
|
+
[6, 34],
|
29
|
+
[6, 22, 38],
|
30
|
+
[6, 24, 42],
|
31
|
+
[6, 26, 46],
|
32
|
+
[6, 28, 50],
|
33
|
+
|
34
|
+
[6, 30, 54],
|
35
|
+
[6, 32, 58],
|
36
|
+
[6, 34, 62],
|
37
|
+
[6, 26, 46, 66],
|
38
|
+
[6, 26, 48, 70],
|
39
|
+
[6, 26, 50, 74],
|
40
|
+
[6, 30, 54, 78],
|
41
|
+
[6, 30, 56, 82],
|
42
|
+
[6, 30, 58, 86],
|
43
|
+
[6, 34, 62, 90],
|
44
|
+
|
45
|
+
[6, 28, 50, 72, 94],
|
46
|
+
[6, 26, 50, 74, 98],
|
47
|
+
[6, 30, 54, 78, 102],
|
48
|
+
[6, 28, 54, 80, 106],
|
49
|
+
[6, 32, 58, 84, 110],
|
50
|
+
[6, 30, 58, 86, 114],
|
51
|
+
[6, 34, 62, 90, 118],
|
52
|
+
[6, 26, 50, 74, 98, 122],
|
53
|
+
[6, 30, 54, 78, 102, 126],
|
54
|
+
[6, 26, 52, 78, 104, 130],
|
55
|
+
|
56
|
+
[6, 26, 52, 78, 104, 130],
|
57
|
+
[6, 30, 56, 82, 108, 134],
|
58
|
+
[6, 34, 60, 86, 112, 138],
|
59
|
+
[6, 30, 58, 86, 114, 142],
|
60
|
+
[6, 34, 62, 90, 118, 146],
|
61
|
+
[6, 30, 54, 78, 102, 126, 150],
|
62
|
+
[6, 24, 50, 76, 102, 128, 154],
|
63
|
+
[6, 28, 54, 80, 106, 132, 158],
|
64
|
+
[6, 32, 58, 84, 110, 136, 162],
|
65
|
+
[6, 26, 54, 82, 110, 138, 166],
|
66
|
+
[6, 30, 58, 86, 114, 142, 170]
|
67
|
+
]
|
68
|
+
|
69
|
+
BLOCK_STRUCTURE = [
|
70
|
+
# to determine block structure, find the row corresponding to QR version
|
71
|
+
# (row 0 = version 1), then find the column corresponding to ECC level
|
72
|
+
# (M / L / H / Q)
|
73
|
+
#
|
74
|
+
# the result array will be:
|
75
|
+
# * block count
|
76
|
+
# * number of data bytes per block
|
77
|
+
# * nubmer of error correction bytes per block
|
78
|
+
# * (optional) number of blocks with one additional data byte
|
79
|
+
#
|
80
|
+
[[ 1, 16, 10 ], [ 1, 19, 7 ], [ 1, 9, 17 ], [ 1, 13, 13, ]],
|
81
|
+
[[ 1, 28, 16 ], [ 1, 34, 10 ], [ 1, 16, 28 ], [ 1, 22, 22, ]],
|
82
|
+
[[ 1, 44, 26 ], [ 1, 55, 15 ], [ 2, 13, 22 ], [ 2, 17, 18, ]],
|
83
|
+
[[ 2, 32, 18 ], [ 1, 80, 20 ], [ 4, 9, 16 ], [ 2, 24, 26, ]],
|
84
|
+
[[ 2, 43, 24 ], [ 1, 108, 26 ], [ 2, 11, 22, 2], [ 2, 15, 18, 2]],
|
85
|
+
[[ 4, 27, 16 ], [ 2, 68, 18 ], [ 4, 15, 28, ], [ 4, 19, 24, ]],
|
86
|
+
[[ 4, 31, 18 ], [ 2, 78, 20 ], [ 4, 13, 26, 1], [ 2, 14, 18, 4]],
|
87
|
+
[[ 2, 38, 22, 2], [ 2, 97, 24 ], [ 4, 14, 26, 2], [ 4, 18, 22, 2]],
|
88
|
+
[[ 3, 36, 22, 2], [ 2, 116, 30 ], [ 4, 12, 24, 4], [ 4, 16, 20, 4]],
|
89
|
+
[[ 4, 43, 26, 1], [ 2, 68, 18, 2], [ 6, 15, 28, 2], [ 6, 19, 24, 2]],
|
90
|
+
|
91
|
+
[[ 1, 50, 30, 4], [ 4, 81, 20 ], [ 3, 12, 24, 8], [ 4, 22, 28, 4]],
|
92
|
+
[[ 6, 36, 22, 2], [ 2, 92, 24, 2], [ 7, 14, 28, 4], [ 4, 20, 26, 6]],
|
93
|
+
[[ 8, 37, 22, 1], [ 4, 107, 26 ], [12, 11, 22, 4], [ 8, 20, 24, 4]],
|
94
|
+
[[ 4, 40, 24, 5], [ 3, 115, 30, 1], [11, 12, 24, 5], [11, 16, 20, 5]],
|
95
|
+
[[ 5, 41, 24, 5], [ 5, 87, 22, 1], [11, 12, 24, 7], [ 5, 24, 30, 7]],
|
96
|
+
[[ 7, 45, 28, 3], [ 5, 98, 24, 1], [ 3, 15, 30, 13], [15, 19, 24, 2]],
|
97
|
+
[[10, 46, 28, 1], [ 1, 107, 28, 5], [ 2, 14, 28, 17], [ 1, 22, 28, 15]],
|
98
|
+
[[ 9, 43, 26, 4], [ 5, 120, 30, 1], [ 2, 14, 28, 19], [17, 22, 28, 1]],
|
99
|
+
[[ 3, 44, 26, 11], [ 3, 113, 28, 4], [ 9, 13, 26, 16], [17, 21, 26, 4]],
|
100
|
+
[[ 3, 41, 26, 13], [ 3, 107, 20, 5], [15, 15, 28, 10], [15, 24, 30, 5]],
|
101
|
+
|
102
|
+
[[17, 42, 26 ], [ 4, 116, 28, 4], [19, 16, 30, 6], [17, 22, 28, 6]],
|
103
|
+
[[17, 46, 28 ], [ 2, 111, 28, 7], [34, 13, 24, ], [ 7, 24, 30, 16]],
|
104
|
+
[[ 4, 47, 28, 14], [ 4, 121, 30, 5], [16, 15, 30, 14], [11, 24, 30, 14]],
|
105
|
+
[[ 6, 45, 28, 14], [ 6, 117, 30, 4], [30, 16, 30, 2], [11, 24, 30, 16]],
|
106
|
+
[[ 8, 47, 28, 13], [ 8, 106, 26, 4], [22, 15, 30, 13], [ 7, 24, 30, 22]],
|
107
|
+
[[19, 46, 28, 4], [10, 114, 28, 2], [33, 16, 30, 4], [28, 22, 28, 6]],
|
108
|
+
[[22, 45, 28, 3], [ 8, 122, 30, 4], [12, 15, 30, 28], [ 8, 23, 30, 26]],
|
109
|
+
[[ 3, 45, 28, 23], [ 3, 117, 30, 10], [11, 15, 30, 31], [ 4, 24, 30, 31]],
|
110
|
+
[[21, 45, 28, 7], [ 7, 116, 30, 7], [19, 15, 30, 26], [ 1, 23, 30, 37]],
|
111
|
+
[[19, 47, 28, 10], [ 5, 115, 30, 10], [23, 15, 30, 25], [15, 24, 30, 25]],
|
112
|
+
|
113
|
+
[[ 2, 46, 28, 29], [13, 115, 30, 3], [23, 15, 30, 28], [42, 24, 30, 1]],
|
114
|
+
[[10, 46, 28, 23], [17, 115, 30 ], [19, 15, 30, 35], [10, 24, 30, 35]],
|
115
|
+
[[14, 46, 28, 21], [17, 115, 30, 1], [11, 15, 30, 46], [29, 24, 30, 19]],
|
116
|
+
[[14, 46, 28, 23], [13, 115, 30, 6], [59, 16, 30, 1], [44, 24, 30, 7]],
|
117
|
+
[[12, 47, 28, 26], [12, 121, 30, 7], [22, 15, 30, 41], [39, 24, 30, 14]],
|
118
|
+
[[ 6, 47, 28, 34], [ 6, 121, 30, 14], [ 2, 15, 30, 64], [46, 24, 30, 10]],
|
119
|
+
[[29, 46, 28, 14], [17, 122, 30, 4], [24, 15, 30, 46], [49, 24, 30, 10]],
|
120
|
+
[[13, 46, 28, 32], [ 4, 122, 30, 18], [42, 15, 30, 32], [48, 24, 30, 14]],
|
121
|
+
[[40, 47, 28, 7], [20, 117, 30, 4], [10, 15, 30, 67], [43, 24, 30, 22]],
|
122
|
+
[[18, 47, 28, 31], [19, 118, 30, 6], [20, 15, 30, 61], [34, 24, 30, 34]]
|
123
|
+
]
|
124
|
+
|
125
|
+
def error_correction_level
|
126
|
+
ERROR_CORRECTION_LEVEL[read_format[:error_correction]]
|
127
|
+
end
|
128
|
+
|
129
|
+
def mask_pattern
|
130
|
+
read_format[:mask_pattern]
|
131
|
+
end
|
132
|
+
|
133
|
+
def version
|
134
|
+
(width - 17) / 4
|
135
|
+
end
|
136
|
+
|
137
|
+
def unmask
|
138
|
+
p = [
|
139
|
+
lambda{|x,y| (x + y) % 2 == 0 },
|
140
|
+
lambda{|x,y| x % 2 == 0 },
|
141
|
+
lambda{|x,y| y % 3 == 0 },
|
142
|
+
lambda{|x,y| (x + y) % 3 == 0 },
|
143
|
+
lambda{|x,y| ((x / 2) + (y / 3)) % 2 == 0 },
|
144
|
+
lambda{|x,y| prod = x * y; (prod % 2) + (prod % 3) == 0 },
|
145
|
+
lambda{|x,y| prod = x * y; (((prod % 2) + (prod % 3)) % 2) == 0 },
|
146
|
+
lambda{|x,y| prod = x * y; sum = x + y; (((prod % 3) + (sum % 2)) % 2) == 0 }
|
147
|
+
][mask_pattern]
|
148
|
+
|
149
|
+
raise "could not load mask pattern #{ mask_pattern }" unless p
|
150
|
+
|
151
|
+
0.upto(height - 1) do |y|
|
152
|
+
0.upto(width - 1) do |x|
|
153
|
+
if data_or_correction?(x, y)
|
154
|
+
self[x, y] = self[x, y] ^ p.call(x, y)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
@unmasked = ! @unmasked
|
160
|
+
end
|
161
|
+
|
162
|
+
# raw bytestream, as read from the QR symbol
|
163
|
+
def raw_bytes
|
164
|
+
@raw_bytes ||= read_raw_bytes
|
165
|
+
end
|
166
|
+
|
167
|
+
def to_s
|
168
|
+
str = ""
|
169
|
+
rows.each do |row|
|
170
|
+
row.each do |m|
|
171
|
+
str << (m ? '#' : ' ')
|
172
|
+
end
|
173
|
+
str << "\n"
|
174
|
+
end
|
175
|
+
|
176
|
+
str
|
177
|
+
end
|
178
|
+
|
179
|
+
def text
|
180
|
+
@text ||= begin
|
181
|
+
text = []
|
182
|
+
|
183
|
+
unmask unless @unmasked
|
184
|
+
|
185
|
+
# deinterlace
|
186
|
+
@blocks = []
|
187
|
+
|
188
|
+
byte_pointer = 0
|
189
|
+
|
190
|
+
# TODO : handle ragged block sizes
|
191
|
+
block_structure.each do |count, data, ecc|
|
192
|
+
data.times do |word_index|
|
193
|
+
block_count.times do |blk_index|
|
194
|
+
@blocks[blk_index] ||= []
|
195
|
+
@blocks[blk_index] << raw_bytes[byte_pointer]
|
196
|
+
byte_pointer += 1
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
@blocks = @blocks.flatten
|
202
|
+
|
203
|
+
set_mode
|
204
|
+
|
205
|
+
character_count = read
|
206
|
+
|
207
|
+
character_count.times do |idx|
|
208
|
+
byte = read
|
209
|
+
text << byte.chr
|
210
|
+
end
|
211
|
+
|
212
|
+
text.join
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def mode
|
217
|
+
MODE[@mode]
|
218
|
+
end
|
219
|
+
|
220
|
+
def word_size
|
221
|
+
@word_size ||= begin
|
222
|
+
widths = WORD_WIDTHS[mode]
|
223
|
+
version_width = widths.detect{|k,v| k.include? version }
|
224
|
+
|
225
|
+
raise "Could not find word width" if version_width.nil?
|
226
|
+
|
227
|
+
version_width.last
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def block_count
|
232
|
+
block_structure.inject(0){|sum, (blocks,data,ecc)| sum += blocks }
|
233
|
+
end
|
234
|
+
|
235
|
+
def block_structure
|
236
|
+
@block_structure ||= begin
|
237
|
+
@short_blocks = []
|
238
|
+
@long_blocks = []
|
239
|
+
|
240
|
+
params = block_structure_params.dup
|
241
|
+
|
242
|
+
@short_blocks = params.slice!(0,3)
|
243
|
+
structure = [@short_blocks]
|
244
|
+
|
245
|
+
unless params.empty?
|
246
|
+
@long_blocks = @short_blocks.dup
|
247
|
+
@long_blocks[0] = params[0]
|
248
|
+
structure << @long_blocks
|
249
|
+
end
|
250
|
+
|
251
|
+
structure
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
def ecc_bytes_per_block
|
256
|
+
block_structure.first.last
|
257
|
+
end
|
258
|
+
|
259
|
+
def data_or_correction?(x, y)
|
260
|
+
! in_finder_pattern?(x, y) &&
|
261
|
+
! in_alignment_pattern?(x, y) &&
|
262
|
+
! in_alignment_line?(x, y)
|
263
|
+
end
|
264
|
+
|
265
|
+
def in_finder_pattern?(x, y)
|
266
|
+
(x < 9 && y < 9) ||
|
267
|
+
(x > (width - 9) && y < 9) ||
|
268
|
+
(x < 9 && y > (height - 9))
|
269
|
+
end
|
270
|
+
|
271
|
+
def in_alignment_pattern?(x, y)
|
272
|
+
return false if version == 1
|
273
|
+
|
274
|
+
alignment_centers = ALIGNMENT_CENTERS[version - 1]
|
275
|
+
|
276
|
+
cy = alignment_centers.detect{|c| (c - y).abs <= 2 }
|
277
|
+
cx = alignment_centers.detect{|c| (c - x).abs <= 2 }
|
278
|
+
|
279
|
+
cx && cy && ! in_finder_pattern?(cx, cy)
|
280
|
+
end
|
281
|
+
|
282
|
+
def draw_alignment_patterns
|
283
|
+
rows = ALIGNMENT_CENTERS[version - 1].dup
|
284
|
+
cols = rows.dup
|
285
|
+
|
286
|
+
cols.each do |cy|
|
287
|
+
rows.each do |cx|
|
288
|
+
unless in_finder_pattern?(cx, cy)
|
289
|
+
((cy - 2)...(cy + 2)).each do |y|
|
290
|
+
((cx - 2)...(cx + 2)).each do |x|
|
291
|
+
self[x, y] = (cx - x).abs == 2 ||
|
292
|
+
(cy - y).abs == 2 ||
|
293
|
+
(x == cx && y == cy)
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
def in_alignment_line?(x, y)
|
302
|
+
(x == 6) || (y == 6)
|
303
|
+
end
|
304
|
+
|
305
|
+
private
|
306
|
+
|
307
|
+
def set_mode
|
308
|
+
@mode ||= begin
|
309
|
+
@pointer ||= 0
|
310
|
+
mode_number = read(4)
|
311
|
+
end
|
312
|
+
raise "Unknown mode #{ @mode }" unless mode
|
313
|
+
end
|
314
|
+
|
315
|
+
def set_data_length
|
316
|
+
@data_length ||= read
|
317
|
+
end
|
318
|
+
|
319
|
+
def block_structure_params
|
320
|
+
BLOCK_STRUCTURE[version - 1][read_format[:error_correction]].dup
|
321
|
+
end
|
322
|
+
|
323
|
+
# read +bits+ bits from bitstream and return the binary
|
324
|
+
def read(bits=nil)
|
325
|
+
bits ||= word_size
|
326
|
+
binary = []
|
327
|
+
|
328
|
+
bits.times do |i|
|
329
|
+
block_index, bit_index = @pointer.divmod(8)
|
330
|
+
data = @blocks[block_index] || 0
|
331
|
+
binary << (((data >> (7 - bit_index)) & 1) == 1)
|
332
|
+
@pointer += 1
|
333
|
+
end
|
334
|
+
|
335
|
+
binary.map{|b| b ? '1' : '0' }.join.to_i(2)
|
336
|
+
end
|
337
|
+
|
338
|
+
def read_raw_bytes
|
339
|
+
@raw_bytes = []
|
340
|
+
@byte = []
|
341
|
+
|
342
|
+
(0..(width - 3)).step(2) do |bcol|
|
343
|
+
bcol = width - 1 - bcol
|
344
|
+
scanning_up = ((bcol / 2) % 2) == 0
|
345
|
+
bcol -= 1 if bcol <= 6
|
346
|
+
|
347
|
+
(0..(height - 1)).each do |brow|
|
348
|
+
brow = height - 1 - brow if scanning_up
|
349
|
+
|
350
|
+
add_bit(bcol, brow)
|
351
|
+
add_bit(bcol - 1, brow)
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
@raw_bytes
|
356
|
+
end
|
357
|
+
|
358
|
+
def add_bit(x, y)
|
359
|
+
if data_or_correction?(x, y)
|
360
|
+
@byte.push self[x, y]
|
361
|
+
|
362
|
+
if @byte.length == 8
|
363
|
+
@raw_bytes << @byte.map{|b| b ? '1' : '0' }.join.to_i(2)
|
364
|
+
@byte = []
|
365
|
+
end
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
def read_format
|
370
|
+
@format ||= begin
|
371
|
+
bits = 0
|
372
|
+
|
373
|
+
0.upto(5) do |x|
|
374
|
+
bits = bits << 1
|
375
|
+
bits += 1 if self[x, 8]
|
376
|
+
end
|
377
|
+
|
378
|
+
bits = bits << 1
|
379
|
+
bits += 1 if self[7, 8]
|
380
|
+
bits = bits << 1
|
381
|
+
bits += 1 if self[8, 8]
|
382
|
+
bits = bits << 1
|
383
|
+
bits += 1 if self[8, 7]
|
384
|
+
|
385
|
+
5.downto(0) do |y|
|
386
|
+
bits = bits << 1
|
387
|
+
bits += 1 if self[8, y]
|
388
|
+
end
|
389
|
+
|
390
|
+
format_string = (bits ^ FORMAT_MASK).to_s(2).rjust(15, '0')
|
391
|
+
|
392
|
+
# TODO check BCH error detection
|
393
|
+
# TODO if too many errors, read alternate format blocks
|
394
|
+
|
395
|
+
{
|
396
|
+
:error_correction => format_string[0,2].to_i(2),
|
397
|
+
:mask_pattern => format_string[2,3].to_i(2),
|
398
|
+
:bch_error_detection => format_string[5..-1].to_i(2)
|
399
|
+
}
|
400
|
+
end
|
401
|
+
end
|
402
|
+
end
|
403
|
+
end
|