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,237 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QrReader
4
+ class Detector
5
+ class Candidate
6
+ # Dimension estimation for a Candidate: module sizes measured across
7
+ # the finder patterns, timing pattern counts and version information
8
+ # (read through the global transform and through frames local to the
9
+ # finders). Mixed into Candidate; relies on its @tl/@tr/@bl, @axes,
10
+ # @matrix, the per-finder module sizes and #transform_for / #stroke.
11
+ module Dimension
12
+ private
13
+
14
+ # Dimension estimates, most likely first: the timing pattern count
15
+ # if row and column agree (or only one is readable and it lies
16
+ # within the range of the per-side distance estimates), else the
17
+ # rounded mean distance estimate; then the per-side estimates and
18
+ # +-1, +-2 versions.
19
+ def dimension_candidates(estimate, top, left)
20
+ dims = [round_dimension(estimate), round_dimension(top), round_dimension(left)]
21
+ row, col = timing_dimensions
22
+ low, high = [top, left].minmax
23
+ timing = [row, col].compact.uniq
24
+ if timing.size == 1 && (row == col || timing[0].between?(low - 4.5, high + 4.5))
25
+ dims.unshift(timing[0])
26
+ else
27
+ timing.each { |t| dims.insert(1, t) if (t - estimate).abs <= 8 }
28
+ end
29
+ first = dims.first
30
+ dims.push(first - 4, first + 4, first - 8, first + 8)
31
+ dims.select { |d| d.between?(21, 177) }.uniq
32
+ end
33
+
34
+ # Nearest dimension of the form 4v + 17.
35
+ def round_dimension(estimate)
36
+ ((estimate - 17) / 4.0).round * 4 + 17
37
+ end
38
+
39
+ # Module size from the 1:1:3:1:1 spread of pattern +p+ along the unit
40
+ # direction (ux, uy); nil if the runs do not fit. If the exact runs
41
+ # do not fit, single noise pixels are bridged for patterns of 3+ px
42
+ # modules. (Not the other way round: modules along this direction
43
+ # may be much thinner - fax pages - and a one pixel light ring
44
+ # real; bridged, the runs can still pass the relaxed ratio test.)
45
+ def line_module_size(p, ux, uy)
46
+ limit = (p.module_size * 10).ceil + 2
47
+ spread_module_size(p, ux, uy, limit, false) ||
48
+ (p.module_size >= 3 && spread_module_size(p, ux, uy, limit, true)) || nil
49
+ end
50
+
51
+ def spread_module_size(p, ux, uy, limit, noise)
52
+ fwd = Geometry.runs(@matrix, p.x, p.y, ux, uy, limit, noise: noise) or return
53
+ back = Geometry.runs(@matrix, p.x, p.y, -ux, -uy, limit, noise: noise) or return
54
+ a, b, c, d, e = back[2], back[1], fwd[0] + back[0] - 1, fwd[1], fwd[2]
55
+ return unless Geometry.finder_ratio?(a, b, c, d, e, relaxed: true)
56
+
57
+ (a + b + c + d + e) / 7.0
58
+ end
59
+
60
+ # Dimensions counted along the timing patterns (row 6 from TL to TR,
61
+ # column 6 from TL to BL); each nil when unreadable.
62
+ def timing_dimensions
63
+ return [nil, nil] if @module_size < 1.2
64
+
65
+ right, down = @axes
66
+ row = count_timing(@tl.x + 3 * @tl_down * down[0], @tl.y + 3 * @tl_down * down[1],
67
+ @tr.x + 3 * @tr_down * down[0], @tr.y + 3 * @tr_down * down[1], @tl_right, @tr_left)
68
+ col = count_timing(@tl.x + 3 * @tl_right * right[0], @tl.y + 3 * @tl_right * right[1],
69
+ @bl.x + 3 * @bl_right * right[0], @bl.y + 3 * @bl_right * right[1], @tl_down, @bl_up)
70
+ [row, col]
71
+ end
72
+
73
+ # Walks from (x0, y0) to (x1, y1) (inside the dark rings of two finder
74
+ # patterns, along a timing pattern) and returns 2 * light runs + 13,
75
+ # or nil if the runs are not a clean alternation. Run lengths may
76
+ # drift along the line (perspective, curved surfaces) but neighbours
77
+ # must be similar and the ends must match the finders' module sizes.
78
+ def count_timing(x0, y0, x1, y1, ms0, ms1)
79
+ len = Math.hypot(x1 - x0, y1 - y0)
80
+ step = [ms0, ms1].min / 4.0
81
+ step = 0.5 if step < 0.5
82
+ n = (len / step).floor
83
+ return if n < 8
84
+
85
+ sx = (x1 - x0) / n
86
+ sy = (y1 - y0) / n
87
+ bits = @matrix.bits
88
+ w = @matrix.width
89
+ h = @matrix.height
90
+ runs = []
91
+ color = nil
92
+ length = 0
93
+ i = 0
94
+ while i <= n
95
+ x = x0 + sx * i
96
+ y = y0 + sy * i
97
+ return unless x >= 0 && y >= 0 && x < w && y < h
98
+
99
+ c = bits.getbyte(y.to_i * w + x.to_i)
100
+ if c == color
101
+ length += 1
102
+ else
103
+ runs << length if color
104
+ color = c
105
+ length = 1
106
+ end
107
+ i += 1
108
+ end
109
+ runs << length
110
+ runs = merge_noise_runs(runs) if [ms0, ms1].min / step >= 3.5
111
+ # Runs alternate dark/light starting and ending with dark (the rings).
112
+ return if runs.size < 3 || runs.size.even?
113
+
114
+ inner = runs[1..-2]
115
+ unit = len / n
116
+ return unless (inner.first * unit / ms0).between?(0.4, 2.0) && (inner.last * unit / ms1).between?(0.4, 2.0)
117
+
118
+ inner.each_cons(2) { |a, b| return nil if a > 3 * b || b > 3 * a }
119
+ inner.each_cons(3) { |a, _, b| return nil if a > 1.7 * b + 1 || b > 1.7 * a + 1 }
120
+ inner.size + 14
121
+ end
122
+
123
+ # Merges runs of a single sample (noise when modules span several
124
+ # samples) into their neighbours: [5, 1, 4] becomes [10].
125
+ def merge_noise_runs(runs)
126
+ out = []
127
+ i = 0
128
+ while i < runs.size
129
+ if runs[i] == 1 && !out.empty? && i + 1 < runs.size
130
+ out[-1] += 1 + runs[i + 1]
131
+ i += 2
132
+ else
133
+ out << runs[i]
134
+ i += 1
135
+ end
136
+ end
137
+ out
138
+ end
139
+
140
+ # The dimension confirmed by the version information blocks (versions
141
+ # 7+): the first candidate dimension for which a block, read through
142
+ # the global transform or through a frame local to its finder
143
+ # pattern, decodes to a version of exactly that dimension. Else the
144
+ # version the first readable block names, if it is close.
145
+ def version_dimension(dims)
146
+ dims.each do |d|
147
+ next if d < 45
148
+
149
+ t = transform_for(d, @mode) or next
150
+ next unless read_version(t, d).any? { |v| v && v.dimension == d } ||
151
+ local_versions(d).any? { |v| v && v.dimension == d }
152
+
153
+ @version_confirmed = true
154
+ return d
155
+ end
156
+ d = dims.find { |x| x >= 45 } or return
157
+ t = transform_for(d, @mode) or return
158
+ v = read_version(t, d).compact.first
159
+ v && (v.dimension - d).abs <= 8 ? v.dimension : nil
160
+ end
161
+
162
+ # Version blocks read through affine frames centred on the TR and BL
163
+ # finders, whose axes follow the finders' own ring strokes; robust
164
+ # against curvature that bends the global transform near the edges.
165
+ def local_versions(d)
166
+ tr = local_frame(@tr, d - 3.5, 3.5, stroke(:tr_down), stroke(:tr_left), @tr_left, @tr_down)
167
+ bl = local_frame(@bl, 3.5, d - 3.5, stroke(:bl_up), stroke(:bl_right), @bl_right, @bl_up)
168
+ out = []
169
+ out << read_version_block(tr, d, :tr) if tr
170
+ out << read_version_block(bl, d, :bl) if bl
171
+ out
172
+ end
173
+
174
+ # Affine frame at finder +p+ (module centre (cx, cy)) mapping module
175
+ # (mx, my) to pixels (see read_version_block); +h_line+ / +v_line+
176
+ # are stroke lines running along the local x / y axes. nil without
177
+ # both strokes.
178
+ def local_frame(p, cx, cy, h_line, v_line, ms_x, ms_y)
179
+ return unless h_line && v_line
180
+
181
+ right, down = @axes
182
+ ex0 = h_line[2]
183
+ ex1 = h_line[3]
184
+ if ex0 * right[0] + ex1 * right[1] < 0
185
+ ex0 = -ex0
186
+ ex1 = -ex1
187
+ end
188
+ ey0 = v_line[2]
189
+ ey1 = v_line[3]
190
+ if ey0 * down[0] + ey1 * down[1] < 0
191
+ ey0 = -ey0
192
+ ey1 = -ey1
193
+ end
194
+ [p.x, p.y, cx, cy, ms_x, ms_y, ex0, ex1, ey0, ey1]
195
+ end
196
+
197
+ # One version block (:tr or :bl) read through a local frame.
198
+ def read_version_block(frame, d, which)
199
+ px, py, cx, cy, ms_x, ms_y, ex0, ex1, ey0, ey1 = frame
200
+ bits = 0
201
+ k = 17
202
+ while k >= 0
203
+ a = d - 11 + k % 3 + 0.5
204
+ b = k / 3 + 0.5
205
+ if which == :tr
206
+ mx = a
207
+ my = b
208
+ else
209
+ mx = b
210
+ my = a
211
+ end
212
+ u = (mx - cx) * ms_x
213
+ v = (my - cy) * ms_y
214
+ bits = (bits << 1) | pixel(px + u * ex0 + v * ey0, py + u * ex1 + v * ey1)
215
+ k -= 1
216
+ end
217
+ SymbolVersion.decode_version_info(bits)
218
+ end
219
+
220
+ # Both 18-bit version information blocks decoded (SymbolVersion or nil).
221
+ def read_version(t, d)
222
+ bits1 = 0
223
+ bits2 = 0
224
+ k = 17
225
+ while k >= 0
226
+ a = d - 11 + k % 3
227
+ b = k / 3
228
+ bits1 = (bits1 << 1) | module_at(t, a, b)
229
+ bits2 = (bits2 << 1) | module_at(t, b, a)
230
+ k -= 1
231
+ end
232
+ [SymbolVersion.decode_version_info(bits1), SymbolVersion.decode_version_info(bits2)]
233
+ end
234
+ end
235
+ end
236
+ end
237
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QrReader
4
+ class Detector
5
+ class Candidate
6
+ # Geometry from the inner ring strokes of the finder patterns (the
7
+ # dark ring sides facing the symbol's interior), traced as straight
8
+ # lines - which they stay under perspective - in the spirit of
9
+ # zxing-cpp's TraceLine. Their intersections give the bottom-right
10
+ # alignment pattern position and the grid points next to the finders,
11
+ # their directions the finders' local axes. Mixed into Candidate.
12
+ module Strokes
13
+ # Offsets (in modules) along a stroke at which its centre is measured.
14
+ STROKE_OFFSETS = [-2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0].freeze
15
+
16
+ private
17
+
18
+ # Intersection of the centre lines of the BL finder's top ring stroke
19
+ # and the TR finder's left ring stroke, which meet at the centre of
20
+ # the bottom-right alignment pattern (module (d - 6.5, d - 6.5)) even
21
+ # under perspective. nil if the strokes cannot be traced.
22
+ def stroke_intersection
23
+ a = stroke(:bl_up) or return
24
+ b = stroke(:tr_left) or return
25
+ p = Geometry.intersect_lines(a, b) or return
26
+ guess = parallelogram_transform(@dimensions.first)&.apply(@dimensions.first - 6.5, @dimensions.first - 6.5)
27
+ return unless guess
28
+
29
+ reach = (@tl.distance(@tr) + @tl.distance(@bl)) * 0.25
30
+ Math.hypot(p[0] - guess[0], p[1] - guess[1]) < reach ? p : nil
31
+ end
32
+
33
+ # Centre line of one of the six inner ring strokes of the finders
34
+ # (memoized; nil if it cannot be traced or does not run along the
35
+ # expected symbol axis).
36
+ def stroke(name)
37
+ @strokes ||= {}
38
+ return @strokes[name] if @strokes.key?(name)
39
+
40
+ (rx, ry), (dx, dy) = @axes
41
+ p, ux, uy, ms, ax, ay =
42
+ case name
43
+ when :tl_right then [@tl, rx, ry, @tl_right, dx, dy]
44
+ when :tl_down then [@tl, dx, dy, @tl_down, rx, ry]
45
+ when :tr_left then [@tr, -rx, -ry, @tr_left, dx, dy]
46
+ when :tr_down then [@tr, dx, dy, @tr_down, rx, ry]
47
+ when :bl_up then [@bl, -dx, -dy, @bl_up, rx, ry]
48
+ when :bl_right then [@bl, rx, ry, @bl_right, dx, dy]
49
+ end
50
+ line = stroke_line(p, ux, uy, ms)
51
+ # The stroke must run roughly along the other symbol axis.
52
+ line = nil if line && (line[2] * ax + line[3] * ay).abs < 0.8
53
+ @strokes[name] = line
54
+ end
55
+
56
+ # Pixel positions of the grid points next to the finder patterns -
57
+ # modules (6.5, 6.5), (d - 6.5, 6.5) and (6.5, d - 6.5) - where the
58
+ # centre lines of each finder's two inner ring strokes cross.
59
+ # Returns {[gx, gy] => [x, y]} keyed by alignment grid index (n = last).
60
+ def finder_corners(n)
61
+ out = {}
62
+ [[@tl, %i[tl_right tl_down], [0, 0]], [@tr, %i[tr_left tr_down], [n, 0]],
63
+ [@bl, %i[bl_up bl_right], [0, n]]].each do |finder, (s1, s2), key|
64
+ a = stroke(s1) or next
65
+ b = stroke(s2) or next
66
+ p = Geometry.intersect_lines(a, b) or next
67
+ r = finder.distance_to(*p) / @module_size
68
+ out[key] = p if r > 3.0 && r < 5.5
69
+ end
70
+ out
71
+ end
72
+
73
+ # Centre line of the ring stroke of finder +p+ facing the unit
74
+ # direction (dx, dy), as [x, y, vx, vy]; nil if it cannot be traced.
75
+ # For points spread along the stroke, the walk starts inside the
76
+ # light ring and measures the dark stroke's extent along (dx, dy).
77
+ def stroke_line(p, dx, dy, ms)
78
+ return if ms < 1.5
79
+
80
+ bits = @matrix.bits
81
+ w = @matrix.width
82
+ h = @matrix.height
83
+ ex = -dy
84
+ ey = dx
85
+ step = ms / 6.0
86
+ step = 0.5 if step < 0.5
87
+ points = []
88
+ STROKE_OFFSETS.each do |s|
89
+ x0 = p.x + (ex * s + dx * 1.9) * ms
90
+ y0 = p.y + (ey * s + dy * 1.9) * ms
91
+ inside = nil
92
+ outside = nil
93
+ t = 0.0
94
+ while t <= 2.8 * ms
95
+ x = x0 + dx * t
96
+ y = y0 + dy * t
97
+ break unless x >= 0 && y >= 0 && x < w && y < h
98
+
99
+ dark = bits.getbyte(y.to_i * w + x.to_i) == 1
100
+ if inside.nil?
101
+ break if dark && t.zero?
102
+
103
+ inside = t if dark
104
+ break if !dark && t > 1.3 * ms
105
+ elsif !dark
106
+ outside = t
107
+ break
108
+ end
109
+ t += step
110
+ end
111
+ next unless inside && outside
112
+
113
+ width = outside - inside
114
+ next if width < 0.4 * ms || width > 1.8 * ms
115
+
116
+ mid = (inside + outside - step) * 0.5
117
+ points << [x0 + dx * mid, y0 + dy * mid]
118
+ end
119
+ return if points.size < 5
120
+
121
+ Geometry.fit_line(points, 0.4 * ms)
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end