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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +16 -0
  3. data/LICENSE.txt +21 -0
  4. data/NOTICE +14 -0
  5. data/README.md +222 -0
  6. data/exe/qr_reader +66 -0
  7. data/lib/qr_reader/binarizer.rb +170 -0
  8. data/lib/qr_reader/bit_matrix.rb +120 -0
  9. data/lib/qr_reader/decoder/bitstream_parser.rb +307 -0
  10. data/lib/qr_reader/decoder/character_set.rb +163 -0
  11. data/lib/qr_reader/decoder/codewords.rb +164 -0
  12. data/lib/qr_reader/decoder/data_mask.rb +57 -0
  13. data/lib/qr_reader/decoder/format_reader.rb +204 -0
  14. data/lib/qr_reader/decoder/function_patterns.rb +62 -0
  15. data/lib/qr_reader/decoder/galois_field.rb +69 -0
  16. data/lib/qr_reader/decoder/reed_solomon.rb +312 -0
  17. data/lib/qr_reader/decoder.rb +241 -0
  18. data/lib/qr_reader/detector/alignment_grid.rb +127 -0
  19. data/lib/qr_reader/detector/alignment_pattern_finder.rb +191 -0
  20. data/lib/qr_reader/detector/candidate/dimension.rb +237 -0
  21. data/lib/qr_reader/detector/candidate/strokes.rb +126 -0
  22. data/lib/qr_reader/detector/candidate.rb +436 -0
  23. data/lib/qr_reader/detector/finder_pattern.rb +63 -0
  24. data/lib/qr_reader/detector/finder_pattern_finder.rb +199 -0
  25. data/lib/qr_reader/detector/geometry.rb +118 -0
  26. data/lib/qr_reader/detector/grid_sampler.rb +350 -0
  27. data/lib/qr_reader/detector/pattern_sets.rb +235 -0
  28. data/lib/qr_reader/detector/perspective_transform.rb +117 -0
  29. data/lib/qr_reader/detector/pure_detector.rb +197 -0
  30. data/lib/qr_reader/detector.rb +215 -0
  31. data/lib/qr_reader/errors.rb +14 -0
  32. data/lib/qr_reader/format_information.rb +79 -0
  33. data/lib/qr_reader/formats/bmp.rb +434 -0
  34. data/lib/qr_reader/formats/ccitt.rb +81 -0
  35. data/lib/qr_reader/formats/gif.rb +272 -0
  36. data/lib/qr_reader/formats/jpeg.rb +152 -0
  37. data/lib/qr_reader/formats/pdf.rb +2935 -0
  38. data/lib/qr_reader/formats/png.rb +311 -0
  39. data/lib/qr_reader/formats/pnm.rb +286 -0
  40. data/lib/qr_reader/formats/tiff.rb +1345 -0
  41. data/lib/qr_reader/formats.rb +279 -0
  42. data/lib/qr_reader/image.rb +231 -0
  43. data/lib/qr_reader/kernels/binarizer.rb +475 -0
  44. data/lib/qr_reader/kernels/ccitt.rb +554 -0
  45. data/lib/qr_reader/kernels/detector.rb +587 -0
  46. data/lib/qr_reader/kernels/image.rb +65 -0
  47. data/lib/qr_reader/kernels/jpeg.rb +3421 -0
  48. data/lib/qr_reader/kernels/lzw.rb +173 -0
  49. data/lib/qr_reader/kernels/pixels.rb +116 -0
  50. data/lib/qr_reader/kernels/png.rb +1138 -0
  51. data/lib/qr_reader/kernels.rb +14 -0
  52. data/lib/qr_reader/native_kernels.rb +130 -0
  53. data/lib/qr_reader/reader.rb +348 -0
  54. data/lib/qr_reader/result.rb +49 -0
  55. data/lib/qr_reader/symbol_version.rb +173 -0
  56. data/lib/qr_reader/version.rb +5 -0
  57. data/lib/qr_reader.rb +77 -0
  58. 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