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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +16 -0
- data/LICENSE.txt +21 -0
- data/NOTICE +14 -0
- data/README.md +222 -0
- data/exe/qr_reader +66 -0
- data/lib/qr_reader/binarizer.rb +170 -0
- data/lib/qr_reader/bit_matrix.rb +120 -0
- data/lib/qr_reader/decoder/bitstream_parser.rb +307 -0
- data/lib/qr_reader/decoder/character_set.rb +163 -0
- data/lib/qr_reader/decoder/codewords.rb +164 -0
- data/lib/qr_reader/decoder/data_mask.rb +57 -0
- data/lib/qr_reader/decoder/format_reader.rb +204 -0
- data/lib/qr_reader/decoder/function_patterns.rb +62 -0
- data/lib/qr_reader/decoder/galois_field.rb +69 -0
- data/lib/qr_reader/decoder/reed_solomon.rb +312 -0
- data/lib/qr_reader/decoder.rb +241 -0
- data/lib/qr_reader/detector/alignment_grid.rb +127 -0
- data/lib/qr_reader/detector/alignment_pattern_finder.rb +191 -0
- data/lib/qr_reader/detector/candidate/dimension.rb +237 -0
- data/lib/qr_reader/detector/candidate/strokes.rb +126 -0
- data/lib/qr_reader/detector/candidate.rb +436 -0
- data/lib/qr_reader/detector/finder_pattern.rb +63 -0
- data/lib/qr_reader/detector/finder_pattern_finder.rb +199 -0
- data/lib/qr_reader/detector/geometry.rb +118 -0
- data/lib/qr_reader/detector/grid_sampler.rb +350 -0
- data/lib/qr_reader/detector/pattern_sets.rb +235 -0
- data/lib/qr_reader/detector/perspective_transform.rb +117 -0
- data/lib/qr_reader/detector/pure_detector.rb +197 -0
- data/lib/qr_reader/detector.rb +215 -0
- data/lib/qr_reader/errors.rb +14 -0
- data/lib/qr_reader/format_information.rb +79 -0
- data/lib/qr_reader/formats/bmp.rb +434 -0
- data/lib/qr_reader/formats/ccitt.rb +81 -0
- data/lib/qr_reader/formats/gif.rb +272 -0
- data/lib/qr_reader/formats/jpeg.rb +152 -0
- data/lib/qr_reader/formats/pdf.rb +2935 -0
- data/lib/qr_reader/formats/png.rb +311 -0
- data/lib/qr_reader/formats/pnm.rb +286 -0
- data/lib/qr_reader/formats/tiff.rb +1345 -0
- data/lib/qr_reader/formats.rb +279 -0
- data/lib/qr_reader/image.rb +231 -0
- data/lib/qr_reader/kernels/binarizer.rb +475 -0
- data/lib/qr_reader/kernels/ccitt.rb +554 -0
- data/lib/qr_reader/kernels/detector.rb +587 -0
- data/lib/qr_reader/kernels/image.rb +65 -0
- data/lib/qr_reader/kernels/jpeg.rb +3421 -0
- data/lib/qr_reader/kernels/lzw.rb +173 -0
- data/lib/qr_reader/kernels/pixels.rb +116 -0
- data/lib/qr_reader/kernels/png.rb +1138 -0
- data/lib/qr_reader/kernels.rb +14 -0
- data/lib/qr_reader/native_kernels.rb +130 -0
- data/lib/qr_reader/reader.rb +348 -0
- data/lib/qr_reader/result.rb +49 -0
- data/lib/qr_reader/symbol_version.rb +173 -0
- data/lib/qr_reader/version.rb +5 -0
- data/lib/qr_reader.rb +77 -0
- metadata +103 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# QrReaderKernels: the hot loops of qr_reader as plain-Ruby module functions
|
|
4
|
+
# over Strings, Integers and Arrays. They run as they are on any Ruby; the
|
|
5
|
+
# optional qr_reader-native gem compiles the same files with Spinel and
|
|
6
|
+
# swaps in the native versions (see DESIGN.md, "Kernels").
|
|
7
|
+
require_relative "kernels/image"
|
|
8
|
+
require_relative "kernels/pixels"
|
|
9
|
+
require_relative "kernels/png"
|
|
10
|
+
require_relative "kernels/jpeg"
|
|
11
|
+
require_relative "kernels/ccitt"
|
|
12
|
+
require_relative "kernels/lzw"
|
|
13
|
+
require_relative "kernels/binarizer"
|
|
14
|
+
require_relative "kernels/detector"
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QrReader
|
|
4
|
+
# Optional native kernels from the qr_reader-native gem.
|
|
5
|
+
#
|
|
6
|
+
# qr_reader-native contains the files under lib/qr_reader/kernels compiled
|
|
7
|
+
# to C by Spinel. When it is installed (and QR_READER_NATIVE is not "0"),
|
|
8
|
+
# its entry points replace the pure-Ruby QrReaderKernels methods - but only
|
|
9
|
+
# if it was built from exactly the kernel sources of this qr_reader version
|
|
10
|
+
# (compared by digest). Otherwise everything keeps running as pure Ruby.
|
|
11
|
+
module NativeKernels
|
|
12
|
+
# Which native entries are used (measured with tmp/kernel_bench-style
|
|
13
|
+
# comparisons on real inputs; entries not listed stay pure Ruby):
|
|
14
|
+
# faster in every mode,
|
|
15
|
+
ALWAYS = %i[binarize_hybrid binarize_local_mean finder_scan ccitt_decode lzw_decode downscale
|
|
16
|
+
jpeg_render jpeg_info].freeze
|
|
17
|
+
# only faster than Ruby without YJIT (on par or slower with it),
|
|
18
|
+
WITHOUT_YJIT = %i[png_rows png_adam7 png_unpack_rows pixels_luma].freeze
|
|
19
|
+
# not used: JPEG entropy decoding compiled by the current Spinel slows
|
|
20
|
+
# down superlinearly with image size (native/README.md).
|
|
21
|
+
NEVER = %i[jpeg_luma jpeg_luma_keep].freeze
|
|
22
|
+
|
|
23
|
+
@active = false
|
|
24
|
+
@pure = {}
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
# True when native kernels are in use.
|
|
28
|
+
def active?
|
|
29
|
+
@active
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Why native kernels are not in use (nil when they are).
|
|
33
|
+
attr_reader :reason
|
|
34
|
+
|
|
35
|
+
# The original pure-Ruby kernel methods, by name (for comparisons).
|
|
36
|
+
def pure(name)
|
|
37
|
+
@pure.fetch(name.to_sym) { QrReaderKernels.method(name) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Loads and installs the native kernels if available; returns active?.
|
|
41
|
+
def install
|
|
42
|
+
return @active if @active
|
|
43
|
+
|
|
44
|
+
if ENV["QR_READER_NATIVE"] == "0"
|
|
45
|
+
@reason = "disabled by QR_READER_NATIVE=0"
|
|
46
|
+
return false
|
|
47
|
+
end
|
|
48
|
+
begin
|
|
49
|
+
require "qr_reader_native/qr_reader_native"
|
|
50
|
+
rescue LoadError => e
|
|
51
|
+
@reason = "qr_reader-native is not installed (#{e.message})"
|
|
52
|
+
return false
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
expected = digest(QrReaderNative.kernel_files)
|
|
56
|
+
if expected != QrReaderNative.kernels_digest
|
|
57
|
+
@reason = "qr_reader-native #{QrReaderNative.kernels_digest[0, 12]} was built from other kernel " \
|
|
58
|
+
"sources than qr_reader #{VERSION} (#{expected[0, 12] if expected}); using pure Ruby"
|
|
59
|
+
warn "qr_reader: #{@reason}" if $VERBOSE || ENV["QR_READER_NATIVE"] == "1"
|
|
60
|
+
return false
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
names = QrReaderNative.singleton_methods(false) - %i[kernels_digest kernel_files]
|
|
64
|
+
names.each do |name|
|
|
65
|
+
next unless QrReaderKernels.respond_to?(name)
|
|
66
|
+
|
|
67
|
+
if ALWAYS.include?(name) then replace(name)
|
|
68
|
+
elsif WITHOUT_YJIT.include?(name) then replace(name, unless_yjit: true)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
@reason = nil
|
|
72
|
+
@active = true
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def replace(name, unless_yjit: false)
|
|
78
|
+
pure = @pure[name] = QrReaderKernels.method(name)
|
|
79
|
+
native = QrReaderNative.method(name)
|
|
80
|
+
# Strings come back from the extension tagged UTF-8; kernels work on bytes.
|
|
81
|
+
if unless_yjit
|
|
82
|
+
QrReaderKernels.define_singleton_method(name) do |*args|
|
|
83
|
+
# YJIT may be enabled after loading (e.g. by Rails), so check per call.
|
|
84
|
+
NativeKernels.yjit? ? pure.call(*args) : NativeKernels.binary(native.call(*args))
|
|
85
|
+
end
|
|
86
|
+
else
|
|
87
|
+
QrReaderKernels.define_singleton_method(name) do |*args|
|
|
88
|
+
NativeKernels.binary(native.call(*args))
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
public
|
|
94
|
+
|
|
95
|
+
def yjit?
|
|
96
|
+
defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled? ? true : false
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Result of a native entry with Strings re-tagged as binary (internal).
|
|
100
|
+
def binary(value)
|
|
101
|
+
case value
|
|
102
|
+
when String then value.force_encoding(Encoding::BINARY)
|
|
103
|
+
when Array then value.each { |v| v.force_encoding(Encoding::BINARY) if v.is_a?(String) }
|
|
104
|
+
else value
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
# SHA-256 over the given files (paths relative to lib/), nil if one is missing.
|
|
111
|
+
def digest(files)
|
|
112
|
+
require "digest"
|
|
113
|
+
lib = File.expand_path("..", __dir__)
|
|
114
|
+
sha = Digest::SHA256.new
|
|
115
|
+
files.each do |rel|
|
|
116
|
+
path = File.join(lib, rel)
|
|
117
|
+
return nil unless File.file?(path)
|
|
118
|
+
|
|
119
|
+
sha << rel << "\0" << File.binread(path) << "\0"
|
|
120
|
+
end
|
|
121
|
+
sha.hexdigest
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# True when the native kernels of the qr_reader-native gem are in use.
|
|
127
|
+
def self.native?
|
|
128
|
+
NativeKernels.active?
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QrReader
|
|
4
|
+
# Orchestrates image loading, binarization, detection and decoding.
|
|
5
|
+
#
|
|
6
|
+
# Strategies run cheapest first and stop at the first decodable symbol
|
|
7
|
+
# (unless reading all codes), so the common case stays fast while hard
|
|
8
|
+
# images still get the expensive treatment when +try_harder+ is set.
|
|
9
|
+
#
|
|
10
|
+
# Large images (photos, page scans) are searched through a resolution
|
|
11
|
+
# pyramid, coarsest level first: a code that fills a good part of the image
|
|
12
|
+
# is found quickly at low resolution, a small code on a 600 DPI page scan is
|
|
13
|
+
# still found at full resolution.
|
|
14
|
+
class Reader
|
|
15
|
+
# The coarsest pyramid level is the largest power-of-two reduction whose
|
|
16
|
+
# longer side is still at least this many pixels.
|
|
17
|
+
PYRAMID_MIN_SIDE = 800
|
|
18
|
+
# The exhaustive (try_harder) strategies only run on pyramid levels up to
|
|
19
|
+
# this size: at full resolution of a 12 MP photo they cost seconds and
|
|
20
|
+
# mostly chase texture noise, while the fast strategies still run there.
|
|
21
|
+
HARD_MAX_SIDE = 2600
|
|
22
|
+
# Pixels at most this far from square (width / height) are used as they are.
|
|
23
|
+
SQUARE_TOLERANCE = 0.15
|
|
24
|
+
# Upper bound on alternative samplings tried per candidate.
|
|
25
|
+
MAX_ALTERNATIVES = 6
|
|
26
|
+
|
|
27
|
+
attr_reader :try_harder, :inverted
|
|
28
|
+
|
|
29
|
+
# try_harder: keep trying slower strategies when the fast ones find nothing.
|
|
30
|
+
# inverted: true, false or :auto - also look for light-on-dark symbols.
|
|
31
|
+
def initialize(try_harder: true, inverted: :auto)
|
|
32
|
+
@try_harder = try_harder
|
|
33
|
+
@inverted = inverted
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# First decoded Result, or nil.
|
|
37
|
+
def read(input)
|
|
38
|
+
each_result(input, false) { |result| return result }
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# All decoded Results (distinct symbols, all pages), possibly empty.
|
|
43
|
+
def read_all(input)
|
|
44
|
+
results = []
|
|
45
|
+
each_result(input, true) { |result| results << result }
|
|
46
|
+
results
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Yields each distinct Result as soon as it is decoded. Returns an
|
|
50
|
+
# Enumerator when no block is given.
|
|
51
|
+
def scan(input, &block)
|
|
52
|
+
return enum_for(:scan, input) unless block
|
|
53
|
+
|
|
54
|
+
each_result(input, true, &block)
|
|
55
|
+
self
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class << self
|
|
59
|
+
# Converts any supported input into an Image (the first page of
|
|
60
|
+
# multi-page documents).
|
|
61
|
+
def load_image(input)
|
|
62
|
+
return input if input.is_a?(Image)
|
|
63
|
+
|
|
64
|
+
bytes = input_bytes(input)
|
|
65
|
+
if bytes
|
|
66
|
+
# Formats.decode reads the first page of TIFF and PDF too; this also
|
|
67
|
+
# finds PDFs with junk before their header.
|
|
68
|
+
if container_format(bytes) == :pdf
|
|
69
|
+
Formats::PDF.each_image(bytes) { |image, _info| return image }
|
|
70
|
+
end
|
|
71
|
+
return Formats.decode(bytes)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
if input.respond_to?(:width) && input.respond_to?(:height) && input.respond_to?(:pixels)
|
|
75
|
+
# ChunkyPNG-style canvas: pixels are 0xRRGGBBAA integers.
|
|
76
|
+
return Image.from_pixels(input.width, input.height, input.pixels.pack("N*"), :rgba)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
raise ArgumentError, "unsupported input #{input.class}: expected a path, IO, image bytes or QrReader::Image"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Encoded image bytes for paths, IOs and byte strings; nil for other objects.
|
|
83
|
+
def input_bytes(input)
|
|
84
|
+
if input.is_a?(String)
|
|
85
|
+
# A short String naming an existing file is a path, even if it
|
|
86
|
+
# starts like image data ("GIF89a.gif", "P5 scan.pgm").
|
|
87
|
+
short = input.bytesize <= 4096 && !input.include?("\0")
|
|
88
|
+
return File.binread(input) if short && File.file?(input)
|
|
89
|
+
return input.b if Formats.format_of(input) || container_format(input)
|
|
90
|
+
raise ImageError, "unrecognised image data" unless short
|
|
91
|
+
|
|
92
|
+
File.binread(input) # raises Errno::ENOENT etc. like File.read
|
|
93
|
+
elsif input.respond_to?(:to_path)
|
|
94
|
+
File.binread(input.to_path)
|
|
95
|
+
elsif input.respond_to?(:read)
|
|
96
|
+
data = input.read
|
|
97
|
+
raise ImageError, "IO returned no data" unless data
|
|
98
|
+
|
|
99
|
+
data.b
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# :tiff or :pdf for multi-page containers, else nil.
|
|
104
|
+
def container_format(bytes)
|
|
105
|
+
head = bytes.byteslice(0, 4)
|
|
106
|
+
return :tiff if ["II*\0", "MM\0*", "II+\0", "MM\0+"].include?(head)
|
|
107
|
+
return :pdf if bytes.byteslice(0, 1024)&.include?("%PDF-")
|
|
108
|
+
|
|
109
|
+
nil
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
# Yields decoded results (deduplicated) until the caller stops iterating.
|
|
116
|
+
def each_result(input, multi, &block)
|
|
117
|
+
@multi = multi
|
|
118
|
+
@emit = block
|
|
119
|
+
@seen = []
|
|
120
|
+
@y_scale = 1.0
|
|
121
|
+
|
|
122
|
+
if input.is_a?(Image)
|
|
123
|
+
scan_pyramid(input, nil)
|
|
124
|
+
return
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
bytes = self.class.input_bytes(input)
|
|
128
|
+
unless bytes
|
|
129
|
+
scan_pyramid(self.class.load_image(input), nil)
|
|
130
|
+
return
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
@y_scale = 1.0
|
|
134
|
+
case self.class.container_format(bytes)
|
|
135
|
+
when :tiff
|
|
136
|
+
# Pages that cannot be decoded are skipped.
|
|
137
|
+
Formats::TIFF.each_image(bytes) do |image, info|
|
|
138
|
+
return if scan_page(image, info) && !multi
|
|
139
|
+
end
|
|
140
|
+
when :pdf
|
|
141
|
+
# Plain JPEG page images come undecoded, for the reduced-size pass.
|
|
142
|
+
Formats::PDF.each_image(bytes, raw_jpeg: true) do |image, info|
|
|
143
|
+
found = if info[:format] == :jpeg
|
|
144
|
+
scan_jpeg_or_full(image, info[:page], info[:pixel_aspect])
|
|
145
|
+
else
|
|
146
|
+
scan_page(image, info)
|
|
147
|
+
end
|
|
148
|
+
return if found && !multi
|
|
149
|
+
end
|
|
150
|
+
else
|
|
151
|
+
if Formats.format_of(bytes) == :jpeg
|
|
152
|
+
scan_jpeg_or_full(bytes, nil, 1.0)
|
|
153
|
+
else
|
|
154
|
+
scan_pyramid(Formats.decode(bytes), nil)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# One page of a TIFF or PDF, resampled to square pixels if needed.
|
|
160
|
+
def scan_page(image, info)
|
|
161
|
+
image, @y_scale = square_pixels(image, info[:pixel_aspect])
|
|
162
|
+
scan_pyramid(image, info[:page])
|
|
163
|
+
ensure
|
|
164
|
+
@y_scale = 1.0
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def scan_jpeg_or_full(bytes, page, aspect)
|
|
168
|
+
aspect ||= 1.0
|
|
169
|
+
if (scale = jpeg_prescale(bytes)) && (aspect - 1.0).abs < SQUARE_TOLERANCE
|
|
170
|
+
scan_jpeg(bytes, scale, page)
|
|
171
|
+
else
|
|
172
|
+
scan_page(Formats::JPEG.decode(bytes), page: page, pixel_aspect: aspect)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Big JPEGs: the coarsest pyramid level comes straight from a reduced-size
|
|
177
|
+
# decode; the coefficients are kept, so the full resolution (only needed
|
|
178
|
+
# when that finds nothing) costs IDCTs but no second entropy decoding.
|
|
179
|
+
def scan_jpeg(bytes, scale, page)
|
|
180
|
+
@page = page
|
|
181
|
+
@tried = {}
|
|
182
|
+
small, coefficients = Formats::JPEG.decode_keeping_coefficients(bytes, scale: scale)
|
|
183
|
+
return true if run_strategies(small, scale.to_f, quick: true) && !@multi
|
|
184
|
+
|
|
185
|
+
small = nil
|
|
186
|
+
full = coefficients ? coefficients.image(scale: 1) : Formats::JPEG.decode(bytes)
|
|
187
|
+
coefficients = nil
|
|
188
|
+
scan_pyramid(full, page, skip_factor: scale)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# [image with square pixels, factor mapping its rows back to the original].
|
|
192
|
+
# Fax pages (204 x 98 dpi) have pixels about twice as tall as wide: their
|
|
193
|
+
# rows are repeated so that finder patterns are square again.
|
|
194
|
+
def square_pixels(image, aspect)
|
|
195
|
+
return [image, 1.0] if aspect.nil? || aspect >= 1.0 - SQUARE_TOLERANCE
|
|
196
|
+
|
|
197
|
+
height = (image.height / aspect).round
|
|
198
|
+
[image.stretch_rows(height), image.height.to_f / height]
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Reduced decode scale (2, 4 or 8) for big JPEGs, or nil.
|
|
202
|
+
def jpeg_prescale(bytes)
|
|
203
|
+
w, h = Formats::JPEG.dimensions(bytes)
|
|
204
|
+
factors = pyramid_factors(w > h ? w : h)
|
|
205
|
+
factors.size > 1 ? [factors.first, 8].min : nil
|
|
206
|
+
rescue Error
|
|
207
|
+
nil
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Power-of-two reduction factors, coarsest first, ending with 1.
|
|
211
|
+
def pyramid_factors(longest)
|
|
212
|
+
f = 1
|
|
213
|
+
f *= 2 while longest / (f * 2) >= PYRAMID_MIN_SIDE
|
|
214
|
+
factors = []
|
|
215
|
+
while f >= 1
|
|
216
|
+
factors << f
|
|
217
|
+
f /= 2
|
|
218
|
+
end
|
|
219
|
+
factors
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Quick strategies on every pyramid level (coarse to fine), then the
|
|
223
|
+
# expensive ones (coarse to fine). Returns true if anything was found.
|
|
224
|
+
def scan_pyramid(image, page, skip_factor: nil)
|
|
225
|
+
@page = page
|
|
226
|
+
@tried = {}
|
|
227
|
+
longest = image.width > image.height ? image.width : image.height
|
|
228
|
+
factors = pyramid_factors(longest)
|
|
229
|
+
levels = { 1 => image }
|
|
230
|
+
level = lambda do |f|
|
|
231
|
+
levels[f] ||= level.call(f / 2).downscale(2)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
found = false
|
|
235
|
+
factors.each do |f|
|
|
236
|
+
next if f == skip_factor
|
|
237
|
+
|
|
238
|
+
@tried = {}
|
|
239
|
+
found = true if run_strategies(level.call(f), f.to_f, quick: true)
|
|
240
|
+
return true if found && !@multi
|
|
241
|
+
end
|
|
242
|
+
return found unless @try_harder
|
|
243
|
+
|
|
244
|
+
factors.each do |f|
|
|
245
|
+
next if longest / f > HARD_MAX_SIDE
|
|
246
|
+
|
|
247
|
+
@tried = {}
|
|
248
|
+
found = true if run_strategies(level.call(f), f.to_f, quick: false, skip_quick: true)
|
|
249
|
+
return true if found && !@multi
|
|
250
|
+
end
|
|
251
|
+
found
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# quick: normal hybrid binarization + detection (both polarities).
|
|
255
|
+
# Otherwise exhaustive row scanning and the global-histogram binarizer
|
|
256
|
+
# (skip_quick: the quick passes already ran on this image).
|
|
257
|
+
def run_strategies(image, factor, quick:, skip_quick: false)
|
|
258
|
+
polarities = case @inverted
|
|
259
|
+
when true then [true]
|
|
260
|
+
when false then [false]
|
|
261
|
+
else [false, true]
|
|
262
|
+
end
|
|
263
|
+
passes = []
|
|
264
|
+
polarities.each { |inv| passes << [:hybrid, inv, false] } unless skip_quick
|
|
265
|
+
unless quick
|
|
266
|
+
polarities.each do |inv|
|
|
267
|
+
passes << [:hybrid, inv, true]
|
|
268
|
+
passes << [:global, inv, true]
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
binarized = {}
|
|
273
|
+
found = false
|
|
274
|
+
passes.each do |kind, inv, harder|
|
|
275
|
+
bits = binarized[[kind, inv]] ||= begin
|
|
276
|
+
base = binarized[[kind, false]] ||=
|
|
277
|
+
kind == :hybrid ? Binarizer.hybrid(image) : Binarizer.global_histogram(image)
|
|
278
|
+
inv ? base.invert : base
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
if (!harder || kind == :global) && (candidate = Detector.detect_pure(bits)) && try_candidate(candidate, factor)
|
|
282
|
+
found = true
|
|
283
|
+
return true unless @multi
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
Detector.new(bits, image: image).each_candidate(try_harder: harder, multi: @multi) do |cand|
|
|
287
|
+
next unless try_candidate(cand, factor)
|
|
288
|
+
|
|
289
|
+
found = true
|
|
290
|
+
return true unless @multi
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
found
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Decodes a candidate (and its alternatives); emits and returns the
|
|
297
|
+
# result, or nil. Candidates already tried at the same place are skipped.
|
|
298
|
+
def try_candidate(candidate, factor)
|
|
299
|
+
key = candidate_key(candidate)
|
|
300
|
+
return nil if key && @tried[key]
|
|
301
|
+
|
|
302
|
+
@tried[key] = true if key
|
|
303
|
+
result = Decoder.decode(candidate.bits)
|
|
304
|
+
unless result
|
|
305
|
+
candidate.alternatives.each_with_index do |bits, i|
|
|
306
|
+
break if i >= MAX_ALTERNATIVES
|
|
307
|
+
|
|
308
|
+
result = Decoder.decode(bits)
|
|
309
|
+
break if result
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
return nil unless result
|
|
313
|
+
|
|
314
|
+
y_factor = factor * @y_scale
|
|
315
|
+
points = candidate.points&.map { |x, y| [x * factor, y * y_factor] }
|
|
316
|
+
# A mirrored symbol was decoded from the transposed grid: its top-right
|
|
317
|
+
# and bottom-left corners are the grid's bottom-left and top-right.
|
|
318
|
+
points = [points[0], points[3], points[2], points[1]] if points && result.mirrored
|
|
319
|
+
result = result.with(points: points, page: @page)
|
|
320
|
+
return result if @seen.any? { |other| same_symbol?(other, result) }
|
|
321
|
+
|
|
322
|
+
@seen << result
|
|
323
|
+
@emit.call(result)
|
|
324
|
+
result
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def candidate_key(candidate)
|
|
328
|
+
pts = candidate.points
|
|
329
|
+
return nil unless pts
|
|
330
|
+
|
|
331
|
+
[candidate.bits.width] + pts.flat_map { |x, y| [(x / 2.0).round, (y / 2.0).round] }
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Two results describe the same symbol if they are on the same page, their
|
|
335
|
+
# payloads match and their centres are close (or positions are unknown).
|
|
336
|
+
def same_symbol?(a, b)
|
|
337
|
+
return false unless a.page == b.page && a.raw_bytes == b.raw_bytes
|
|
338
|
+
return true unless a.points && b.points
|
|
339
|
+
|
|
340
|
+
ax = a.points.sum { |p| p[0] } / 4.0
|
|
341
|
+
ay = a.points.sum { |p| p[1] } / 4.0
|
|
342
|
+
bx = b.points.sum { |p| p[0] } / 4.0
|
|
343
|
+
by = b.points.sum { |p| p[1] } / 4.0
|
|
344
|
+
size = Math.hypot(a.points[1][0] - a.points[0][0], a.points[1][1] - a.points[0][1])
|
|
345
|
+
Math.hypot(ax - bx, ay - by) < [size / 2, 10].max
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QrReader
|
|
4
|
+
# Structured Append header (ISO/IEC 18004 §8.3.12): this symbol is
|
|
5
|
+
# +index+ (0-based) of +count+ symbols; +parity+ is the XOR of all data bytes.
|
|
6
|
+
StructuredAppend = Data.define(:index, :count, :parity)
|
|
7
|
+
|
|
8
|
+
# One decoded data segment. +mode+ is :numeric, :alphanumeric, :byte,
|
|
9
|
+
# :kanji or :hanzi; +text+ is the decoded UTF-8 string; +eci+ the ECI
|
|
10
|
+
# designator in effect (nil if none).
|
|
11
|
+
Segment = Data.define(:mode, :text, :eci)
|
|
12
|
+
|
|
13
|
+
# A decoded QR code.
|
|
14
|
+
#
|
|
15
|
+
# text - decoded content as a UTF-8 String
|
|
16
|
+
# raw_bytes - raw payload bytes (binary String) before character set decoding
|
|
17
|
+
# version - symbol version, 1..40
|
|
18
|
+
# ec_level - error correction level: :l, :m, :q or :h (as in rqrcode)
|
|
19
|
+
# mask - data mask pattern, 0..7
|
|
20
|
+
# points - [[x, y], ...] corners in image pixel coordinates:
|
|
21
|
+
# top-left, top-right, bottom-right, bottom-left (nil if unknown)
|
|
22
|
+
# mirrored - true if the symbol was read mirrored
|
|
23
|
+
# errors_corrected - number of codewords repaired by Reed-Solomon
|
|
24
|
+
# eci - first ECI designator found, or nil
|
|
25
|
+
# fnc1 - nil, :first or [:second, application_indicator]
|
|
26
|
+
# structured_append - StructuredAppend or nil
|
|
27
|
+
# symbology_identifier - AIM identifier, e.g. "]Q1"
|
|
28
|
+
# segments - Array of Segment
|
|
29
|
+
# page - 0-based page/image index for multi-page inputs (TIFF, PDF), else nil
|
|
30
|
+
Result = Data.define(:text, :raw_bytes, :version, :ec_level, :mask, :points, :mirrored,
|
|
31
|
+
:errors_corrected, :eci, :fnc1, :structured_append,
|
|
32
|
+
:symbology_identifier, :segments, :page) do
|
|
33
|
+
def initialize(text:, raw_bytes:, version:, ec_level:, mask:, points: nil, mirrored: false,
|
|
34
|
+
errors_corrected: 0, eci: nil, fnc1: nil, structured_append: nil,
|
|
35
|
+
symbology_identifier: "]Q1", segments: [], page: nil)
|
|
36
|
+
super
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
alias_method :to_s, :text
|
|
40
|
+
|
|
41
|
+
def mirrored?
|
|
42
|
+
mirrored
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def structured_append?
|
|
46
|
+
!structured_append.nil?
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|