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.
@@ -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
+
@@ -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