qr_reader 0.0.1 → 0.0.2

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.
@@ -7,13 +7,17 @@ require_relative "../kernels/lzw"
7
7
  module QrReader
8
8
  module Formats
9
9
  # Extracts the raster images of PDF documents page by page: scanned
10
- # pages, faxes and photos placed in PDFs. Vector graphics (including
11
- # vector-drawn QR codes) are not rendered.
10
+ # pages, faxes and photos placed in PDFs. Vector graphics are not
11
+ # rendered, except that with +vector: true+ groups of paths that may
12
+ # be a vector-drawn QR code are rasterized (see PathCollector and
13
+ # VectorRenderer) and yielded after the page's raster images, with
14
+ # +info[:vector]+ true and +info[:name]+ nil.
12
15
  #
13
16
  # PDF.each_image(bytes) { |image, info| ... } # info: {page: 0, name: "Im0", width: 2480, height: 3508,
14
17
  # # pixel_aspect: 1.0, dpi: [300.0, 300.0]}
15
18
  # PDF.each_image(bytes) # => Enumerator
16
19
  # PDF.each_image(bytes, raw_jpeg: true) { |image_or_jpeg, info| ... }
20
+ # PDF.each_image(bytes, vector: true) { |image, info| ... }
17
21
  # PDF.images(bytes) # => [#<QrReader::Image 2480x3508>, ...]
18
22
  #
19
23
  # Structure: the header may sit anywhere in the first KB; classic
@@ -43,6 +47,8 @@ module QrReader
43
47
  # with +info[:format]+ :jpeg and the size of the JPEG frame, so that a
44
48
  # caller can decode it at reduced size first (JPEG.decode(data,
45
49
  # scale:)). JPEG.decode of such data may still raise ImageError.
50
+ # JPXDecode images come undecoded too, as a RawJpx (+info[:format]+
51
+ # :jpx), whose #decode(reduce:) makes the Image.
46
52
  #
47
53
  # Images: 1/2/4/8/16 bits per component; DeviceGray/RGB/CMYK, CalGray,
48
54
  # CalRGB, ICCBased (by /N), Indexed, Separation and DeviceN (tint =
@@ -50,9 +56,10 @@ module QrReader
50
56
  # arrays. Filters: FlateDecode and LZWDecode (with PNG and TIFF
51
57
  # predictors), RunLengthDecode, ASCIIHexDecode, ASCII85Decode,
52
58
  # DCTDecode (JPEG), CCITTFaxDecode and JBIG2Decode (generic regions;
53
- # symbol, halftone and refinement coding are skipped). JPEG 2000 images
54
- # are skipped. Colour is reduced to luminance with Image.luma; alpha
55
- # (/SMask) is ignored.
59
+ # symbol, halftone and refinement coding are skipped) and JPXDecode
60
+ # (JPEG 2000, its own colour information unless the image dictionary
61
+ # has a /ColorSpace). Colour is reduced to luminance with Image.luma;
62
+ # alpha (/SMask, JPX opacity channels) is ignored.
56
63
  #
57
64
  # Encrypted documents are decrypted when their user password is empty
58
65
  # (standard security handler: RC4, AES-128, AES-256); other encrypted
@@ -82,11 +89,31 @@ module QrReader
82
89
  # A DCT image handed over undecoded (each_image with raw_jpeg: true):
83
90
  # the JPEG data and its frame size.
84
91
  RawJpeg = Struct.new(:data, :width, :height)
92
+ # A JPXDecode image handed over undecoded (each_image with raw_jpeg:
93
+ # true): the JPEG 2000 data, its full size and the PDF colour space
94
+ # that overrides the JP2 colour information (nil when the image
95
+ # dictionary has none). #decode makes the luminance Image, at a
96
+ # reduced resolution with +reduce+ (default: the least the decoder
97
+ # accepts, see JPEG2000.fitting_reduce).
98
+ RawJpx = Struct.new(:data, :width, :height, :space) do
99
+ def fitting_reduce
100
+ JPEG2000.fitting_reduce(data)
101
+ end
102
+
103
+ def decode(reduce: fitting_reduce)
104
+ return JPEG2000.decode(data, reduce: reduce) unless space
105
+
106
+ comps = JPEG2000.decode_components(data, reduce: reduce, palette: false)
107
+ Image.new(comps.width, comps.height, PDF.jpx_luminance(comps, space))
108
+ end
109
+ end
85
110
 
86
111
  # Largest decoded stream we produce for anything but image samples.
87
112
  STREAM_LIMIT = 1 << 26
