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,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QrReader
4
+ class Detector
5
+ # A planar projective transform (homography):
6
+ #
7
+ # x' = (m0 x + m1 y + m2) / (m6 x + m7 y + m8)
8
+ # y' = (m3 x + m4 y + m5) / (m6 x + m7 y + m8)
9
+ #
10
+ # Built from four point correspondences, like ZXing's PerspectiveTransform.
11
+ class PerspectiveTransform
12
+ attr_reader :m
13
+
14
+ # +m+ - the 9 coefficients, row-major.
15
+ def initialize(m)
16
+ @m = m.freeze
17
+ freeze
18
+ end
19
+
20
+ class << self
21
+ # Maps quad (x0,y0)..(x3,y3) onto quad (u0,v0)..(u3,v3); points
22
+ # correspond in order. Returns nil for degenerate quads.
23
+ def quad_to_quad(src, dst)
24
+ to_square = square_to_quad(src)&.adjoint
25
+ from_square = square_to_quad(dst)
26
+ return unless to_square && from_square
27
+
28
+ t = from_square * to_square
29
+ t.finite? ? t : nil
30
+ end
31
+
32
+ # Maps the unit square (0,0), (1,0), (1,1), (0,1) onto the quad
33
+ # [[x0, y0], [x1, y1], [x2, y2], [x3, y3]]. nil if degenerate.
34
+ def square_to_quad(quad)
35
+ (x0, y0), (x1, y1), (x2, y2), (x3, y3) = quad
36
+ # Three corners on a line make the quad degenerate.
37
+ return if collinear?(x3, y3, x0, y0, x1, y1) || collinear?(x0, y0, x1, y1, x2, y2) ||
38
+ collinear?(x1, y1, x2, y2, x3, y3) || collinear?(x2, y2, x3, y3, x0, y0)
39
+
40
+ dx3 = x0 - x1 + x2 - x3
41
+ dy3 = y0 - y1 + y2 - y3
42
+ if dx3.abs < 1e-9 && dy3.abs < 1e-9
43
+ new([x1 - x0, x3 - x0, x0, y1 - y0, y3 - y0, y0, 0.0, 0.0, 1.0])
44
+ else
45
+ dx1 = x1 - x2
46
+ dx2 = x3 - x2
47
+ dy1 = y1 - y2
48
+ dy2 = y3 - y2
49
+ den = (dx1 * dy2 - dx2 * dy1).to_f
50
+ return if den.abs < 1e-12
51
+
52
+ g = (dx3 * dy2 - dx2 * dy3) / den
53
+ h = (dx1 * dy3 - dx3 * dy1) / den
54
+ new([x1 - x0 + g * x1, x3 - x0 + h * x3, x0.to_f,
55
+ y1 - y0 + g * y1, y3 - y0 + h * y3, y0.to_f,
56
+ g, h, 1.0])
57
+ end
58
+ end
59
+ end
60
+
61
+ # True if (bx, by) lies on the line through (ax, ay) and (cx, cy).
62
+ def self.collinear?(ax, ay, bx, by, cx, cy)
63
+ ((bx - ax) * (cy - ay) - (by - ay) * (cx - ax)).abs < 1e-9
64
+ end
65
+ private_class_method :collinear?
66
+
67
+ # Transformed point as [x', y'] (nil if the point maps to infinity).
68
+ def apply(x, y)
69
+ m = @m
70
+ den = m[6] * x + m[7] * y + m[8]
71
+ return if den.abs < 1e-12
72
+
73
+ [(m[0] * x + m[1] * y + m[2]) / den, (m[3] * x + m[4] * y + m[5]) / den]
74
+ end
75
+
76
+ # Matrix product (self after other).
77
+ def *(other)
78
+ a = @m
79
+ b = other.m
80
+ PerspectiveTransform.new([
81
+ a[0] * b[0] + a[1] * b[3] + a[2] * b[6],
82
+ a[0] * b[1] + a[1] * b[4] + a[2] * b[7],
83
+ a[0] * b[2] + a[1] * b[5] + a[2] * b[8],
84
+ a[3] * b[0] + a[4] * b[3] + a[5] * b[6],
85
+ a[3] * b[1] + a[4] * b[4] + a[5] * b[7],
86
+ a[3] * b[2] + a[4] * b[5] + a[5] * b[8],
87
+ a[6] * b[0] + a[7] * b[3] + a[8] * b[6],
88
+ a[6] * b[1] + a[7] * b[4] + a[8] * b[7],
89
+ a[6] * b[2] + a[7] * b[5] + a[8] * b[8]
90
+ ])
91
+ end
92
+
93
+ # Adjugate matrix: the inverse up to a scale factor, which is all a
94
+ # homography needs.
95
+ def adjoint
96
+ a0, a1, a2, a3, a4, a5, a6, a7, a8 = @m
97
+ PerspectiveTransform.new([
98
+ a4 * a8 - a5 * a7, a2 * a7 - a1 * a8, a1 * a5 - a2 * a4,
99
+ a5 * a6 - a3 * a8, a0 * a8 - a2 * a6, a2 * a3 - a0 * a5,
100
+ a3 * a7 - a4 * a6, a1 * a6 - a0 * a7, a0 * a4 - a1 * a3
101
+ ])
102
+ end
103
+
104
+ def finite?
105
+ @m.all? { |v| v.finite? }
106
+ end
107
+
108
+ # True if the denominator keeps one sign (no point maps through
109
+ # infinity) over the quad with corners (x0, y0) and (x1, y1).
110
+ def regular_over?(x0, y0, x1, y1)
111
+ m = @m
112
+ dens = [[x0, y0], [x1, y0], [x1, y1], [x0, y1]].map { |x, y| m[6] * x + m[7] * y + m[8] }
113
+ dens.all? { |d| d > 1e-9 } || dens.all? { |d| d < -1e-9 }
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QrReader
4
+ class Detector
5
+ # Fast path for clean, axis-aligned, unrotated-or-quarter-turned symbols
6
+ # such as generated images: the symbol is the bounding box of all dark
7
+ # pixels, three of its corners must hold finder patterns (checked
8
+ # along the corner diagonals, which also yields the module size), and
9
+ # modules are sampled at their centres. Costs a few String#index calls
10
+ # when the image is not such a symbol.
11
+ module PureDetector
12
+ DARK = BitMatrix::DARK
13
+
14
+ # A detected pure symbol; same interface as Candidate.
15
+ class Result
16
+ attr_reader :bits, :points, :finder_patterns, :dimension, :module_size
17
+
18
+ def initialize(bits, points, finder_patterns, module_size)
19
+ @bits = bits
20
+ @points = points
21
+ @finder_patterns = finder_patterns
22
+ @dimension = bits.width
23
+ @module_size = module_size
24
+ end
25
+
26
+ def alternatives
27
+ return enum_for(:alternatives) unless block_given?
28
+
29
+ nil
30
+ end
31
+
32
+ def uses?(_pattern)
33
+ false
34
+ end
35
+
36
+ def key
37
+ @finder_patterns.map { |x, y| [x.round, y.round] }
38
+ end
39
+
40
+ def inspect
41
+ "#<PureDetector::Result dim=#{@dimension} module_size=#{@module_size.round(2)}>"
42
+ end
43
+ end
44
+
45
+ module_function
46
+
47
+ # Result or nil.
48
+ def detect(matrix)
49
+ box = bounding_box(matrix) or return
50
+ left, top, right, bottom = box
51
+ width = right - left
52
+ height = bottom - top
53
+ return if width < 21 || height < 21 || (width - height).abs > 2 + width / 50
54
+
55
+ # Diagonal walks into each corner: TL, TR, BR, BL.
56
+ corners = [[left, top, 1, 1], [right - 1, top, -1, 1],
57
+ [right - 1, bottom - 1, -1, -1], [left, bottom - 1, 1, -1]]
58
+ sizes = corners.map { |x, y, dx, dy| corner_finder(matrix, x, y, dx, dy, width) }
59
+ missing = sizes.each_index.select { |i| sizes[i].nil? }
60
+ return unless missing.size == 1
61
+
62
+ module_size = sizes.compact.sum / 3.0
63
+ dimension = ((width + height) * 0.5 / module_size).round
64
+ return unless SymbolVersion.for_dimension(dimension)
65
+
66
+ mx = width.to_f / dimension
67
+ my = height.to_f / dimension
68
+ return if (mx / module_size - 1).abs > 0.2 || (my / module_size - 1).abs > 0.2
69
+
70
+ bits = sample(matrix, left, top, mx, my, dimension)
71
+ # Turn the symbol so that the corner without finder is bottom-right.
72
+ quarter_turns = (2 - missing.first) % 4
73
+ bits = rotate(bits, quarter_turns)
74
+ points = [[left, top], [right, top], [right, bottom], [left, bottom]].rotate(-quarter_turns % 4)
75
+ mid_x = (left + right) * 0.5
76
+ mid_y = (top + bottom) * 0.5
77
+ centre = ->(px, py) { [px + (mid_x <=> px) * 3.5 * mx, py + (mid_y <=> py) * 3.5 * my] }
78
+ finders = [centre.call(*points[0]), centre.call(*points[1]), centre.call(*points[3])]
79
+ Result.new(bits, points.map { |x, y| [x.to_f, y.to_f] }, finders, module_size)
80
+ end
81
+
82
+ # [left, top, right, bottom] (right/bottom exclusive) of the dark pixels.
83
+ def bounding_box(matrix)
84
+ bits = matrix.bits
85
+ w = matrix.width
86
+ first = bits.index(DARK) or return
87
+ last = bits.rindex(DARK)
88
+ top = first / w
89
+ bottom = last / w + 1
90
+ left = w
91
+ right = 0
92
+ y = top
93
+ off = top * w
94
+ while y < bottom
95
+ # No substring per row: the first dark pixel at or after the row
96
+ # start is in this row, or rows without any are skipped at once.
97
+ l = bits.index(DARK, off)
98
+ if l >= off + w
99
+ y = l / w
100
+ off = y * w
101
+ next
102
+ end
103
+ left = l - off if l - off < left
104
+ r = bits.rindex(DARK, off + w - 1)
105
+ right = r - off + 1 if r - off >= right
106
+ off += w
107
+ y += 1
108
+ end
109
+ [left, top, right, bottom]
110
+ end
111
+
112
+ # Module size of a finder pattern whose outer corner is at (x, y),
113
+ # from the 1:1:3:1:1 runs along the diagonal (dx, dy); the edges
114
+ # leaving the corner must be dark for 7 modules, then light. nil if
115
+ # there is no finder pattern.
116
+ def corner_finder(matrix, x, y, dx, dy, limit)
117
+ runs = diagonal_runs(matrix, x, y, dx, dy, limit)
118
+ return unless runs.size == 5 && Geometry.finder_ratio?(*runs, relaxed: true)
119
+
120
+ module_size = runs.sum / 7.0
121
+ return unless edge_run?(matrix, x, y, dx, 0, module_size) && edge_run?(matrix, x, y, 0, dy, module_size)
122
+
123
+ module_size
124
+ end
125
+
126
+ # Up to five alternating run lengths along a diagonal, starting dark.
127
+ def diagonal_runs(matrix, x, y, dx, dy, limit)
128
+ bits = matrix.bits
129
+ w = matrix.width
130
+ h = matrix.height
131
+ runs = [0]
132
+ dark = true
133
+ steps = 0
134
+ while steps < limit && x >= 0 && y >= 0 && x < w && y < h
135
+ if (bits.getbyte(y * w + x) == 1) == dark
136
+ runs[-1] += 1
137
+ else
138
+ break if runs.size == 5
139
+
140
+ runs << 1
141
+ dark = !dark
142
+ end
143
+ x += dx
144
+ y += dy
145
+ steps += 1
146
+ end
147
+ runs
148
+ end
149
+
150
+ # True if the dark run from (x, y) along (dx, dy) is 7 modules long.
151
+ def edge_run?(matrix, x, y, dx, dy, module_size)
152
+ bits = matrix.bits
153
+ w = matrix.width
154
+ h = matrix.height
155
+ n = 0
156
+ while x >= 0 && y >= 0 && x < w && y < h && bits.getbyte(y * w + x) == 1
157
+ n += 1
158
+ x += dx
159
+ y += dy
160
+ end
161
+ (n / module_size - 7).abs < 1.0
162
+ end
163
+
164
+ def sample(matrix, left, top, mx, my, dimension)
165
+ bits = matrix.bits
166
+ w = matrix.width
167
+ out = "\0".b * (dimension * dimension)
168
+ j = 0
169
+ while j < dimension
170
+ y = (top + (j + 0.5) * my).to_i
171
+ off = y * w
172
+ i = 0
173
+ while i < dimension
174
+ out.setbyte(j * dimension + i, 1) if bits.getbyte(off + (left + (i + 0.5) * mx).to_i) == 1
175
+ i += 1
176
+ end
177
+ j += 1
178
+ end
179
+ BitMatrix.new(dimension, dimension, out)
180
+ end
181
+
182
+ # Rotates a square matrix by +turns+ quarter turns clockwise.
183
+ def rotate(matrix, turns)
184
+ turns.times do
185
+ n = matrix.width
186
+ src = matrix.bits
187
+ out = "\0".b * (n * n)
188
+ n.times do |y|
189
+ n.times { |x| out.setbyte(x * n + (n - 1 - y), src.getbyte(y * n + x)) }
190
+ end
191
+ matrix = BitMatrix.new(n, n, out)
192
+ end
193
+ matrix
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,215 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "binarizer"
4
+ require_relative "kernels/detector"
5
+ require_relative "detector/geometry"
6
+ require_relative "detector/finder_pattern"
7
+ require_relative "detector/finder_pattern_finder"
8
+ require_relative "detector/pattern_sets"
9
+ require_relative "detector/perspective_transform"
10
+ require_relative "detector/grid_sampler"
11
+ require_relative "detector/alignment_pattern_finder"
12
+ require_relative "detector/alignment_grid"
13
+ require_relative "detector/candidate"
14
+ require_relative "detector/pure_detector"
15
+
16
+ module QrReader
17
+ # Locates QR symbols in a binarized image and samples their module grids.
18
+ #
19
+ # detector = QrReader::Detector.new(QrReader::Binarizer.hybrid(image), image: image)
20
+ # detector.each_candidate do |candidate|
21
+ # result = QrReader::Decoder.decode(candidate.bits) ||
22
+ # candidate.alternatives.lazy.filter_map { |bits| QrReader::Decoder.decode(bits) }.first
23
+ # break result if result
24
+ # end
25
+ #
26
+ # Candidates are produced lazily, most likely first: finder patterns are
27
+ # searched once per mode and cached; every plausible triple of them is
28
+ # scored (PatternSets) and turned into a Candidate whose module grid is
29
+ # only sampled when asked for.
30
+ class Detector
31
+ # Finder pattern triples tried per mode.
32
+ FAST_TRIPLES = 12
33
+ HARD_TRIPLES = 40
34
+ MULTI_TRIPLES = 120
35
+ # With multi, up to this many finder patterns are considered...
36
+ MULTI_PATTERNS = 400
37
+ # ... and at most this many "foreign" triples (with other finder
38
+ # patterns inside, see PatternSets.partition_clean): on pages with
39
+ # several symbols nearly all triples joining finders of different
40
+ # symbols are foreign, real symbols practically never.
41
+ MULTI_FOREIGN = 12
42
+ # Score penalty of foreign triples in single-symbol mode (a perfect
43
+ # triple scores 0, see PatternSets.evaluate).
44
+ FOREIGN_PENALTY = 0.5
45
+ # Besides the best scored triples overall, each finder pattern is
46
+ # combined with pairs of this many of its nearest neighbours.
47
+ NEIGHBOURS = 8
48
+
49
+ attr_reader :matrix, :image
50
+
51
+ # Fast path for clean axis-aligned symbols (see PureDetector);
52
+ # returns a candidate or nil.
53
+ def self.detect_pure(matrix)
54
+ PureDetector.detect(matrix)
55
+ end
56
+
57
+ # +matrix+ - binarized image (BitMatrix, 1 = dark)
58
+ # +image+ - the grayscale Image it came from (optional; ignored unless
59
+ # it has the matrix's size)
60
+ def initialize(matrix, image: nil)
61
+ @matrix = matrix
62
+ @image = image if image && image.width == matrix.width && image.height == matrix.height
63
+ @finder = FinderPatternFinder.new(matrix)
64
+ @confirmed = []
65
+ end
66
+
67
+ # Yields Candidates, most likely first (Enumerator without a block).
68
+ # Candidates whose timing patterns read like noise (false finder
69
+ # triples, mostly) are only produced with try_harder, after all others
70
+ # (with multi, the FAST_TRIPLES best of those that span no other finder
71
+ # pattern).
72
+ #
73
+ # try_harder: scan every row and try more finder pattern triples.
74
+ # multi: look for all symbols - also triples of neighbouring finder
75
+ # patterns (pages with many symbols), skipping triples that share a
76
+ # finder pattern with (or lie inside) a candidate passed to #confirm.
77
+ def each_candidate(try_harder: false, multi: false)
78
+ return enum_for(:each_candidate, try_harder: try_harder, multi: multi) unless block_given?
79
+
80
+ patterns, triples = multi ? multi_triples(try_harder) : single_triples(try_harder)
81
+ deferred = []
82
+ triples.each do |triple|
83
+ next if multi && confirmed?(triple)
84
+
85
+ candidate = Candidate.locate(self, triple) or next
86
+ if candidate.likely?
87
+ yield candidate
88
+ elsif try_harder && !(multi && spans_others?(candidate, patterns))
89
+ deferred << candidate
90
+ end
91
+ end
92
+ deferred = deferred.first(FAST_TRIPLES) if multi
93
+ deferred.each { |candidate| yield candidate unless multi && confirmed?(candidate) }
94
+ nil
95
+ end
96
+
97
+ # Records a decoded symbol, so that multi-symbol detection skips
98
+ # triples that reuse its finder patterns or lie inside it: a Candidate
99
+ # (or detect_pure result), anything else with #points (e.g. a Result
100
+ # decoded earlier from another binarization of the same image, its
101
+ # points scaled to this matrix), or the corner quad [[x, y], ...]
102
+ # itself. Returns self.
103
+ def confirm(symbol)
104
+ quad = symbol.is_a?(Array) ? symbol : symbol.points
105
+ if quad.is_a?(Array) && quad.size == 4 && quad.flatten.all? { |v| v.is_a?(Numeric) && v.finite? }
106
+ @confirmed << [quad, symbol.respond_to?(:uses?) ? symbol : nil]
107
+ end
108
+ self
109
+ end
110
+
111
+ # Finder patterns found in the image (cached per mode).
112
+ def finder_patterns(try_harder: false)
113
+ @finder.find(try_harder: try_harder)
114
+ end
115
+
116
+ def alignment_finder
117
+ @alignment_finder ||= AlignmentPatternFinder.new(@matrix)
118
+ end
119
+
120
+ # True if the bit matrix is the negative of the image (dark cells are
121
+ # brighter on average), e.g. when a caller inverted it to read
122
+ # light-on-dark symbols; luminance sampling then flips its comparison.
123
+ def inverted_image?
124
+ return @inverted_image unless @inverted_image.nil?
125
+
126
+ @inverted_image = false
127
+ return false unless @image && @image.width == @matrix.width && @image.height == @matrix.height
128
+
129
+ data = @image.data
130
+ bits = @matrix.bits
131
+ step = [data.bytesize / 4096, 1].max
132
+ sums = [0, 0]
133
+ counts = [0, 0]
134
+ i = 0
135
+ while i < data.bytesize
136
+ b = bits.getbyte(i)
137
+ sums[b] += data.getbyte(i)
138
+ counts[b] += 1
139
+ i += step
140
+ end
141
+ @inverted_image = counts.all?(&:positive?) && sums[1] * counts[0] > sums[0] * counts[1]
142
+ end
143
+
144
+ private
145
+
146
+ # [patterns, triples] for single-symbol detection: the best scored
147
+ # triples of the most consistent finder patterns and of each pattern's
148
+ # nearest neighbours, those with other symbols' finder patterns inside
149
+ # (foreign, see PatternSets.partition_clean) ranked as if their score
150
+ # were FOREIGN_PENALTY worse: on a sheet of labels the best shaped
151
+ # triples join finders of neighbouring symbols, but a large symbol
152
+ # with small modules can have finder look-alikes in its data area and
153
+ # must still beat the ill-shaped triples of those look-alikes.
154
+ def single_triples(try_harder)
155
+ patterns = finder_patterns(try_harder: try_harder)
156
+ limit = try_harder ? HARD_TRIPLES : FAST_TRIPLES
157
+ clean, foreign = ranked_triples(PatternSets.generate(patterns, 4 * limit), patterns, patterns)
158
+ ranked = clean.each_with_index.map { |t, i| [t.score, i, t] }
159
+ foreign.each_with_index { |t, i| ranked << [t.score + FOREIGN_PENALTY, clean.size + i, t] }
160
+ [patterns, ranked.sort!.first(limit).map(&:last)]
161
+ end
162
+
163
+ # [patterns, triples] for multi-symbol detection: as for a single
164
+ # symbol, but from up to MULTI_PATTERNS patterns and MULTI_TRIPLES
165
+ # triples (a page with that many symbols), MULTI_FOREIGN of them
166
+ # foreign.
167
+ def multi_triples(try_harder)
168
+ all = @finder.find(try_harder: try_harder, limit: MULTI_PATTERNS)
169
+ best = if all.size > FinderPatternFinder::MAX_PATTERNS
170
+ all.max_by(FinderPatternFinder::MAX_PATTERNS) { |p| p.quality * Math.sqrt(p.count) }
171
+ else
172
+ all
173
+ end
174
+ clean, foreign = ranked_triples(PatternSets.generate(best, MULTI_TRIPLES), all, all)
175
+ [all, (clean + foreign.first(MULTI_FOREIGN)).first(MULTI_TRIPLES)]
176
+ end
177
+
178
+ # +triples+ plus the neighbourhood triples of +near+, split into
179
+ # [clean, foreign] (see PatternSets.partition_clean against +all+), the
180
+ # clean ones best first.
181
+ def ranked_triples(triples, near, all)
182
+ seen = triples.to_h { |t| [t.patterns.map(&:object_id), true] }
183
+ PatternSets.neighbourhood(near, NEIGHBOURS).each do |t|
184
+ triples << t unless seen[t.patterns.map(&:object_id)]
185
+ end
186
+ clean, foreign = PatternSets.partition_clean(triples, all)
187
+ [clean.sort_by!(&:score), foreign]
188
+ end
189
+
190
+ # True if a triple (or candidate) shares a finder pattern with a
191
+ # confirmed symbol or has one inside it.
192
+ def confirmed?(set)
193
+ patterns = set.respond_to?(:patterns) ? set.patterns : [set.tl, set.tr, set.bl]
194
+ @confirmed.any? do |quad, owner|
195
+ patterns.any? { |p| owner&.uses?(p) || inside?(quad, p.x, p.y) }
196
+ end
197
+ end
198
+
199
+ # True if a finder pattern other than the candidate's own lies inside
200
+ # its symbol area.
201
+ def spans_others?(candidate, patterns)
202
+ quad = candidate.points
203
+ patterns.any? { |p| !candidate.uses?(p) && inside?(quad, p.x, p.y) }
204
+ end
205
+
206
+ # Point-in-convex-quad test (either winding).
207
+ def inside?(quad, x, y)
208
+ signs = quad.each_with_index.map do |(x0, y0), i|
209
+ x1, y1 = quad[(i + 1) % 4]
210
+ (x1 - x0) * (y - y0) - (y1 - y0) * (x - x0)
211
+ end
212
+ signs.all?(&:positive?) || signs.all?(&:negative?)
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QrReader
4
+ class Error < StandardError; end
5
+
6
+ # The input could not be turned into an image (unknown or corrupt format).
7
+ class ImageError < Error; end
8
+
9
+ # The image format was recognised but is not supported (e.g. arithmetic-coded JPEG).
10
+ class UnsupportedImageError < ImageError; end
11
+
12
+ # Raised by QrReader.decode! when no QR code could be read.
13
+ class NotFoundError < Error; end
14
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QrReader
4
+ # The 15-bit format information word: error correction level + mask pattern,
5
+ # protected by a BCH(15,5) code and XOR-masked with 0x5412.
6
+ class FormatInformation
7
+ MASK = 0x5412
8
+ GENERATOR = 0x537
9
+
10
+ attr_reader :ec_level, :mask, :bits
11
+
12
+ def initialize(data_bits)
13
+ @ec_level = SymbolVersion::EC_LEVEL_FOR_BITS[(data_bits >> 3) & 0x03]
14
+ @mask = data_bits & 0x07
15
+ @bits = self.class.encode(data_bits)
16
+ freeze
17
+ end
18
+
19
+ def ==(other)
20
+ other.is_a?(FormatInformation) && other.bits == bits
21
+ end
22
+ alias eql? ==
23
+
24
+ def hash
25
+ bits.hash
26
+ end
27
+
28
+ def inspect
29
+ "#<QrReader::FormatInformation ec=#{@ec_level} mask=#{@mask}>"
30
+ end
31
+
32
+ class << self
33
+ # 15-bit masked code word for 5 data bits (2 EC level bits + 3 mask bits).
34
+ def encode(data_bits)
35
+ rem = data_bits
36
+ 10.times { rem = (rem << 1) ^ ((rem >> 9) * GENERATOR) }
37
+ ((data_bits << 10) | rem) ^ MASK
38
+ end
39
+
40
+ # Decodes the two 15-bit copies read from a symbol (pass the same value
41
+ # twice when only one copy is available). Returns the FormatInformation
42
+ # closest in Hamming distance to either copy if that distance is <= 3,
43
+ # else nil. Also accepts copies that lack the 0x5412 XOR mask, as
44
+ # written by some broken encoders.
45
+ def decode(bits1, bits2 = bits1)
46
+ closest(bits1, bits2) || closest(bits1 ^ MASK, bits2 ^ MASK)
47
+ end
48
+
49
+ def closest(bits1, bits2)
50
+ best = nil
51
+ best_distance = 4
52
+ ALL.each do |info|
53
+ code = info.bits
54
+ return info if code == bits1 || code == bits2
55
+
56
+ distance = popcount(bits1 ^ code)
57
+ d2 = popcount(bits2 ^ code)
58
+ distance = d2 if d2 < distance
59
+ if distance < best_distance
60
+ best = info
61
+ best_distance = distance
62
+ end
63
+ end
64
+ best
65
+ end
66
+
67
+ def popcount(value)
68
+ count = 0
69
+ while value != 0
70
+ value &= value - 1
71
+ count += 1
72
+ end
73
+ count
74
+ end
75
+ end
76
+
77
+ ALL = (0..31).map { |d| new(d) }.freeze
78
+ end
79
+ end