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.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +27 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +275 -0
  5. data/exe/zxing-scan +6 -0
  6. data/lib/zxing_ffi/barcode.rb +77 -0
  7. data/lib/zxing_ffi/cli.rb +152 -0
  8. data/lib/zxing_ffi/config.rb +119 -0
  9. data/lib/zxing_ffi/dedupe.rb +79 -0
  10. data/lib/zxing_ffi/diagnostics.rb +69 -0
  11. data/lib/zxing_ffi/dpi.rb +103 -0
  12. data/lib/zxing_ffi/errors.rb +69 -0
  13. data/lib/zxing_ffi/formats.rb +207 -0
  14. data/lib/zxing_ffi/geometry.rb +508 -0
  15. data/lib/zxing_ffi/header_probe.rb +98 -0
  16. data/lib/zxing_ffi/image.rb +123 -0
  17. data/lib/zxing_ffi/image_magick.rb +95 -0
  18. data/lib/zxing_ffi/library_defaults.rb +7 -0
  19. data/lib/zxing_ffi/library_loader.rb +176 -0
  20. data/lib/zxing_ffi/loaders/base.rb +155 -0
  21. data/lib/zxing_ffi/loaders/image_magick.rb +159 -0
  22. data/lib/zxing_ffi/loaders/pnm.rb +113 -0
  23. data/lib/zxing_ffi/loaders/poppler.rb +260 -0
  24. data/lib/zxing_ffi/loaders/registry.rb +54 -0
  25. data/lib/zxing_ffi/loaders/vips.rb +332 -0
  26. data/lib/zxing_ffi/loaders.rb +41 -0
  27. data/lib/zxing_ffi/native.rb +237 -0
  28. data/lib/zxing_ffi/pnm.rb +676 -0
  29. data/lib/zxing_ffi/reader.rb +271 -0
  30. data/lib/zxing_ffi/scanner.rb +295 -0
  31. data/lib/zxing_ffi/sniffer.rb +155 -0
  32. data/lib/zxing_ffi/source.rb +77 -0
  33. data/lib/zxing_ffi/strategy.rb +293 -0
  34. data/lib/zxing_ffi/subprocess.rb +416 -0
  35. data/lib/zxing_ffi/transformers/base.rb +69 -0
  36. data/lib/zxing_ffi/transformers/image_magick.rb +72 -0
  37. data/lib/zxing_ffi/transformers/vips.rb +85 -0
  38. data/lib/zxing_ffi/transformers.rb +36 -0
  39. data/lib/zxing_ffi/version.rb +5 -0
  40. data/lib/zxing_ffi.rb +82 -0
  41. metadata +108 -0
