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,279 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QrReader
4
+ # Image file decoders. Each one turns encoded bytes into an 8-bit luminance
5
+ # QrReader::Image: colour is converted with Image.luma and alpha is composited
6
+ # over white, exactly like Image.from_pixels, so every format agrees.
7
+ #
8
+ # QrReader::Formats.decode(File.binread("code.png")) # => #<QrReader::Image 290x290>
9
+ # QrReader::Formats.format_of(bytes) # => :png
10
+ # QrReader::Formats.dimensions(bytes) # => [290, 290]
11
+ #
12
+ # Unknown data raises ImageError; formats that are recognised but not
13
+ # supported (WebP, HEIC, AVIF, JPEG XL, ...) raise UnsupportedImageError.
14
+ module Formats
15
+ # Largest image, in pixels, that the decoders will allocate. Larger
16
+ # (or maliciously declared) sizes raise UnsupportedImageError.
17
+ MAX_PIXELS = 1 << 27
18
+
19
+ GIF_MAGIC = /\AGIF8[79]a/n
20
+ PNM_MAGIC = /\AP[1-7][\s#]/n
21
+ TIFF_MAGIC = ["II*\0", "MM\0*", "II+\0", "MM\0+"].map(&:b).freeze
22
+ WHITESPACE = " \t\n\v\f\r"
23
+ ALL_BYTES = (0..255).to_a.freeze
24
+ WHITE = "\xFF".b
25
+
26
+ # Recognised formats that Formats.decode does not read: [pattern, name,
27
+ # module that reads them (nil if none)].
28
+ UNSUPPORTED = [
29
+ [/\ARIFF....WEBP/mn, "WebP"],
30
+ [/\A....ftyp(?:avif|avis)/mn, "AVIF"],
31
+ [/\A....ftyp(?:heic|heix|hevc|hevx|heim|heis|mif1|msf1)/mn, "HEIC/HEIF"],
32
+ [/\A(?:\xFF\x0A|\0\0\0\x0CJXL \r\n\x87\n)/n, "JPEG XL"],
33
+ [/\A(?:\0\0\0\x0CjP \r\n\x87\n|\xFF\x4F\xFF\x51)/n, "JPEG 2000"],
34
+ [/\AP[Ff][\s#]/n, "PFM (floating point PNM)"],
35
+ [/\A8BPS/n, "Photoshop PSD"],
36
+ [/\A\s*<(?:\?xml|svg|!DOCTYPE svg)/n, "SVG"]
37
+ ].freeze
38
+ private_constant :GIF_MAGIC, :PNM_MAGIC, :TIFF_MAGIC, :WHITESPACE, :ALL_BYTES, :WHITE, :UNSUPPORTED
39
+
40
+ class << self
41
+ # Decodes an encoded image (format sniffed from its magic bytes) into a
42
+ # luminance Image. +scale+ (1, 2, 4 or 8) is honoured by JPEG only.
43
+ def decode(bytes, scale: 1)
44
+ bytes = Support.binary(bytes)
45
+ case format_of(bytes)
46
+ when :png then PNG.decode(bytes)
47
+ when :jpeg then JPEG.decode(bytes, scale: scale)
48
+ when :gif then GIF.decode(bytes)
49
+ when :bmp then BMP.decode(bytes)
50
+ when :pnm then PNM.decode(bytes)
51
+ when :tiff then TIFF.decode(bytes)
52
+ when :pdf then first_pdf_image(bytes)
53
+ else raise unrecognized(bytes)
54
+ end
55
+ end
56
+
57
+ # :png, :jpeg, :gif, :bmp, :pnm, :tiff, :pdf, or nil when the magic
58
+ # bytes are not those of a supported format. TIFF and PDF are
59
+ # multi-page: Formats.decode returns their first page or image.
60
+ def format_of(bytes)
61
+ return nil unless bytes.is_a?(String) && bytes.bytesize >= 2
62
+
63
+ head = bytes.byteslice(0, 32).b
64
+ b0 = head.getbyte(0)
65
+ if b0 == 0x89 then :png if head.start_with?(PNG::SIGNATURE)
66
+ elsif b0 == 0xFF then :jpeg if head.getbyte(1) == 0xD8 && head.getbyte(2) == 0xFF
67
+ elsif b0 == 0x47 then :gif if GIF_MAGIC.match?(head)
68
+ elsif b0 == 0x42 then :bmp if BMP.bmp?(head)
69
+ elsif b0 == 0x50 then :pnm if PNM_MAGIC.match?(head)
70
+ elsif b0 == 0x49 || b0 == 0x4D then :tiff if TIFF_MAGIC.include?(head.byteslice(0, 4))
71
+ elsif b0 == 0x25 then :pdf if head.start_with?("%PDF-")
72
+ end
73
+ end
74
+
75
+ # [width, height] of an encoded image, read from its header only (the
76
+ # same size Formats.decode would produce at scale 1).
77
+ def dimensions(bytes)
78
+ bytes = Support.binary(bytes)
79
+ case format_of(bytes)
80
+ when :png then PNG.dimensions(bytes)
81
+ when :jpeg then JPEG.dimensions(bytes)
82
+ when :gif then GIF.dimensions(bytes)
83
+ when :bmp then BMP.dimensions(bytes)
84
+ when :pnm then PNM.dimensions(bytes)
85
+ when :tiff then TIFF.dimensions(bytes)
86
+ when :pdf then first_pdf_image(bytes).then { |image| [image.width, image.height] }
87
+ else raise unrecognized(bytes)
88
+ end
89
+ end
90
+
91
+ private
92
+
93
+ def first_pdf_image(bytes)
94
+ PDF.each_image(bytes) { |image, _info| return image }
95
+ raise UnsupportedImageError, "PDF contains no decodable raster image"
96
+ end
97
+
98
+ def unrecognized(bytes)
99
+ head = bytes.byteslice(0, 64)
100
+ UNSUPPORTED.each do |pattern, name, container|
101
+ next unless pattern.match?(head)
102
+
103
+ where = " here (QrReader.read and QrReader::Formats::#{container} read them)" if container
104
+ return UnsupportedImageError.new(
105
+ "#{name} images are not supported#{where}; convert to PNG, JPEG, GIF, BMP or PNM first, " \
106
+ "e.g. `magick input output.png`"
107
+ )
108
+ end
109
+ return ImageError.new("empty image data") if bytes.empty?
110
+
111
+ ImageError.new("unrecognised image format (magic bytes #{head.byteslice(0, 8).unpack1('H*')})")
112
+ end
113
+ end
114
+
115
+ # Helpers shared by the decoders.
116
+ module Support
117
+ module_function
118
+
119
+ # An empty binary String with room for +size+ bytes. Hot methods call
120
+ # this instead of String.new(capacity:, encoding:) themselves: YJIT
121
+ # side-exits on that keyword-argument call, and the caller's loop
122
+ # would then run in the interpreter for the rest of the call.
123
+ def buffer(size)
124
+ String.new(capacity: size, encoding: Encoding::BINARY)
125
+ end
126
+
127
+ # +bytes+ as a binary String (decoders work on bytes, never characters).
128
+ def binary(bytes)
129
+ raise ImageError, "expected a String of encoded image data, got #{bytes.class}" unless bytes.is_a?(String)
130
+
131
+ bytes.encoding == Encoding::BINARY ? bytes : bytes.b
132
+ end
133
+
134
+ # Validates declared image dimensions before anything is allocated.
135
+ def check_dimensions(width, height, format)
136
+ raise ImageError, "invalid #{format} dimensions #{width}x#{height}" unless width.positive? && height.positive?
137
+ return if width * height <= MAX_PIXELS
138
+
139
+ raise UnsupportedImageError, "#{format} image too large (#{width}x#{height}, limit #{MAX_PIXELS} pixels)"
140
+ end
141
+
142
+ # Composites luminance +v+ with alpha +a+ over white (Image.from_pixels).
143
+ def over_white(v, a)
144
+ a == 255 ? v : (v * a + 255 * (255 - a) + 127) / 255
145
+ end
146
+
147
+ # over_white for every alpha and luminance as a 64 KiB String indexed
148
+ # by a << 8 | v: one getbyte instead of a division per pixel. Built on
149
+ # first use (about 65k iterations), then shared.
150
+ def over_white_table
151
+ @over_white_table ||= begin
152
+ table = "\0".b * 65_536
153
+ a = 0
154
+ while a < 256
155
+ fill_over_white_row(table, a)
156
+ a += 1
157
+ end
158
+ table.freeze
159
+ end
160
+ end
161
+
162
+ def fill_over_white_row(table, a)
163
+ base = a << 8
164
+ rest = 255 * (255 - a) + 127
165
+ v = 0
166
+ while v < 256
167
+ table.setbyte(base | v, (v * a + rest) / 255)
168
+ v += 1
169
+ end
170
+ end
171
+
172
+ # Translates every byte of +str+ in place: from_bytes[i] -> to_bytes[i].
173
+ def translate!(str, from_bytes, to_bytes)
174
+ str.tr!(Image::TR_ESCAPE.call(from_bytes), Image::TR_ESCAPE.call(to_bytes))
175
+ str
176
+ end
177
+
178
+ # Luminance of 256 palette entries; +palette+ holds packed colour
179
+ # triples (+order+ :rgb or :bgr, +entry_size+ bytes apart), +alpha+ is
180
+ # an optional String of per-entry alpha. Missing entries are black.
181
+ def palette_lumas(palette, entry_size: 3, order: :rgb, alpha: nil)
182
+ count = [palette.bytesize / entry_size, 256].min
183
+ ro, bo = order == :rgb ? [0, 2] : [2, 0]
184
+ lumas = Array.new(256, 0)
185
+ i = 0
186
+ while i < count
187
+ o = i * entry_size
188
+ v = Image.luma(palette.getbyte(o + ro), palette.getbyte(o + 1), palette.getbyte(o + bo))
189
+ a = alpha&.getbyte(i)
190
+ lumas[i] = a ? over_white(v, a) : v
191
+ i += 1
192
+ end
193
+ lumas
194
+ end
195
+ end
196
+ private_constant :Support
197
+
198
+ # Per-row pixel converters shared by the decoders: they read +n+ pixels
199
+ # of +stride+ bytes starting at byte +off+ of +src+ and append one
200
+ # luminance byte per pixel to +out+, with the arithmetic of
201
+ # Image.from_pixels inlined. Channel offsets select the layout (BGR,
202
+ # BGRX, or the high bytes of big-endian 16-bit samples). Decoders call
203
+ # them once per row so that YJIT compiles them: it never compiles a
204
+ # method that is entered only once, however long its loop runs.
205
+ module Rows
206
+ # Byte -> its four 2-bit samples as bytes 0..3.
207
+ TWO_BIT = Array.new(256) { |b| [b >> 6, (b >> 4) & 3, (b >> 2) & 3, b & 3].pack("C4").freeze }.freeze
208
+
209
+ module_function
210
+
211
+ def rgb(src, off, n, out, ro = 0, go = 1, bo = 2, stride = 3)
212
+ i = off
213
+ stop = off + n * stride
214
+ while i < stop
215
+ out << ((306 * src.getbyte(i + ro) + 601 * src.getbyte(i + go) + 117 * src.getbyte(i + bo) + 0x200) >> 10)
216
+ i += stride
217
+ end
218
+ out
219
+ end
220
+
221
+ # Colour + alpha, composited over white through +table+
222
+ # (Support.over_white_table).
223
+ def rgba(src, off, n, out, table, ro = 0, go = 1, bo = 2, ao = 3, stride = 4)
224
+ i = off
225
+ stop = off + n * stride
226
+ while i < stop
227
+ v = (306 * src.getbyte(i + ro) + 601 * src.getbyte(i + go) + 117 * src.getbyte(i + bo) + 0x200) >> 10
228
+ out << table.getbyte((src.getbyte(i + ao) << 8) | v)
229
+ i += stride
230
+ end
231
+ out
232
+ end
233
+
234
+ # Gray + alpha, composited over white through +table+.
235
+ def gray_alpha(src, off, n, out, table, ao = 1, stride = 2)
236
+ i = off
237
+ stop = off + n * stride
238
+ while i < stop
239
+ out << table.getbyte((src.getbyte(i + ao) << 8) | src.getbyte(i))
240
+ i += stride
241
+ end
242
+ out
243
+ end
244
+
245
+ # The high byte of +n+ big-endian 16-bit samples. (A plain loop: it
246
+ # beats unpack("v*").pack("C*") by 6x with YJIT and 1.3x without.)
247
+ def high_bytes(src, off, n, out)
248
+ i = off
249
+ stop = off + 2 * n
250
+ while i < stop
251
+ out << src.getbyte(i)
252
+ i += 2
253
+ end
254
+ out
255
+ end
256
+
257
+ # 2-bit samples (MSB first) -> one byte 0..3 per pixel.
258
+ def unpack2(src, off, n, out)
259
+ full = n >> 2
260
+ i = off
261
+ stop = off + full
262
+ while i < stop
263
+ out << TWO_BIT[src.getbyte(i)]
264
+ i += 1
265
+ end
266
+ rest = n & 3
267
+ out << TWO_BIT[src.getbyte(stop)].byteslice(0, rest) if rest.positive?
268
+ out
269
+ end
270
+ end
271
+ private_constant :Rows
272
+ end
273
+ end
274
+
275
+ require_relative "formats/png"
276
+ require_relative "formats/gif"
277
+ require_relative "formats/bmp"
278
+ require_relative "formats/pnm"
279
+ require_relative "formats/jpeg"
@@ -0,0 +1,231 @@
1
+ # frozen_string_literal: true
2
+
3
+ module QrReader
4
+ # An 8-bit grayscale (luminance) image: one byte per pixel, row-major,
5
+ # 0 = black, 255 = white. +data+ is a binary String of width * height bytes.
6
+ #
7
+ # This is the common currency of the library: every image format decoder
8
+ # produces one and the detector consumes one.
9
+ class Image
10
+ attr_reader :width, :height, :data
11
+
12
+ # Channel layouts accepted by Image.from_pixels.
13
+ PIXEL_FORMATS = {
14
+ gray: 1, gray_alpha: 2, rgb: 3, rgba: 4, bgr: 3, bgra: 4, argb: 4, abgr: 4
15
+ }.freeze
16
+
17
+ # String#tr tables mapping every byte v to 255 - v ('\\', '-' and '^' escaped).
18
+ TR_ESCAPE = ->(bytes) { bytes.map { |b| c = b.chr.b; "\\-^".include?(c) ? "\\".b + c : c }.join.b.freeze }
19
+ INVERT_FROM = TR_ESCAPE.call((0..255).to_a)
20
+ INVERT_TO = TR_ESCAPE.call(255.downto(0).to_a)
21
+
22
+ # [red, green, blue, alpha] byte offsets within a pixel.
23
+ CHANNEL_OFFSETS = {
24
+ rgb: [0, 1, 2, nil], bgr: [2, 1, 0, nil], rgba: [0, 1, 2, 3], bgra: [2, 1, 0, 3],
25
+ argb: [1, 2, 3, 0], abgr: [3, 2, 1, 0], gray_alpha: []
26
+ }.freeze
27
+
28
+ def initialize(width, height, data)
29
+ unless width.is_a?(Integer) && height.is_a?(Integer) && width.positive? && height.positive?
30
+ raise ArgumentError, "invalid image size #{width.inspect}x#{height.inspect}"
31
+ end
32
+ unless data.bytesize == width * height
33
+ raise ArgumentError, "expected #{width * height} bytes of pixel data, got #{data.bytesize}"
34
+ end
35
+
36
+ @width = width
37
+ @height = height
38
+ @data = data.encoding == Encoding::BINARY ? data : data.b
39
+ end
40
+
41
+ class << self
42
+ def from_gray(width, height, data)
43
+ new(width, height, data)
44
+ end
45
+
46
+ # Builds an image from interleaved 8-bit samples. Alpha is composited
47
+ # over a white background (transparent QR codes are usually dark modules
48
+ # on a transparent background).
49
+ def from_pixels(width, height, data, format = :rgba)
50
+ channels = PIXEL_FORMATS.fetch(format) { raise ArgumentError, "unknown pixel format #{format.inspect}" }
51
+ expected = width * height * channels
52
+ raise ArgumentError, "expected #{expected} bytes of #{format} data, got #{data.bytesize}" if data.bytesize != expected
53
+ return new(width, height, data) if format == :gray
54
+
55
+ out = buffer(width * height)
56
+ ro, go, bo, ao = CHANNEL_OFFSETS.fetch(format)
57
+ stride = width * channels
58
+ y = 0
59
+ # One call per row: YJIT compiles methods, not loops, so a whole-image
60
+ # loop entered once would stay interpreted.
61
+ while y < height
62
+ if format == :gray_alpha
63
+ convert_gray_alpha_row(data, out, y * stride, width)
64
+ elsif ao
65
+ convert_rgba_row(data, out, y * stride, width, ro, go, bo, ao)
66
+ else
67
+ convert_rgb_row(data, out, y * stride, width, ro, go, bo)
68
+ end
69
+ y += 1
70
+ end
71
+ new(width, height, out)
72
+ end
73
+
74
+ def from_rgb(width, height, data)
75
+ from_pixels(width, height, data, :rgb)
76
+ end
77
+
78
+ def from_rgba(width, height, data)
79
+ from_pixels(width, height, data, :rgba)
80
+ end
81
+
82
+ # Rec. 601 luma with 10-bit fixed point weights (same as zxing-cpp).
83
+ def luma(r, g, b)
84
+ (306 * r + 601 * g + 117 * b + 0x200) >> 10
85
+ end
86
+
87
+ # Empty binary String with room for +size+ bytes.
88
+ def buffer(size)
89
+ String.new(capacity: size, encoding: Encoding::BINARY)
90
+ end
91
+
92
+ private
93
+
94
+ def convert_gray_alpha_row(src, out, j, n)
95
+ e = j + 2 * n
96
+ while j < e
97
+ v = src.getbyte(j)
98
+ a = src.getbyte(j + 1)
99
+ out << (a == 255 ? v : (v * a + 255 * (255 - a) + 127) / 255)
100
+ j += 2
101
+ end
102
+ end
103
+
104
+ def convert_rgb_row(src, out, j, n, ro, go, bo)
105
+ e = j + 3 * n
106
+ while j < e
107
+ out << ((306 * src.getbyte(j + ro) + 601 * src.getbyte(j + go) + 117 * src.getbyte(j + bo) + 0x200) >> 10)
108
+ j += 3
109
+ end
110
+ end
111
+
112
+ def convert_rgba_row(src, out, j, n, ro, go, bo, ao)
113
+ e = j + 4 * n
114
+ while j < e
115
+ v = (306 * src.getbyte(j + ro) + 601 * src.getbyte(j + go) + 117 * src.getbyte(j + bo) + 0x200) >> 10
116
+ a = src.getbyte(j + ao)
117
+ out << (a == 255 ? v : (v * a + 255 * (255 - a) + 127) / 255)
118
+ j += 4
119
+ end
120
+ end
121
+ end
122
+
123
+ # Luminance at (x, y).
124
+ def [](x, y)
125
+ @data.getbyte(y * @width + x)
126
+ end
127
+
128
+ def size
129
+ @width * @height
130
+ end
131
+
132
+ # Bytes of row y as a binary String.
133
+ def row(y)
134
+ @data.byteslice(y * @width, @width)
135
+ end
136
+
137
+ # Photographic negative (for light-on-dark codes).
138
+ def invert
139
+ Image.new(@width, @height, @data.tr(INVERT_FROM, INVERT_TO))
140
+ end
141
+
142
+ def rotate180
143
+ Image.new(@width, @height, @data.reverse)
144
+ end
145
+
146
+ # Rotates 90 degrees clockwise.
147
+ def rotate90
148
+ w = @width
149
+ h = @height
150
+ out = Image.buffer(w * h)
151
+ x = 0
152
+ # Output row x is source column x read bottom to top.
153
+ while x < w
154
+ column_up(out, x)
155
+ x += 1
156
+ end
157
+ Image.new(h, w, out)
158
+ end
159
+
160
+ # Rotates 90 degrees counter-clockwise.
161
+ def rotate270
162
+ rotate90.rotate180
163
+ end
164
+
165
+ # Rotates by a multiple of 90 degrees (clockwise).
166
+ def rotate(degrees)
167
+ case degrees % 360
168
+ when 0 then self
169
+ when 90 then rotate90
170
+ when 180 then rotate180
171
+ when 270 then rotate270
172
+ else raise ArgumentError, "rotation must be a multiple of 90 degrees"
173
+ end
174
+ end
175
+
176
+ # Horizontal mirror image.
177
+ def mirror
178
+ w = @width
179
+ out = String.new(capacity: @data.bytesize, encoding: Encoding::BINARY)
180
+ @height.times { |y| out << @data.byteslice(y * w, w).reverse }
181
+ Image.new(w, @height, out)
182
+ end
183
+
184
+ # The image stretched (or shrunk) vertically to +new_height+ rows by
185
+ # repeating (or dropping) whole rows - nearest neighbour, C-speed copies.
186
+ def stretch_rows(new_height)
187
+ raise ArgumentError, "invalid height #{new_height}" unless new_height.positive?
188
+
189
+ out = Image.buffer(@width * new_height)
190
+ y = 0
191
+ while y < new_height
192
+ out << row(y * @height / new_height)
193
+ y += 1
194
+ end
195
+ Image.new(@width, new_height, out)
196
+ end
197
+
198
+ def crop(x, y, w, h)
199
+ raise ArgumentError, "crop rectangle outside image" if x.negative? || y.negative? || w < 1 || h < 1 || x + w > @width || y + h > @height
200
+
201
+ out = String.new(capacity: w * h, encoding: Encoding::BINARY)
202
+ h.times { |dy| out << @data.byteslice((y + dy) * @width + x, w) }
203
+ Image.new(w, h, out)
204
+ end
205
+
206
+ # Box-filter downscale by an integer factor (partial edge blocks are dropped).
207
+ def downscale(factor)
208
+ return self if factor <= 1
209
+ raise ArgumentError, "downscale factor too large" if @width < factor || @height < factor
210
+
211
+ Image.new(@width / factor, @height / factor, QrReaderKernels.downscale(@data, @width, @height, factor))
212
+ end
213
+
214
+ def inspect
215
+ "#<QrReader::Image #{@width}x#{@height}>"
216
+ end
217
+
218
+ private
219
+
220
+ # Appends source column x, bottom to top, to +out+.
221
+ def column_up(out, x)
222
+ src = @data
223
+ w = @width
224
+ i = (@height - 1) * w + x
225
+ while i >= 0
226
+ out << src.getbyte(i)
227
+ i -= w
228
+ end
229
+ end
230
+ end
231
+ end