qr_reader 0.0.1 → 0.0.2

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.
@@ -0,0 +1,799 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QrReader
4
+ module Formats
5
+ module PDF
6
+ # Filled and stroked paths of a page, collected while its content
7
+ # stream is scanned, so that QR codes drawn as vector graphics (one
8
+ # rectangle per module or run of modules, as most PDF generators do)
9
+ # can be rasterized; also the glyphs of text set in a barcode font
10
+ # (see ContentScanner#text_operator). Shadings and clipping are
11
+ # ignored; colours are reduced to luminance.
12
+ class PathCollector
13
+ # Most subpaths (and points) kept per page; the rest is ignored.
14
+ MAX_SUBPATHS = 50_000
15
+ MAX_POINTS = 400_000
16
+ # Line segments a Bézier curve is flattened into.
17
+ CURVE_STEPS = 4
18
+ OPERATORS = %w[m l c v y h re f F f* B B* b b* S s n w J g G rg RG k K cs CS sc scn SC SCN]
19
+ .to_h { |op| [op, true] }.freeze
20
+
21
+ # A painted path in page space (points, y up): +polys+ are flat
22
+ # [x0, y0, x1, y1, ...] Arrays, +luma+ the paint's luminance.
23
+ # +union+: each polygon is painted on its own (stroke outlines).
24
+ Shape = Struct.new(:polys, :luma, :even_odd, :union, :bbox)
25
+ # Paint and text state saved by q / Q.
26
+ State = Struct.new(:fill_space, :fill_luma, :stroke_space, :stroke_luma, :line_width, :line_cap,
27
+ :font, :font_size, :char_spacing, :word_spacing, :scaling, :leading, :rise, :render)
28
+
29
+ attr_reader :shapes, :state
30
+
31
+ def initialize(doc, resources)
32
+ @doc = doc
33
+ @resources = resources
34
+ @shapes = []
35
+ @state = State.new(ColorSpace::GRAY, 0, ColorSpace::GRAY, 0, 1.0, 0,
36
+ nil, 0.0, 0.0, 0.0, 100.0, 0.0, 0.0, 0)
37
+ @stack = []
38
+ @polys = []
39
+ @current = nil
40
+ @closed = []
41
+ @subpaths = 0
42
+ @points = 0
43
+ end
44
+
45
+ def self.operator?(token)
46
+ OPERATORS[token]
47
+ end
48
+
49
+ def save
50
+ @stack << @state.dup if @stack.size < ContentScanner::MAX_STACK
51
+ end
52
+
53
+ def restore
54
+ @state = @stack.pop || @state
55
+ end
56
+
57
+ # Runs a path or colour operator with its +operands+ (Strings)
58
+ # under the transformation +ctm+.
59
+ def operator(token, operands, ctm, resources)
60
+ return if @broken
61
+
62
+ case token
63
+ when "m" then move(operands, ctm)
64
+ when "l" then line(operands, ctm)
65
+ when "c" then curve(operands, ctm, 6)
66
+ when "v", "y" then curve(operands, ctm, 4, token)
67
+ when "h" then @closed[@polys.size - 1] = true if @current
68
+ when "re" then rectangle(operands, ctm)
69
+ when "f", "F" then paint(ctm, fill: true)
70
+ when "f*" then paint(ctm, fill: true, even_odd: true)
71
+ when "B", "b" then paint(ctm, fill: true, stroke: true, close: token == "b")
72
+ when "B*", "b*" then paint(ctm, fill: true, even_odd: true, stroke: true, close: token == "b*")
73
+ when "S", "s" then paint(ctm, stroke: true, close: token == "s")
74
+ when "n" then clear
75
+ when "w" then @state.line_width = number(operands.last) || @state.line_width
76
+ when "J" then @state.line_cap = number(operands.last)&.to_i || @state.line_cap
77
+ when "g", "rg", "k" then set_device(operands, token, fill: true)
78
+ when "G", "RG", "K" then set_device(operands, token.downcase, fill: false)
79
+ when "cs" then set_space(operands, resources, fill: true)
80
+ when "CS" then set_space(operands, resources, fill: false)
81
+ when "sc", "scn" then set_color(operands, fill: true)
82
+ when "SC", "SCN" then set_color(operands, fill: false)
83
+ end
84
+ rescue *INTERNAL_ERRORS
85
+ # Garbage operands: give up on this page's paths, keep its images.
86
+ @broken = true
87
+ clear
88
+ end
89
+
90
+ # Paints glyph outline +polys+ (text space) placed by +matrix+ with
91
+ # the colour of the text rendering mode.
92
+ def add_glyph(polys, matrix)
93
+ return if @broken
94
+
95
+ luma = [1, 5].include?(@state.render) ? @state.stroke_luma : @state.fill_luma
96
+ points = polys.sum(&:size) / 2
97
+ return unless luma && @subpaths + polys.size <= MAX_SUBPATHS && @points + points <= MAX_POINTS
98
+
99
+ @subpaths += polys.size
100
+ @points += points
101
+ a, b, c, d, e, f = matrix
102
+ placed = polys.map do |p|
103
+ q = Array.new(p.size)
104
+ i = 0
105
+ while i < p.size
106
+ x = p[i]
107
+ y = p[i + 1]
108
+ q[i] = a * x + c * y + e
109
+ q[i + 1] = b * x + d * y + f
110
+ i += 2
111
+ end
112
+ q
113
+ end
114
+ add_shape(placed, luma, false, false)
115
+ end
116
+
117
+ private
118
+
119
+ def number(token)
120
+ return nil unless token.is_a?(String) && !token.start_with?("/")
121
+
122
+ value = token.to_f
123
+ value.finite? ? value : nil
124
+ end
125
+
126
+ def numbers(operands, n)
127
+ return nil if operands.size < n
128
+
129
+ values = operands.last(n).map { |t| number(t) }
130
+ values.include?(nil) ? nil : values
131
+ end
132
+
133
+ def room?(points)
134
+ @subpaths < MAX_SUBPATHS && @points + points <= MAX_POINTS
135
+ end
136
+
137
+ def move(operands, ctm)
138
+ values = numbers(operands, 2) or return
139
+ x, y = values
140
+ @user = [x, y]
141
+ return @current = nil unless room?(1)
142
+
143
+ @current = transform(ctm, x, y)
144
+ @polys << @current
145
+ @subpaths += 1
146
+ @points += 1
147
+ end
148
+
149
+ def line(operands, ctm)
150
+ values = numbers(operands, 2) or return
151
+ x, y = values
152
+ add_point(ctm, x, y)
153
+ end
154
+
155
+ def add_point(ctm, x, y)
156
+ @user = [x, y]
157
+ return unless @current && room?(1)
158
+
159
+ @current << ctm[0] * x + ctm[2] * y + ctm[4] << ctm[1] * x + ctm[3] * y + ctm[5]
160
+ @points += 1
161
+ end
162
+
163
+ # c (6 operands), v (current point is the first control point) and
164
+ # y (the end point is the second one). Curves are only painted (a
165
+ # module is never curved), so a coarse flattening does.
166
+ def curve(operands, ctm, n, kind = nil)
167
+ values = numbers(operands, n) or return
168
+ return unless @user
169
+
170
+ x0, y0 = @user
171
+ x1, y1, x2, y2, x3, y3 = case kind
172
+ when "v" then [x0, y0, *values]
173
+ when "y" then [values[0], values[1], values[2], values[3], values[2], values[3]]
174
+ else values
175
+ end
176
+ @user = [x3, y3]
177
+ return unless @current && room?(CURVE_STEPS)
178
+
179
+ current = @current
180
+ a, b, c, d, e, f = ctm
181
+ i = 1
182
+ while i <= CURVE_STEPS
183
+ t = i.to_f / CURVE_STEPS
184
+ u = 1 - t
185
+ k0 = u * u * u
186
+ k1 = 3 * u * u * t
187
+ k2 = 3 * u * t * t
188
+ k3 = t * t * t
189
+ x = k0 * x0 + k1 * x1 + k2 * x2 + k3 * x3
190
+ y = k0 * y0 + k1 * y1 + k2 * y2 + k3 * y3
191
+ current << a * x + c * y + e << b * x + d * y + f
192
+ i += 1
193
+ end
194
+ @points += CURVE_STEPS
195
+ end
196
+
197
+ def rectangle(operands, ctm)
198
+ values = numbers(operands, 4) or return
199
+ x, y, w, h = values
200
+ @user = [x, y]
201
+ return @current = nil unless room?(4)
202
+
203
+ poly = transform(ctm, x, y)
204
+ poly.concat(transform(ctm, x + w, y), transform(ctm, x + w, y + h), transform(ctm, x, y + h))
205
+ @polys << poly
206
+ @closed[@polys.size - 1] = true
207
+ @current = nil
208
+ @subpaths += 1
209
+ @points += 4
210
+ end
211
+
212
+ def transform(ctm, x, y)
213
+ a, b, c, d, e, f = ctm
214
+ [a * x + c * y + e, b * x + d * y + f]
215
+ end
216
+
217
+ def clear
218
+ @polys = []
219
+ @closed = []
220
+ @current = nil
221
+ end
222
+
223
+ def paint(ctm, fill: false, even_odd: false, stroke: false, close: false)
224
+ polys = @polys
225
+ closed = @closed
226
+ clear
227
+ if fill && @state.fill_luma
228
+ areas = polys.select { |p| p.size >= 6 }
229
+ add_shape(areas, @state.fill_luma, even_odd, false) unless areas.empty?
230
+ end
231
+ return unless stroke && @state.stroke_luma
232
+
233
+ a, b, c, d = ctm
234
+ width = @state.line_width * Math.sqrt((a * d - b * c).abs)
235
+ width = 0.25 if width < 0.25 # zero width: the thinnest line the device can draw
236
+ capped = @state.line_cap != 0
237
+ outlines = []
238
+ polys.each_with_index do |poly, i|
239
+ stroke_outline(poly, close || closed[i], width / 2, capped, outlines)
240
+ end
241
+ add_shape(outlines, @state.stroke_luma, false, true) unless outlines.empty?
242
+ end
243
+
244
+ # One quadrilateral per segment of +poly+. Segments are extended by
245
+ # half the line width where they meet (which fills the corners of
246
+ # right-angled joins) and at the ends of open paths unless they have
247
+ # butt caps.
248
+ def stroke_outline(poly, closed, half, capped, out)
249
+ n = poly.size / 2
250
+ if capped && poly.each_slice(2).uniq.size == 1
251
+ # A zero-length path is a dot (a square, for round caps too).
252
+ x, y = poly
253
+ out << [x - half, y - half, x + half, y - half, x + half, y + half, x - half, y + half]
254
+ return
255
+ end
256
+
257
+ segments = closed && n > 2 ? n : n - 1
258
+ s = 0
259
+ while s < segments
260
+ x0 = poly[2 * s]
261
+ y0 = poly[2 * s + 1]
262
+ j = (s + 1) % n
263
+ x1 = poly[2 * j]
264
+ y1 = poly[2 * j + 1]
265
+ length = Math.hypot(x1 - x0, y1 - y0)
266
+ if length.positive?
267
+ ux = (x1 - x0) / length * half
268
+ uy = (y1 - y0) / length * half
269
+ start = closed || capped || s.positive? ? 1 : 0
270
+ stop = closed || capped || s < segments - 1 ? 1 : 0
271
+ ax = x0 - ux * start
272
+ ay = y0 - uy * start
273
+ bx = x1 + ux * stop
274
+ by = y1 + uy * stop
275
+ out << [ax - uy, ay + ux, bx - uy, by + ux, bx + uy, by - ux, ax + uy, ay - ux]
276
+ end
277
+ s += 1
278
+ end
279
+ end
280
+
281
+ def add_shape(polys, luma, even_odd, union)
282
+ x0 = y0 = Float::INFINITY
283
+ x1 = y1 = -Float::INFINITY
284
+ polys.each do |p|
285
+ i = 0
286
+ while i < p.size
287
+ x = p[i]
288
+ y = p[i + 1]
289
+ x0 = x if x < x0
290
+ x1 = x if x > x1
291
+ y0 = y if y < y0
292
+ y1 = y if y > y1
293
+ i += 2
294
+ end
295
+ end
296
+ return unless x0.finite? && x1.finite? && y0.finite? && y1.finite?
297
+
298
+ @shapes << Shape.new(polys, luma, even_odd, union, [x0, y0, x1, y1])
299
+ end
300
+
301
+ def set_device(operands, kind, fill:)
302
+ values = numbers(operands, { "g" => 1, "rg" => 3, "k" => 4 }.fetch(kind)) or return
303
+ space = { "g" => ColorSpace::GRAY, "rg" => ColorSpace::RGB, "k" => ColorSpace::CMYK }.fetch(kind)
304
+ store(fill, space, luma(space, values))
305
+ end
306
+
307
+ def set_space(operands, resources, fill:)
308
+ name = operands.last
309
+ return unless name.is_a?(String) && name.start_with?("/")
310
+
311
+ symbol = Parser.name_symbol(name)
312
+ space = begin
313
+ symbol == :Pattern ? nil : ColorSpace.build(@doc, symbol, resources || @resources)
314
+ rescue Error, *INTERNAL_ERRORS
315
+ nil
316
+ end
317
+ # The initial colour: black (gray, RGB, CMYK, Lab), full ink
318
+ # (Separation, DeviceN) or the first palette entry.
319
+ initial = space && (%i[separation device_n].include?(space.kind) ? [1.0] * space.components : nil)
320
+ store(fill, space, space && luma(space, initial || default_color(space)))
321
+ end
322
+
323
+ def default_color(space)
324
+ space.kind == :cmyk ? [0.0, 0.0, 0.0, 1.0] : [0.0] * space.components
325
+ end
326
+
327
+ def set_color(operands, fill:)
328
+ space = fill ? @state.fill_space : @state.stroke_space
329
+ if space.nil? || operands.last.to_s.start_with?("/") # pattern
330
+ store(fill, space, nil)
331
+ return
332
+ end
333
+
334
+ values = numbers(operands, space.components) or return
335
+ store(fill, space, luma(space, values))
336
+ end
337
+
338
+ def store(fill, space, luma)
339
+ if fill
340
+ @state.fill_space = space
341
+ @state.fill_luma = luma
342
+ else
343
+ @state.stroke_space = space
344
+ @state.stroke_luma = luma
345
+ end
346
+ end
347
+
348
+ # Luminance (0..255) of colour component +values+ (0..1; palette
349
+ # index for Indexed; L* 0..100 for Lab).
350
+ def luma(space, values)
351
+ case space.kind
352
+ when :indexed then space.luma_lut(8, nil)[values[0].round.clamp(0, 255)]
353
+ when :lab then (values[0] * 2.55).round.clamp(0, 255)
354
+ else space.luma_at(values.map { |v| (v * 255).round.clamp(0, 255) }.pack("C*"), 0)
355
+ end
356
+ rescue Error, *INTERNAL_ERRORS
357
+ nil
358
+ end
359
+ end
360
+
361
+ # Finds groups of small shapes that could be a QR code and renders
362
+ # each group to an Image (white background, painter's order).
363
+ module VectorRenderer
364
+ # A subpath is a possible module (or run of modules) when its
365
+ # shorter side is at most this many points ...
366
+ MAX_MODULE = 12.0
367
+ # ... and its longer side at most this (a QR-bill code is 130 pt).
368
+ MAX_RUN = 300.0
369
+ # Fewest possible modules in a group worth rendering, and the least
370
+ # part of its area they cover (dark modules cover about half of a
371
+ # code; table rules and text much less).
372
+ MIN_SHAPES = 20
373
+ MIN_COVERAGE = 0.2
374
+ # Groups rendered per page, and their size limits in pixels.
375
+ MAX_GROUPS = 16
376
+ MAX_SIDE = 3000
377
+ # Pixels per module aimed at, and the quiet zone added (in modules).
378
+ PIXELS_PER_MODULE = 4.0
379
+ MARGIN_MODULES = 4.0
380
+ # Cells of the grid that groups are found on (per longer side).
381
+ GRID_CELLS = 1500
382
+
383
+ # A group of shapes: bounding box (page space) and module estimate.
384
+ Group = Struct.new(:x0, :y0, :x1, :y1, :count, :sides, :area)
385
+
386
+ module_function
387
+
388
+ # [[Image, dpi], ...] for the groups of +shapes+.
389
+ def images(shapes)
390
+ groups(shapes).first(MAX_GROUPS).filter_map { |group| render_group(shapes, group) }
391
+ end
392
+
393
+ # Groups of nearby small subpaths (each grown by its shorter side,
394
+ # so that modules one light module apart still touch).
395
+ def groups(shapes)
396
+ boxes = candidate_boxes(shapes)
397
+ return [] if boxes.size < MIN_SHAPES
398
+
399
+ gx0 = boxes.map { |b| b[0] }.min
400
+ gy0 = boxes.map { |b| b[1] }.min
401
+ gx1 = boxes.map { |b| b[2] }.max
402
+ gy1 = boxes.map { |b| b[3] }.max
403
+ cell = [[gx1 - gx0, gy1 - gy0].max / GRID_CELLS, 0.5].max
404
+ cols = ((gx1 - gx0) / cell).ceil + 1
405
+ rows = ((gy1 - gy0) / cell).ceil + 1
406
+ grid = Array.new(rows) { "\0".b * cols }
407
+ boxes.each do |b|
408
+ c0 = ((b[0] - gx0) / cell).floor
409
+ c1 = ((b[2] - gx0) / cell).floor
410
+ fill = "\1".b * (c1 - c0 + 1)
411
+ r = ((b[1] - gy0) / cell).floor
412
+ r1 = ((b[3] - gy0) / cell).floor
413
+ while r <= r1
414
+ grid[r].bytesplice(c0, fill.bytesize, fill)
415
+ r += 1
416
+ end
417
+ end
418
+ components(grid, boxes, gx0, gy0, cell)
419
+ end
420
+
421
+ # Grown bounding boxes [x0, y0, x1, y1, side, area] of possible
422
+ # modules: small polygons with right angles only (rectangles, runs,
423
+ # outlines of module groups) and dots (round or rounded modules).
424
+ # Glyph outlines are rarely either.
425
+ def candidate_boxes(shapes)
426
+ boxes = []
427
+ shapes.each do |shape|
428
+ shape.polys.each do |p|
429
+ x0 = x1 = p[0]
430
+ y0 = y1 = p[1]
431
+ i = 2
432
+ while i < p.size
433
+ x = p[i]
434
+ y = p[i + 1]
435
+ x0 = x if x < x0
436
+ x1 = x if x > x1
437
+ y0 = y if y < y0
438
+ y1 = y if y > y1
439
+ i += 2
440
+ end
441
+ w = x1 - x0
442
+ h = y1 - y0
443
+ side = w < h ? w : h
444
+ long = w < h ? h : w
445
+ next unless side.positive? && side <= MAX_MODULE && long <= MAX_RUN && (rectilinear?(p) || dot?(p, w, h))
446
+
447
+ boxes << [x0 - side, y0 - side, x1 + side, y1 + side, side, area(p)]
448
+ end
449
+ end
450
+ boxes
451
+ end
452
+
453
+ # True if polygon +p+ could be (part of) a module: right-angled or a
454
+ # dot (see #candidate_boxes; size is not checked).
455
+ def module_shape?(p)
456
+ return false if p.size < 6
457
+
458
+ xs = p.each_slice(2).map(&:first)
459
+ ys = p.each_slice(2).map(&:last)
460
+ rectilinear?(p) || dot?(p, xs.max - xs.min, ys.max - ys.min)
461
+ end
462
+
463
+ # True if every edge of polygon +p+ is parallel or perpendicular to
464
+ # its first one.
465
+ def rectilinear?(p)
466
+ n = p.size / 2
467
+ ux = uy = nil
468
+ k = 0
469
+ while k < n
470
+ j = k + 1 == n ? 0 : k + 1
471
+ dx = p[2 * j] - p[2 * k]
472
+ dy = p[2 * j + 1] - p[2 * k + 1]
473
+ length = Math.hypot(dx, dy)
474
+ if length > 1e-9
475
+ if ux
476
+ tolerance = 0.02 * length
477
+ return false unless (dx * uy - dy * ux).abs <= tolerance || (dx * ux + dy * uy).abs <= tolerance
478
+ else
479
+ ux = dx / length
480
+ uy = dy / length
481
+ end
482
+ end
483
+ k += 1
484
+ end
485
+ !ux.nil?
486
+ end
487
+
488
+ # True if polygon +p+ (+w+ x +h+) is a dot: about as wide as high,
489
+ # convex, and filling 60 to 100 % of its bounding box (a circle
490
+ # fills 79 %, a square with rounded corners more).
491
+ def dot?(p, w, h)
492
+ return false if p.size < 12 || w > 1.25 * h || h > 1.25 * w
493
+ return false unless area(p).between?(0.6 * w * h, 1.0 * w * h)
494
+
495
+ convex?(p)
496
+ end
497
+
498
+ # True if polygon +p+ turns the same way at every corner.
499
+ def convex?(p)
500
+ n = p.size / 2
501
+ sign = 0
502
+ k = 0
503
+ while k < n
504
+ j = (k + 1) % n
505
+ l = (k + 2) % n
506
+ cross = (p[2 * j] - p[2 * k]) * (p[2 * l + 1] - p[2 * j + 1]) -
507
+ (p[2 * j + 1] - p[2 * k + 1]) * (p[2 * l] - p[2 * j])
508
+ if cross.abs > 1e-9
509
+ s = cross.positive? ? 1 : -1
510
+ return false if sign != 0 && s != sign
511
+
512
+ sign = s
513
+ end
514
+ k += 1
515
+ end
516
+ sign != 0
517
+ end
518
+
519
+ # Area of polygon +p+ (shoelace formula).
520
+ def area(p)
521
+ n = p.size / 2
522
+ sum = 0.0
523
+ k = 0
524
+ while k < n
525
+ j = k + 1 == n ? 0 : k + 1
526
+ sum += p[2 * k] * p[2 * j + 1] - p[2 * j] * p[2 * k + 1]
527
+ k += 1
528
+ end
529
+ sum.abs / 2
530
+ end
531
+
532
+ # Connected regions of the grid (runs of marked cells joined row by
533
+ # row with a union-find), with the boxes whose centre lies in them;
534
+ # largest first.
535
+ def components(grid, boxes, gx0, gy0, cell)
536
+ parent = []
537
+ find = lambda do |i|
538
+ i = parent[i] = parent[parent[i]] while parent[i] != i
539
+ i
540
+ end
541
+ runs_by_row = []
542
+ extent = [] # per run: [c0, c1, row]
543
+ previous = []
544
+ grid.each_with_index do |row, r|
545
+ runs = []
546
+ c = 0
547
+ p = 0 # first run of the previous row that may still touch
548
+ while (start = row.index("\1", c))
549
+ stop = row.index("\0", start) || row.bytesize
550
+ id = parent.size
551
+ parent << id
552
+ extent << [start, stop - 1, r]
553
+ p += 1 while p < previous.size && extent[previous[p]][1] < start
554
+ q = p
555
+ while q < previous.size && extent[previous[q]][0] < stop
556
+ a = find.call(previous[q])
557
+ b = find.call(id)
558
+ parent[a] = b if a != b
559
+ q += 1
560
+ end
561
+ runs << id
562
+ c = stop
563
+ end
564
+ runs_by_row << runs
565
+ previous = runs
566
+ end
567
+
568
+ groups = {}
569
+ extent.each_with_index do |(c0, c1, r), id|
570
+ g = groups[find.call(id)] ||= Group.new(c0, r, c1, r, 0, [], 0.0)
571
+ g.x0 = c0 if c0 < g.x0
572
+ g.x1 = c1 if c1 > g.x1
573
+ g.y0 = r if r < g.y0
574
+ g.y1 = r if r > g.y1
575
+ end
576
+ boxes.each do |b|
577
+ side = b[4]
578
+ cx = (((b[0] + b[2]) / 2 - gx0) / cell).floor
579
+ r = (((b[1] + b[3]) / 2 - gy0) / cell).floor
580
+ id = runs_by_row[r]&.bsearch { |rid| extent[rid][1] >= cx } or next
581
+ next if extent[id][0] > cx
582
+
583
+ g = groups[find.call(id)]
584
+ g.count += 1
585
+ g.sides << side
586
+ g.area += b[5]
587
+ end
588
+ found = groups.values.select { |g| g.count >= MIN_SHAPES }.filter_map do |g|
589
+ # Back to page space, without the growth by one module.
590
+ m = module_size(g.sides)
591
+ group = Group.new(gx0 + g.x0 * cell + m, gy0 + g.y0 * cell + m, gx0 + (g.x1 + 1) * cell - m,
592
+ gy0 + (g.y1 + 1) * cell - m, g.count, g.sides, g.area)
593
+ box = (group.x1 - group.x0) * (group.y1 - group.y0)
594
+ group if box.positive? && group.area >= MIN_COVERAGE * box
595
+ end
596
+ found.sort_by { |g| -g.count }
597
+ end
598
+
599
+ # A low percentile of the shorter sides: single modules (runs and
600
+ # finder pattern rings are longer, hairlines are rare).
601
+ def module_size(sides)
602
+ sorted = sides.sort
603
+ sorted[sorted.size / 4]
604
+ end
605
+
606
+ def render_group(shapes, group)
607
+ m = module_size(group.sides)
608
+ w = group.x1 - group.x0
609
+ h = group.y1 - group.y0
610
+ return nil unless w.positive? && h.positive?
611
+ # A code is about square; long thin groups are text lines or rules.
612
+ return nil if w > 4 * h || h > 4 * w
613
+
614
+ margin = MARGIN_MODULES * m
615
+ x0 = group.x0 - margin
616
+ y0 = group.y0 - margin
617
+ x1 = group.x1 + margin
618
+ y1 = group.y1 + margin
619
+ scale = PIXELS_PER_MODULE / m
620
+ longest = [x1 - x0, y1 - y0].max
621
+ scale = MAX_SIDE / longest if longest * scale > MAX_SIDE
622
+ width = ((x1 - x0) * scale).ceil
623
+ height = ((y1 - y0) * scale).ceil
624
+ return nil if width < 21 || height < 21
625
+
626
+ [rasterize(shapes, x0, y1, x1, y0, scale, width, height, m), (scale * 72).round(2)]
627
+ end
628
+
629
+ # Paints +shapes+ inside the page rectangle (left, top, right,
630
+ # bottom) at +scale+ pixels per point onto a white Image. Dots of
631
+ # about the module size +m+ are painted as whole modules, so that
632
+ # codes drawn with round modules (finder patterns too) read like
633
+ # ordinary ones.
634
+ def rasterize(shapes, left, top, right, bottom, scale, width, height, m)
635
+ data = "\xFF".b * (width * height)
636
+ inside = shapes.select do |shape|
637
+ bx0, by0, bx1, by1 = shape.bbox
638
+ bx1 >= left && bx0 <= right && by1 >= bottom && by0 <= top
639
+ end
640
+ dots = dot_centers(inside, m)
641
+ r = dot_pitch(dots.values, m) / 2
642
+ inside.each do |shape|
643
+ polys = shape.polys.map do |p|
644
+ next p unless (cx, cy = dots[p])
645
+
646
+ [cx - r, cy - r, cx + r, cy - r, cx + r, cy + r, cx - r, cy + r]
647
+ end
648
+ to_pixels = polys.map do |p|
649
+ q = Array.new(p.size)
650
+ i = 0
651
+ while i < p.size
652
+ q[i] = (p[i] - left) * scale
653
+ q[i + 1] = (top - p[i + 1]) * scale
654
+ i += 2
655
+ end
656
+ q
657
+ end
658
+ chr = shape.luma.chr
659
+ if shape.union || to_pixels.size == 1
660
+ to_pixels.each { |q| fill_polygons([q], false, chr, data, width, height) }
661
+ else
662
+ fill_polygons(to_pixels, shape.even_odd, chr, data, width, height)
663
+ end
664
+ end
665
+ Image.new(width, height, data)
666
+ end
667
+
668
+ # Centres of the dots (0.5 to 1.2 modules +m+ wide) of filled
669
+ # +shapes+, by polygon (compared by identity).
670
+ def dot_centers(shapes, m)
671
+ centers = {}.compare_by_identity
672
+ shapes.each do |shape|
673
+ next if shape.union
674
+
675
+ shape.polys.each do |p|
676
+ xs = p.each_slice(2).map(&:first)
677
+ ys = p.each_slice(2).map(&:last)
678
+ w = xs.max - xs.min
679
+ h = ys.max - ys.min
680
+ next unless w.between?(0.5 * m, 1.2 * m) && h.between?(0.5 * m, 1.2 * m) && dot?(p, w, h)
681
+
682
+ centers[p] = [(xs.max + xs.min) / 2, (ys.max + ys.min) / 2]
683
+ end
684
+ end
685
+ centers
686
+ end
687
+
688
+ # Distance between neighbouring dot columns and rows (dots are a
689
+ # bit smaller than the modules they stand for): the median step
690
+ # between distinct centre coordinates, within 0.9 to 2 modules +m+.
691
+ def dot_pitch(centers, m)
692
+ steps = []
693
+ [0, 1].each do |axis|
694
+ previous = nil
695
+ centers.map { |c| c[axis] }.sort.each do |v|
696
+ next if previous && v - previous < 0.25 * m
697
+
698
+ steps << v - previous if previous
699
+ previous = v
700
+ end
701
+ end
702
+ return m if steps.empty?
703
+
704
+ steps.sort[steps.size / 2].clamp(0.9 * m, 2.0 * m)
705
+ end
706
+
707
+ # Scanline fill (pixel centres) of +polys+ with the nonzero or
708
+ # even-odd rule; axis-aligned rectangles take a shortcut.
709
+ def fill_polygons(polys, even_odd, chr, data, width, height)
710
+ return fill_rect(polys[0], chr, data, width, height) if polys.size == 1 && rect?(polys[0])
711
+
712
+ edges = []
713
+ polys.each do |q|
714
+ n = q.size / 2
715
+ next if n < 3
716
+
717
+ k = 0
718
+ while k < n
719
+ xa = q[2 * k]
720
+ ya = q[2 * k + 1]
721
+ j = k + 1 == n ? 0 : k + 1
722
+ xb = q[2 * j]
723
+ yb = q[2 * j + 1]
724
+ if ya != yb
725
+ edges << (ya < yb ? [ya, yb, xa, (xb - xa) / (yb - ya), 1] : [yb, ya, xb, (xa - xb) / (ya - yb), -1])
726
+ end
727
+ k += 1
728
+ end
729
+ end
730
+ return if edges.empty?
731
+
732
+ edges.sort_by!(&:first)
733
+ r = (edges.first[0] - 0.5).ceil
734
+ r = 0 if r.negative?
735
+ last = (edges.map { |e| e[1] }.max - 0.5).ceil - 1
736
+ last = height - 1 if last >= height
737
+ active = []
738
+ next_edge = 0
739
+ crossings = []
740
+ while r <= last
741
+ yc = r + 0.5
742
+ while next_edge < edges.size && edges[next_edge][0] <= yc
743
+ active << edges[next_edge]
744
+ next_edge += 1
745
+ end
746
+ active.reject! { |e| e[1] <= yc }
747
+ crossings.clear
748
+ active.each { |e| crossings << [e[2] + (yc - e[0]) * e[3], e[4]] if e[0] <= yc }
749
+ crossings.sort_by!(&:first)
750
+ winding = 0
751
+ i = 0
752
+ while i < crossings.size - 1
753
+ winding += even_odd ? 1 : crossings[i][1]
754
+ inside = even_odd ? winding.odd? : winding != 0
755
+ span(crossings[i][0], crossings[i + 1][0], r, chr, data, width) if inside
756
+ i += 1
757
+ end
758
+ r += 1
759
+ end
760
+ end
761
+
762
+ def rect?(q)
763
+ return false unless q.size == 8
764
+
765
+ (q[0] == q[2] && q[3] == q[5] && q[4] == q[6] && q[7] == q[1]) ||
766
+ (q[1] == q[3] && q[2] == q[4] && q[5] == q[7] && q[6] == q[0])
767
+ end
768
+
769
+ def fill_rect(q, chr, data, width, height)
770
+ xs = [q[0], q[2], q[4], q[6]]
771
+ ys = [q[1], q[3], q[5], q[7]]
772
+ r = (ys.min - 0.5).ceil
773
+ r = 0 if r.negative?
774
+ stop = (ys.max - 0.5).ceil - 1
775
+ stop = height - 1 if stop >= height
776
+ xa = xs.min
777
+ xb = xs.max
778
+ while r <= stop
779
+ span(xa, xb, r, chr, data, width)
780
+ r += 1
781
+ end
782
+ end
783
+
784
+ # Pixels of row +r+ whose centres lie in [xa, xb).
785
+ def span(xa, xb, r, chr, data, width)
786
+ c0 = (xa - 0.5).ceil
787
+ c1 = (xb - 0.5).ceil - 1
788
+ c0 = 0 if c0.negative?
789
+ c1 = width - 1 if c1 >= width
790
+ return if c1 < c0
791
+
792
+ data.bytesplice(r * width + c0, c1 - c0 + 1, chr * (c1 - c0 + 1))
793
+ end
794
+ end
795
+
796
+ private_constant :PathCollector, :VectorRenderer
797
+ end
798
+ end
799
+ end