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,272 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module QrReader
|
|
4
|
+
module Formats
|
|
5
|
+
# GIF decoder (87a/89a). Decodes the first frame only, drawn on a white
|
|
6
|
+
# canvas of the logical screen size (grown to fit the frame if the
|
|
7
|
+
# screen is smaller); transparent pixels (graphic control extension)
|
|
8
|
+
# show the white canvas. Global and local palettes and interlacing are
|
|
9
|
+
# supported.
|
|
10
|
+
module GIF
|
|
11
|
+
# Interlaced row order: [first row, step] of the four passes.
|
|
12
|
+
INTERLACE_PASSES = [[0, 8], [4, 8], [2, 4], [1, 2]].freeze
|
|
13
|
+
|
|
14
|
+
# One-byte Strings, the initial LZW dictionary entries.
|
|
15
|
+
BYTES = Array.new(256) { |b| b.chr.b.freeze }.freeze
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
def decode(bytes)
|
|
19
|
+
Decoder.new(Support.binary(bytes)).decode
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def dimensions(bytes)
|
|
23
|
+
Decoder.new(Support.binary(bytes)).dimensions
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Reads the stream up to the first image descriptor on creation.
|
|
28
|
+
class Decoder
|
|
29
|
+
def initialize(bytes)
|
|
30
|
+
raise ImageError, "not a GIF file" unless GIF_MAGIC.match?(bytes)
|
|
31
|
+
raise ImageError, "truncated GIF header" if bytes.bytesize < 13
|
|
32
|
+
|
|
33
|
+
@bytes = bytes
|
|
34
|
+
screen_w, screen_h, flags = bytes.unpack("vvC", offset: 6)
|
|
35
|
+
pos = 13
|
|
36
|
+
if flags.anybits?(0x80)
|
|
37
|
+
@palette = read_palette(pos, flags)
|
|
38
|
+
pos += @palette.bytesize
|
|
39
|
+
end
|
|
40
|
+
read_first_image(pos)
|
|
41
|
+
@canvas_w = [screen_w, @left + @frame_w].max
|
|
42
|
+
@canvas_h = [screen_h, @top + @frame_h].max
|
|
43
|
+
Support.check_dimensions(@canvas_w, @canvas_h, "GIF")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def dimensions
|
|
47
|
+
[@canvas_w, @canvas_h]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def decode
|
|
51
|
+
n = @frame_w * @frame_h
|
|
52
|
+
return Image.new(@canvas_w, @canvas_h, WHITE * (@canvas_w * @canvas_h)) if n.zero?
|
|
53
|
+
|
|
54
|
+
min_size = @bytes.getbyte(@data_pos) or raise ImageError, "truncated GIF image data"
|
|
55
|
+
indices = LZW.new(read_sub_blocks(@data_pos + 1), min_size).decode(n)
|
|
56
|
+
pixels = indices.empty? ? WHITE * n : Support.translate!(indices, ALL_BYTES, lumas)
|
|
57
|
+
pixels << (WHITE * (n - pixels.bytesize)) if pixels.bytesize < n # stream ended early
|
|
58
|
+
pixels = deinterlace(pixels) if @interlaced
|
|
59
|
+
Image.new(@canvas_w, @canvas_h, compose(pixels))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def read_palette(pos, flags)
|
|
65
|
+
size = 3 << ((flags & 7) + 1)
|
|
66
|
+
raise ImageError, "truncated GIF palette" if pos + size > @bytes.bytesize
|
|
67
|
+
|
|
68
|
+
@bytes.byteslice(pos, size)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Skips extensions (remembering the transparent colour of a graphic
|
|
72
|
+
# control extension) up to the first image descriptor. Stray bytes
|
|
73
|
+
# between blocks are skipped, as ImageMagick, Pillow and browsers do.
|
|
74
|
+
def read_first_image(pos)
|
|
75
|
+
bytes = @bytes
|
|
76
|
+
loop do
|
|
77
|
+
case bytes.getbyte(pos)
|
|
78
|
+
when 0x2C
|
|
79
|
+
raise ImageError, "truncated GIF image descriptor" if pos + 10 > bytes.bytesize
|
|
80
|
+
|
|
81
|
+
@left, @top, @frame_w, @frame_h, flags = bytes.unpack("vvvvC", offset: pos + 1)
|
|
82
|
+
@interlaced = flags.anybits?(0x40)
|
|
83
|
+
pos += 10
|
|
84
|
+
if flags.anybits?(0x80)
|
|
85
|
+
@palette = read_palette(pos, flags)
|
|
86
|
+
pos += @palette.bytesize
|
|
87
|
+
end
|
|
88
|
+
@data_pos = pos
|
|
89
|
+
return
|
|
90
|
+
when 0x21
|
|
91
|
+
label = bytes.getbyte(pos + 1)
|
|
92
|
+
if label == 0xF9 && bytes.getbyte(pos + 2) == 4 && pos + 7 <= bytes.bytesize
|
|
93
|
+
@transparent = (bytes.getbyte(pos + 6) if bytes.getbyte(pos + 3).anybits?(1))
|
|
94
|
+
end
|
|
95
|
+
pos = skip_sub_blocks(pos + 2)
|
|
96
|
+
when 0x3B then raise ImageError, "GIF contains no image"
|
|
97
|
+
when nil then raise ImageError, "truncated GIF (no image)"
|
|
98
|
+
else pos += 1
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def skip_sub_blocks(pos)
|
|
104
|
+
bytes = @bytes
|
|
105
|
+
while (n = bytes.getbyte(pos))
|
|
106
|
+
return pos + 1 if n.zero?
|
|
107
|
+
|
|
108
|
+
pos += n + 1
|
|
109
|
+
end
|
|
110
|
+
raise ImageError, "truncated GIF extension"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Concatenated data sub-blocks (shorter if the file is truncated).
|
|
114
|
+
def read_sub_blocks(pos)
|
|
115
|
+
bytes = @bytes
|
|
116
|
+
data = Support.buffer(0)
|
|
117
|
+
while (n = bytes.getbyte(pos)) && n.positive?
|
|
118
|
+
data << bytes.byteslice(pos + 1, n)
|
|
119
|
+
pos += n + 1
|
|
120
|
+
end
|
|
121
|
+
data
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Palette luminance per index; the transparent index is white.
|
|
125
|
+
# Without any palette the indices are used as gray levels.
|
|
126
|
+
def lumas
|
|
127
|
+
lumas = @palette ? Support.palette_lumas(@palette) : Array.new(256) { |i| i }
|
|
128
|
+
lumas[@transparent] = 255 if @transparent
|
|
129
|
+
lumas
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def deinterlace(pixels)
|
|
133
|
+
w = @frame_w
|
|
134
|
+
h = @frame_h
|
|
135
|
+
rows = Array.new(h)
|
|
136
|
+
src = 0
|
|
137
|
+
INTERLACE_PASSES.each do |first, step|
|
|
138
|
+
y = first
|
|
139
|
+
while y < h
|
|
140
|
+
rows[y] = pixels.byteslice(src, w)
|
|
141
|
+
src += w
|
|
142
|
+
y += step
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
rows.join
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Places the frame on the white canvas.
|
|
149
|
+
def compose(pixels)
|
|
150
|
+
cw = @canvas_w
|
|
151
|
+
fw = @frame_w
|
|
152
|
+
return pixels if fw == cw && @frame_h == @canvas_h
|
|
153
|
+
|
|
154
|
+
canvas = WHITE * (cw * @canvas_h)
|
|
155
|
+
r = 0
|
|
156
|
+
while r < @frame_h
|
|
157
|
+
canvas.bytesplice((@top + r) * cw + @left, fw, pixels.byteslice(r * fw, fw))
|
|
158
|
+
r += 1
|
|
159
|
+
end
|
|
160
|
+
canvas
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
private_constant :Decoder
|
|
164
|
+
|
|
165
|
+
# Variable-width LZW decoder (LSB-first codes, 12 bits max, deferred
|
|
166
|
+
# clear). Dictionary entries are Strings, so each code costs one
|
|
167
|
+
# C-level copy. Decoding resumes in slices of SLICE output bytes, with
|
|
168
|
+
# the state in instance variables in between: YJIT only compiles
|
|
169
|
+
# methods that are entered repeatedly, never a single long loop.
|
|
170
|
+
class LZW
|
|
171
|
+
SLICE = 2048
|
|
172
|
+
|
|
173
|
+
def initialize(data, min_size)
|
|
174
|
+
raise ImageError, "invalid GIF LZW code size #{min_size}" unless min_size.between?(1, 8)
|
|
175
|
+
|
|
176
|
+
@data = data
|
|
177
|
+
@min_size = min_size
|
|
178
|
+
@clear = 1 << min_size
|
|
179
|
+
@dict = BYTES.first(@clear)
|
|
180
|
+
@size = min_size + 1
|
|
181
|
+
@avail = @clear + 2
|
|
182
|
+
@prev = nil
|
|
183
|
+
@buf = @bits = @pos = 0
|
|
184
|
+
@finished = false
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Up to +n+ palette indices; fewer only if an end code arrives early.
|
|
188
|
+
def decode(n)
|
|
189
|
+
# The k-th code after a clear yields at most k bytes: reserve no
|
|
190
|
+
# more than the data can produce (a few bytes may claim 2^27 pixels).
|
|
191
|
+
codes = @data.bytesize * 8 / (@min_size + 1) + 1
|
|
192
|
+
out = Support.buffer([n, codes * (codes + 1) / 2].min)
|
|
193
|
+
decode_slice(out, [out.bytesize + SLICE, n].min) until @finished || out.bytesize >= n
|
|
194
|
+
out.bytesize > n ? out.byteslice(0, n) : out
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
private
|
|
198
|
+
|
|
199
|
+
def decode_slice(out, target)
|
|
200
|
+
data = @data
|
|
201
|
+
len = data.bytesize
|
|
202
|
+
dict = @dict
|
|
203
|
+
clear = @clear
|
|
204
|
+
size = @size
|
|
205
|
+
mask = (1 << size) - 1
|
|
206
|
+
avail = @avail
|
|
207
|
+
prev = @prev
|
|
208
|
+
buf = @buf
|
|
209
|
+
bits = @bits
|
|
210
|
+
pos = @pos
|
|
211
|
+
while out.bytesize < target
|
|
212
|
+
while bits < size
|
|
213
|
+
raise ImageError, "truncated GIF image data" if pos >= len
|
|
214
|
+
|
|
215
|
+
buf |= data.getbyte(pos) << bits
|
|
216
|
+
bits += 8
|
|
217
|
+
pos += 1
|
|
218
|
+
end
|
|
219
|
+
code = buf & mask
|
|
220
|
+
buf >>= size
|
|
221
|
+
bits -= size
|
|
222
|
+
|
|
223
|
+
if code == clear
|
|
224
|
+
size = @min_size + 1
|
|
225
|
+
mask = (1 << size) - 1
|
|
226
|
+
avail = clear + 2
|
|
227
|
+
prev = nil
|
|
228
|
+
next
|
|
229
|
+
end
|
|
230
|
+
if code == clear + 1 # end of information
|
|
231
|
+
@finished = true
|
|
232
|
+
break
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
added = nil
|
|
236
|
+
if prev.nil? # first code after a clear: a literal
|
|
237
|
+
raise ImageError, "corrupt GIF LZW data" unless code < clear
|
|
238
|
+
|
|
239
|
+
entry = dict[code]
|
|
240
|
+
elsif code < avail
|
|
241
|
+
entry = dict[code]
|
|
242
|
+
added = prev + BYTES[entry.getbyte(0)]
|
|
243
|
+
elsif code == avail # the entry being defined: prev + its own first byte
|
|
244
|
+
entry = added = prev + BYTES[prev.getbyte(0)]
|
|
245
|
+
else
|
|
246
|
+
raise ImageError, "corrupt GIF LZW data"
|
|
247
|
+
end
|
|
248
|
+
out << entry
|
|
249
|
+
if added && avail < 4096
|
|
250
|
+
dict[avail] = added
|
|
251
|
+
avail += 1
|
|
252
|
+
end
|
|
253
|
+
# Grow once the next free code needs another bit (with minimum
|
|
254
|
+
# code size 1 that is already the case after the first literal).
|
|
255
|
+
if avail > mask && size < 12
|
|
256
|
+
size += 1
|
|
257
|
+
mask = (1 << size) - 1
|
|
258
|
+
end
|
|
259
|
+
prev = entry
|
|
260
|
+
end
|
|
261
|
+
@size = size
|
|
262
|
+
@avail = avail
|
|
263
|
+
@prev = prev
|
|
264
|
+
@buf = buf
|
|
265
|
+
@bits = bits
|
|
266
|
+
@pos = pos
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
private_constant :LZW
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../kernels/jpeg"
|
|
4
|
+
|
|
5
|
+
module QrReader
|
|
6
|
+
module Formats
|
|
7
|
+
# Pure-Ruby JPEG decoder that produces luminance only.
|
|
8
|
+
#
|
|
9
|
+
# JPEG.decode(bytes) # => QrReader::Image
|
|
10
|
+
# JPEG.decode(bytes, scale: 4) # => ceil(w/4) x ceil(h/4), like libjpeg's scaled decoding
|
|
11
|
+
# JPEG.dimensions(bytes) # => [width, height]
|
|
12
|
+
#
|
|
13
|
+
# # Two resolutions for about the price of one decode:
|
|
14
|
+
# small, coefficients = JPEG.decode_keeping_coefficients(bytes, scale: 4)
|
|
15
|
+
# full = coefficients.image # same pixels as JPEG.decode(bytes)
|
|
16
|
+
#
|
|
17
|
+
# Supported: baseline (SOF0), extended sequential Huffman (SOF1) and
|
|
18
|
+
# progressive (SOF2) 8-bit JPEGs with any sampling factors, restart
|
|
19
|
+
# intervals, 8/16-bit quantisation tables, 1 component (gray), 3 components
|
|
20
|
+
# (YCbCr, or RGB via Adobe APP14 / component ids "RGB") and 4 components
|
|
21
|
+
# (Adobe CMYK and YCCK). Arithmetic coding, lossless, hierarchical and
|
|
22
|
+
# 12-bit and JPEG-LS files raise UnsupportedImageError. The output
|
|
23
|
+
# matches libjpeg-turbo's default "islow" IDCT exactly (as built with
|
|
24
|
+
# SIMD, which saturates out-of-range samples), including the reduced
|
|
25
|
+
# IDCTs, per-component DCT scaling and upsampling used for scale 2, 4
|
|
26
|
+
# and 8. Truncated scan data decodes what is present (missing blocks
|
|
27
|
+
# stay mid-gray); damaged restart intervals are resynchronised by their
|
|
28
|
+
# marker numbers; other corruption raises ImageError. EXIF orientation
|
|
29
|
+
# is ignored.
|
|
30
|
+
#
|
|
31
|
+
# The decoder itself is the jpeg kernel (lib/qr_reader/kernels/jpeg.rb,
|
|
32
|
+
# see DESIGN.md "Kernels"), which the optional qr_reader-native gem
|
|
33
|
+
# compiles to C; this module validates arguments and wraps its results.
|
|
34
|
+
module JPEG
|
|
35
|
+
# Largest supported output (64 Mi pixels, e.g. 8192 x 8192); bigger
|
|
36
|
+
# images need a larger +scale+.
|
|
37
|
+
MAX_PIXELS = QrReaderKernels::JPEG_MAX_PIXELS
|
|
38
|
+
# Decoded scans after which the rest of a file is ignored.
|
|
39
|
+
MAX_SCANS = QrReaderKernels::JPEG_MAX_SCANS
|
|
40
|
+
|
|
41
|
+
# Exceptions that only a bug triggered by corrupt input could raise;
|
|
42
|
+
# they are reported as ImageError.
|
|
43
|
+
INTERNAL_ERRORS = [NoMethodError, TypeError, ArgumentError, RangeError, IndexError, ZeroDivisionError,
|
|
44
|
+
EncodingError, FrozenError, FloatDomainError].freeze
|
|
45
|
+
|
|
46
|
+
class << self
|
|
47
|
+
# Decodes JPEG +bytes+ to a luminance Image. +scale+ (1, 2, 4 or 8)
|
|
48
|
+
# decodes at reduced size: ceil(width / scale) x ceil(height / scale).
|
|
49
|
+
def decode(bytes, scale: 1)
|
|
50
|
+
scale = check_scale(scale)
|
|
51
|
+
bytes = binary(bytes)
|
|
52
|
+
data = guard("data") { QrReaderKernels.jpeg_luma(bytes, scale) }
|
|
53
|
+
image(bytes, scale, data)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Like #decode, and also returns the entropy-decoded coefficients of
|
|
57
|
+
# the luminance components, from which Coefficients#image renders
|
|
58
|
+
# any other scale with IDCTs only (no second entropy decoding):
|
|
59
|
+
# [Image, Coefficients]. The coefficients take about 1 to 2 bytes
|
|
60
|
+
# per pixel for photos and much less for scanned pages.
|
|
61
|
+
def decode_keeping_coefficients(bytes, scale: 1)
|
|
62
|
+
scale = check_scale(scale)
|
|
63
|
+
bytes = binary(bytes)
|
|
64
|
+
data, store = guard("data") { QrReaderKernels.jpeg_luma_keep(bytes, scale) }
|
|
65
|
+
image = image(bytes, scale, data)
|
|
66
|
+
width, height = dimensions(bytes)
|
|
67
|
+
# Images too large to render at full size keep no store.
|
|
68
|
+
[image, Coefficients.new(width, height, store.empty? ? bytes : nil, store)]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# [width, height] from the frame header, without decoding.
|
|
72
|
+
def dimensions(bytes)
|
|
73
|
+
bytes = binary(bytes)
|
|
74
|
+
guard("header") { QrReaderKernels.jpeg_info(bytes) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# The validated +scale+ as an Integer (internal).
|
|
78
|
+
def check_scale(scale)
|
|
79
|
+
unless scale.is_a?(Numeric) && [1, 2, 4, 8].include?(scale)
|
|
80
|
+
raise ArgumentError, "scale must be 1, 2, 4 or 8, not #{scale.inspect}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
scale.to_i # also accepts 2.0
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Runs a kernel call, reporting internal errors as ImageError
|
|
87
|
+
# (internal).
|
|
88
|
+
def guard(what)
|
|
89
|
+
yield
|
|
90
|
+
rescue *INTERNAL_ERRORS => e
|
|
91
|
+
# Safety net: garbage input must only ever raise ImageError.
|
|
92
|
+
raise ImageError, "corrupt JPEG #{what} (internal #{e.class}: #{e.message})"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# An Image of the luma bytes the block returns (internal).
|
|
96
|
+
def luma(width, height, scale)
|
|
97
|
+
w = (width + scale - 1) / scale
|
|
98
|
+
h = (height + scale - 1) / scale
|
|
99
|
+
data = guard("data") { yield }
|
|
100
|
+
raise ImageError, "corrupt JPEG data (decoded #{data.bytesize} bytes)" unless data.bytesize == w * h
|
|
101
|
+
|
|
102
|
+
Image.new(w, h, data)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
# The Image of luma +data+ decoded from +bytes+ at +scale+.
|
|
108
|
+
def image(bytes, scale, data)
|
|
109
|
+
width, height = dimensions(bytes)
|
|
110
|
+
luma(width, height, scale) { data }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def binary(bytes)
|
|
114
|
+
raise ImageError, "JPEG data must be a String" unless bytes.is_a?(String)
|
|
115
|
+
|
|
116
|
+
bytes.encoding == Encoding::BINARY ? bytes : bytes.b
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Entropy-decoded coefficients of a JPEG's luminance components (see
|
|
121
|
+
# JPEG.decode_keeping_coefficients), rendered at any scale by #image.
|
|
122
|
+
class Coefficients
|
|
123
|
+
attr_reader :width, :height
|
|
124
|
+
|
|
125
|
+
def initialize(width, height, bytes, store) # :nodoc:
|
|
126
|
+
@width = width
|
|
127
|
+
@height = height
|
|
128
|
+
@bytes = bytes
|
|
129
|
+
@store = store
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# The image at 1/+scale+ (1, 2, 4 or 8) of the size: the same pixels
|
|
133
|
+
# as JPEG.decode(bytes, scale: scale).
|
|
134
|
+
def image(scale: 1)
|
|
135
|
+
scale = JPEG.check_scale(scale)
|
|
136
|
+
JPEG.luma(@width, @height, scale) do
|
|
137
|
+
if @bytes
|
|
138
|
+
QrReaderKernels.jpeg_luma(@bytes, scale)
|
|
139
|
+
else
|
|
140
|
+
QrReaderKernels.jpeg_render(@store, scale)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Memory held by the coefficients, in bytes.
|
|
146
|
+
def bytesize
|
|
147
|
+
@store.bytesize
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|