88
113
  # Largest decoded image sample buffer.
89
114
  SAMPLE_LIMIT = 1 << 28
115
+ # Most rows by which two bands of one picture may overlap (see Band).
116
+ MAX_OVERLAP_ROWS = 2
90
117
 
91
118
  # Errors that garbage input could trigger deep inside the decoders.
92
119
  INTERNAL_ERRORS = [NoMethodError, TypeError, ArgumentError, RangeError, IndexError, ZeroDivisionError,
@@ -94,10 +121,11 @@ module QrReader
94
121
 
95
122
  class << self
96
123
  # Yields each raster image of the document with an info Hash
97
- # ({page:, name:, width:, height:}), page by page in drawing order.
124
+ # ({page:, name:, width:, height:}), page by page in drawing order;
125
+ # with +vector+ also the rendered groups of vector shapes.
98
126
  # Returns an Enumerator without a block.
99
- def each_image(bytes, raw_jpeg: false)
100
- return enum_for(:each_image, bytes, raw_jpeg: raw_jpeg) unless block_given?
127
+ def each_image(bytes, raw_jpeg: false, vector: false)
128
+ return enum_for(:each_image, bytes, raw_jpeg: raw_jpeg, vector: vector) unless block_given?
101
129
 
102
130
  document = guard { Document.new(binary(bytes)) }
103
131
  pages = guard { document.pages }
@@ -105,7 +133,8 @@ module QrReader
105
133
  count = 0
106
134
  pages.each_with_index do |page, index|
107
135
  band = nil # the latest image, held back in case the next one continues it
108
- sources(document, page, failures).each do |source|
136
+ paths = PathCollector.new(document, page.resources) if vector
137
+ sources(document, page, failures, paths).each do |source|
109
138
  part = decode(document, source, failures, raw_jpeg) or next
110
139
  next if band&.join(part, source.ctm)
111
140
 
@@ -115,10 +144,17 @@ module QrReader
115
144
  end
116
145
  band = Band.new([part], source.ctm, source.name)
117
146
  end
118
- next unless band && (image = band.image)
147
+ if band && (image = band.image)
148
+ count += 1
149
+ yield image, band.info(index, image)
150
+ end
151
+ next unless paths
119
152
 
120
- count += 1
121
- yield image, band.info(index, image)
153
+ vector_images(paths, failures).each do |image, dpi|
154
+ count += 1
155
+ yield image, { page: index, name: nil, width: image.width, height: image.height, pixel_aspect: 1.0,
156
+ dpi: [dpi, dpi], vector: true }
157
+ end
122
158
  end
123
159
  raise no_images(failures) if count.zero?
124
160
 
@@ -130,6 +166,21 @@ module QrReader
130
166
  each_image(bytes).map { |image, _info| image }
131
167
  end
132
168
 
169
+ # Luminance of decoded JPEG 2000 +comps+ in the PDF colour space
170
+ # +space+, which overrides the JP2 colour information (internal).
171
+ def jpx_luminance(comps, space)
172
+ alpha = comps.alpha || []
173
+ channels = comps.components.reject.with_index { |_, i| alpha.include?(i) }
174
+ if space.components == 1 || channels.size < space.components
175
+ lut = space.luma_lut(8, nil)
176
+ first = channels.first or raise ImageError, "JPEG 2000 image without colour channels"
177
+ return lut == ImageDecoder::IDENTITY ? first : first.tr(ImageDecoder::ALL_BYTES, Image::TR_ESCAPE.call(lut))
178
+ end
179
+
180
+ color = { rgb: :rgb, cmyk: :cmyk, lab: :gray }[space.kind]
181
+ JPEG2000.luminance(JPEG2000::Components.new(comps.width, comps.height, channels, color, false, []))
182
+ end
183
+
133
184
  private
134
185
 
135
186
  def binary(bytes)
@@ -144,8 +195,15 @@ module QrReader
144
195
  raise ImageError, "corrupt PDF (#{e.class}: #{e.message})"
145
196
  end
146
197
 
147
- def sources(document, page, failures)
148
- document.page_images(page)
198
+ def sources(document, page, failures, paths)
199
+ document.page_images(page, paths)
200
+ rescue Error, *INTERNAL_ERRORS => e
201
+ failures << e
202
+ []
203
+ end
204
+
205
+ def vector_images(paths, failures)
206
+ VectorRenderer.images(paths.shapes)
149
207
  rescue Error, *INTERNAL_ERRORS => e
150
208
  failures << e
151
209
  []
@@ -173,29 +231,50 @@ module QrReader
173
231
  # Images drawn directly above or below each other with the same width
174
232
  # and scale are bands of one picture (scanner drivers that print
175
233
  # through PostScript cut page images into strips), joined so that a
176
- # code across a band boundary stays readable. +parts+ (Images or
177
- # RawJpegs) are top to bottom; +ctm+ maps the unit square onto the
234
+ # code across a band boundary stays readable. Bands may overlap by up
235
+ # to MAX_OVERLAP_ROWS rows (drivers often repeat a row so that no gap
236
+ # shows); the repeated rows are dropped. +parts+ (Images or RawJpegs)
237
+ # are top to bottom, +skips+ the number of rows hidden at the top of
238
+ # each (by the part above); +ctm+ maps the unit square onto the
178
239
  # joined picture.
179
- Band = Struct.new(:parts, :ctm, :name) do
180
- # The picture: an Image, the JPEG data of a lone RawJpeg, or nil
181
- # when a joined RawJpeg cannot be decoded.
240
+ Band = Struct.new(:parts, :ctm, :name, :skips) do
241
+ def initialize(parts, ctm, name)
242
+ super(parts, ctm, name, Array.new(parts.size, 0))
243
+ end
244
+
245
+ # The picture: an Image, the JPEG data of a lone RawJpeg, a lone
246
+ # RawJpx, or nil when a joined RawJpeg or RawJpx cannot be decoded.
182
247
  def image
183
248
  part = parts.first
184
249
  return part.is_a?(RawJpeg) ? part.data : part if parts.size == 1
185
250
 
186
- images = parts.map { |p| p.is_a?(RawJpeg) ? JPEG.decode(p.data) : p }
187
- Image.new(images.first.width, images.sum(&:height), images.map(&:data).join)
251
+ data = parts.each_with_index.map do |p, i|
252
+ p = JPEG.decode(p.data) if p.is_a?(RawJpeg)
253
+ if p.is_a?(RawJpx)
254
+ raw = p
255
+ p = raw.decode(reduce: 0)
256
+ raise ImageError, "JPX band of another size" unless [p.width, p.height] == [raw.width, raw.height]
257
+ end
258
+ skip = skips[i] * p.width
259
+ skip.zero? ? p.data : p.data.byteslice(skip, p.data.bytesize - skip)
260
+ end
261
+ Image.new(parts.first.width, rows, data.join)
188
262
  rescue Error
189
263
  nil
190
264
  end
191
265
 
266
+ # Height of the joined picture.
267
+ def rows
268
+ parts.sum(&:height) - skips.sum
269
+ end
270
+
192
271
  # The info Hash for +image+ (see PDF.each_image).
193
272
  def info(page, image)
194
273
  info = { page: page, name: name }
195
- if image.is_a?(String)
274
+ if image.is_a?(String) || image.is_a?(RawJpx)
196
275
  info[:width] = parts.first.width
197
276
  info[:height] = parts.first.height
198
- info[:format] = :jpeg
277
+ info[:format] = image.is_a?(String) ? :jpeg : :jpx
199
278
  else
200
279
  info[:width] = image.width
201
280
  info[:height] = image.height
@@ -223,7 +302,7 @@ module QrReader
223
302
  return false unless ctm && matrix && upright?(ctm) && upright?(matrix)
224
303
 
225
304
  width = parts.first.width
226
- height = parts.sum(&:height)
305
+ height = rows
227
306
  a, _, _, d, e, f = ctm
228
307
  a2, _, _, d2, e2, f2 = matrix
229
308
  row = d / height
@@ -232,12 +311,19 @@ module QrReader
232
311
  return false unless (a2 - a).abs <= a * 0.01 && (e2 - e).abs <= a / width / 2
233
312
  return false unless (d2 / image.height - row).abs <= row * 0.01
234
313
 
235
- if (f2 + d2 - f).abs <= row / 2 # directly below
314
+ if (overlap = overlap_rows(f2 + d2 - f, row)) # directly below
315
+ return false if overlap >= image.height
316
+
236
317
  parts << image
237
- self.ctm = [a, 0.0, 0.0, d + d2, e, f2]
238
- elsif (f + d - f2).abs <= row / 2 # directly above
318
+ skips << overlap
319
+ self.ctm = [a, 0.0, 0.0, f + d - f2, e, f2]
320
+ elsif (overlap = overlap_rows(f + d - f2, row)) # directly above
321
+ return false if overlap >= parts.first.height - skips.first
322
+
323
+ skips[0] += overlap
239
324
  parts.unshift(image)
240
- self.ctm = [a, 0.0, 0.0, d + d2, e, f]
325
+ skips.unshift(0)
326
+ self.ctm = [a, 0.0, 0.0, f2 + d2 - f, e, f]
241
327
  else
242
328
  return false
243
329
  end
@@ -246,6 +332,13 @@ module QrReader
246
332
 
247
333
  private
248
334
 
335
+ # Whole rows by which two bands overlap (+overlap+ in points, 0 when
336
+ # they just touch), or nil when they are not adjacent.
337
+ def overlap_rows(overlap, row)
338
+ n = (overlap / row).round
339
+ n.between?(0, MAX_OVERLAP_ROWS) && (overlap - n * row).abs <= row / 2 ? n : nil
340
+ end
341
+
249
342
  def upright?(m)
250
343
  m[1].abs < 1e-6 && m[2].abs < 1e-6 && m[0].positive? && m[3].positive?
251
344
  end
@@ -679,6 +772,11 @@ module QrReader
679
772
  Filters.decode(stream_bytes(stream), filters, params, limit)
680
773
  end
681
774
 
775
+ # Fonts loaded for text drawing, by font dictionary (see Font).
776
+ def fonts
777
+ @fonts ||= {}
778
+ end
779
+
682
780
  # Decoded content stream, or nil when it cannot be decoded (or the
683
781
  # document's CONTENT_BUDGET is spent).
684
782
  def content(stream)
@@ -700,8 +798,9 @@ module QrReader
700
798
  list
701
799
  end
702
800
 
703
- # ImageSources drawn by +page+ in drawing order.
704
- def page_images(page)
801
+ # ImageSources drawn by +page+ in drawing order; its paths go to
802
+ # +paths+ (a PathCollector) if given.
803
+ def page_images(page, paths = nil)
705
804
  return [ImageSource.new(page.orphan.dict, page.orphan, nil, {}, nil, nil)] if page.orphan
706
805
 
707
806
  resources = page.resources
@@ -713,7 +812,7 @@ module QrReader
713
812
  # Undecodable content: list the page's image resources instead.
714
813
  return resource_images(resources) if parts.empty?
715
814
 
716
- ContentScanner.new(self).images(parts.join("\n"), resources)
815
+ ContentScanner.new(self, paths).images(parts.join("\n"), resources)
717
816
  end
718
817
 
719
818
  private
@@ -1976,8 +2075,9 @@ module QrReader
1976
2075
  TEXTUAL = /\A[\t\n\r\x20-\x7E]*\z/
1977
2076
  STRING_SPECIAL = /[()\\]/
1978
2077
 
1979
- def initialize(doc)
2078
+ def initialize(doc, paths = nil)
1980
2079
  @doc = doc
2080
+ @paths = paths
1981
2081
  @images = []
1982
2082
  @seen = {}
1983
2083
  @forms = {}
@@ -2006,16 +2106,29 @@ module QrReader
2006
2106
  case b
2007
2107
  when 0x2F then operand(operands, Parser.scan_token(s, data, Parser::NAME_RUN))
2008
2108
  when 0x28
2009
- skip_string(s, data)
2010
- operand(operands, nil)
2109
+ # Strings and arrays only matter for text (see pdf/text.rb).
2110
+ if @paths
2111
+ operand(operands, text_operand(s, data))
2112
+ else
2113
+ skip_string(s, data)
2114
+ operand(operands, nil)
2115
+ end
2011
2116
  when 0x3C
2012
2117
  if data.getbyte(s.pos + 1) == 0x3C
2013
2118
  s.pos += 2
2119
+ elsif @paths
2120
+ operand(operands, text_operand(s, data))
2014
2121
  else
2015
2122
  s.skip_until(/>/) || s.terminate
2016
2123
  operand(operands, nil)
2017
2124
  end
2018
- when 0x3E, 0x5B, 0x5D, 0x7B, 0x7D, 0x29, 0x25 then s.pos += 1
2125
+ when 0x5B
2126
+ if @paths
2127
+ operand(operands, text_operand(s, data))
2128
+ else
2129
+ s.pos += 1
2130
+ end
2131
+ when 0x3E, 0x5D, 0x7B, 0x7D, 0x29, 0x25 then s.pos += 1
2019
2132
  else
2020
2133
  # (Parser.scan_token inlined.)
2021
2134
  token = s.scan(token_run)
@@ -2028,10 +2141,22 @@ module QrReader
2028
2141
  end
2029
2142
  case token
2030
2143
  when "cm" then ctm = multiply(operands.last(6).map(&:to_f), ctm) if operands.size >= 6
2031
- when "q" then stack << ctm if stack.size < MAX_STACK
2032
- when "Q" then ctm = stack.pop || ctm
2144
+ when "q"
2145
+ stack << ctm if stack.size < MAX_STACK
2146
+ @paths&.save
2147
+ when "Q"
2148
+ ctm = stack.pop || ctm
2149
+ @paths&.restore
2033
2150
  when "Do" then draw(operands.last, resources, ctm, depth)
2034
2151
  when "BI" then inline_image(s, data, resources, ctm)
2152
+ else
2153
+ if @paths
2154
+ if PathCollector.operator?(token)
2155
+ @paths.operator(token, operands, ctm, resources)
2156
+ elsif TEXT_OPERATORS[token]
2157
+ text_operator(token, operands, ctm, resources, depth)
2158
+ end
2159
+ end
2035
2160
  end
2036
2161
  operands.clear
2037
2162
  end
@@ -2081,7 +2206,7 @@ module QrReader
2081
2206
  dict = stream.dict
2082
2207
  subtype = @doc.resolve(dict[:Subtype])
2083
2208
  if subtype == :Image || (subtype.nil? && dict.key?(:Width))
2084
- return if @seen[key]
2209
+ return if @seen[key] || @in_glyph&.positive?
2085
2210
 
2086
2211
  @seen[key] = true
2087
2212
  @images << ImageSource.new(dict, stream, nil, resources, name.to_s, ctm)
@@ -2093,7 +2218,9 @@ module QrReader
2093
2218
  matrix = @doc.resolve(dict[:Matrix])
2094
2219
  matrix = matrix.map { |v| Util.number(@doc.resolve(v)) || 0.0 } if matrix.is_a?(Array) && matrix.size == 6
2095
2220
  ctm = multiply(matrix, ctm) if matrix.is_a?(Array) && matrix.size == 6
2221
+ @paths&.save
2096
2222
  scan(content, dict.key?(:Resources) ? dict[:Resources] : resources, ctm, depth + 1)
2223
+ @paths&.restore
2097
2224
  end
2098
2225
  end
2099
2226
 
@@ -2115,6 +2242,8 @@ module QrReader
2115
2242
  start += 1 if start < data.bytesize && " \t\n\f\r\x00".include?(data.byteslice(start, 1))
2116
2243
  stop, after = extent(data, start, dict, resources)
2117
2244
  s.pos = after
2245
+ return if @in_glyph&.positive?
2246
+
2118
2247
  @images << ImageSource.new(dict, nil, data.byteslice(start, stop - start), resources, nil, ctm)
2119
2248
  rescue ParseError
2120
2249
  s.skip_until(EI) || s.terminate
@@ -2221,8 +2350,11 @@ module QrReader
2221
2350
  when :JBIG2Decode
2222
2351
  data = Filters.decode(data, filters, params, SAMPLE_LIMIT)
2223
2352
  jbig2_image(data, codec_params, width, height, one_component(space), decode)
2224
- else
2225
- raise UnsupportedImageError, "JPEG 2000 (JPXDecode) images are not supported"
2353
+ else # JPXDecode: its own colour information unless /ColorSpace (or /ImageMask) says otherwise
2354
+ jpx = Filters.decode(data, filters, params, SAMPLE_LIMIT)
2355
+ jw, jh = JPEG2000.dimensions(jpx)
2356
+ raw = RawJpx.new(jpx, jw, jh, @dict.key?(:ColorSpace) || mask ? space : nil)
2357
+ @raw_jpeg ? raw : raw.decode
2226
2358
  end
2227
2359
  end
2228
2360
 
@@ -2355,10 +2487,11 @@ module QrReader
2355
2487
  image = JPEG.decode(jpeg)
2356
2488
  lut == IDENTITY ? image : Image.new(image.width, image.height, translate(image.data, lut))
2357
2489
  when 4
2358
- # JPEG.decode reads CMYK the Adobe way (stored inverted), which
2359
- # PDFs declare with /Decode [1 0 1 0 1 0 1 0].
2490
+ # PDFs store ink amounts (0 = no ink); data written the Adobe
2491
+ # way (255 = no ink, JPEG.decode's default) is declared with
2492
+ # /Decode [1 0 1 0 1 0 1 0].
2360
2493
  cmyk = space&.components == 4 ? space : ColorSpace::CMYK
2361
- raise UnsupportedImageError, "CMYK JPEG image without an inverting /Decode array" unless cmyk.inverted?(decode)
2494
+ return JPEG.decode(jpeg, cmyk: :plain) unless cmyk.inverted?(decode)
2362
2495
 
2363
2496
  @raw_jpeg ? RawJpeg.new(jpeg, jw, jh) : JPEG.decode(jpeg)
2364
2497
  else
@@ -2927,9 +3060,15 @@ module QrReader
2927
3060
  end
2928
3061
  end
2929
3062
 
2930
- private_constant :ParseError, :Ref, :Keyword, :Stream, :Page, :ImageSource, :Band, :STREAM_LIMIT, :SAMPLE_LIMIT,
3063
+ private_constant :ParseError, :Ref, :Keyword, :Stream, :Page, :ImageSource, :Band, :STREAM_LIMIT, :SAMPLE_LIMIT, :MAX_OVERLAP_ROWS,
2931
3064
  :INTERNAL_ERRORS, :Util, :Parser, :Document, :Security, :Filters, :ColorSpace, :Pixels,
2932
3065
  :ContentScanner, :ImageDecoder, :JBIG2
2933
3066
  end
2934
3067
  end
2935
3068
  end
3069
+
3070
+ require_relative "pdf/font_tables"
3071
+ require_relative "pdf/font_programs"
3072
+ require_relative "pdf/fonts"
3073
+ require_relative "pdf/text"
3074
+ require_relative "pdf/vector"
@@ -19,6 +19,8 @@ module QrReader
19
19
  GIF_MAGIC = /\AGIF8[79]a/n
20
20
  PNM_MAGIC = /\AP[1-7][\s#]/n
21
21
  TIFF_MAGIC = ["II*\0", "MM\0*", "II+\0", "MM\0+"].map(&:b).freeze
22
+ JP2_SIGNATURE = "\0\0\0\x0CjP \r\n\x87\n".b.freeze
23
+ JP2_CODESTREAM = "\xFF\x4F\xFF\x51".b.freeze
22
24
  WHITESPACE = " \t\n\v\f\r"
23
25
  ALL_BYTES = (0..255).to_a.freeze
24
26
  WHITE = "\xFF".b
@@ -30,12 +32,11 @@ module QrReader
30
32
  [/\A....ftyp(?:avif|avis)/mn, "AVIF"],
31
33
  [/\A....ftyp(?:heic|heix|hevc|hevx|heim|heis|mif1|msf1)/mn, "HEIC/HEIF"],
32
34
  [/\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
35
  [/\AP[Ff][\s#]/n, "PFM (floating point PNM)"],
35
36
  [/\A8BPS/n, "Photoshop PSD"],
36
37
  [/\A\s*<(?:\?xml|svg|!DOCTYPE svg)/n, "SVG"]
37
38
  ].freeze
38
- private_constant :GIF_MAGIC, :PNM_MAGIC, :TIFF_MAGIC, :WHITESPACE, :ALL_BYTES, :WHITE, :UNSUPPORTED
39
+ private_constant :GIF_MAGIC, :PNM_MAGIC, :TIFF_MAGIC, :JP2_SIGNATURE, :JP2_CODESTREAM, :WHITESPACE, :ALL_BYTES, :WHITE, :UNSUPPORTED
39
40
 
40
41
  class << self
41
42
  # Decodes an encoded image (format sniffed from its magic bytes) into a
@@ -49,12 +50,14 @@ module QrReader
49
50
  when :bmp then BMP.decode(bytes)
50
51
  when :pnm then PNM.decode(bytes)
51
52
  when :tiff then TIFF.decode(bytes)
53
+ when :jpeg2000 then JPEG2000.decode(bytes, reduce: JPEG2000.fitting_reduce(bytes))
52
54
  when :pdf then first_pdf_image(bytes)
53
55
  else raise unrecognized(bytes)
54
56
  end
55
57
  end
56
58
 
57
- # :png, :jpeg, :gif, :bmp, :pnm, :tiff, :pdf, or nil when the magic
59
+ # :png, :jpeg, :gif, :bmp, :pnm, :tiff, :jpeg2000 (JP2 file or raw
60
+ # codestream), :pdf, or nil when the magic
58
61
  # bytes are not those of a supported format. TIFF and PDF are
59
62
  # multi-page: Formats.decode returns their first page or image.
60
63
  def format_of(bytes)
@@ -63,7 +66,11 @@ module QrReader
63
66
  head = bytes.byteslice(0, 32).b
64
67
  b0 = head.getbyte(0)
65
68
  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
69
+ elsif b0 == 0xFF
70
+ if head.getbyte(1) == 0xD8 && head.getbyte(2) == 0xFF then :jpeg
71
+ elsif head.start_with?(JP2_CODESTREAM) then :jpeg2000
72
+ end
73
+ elsif b0 == 0x00 then :jpeg2000 if head.start_with?(JP2_SIGNATURE)
67
74
  elsif b0 == 0x47 then :gif if GIF_MAGIC.match?(head)
68
75
  elsif b0 == 0x42 then :bmp if BMP.bmp?(head)
69
76
  elsif b0 == 0x50 then :pnm if PNM_MAGIC.match?(head)
@@ -83,6 +90,7 @@ module QrReader
83
90
  when :bmp then BMP.dimensions(bytes)
84
91
  when :pnm then PNM.dimensions(bytes)
85
92
  when :tiff then TIFF.dimensions(bytes)
93
+ when :jpeg2000 then JPEG2000.dimensions(bytes)
86
94
  when :pdf then first_pdf_image(bytes).then { |image| [image.width, image.height] }
87
95
  else raise unrecognized(bytes)
88
96
  end
@@ -277,3 +285,4 @@ require_relative "formats/gif"
277
285
  require_relative "formats/bmp"
278
286
  require_relative "formats/pnm"
279
287
  require_relative "formats/jpeg"
288
+ require_relative "formats/jpeg2000"
@@ -15,6 +15,9 @@ module QrReader
15
15
  # The coarsest pyramid level is the largest power-of-two reduction whose
16
16
  # longer side is still at least this many pixels.
17
17
  PYRAMID_MIN_SIDE = 800
18
+ # ... and whose shorter side still has room for the smallest symbol (21
19
+ # modules), so long thin images (banners, rules) are not reduced to nothing.
20
+ PYRAMID_MIN_SHORT_SIDE = 21
18
21
  # The exhaustive (try_harder) strategies only run on pyramid levels up to
19
22
  # this size: at full resolution of a 12 MP photo they cost seconds and
20
23
  # mostly chase texture noise, while the fast strategies still run there.
@@ -138,12 +141,13 @@ module QrReader
138
141
  return if scan_page(image, info) && !multi
139
142
  end
140
143
  when :pdf
141
- # Plain JPEG page images come undecoded, for the reduced-size pass.
142
- Formats::PDF.each_image(bytes, raw_jpeg: true) do |image, info|
143
- found = if info[:format] == :jpeg
144
- scan_jpeg_or_full(image, info[:page], info[:pixel_aspect])
145
- else
146
- scan_page(image, info)
144
+ # Plain JPEG page images come undecoded, for the reduced-size pass;
145
+ # groups of vector shapes that may be a code come rendered.
146
+ Formats::PDF.each_image(bytes, raw_jpeg: true, vector: true) do |image, info|
147
+ found = case info[:format]
148
+ when :jpeg then scan_jpeg_or_full(image, info[:page], info[:pixel_aspect])
149
+ when :jpx then scan_jpx(image, info[:page], info[:pixel_aspect])
150
+ else scan_page(image, info)
147
151
  end
148
152
  return if found && !multi
149
153
  end
@@ -188,6 +192,28 @@ module QrReader
188
192
  scan_pyramid(full, page, skip_factor: scale)
189
193
  end
190
194
 
195
+ # A JPEG 2000 image of a PDF (a PDF::RawJpx): large ones are first
196
+ # decoded at reduced resolution, which skips most of the decoding work
197
+ # (JPEG 2000 resolutions are coded separately), and only decoded in
198
+ # full when the quick strategies find nothing there.
199
+ def scan_jpx(raw, page, aspect)
200
+ aspect ||= 1.0
201
+ base = raw.fitting_reduce
202
+ longest = raw.width > raw.height ? raw.width : raw.height
203
+ preview = base
204
+ preview += 1 while preview < base + 3 && (longest >> (preview + 1)) >= PYRAMID_MIN_SIDE
205
+ if preview > base && (aspect - 1.0).abs < SQUARE_TOLERANCE
206
+ @page = page
207
+ @tried = {}
208
+ small = raw.decode(reduce: preview)
209
+ return true if run_strategies(small, (1 << preview).to_f, quick: true) && !@multi
210
+
211
+ small = nil
212
+ return scan_pyramid(raw.decode(reduce: base), page, skip_factor: 1 << (preview - base))
213
+ end
214
+ scan_page(raw.decode(reduce: base), page: page, pixel_aspect: aspect)
215
+ end
216
+
191
217
  # [image with square pixels, factor mapping its rows back to the original].
192
218
  # Fax pages (204 x 98 dpi) have pixels about twice as tall as wide: their
193
219
  # rows are repeated so that finder patterns are square again.
@@ -201,16 +227,16 @@ module QrReader
201
227
  # Reduced decode scale (2, 4 or 8) for big JPEGs, or nil.
202
228
  def jpeg_prescale(bytes)
203
229
  w, h = Formats::JPEG.dimensions(bytes)
204
- factors = pyramid_factors(w > h ? w : h)
230
+ factors = pyramid_factors(w > h ? w : h, w < h ? w : h)
205
231
  factors.size > 1 ? [factors.first, 8].min : nil
206
232
  rescue Error
207
233
  nil
208
234
  end
209
235
 
210
236
  # Power-of-two reduction factors, coarsest first, ending with 1.
211
- def pyramid_factors(longest)
237
+ def pyramid_factors(longest, shortest)
212
238
  f = 1
213
- f *= 2 while longest / (f * 2) >= PYRAMID_MIN_SIDE
239
+ f *= 2 while longest / (f * 2) >= PYRAMID_MIN_SIDE && shortest / (f * 2) >= PYRAMID_MIN_SHORT_SIDE
214
240
  factors = []
215
241
  while f >= 1
216
242
  factors << f
@@ -225,7 +251,7 @@ module QrReader
225
251
  @page = page
226
252
  @tried = {}
227
253
  longest = image.width > image.height ? image.width : image.height
228
- factors = pyramid_factors(longest)
254
+ factors = pyramid_factors(longest, image.width < image.height ? image.width : image.height)
229
255
  levels = { 1 => image }
230
256
  level = lambda do |f|
231
257
  levels[f] ||= level.call(f / 2).downscale(2)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module QrReader
4
- VERSION = "0.0.1"
4
+ VERSION = "0.0.2"
5
5
  end
data/lib/qr_reader.rb CHANGED
@@ -25,7 +25,7 @@ require_relative "qr_reader/native_kernels"
25
25
  # QrReader.scan("scans.tiff") { |code| puts "page #{code.page}: #{code.text}" }
26
26
  #
27
27
  # Every method accepts a file path (String or Pathname), an IO, encoded image
28
- # bytes (PNG, JPEG, GIF, BMP, PNM, TIFF, PDF), a QrReader::Image, or a
28
+ # bytes (PNG, JPEG, JPEG 2000, GIF, BMP, PNM, TIFF, PDF), a QrReader::Image, or a
29
29
  # ChunkyPNG-style canvas (#width, #height, #pixels), plus these options:
30
30
  #
31
31
  # try_harder: true - fall back to slower strategies when the fast ones find nothing
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: qr_reader
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andi Idogawa
@@ -10,10 +10,10 @@ cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
12
12
  description: Detects and decodes QR codes in photos and scanned documents (PNG, JPEG,
13
- GIF, BMP, PNM, TIFF incl. CCITT fax, PDF) without native extensions or external
14
- tools.
13
+ JPEG 2000, GIF, BMP, PNM, TIFF incl. CCITT fax, PDF incl. vector-drawn codes) without
14
+ native extensions or external tools.
15
15
  email:
16
- - andi@idogawa.com
16
+ - web@idogawa.com
17
17
  executables:
18
18
  - qr_reader
19
19
  extensions: []
@@ -56,7 +56,18 @@ files:
56
56
  - lib/qr_reader/formats/ccitt.rb
57
57
  - lib/qr_reader/formats/gif.rb
58
58
  - lib/qr_reader/formats/jpeg.rb
59
+ - lib/qr_reader/formats/jpeg2000.rb
60
+ - lib/qr_reader/formats/jpeg2000/codestream.rb
61
+ - lib/qr_reader/formats/jpeg2000/container.rb
62
+ - lib/qr_reader/formats/jpeg2000/dwt.rb
63
+ - lib/qr_reader/formats/jpeg2000/tier1.rb
64
+ - lib/qr_reader/formats/jpeg2000/tile.rb
59
65
  - lib/qr_reader/formats/pdf.rb
66
+ - lib/qr_reader/formats/pdf/font_programs.rb
67
+ - lib/qr_reader/formats/pdf/font_tables.rb
68
+ - lib/qr_reader/formats/pdf/fonts.rb
69
+ - lib/qr_reader/formats/pdf/text.rb
70
+ - lib/qr_reader/formats/pdf/vector.rb
60
71
  - lib/qr_reader/formats/png.rb
61
72
  - lib/qr_reader/formats/pnm.rb
62
73
  - lib/qr_reader/formats/tiff.rb