qr_reader 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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +16 -0
  3. data/LICENSE.txt +21 -0
  4. data/NOTICE +14 -0
  5. data/README.md +222 -0
  6. data/exe/qr_reader +66 -0
  7. data/lib/qr_reader/binarizer.rb +170 -0
  8. data/lib/qr_reader/bit_matrix.rb +120 -0
  9. data/lib/qr_reader/decoder/bitstream_parser.rb +307 -0
  10. data/lib/qr_reader/decoder/character_set.rb +163 -0
  11. data/lib/qr_reader/decoder/codewords.rb +164 -0
  12. data/lib/qr_reader/decoder/data_mask.rb +57 -0
  13. data/lib/qr_reader/decoder/format_reader.rb +204 -0
  14. data/lib/qr_reader/decoder/function_patterns.rb +62 -0
  15. data/lib/qr_reader/decoder/galois_field.rb +69 -0
  16. data/lib/qr_reader/decoder/reed_solomon.rb +312 -0
  17. data/lib/qr_reader/decoder.rb +241 -0
  18. data/lib/qr_reader/detector/alignment_grid.rb +127 -0
  19. data/lib/qr_reader/detector/alignment_pattern_finder.rb +191 -0
  20. data/lib/qr_reader/detector/candidate/dimension.rb +237 -0
  21. data/lib/qr_reader/detector/candidate/strokes.rb +126 -0
  22. data/lib/qr_reader/detector/candidate.rb +436 -0
  23. data/lib/qr_reader/detector/finder_pattern.rb +63 -0
  24. data/lib/qr_reader/detector/finder_pattern_finder.rb +199 -0
  25. data/lib/qr_reader/detector/geometry.rb +118 -0
  26. data/lib/qr_reader/detector/grid_sampler.rb +350 -0
  27. data/lib/qr_reader/detector/pattern_sets.rb +235 -0
  28. data/lib/qr_reader/detector/perspective_transform.rb +117 -0
  29. data/lib/qr_reader/detector/pure_detector.rb +197 -0
  30. data/lib/qr_reader/detector.rb +215 -0
  31. data/lib/qr_reader/errors.rb +14 -0
  32. data/lib/qr_reader/format_information.rb +79 -0
  33. data/lib/qr_reader/formats/bmp.rb +434 -0
  34. data/lib/qr_reader/formats/ccitt.rb +81 -0
  35. data/lib/qr_reader/formats/gif.rb +272 -0
  36. data/lib/qr_reader/formats/jpeg.rb +152 -0
  37. data/lib/qr_reader/formats/pdf.rb +2935 -0
  38. data/lib/qr_reader/formats/png.rb +311 -0
  39. data/lib/qr_reader/formats/pnm.rb +286 -0
  40. data/lib/qr_reader/formats/tiff.rb +1345 -0
  41. data/lib/qr_reader/formats.rb +279 -0
  42. data/lib/qr_reader/image.rb +231 -0
  43. data/lib/qr_reader/kernels/binarizer.rb +475 -0
  44. data/lib/qr_reader/kernels/ccitt.rb +554 -0
  45. data/lib/qr_reader/kernels/detector.rb +587 -0
  46. data/lib/qr_reader/kernels/image.rb +65 -0
  47. data/lib/qr_reader/kernels/jpeg.rb +3421 -0
  48. data/lib/qr_reader/kernels/lzw.rb +173 -0
  49. data/lib/qr_reader/kernels/pixels.rb +116 -0
  50. data/lib/qr_reader/kernels/png.rb +1138 -0
  51. data/lib/qr_reader/kernels.rb +14 -0
  52. data/lib/qr_reader/native_kernels.rb +130 -0
  53. data/lib/qr_reader/reader.rb +348 -0
  54. data/lib/qr_reader/result.rb +49 -0
  55. data/lib/qr_reader/symbol_version.rb +173 -0
  56. data/lib/qr_reader/version.rb +5 -0
  57. data/lib/qr_reader.rb +77 -0
  58. metadata +103 -0
