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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +23 -7
- data/exe/qr_reader +1 -1
- data/lib/qr_reader/formats/jpeg.rb +61 -2
- data/lib/qr_reader/formats/jpeg2000/codestream.rb +447 -0
- data/lib/qr_reader/formats/jpeg2000/container.rb +156 -0
- data/lib/qr_reader/formats/jpeg2000/dwt.rb +607 -0
- data/lib/qr_reader/formats/jpeg2000/tier1.rb +1084 -0
- data/lib/qr_reader/formats/jpeg2000/tile.rb +722 -0
- data/lib/qr_reader/formats/jpeg2000.rb +764 -0
- data/lib/qr_reader/formats/pdf/font_programs.rb +1205 -0
- data/lib/qr_reader/formats/pdf/font_tables.rb +224 -0
- data/lib/qr_reader/formats/pdf/fonts.rb +412 -0
- data/lib/qr_reader/formats/pdf/text.rb +151 -0
- data/lib/qr_reader/formats/pdf/vector.rb +799 -0
- data/lib/qr_reader/formats/pdf.rb +183 -44
- data/lib/qr_reader/formats.rb +13 -4
- data/lib/qr_reader/reader.rb +36 -10
- data/lib/qr_reader/version.rb +1 -1
- data/lib/qr_reader.rb +1 -1
- metadata +15 -4
|
@@ -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
|