zxing_ffi 0.1.0
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 +27 -0
- data/LICENSE.txt +21 -0
- data/README.md +275 -0
- data/exe/zxing-scan +6 -0
- data/lib/zxing_ffi/barcode.rb +77 -0
- data/lib/zxing_ffi/cli.rb +152 -0
- data/lib/zxing_ffi/config.rb +119 -0
- data/lib/zxing_ffi/dedupe.rb +79 -0
- data/lib/zxing_ffi/diagnostics.rb +69 -0
- data/lib/zxing_ffi/dpi.rb +103 -0
- data/lib/zxing_ffi/errors.rb +69 -0
- data/lib/zxing_ffi/formats.rb +207 -0
- data/lib/zxing_ffi/geometry.rb +508 -0
- data/lib/zxing_ffi/header_probe.rb +98 -0
- data/lib/zxing_ffi/image.rb +123 -0
- data/lib/zxing_ffi/image_magick.rb +95 -0
- data/lib/zxing_ffi/library_defaults.rb +7 -0
- data/lib/zxing_ffi/library_loader.rb +176 -0
- data/lib/zxing_ffi/loaders/base.rb +155 -0
- data/lib/zxing_ffi/loaders/image_magick.rb +159 -0
- data/lib/zxing_ffi/loaders/pnm.rb +113 -0
- data/lib/zxing_ffi/loaders/poppler.rb +260 -0
- data/lib/zxing_ffi/loaders/registry.rb +54 -0
- data/lib/zxing_ffi/loaders/vips.rb +332 -0
- data/lib/zxing_ffi/loaders.rb +41 -0
- data/lib/zxing_ffi/native.rb +237 -0
- data/lib/zxing_ffi/pnm.rb +676 -0
- data/lib/zxing_ffi/reader.rb +271 -0
- data/lib/zxing_ffi/scanner.rb +295 -0
- data/lib/zxing_ffi/sniffer.rb +155 -0
- data/lib/zxing_ffi/source.rb +77 -0
- data/lib/zxing_ffi/strategy.rb +293 -0
- data/lib/zxing_ffi/subprocess.rb +416 -0
- data/lib/zxing_ffi/transformers/base.rb +69 -0
- data/lib/zxing_ffi/transformers/image_magick.rb +72 -0
- data/lib/zxing_ffi/transformers/vips.rb +85 -0
- data/lib/zxing_ffi/transformers.rb +36 -0
- data/lib/zxing_ffi/version.rb +5 -0
- data/lib/zxing_ffi.rb +82 -0
- metadata +108 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ZXingFFI
|
|
4
|
+
module Loaders
|
|
5
|
+
# In-process loader built on libvips via the optional ruby-vips gem: rasters, and PDFs as a fallback.
|
|
6
|
+
#
|
|
7
|
+
# Every input is opened with the libvips loader matching its sniffed kind (e.g. +pngload+), so libvips never
|
|
8
|
+
# re-detects a different format from the content. Operations libvips flags as *untrusted* (in libvips 8.18:
|
|
9
|
+
# +pdfload+, +magickload+, +ppmload+) are refused while +config.vips_block_untrusted+ is true (the default); this
|
|
10
|
+
# gives the protection of +vips_block_untrusted_set+ without changing libvips' process-global state.
|
|
11
|
+
# Consequently vips renders PDFs only after opting in with +vips_block_untrusted = false+ — and a crash in
|
|
12
|
+
# libvips or poppler-glib then takes down the Ruby process.
|
|
13
|
+
#
|
|
14
|
+
# Documents are safe to render concurrently (each render builds its own libvips pipeline).
|
|
15
|
+
class VipsLoader < Base
|
|
16
|
+
# Sniffed kind => libvips load operation.
|
|
17
|
+
OPERATIONS = {
|
|
18
|
+
png: "pngload", jpeg: "jpegload", tiff: "tiffload", gif: "gifload", webp: "webpload",
|
|
19
|
+
heif: "heifload", avif: "heifload", pnm: "ppmload", bmp: "magickload", pdf: "pdfload"
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
# Loaders that take a +page:+ argument.
|
|
23
|
+
PAGED = %w[tiffload gifload webpload heifload pdfload magickload].freeze
|
|
24
|
+
|
|
25
|
+
# VIPS_OPERATION_UNTRUSTED in VipsOperationFlags.
|
|
26
|
+
UNTRUSTED_FLAG = 16
|
|
27
|
+
|
|
28
|
+
# Resolutions (pixels per inch) differing by more than this ratio are corrected.
|
|
29
|
+
ASPECT_TOLERANCE = 0.05
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
# (see Base.loader_name)
|
|
33
|
+
def loader_name = :vips
|
|
34
|
+
|
|
35
|
+
# (see Base.kinds)
|
|
36
|
+
def kinds = OPERATIONS.keys
|
|
37
|
+
|
|
38
|
+
# (see Base.install_hint)
|
|
39
|
+
def install_hint
|
|
40
|
+
"install libvips (brew install vips / apt install libvips-tools) and the ruby-vips gem"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Whether this libvips can load +kind+ right now (operation present and, unless opted in, trusted).
|
|
44
|
+
def supports?(kind)
|
|
45
|
+
operation = OPERATIONS[kind.to_sym]
|
|
46
|
+
return false unless operation && available? && operation?(operation)
|
|
47
|
+
|
|
48
|
+
!(ZXingFFI.config.vips_block_untrusted && untrusted?(operation))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# (see Base.unsupported_reason)
|
|
52
|
+
def unsupported_reason(kind)
|
|
53
|
+
operation = OPERATIONS[kind.to_sym]
|
|
54
|
+
return nil unless operation && available?
|
|
55
|
+
return "this libvips has no #{operation}" unless operation?(operation)
|
|
56
|
+
return nil unless ZXingFFI.config.vips_block_untrusted && untrusted?(operation)
|
|
57
|
+
|
|
58
|
+
"libvips flags #{operation} as untrusted; it is refused while config.vips_block_untrusted is true " \
|
|
59
|
+
"(set it to false to allow it)"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# (see Base.diagnostics)
|
|
63
|
+
def diagnostics
|
|
64
|
+
return super unless available?
|
|
65
|
+
|
|
66
|
+
super.merge(
|
|
67
|
+
version: ::Vips.version_string,
|
|
68
|
+
block_untrusted: ZXingFFI.config.vips_block_untrusted,
|
|
69
|
+
untrusted_operations: OPERATIONS.values.uniq.select { |op| operation?(op) && untrusted?(op) }
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @api private
|
|
74
|
+
def operation?(name)
|
|
75
|
+
@operations ||= {}
|
|
76
|
+
return @operations[name] if @operations.key?(name)
|
|
77
|
+
|
|
78
|
+
@operations[name] = ::Vips.type_find("VipsOperation", name) != 0
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# @api private
|
|
82
|
+
def untrusted?(name)
|
|
83
|
+
@untrusted ||= {}
|
|
84
|
+
return @untrusted[name] if @untrusted.key?(name)
|
|
85
|
+
|
|
86
|
+
@untrusted[name] = (::Vips.vips_operation_get_flags(::Vips::Operation.new(name)) & UNTRUSTED_FLAG) != 0
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# (see Base.reset!)
|
|
90
|
+
def reset!
|
|
91
|
+
super
|
|
92
|
+
@operations = nil
|
|
93
|
+
@untrusted = nil
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def probe
|
|
99
|
+
require "vips"
|
|
100
|
+
true
|
|
101
|
+
rescue LoadError, StandardError => e
|
|
102
|
+
@unavailable_reason = "ruby-vips/libvips not loadable (#{e.class}: #{e.message.lines.first&.strip})"
|
|
103
|
+
false
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# (see Base#open)
|
|
108
|
+
def open(source, password: nil)
|
|
109
|
+
unless self.class.supports?(source.kind)
|
|
110
|
+
raise LoaderUnavailable, "libvips cannot load #{source.kind} here (missing or untrusted #{OPERATIONS[source.kind]})"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
if source.kind == :pdf
|
|
114
|
+
PdfDocument.new(source, config, password)
|
|
115
|
+
else
|
|
116
|
+
RasterDocument.new(source, config, OPERATIONS.fetch(source.kind))
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Shared helpers for libvips-backed documents.
|
|
121
|
+
module Pipeline
|
|
122
|
+
# Loads +path+ with +operation+ without libvips' operation cache handing back a stale image: libvips caches
|
|
123
|
+
# loads by file name, so a file overwritten at the same path would otherwise decode as its old contents
|
|
124
|
+
# (e.g. an upload handler reusing a temp path). libvips ≥ 8.15 has +revalidate+; older versions load from a
|
|
125
|
+
# buffer, which is never served from the file-name cache.
|
|
126
|
+
# @api private
|
|
127
|
+
def self.load_fresh(operation, path, **options)
|
|
128
|
+
::Vips.vips_error_clear # see error_summary
|
|
129
|
+
if ::Vips.at_least_libvips?(8, 15)
|
|
130
|
+
::Vips::Image.public_send(operation, path, revalidate: true, **options)
|
|
131
|
+
else
|
|
132
|
+
::Vips::Image.public_send(:"#{operation}_buffer", File.binread(path), **options)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# The cause of a libvips failure. A +Vips::Error+'s message is libvips' process-wide error buffer, oldest line
|
|
137
|
+
# first, and successful calls can leave warnings there (e.g. heifload's "bad seek"); so the buffer is cleared
|
|
138
|
+
# before our calls and the last lines are the ones that describe this failure.
|
|
139
|
+
# @api private
|
|
140
|
+
def self.error_summary(error, lines: 3)
|
|
141
|
+
error.message.lines.map(&:strip).reject(&:empty?).last(lines).join(" ")
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
def vips_call
|
|
147
|
+
::Vips.vips_error_clear
|
|
148
|
+
yield
|
|
149
|
+
rescue ::Vips::Error => e
|
|
150
|
+
raise RenderError.new("libvips failed on #{source.name}: #{Pipeline.error_summary(e)}", stderr: e.message[0, 4096])
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Flattens alpha onto white, converts to 8-bit single-band luminance and copies it into an Image.
|
|
154
|
+
def to_image(vimage)
|
|
155
|
+
check_pixels!(vimage.width, vimage.height)
|
|
156
|
+
if vimage.has_alpha?
|
|
157
|
+
white = %i[ushort short].include?(vimage.format) ? 65_535 : 255
|
|
158
|
+
vimage = vimage.flatten(background: [white])
|
|
159
|
+
end
|
|
160
|
+
vimage = vimage.colourspace(:b_w) unless vimage.interpretation == :"b-w" && vimage.format == :uchar
|
|
161
|
+
vimage = vimage.extract_band(0) if vimage.bands > 1
|
|
162
|
+
vimage = vimage.cast(:uchar) unless vimage.format == :uchar
|
|
163
|
+
Image.new(vimage.write_to_memory, width: vimage.width, height: vimage.height)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def check_pixels!(width, height)
|
|
167
|
+
pixels = width * height
|
|
168
|
+
return unless @config.max_pixels && pixels > @config.max_pixels
|
|
169
|
+
|
|
170
|
+
raise LimitExceeded.new("#{width}x#{height} (#{pixels} pixels) exceeds max_pixels #{@config.max_pixels}",
|
|
171
|
+
limit: :max_pixels, value: pixels)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# A raster file: one page per frame (multi-page TIFF, animated GIF/WebP, HEIF collections).
|
|
176
|
+
class RasterDocument < Document
|
|
177
|
+
include Pipeline
|
|
178
|
+
|
|
179
|
+
def initialize(source, config, operation)
|
|
180
|
+
super(source)
|
|
181
|
+
@config = config
|
|
182
|
+
@operation = operation
|
|
183
|
+
header = load(0)
|
|
184
|
+
@page_count = (header.get_typeof("n-pages") != 0) ? [header.get("n-pages"), 1].max : 1
|
|
185
|
+
@infos = {}
|
|
186
|
+
@mutex = Mutex.new
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
attr_reader :page_count
|
|
190
|
+
|
|
191
|
+
# (see Document#page_info)
|
|
192
|
+
def page_info(number)
|
|
193
|
+
check_page!(number)
|
|
194
|
+
@mutex.synchronize { @infos[number] ||= build_info(number) }
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Applies the normalization contract: aspect correction (on the stored axes, because libvips'
|
|
198
|
+
# autorot does not swap xres/yres), EXIF orientation, alpha → white, 16-bit scaling, 8-bit gray.
|
|
199
|
+
def render(number, dpi: nil, timeout: nil) # timeout: in-process, cannot be enforced
|
|
200
|
+
check_page!(number)
|
|
201
|
+
vips_call do
|
|
202
|
+
vimage = load(number - 1)
|
|
203
|
+
vimage, aspect = correct_aspect(vimage)
|
|
204
|
+
orientation = orientation_of(vimage)
|
|
205
|
+
orientation = nil if orientation == 1 # "normal": nothing to apply
|
|
206
|
+
vimage = vimage.autorot if orientation
|
|
207
|
+
image = to_image(vimage)
|
|
208
|
+
Page.new(number: number, image: image, dpi: nil, scale_to_base: 1.0, metadata: {
|
|
209
|
+
loader: :vips, orientation_applied: orientation, aspect_corrected: aspect
|
|
210
|
+
})
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
private
|
|
215
|
+
|
|
216
|
+
def load(index)
|
|
217
|
+
vips_call do
|
|
218
|
+
options = PAGED.include?(@operation) ? {page: index} : {}
|
|
219
|
+
Pipeline.load_fresh(@operation, source.path, **options)
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def build_info(number)
|
|
224
|
+
vimage = load(number - 1)
|
|
225
|
+
width, height = corrected_size(vimage)
|
|
226
|
+
orientation = orientation_of(vimage)
|
|
227
|
+
width, height = height, width if [5, 6, 7, 8].include?(orientation)
|
|
228
|
+
PageInfo.new(number: number, width: width, height: height, unit: :px,
|
|
229
|
+
rotation: {3 => 180, 6 => 90, 8 => 270}.fetch(orientation, 0), native_ppi: ppi(vimage))
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def orientation_of(vimage)
|
|
233
|
+
(vimage.get_typeof("orientation") != 0) ? vimage.get("orientation") : nil
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# libvips resolutions are pixels per millimetre; 1.0 px/mm is its "unknown" default.
|
|
237
|
+
def resolutions(vimage)
|
|
238
|
+
x = vimage.xres * 25.4
|
|
239
|
+
y = vimage.yres * 25.4
|
|
240
|
+
(x > 25.5 || y > 25.5) ? [x, y] : nil
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def ppi(vimage)
|
|
244
|
+
res = resolutions(vimage)
|
|
245
|
+
res&.max&.round(2)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def aspect_scale(vimage)
|
|
249
|
+
x, y = resolutions(vimage)
|
|
250
|
+
return nil unless x && y && x.positive? && y.positive?
|
|
251
|
+
return nil if (x - y).abs / [x, y].max <= ASPECT_TOLERANCE
|
|
252
|
+
|
|
253
|
+
(x < y) ? [y / x, 1.0] : [1.0, x / y]
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def corrected_size(vimage)
|
|
257
|
+
hscale, vscale = aspect_scale(vimage) || [1.0, 1.0]
|
|
258
|
+
[(vimage.width * hscale).round, (vimage.height * vscale).round]
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Resamples the lower-resolution axis up (standard fax: 204×98 DPI is stored squashed 2:1).
|
|
262
|
+
def correct_aspect(vimage)
|
|
263
|
+
hscale, vscale = aspect_scale(vimage)
|
|
264
|
+
return [vimage, nil] unless hscale
|
|
265
|
+
|
|
266
|
+
width, height = corrected_size(vimage)
|
|
267
|
+
check_pixels!(width, height)
|
|
268
|
+
resized = vimage.resize(hscale, vscale: vscale, kernel: :linear)
|
|
269
|
+
[resized, resolutions(vimage).map { |r| r.round(1) }]
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# PDF pages rendered in-process by libvips' pdfload (opt-in, see the class comment).
|
|
274
|
+
class PdfDocument < Document
|
|
275
|
+
include Pipeline
|
|
276
|
+
|
|
277
|
+
def initialize(source, config, password)
|
|
278
|
+
super(source)
|
|
279
|
+
@config = config
|
|
280
|
+
@password = password
|
|
281
|
+
header = load(0, 72)
|
|
282
|
+
@page_count = (header.get_typeof("n-pages") != 0) ? header.get("n-pages") : 1
|
|
283
|
+
@infos = {}
|
|
284
|
+
@mutex = Mutex.new
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
attr_reader :page_count
|
|
288
|
+
|
|
289
|
+
# Sizes in points of the page as displayed (pdfload applies /Rotate).
|
|
290
|
+
def page_info(number)
|
|
291
|
+
check_page!(number)
|
|
292
|
+
@mutex.synchronize do
|
|
293
|
+
@infos[number] ||= begin
|
|
294
|
+
vimage = load(number - 1, 72)
|
|
295
|
+
PageInfo.new(number: number, width: vimage.width, height: vimage.height, unit: :pt, rotation: 0, native_ppi: nil)
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# (see Document#render)
|
|
301
|
+
def render(number, dpi: nil, timeout: nil) # timeout: in-process, cannot be enforced
|
|
302
|
+
check_page!(number)
|
|
303
|
+
dpi ||= @config.default_dpi
|
|
304
|
+
vips_call do
|
|
305
|
+
image = to_image(load(number - 1, dpi))
|
|
306
|
+
Page.new(number: number, image: image, dpi: dpi, scale_to_base: 1.0, metadata: {loader: :vips})
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
private
|
|
311
|
+
|
|
312
|
+
def load(index, dpi)
|
|
313
|
+
options = {page: index, dpi: dpi, background: [255]}
|
|
314
|
+
options[:password] = @password if @password
|
|
315
|
+
Pipeline.load_fresh("pdfload", source.path, **options)
|
|
316
|
+
rescue ::Vips::Error => e
|
|
317
|
+
raise password_error(e) if e.message.match?(/encrypted|password/i)
|
|
318
|
+
|
|
319
|
+
raise RenderError.new("libvips pdfload failed on #{source.name}: #{Pipeline.error_summary(e, lines: 1)}", stderr: e.message[0, 4096])
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def password_error(error)
|
|
323
|
+
if @password
|
|
324
|
+
IncorrectPassword.new("the password for #{source.name} was rejected")
|
|
325
|
+
else
|
|
326
|
+
PasswordRequired.new("#{source.name} is encrypted; pass password:")
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ZXingFFI
|
|
4
|
+
# Input loaders: each turns a file into pages of normalized 8-bit grayscale {Image}s.
|
|
5
|
+
#
|
|
6
|
+
# Loader classes are looked up by name (+:poppler+, +:vips+, +:image_magick+, +:pnm+). Every loader file can
|
|
7
|
+
# be required without its external tool or gem; availability is checked at runtime.
|
|
8
|
+
module Loaders
|
|
9
|
+
autoload :Base, "zxing_ffi/loaders/base"
|
|
10
|
+
autoload :Document, "zxing_ffi/loaders/base"
|
|
11
|
+
autoload :Page, "zxing_ffi/loaders/base"
|
|
12
|
+
autoload :PageInfo, "zxing_ffi/loaders/base"
|
|
13
|
+
autoload :Registry, "zxing_ffi/loaders/registry"
|
|
14
|
+
autoload :PopplerLoader, "zxing_ffi/loaders/poppler"
|
|
15
|
+
autoload :VipsLoader, "zxing_ffi/loaders/vips"
|
|
16
|
+
autoload :ImageMagickLoader, "zxing_ffi/loaders/image_magick"
|
|
17
|
+
autoload :PnmLoader, "zxing_ffi/loaders/pnm"
|
|
18
|
+
|
|
19
|
+
# Loader name => class name.
|
|
20
|
+
NAMES = {
|
|
21
|
+
poppler: :PopplerLoader,
|
|
22
|
+
vips: :VipsLoader,
|
|
23
|
+
image_magick: :ImageMagickLoader,
|
|
24
|
+
pnm: :PnmLoader
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
# @param name [Symbol] e.g. +:poppler+
|
|
29
|
+
# @return [Class<Base>]
|
|
30
|
+
# @raise [ArgumentError] for unknown names
|
|
31
|
+
def fetch(name)
|
|
32
|
+
const_get(NAMES.fetch(name.to_sym) { raise ArgumentError, "unknown loader #{name.inspect}; known: #{NAMES.keys.join(", ")}" })
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @return [Hash{Symbol => Class<Base>}] every loader, available or not
|
|
36
|
+
def registry
|
|
37
|
+
NAMES.keys.to_h { |name| [name, fetch(name)] }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ffi"
|
|
4
|
+
|
|
5
|
+
module ZXingFFI
|
|
6
|
+
# FFI declarations for the zxing-cpp C API (ZXingC.h, verified against 3.1.1).
|
|
7
|
+
#
|
|
8
|
+
# This module knows nothing about Ruby-level types: it only declares functions, enums and structs.
|
|
9
|
+
# {Reader} owns every native object's lifetime. The library is loaded lazily by {.load!}.
|
|
10
|
+
#
|
|
11
|
+
# Ownership (from the header): non-const +ZXing_X*+ results are freed with +ZXing_X_delete+; non-const
|
|
12
|
+
# +char*+/+uint8_t*+ results with +ZXing_free+; const results belong to their parent.
|
|
13
|
+
module Native
|
|
14
|
+
extend FFI::Library
|
|
15
|
+
|
|
16
|
+
# ZXing_ImageFormat (explicit values in the header).
|
|
17
|
+
enum :image_format, [
|
|
18
|
+
:none, 0,
|
|
19
|
+
:lum, 0x01000000,
|
|
20
|
+
:lum_a, 0x02000000,
|
|
21
|
+
:rgb, 0x03000102,
|
|
22
|
+
:bgr, 0x03020100,
|
|
23
|
+
:rgba, 0x04000102,
|
|
24
|
+
:argb, 0x04010203,
|
|
25
|
+
:bgra, 0x04020100,
|
|
26
|
+
:abgr, 0x04030201
|
|
27
|
+
]
|
|
28
|
+
enum :binarizer, [:local_average, 0, :global_histogram, :fixed_threshold, :bool_cast]
|
|
29
|
+
enum :ean_add_on_symbol, [:ignore, 0, :read, :require]
|
|
30
|
+
enum :text_mode, [:plain, 0, :eci, :hri, :escaped, :hex, :hex_eci]
|
|
31
|
+
enum :content_type, [:text, 0, :binary, :mixed, :gs1, :iso15434, :unknown_eci]
|
|
32
|
+
enum :error_type, [:none, 0, :format, :checksum, :unsupported]
|
|
33
|
+
|
|
34
|
+
# ZXing_BarcodeFormat values are generated by macros and changed in 3.0: never hardcode them.
|
|
35
|
+
# They are plain C enum values, i.e. ints; see {Formats} for the runtime map.
|
|
36
|
+
typedef :int, :barcode_format
|
|
37
|
+
|
|
38
|
+
# ZXing_PointI
|
|
39
|
+
class PointI < FFI::Struct
|
|
40
|
+
layout :x, :int, :y, :int
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# ZXing_Position
|
|
44
|
+
class Position < FFI::Struct
|
|
45
|
+
layout :top_left, PointI, :top_right, PointI, :bottom_right, PointI, :bottom_left, PointI
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# [name, args, return, options] of functions every supported library exports.
|
|
49
|
+
REQUIRED_FUNCTIONS = [
|
|
50
|
+
[:ZXing_Version, [], :string],
|
|
51
|
+
[:ZXing_free, [:pointer], :void],
|
|
52
|
+
[:ZXing_LastErrorMsg, [], :pointer],
|
|
53
|
+
|
|
54
|
+
[:ZXing_ImageView_new, [:pointer, :int, :int, :image_format, :int, :int], :pointer],
|
|
55
|
+
[:ZXing_ImageView_delete, [:pointer], :void],
|
|
56
|
+
[:ZXing_ImageView_crop, [:pointer, :int, :int, :int, :int], :void],
|
|
57
|
+
[:ZXing_ImageView_rotate, [:pointer, :int], :void],
|
|
58
|
+
|
|
59
|
+
[:ZXing_BarcodeFormatSymbology, [:barcode_format], :barcode_format],
|
|
60
|
+
[:ZXing_BarcodeFormatFromString, [:string], :barcode_format],
|
|
61
|
+
[:ZXing_BarcodeFormatToString, [:barcode_format], :pointer],
|
|
62
|
+
[:ZXing_BarcodeFormatsList, [:barcode_format, :pointer], :pointer],
|
|
63
|
+
[:ZXing_BarcodeFormatsFromString, [:string, :pointer], :pointer],
|
|
64
|
+
[:ZXing_BarcodeFormatsToString, [:pointer, :int], :pointer],
|
|
65
|
+
|
|
66
|
+
[:ZXing_Barcode_isValid, [:pointer], :bool],
|
|
67
|
+
[:ZXing_Barcode_errorType, [:pointer], :error_type],
|
|
68
|
+
[:ZXing_Barcode_errorMsg, [:pointer], :pointer],
|
|
69
|
+
[:ZXing_Barcode_format, [:pointer], :barcode_format],
|
|
70
|
+
[:ZXing_Barcode_symbology, [:pointer], :barcode_format],
|
|
71
|
+
[:ZXing_Barcode_contentType, [:pointer], :content_type],
|
|
72
|
+
[:ZXing_Barcode_bytes, [:pointer, :pointer], :pointer],
|
|
73
|
+
[:ZXing_Barcode_bytesECI, [:pointer, :pointer], :pointer],
|
|
74
|
+
[:ZXing_Barcode_text, [:pointer], :pointer],
|
|
75
|
+
[:ZXing_Barcode_symbologyIdentifier, [:pointer], :pointer],
|
|
76
|
+
[:ZXing_Barcode_position, [:pointer], Position.by_value],
|
|
77
|
+
[:ZXing_Barcode_orientation, [:pointer], :int],
|
|
78
|
+
[:ZXing_Barcode_hasECI, [:pointer], :bool],
|
|
79
|
+
[:ZXing_Barcode_isInverted, [:pointer], :bool],
|
|
80
|
+
[:ZXing_Barcode_isMirrored, [:pointer], :bool],
|
|
81
|
+
[:ZXing_Barcode_lineCount, [:pointer], :int],
|
|
82
|
+
[:ZXing_Barcode_sequenceIndex, [:pointer], :int],
|
|
83
|
+
[:ZXing_Barcode_sequenceSize, [:pointer], :int],
|
|
84
|
+
[:ZXing_Barcode_sequenceId, [:pointer], :pointer],
|
|
85
|
+
[:ZXing_Barcode_extra, [:pointer, :string], :pointer],
|
|
86
|
+
|
|
87
|
+
[:ZXing_Barcodes_delete, [:pointer], :void],
|
|
88
|
+
[:ZXing_Barcodes_size, [:pointer], :int],
|
|
89
|
+
[:ZXing_Barcodes_at, [:pointer, :int], :pointer],
|
|
90
|
+
|
|
91
|
+
[:ZXing_ReaderOptions_new, [], :pointer],
|
|
92
|
+
[:ZXing_ReaderOptions_delete, [:pointer], :void],
|
|
93
|
+
[:ZXing_ReaderOptions_setTryHarder, [:pointer, :bool], :void],
|
|
94
|
+
[:ZXing_ReaderOptions_setTryRotate, [:pointer, :bool], :void],
|
|
95
|
+
[:ZXing_ReaderOptions_setTryInvert, [:pointer, :bool], :void],
|
|
96
|
+
[:ZXing_ReaderOptions_setTryDownscale, [:pointer, :bool], :void],
|
|
97
|
+
[:ZXing_ReaderOptions_setIsPure, [:pointer, :bool], :void],
|
|
98
|
+
[:ZXing_ReaderOptions_setValidateOptionalChecksum, [:pointer, :bool], :void],
|
|
99
|
+
[:ZXing_ReaderOptions_setReturnErrors, [:pointer, :bool], :void],
|
|
100
|
+
[:ZXing_ReaderOptions_setFormats, [:pointer, :pointer, :int], :void],
|
|
101
|
+
[:ZXing_ReaderOptions_setBinarizer, [:pointer, :binarizer], :void],
|
|
102
|
+
[:ZXing_ReaderOptions_setEanAddOnSymbol, [:pointer, :ean_add_on_symbol], :void],
|
|
103
|
+
[:ZXing_ReaderOptions_setTextMode, [:pointer, :text_mode], :void],
|
|
104
|
+
[:ZXing_ReaderOptions_setMinLineCount, [:pointer, :int], :void],
|
|
105
|
+
[:ZXing_ReaderOptions_setMaxNumberOfSymbols, [:pointer, :int], :void],
|
|
106
|
+
[:ZXing_ReaderOptions_getTryHarder, [:pointer], :bool],
|
|
107
|
+
[:ZXing_ReaderOptions_getTryRotate, [:pointer], :bool],
|
|
108
|
+
[:ZXing_ReaderOptions_getTryInvert, [:pointer], :bool],
|
|
109
|
+
[:ZXing_ReaderOptions_getTryDownscale, [:pointer], :bool],
|
|
110
|
+
[:ZXing_ReaderOptions_getIsPure, [:pointer], :bool],
|
|
111
|
+
[:ZXing_ReaderOptions_getValidateOptionalChecksum, [:pointer], :bool],
|
|
112
|
+
[:ZXing_ReaderOptions_getReturnErrors, [:pointer], :bool],
|
|
113
|
+
[:ZXing_ReaderOptions_getFormats, [:pointer, :pointer], :pointer],
|
|
114
|
+
[:ZXing_ReaderOptions_getBinarizer, [:pointer], :binarizer],
|
|
115
|
+
[:ZXing_ReaderOptions_getEanAddOnSymbol, [:pointer], :ean_add_on_symbol],
|
|
116
|
+
[:ZXing_ReaderOptions_getTextMode, [:pointer], :text_mode],
|
|
117
|
+
[:ZXing_ReaderOptions_getMinLineCount, [:pointer], :int],
|
|
118
|
+
[:ZXing_ReaderOptions_getMaxNumberOfSymbols, [:pointer], :int],
|
|
119
|
+
|
|
120
|
+
# Releases the GVL so several threads decode concurrently.
|
|
121
|
+
[:ZXing_ReadBarcodes, [:pointer, :pointer], :pointer, {blocking: true}]
|
|
122
|
+
].freeze
|
|
123
|
+
|
|
124
|
+
# Optional features: feature name => [[name, args, return], ...]; all functions must resolve.
|
|
125
|
+
OPTIONAL_FUNCTIONS = {
|
|
126
|
+
# only with ZXING_EXPERIMENTAL_API
|
|
127
|
+
try_denoise: [
|
|
128
|
+
[:ZXing_ReaderOptions_setTryDenoise, [:pointer, :bool], :void],
|
|
129
|
+
[:ZXing_ReaderOptions_getTryDenoise, [:pointer], :bool]
|
|
130
|
+
],
|
|
131
|
+
# newer name of ZXing_Barcode_orientation
|
|
132
|
+
rotation: [[:ZXing_Barcode_rotation, [:pointer], :int]],
|
|
133
|
+
# bounds-checked ImageView constructor
|
|
134
|
+
image_view_new_checked: [
|
|
135
|
+
[:ZXing_ImageView_new_checked, [:pointer, :int, :int, :int, :image_format, :int, :int], :pointer]
|
|
136
|
+
]
|
|
137
|
+
}.freeze
|
|
138
|
+
|
|
139
|
+
@mutex = Mutex.new
|
|
140
|
+
@library = nil
|
|
141
|
+
@features = {}.freeze
|
|
142
|
+
|
|
143
|
+
class << self
|
|
144
|
+
# Loads and attaches libZXing once (thread safe). Later calls are no-ops.
|
|
145
|
+
#
|
|
146
|
+
# @return [LibraryLoader::Found]
|
|
147
|
+
# @raise [LibraryNotFound, IncompatibleLibrary]
|
|
148
|
+
def load!
|
|
149
|
+
return @library if @library
|
|
150
|
+
|
|
151
|
+
@mutex.synchronize do
|
|
152
|
+
return @library if @library
|
|
153
|
+
|
|
154
|
+
found = LibraryLoader.find
|
|
155
|
+
ffi_lib(found.path)
|
|
156
|
+
REQUIRED_FUNCTIONS.each { |name, args, ret, opts| attach_function(name, args, ret, **(opts || {})) }
|
|
157
|
+
@features = OPTIONAL_FUNCTIONS.to_h { |feature, functions| [feature, attach_optional(functions)] }.freeze
|
|
158
|
+
@library = found
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# @return [Boolean] whether {.load!} has succeeded
|
|
163
|
+
def loaded?
|
|
164
|
+
!@library.nil?
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# @return [LibraryLoader::Found, nil]
|
|
168
|
+
attr_reader :library
|
|
169
|
+
|
|
170
|
+
# Whether an optional native feature is present: +:try_denoise+, +:rotation+, +:image_view_new_checked+.
|
|
171
|
+
# @return [Boolean]
|
|
172
|
+
def supports?(feature)
|
|
173
|
+
load!
|
|
174
|
+
@features.fetch(feature.to_sym) { raise ArgumentError, "unknown native feature: #{feature.inspect}" }
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# @return [Array<Symbol>] optional features present in the loaded library
|
|
178
|
+
def optional_features
|
|
179
|
+
load!
|
|
180
|
+
@features.select { |_, present| present }.keys
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Rotation of a barcode in degrees as reported by the library (-180..180).
|
|
184
|
+
def barcode_rotation(barcode)
|
|
185
|
+
@features[:rotation] ? ZXing_Barcode_rotation(barcode) : ZXing_Barcode_orientation(barcode)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Copies a caller-owned C string into a UTF-8 Ruby String and frees it. NULL → nil.
|
|
189
|
+
def take_string(pointer)
|
|
190
|
+
return nil if pointer.nil? || pointer.null?
|
|
191
|
+
|
|
192
|
+
pointer.read_string.force_encoding(Encoding::UTF_8)
|
|
193
|
+
ensure
|
|
194
|
+
ZXing_free(pointer) if pointer && !pointer.null?
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Calls the block with an +int*+ out-parameter, copies the returned caller-owned
|
|
198
|
+
# buffer of that many bytes into a BINARY String and frees it.
|
|
199
|
+
def take_bytes
|
|
200
|
+
length = FFI::MemoryPointer.new(:int)
|
|
201
|
+
pointer = yield length
|
|
202
|
+
return "".b if pointer.nil? || pointer.null?
|
|
203
|
+
|
|
204
|
+
pointer.read_bytes(length.read_int)
|
|
205
|
+
ensure
|
|
206
|
+
ZXing_free(pointer) if pointer && !pointer.null?
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Like {.take_bytes} for caller-owned +ZXing_BarcodeFormat*+ arrays.
|
|
210
|
+
# @return [Array<Integer>, nil] nil when the library returned NULL
|
|
211
|
+
def take_formats
|
|
212
|
+
count = FFI::MemoryPointer.new(:int)
|
|
213
|
+
pointer = yield count
|
|
214
|
+
return nil if pointer.nil? || pointer.null?
|
|
215
|
+
|
|
216
|
+
pointer.read_array_of_int(count.read_int)
|
|
217
|
+
ensure
|
|
218
|
+
ZXing_free(pointer) if pointer && !pointer.null?
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Fetches (and clears) the calling thread's last error message.
|
|
222
|
+
# @return [String, nil]
|
|
223
|
+
def last_error_message
|
|
224
|
+
take_string(ZXing_LastErrorMsg())
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
private
|
|
228
|
+
|
|
229
|
+
def attach_optional(functions)
|
|
230
|
+
functions.each { |name, args, ret| attach_function(name, args, ret) }
|
|
231
|
+
true
|
|
232
|
+
rescue FFI::NotFoundError
|
|
233
|
+
false
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|