@@ -0,0 +1,475 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Binarizer kernels: local-threshold binarization of a grayscale buffer
4
+ # into a bit buffer (one byte per pixel, 1 = dark). Kernel files are plain
5
+ # Ruby in the subset that Spinel compiles (see DESIGN.md, "Kernels"): the
6
+ # optional qr_reader-native gem builds them into a C extension, otherwise
7
+ # they run as they are. See QrReader::Binarizer for the methods.
8
+ module QrReaderKernels
9
+ # Blocks with a luminance range up to this are treated as flat.
10
+ BIN_MIN_RANGE = 24
11
+
12
+ # zxing-cpp-like hybrid binarization of a width x height (both >= 40)
13
+ # gray buffer: per 8x8 block the threshold (min + max) / 2 if it has
14
+ # contrast, thresholds across edges that run exactly along block borders,
15
+ # 5x5 block means of the thresholds, gaps filled in scan order. Returns
16
+ # the bit buffer.
17
+ # spinel-sample: QrReaderKernels.binarize_hybrid("\x10\xf0\x80\x20".b * 400, 40, 40)
18
+ def self.binarize_hybrid(gray, width, height)
19
+ sub_width = (width + 7) >> 3
20
+ sub_height = (height + 7) >> 3
21
+ size = sub_width * sub_height
22
+ block = Array.new(size, 0)
23
+ lows = Array.new(size, 0)
24
+ highs = Array.new(size, 0)
25
+ by = 0
26
+ while by < sub_height
27
+ y0 = by << 3
28
+ y0 = height - 8 if y0 > height - 8
29
+ bin_midranges_row(gray, width, y0 * width, block, lows, highs, by * sub_width, sub_width)
30
+ by += 1
31
+ end
32
+ by = 0
33
+ while by < sub_height
34
+ bin_bridge_row(block, lows, highs, by * sub_width, sub_width, by + 1 < sub_height)
35
+ by += 1
36
+ end
37
+ thresholds = bin_smooth(block, sub_width, sub_height)
38
+ bin_apply(gray, width, height, thresholds, lows, highs)
39
+ end
40
+
41
+ # ZXing (Java) HybridBinarizer-like local means: per 8x8 block its mean,
42
+ # or for flat blocks half its minimum unless the neighbours suggest a dark
43
+ # area; averaged over 5x5 blocks. Returns the bit buffer (width, height
44
+ # >= 40).
45
+ # spinel-sample: QrReaderKernels.binarize_local_mean("\x10\xf0\x80\x20".b * 400, 40, 40)
46
+ def self.binarize_local_mean(gray, width, height)
47
+ sub_width = (width + 7) >> 3
48
+ sub_height = (height + 7) >> 3
49
+ size = sub_width * sub_height
50
+ points = Array.new(size, 0)
51
+ by = 0
52
+ while by < sub_height
53
+ y0 = by << 3
54
+ y0 = height - 8 if y0 > height - 8
55
+ bin_black_points_row(gray, width, y0 * width, points, by * sub_width, sub_width)
56
+ by += 1
57
+ end
58
+ hsum = Array.new(size, 0)
59
+ by = 0
60
+ while by < sub_height
61
+ bin_five_sums_row(points, hsum, by * sub_width, sub_width)
62
+ by += 1
63
+ end
64
+ means = Array.new(size, 0)
65
+ by = 0
66
+ while by < sub_height
67
+ centre = by
68
+ centre = 2 if centre < 2
69
+ centre = sub_height - 3 if centre > sub_height - 3
70
+ bin_five_means_row(hsum, means, by * sub_width, centre * sub_width, sub_width)
71
+ by += 1
72
+ end
73
+ # Block statistics that never classify a block as uniform.
74
+ lows = Array.new(size, 0)
75
+ highs = Array.new(size, 256)
76
+ bin_apply(gray, width, height, means, lows, highs)
77
+ end
78
+
79
+ # Black points of one row of 8x8 blocks whose top row starts at +off0+
80
+ # (edge blocks shifted inwards to stay inside the image).
81
+ def self.bin_black_points_row(data, width, off0, points, base, sub_width)
82
+ max_x = width - 8
83
+ bx = 0
84
+ while bx < sub_width
85
+ x0 = bx << 3
86
+ x0 = max_x if x0 > max_x
87
+ off = off0 + x0
88
+ sum = 0
89
+ min = 255
90
+ max = 0
91
+ rows = 0
92
+ while rows < 8
93
+ a = data.getbyte(off)
94
+ b = data.getbyte(off + 1)
95
+ c = data.getbyte(off + 2)
96
+ d = data.getbyte(off + 3)
97
+ e = data.getbyte(off + 4)
98
+ f = data.getbyte(off + 5)
99
+ g = data.getbyte(off + 6)
100
+ h = data.getbyte(off + 7)
101
+ sum += a + b + c + d + e + f + g + h
102
+ min = a if a < min
103
+ max = a if a > max
104
+ min = b if b < min
105
+ max = b if b > max
106
+ min = c if c < min
107
+ max = c if c > max
108
+ min = d if d < min
109
+ max = d if d > max
110
+ min = e if e < min
111
+ max = e if e > max
112
+ min = f if f < min
113
+ max = f if f > max
114
+ min = g if g < min
115
+ max = g if g > max
116
+ min = h if h < min
117
+ max = h if h > max
118
+ off += width
119
+ rows += 1
120
+ next if max - min <= BIN_MIN_RANGE
121
+
122
+ # Enough contrast already: only the sum is still needed.
123
+ while rows < 8
124
+ sum += data.getbyte(off) + data.getbyte(off + 1) + data.getbyte(off + 2) + data.getbyte(off + 3) +
125
+ data.getbyte(off + 4) + data.getbyte(off + 5) + data.getbyte(off + 6) + data.getbyte(off + 7)
126
+ off += width
127
+ rows += 1
128
+ end
129
+ end
130
+
131
+ point = sum >> 6
132
+ if max - min <= BIN_MIN_RANGE
133
+ # A flat block is assumed to be background (white it out) unless
134
+ # its neighbours suggest it lies inside a dark area.
135
+ point = min >> 1
136
+ if base > 0 && bx > 0
137
+ up = base - sub_width + bx
138
+ neighbours = (points[up] + 2 * points[base + bx - 1] + points[up - 1]) >> 2
139
+ point = neighbours if min < neighbours
140
+ end
141
+ end
142
+ points[base + bx] = point
143
+ bx += 1
144
+ end
145
+ end
146
+
147
+ # Sums over the 5 blocks around each block of one block row (window
148
+ # clamped to the row).
149
+ def self.bin_five_sums_row(values, hsum, base, sub_width)
150
+ last = sub_width - 3
151
+ bx = 0
152
+ while bx < sub_width
153
+ i = base + (bx < 2 ? 2 : (bx > last ? last : bx))
154
+ hsum[base + bx] = values[i - 2] + values[i - 1] + values[i] + values[i + 1] + values[i + 2]
155
+ bx += 1
156
+ end
157
+ end
158
+
159
+ # Means over 5 block rows (centred on the row starting at +centre+).
160
+ def self.bin_five_means_row(hsum, means, base, centre, sub_width)
161
+ two_rows = 2 * sub_width
162
+ bx = 0
163
+ while bx < sub_width
164
+ i = centre + bx
165
+ means[base + bx] = (hsum[i - two_rows] + hsum[i - sub_width] + hsum[i] +
166
+ hsum[i + sub_width] + hsum[i + two_rows]) / 25
167
+ bx += 1
168
+ end
169
+ end
170
+
171
+ # (min + max) / 2 of each block of one block row (top pixel row at
172
+ # +off0+), 0 for flat blocks; minima and maxima into +lows+ / +highs+.
173
+ # Min and max of each 8 pixel row segment come from pairwise
174
+ # comparisons (12 instead of 16 per segment).
175
+ def self.bin_midranges_row(data, width, off0, out, lows, highs, base, sub_width)
176
+ max_x = width - 8
177
+ bx = 0
178
+ while bx < sub_width
179
+ x0 = bx << 3
180
+ x0 = max_x if x0 > max_x
181
+ off = off0 + x0
182
+ min = 255
183
+ max = 0
184
+ stop = off + 8 * width
185
+ while off < stop
186
+ a = data.getbyte(off)
187
+ b = data.getbyte(off + 1)
188
+ if a < b
189
+ lo = a
190
+ hi = b
191
+ else
192
+ lo = b
193
+ hi = a
194
+ end
195
+ a = data.getbyte(off + 2)
196
+ b = data.getbyte(off + 3)
197
+ if a < b
198
+ lo = a if a < lo
199
+ hi = b if b > hi
200
+ else
201
+ lo = b if b < lo
202
+ hi = a if a > hi
203
+ end
204
+ a = data.getbyte(off + 4)
205
+ b = data.getbyte(off + 5)
206
+ if a < b
207
+ lo = a if a < lo
208
+ hi = b if b > hi
209
+ else
210
+ lo = b if b < lo
211
+ hi = a if a > hi
212
+ end
213
+ a = data.getbyte(off + 6)
214
+ b = data.getbyte(off + 7)
215
+ if a < b
216
+ lo = a if a < lo
217
+ hi = b if b > hi
218
+ else
219
+ lo = b if b < lo
220
+ hi = a if a > hi
221
+ end
222
+ min = lo if lo < min
223
+ max = hi if hi > max
224
+ off += width
225
+ end
226
+ i = base + bx
227
+ lows[i] = min
228
+ highs[i] = max
229
+ out[i] = max - min > BIN_MIN_RANGE ? (max + min) >> 1 : 0
230
+ bx += 1
231
+ end
232
+ end
233
+
234
+ # Gives flat blocks of the block row starting at +base+ a threshold when
235
+ # an edge runs along their border: a flat right (or lower) neighbour
236
+ # whose luminance range is more than BIN_MIN_RANGE away from theirs. Both
237
+ # get the threshold halfway across the gap (set thresholds are kept). A
238
+ # smooth gradient never qualifies.
239
+ def self.bin_bridge_row(block, lows, highs, base, sub_width, below)
240
+ i = base
241
+ last = base + sub_width - 1
242
+ while i <= last
243
+ lo = lows[i]
244
+ hi = highs[i]
245
+ if hi - lo <= BIN_MIN_RANGE
246
+ bin_bridge_pair(block, lows, highs, i, i + 1, lo, hi) if i < last
247
+ bin_bridge_pair(block, lows, highs, i, i + sub_width, lo, hi) if below
248
+ end
249
+ i += 1
250
+ end
251
+ end
252
+
253
+ # bin_bridge_row for flat block i (range lo..hi) and block j.
254
+ def self.bin_bridge_pair(block, lows, highs, i, j, lo, hi)
255
+ lo2 = lows[j]
256
+ hi2 = highs[j]
257
+ return if hi2 - lo2 > BIN_MIN_RANGE
258
+
259
+ t = 0
260
+ if lo2 - hi > BIN_MIN_RANGE
261
+ t = (hi + lo2) >> 1
262
+ elsif lo - hi2 > BIN_MIN_RANGE
263
+ t = (hi2 + lo) >> 1
264
+ else
265
+ return
266
+ end
267
+ block[i] = t if block[i] == 0
268
+ block[j] = t if block[j] == 0
269
+ end
270
+
271
+ # Mean over the 5x5 blocks around each block of the thresholds that are
272
+ # set (the block itself counts twice), via separable window sums of the
273
+ # thresholds and of an is-set indicator; then gaps filled in scan order.
274
+ def self.bin_smooth(block, sub_width, sub_height)
275
+ size = sub_width * sub_height
276
+ hsum = Array.new(size, 0)
277
+ hcount = Array.new(size, 0)
278
+ by = 0
279
+ while by < sub_height
280
+ bin_window_row(block, hsum, hcount, by * sub_width, sub_width)
281
+ by += 1
282
+ end
283
+ out = Array.new(size, 0)
284
+ by = 0
285
+ while by < sub_height
286
+ top = by
287
+ top = 2 if top < 2
288
+ top = sub_height - 3 if top > sub_height - 3
289
+ bin_window_means(block, hsum, hcount, out, by * sub_width, (top - 2) * sub_width, sub_width)
290
+ by += 1
291
+ end
292
+ bin_fill_gaps(out, sub_width)
293
+ out
294
+ end
295
+
296
+ # Sums and counts of the set (positive) thresholds over the 5 blocks
297
+ # around each block of one block row (window clamped to the row).
298
+ def self.bin_window_row(block, hsum, hcount, base, sub_width)
299
+ last = sub_width - 3
300
+ bx = 0
301
+ while bx < sub_width
302
+ i = base + (bx < 2 ? 0 : (bx > last ? last - 2 : bx - 2))
303
+ a = block[i]
304
+ b = block[i + 1]
305
+ c = block[i + 2]
306
+ d = block[i + 3]
307
+ e = block[i + 4]
308
+ sum = 0
309
+ count = 0
310
+ if a > 0
311
+ sum += a
312
+ count += 1
313
+ end
314
+ if b > 0
315
+ sum += b
316
+ count += 1
317
+ end
318
+ if c > 0
319
+ sum += c
320
+ count += 1
321
+ end
322
+ if d > 0
323
+ sum += d
324
+ count += 1
325
+ end
326
+ if e > 0
327
+ sum += e
328
+ count += 1
329
+ end
330
+ hsum[base + bx] = sum
331
+ hcount[base + bx] = count
332
+ bx += 1
333
+ end
334
+ end
335
+
336
+ # 5x5 means of one block row from the row sums of the 5 block rows
337
+ # starting at +top+ (0 where no threshold is set nearby).
338
+ def self.bin_window_means(block, hsum, hcount, out, base, top, sub_width)
339
+ s1 = sub_width
340
+ s2 = 2 * sub_width
341
+ s3 = 3 * sub_width
342
+ s4 = 4 * sub_width
343
+ bx = 0
344
+ while bx < sub_width
345
+ i = top + bx
346
+ sum = hsum[i] + hsum[i + s1] + hsum[i + s2] + hsum[i + s3] + hsum[i + s4]
347
+ count = hcount[i] + hcount[i + s1] + hcount[i + s2] + hcount[i + s3] + hcount[i + s4]
348
+ centre = block[base + bx]
349
+ if centre > 0
350
+ sum += 2 * centre
351
+ count += 2
352
+ end
353
+ out[base + bx] = count > 0 ? sum / count : 0
354
+ bx += 1
355
+ end
356
+ end
357
+
358
+ # Sets every 0 entry to the previous non-zero one in scan order (leading
359
+ # zeros to the first non-zero one); all zero (no contrast anywhere)
360
+ # becomes all -1: everything light.
361
+ def self.bin_fill_gaps(values, row_size)
362
+ n = values.size
363
+ first = 0
364
+ first += 1 while first < n && values[first] <= 0
365
+ if first == n
366
+ i = 0
367
+ while i < n
368
+ values[i] = -1
369
+ i += 1
370
+ end
371
+ return
372
+ end
373
+ last = values[first]
374
+ i = 0
375
+ while i < n
376
+ stop = i + row_size
377
+ stop = n if stop > n
378
+ last = bin_fill_span(values, i, stop, last)
379
+ i = stop
380
+ end
381
+ end
382
+
383
+ # bin_fill_gaps for values[i...stop]; returns the last non-zero value.
384
+ def self.bin_fill_span(values, i, stop, last)
385
+ while i < stop
386
+ t = values[i]
387
+ if t == 0
388
+ values[i] = last
389
+ else
390
+ last = t
391
+ end
392
+ i += 1
393
+ end
394
+ last
395
+ end
396
+
397
+ # Bit buffer of the pixels at or below their block's threshold (columns
398
+ # past the last full block use the last block). Blocks whose whole range
399
+ # (+lows+ / +highs+ over the 8x8 pixels the threshold was derived from,
400
+ # which contain every pixel that uses it) lies on one side of their
401
+ # threshold are filled without looking at the pixels.
402
+ def self.bin_apply(gray, width, height, thresholds, lows, highs)
403
+ sub_width = (width + 7) >> 3
404
+ patterns = bin_patterns
405
+ out = String.new(capacity: width * height, encoding: Encoding::BINARY)
406
+ y = 0
407
+ off = 0
408
+ while y < height
409
+ out << bin_apply_row(gray, off, width, thresholds, lows, highs, (y >> 3) * sub_width, sub_width, patterns)
410
+ off += width
411
+ y += 1
412
+ end
413
+ out
414
+ end
415
+
416
+ # The 256 rows of 8 bits: pattern m has byte k set iff bit k of m is.
417
+ def self.bin_patterns
418
+ patterns = []
419
+ m = 0
420
+ while m < 256
421
+ s = String.new(capacity: 8, encoding: Encoding::BINARY)
422
+ k = 0
423
+ while k < 8
424
+ s << ((m >> k) & 1)
425
+ k += 1
426
+ end
427
+ patterns << s
428
+ m += 1
429
+ end
430
+ patterns
431
+ end
432
+
433
+ # One row of bin_apply (block row starting at +base+). Each 8 pixel
434
+ # segment becomes one appended pattern (String#setbyte per pixel costs
435
+ # more than the comparisons).
436
+ def self.bin_apply_row(gray, off, width, thresholds, lows, highs, base, sub_width, patterns)
437
+ full = width >> 3
438
+ row = String.new(capacity: width, encoding: Encoding::BINARY)
439
+ light = patterns[0]
440
+ dark = patterns[255]
441
+ b = base
442
+ stop = base + full
443
+ i = off
444
+ while b < stop
445
+ t = thresholds[b]
446
+ if lows[b] > t
447
+ row << light
448
+ elsif highs[b] <= t
449
+ row << dark
450
+ else
451
+ m = 0
452
+ m |= 1 if gray.getbyte(i) <= t
453
+ m |= 2 if gray.getbyte(i + 1) <= t
454
+ m |= 4 if gray.getbyte(i + 2) <= t
455
+ m |= 8 if gray.getbyte(i + 3) <= t
456
+ m |= 16 if gray.getbyte(i + 4) <= t
457
+ m |= 32 if gray.getbyte(i + 5) <= t
458
+ m |= 64 if gray.getbyte(i + 6) <= t
459
+ m |= 128 if gray.getbyte(i + 7) <= t
460
+ row << patterns[m]
461
+ end
462
+ i += 8
463
+ b += 1
464
+ end
465
+ return row if full == sub_width
466
+
467
+ t = thresholds[base + sub_width - 1]
468
+ stop = off + width
469
+ while i < stop
470
+ row << (gray.getbyte(i) <= t ? 1 : 0)
471
+ i += 1
472
+ end
473
+ row
474
+ end
475
+ end