zxing_ffi 0.1.0-aarch64-linux-gnu → 0.2.0-aarch64-linux-gnu
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 +4 -4
- data/CHANGELOG.md +14 -0
- data/README.md +8 -3
- data/lib/zxing_ffi/config.rb +9 -0
- data/lib/zxing_ffi/header_probe.rb +4 -2
- data/lib/zxing_ffi/loaders/base.rb +11 -2
- data/lib/zxing_ffi/loaders/vips.rb +73 -10
- data/lib/zxing_ffi/scanner.rb +41 -12
- data/lib/zxing_ffi/transformers/base.rb +3 -0
- data/lib/zxing_ffi/transformers/image_magick.rb +4 -4
- data/lib/zxing_ffi/transformers/vips.rb +14 -9
- data/lib/zxing_ffi/version.rb +1 -1
- data/vendor/lib/NOTICE.txt +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e41f65a3755d8af5128bc78740627320142c91bfb57ae4085f6d625ed9933610
|
|
4
|
+
data.tar.gz: f2de2db8d03b5d82d9a149034e9fc02e726daf6f6702286729a02f3782c5bb9b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d0ba4f3e47a9d0ef901745a20fea90be55524f57d0db8eb785aa9d4b2904dd3789876ba11cceef073a58efd3c83138f741d08eacb8a504bca1b01cef59f30213
|
|
7
|
+
data.tar.gz: 988a3f5800c083d451c7332796915835c521c3c6dec55e8f2c0754b89661c38ef4184846026064c078e7bde4961e9deba59ce64969d0724c433f8117604cbd6d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0 (2026-10-08)
|
|
4
|
+
|
|
5
|
+
- **`oversize: :downscale`** (scan option and `config.oversize`): rasters over `max_pixels` are decoded smaller to fit
|
|
6
|
+
instead of raising `LimitExceeded`, by the libvips loader (JPEG and WebP shrink while decoding, other formats
|
|
7
|
+
stream), up to the new `max_source_pixels` (default 1 GP; larger ones still raise). Positions and page sizes stay
|
|
8
|
+
in the original's pixels and `PageResult#metadata[:downscaled]` records the scale. The default stays `:raise`.
|
|
9
|
+
- **`high_res` pass**: rasters are upscaled with a centered bicubic (Catmull-Rom) filter instead of pixel
|
|
10
|
+
replication, which kept the aliasing of codes rasterized at 1–2 px per module. QR codes at ~1.5 px per module
|
|
11
|
+
that every pass missed are now found at `effort: :normal`. Both transformers' `resize` upscale this way at any
|
|
12
|
+
scale above 1.
|
|
13
|
+
- **Tooling**: `rake release` publishes a release from `main`: it tags the commit, waits for the Platform gems
|
|
14
|
+
workflow, checks the seven gems that workflow built, pushes them to RubyGems (asking for the OTP) and creates the
|
|
15
|
+
GitHub release. It replaces Bundler's `rake release`, which pushes a locally built ruby gem only.
|
|
16
|
+
|
|
3
17
|
## 0.1.0 (2026-10-01)
|
|
4
18
|
|
|
5
19
|
First release candidate of `zxing_ffi`, a replacement for the unmaintained `zbar` gem.
|
data/README.md
CHANGED
|
@@ -134,7 +134,8 @@ page in page order (or returns an Enumerator). Inputs can be a path (`String`/`P
|
|
|
134
134
|
| `min_length` | `{}` | minimum text length per format, e.g. `{itf: 6}` |
|
|
135
135
|
| `timeout` | — | per-page budget in seconds: caps renders (re-renders get what is left) and stops escalation (the first pass always runs) |
|
|
136
136
|
| `instrument` | — | callable receiving `(event, payload)` for `:page_loaded`, `:pass_completed`, `:page_completed` |
|
|
137
|
-
| `max_pixels
|
|
137
|
+
| `oversize` | `:raise` | rasters over `max_pixels`: `:raise` raises `LimitExceeded`; `:downscale` decodes them smaller to fit (libvips loader only: JPEG and WebP shrink while decoding, other formats stream), up to `max_source_pixels`. Positions and `PageResult#width`/`height` stay in the original's pixels and `PageResult#metadata[:downscaled]` holds the scale; codes left under ~2 px per module are lost |
|
|
138
|
+
| `max_pixels`, `max_source_pixels`, `max_pages` | config | per-call limits |
|
|
138
139
|
|
|
139
140
|
Every `ZXingFFI.read` option below is accepted too and applies to every pass.
|
|
140
141
|
|
|
@@ -145,7 +146,7 @@ Every `ZXingFFI.read` option below is accepted too and applies to every pass.
|
|
|
145
146
|
| `base` | library defaults + your options | fast, normal, thorough |
|
|
146
147
|
| `inverted` | decodes a pixel-inverted copy for white-on-black codes (zxing-cpp's `try_invert` only covers 2D codes, so with it on this pass handles the linear formats) | normal, thorough |
|
|
147
148
|
| `global_binarizer` | global-histogram binarizer | normal, thorough |
|
|
148
|
-
| `high_res` | PDF re-rendered at 2× DPI;
|
|
149
|
+
| `high_res` | PDF re-rendered at 2× DPI; rasters under 2000 px upscaled 2× (bicubic, so codes at 1–2 px per module resolve) | normal, thorough |
|
|
149
150
|
| `tiles` | overlapping 1024–1536 px tiles, no downscaling (tiny codes on big pages) | thorough |
|
|
150
151
|
| `rotated_45` | image rotated 45° for linear codes at odd angles (also covers 135°/225°/315°) | thorough |
|
|
151
152
|
| `denoise` | `try_denoise: true` (libZXing built with `ZXING_EXPERIMENTAL_API` only) | thorough |
|
|
@@ -216,6 +217,8 @@ ZXingFFI.configure do |c|
|
|
|
216
217
|
c.default_dpi = 300
|
|
217
218
|
c.max_dpi = 600
|
|
218
219
|
c.max_pixels = 64_000_000
|
|
220
|
+
c.oversize = :raise # or :downscale (see the scan options)
|
|
221
|
+
c.max_source_pixels = 1_000_000_000 # with oversize: :downscale, larger rasters still raise
|
|
219
222
|
c.max_pages = nil
|
|
220
223
|
c.render_timeout = 60
|
|
221
224
|
c.subprocess_memory_limit = 2 * 1024**3
|
|
@@ -237,7 +240,9 @@ The gem is meant to process untrusted documents:
|
|
|
237
240
|
limit (`subprocess_memory_limit`, enforced on Linux only — macOS ignores `RLIMIT_AS`). Passwords are only ever passed
|
|
238
241
|
as the argument after `-upw`.
|
|
239
242
|
- **Pixel cap**: every bitmap is checked against `max_pixels` before it is rendered or decoded. PDF pages that would
|
|
240
|
-
exceed it are rendered at a lower (possibly fractional) DPI; rasters raise `ZXingFFI::LimitExceeded
|
|
243
|
+
exceed it are rendered at a lower (possibly fractional) DPI; rasters raise `ZXingFFI::LimitExceeded`, unless
|
|
244
|
+
`oversize: :downscale` has libvips decode them smaller to fit (never holding them at full size), in which case the
|
|
245
|
+
header check holds them to `max_source_pixels` instead.
|
|
241
246
|
- **No format confusion**: inputs are identified by magic bytes, never by extension. ImageMagick always reads with an
|
|
242
247
|
explicit coder (`png:/path`), never sees PDF, PostScript, SVG, MVG, MSL or text, and gets special characters in
|
|
243
248
|
paths neutralized. libvips is called with the loader for the sniffed type, and operations libvips flags as
|
data/lib/zxing_ffi/config.rb
CHANGED
|
@@ -34,6 +34,11 @@ module ZXingFFI
|
|
|
34
34
|
attr_accessor :max_dpi
|
|
35
35
|
# @return [Integer] maximum pixels per bitmap
|
|
36
36
|
attr_accessor :max_pixels
|
|
37
|
+
# @return [Integer, nil] with +oversize: :downscale+, the most pixels a raster may have; larger ones still raise
|
|
38
|
+
attr_accessor :max_source_pixels
|
|
39
|
+
# @return [Symbol] rasters over +max_pixels+: +:raise+ ({LimitExceeded}) or +:downscale+ (the libvips loader decodes
|
|
40
|
+
# them smaller to fit; positions stay in the original's pixels)
|
|
41
|
+
attr_accessor :oversize
|
|
37
42
|
# @return [Integer, nil] maximum number of pages per document (nil = unlimited)
|
|
38
43
|
attr_accessor :max_pages
|
|
39
44
|
# @return [Numeric] seconds allowed per page render
|
|
@@ -53,6 +58,8 @@ module ZXingFFI
|
|
|
53
58
|
@default_dpi = 300
|
|
54
59
|
@max_dpi = 600
|
|
55
60
|
@max_pixels = 64_000_000
|
|
61
|
+
@max_source_pixels = 1_000_000_000
|
|
62
|
+
@oversize = :raise
|
|
56
63
|
@max_pages = nil
|
|
57
64
|
@render_timeout = 60
|
|
58
65
|
@subprocess_memory_limit = 2 * 1024**3
|
|
@@ -89,6 +96,8 @@ module ZXingFFI
|
|
|
89
96
|
default_dpi: default_dpi,
|
|
90
97
|
max_dpi: max_dpi,
|
|
91
98
|
max_pixels: max_pixels,
|
|
99
|
+
max_source_pixels: max_source_pixels,
|
|
100
|
+
oversize: oversize,
|
|
92
101
|
max_pages: max_pages,
|
|
93
102
|
render_timeout: render_timeout,
|
|
94
103
|
subprocess_memory_limit: subprocess_memory_limit,
|
|
@@ -35,15 +35,17 @@ module ZXingFFI
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
# Raises when the header declares more than +max_pixels+ pixels.
|
|
38
|
+
# @param limit [Symbol] the setting +max_pixels+ comes from, for the error (+:max_source_pixels+ when the image
|
|
39
|
+
# would be downscaled to fit)
|
|
38
40
|
# @raise [LimitExceeded]
|
|
39
|
-
def check!(path, kind, max_pixels)
|
|
41
|
+
def check!(path, kind, max_pixels, limit: :max_pixels)
|
|
40
42
|
return unless max_pixels
|
|
41
43
|
|
|
42
44
|
width, height = dimensions(path, kind)
|
|
43
45
|
return unless width && height && width * height > max_pixels
|
|
44
46
|
|
|
45
47
|
raise LimitExceeded.new("#{File.basename(path)} declares #{width}x#{height} (#{width * height} pixels), " \
|
|
46
|
-
"exceeding
|
|
48
|
+
"exceeding #{limit} #{max_pixels}", limit: limit, value: width * height)
|
|
47
49
|
end
|
|
48
50
|
|
|
49
51
|
private
|
|
@@ -18,8 +18,10 @@ module ZXingFFI
|
|
|
18
18
|
# @!attribute number [Integer] 1-based page/frame number
|
|
19
19
|
# @!attribute image [Image] normalized 8-bit luminance image
|
|
20
20
|
# @!attribute dpi [Integer, nil] render DPI (PDF) or nil for rasters
|
|
21
|
-
# @!attribute scale_to_base [Float] factor mapping this image's pixels to the page's base image
|
|
22
|
-
#
|
|
21
|
+
# @!attribute scale_to_base [Float] factor mapping this image's pixels to the page's base image: 1.0, or above 1
|
|
22
|
+
# for a raster decoded downscaled, whose positions are reported in the original's pixels
|
|
23
|
+
# @!attribute metadata [Hash] what was applied, e.g. +orientation_applied:+, +aspect_corrected:+, +dpi_capped:+,
|
|
24
|
+
# +downscaled:+ (+{from: [width, height], scale:}+)
|
|
23
25
|
Page = Data.define(:number, :image, :dpi, :scale_to_base, :metadata)
|
|
24
26
|
|
|
25
27
|
# Base class for loaders. Subclasses implement the class-level capability checks and {#open}.
|
|
@@ -69,6 +71,13 @@ module ZXingFFI
|
|
|
69
71
|
""
|
|
70
72
|
end
|
|
71
73
|
|
|
74
|
+
# Whether this loader decodes rasters over +max_pixels+ smaller to fit when +oversize+ is +:downscale+
|
|
75
|
+
# (the others raise {LimitExceeded} regardless).
|
|
76
|
+
# @return [Boolean]
|
|
77
|
+
def downscales?
|
|
78
|
+
false
|
|
79
|
+
end
|
|
80
|
+
|
|
72
81
|
# Tool versions and capabilities, for {ZXingFFI.diagnostics}.
|
|
73
82
|
# @return [Hash]
|
|
74
83
|
def diagnostics
|
|
@@ -11,6 +11,10 @@ module ZXingFFI
|
|
|
11
11
|
# Consequently vips renders PDFs only after opting in with +vips_block_untrusted = false+ — and a crash in
|
|
12
12
|
# libvips or poppler-glib then takes down the Ruby process.
|
|
13
13
|
#
|
|
14
|
+
# With +oversize: :downscale+, rasters over +max_pixels+ (up to +max_source_pixels+) are decoded smaller to fit,
|
|
15
|
+
# without ever holding them at full size: JPEG decodes at 1/2, 1/4 or 1/8 and WebP at any scale directly, other
|
|
16
|
+
# formats stream through the resize.
|
|
17
|
+
#
|
|
14
18
|
# Documents are safe to render concurrently (each render builds its own libvips pipeline).
|
|
15
19
|
class VipsLoader < Base
|
|
16
20
|
# Sniffed kind => libvips load operation.
|
|
@@ -40,6 +44,9 @@ module ZXingFFI
|
|
|
40
44
|
"install libvips (brew install vips / apt install libvips-tools) and the ruby-vips gem"
|
|
41
45
|
end
|
|
42
46
|
|
|
47
|
+
# (see Base.downscales?)
|
|
48
|
+
def downscales? = true
|
|
49
|
+
|
|
43
50
|
# Whether this libvips can load +kind+ right now (operation present and, unless opted in, trusted).
|
|
44
51
|
def supports?(kind)
|
|
45
52
|
operation = OPERATIONS[kind.to_sym]
|
|
@@ -150,9 +157,15 @@ module ZXingFFI
|
|
|
150
157
|
raise RenderError.new("libvips failed on #{source.name}: #{Pipeline.error_summary(e)}", stderr: e.message[0, 4096])
|
|
151
158
|
end
|
|
152
159
|
|
|
153
|
-
#
|
|
160
|
+
# Converts to 8-bit single-band luminance and copies it into an Image.
|
|
154
161
|
def to_image(vimage)
|
|
155
162
|
check_pixels!(vimage.width, vimage.height)
|
|
163
|
+
vimage = gray(vimage)
|
|
164
|
+
Image.new(vimage.write_to_memory, width: vimage.width, height: vimage.height)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Alpha flattened onto white, then 8-bit single-band luminance.
|
|
168
|
+
def gray(vimage)
|
|
156
169
|
if vimage.has_alpha?
|
|
157
170
|
white = %i[ushort short].include?(vimage.format) ? 65_535 : 255
|
|
158
171
|
vimage = vimage.flatten(background: [white])
|
|
@@ -160,7 +173,7 @@ module ZXingFFI
|
|
|
160
173
|
vimage = vimage.colourspace(:b_w) unless vimage.interpretation == :"b-w" && vimage.format == :uchar
|
|
161
174
|
vimage = vimage.extract_band(0) if vimage.bands > 1
|
|
162
175
|
vimage = vimage.cast(:uchar) unless vimage.format == :uchar
|
|
163
|
-
|
|
176
|
+
vimage
|
|
164
177
|
end
|
|
165
178
|
|
|
166
179
|
def check_pixels!(width, height)
|
|
@@ -195,31 +208,81 @@ module ZXingFFI
|
|
|
195
208
|
end
|
|
196
209
|
|
|
197
210
|
# 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.
|
|
211
|
+
# autorot does not swap xres/yres), EXIF orientation, alpha → white, 16-bit scaling, 8-bit gray. A frame over
|
|
212
|
+
# max_pixels raises, or with +oversize: :downscale+ is decoded smaller to fit.
|
|
199
213
|
def render(number, dpi: nil, timeout: nil) # timeout: in-process, cannot be enforced
|
|
200
214
|
check_page!(number)
|
|
201
215
|
vips_call do
|
|
202
216
|
vimage = load(number - 1)
|
|
203
|
-
|
|
217
|
+
width, height = corrected_size(vimage)
|
|
218
|
+
scale = fit_scale(width, height)
|
|
219
|
+
vimage, aspect, scale = (scale < 1) ? downscaled(number - 1, vimage, scale) : [*correct_aspect(vimage), 1.0]
|
|
204
220
|
orientation = orientation_of(vimage)
|
|
205
221
|
orientation = nil if orientation == 1 # "normal": nothing to apply
|
|
206
222
|
vimage = vimage.autorot if orientation
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
223
|
+
metadata = {loader: :vips, orientation_applied: orientation, aspect_corrected: aspect}
|
|
224
|
+
if scale < 1
|
|
225
|
+
width, height = height, width if [5, 6, 7, 8].include?(orientation)
|
|
226
|
+
metadata[:downscaled] = {from: [width, height], scale: scale}
|
|
227
|
+
end
|
|
228
|
+
Page.new(number: number, image: to_image(vimage), dpi: nil, scale_to_base: 1.0 / scale, metadata: metadata)
|
|
211
229
|
end
|
|
212
230
|
end
|
|
213
231
|
|
|
214
232
|
private
|
|
215
233
|
|
|
216
|
-
def load(index)
|
|
234
|
+
def load(index, **options)
|
|
217
235
|
vips_call do
|
|
218
|
-
options = PAGED.include?(@operation)
|
|
236
|
+
options[:page] = index if PAGED.include?(@operation)
|
|
219
237
|
Pipeline.load_fresh(@operation, source.path, **options)
|
|
220
238
|
end
|
|
221
239
|
end
|
|
222
240
|
|
|
241
|
+
# 1.0 when a frame of +width+ x +height+ fits max_pixels. Otherwise it raises LimitExceeded, unless
|
|
242
|
+
# +oversize: :downscale+ lets it be decoded smaller (up to max_source_pixels): then the scale that fits.
|
|
243
|
+
def fit_scale(width, height)
|
|
244
|
+
pixels = width * height
|
|
245
|
+
return 1.0 unless @config.max_pixels && pixels > @config.max_pixels
|
|
246
|
+
|
|
247
|
+
check_pixels!(width, height) unless @config.oversize == :downscale
|
|
248
|
+
ceiling = @config.max_source_pixels
|
|
249
|
+
if ceiling && pixels > ceiling
|
|
250
|
+
raise LimitExceeded.new("#{width}x#{height} (#{pixels} pixels) exceeds max_source_pixels #{ceiling}",
|
|
251
|
+
limit: :max_source_pixels, value: pixels)
|
|
252
|
+
end
|
|
253
|
+
Math.sqrt(@config.max_pixels.to_f / pixels)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Frame +index+ (+header+ is its lazily loaded image) decoded at about +scale+ and aspect-corrected, as gray
|
|
257
|
+
# in memory, without holding it at full size. Returns the image, the aspect correction applied and the exact
|
|
258
|
+
# scale, lowered until the rounded size fits max_pixels.
|
|
259
|
+
def downscaled(index, header, scale)
|
|
260
|
+
hscale, vscale = aspect_scale(header) || [1.0, 1.0]
|
|
261
|
+
loaded, options = shrink_on_load(scale * [hscale, vscale].max)
|
|
262
|
+
options[:access] = :sequential if [hscale, vscale].max * scale <= loaded # an axis to upsample needs random access
|
|
263
|
+
vimage = gray(load(index, **options))
|
|
264
|
+
scale *= 0.9999 while (vimage.width * hscale * scale / loaded).ceil * (vimage.height * vscale * scale / loaded).ceil > @config.max_pixels
|
|
265
|
+
x = hscale * scale / loaded
|
|
266
|
+
y = vscale * scale / loaded
|
|
267
|
+
vimage = vimage.resize(x, vscale: y) unless x == 1 && y == 1
|
|
268
|
+
aspect = resolutions(header).map { |r| r.round(1) } unless hscale == 1 && vscale == 1
|
|
269
|
+
[vimage.copy_memory, aspect, scale]
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Load options that make the decoder itself shrink by about +scale+, never below it: JPEG by 1/2, 1/4 or 1/8,
|
|
273
|
+
# WebP by any factor. Returns the factor they apply and the options.
|
|
274
|
+
def shrink_on_load(scale)
|
|
275
|
+
case @operation
|
|
276
|
+
when "jpegload"
|
|
277
|
+
shrink = [8, 4, 2].find { |factor| factor * scale <= 1 }
|
|
278
|
+
shrink ? [1.0 / shrink, {shrink: shrink}] : [1.0, {}]
|
|
279
|
+
when "webpload"
|
|
280
|
+
::Vips::Introspect.get("webpload").optional_input.key?("scale") ? [scale, {scale: scale}] : [1.0, {}]
|
|
281
|
+
else
|
|
282
|
+
[1.0, {}]
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
223
286
|
def build_info(number)
|
|
224
287
|
vimage = load(number - 1)
|
|
225
288
|
width, height = corrected_size(vimage)
|
data/lib/zxing_ffi/scanner.rb
CHANGED
|
@@ -6,8 +6,8 @@ module ZXingFFI
|
|
|
6
6
|
# @!attribute page [Integer] 1-based
|
|
7
7
|
# @!attribute barcodes [Array<Barcode>] ordered top-to-bottom, left-to-right
|
|
8
8
|
# @!attribute dpi [Integer, nil] base render DPI (PDF) or nil
|
|
9
|
-
# @!attribute width [Integer, nil] base image width in pixels
|
|
10
|
-
# @!attribute height [Integer, nil] base image height in pixels
|
|
9
|
+
# @!attribute width [Integer, nil] base image width in pixels (the original's for a raster decoded downscaled)
|
|
10
|
+
# @!attribute height [Integer, nil] base image height in pixels (the original's for a raster decoded downscaled)
|
|
11
11
|
# @!attribute passes_run [Array<Symbol>]
|
|
12
12
|
# @!attribute skipped_passes [Hash{Symbol => String}] passes that could not run, with the reason
|
|
13
13
|
# @!attribute duration [Float] seconds spent on the page
|
|
@@ -20,8 +20,8 @@ module ZXingFFI
|
|
|
20
20
|
class Scanner
|
|
21
21
|
# Options that only the scanner understands.
|
|
22
22
|
SCAN_OPTIONS = %i[
|
|
23
|
-
effort passes stop dpi max_dpi max_pixels max_pages pages password threads
|
|
24
|
-
min_length timeout instrument
|
|
23
|
+
effort passes stop dpi max_dpi max_pixels max_source_pixels oversize max_pages pages password threads
|
|
24
|
+
on_page_error loader min_length timeout instrument
|
|
25
25
|
].freeze
|
|
26
26
|
|
|
27
27
|
# Every option +scan+/+scan_pages+ accept: the scanner's plus every {ZXingFFI.read} option.
|
|
@@ -30,6 +30,9 @@ module ZXingFFI
|
|
|
30
30
|
# Accepted values of +on_page_error:+.
|
|
31
31
|
ON_PAGE_ERROR = %i[raise skip].freeze
|
|
32
32
|
|
|
33
|
+
# Accepted values of +oversize:+.
|
|
34
|
+
OVERSIZE = %i[raise downscale].freeze
|
|
35
|
+
|
|
33
36
|
# @param options [Hash] see {ZXingFFI.scan}
|
|
34
37
|
# @raise [ArgumentError] for unknown or invalid options (before any input is opened)
|
|
35
38
|
def initialize(**options)
|
|
@@ -51,6 +54,12 @@ module ZXingFFI
|
|
|
51
54
|
raise ArgumentError, "instrument must respond to #call" if @instrument && !@instrument.respond_to?(:call)
|
|
52
55
|
|
|
53
56
|
@config = effective_config(options)
|
|
57
|
+
raise ArgumentError, "oversize must be :raise or :downscale, got #{@config.oversize.inspect}" unless OVERSIZE.include?(@config.oversize)
|
|
58
|
+
source_pixels = @config.max_source_pixels
|
|
59
|
+
if source_pixels && !(source_pixels.is_a?(Integer) && source_pixels.positive?)
|
|
60
|
+
raise ArgumentError, "max_source_pixels must be a positive Integer or nil, got #{source_pixels.inspect}"
|
|
61
|
+
end
|
|
62
|
+
|
|
54
63
|
@reader_options = Reader::Options.new(options.slice(*Reader::OPTION_KEYS))
|
|
55
64
|
@min_length = validate_min_length(options.fetch(:min_length, {}))
|
|
56
65
|
@notify_mutex = Mutex.new
|
|
@@ -102,7 +111,10 @@ module ZXingFFI
|
|
|
102
111
|
private
|
|
103
112
|
|
|
104
113
|
def scan_source(source, &block)
|
|
105
|
-
|
|
114
|
+
unless source.kind == :pdf
|
|
115
|
+
pixels, limit = raster_limit(source.kind)
|
|
116
|
+
HeaderProbe.check!(source.path, source.kind, pixels, limit: limit)
|
|
117
|
+
end
|
|
106
118
|
loader_class = Loaders::Registry.loader_for(source.kind, config: @config, loader: @loader)
|
|
107
119
|
document = loader_class.new(@config).open(source, password: @password)
|
|
108
120
|
begin
|
|
@@ -176,10 +188,11 @@ module ZXingFFI
|
|
|
176
188
|
rerender = pdf ? ->(dpi) { document.render(number, dpi: dpi, timeout: deadline && [deadline - monotonic, 0.05].max) } : nil
|
|
177
189
|
result = strategy.run(page, rerender: rerender, page_info: info, deadline: deadline,
|
|
178
190
|
notify: ->(event, payload) { notify(event, page: number, **payload) })
|
|
179
|
-
barcodes = Scanner.order(result.barcodes.map { |barcode| finalize(barcode, number, page
|
|
191
|
+
barcodes = Scanner.order(result.barcodes.map { |barcode| finalize(barcode, number, page) })
|
|
180
192
|
metadata = page.metadata.merge(choice ? {dpi_source: choice.source, dpi_capped: choice.capped} : {})
|
|
193
|
+
width, height = page.metadata.dig(:downscaled, :from) || [page.image.width, page.image.height]
|
|
181
194
|
page_result = PageResult.new(
|
|
182
|
-
page: number, barcodes: barcodes, dpi: page.dpi, width:
|
|
195
|
+
page: number, barcodes: barcodes, dpi: page.dpi, width: width, height: height,
|
|
183
196
|
passes_run: result.passes_run, skipped_passes: result.skipped_passes, duration: monotonic - started,
|
|
184
197
|
error: nil, metadata: metadata
|
|
185
198
|
)
|
|
@@ -206,10 +219,26 @@ module ZXingFFI
|
|
|
206
219
|
default_dpi: @config.default_dpi, max_dpi: @config.max_dpi, max_pixels: @config.max_pixels)
|
|
207
220
|
end
|
|
208
221
|
|
|
209
|
-
# Adds page metadata and PDF page coordinates (points from the top-left of the displayed page)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
222
|
+
# Adds page metadata and PDF page coordinates (points from the top-left of the displayed page); positions in a
|
|
223
|
+
# raster decoded downscaled are scaled back to the original's pixels.
|
|
224
|
+
def finalize(barcode, number, page)
|
|
225
|
+
dpi = page.dpi
|
|
226
|
+
position = barcode.position.transform(Geometry::Affine.scale(page.scale_to_base)).round
|
|
227
|
+
page_position = dpi && position.map { |p| Geometry::Point.new((p.x * 72.0 / dpi).round(2), (p.y * 72.0 / dpi).round(2)) }
|
|
228
|
+
barcode.with(position: position, page: number, dpi: dpi, page_position: page_position)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# The pixels a raster may declare, with the limit's name: max_source_pixels when +oversize: :downscale+ and its
|
|
232
|
+
# loader can decode it smaller to fit, otherwise max_pixels.
|
|
233
|
+
def raster_limit(kind)
|
|
234
|
+
return [@config.max_pixels, :max_pixels] unless @config.oversize == :downscale
|
|
235
|
+
|
|
236
|
+
downscales = begin
|
|
237
|
+
Loaders::Registry.loader_for(kind, config: @config, loader: @loader).downscales?
|
|
238
|
+
rescue LoaderUnavailable
|
|
239
|
+
false
|
|
240
|
+
end
|
|
241
|
+
downscales ? [@config.max_source_pixels, :max_source_pixels] : [@config.max_pixels, :max_pixels]
|
|
213
242
|
end
|
|
214
243
|
|
|
215
244
|
def select_pages(count)
|
|
@@ -239,7 +268,7 @@ module ZXingFFI
|
|
|
239
268
|
end
|
|
240
269
|
|
|
241
270
|
def effective_config(options)
|
|
242
|
-
overrides = options.slice(:max_dpi, :max_pixels, :max_pages)
|
|
271
|
+
overrides = options.slice(:max_dpi, :max_pixels, :max_source_pixels, :oversize, :max_pages)
|
|
243
272
|
if @timeout
|
|
244
273
|
overrides[:render_timeout] = [ZXingFFI.config.render_timeout, @timeout].compact.min
|
|
245
274
|
end
|
|
@@ -7,6 +7,9 @@ module ZXingFFI
|
|
|
7
7
|
# Geometry contract (shared with {Geometry.rotation_canvas}): {#rotate} turns the image clockwise as
|
|
8
8
|
# displayed (y axis down) about its center and expands the canvas to the rotated bounding box, filling
|
|
9
9
|
# uncovered areas with +background+. {#resize} scales both axes by +scale+ (output side = round(side × scale)).
|
|
10
|
+
# Upscales are bicubic (Catmull-Rom) with pixel centers aligned: output pixel x samples the input at
|
|
11
|
+
# (x + 0.5) / f - 0.5, f being output side / input side. Replicating pixels would keep the aliasing of codes
|
|
12
|
+
# rasterized at 1-2 px per module, which the high_res upscale is there to resolve.
|
|
10
13
|
class Base
|
|
11
14
|
class << self
|
|
12
15
|
# @return [Symbol] registry name
|
|
@@ -5,8 +5,8 @@ module ZXingFFI
|
|
|
5
5
|
# Resize and rotate by piping PGM through ImageMagick.
|
|
6
6
|
#
|
|
7
7
|
# +-rotate+ turns clockwise and expands the canvas (ImageMagick's canvas can be 1 px larger than
|
|
8
|
-
# {Geometry.rotation_canvas}; the strategy re-centers on the actual size).
|
|
9
|
-
# (
|
|
8
|
+
# {Geometry.rotation_canvas}; the strategy re-centers on the actual size). Upscales use +-resize+ with the
|
|
9
|
+
# Catmull-Rom filter (+Catrom+); downscales use +-resize+ with its default filter.
|
|
10
10
|
class ImageMagickTransformer < Base
|
|
11
11
|
class << self
|
|
12
12
|
def transformer_name = :image_magick
|
|
@@ -36,8 +36,8 @@ module ZXingFFI
|
|
|
36
36
|
|
|
37
37
|
width = [(image.width * scale).round, 1].max
|
|
38
38
|
height = [(image.height * scale).round, 1].max
|
|
39
|
-
|
|
40
|
-
pipe(image, [
|
|
39
|
+
filter = (scale > 1) ? %w[-filter Catrom] : []
|
|
40
|
+
pipe(image, [*filter, "-resize", "#{width}x#{height}!"], width, height)
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def rotate(image, degrees, background: 255)
|
|
@@ -6,8 +6,8 @@ module ZXingFFI
|
|
|
6
6
|
#
|
|
7
7
|
# Quarter turns use +rot+ (exact); other angles use libvips' +rotate+, which turns clockwise about the center
|
|
8
8
|
# and expands the canvas like {Geometry.rotation_canvas} (canvas sizes may differ by 1 px because libvips rounds
|
|
9
|
-
# where the geometry rounds up; the strategy re-centers on the actual output size).
|
|
10
|
-
#
|
|
9
|
+
# where the geometry rounds up; the strategy re-centers on the actual output size). Upscales go through
|
|
10
|
+
# +affine+ with the bicubic interpolator; downscales use +resize+ with a linear kernel.
|
|
11
11
|
class VipsTransformer < Base
|
|
12
12
|
QUARTER_TURNS = {90 => :d90, 180 => :d180, 270 => :d270}.freeze
|
|
13
13
|
|
|
@@ -39,13 +39,7 @@ module ZXingFFI
|
|
|
39
39
|
raise ArgumentError, "scale must be positive, got #{scale.inspect}" unless scale.is_a?(Numeric) && scale.positive?
|
|
40
40
|
|
|
41
41
|
vimage = to_vips(image)
|
|
42
|
-
|
|
43
|
-
if scale == scale.to_i && scale >= 1
|
|
44
|
-
vimage.zoom(scale.to_i, scale.to_i)
|
|
45
|
-
else
|
|
46
|
-
vimage.resize(scale, kernel: :linear)
|
|
47
|
-
end
|
|
48
|
-
to_image(out)
|
|
42
|
+
to_image((scale > 1) ? upscale(vimage, scale) : vimage.resize(scale, kernel: :linear))
|
|
49
43
|
end
|
|
50
44
|
|
|
51
45
|
def rotate(image, degrees, background: 255)
|
|
@@ -66,6 +60,17 @@ module ZXingFFI
|
|
|
66
60
|
|
|
67
61
|
private
|
|
68
62
|
|
|
63
|
+
# Not +resize+: its upscales sample a quarter input pixel off center, so at 2x the pixel straddling each
|
|
64
|
+
# black/white edge comes out mid-gray, right at the binarizer's threshold (crisp EAN-13s in the corpus misread).
|
|
65
|
+
def upscale(vimage, scale)
|
|
66
|
+
width = (vimage.width * scale).round
|
|
67
|
+
height = (vimage.height * scale).round
|
|
68
|
+
fx = width.to_f / vimage.width
|
|
69
|
+
fy = height.to_f / vimage.height
|
|
70
|
+
vimage.affine([fx, 0, 0, fy], interpolate: ::Vips::Interpolate.new("bicubic"), oarea: [0, 0, width, height],
|
|
71
|
+
idx: 0.5 - 0.5 / fx, idy: 0.5 - 0.5 / fy, extend: :copy)
|
|
72
|
+
end
|
|
73
|
+
|
|
69
74
|
def to_vips(image)
|
|
70
75
|
raise ArgumentError, "expected a :lum image, got #{image.format.inspect}" unless image.format == :lum
|
|
71
76
|
|
data/lib/zxing_ffi/version.rb
CHANGED
data/vendor/lib/NOTICE.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Bundled libZXing: zxing_ffi 0.
|
|
1
|
+
Bundled libZXing: zxing_ffi 0.2.0 (aarch64-linux-gnu)
|
|
2
2
|
|
|
3
3
|
libZXing.so in this directory is zxing-cpp 3.1.1 (https://github.com/zxing-cpp/zxing-cpp),
|
|
4
4
|
compiled from the release asset zxing-cpp-3.1.1.tar.gz (SHA-256 c3c02c29c0b519de7bd4e25b376e606e87f0761befd1282815642a2246613d14)
|