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,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