@@ -0,0 +1,508 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZXingFFI
4
+ # Plane geometry for barcode positions: {Point}, {Quad}, {Affine}, the canvas transform of rotated
5
+ # passes ({.rotation_canvas}) and the tile layout of the tiles pass ({.tile_grid}).
6
+ #
7
+ # Coordinates are image pixels: origin at the top-left corner, x to the right, y down. An image of width w
8
+ # and height h covers the continuous region [0, w] × [0, h]. Angles are in degrees; positive angles turn
9
+ # clockwise as displayed.
10
+ module Geometry
11
+ # [cos, sin] of 0°, 90°, 180° and 270°, exact so that quarter turns map Integer points to Integer points.
12
+ QUARTER_TURNS = [[1, 0], [0, 1], [-1, 0], [0, -1]].freeze
13
+ # Floating-point noise ignored before a rotated canvas extent is rounded up to whole pixels.
14
+ CANVAS_TOLERANCE = 1e-6
15
+ # Floating-point noise ignored when converting the overlap fraction to whole pixels.
16
+ OVERLAP_TOLERANCE = 1e-9
17
+ private_constant :QUARTER_TURNS, :CANVAS_TOLERANCE, :OVERLAP_TOLERANCE
18
+
19
+ # A point in image coordinates. Coordinates keep their numeric type (usually Integer or Float).
20
+ #
21
+ # @!attribute [r] x
22
+ # @return [Numeric] horizontal coordinate, growing to the right
23
+ # @!attribute [r] y
24
+ # @return [Numeric] vertical coordinate, growing downwards
25
+ Point = Data.define(:x, :y) do
26
+ # Converts +value+ to a Point.
27
+ #
28
+ # @param value [Point, Array(Numeric, Numeric)] a Point (returned as is) or an +[x, y]+ pair
29
+ # @return [Point]
30
+ # @raise [ArgumentError] if +value+ is neither
31
+ def self.from(value)
32
+ case value
33
+ when Point then value
34
+ when Array
35
+ raise ArgumentError, "expected an [x, y] pair, got #{value.inspect}" unless value.size == 2
36
+
37
+ new(*value)
38
+ else
39
+ raise ArgumentError, "expected a Point or an [x, y] pair, got #{value.inspect}"
40
+ end
41
+ end
42
+
43
+ # @raise [ArgumentError] if a coordinate is not a finite real number
44
+ def initialize(x:, y:)
45
+ super(x: Geometry.number!(x, :x), y: Geometry.number!(y, :y))
46
+ end
47
+
48
+ # @param other [Point, Array(Numeric, Numeric)]
49
+ # @return [Point] the component-wise sum
50
+ def +(other)
51
+ other = Point.from(other)
52
+ with(x: x + other.x, y: y + other.y)
53
+ end
54
+
55
+ # @param other [Point, Array(Numeric, Numeric)]
56
+ # @return [Point] the component-wise difference
57
+ def -(other)
58
+ other = Point.from(other)
59
+ with(x: x - other.x, y: y - other.y)
60
+ end
61
+
62
+ # @param other [Point, Array(Numeric, Numeric)]
63
+ # @return [Float] the Euclidean distance to +other+
64
+ def distance_to(other)
65
+ other = Point.from(other)
66
+ Math.hypot(x - other.x, y - other.y)
67
+ end
68
+
69
+ # @return [Point] the point with both coordinates rounded to Integers (halves away from zero)
70
+ def round
71
+ with(x: x.round, y: y.round)
72
+ end
73
+
74
+ # @return [Array(Numeric, Numeric)] +[x, y]+
75
+ def to_a
76
+ [x, y]
77
+ end
78
+ end
79
+
80
+ # The four corners of a barcode, named in the symbol's own orientation: +top_left+ is the corner that is
81
+ # top-left when the symbol is upright, wherever it lies in the image. Corners may be given as {Point}s or
82
+ # +[x, y]+ pairs.
83
+ #
84
+ # @!attribute [r] top_left
85
+ # @return [Point]
86
+ # @!attribute [r] top_right
87
+ # @return [Point]
88
+ # @!attribute [r] bottom_right
89
+ # @return [Point]
90
+ # @!attribute [r] bottom_left
91
+ # @return [Point]
92
+ Quad = Data.define(:top_left, :top_right, :bottom_right, :bottom_left) do
93
+ # @param points [Array<Point, Array(Numeric, Numeric)>] exactly four corners, in the order top-left,
94
+ # top-right, bottom-right, bottom-left
95
+ # @return [Quad]
96
+ # @raise [ArgumentError] unless there are exactly four valid points
97
+ def self.from_points(points)
98
+ points = Array(points)
99
+ raise ArgumentError, "expected 4 points, got #{points.size}" unless points.size == 4
100
+
101
+ new(*points)
102
+ end
103
+
104
+ # @raise [ArgumentError] if a corner is not a Point or an +[x, y]+ pair
105
+ def initialize(top_left:, top_right:, bottom_right:, bottom_left:)
106
+ super(
107
+ top_left: Point.from(top_left),
108
+ top_right: Point.from(top_right),
109
+ bottom_right: Point.from(bottom_right),
110
+ bottom_left: Point.from(bottom_left)
111
+ )
112
+ end
113
+
114
+ # @return [Array<Point>] +[top_left, top_right, bottom_right, bottom_left]+
115
+ def points
116
+ [top_left, top_right, bottom_right, bottom_left]
117
+ end
118
+ alias_method :to_a, :points
119
+
120
+ # @return [Point] the mean of the four corners (Float coordinates)
121
+ def center
122
+ corners = points
123
+ Point.new(corners.sum(&:x).fdiv(4), corners.sum(&:y).fdiv(4))
124
+ end
125
+
126
+ # @return [Array<Float>] lengths of the top, right, bottom and left sides
127
+ def side_lengths
128
+ corners = points
129
+ corners.zip(corners.rotate).map { |from, to| from.distance_to(to) }
130
+ end
131
+
132
+ # @return [Float] length of the shortest side
133
+ def shorter_side
134
+ side_lengths.min
135
+ end
136
+
137
+ # @return [Float] length of the longest side
138
+ def longer_side
139
+ side_lengths.max
140
+ end
141
+
142
+ # Area enclosed by the corners (shoelace formula), whatever their winding order. Only meaningful for a
143
+ # simple (not self-intersecting) quad.
144
+ # @return [Float]
145
+ def area
146
+ corners = points
147
+ corners.zip(corners.rotate).sum { |from, to| from.x * to.y - to.x * from.y }.abs.fdiv(2)
148
+ end
149
+
150
+ # @return [Array(Numeric, Numeric, Numeric, Numeric)] +[min_x, min_y, max_x, max_y]+
151
+ def bounding_box
152
+ xs = points.map(&:x)
153
+ ys = points.map(&:y)
154
+ [xs.min, ys.min, xs.max, ys.max]
155
+ end
156
+
157
+ # @param affine [Affine]
158
+ # @return [Quad] the quad with every corner transformed (see {Affine#apply_quad})
159
+ def transform(affine)
160
+ raise ArgumentError, "expected an Affine, got #{affine.inspect}" unless affine.is_a?(Affine)
161
+
162
+ affine.apply_quad(self)
163
+ end
164
+
165
+ # @return [Quad] the quad with every corner rounded to Integer coordinates
166
+ def round
167
+ map(&:round)
168
+ end
169
+
170
+ # Builds a new Quad from the block's result for each corner, keeping the corner roles.
171
+ #
172
+ # @yieldparam point [Point]
173
+ # @yieldreturn [Point, Array(Numeric, Numeric)]
174
+ # @return [Quad, Enumerator] an Enumerator without a block
175
+ def map(&block)
176
+ return enum_for(:map) unless block
177
+
178
+ Quad.new(*points.map(&block))
179
+ end
180
+
181
+ # @return [Hash{Symbol => Hash{Symbol => Numeric}}] plain nested Hashes (JSON-friendly),
182
+ # e.g. +{top_left: {x: 0, y: 0}, top_right: {x: 10, y: 0}, ...}+
183
+ def to_h(&block)
184
+ hash = members.to_h { |corner| [corner, public_send(corner).to_h] }
185
+ block ? hash.to_h(&block) : hash
186
+ end
187
+ end
188
+
189
+ # An immutable 2-D affine transform: the top two rows of a 3×3 homogeneous matrix.
190
+ #
191
+ # | a b c | x' = a·x + b·y + c
192
+ # | d e f | y' = d·x + e·y + f
193
+ #
194
+ # Coefficients keep their numeric type, so transforms built from Integers (translations, quarter turns)
195
+ # map Integer points to Integer points exactly. Value semantics as for Arrays: +==+ compares coefficients
196
+ # numerically, +eql?+ and +hash+ also compare their classes.
197
+ #
198
+ # Pass transforms are chained with {#compose} (mathematical order) or {#then} (pipeline order):
199
+ #
200
+ # @example Tile at (tx, ty) of a 2× upscaled image, back to base-image pixels
201
+ # to_base = Affine.scale(0.5).compose(Affine.translation(tx, ty))
202
+ # to_base = Affine.translation(tx, ty).then(Affine.scale(0.5)) # the same transform
203
+ #
204
+ # @!attribute [r] a
205
+ # @return [Numeric] x' coefficient of x
206
+ # @!attribute [r] b
207
+ # @return [Numeric] x' coefficient of y
208
+ # @!attribute [r] c
209
+ # @return [Numeric] x' translation
210
+ # @!attribute [r] d
211
+ # @return [Numeric] y' coefficient of x
212
+ # @!attribute [r] e
213
+ # @return [Numeric] y' coefficient of y
214
+ # @!attribute [r] f
215
+ # @return [Numeric] y' translation
216
+ Affine = Data.define(:a, :b, :c, :d, :e, :f) do
217
+ class << self
218
+ # @return [Affine] the transform that leaves every point in place
219
+ def identity
220
+ new(1, 0, 0, 0, 1, 0)
221
+ end
222
+
223
+ # @param dx [Numeric]
224
+ # @param dy [Numeric]
225
+ # @return [Affine] (x, y) → (x + dx, y + dy)
226
+ def translation(dx, dy)
227
+ new(1, 0, dx, 0, 1, dy)
228
+ end
229
+
230
+ # @param sx [Numeric]
231
+ # @param sy [Numeric] defaults to +sx+ (uniform scale)
232
+ # @return [Affine] (x, y) → (sx·x, sy·y)
233
+ def scale(sx, sy = sx)
234
+ new(sx, 0, 0, 0, sy, 0)
235
+ end
236
+
237
+ # Rotation by +degrees+ about (+cx+, +cy+), clockwise as displayed: the x axis (1, 0) turns towards
238
+ # the y axis (0, 1), which points down. Multiples of 90° have exact Integer cos/sin coefficients.
239
+ #
240
+ # @param degrees [Numeric]
241
+ # @param cx [Numeric]
242
+ # @param cy [Numeric]
243
+ # @return [Affine]
244
+ def rotation(degrees, cx = 0, cy = 0)
245
+ cos, sin = Geometry.cos_sin(degrees)
246
+ Geometry.number!(cx, :cx)
247
+ Geometry.number!(cy, :cy)
248
+ new(cos, -sin, cx - cos * cx + sin * cy, sin, cos, cy - sin * cx - cos * cy)
249
+ end
250
+ end
251
+
252
+ # @raise [ArgumentError] if a coefficient is not a finite real number
253
+ def initialize(a:, b:, c:, d:, e:, f:)
254
+ super(
255
+ a: Geometry.number!(a, :a), b: Geometry.number!(b, :b), c: Geometry.number!(c, :c),
256
+ d: Geometry.number!(d, :d), e: Geometry.number!(e, :e), f: Geometry.number!(f, :f)
257
+ )
258
+ end
259
+
260
+ # Mathematical composition self ∘ other: the transform that applies +other+ first, then +self+
261
+ # (the matrix product self × other), so +compose(other).apply(p) == apply(other.apply(p))+.
262
+ #
263
+ # @param other [Affine]
264
+ # @return [Affine]
265
+ def compose(other)
266
+ raise ArgumentError, "expected an Affine, got #{other.inspect}" unless other.is_a?(Affine)
267
+
268
+ Affine.new(
269
+ a * other.a + b * other.d, a * other.b + b * other.e, a * other.c + b * other.f + c,
270
+ d * other.a + e * other.d, d * other.b + e * other.e, d * other.c + e * other.f + f
271
+ )
272
+ end
273
+
274
+ # Pipeline order: the transform that applies +self+ first, then +other+, i.e. +other.compose(self)+,
275
+ # so +self.then(other).apply(p) == other.apply(apply(p))+. With a block and no argument this is
276
+ # +Kernel#then+.
277
+ #
278
+ # @param other [Affine]
279
+ # @return [Affine]
280
+ def then(other = nil, &block)
281
+ return super(&block) if other.nil? && block
282
+ raise ArgumentError, "expected an Affine, got #{other.inspect}" unless other.is_a?(Affine)
283
+
284
+ other.compose(self)
285
+ end
286
+
287
+ # @param point [Point, Array(Numeric, Numeric)]
288
+ # @return [Point] the transformed point
289
+ def apply(point)
290
+ point = Point.from(point)
291
+ Point.new(a * point.x + b * point.y + c, d * point.x + e * point.y + f)
292
+ end
293
+
294
+ # @param quad [Quad]
295
+ # @return [Quad] the quad with every corner transformed; each corner keeps its role (+top_left+ stays
296
+ # +top_left+), so the result is still named in the symbol's own orientation
297
+ def apply_quad(quad)
298
+ raise ArgumentError, "expected a Quad, got #{quad.inspect}" unless quad.is_a?(Quad)
299
+
300
+ quad.map { |point| apply(point) }
301
+ end
302
+
303
+ # @return [Numeric] determinant of the linear part (a·e − b·d): the area scale factor, negative when
304
+ # the transform mirrors
305
+ def determinant
306
+ a * e - b * d
307
+ end
308
+
309
+ # Coefficients stay Integers where the division is exact (e.g. translations and quarter turns).
310
+ #
311
+ # @return [Affine] the inverse transform
312
+ # @raise [ArgumentError] if the transform is singular or its inverse is not finite
313
+ def invert
314
+ det = determinant
315
+ raise ArgumentError, "cannot invert a singular transform (determinant 0): #{inspect}" if det.zero?
316
+
317
+ inverse = [e, -b, b * f - c * e, -d, a, c * d - a * f].map { |value| Geometry.quotient(value, det) }
318
+ raise ArgumentError, "cannot invert a nearly singular transform: #{inspect}" unless inverse.all?(&:finite?)
319
+
320
+ Affine.new(*inverse)
321
+ end
322
+
323
+ # Rotation component in degrees, clockwise as displayed, normalized to 0...360. The scanner adds it to a
324
+ # barcode's reported rotation when mapping results back to the base image.
325
+ #
326
+ # This is the rotation of the polar decomposition, atan2(d − b, a + e): exact for any composition of
327
+ # rotations, translations and uniform scales, otherwise the rotation of the closest similarity. The
328
+ # result is rounded to 1e-9° so that quarter turns come out exact.
329
+ #
330
+ # @return [Float]
331
+ # @raise [ArgumentError] if the transform mirrors or collapses the plane (determinant ≤ 0)
332
+ def rotation_degrees
333
+ unless determinant.positive?
334
+ raise ArgumentError, "rotation is undefined for a mirroring or singular transform: #{inspect}"
335
+ end
336
+
337
+ degrees = (Math.atan2(d - b, a + e) * 180 / Math::PI).round(9) % 360
338
+ degrees + 0.0 # -0.0 → 0.0
339
+ end
340
+
341
+ # @param other [Affine]
342
+ # @param eps [Numeric] absolute tolerance per coefficient
343
+ # @return [Boolean] whether +other+ is an Affine whose coefficients all differ by at most +eps+
344
+ def approx_equal?(other, eps = 1e-9)
345
+ return false unless other.is_a?(Affine)
346
+
347
+ to_a.flatten.zip(other.to_a.flatten).all? { |mine, theirs| (mine - theirs).abs <= eps }
348
+ end
349
+
350
+ # @return [Array(Array(Numeric, Numeric, Numeric), Array(Numeric, Numeric, Numeric))] the rows
351
+ # +[[a, b, c], [d, e, f]]+
352
+ def to_a
353
+ [[a, b, c], [d, e, f]]
354
+ end
355
+ end
356
+
357
+ class << self
358
+ # Transform from original image coordinates into the canvas of the image rotated about its center by
359
+ # +degrees+ (clockwise), with the canvas expanded to fit the whole rotated image and starting at (0, 0):
360
+ # the geometry of +Transformers::Base#rotate+. Map results found on the rotated canvas back to the
361
+ # original image with +to_canvas.invert+.
362
+ #
363
+ # Quarter turns are exact: 90° gives an h×w canvas and (x, y) → (h − y, x). Other angles give the
364
+ # rotated bounding box rounded up to whole pixels, with the image centered on the canvas.
365
+ #
366
+ # @param width [Integer] original image width
367
+ # @param height [Integer] original image height
368
+ # @param degrees [Numeric] clockwise rotation
369
+ # @return [Array(Affine, Integer, Integer)] +[to_canvas, canvas_width, canvas_height]+
370
+ # @raise [ArgumentError] on a non-positive or non-Integer size, or a non-finite angle
371
+ def rotation_canvas(width, height, degrees)
372
+ dimension!(width, :width)
373
+ dimension!(height, :height)
374
+ cos, sin = cos_sin(degrees)
375
+ canvas_width, canvas_height = rotation_canvas_size(width, height, cos, sin)
376
+ offset_x, offset_y = rotation_canvas_offset(width, height, canvas_width, canvas_height, cos, sin)
377
+ [Affine.new(cos, -sin, offset_x, sin, cos, offset_y), canvas_width, canvas_height]
378
+ end
379
+
380
+ # Overlapping tiles covering a width × height image, for the tiles pass.
381
+ #
382
+ # Each axis is split independently. A side of at most +max_tile+ pixels is a single tile spanning it.
383
+ # A longer side gets the fewest tiles of one size within +min_tile+..+max_tile+ such that adjacent
384
+ # tiles overlap by at least +overlap+ × the tile size (rounded up to whole pixels), using the smallest
385
+ # size that achieves that count. The first tile starts at 0, the last one ends at the far edge, and the
386
+ # ones in between are spaced evenly (rounded to whole pixels). The result is deterministic.
387
+ #
388
+ # @param width [Integer]
389
+ # @param height [Integer]
390
+ # @param min_tile [Integer] smallest tile side (unless the image side itself is smaller)
391
+ # @param max_tile [Integer] largest tile side
392
+ # @param overlap [Numeric] minimum overlap of adjacent tiles as a fraction of the tile side, 0...1
393
+ # @return [Array<Array(Integer, Integer, Integer, Integer)>] +[x, y, width, height]+ per tile, row-major
394
+ # (top row first, each row left to right)
395
+ # @raise [ArgumentError] on invalid sizes or parameters
396
+ def tile_grid(width, height, min_tile: 1024, max_tile: 1536, overlap: 0.2)
397
+ dimension!(width, :width)
398
+ dimension!(height, :height)
399
+ validate_tiling!(min_tile, max_tile, overlap)
400
+ columns = tile_spans(width, min_tile, max_tile, overlap)
401
+ rows = tile_spans(height, min_tile, max_tile, overlap)
402
+ rows.flat_map { |y, tile_height| columns.map { |x, tile_width| [x, y, tile_width, tile_height] } }
403
+ end
404
+
405
+ # @api private
406
+ # @return [Numeric] +value+ when it is a finite real number
407
+ # @raise [ArgumentError] otherwise
408
+ def number!(value, name)
409
+ return value if value.is_a?(Numeric) && value.real? && value.finite?
410
+
411
+ raise ArgumentError, "#{name} must be a finite real number, got #{value.inspect}"
412
+ end
413
+
414
+ # @api private
415
+ # @return [Array(Numeric, Numeric)] +[cos, sin]+ of +degrees+; exact Integers for multiples of 90°
416
+ def cos_sin(degrees)
417
+ turn = number!(degrees, :degrees) % 360
418
+ return QUARTER_TURNS.fetch((turn / 90).to_i) if (turn % 90).zero?
419
+
420
+ turn -= 360 if turn > 180 # symmetric range: rotation(-θ) is the exact mirror of rotation(θ)
421
+ radians = turn * Math::PI / 180
422
+ [Math.cos(radians), Math.sin(radians)]
423
+ end
424
+
425
+ # @api private
426
+ # @return [Numeric] numerator / denominator: an Integer when both are Integers and the division is
427
+ # exact, a Float otherwise
428
+ def quotient(numerator, denominator)
429
+ if numerator.is_a?(Integer) && denominator.is_a?(Integer) && (numerator % denominator).zero?
430
+ numerator / denominator
431
+ else
432
+ numerator.fdiv(denominator)
433
+ end
434
+ end
435
+
436
+ private
437
+
438
+ # Canvas size for a width × height image rotated by (cos, sin): the rotated bounding box, rounded up
439
+ # to whole pixels (quarter turns are exact).
440
+ def rotation_canvas_size(width, height, cos, sin)
441
+ [width * cos.abs + height * sin.abs, width * sin.abs + height * cos.abs].map do |extent|
442
+ extent.is_a?(Integer) ? extent : [(extent - CANVAS_TOLERANCE).ceil, 1].max
443
+ end
444
+ end
445
+
446
+ # Translation (c, f) that places the rotated image on its canvas. The single place that decides where
447
+ # the image lands: the image center (w/2, h/2) maps to the canvas center, so the slack from rounding the
448
+ # canvas size up is split evenly between opposite edges. Adjust here if a transformer (libvips +rotate+,
449
+ # ImageMagick +-rotate+) positions the image differently.
450
+ def rotation_canvas_offset(width, height, canvas_width, canvas_height, cos, sin)
451
+ [
452
+ quotient(canvas_width - cos * width + sin * height, 2),
453
+ quotient(canvas_height - sin * width - cos * height, 2)
454
+ ]
455
+ end
456
+
457
+ # [[offset, size], ...] of the tiles along one axis of +length+ pixels.
458
+ def tile_spans(length, min_tile, max_tile, overlap)
459
+ return [[0, length]] if length <= max_tile
460
+
461
+ count = 1 + ceil_div(length - max_tile, max_tile - min_overlap(max_tile, overlap))
462
+ # tiles_fit? is monotonic in the size and holds for max_tile, so this finds the smallest fitting size.
463
+ size = (min_tile..max_tile).bsearch { |candidate| tiles_fit?(length, count, candidate, overlap) }
464
+ travel = length - size
465
+ # i × travel / (count − 1), rounded half up in Integer arithmetic
466
+ Array.new(count) { |i| [(2 * i * travel + count - 1) / (2 * (count - 1)), size] }
467
+ end
468
+
469
+ # Whether +count+ evenly spaced tiles of +size+ (count ≥ 2) span +length+ with the minimum overlap.
470
+ # Rounded offsets advance by at most ceil(travel / (count - 1)) pixels.
471
+ def tiles_fit?(length, count, size, overlap)
472
+ size - ceil_div(length - size, count - 1) >= min_overlap(size, overlap)
473
+ end
474
+
475
+ # Minimum overlap in whole pixels between adjacent tiles of +size+.
476
+ def min_overlap(size, overlap)
477
+ (overlap * size - OVERLAP_TOLERANCE).ceil
478
+ end
479
+
480
+ def ceil_div(numerator, denominator)
481
+ (numerator + denominator - 1) / denominator
482
+ end
483
+
484
+ def dimension!(value, name)
485
+ return if value.is_a?(Integer) && value.positive?
486
+
487
+ raise ArgumentError, "#{name} must be a positive Integer, got #{value.inspect}"
488
+ end
489
+
490
+ def validate_tiling!(min_tile, max_tile, overlap)
491
+ dimension!(min_tile, :min_tile)
492
+ dimension!(max_tile, :max_tile)
493
+ raise ArgumentError, "min_tile (#{min_tile}) must not exceed max_tile (#{max_tile})" if min_tile > max_tile
494
+ unless overlap.is_a?(Numeric) && overlap.real? && overlap.finite? && overlap >= 0 && overlap < 1
495
+ raise ArgumentError, "overlap must be a number in 0...1, got #{overlap.inspect}"
496
+ end
497
+ return if min_overlap(max_tile, overlap) < max_tile
498
+
499
+ raise ArgumentError, "overlap #{overlap} leaves no room to advance between tiles of #{max_tile} px"
500
+ end
501
+ end
502
+ end
503
+
504
+ # Shorthand for {Geometry::Point}.
505
+ Point = Geometry::Point
506
+ # Shorthand for {Geometry::Quad}.
507
+ Quad = Geometry::Quad
508
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ZXingFFI
4
+ # Reads raster dimensions straight from file headers, without decoding anything, for the formats
5
+ # whose headers are simple: PNG, GIF, BMP, PNM and JPEG. The scanner checks them against +max_pixels+ before any
6
+ # loader runs, so a small file declaring a huge image is rejected the same way whichever loader is installed
7
+ # (ImageMagick, for one, refuses to report the size of a truncated BMP/PGM, and Ubuntu's ImageMagick policy rejects
8
+ # large JPEG headers before we see them). Other formats return nil and are checked by the loaders themselves.
9
+ module HeaderProbe
10
+ # Bytes read from the start of the file.
11
+ HEAD_SIZE = 4096
12
+
13
+ # JPEG start-of-frame markers: every SOFn except DHT (C4), JPG (C8) and DAC (CC).
14
+ JPEG_SOF = [0xC0, 0xC1, 0xC2, 0xC3, 0xC5, 0xC6, 0xC7, 0xC9, 0xCA, 0xCB, 0xCD, 0xCE, 0xCF].freeze
15
+
16
+ # Marker segments walked before giving up on finding a JPEG frame header.
17
+ JPEG_MAX_SEGMENTS = 256
18
+
19
+ class << self
20
+ # @param path [String]
21
+ # @param kind [Symbol] sniffed kind
22
+ # @return [Array(Integer, Integer), nil] width and height, or nil when unknown or unreadable
23
+ def dimensions(path, kind)
24
+ return jpeg(path) if kind == :jpeg
25
+
26
+ head = File.open(path, "rb") { |file| file.read(HEAD_SIZE) } || "".b
27
+ case kind
28
+ when :png then png(head)
29
+ when :gif then valid(head.unpack("@6v2")) if head.bytesize >= 10
30
+ when :bmp then bmp(head)
31
+ when :pnm then ZXingFFI::Pnm.read_header(head).then { |header| [header.width, header.height] }
32
+ end
33
+ rescue UnsupportedInput, ArgumentError, TypeError, SystemCallError, EOFError
34
+ nil
35
+ end
36
+
37
+ # Raises when the header declares more than +max_pixels+ pixels.
38
+ # @raise [LimitExceeded]
39
+ def check!(path, kind, max_pixels)
40
+ return unless max_pixels
41
+
42
+ width, height = dimensions(path, kind)
43
+ return unless width && height && width * height > max_pixels
44
+
45
+ raise LimitExceeded.new("#{File.basename(path)} declares #{width}x#{height} (#{width * height} pixels), " \
46
+ "exceeding max_pixels #{max_pixels}", limit: :max_pixels, value: width * height)
47
+ end
48
+
49
+ private
50
+
51
+ def png(head)
52
+ return nil unless head.bytesize >= 24 && head.byteslice(12, 4) == "IHDR"
53
+
54
+ valid(head.unpack("@16N2"))
55
+ end
56
+
57
+ # BITMAPCOREHEADER (12 bytes) has 16-bit sizes; later headers 32-bit, with a negative height for top-down.
58
+ def bmp(head)
59
+ return nil unless head.bytesize >= 26
60
+
61
+ if head.unpack1("@14V") == 12
62
+ valid(head.unpack("@18v2"))
63
+ else
64
+ width, height = head.unpack("@18l<2")
65
+ valid([width, height.abs])
66
+ end
67
+ end
68
+
69
+ # Walks the marker segments to the frame header, seeking past their data: EXIF, ICC and XMP segments (up to
70
+ # 64 KiB each) can put it far beyond HEAD_SIZE. Stops at the first scan (SOS): a frame header must precede it.
71
+ def jpeg(path)
72
+ File.open(path, "rb") do |file|
73
+ return nil unless file.read(2) == "\xFF\xD8".b
74
+
75
+ JPEG_MAX_SEGMENTS.times do
76
+ return nil unless file.readbyte == 0xFF
77
+
78
+ marker = file.readbyte
79
+ marker = file.readbyte while marker == 0xFF # fill bytes
80
+ next if marker == 0x01 || marker.between?(0xD0, 0xD8) # TEM, RSTn, SOI: no length
81
+ return nil if marker.zero? || marker == 0xD9 || marker == 0xDA # not a marker, EOI, or SOS first
82
+
83
+ length = file.read(2)&.unpack1("n")
84
+ return nil unless length && length >= 2
85
+ return valid(file.read(5).to_s.unpack("@1n2").reverse) if JPEG_SOF.include?(marker) # height, then width
86
+
87
+ file.seek(length - 2, IO::SEEK_CUR)
88
+ end
89
+ nil
90
+ end
91
+ end
92
+
93
+ def valid(dimensions)
94
+ (dimensions.all? { |value| value.is_a?(Integer) && value.positive? }) ? dimensions : nil
95
+ end
96
+ end
97
+ end
98
+ end