qr_reader 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +16 -0
- data/LICENSE.txt +21 -0
- data/NOTICE +14 -0
- data/README.md +222 -0
- data/exe/qr_reader +66 -0
- data/lib/qr_reader/binarizer.rb +170 -0
- data/lib/qr_reader/bit_matrix.rb +120 -0
- data/lib/qr_reader/decoder/bitstream_parser.rb +307 -0
- data/lib/qr_reader/decoder/character_set.rb +163 -0
- data/lib/qr_reader/decoder/codewords.rb +164 -0
- data/lib/qr_reader/decoder/data_mask.rb +57 -0
- data/lib/qr_reader/decoder/format_reader.rb +204 -0
- data/lib/qr_reader/decoder/function_patterns.rb +62 -0
- data/lib/qr_reader/decoder/galois_field.rb +69 -0
- data/lib/qr_reader/decoder/reed_solomon.rb +312 -0
- data/lib/qr_reader/decoder.rb +241 -0
- data/lib/qr_reader/detector/alignment_grid.rb +127 -0
- data/lib/qr_reader/detector/alignment_pattern_finder.rb +191 -0
- data/lib/qr_reader/detector/candidate/dimension.rb +237 -0
- data/lib/qr_reader/detector/candidate/strokes.rb +126 -0
- data/lib/qr_reader/detector/candidate.rb +436 -0
- data/lib/qr_reader/detector/finder_pattern.rb +63 -0
- data/lib/qr_reader/detector/finder_pattern_finder.rb +199 -0
- data/lib/qr_reader/detector/geometry.rb +118 -0
- data/lib/qr_reader/detector/grid_sampler.rb +350 -0
- data/lib/qr_reader/detector/pattern_sets.rb +235 -0
- data/lib/qr_reader/detector/perspective_transform.rb +117 -0
- data/lib/qr_reader/detector/pure_detector.rb +197 -0
- data/lib/qr_reader/detector.rb +215 -0
- data/lib/qr_reader/errors.rb +14 -0
- data/lib/qr_reader/format_information.rb +79 -0
- data/lib/qr_reader/formats/bmp.rb +434 -0
- data/lib/qr_reader/formats/ccitt.rb +81 -0
- data/lib/qr_reader/formats/gif.rb +272 -0
- data/lib/qr_reader/formats/jpeg.rb +152 -0
- data/lib/qr_reader/formats/pdf.rb +2935 -0
- data/lib/qr_reader/formats/png.rb +311 -0
- data/lib/qr_reader/formats/pnm.rb +286 -0
- data/lib/qr_reader/formats/tiff.rb +1345 -0
- data/lib/qr_reader/formats.rb +279 -0
- data/lib/qr_reader/image.rb +231 -0
- data/lib/qr_reader/kernels/binarizer.rb +475 -0
- data/lib/qr_reader/kernels/ccitt.rb +554 -0
- data/lib/qr_reader/kernels/detector.rb +587 -0
- data/lib/qr_reader/kernels/image.rb +65 -0
- data/lib/qr_reader/kernels/jpeg.rb +3421 -0
- data/lib/qr_reader/kernels/lzw.rb +173 -0
- data/lib/qr_reader/kernels/pixels.rb +116 -0
- data/lib/qr_reader/kernels/png.rb +1138 -0
- data/lib/qr_reader/kernels.rb +14 -0
- data/lib/qr_reader/native_kernels.rb +130 -0
- data/lib/qr_reader/reader.rb +348 -0
- data/lib/qr_reader/result.rb +49 -0
- data/lib/qr_reader/symbol_version.rb +173 -0
- data/lib/qr_reader/version.rb +5 -0
- data/lib/qr_reader.rb +77 -0
- metadata +103 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QrReader
|
|
4
|
+
module Formats
|
|
5
|
+
# Windows/OS2 bitmap decoder: 1/2/4/8 bpp palette images, 16/24/32 bpp
|
|
6
|
+
# (BI_RGB, BI_BITFIELDS and BI_ALPHABITFIELDS, alpha masks included),
|
|
7
|
+
# RLE8 and RLE4, bottom-up and top-down rows, OS/2 core headers and
|
|
8
|
+
# BITMAPINFOHEADER through V5.
|
|
9
|
+
#
|
|
10
|
+
# Like browsers, the fourth byte of 32 bpp BI_RGB pixels is used as alpha,
|
|
11
|
+
# but an alpha channel that leaves the whole image transparent is ignored
|
|
12
|
+
# (many writers leave it zero). Pixels skipped by RLE deltas are
|
|
13
|
+
# transparent, i.e. white. Mask fields narrower than 8 bits are widened
|
|
14
|
+
# by bit replication (5 bits: v << 3 | v >> 2, like ImageMagick), wider
|
|
15
|
+
# ones keep their top 8 bits.
|
|
16
|
+
module BMP
|
|
17
|
+
# DIB header sizes: OS/2 core (12), OS/2 2.x (16, 64), INFO (40), V2-V5.
|
|
18
|
+
HEADER_SIZES = [12, 16, 40, 52, 56, 64, 108, 124].freeze
|
|
19
|
+
|
|
20
|
+
BI_RGB = 0
|
|
21
|
+
BI_RLE8 = 1
|
|
22
|
+
BI_RLE4 = 2
|
|
23
|
+
BI_BITFIELDS = 3
|
|
24
|
+
BI_ALPHABITFIELDS = 6
|
|
25
|
+
|
|
26
|
+
# Default channel masks for BI_RGB [red, green, blue, alpha].
|
|
27
|
+
MASKS_555 = [0x7C00, 0x03E0, 0x001F, 0].freeze
|
|
28
|
+
MASKS_8888 = [0xFF0000, 0xFF00, 0xFF, 0xFF000000].freeze
|
|
29
|
+
|
|
30
|
+
HEX_DIGITS = "0123456789abcdef".bytes.freeze
|
|
31
|
+
|
|
32
|
+
# RLE opcodes decoded per call of Decoder#rle_slice.
|
|
33
|
+
RLE_SLICE = 256
|
|
34
|
+
|
|
35
|
+
class << self
|
|
36
|
+
# Plausibility check on the first bytes ("BM" + known DIB header size).
|
|
37
|
+
def bmp?(head)
|
|
38
|
+
head.bytesize >= 18 && head.start_with?("BM") && HEADER_SIZES.include?(head.unpack1("V", offset: 14))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def decode(bytes)
|
|
42
|
+
Decoder.new(Support.binary(bytes)).decode
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def dimensions(bytes)
|
|
46
|
+
Decoder.new(Support.binary(bytes)).dimensions
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Parses the headers on creation.
|
|
51
|
+
class Decoder
|
|
52
|
+
def initialize(bytes)
|
|
53
|
+
raise ImageError, "not a BMP file" unless BMP.bmp?(bytes)
|
|
54
|
+
|
|
55
|
+
@bytes = bytes
|
|
56
|
+
header_size = bytes.unpack1("V", offset: 14)
|
|
57
|
+
raise ImageError, "truncated BMP header" if bytes.bytesize < 14 + header_size
|
|
58
|
+
|
|
59
|
+
read_header(header_size)
|
|
60
|
+
Support.check_dimensions(@width, @height, "BMP")
|
|
61
|
+
read_palette if @bpp <= 8
|
|
62
|
+
@data_offset = @palette_end unless @data_offset.between?(@palette_end, bytes.bytesize - 1)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def dimensions
|
|
66
|
+
[@width, @height]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def decode
|
|
70
|
+
gray =
|
|
71
|
+
case @compression
|
|
72
|
+
when BI_RLE8, BI_RLE4 then decode_rle
|
|
73
|
+
else decode_raw
|
|
74
|
+
end
|
|
75
|
+
Image.new(@width, @height, gray)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def read_header(header_size)
|
|
81
|
+
bytes = @bytes
|
|
82
|
+
@data_offset = bytes.unpack1("V", offset: 10)
|
|
83
|
+
@palette_entry = 4
|
|
84
|
+
if header_size == 12 # OS/2 1.x BITMAPCOREHEADER: 16-bit sizes, RGB triples
|
|
85
|
+
@width, @height, _planes, @bpp = bytes.unpack("vvvv", offset: 18)
|
|
86
|
+
@compression = BI_RGB
|
|
87
|
+
@palette_entry = 3
|
|
88
|
+
colors = 0
|
|
89
|
+
else
|
|
90
|
+
@width, @height, _planes, @bpp = bytes.unpack("l<l<vv", offset: 18)
|
|
91
|
+
@compression = header_size >= 20 ? bytes.unpack1("V", offset: 30) : BI_RGB
|
|
92
|
+
colors = header_size >= 36 ? bytes.unpack1("V", offset: 46) : 0
|
|
93
|
+
end
|
|
94
|
+
@top_down = @height.negative?
|
|
95
|
+
@height = -@height if @top_down
|
|
96
|
+
@palette_start = 14 + header_size
|
|
97
|
+
check_compression(header_size)
|
|
98
|
+
read_masks(header_size)
|
|
99
|
+
@palette_end = @palette_start
|
|
100
|
+
@colors = colors
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def check_compression(header_size)
|
|
104
|
+
os2 = header_size == 16 || header_size == 64
|
|
105
|
+
case @compression
|
|
106
|
+
when BI_RGB then nil
|
|
107
|
+
when BI_RLE8 then raise ImageError, "RLE8 BMP must have 8 bpp" unless @bpp == 8
|
|
108
|
+
when BI_RLE4 then raise ImageError, "RLE4 BMP must have 4 bpp" unless @bpp == 4
|
|
109
|
+
when BI_BITFIELDS, BI_ALPHABITFIELDS
|
|
110
|
+
raise UnsupportedImageError, "OS/2 Huffman-compressed BMPs are not supported" if os2
|
|
111
|
+
|
|
112
|
+
raise ImageError, "BI_BITFIELDS BMP must have 16 or 32 bpp" unless @bpp == 16 || @bpp == 32
|
|
113
|
+
when 4, 5
|
|
114
|
+
kind = os2 ? "RLE24" : (@compression == 4 ? "JPEG" : "PNG")
|
|
115
|
+
raise UnsupportedImageError, "#{kind}-compressed BMPs are not supported"
|
|
116
|
+
else
|
|
117
|
+
raise UnsupportedImageError, "BMP compression type #{@compression} is not supported"
|
|
118
|
+
end
|
|
119
|
+
return if [1, 2, 4, 8, 16, 24, 32].include?(@bpp)
|
|
120
|
+
|
|
121
|
+
raise UnsupportedImageError, "BMPs with #{@bpp} bits per pixel are not supported"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Channel masks for 16 and 32 bpp; they follow a 40-byte header
|
|
125
|
+
# when BI_(ALPHA)BITFIELDS is used and are part of V2+ headers.
|
|
126
|
+
def read_masks(header_size)
|
|
127
|
+
return unless @bpp == 16 || @bpp == 32
|
|
128
|
+
|
|
129
|
+
bytes = @bytes
|
|
130
|
+
@masks = @bpp == 16 ? MASKS_555 : MASKS_8888
|
|
131
|
+
return if @compression == BI_RGB
|
|
132
|
+
|
|
133
|
+
count = @compression == BI_ALPHABITFIELDS ? 4 : 3
|
|
134
|
+
if header_size >= 52
|
|
135
|
+
count = 4 if header_size >= 56
|
|
136
|
+
pos = 54
|
|
137
|
+
else
|
|
138
|
+
# Some writers set BI_BITFIELDS but omit the masks (pixel data
|
|
139
|
+
# starts right after the header); assume the defaults then.
|
|
140
|
+
return if @data_offset == @palette_start
|
|
141
|
+
|
|
142
|
+
pos = @palette_start
|
|
143
|
+
@palette_start += count * 4
|
|
144
|
+
end
|
|
145
|
+
raise ImageError, "truncated BMP colour masks" if pos + count * 4 > bytes.bytesize
|
|
146
|
+
|
|
147
|
+
@masks = bytes.unpack("V#{count}", offset: pos)
|
|
148
|
+
@masks << 0 if count == 3
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def read_palette
|
|
152
|
+
entry = @palette_entry
|
|
153
|
+
count = @colors.positive? ? [@colors, 256].min : 1 << @bpp
|
|
154
|
+
if @data_offset.between?(@palette_start, @bytes.bytesize)
|
|
155
|
+
count = [count, (@data_offset - @palette_start) / entry].min
|
|
156
|
+
end
|
|
157
|
+
palette = @bytes.byteslice(@palette_start, count * entry) || "".b
|
|
158
|
+
@palette_end = @palette_start + palette.bytesize
|
|
159
|
+
@lumas = Support.palette_lumas(palette, entry_size: entry, order: :bgr)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Uncompressed pixel rows (stride padded to 4 bytes).
|
|
163
|
+
def decode_raw
|
|
164
|
+
w = @width
|
|
165
|
+
h = @height
|
|
166
|
+
bpp = @bpp
|
|
167
|
+
stride = ((w * bpp + 31) >> 5) << 2
|
|
168
|
+
row_bytes = (w * bpp + 7) >> 3
|
|
169
|
+
# The padding of the last row is often missing; tolerate that.
|
|
170
|
+
if @data_offset + stride * (h - 1) + row_bytes > @bytes.bytesize
|
|
171
|
+
raise ImageError, "truncated BMP pixel data"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
out = convert_rows(row_converter(alpha: true), stride)
|
|
175
|
+
# An alpha channel that hides everything is an unset field (most
|
|
176
|
+
# often all zero): decode the pixels as opaque instead.
|
|
177
|
+
if @masks && @masks[3].positive? && out.count(WHITE) == out.bytesize
|
|
178
|
+
out = convert_rows(row_converter(alpha: false), stride)
|
|
179
|
+
end
|
|
180
|
+
finish_palette(out)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def convert_rows(converter, stride)
|
|
184
|
+
h = @height
|
|
185
|
+
out = Support.buffer(@width * h)
|
|
186
|
+
y = 0
|
|
187
|
+
while y < h
|
|
188
|
+
converter.call(@data_offset + (@top_down ? y : h - 1 - y) * stride, out)
|
|
189
|
+
y += 1
|
|
190
|
+
end
|
|
191
|
+
out
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# A lambda converting one row at a byte offset, appending luminance
|
|
195
|
+
# (or palette indices, mapped later by finish_palette).
|
|
196
|
+
def row_converter(alpha:)
|
|
197
|
+
bytes = @bytes
|
|
198
|
+
w = @width
|
|
199
|
+
case @bpp
|
|
200
|
+
when 1
|
|
201
|
+
bits = "B#{w}"
|
|
202
|
+
->(off, out) { out << bytes.unpack1(bits, offset: off) }
|
|
203
|
+
when 2 then ->(off, out) { Rows.unpack2(bytes, off, w, out) }
|
|
204
|
+
when 4
|
|
205
|
+
nibbles = "H#{w}"
|
|
206
|
+
->(off, out) { out << bytes.unpack1(nibbles, offset: off) }
|
|
207
|
+
when 8 then ->(off, out) { out << bytes.byteslice(off, w) }
|
|
208
|
+
when 24 then ->(off, out) { Rows.rgb(bytes, off, w, out, 2, 1, 0) }
|
|
209
|
+
else masked_converter(alpha)
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def masked_converter(alpha)
|
|
214
|
+
bytes = @bytes
|
|
215
|
+
w = @width
|
|
216
|
+
rm, gm, bm, am = @masks
|
|
217
|
+
am = 0 unless alpha
|
|
218
|
+
if @bpp == 32 && rm == 0xFF0000 && gm == 0xFF00 && bm == 0xFF
|
|
219
|
+
return ->(off, out) { Rows.rgb(bytes, off, w, out, 2, 1, 0, 4) } if am.zero?
|
|
220
|
+
if am == 0xFF000000
|
|
221
|
+
table = Support.over_white_table
|
|
222
|
+
return ->(off, out) { Rows.rgba(bytes, off, w, out, table, 2, 1, 0, 3) }
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
fields = [rm, gm, bm, am].map { |mask| Field.new(mask) }
|
|
227
|
+
wide = @bpp == 32
|
|
228
|
+
table = Support.over_white_table unless am.zero?
|
|
229
|
+
->(off, out) { masked_row(bytes, off, w, wide, fields, table, out) }
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Converts a row of +n+ little-endian 16-bit (or, if +wide+, 32-bit)
|
|
233
|
+
# pixels at +off+ with arbitrary channel masks; an alpha field
|
|
234
|
+
# (non-zero mask) is composited over white through +table+.
|
|
235
|
+
def masked_row(bytes, off, n, wide, fields, table, out)
|
|
236
|
+
r, g, b, a = fields
|
|
237
|
+
rm = r.mask
|
|
238
|
+
rs = r.shift
|
|
239
|
+
rt = r.table
|
|
240
|
+
gm = g.mask
|
|
241
|
+
gs = g.shift
|
|
242
|
+
gt = g.table
|
|
243
|
+
bm = b.mask
|
|
244
|
+
bs = b.shift
|
|
245
|
+
bt = b.table
|
|
246
|
+
am = a.mask
|
|
247
|
+
as = a.shift
|
|
248
|
+
at = a.table
|
|
249
|
+
i = off
|
|
250
|
+
stop = off + n * (wide ? 4 : 2)
|
|
251
|
+
while i < stop
|
|
252
|
+
v = bytes.getbyte(i) | (bytes.getbyte(i + 1) << 8)
|
|
253
|
+
if wide
|
|
254
|
+
v |= (bytes.getbyte(i + 2) << 16) | (bytes.getbyte(i + 3) << 24)
|
|
255
|
+
i += 4
|
|
256
|
+
else
|
|
257
|
+
i += 2
|
|
258
|
+
end
|
|
259
|
+
l = (306 * rt[(v & rm) >> rs] + 601 * gt[(v & gm) >> gs] + 117 * bt[(v & bm) >> bs] + 0x200) >> 10
|
|
260
|
+
l = table.getbyte((at[(v & am) >> as] << 8) | l) if am != 0
|
|
261
|
+
out << l
|
|
262
|
+
end
|
|
263
|
+
out
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def finish_palette(out)
|
|
267
|
+
from =
|
|
268
|
+
case @bpp
|
|
269
|
+
when 1 then [48, 49] # "0", "1"
|
|
270
|
+
when 2 then [0, 1, 2, 3]
|
|
271
|
+
when 4 then HEX_DIGITS
|
|
272
|
+
when 8 then ALL_BYTES
|
|
273
|
+
end
|
|
274
|
+
from ? Support.translate!(out, from, @lumas.first(from.size)) : out
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# RLE8/RLE4: runs and absolute groups of palette indices; deltas and
|
|
278
|
+
# early line/bitmap ends leave pixels transparent (white). The
|
|
279
|
+
# opcodes are read in slices (rle_slice) so that YJIT compiles the
|
|
280
|
+
# loop; absolute groups are mapped pixel by pixel (rle_absolute).
|
|
281
|
+
def decode_rle
|
|
282
|
+
raise ImageError, "truncated BMP RLE data" if @data_offset + 2 > @bytes.bytesize
|
|
283
|
+
|
|
284
|
+
@rle4 = @compression == BI_RLE4
|
|
285
|
+
lum = @lumas.map { |v| v.chr.b }
|
|
286
|
+
# RLE4 runs alternate the two nibbles of their byte.
|
|
287
|
+
@runs = @rle4 ? Array.new(256) { |b| lum[b >> 4] + lum[b & 15] } : lum
|
|
288
|
+
@out = WHITE * (@width * @height)
|
|
289
|
+
@pos = @data_offset
|
|
290
|
+
@x = @y = 0
|
|
291
|
+
rle_slice while @y < @height
|
|
292
|
+
@out
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Decodes up to RLE_SLICE opcodes from @pos; the position (@pos, @x,
|
|
296
|
+
# @y) carries over to the next call. @y reaches the height at the end.
|
|
297
|
+
def rle_slice
|
|
298
|
+
bytes = @bytes
|
|
299
|
+
size = bytes.bytesize
|
|
300
|
+
w = @width
|
|
301
|
+
h = @height
|
|
302
|
+
pos = @pos
|
|
303
|
+
x = @x
|
|
304
|
+
y = @y
|
|
305
|
+
ops = 0
|
|
306
|
+
while y < h && ops < RLE_SLICE
|
|
307
|
+
raise ImageError, "truncated BMP RLE data" if pos + 2 > size
|
|
308
|
+
|
|
309
|
+
count = bytes.getbyte(pos)
|
|
310
|
+
value = bytes.getbyte(pos + 1)
|
|
311
|
+
pos += 2
|
|
312
|
+
ops += 1
|
|
313
|
+
if count.positive?
|
|
314
|
+
rle_run(x, y, count, value) if x < w
|
|
315
|
+
x += count
|
|
316
|
+
elsif value.zero? # end of line
|
|
317
|
+
x = 0
|
|
318
|
+
y += 1
|
|
319
|
+
elsif value == 1 # end of bitmap
|
|
320
|
+
y = h
|
|
321
|
+
elsif value == 2 # delta
|
|
322
|
+
raise ImageError, "truncated BMP RLE data" if pos + 2 > size
|
|
323
|
+
|
|
324
|
+
x += bytes.getbyte(pos)
|
|
325
|
+
y += bytes.getbyte(pos + 1)
|
|
326
|
+
pos += 2
|
|
327
|
+
else # absolute mode: +value+ indices, padded to 16 bits
|
|
328
|
+
nbytes = @rle4 ? (value + 1) >> 1 : value
|
|
329
|
+
raise ImageError, "truncated BMP RLE data" if pos + nbytes > size
|
|
330
|
+
|
|
331
|
+
rle_absolute(pos, x, y, value) if x < w
|
|
332
|
+
x += value
|
|
333
|
+
pos += nbytes + (nbytes & 1)
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
@pos = pos
|
|
337
|
+
@x = x
|
|
338
|
+
@y = y
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# A run of +count+ pixels at (x, y) in file row order, clipped
|
|
342
|
+
# (x is inside the image). Short runs, the most common ones, are
|
|
343
|
+
# written byte by byte rather than built as Strings.
|
|
344
|
+
def rle_run(x, y, count, value)
|
|
345
|
+
w = @width
|
|
346
|
+
n = count < w - x ? count : w - x
|
|
347
|
+
dst = row_offset(y) + x
|
|
348
|
+
out = @out
|
|
349
|
+
if n > 8
|
|
350
|
+
run = @rle4 ? @runs[value] * ((n + 1) >> 1) : @runs[value] * n
|
|
351
|
+
out.bytesplice(dst, n, run.bytesize > n ? run.byteslice(0, n) : run)
|
|
352
|
+
elsif @rle4
|
|
353
|
+
even = @lumas[value >> 4]
|
|
354
|
+
odd = @lumas[value & 15]
|
|
355
|
+
k = 0
|
|
356
|
+
while k < n
|
|
357
|
+
out.setbyte(dst + k, (k & 1).zero? ? even : odd)
|
|
358
|
+
k += 1
|
|
359
|
+
end
|
|
360
|
+
else
|
|
361
|
+
v = @lumas[value]
|
|
362
|
+
stop = dst + n
|
|
363
|
+
while dst < stop
|
|
364
|
+
out.setbyte(dst, v)
|
|
365
|
+
dst += 1
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# An absolute group of +count+ indices at +pos+, drawn at (x, y),
|
|
371
|
+
# clipped (x is inside the image).
|
|
372
|
+
def rle_absolute(pos, x, y, count)
|
|
373
|
+
w = @width
|
|
374
|
+
n = count < w - x ? count : w - x
|
|
375
|
+
bytes = @bytes
|
|
376
|
+
lumas = @lumas
|
|
377
|
+
out = @out
|
|
378
|
+
dst = row_offset(y) + x
|
|
379
|
+
stop = dst + n
|
|
380
|
+
if @rle4
|
|
381
|
+
k = 0
|
|
382
|
+
while dst < stop
|
|
383
|
+
b = bytes.getbyte(pos + (k >> 1))
|
|
384
|
+
out.setbyte(dst, lumas[(k & 1).zero? ? b >> 4 : b & 15])
|
|
385
|
+
dst += 1
|
|
386
|
+
k += 1
|
|
387
|
+
end
|
|
388
|
+
else
|
|
389
|
+
while dst < stop
|
|
390
|
+
out.setbyte(dst, lumas[bytes.getbyte(pos)])
|
|
391
|
+
dst += 1
|
|
392
|
+
pos += 1
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# Offset of file row +y+ in the top-down output.
|
|
398
|
+
def row_offset(y)
|
|
399
|
+
(@top_down ? y : @height - 1 - y) * @width
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
private_constant :Decoder
|
|
403
|
+
|
|
404
|
+
# A colour channel described by a bit mask, with a table from field
|
|
405
|
+
# value to 8 bits: narrower fields are widened by bit replication,
|
|
406
|
+
# wider ones keep their top 8 bits.
|
|
407
|
+
class Field
|
|
408
|
+
attr_reader :mask, :shift, :table
|
|
409
|
+
|
|
410
|
+
def initialize(mask)
|
|
411
|
+
@mask = mask
|
|
412
|
+
@shift = mask.zero? ? 0 : (mask & -mask).bit_length - 1
|
|
413
|
+
bits = (mask >> @shift).bit_length
|
|
414
|
+
if bits > 8
|
|
415
|
+
@shift += bits - 8
|
|
416
|
+
bits = 8
|
|
417
|
+
end
|
|
418
|
+
@table = bits.zero? ? [0].freeze : Array.new(1 << bits) { |v| Field.widen(v, bits) }.freeze
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def self.widen(value, bits)
|
|
422
|
+
wide = 0
|
|
423
|
+
filled = 0
|
|
424
|
+
while filled < 8
|
|
425
|
+
wide = (wide << bits) | value
|
|
426
|
+
filled += bits
|
|
427
|
+
end
|
|
428
|
+
wide >> (filled - 8)
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
private_constant :Field
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../kernels/ccitt"
|
|
4
|
+
|
|
5
|
+
module QrReader
|
|
6
|
+
module Formats
|
|
7
|
+
# CCITT fax decoder: Group 3 (ITU-T T.4: 1-D Modified Huffman and 2-D
|
|
8
|
+
# Modified READ) and Group 4 (T.6, MMR), the bilevel codings that scanners
|
|
9
|
+
# and fax machines write into TIFF files (compression 2, 3 and 4) and PDFs
|
|
10
|
+
# embed as CCITTFaxDecode streams.
|
|
11
|
+
#
|
|
12
|
+
# CCITT.decode(g4_data, columns: 2480, rows: 3508) # => #<QrReader::Image 2480x3508>
|
|
13
|
+
# CCITT.decode(g3_data, columns: 1728, k: 0) # rows until RTC / end of data
|
|
14
|
+
#
|
|
15
|
+
# Parameters follow PDF's CCITTFaxDecode filter:
|
|
16
|
+
# * +k+ < 0: Group 4; 0: Group 3 1-D; > 0: Group 3 2-D (each row's tag
|
|
17
|
+
# bit selects 1-D or 2-D coding).
|
|
18
|
+
# * +rows+ nil or 0: decode until EOFB / RTC or the end of the data.
|
|
19
|
+
# Missing rows (truncated data) are white.
|
|
20
|
+
# * +black_is_1+ false: coded white is 255 and coded black 0 (what a PDF
|
|
21
|
+
# shows with the default Decode array); true inverts the image (also
|
|
22
|
+
# what TIFF's BlackIsZero photometric means for fax data).
|
|
23
|
+
# * +encoded_byte_align+: every row starts on a byte boundary (TIFF
|
|
24
|
+
# compression 2 rows, PDF EncodedByteAlign).
|
|
25
|
+
# * +fill_order+ 2: bits are stored least significant first (TIFF FillOrder).
|
|
26
|
+
# * +end_of_line+: EOL codes precede the rows. EOLs, fill bits before
|
|
27
|
+
# them and EOFB/RTC are recognised either way, so the flag only
|
|
28
|
+
# decides whether an unaligned EOL may stand before a byte-aligned row.
|
|
29
|
+
# * +end_of_block+ and +damaged_rows_before_error+ are accepted for PDF
|
|
30
|
+
# compatibility. Decoding is always lenient: rows stop at EOFB/RTC or
|
|
31
|
+
# after +rows+ rows, and damaged rows never raise.
|
|
32
|
+
#
|
|
33
|
+
# Damaged rows of EOL-delimited (Group 3) data are replaced by the row
|
|
34
|
+
# above and decoding resumes at the next EOL. Group 4 cannot
|
|
35
|
+
# resynchronise, so an error ends the image there (the rest stays
|
|
36
|
+
# white). Only data in which not even the first row decodes raises
|
|
37
|
+
# ImageError. Uncompressed-mode extensions count as damage.
|
|
38
|
+
#
|
|
39
|
+
# The decoding itself is the kernel QrReaderKernels.ccitt_decode.
|
|
40
|
+
module CCITT
|
|
41
|
+
# Byte -> byte with its bits reversed (FillOrder 2), as String#tr tables.
|
|
42
|
+
REVERSE_FROM = Image::TR_ESCAPE.call((0..255).to_a)
|
|
43
|
+
REVERSE_TO = Image::TR_ESCAPE.call((0..255).map { |b| format("%08b", b).reverse.to_i(2) })
|
|
44
|
+
|
|
45
|
+
class << self
|
|
46
|
+
# Decodes a CCITT stream to an Image (see the module documentation
|
|
47
|
+
# for the parameters). end_of_block and damaged_rows_before_error
|
|
48
|
+
# are accepted for PDF compatibility; decoding is always lenient.
|
|
49
|
+
# rubocop:disable Lint/UnusedMethodArgument
|
|
50
|
+
def decode(data, columns:, rows: nil, k: -1, black_is_1: false, encoded_byte_align: false, fill_order: 1,
|
|
51
|
+
end_of_line: false, end_of_block: true, damaged_rows_before_error: 0)
|
|
52
|
+
raise ImageError, "CCITT data must be a String" unless data.is_a?(String)
|
|
53
|
+
|
|
54
|
+
rows = nil if rows.is_a?(Integer) && rows.zero?
|
|
55
|
+
check_size(columns, rows)
|
|
56
|
+
data = data.b unless data.encoding == Encoding::BINARY
|
|
57
|
+
data = data.tr(REVERSE_FROM, REVERSE_TO) if fill_order == 2
|
|
58
|
+
k = -1 unless k.is_a?(Integer)
|
|
59
|
+
pixels = QrReaderKernels.ccitt_decode(data, columns, rows || 0, k.clamp(-1, 1), encoded_byte_align ? true : false,
|
|
60
|
+
end_of_line ? true : false, black_is_1 ? true : false,
|
|
61
|
+
MAX_PIXELS / columns)
|
|
62
|
+
Image.new(columns, pixels.bytesize / columns, pixels)
|
|
63
|
+
end
|
|
64
|
+
# rubocop:enable Lint/UnusedMethodArgument
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def check_size(columns, rows)
|
|
69
|
+
unless columns.is_a?(Integer) && columns.positive? && (rows.nil? || (rows.is_a?(Integer) && rows.positive?))
|
|
70
|
+
raise ImageError, "invalid CCITT dimensions #{columns.inspect}x#{rows.inspect}"
|
|
71
|
+
end
|
|
72
|
+
return if columns * (rows || 1) <= MAX_PIXELS
|
|
73
|
+
|
|
74
|
+
raise UnsupportedImageError, "CCITT image too large (#{columns}x#{rows}, limit #{MAX_PIXELS} pixels)"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private_constant :REVERSE_FROM, :REVERSE_TO
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|