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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +23 -7
- data/exe/qr_reader +1 -1
- data/lib/qr_reader/formats/jpeg.rb +61 -2
- data/lib/qr_reader/formats/jpeg2000/codestream.rb +447 -0
- data/lib/qr_reader/formats/jpeg2000/container.rb +156 -0
- data/lib/qr_reader/formats/jpeg2000/dwt.rb +607 -0
- data/lib/qr_reader/formats/jpeg2000/tier1.rb +1084 -0
- data/lib/qr_reader/formats/jpeg2000/tile.rb +722 -0
- data/lib/qr_reader/formats/jpeg2000.rb +764 -0
- data/lib/qr_reader/formats/pdf/font_programs.rb +1205 -0
- data/lib/qr_reader/formats/pdf/font_tables.rb +224 -0
- data/lib/qr_reader/formats/pdf/fonts.rb +412 -0
- data/lib/qr_reader/formats/pdf/text.rb +151 -0
- data/lib/qr_reader/formats/pdf/vector.rb +799 -0
- data/lib/qr_reader/formats/pdf.rb +183 -44
- data/lib/qr_reader/formats.rb +13 -4
- data/lib/qr_reader/reader.rb +36 -10
- data/lib/qr_reader/version.rb +1 -1
- data/lib/qr_reader.rb +1 -1
- metadata +15 -4
|
@@ -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
|
|
11
|
-
# vector
|
|
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)
|
|
54
|
-
#
|
|
55
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
121
|
-
|
|
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.
|
|
177
|
-
#
|
|
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
|
-
|
|
181
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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 =
|
|
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
|
|
314
|
+
if (overlap = overlap_rows(f2 + d2 - f, row)) # directly below
|
|
315
|
+
return false if overlap >= image.height
|
|
316
|
+
|
|
236
317
|
parts << image
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2010
|
-
|
|
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
|
|
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"
|
|
2032
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
2359
|
-
#
|
|
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
|
-
|
|
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"
|
data/lib/qr_reader/formats.rb
CHANGED
|
@@ -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, :
|
|
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
|
|
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"
|
data/lib/qr_reader/reader.rb
CHANGED
|
@@ -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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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)
|
data/lib/qr_reader/version.rb
CHANGED
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.
|
|
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
|
|
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
|
-
-
|
|
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
|