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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +16 -0
- data/LICENSE.txt +21 -0
- data/NOTICE +14 -0
- data/README.md +222 -0
- data/exe/qr_reader +66 -0
- data/lib/qr_reader/binarizer.rb +170 -0
- data/lib/qr_reader/bit_matrix.rb +120 -0
- data/lib/qr_reader/decoder/bitstream_parser.rb +307 -0
- data/lib/qr_reader/decoder/character_set.rb +163 -0
- data/lib/qr_reader/decoder/codewords.rb +164 -0
- data/lib/qr_reader/decoder/data_mask.rb +57 -0
- data/lib/qr_reader/decoder/format_reader.rb +204 -0
- data/lib/qr_reader/decoder/function_patterns.rb +62 -0
- data/lib/qr_reader/decoder/galois_field.rb +69 -0
- data/lib/qr_reader/decoder/reed_solomon.rb +312 -0
- data/lib/qr_reader/decoder.rb +241 -0
- data/lib/qr_reader/detector/alignment_grid.rb +127 -0
- data/lib/qr_reader/detector/alignment_pattern_finder.rb +191 -0
- data/lib/qr_reader/detector/candidate/dimension.rb +237 -0
- data/lib/qr_reader/detector/candidate/strokes.rb +126 -0
- data/lib/qr_reader/detector/candidate.rb +436 -0
- data/lib/qr_reader/detector/finder_pattern.rb +63 -0
- data/lib/qr_reader/detector/finder_pattern_finder.rb +199 -0
- data/lib/qr_reader/detector/geometry.rb +118 -0
- data/lib/qr_reader/detector/grid_sampler.rb +350 -0
- data/lib/qr_reader/detector/pattern_sets.rb +235 -0
- data/lib/qr_reader/detector/perspective_transform.rb +117 -0
- data/lib/qr_reader/detector/pure_detector.rb +197 -0
- data/lib/qr_reader/detector.rb +215 -0
- data/lib/qr_reader/errors.rb +14 -0
- data/lib/qr_reader/format_information.rb +79 -0
- data/lib/qr_reader/formats/bmp.rb +434 -0
- data/lib/qr_reader/formats/ccitt.rb +81 -0
- data/lib/qr_reader/formats/gif.rb +272 -0
- data/lib/qr_reader/formats/jpeg.rb +152 -0
- data/lib/qr_reader/formats/pdf.rb +2935 -0
- data/lib/qr_reader/formats/png.rb +311 -0
- data/lib/qr_reader/formats/pnm.rb +286 -0
- data/lib/qr_reader/formats/tiff.rb +1345 -0
- data/lib/qr_reader/formats.rb +279 -0
- data/lib/qr_reader/image.rb +231 -0
- data/lib/qr_reader/kernels/binarizer.rb +475 -0
- data/lib/qr_reader/kernels/ccitt.rb +554 -0
- data/lib/qr_reader/kernels/detector.rb +587 -0
- data/lib/qr_reader/kernels/image.rb +65 -0
- data/lib/qr_reader/kernels/jpeg.rb +3421 -0
- data/lib/qr_reader/kernels/lzw.rb +173 -0
- data/lib/qr_reader/kernels/pixels.rb +116 -0
- data/lib/qr_reader/kernels/png.rb +1138 -0
- data/lib/qr_reader/kernels.rb +14 -0
- data/lib/qr_reader/native_kernels.rb +130 -0
- data/lib/qr_reader/reader.rb +348 -0
- data/lib/qr_reader/result.rb +49 -0
- data/lib/qr_reader/symbol_version.rb +173 -0
- data/lib/qr_reader/version.rb +5 -0
- data/lib/qr_reader.rb +77 -0
- metadata +103 -0
|
@@ -0,0 +1,1345 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zlib"
|
|
4
|
+
require_relative "../kernels/lzw"
|
|
5
|
+
|
|
6
|
+
module QrReader
|
|
7
|
+
module Formats
|
|
8
|
+
# TIFF decoder, aimed at what scanners, fax software and image editors
|
|
9
|
+
# write: bilevel CCITT pages as well as gray, colour and palette images.
|
|
10
|
+
#
|
|
11
|
+
# TIFF.decode(bytes) # => first page as a QrReader::Image
|
|
12
|
+
# TIFF.decode(bytes, page: -1) # => last page (negative indices count from the end)
|
|
13
|
+
# TIFF.page_count(bytes) # => 3
|
|
14
|
+
# TIFF.each_page(bytes) { |image| } # decodes one page at a time; Enumerator without a block
|
|
15
|
+
# TIFF.dimensions(bytes, page: 1) # => [2480, 3508], from the page's directory only
|
|
16
|
+
#
|
|
17
|
+
# Supported: little and big endian, BigTIFF, multi-page directory chains,
|
|
18
|
+
# strips and tiles; compression none, CCITT RLE / Group 3 / Group 4 (see
|
|
19
|
+
# CCITT), LZW (also the old LSB-first variant), JPEG (JPEGTables merged
|
|
20
|
+
# into each strip or tile; old-style JPEG only via JPEGInterchangeFormat),
|
|
21
|
+
# Deflate and PackBits; predictor 2 (horizontal differencing) and 3
|
|
22
|
+
# (floating point); photometric WhiteIsZero, BlackIsZero, RGB, palette,
|
|
23
|
+
# CMYK, YCbCr (luma only) and CIELab (L* as luminance); 1 to 64 bits per
|
|
24
|
+
# sample (unsigned, signed or floating point); planar configurations 1 and
|
|
25
|
+
# 2; FillOrder 2; extra samples (associated or unassociated alpha is
|
|
26
|
+
# composited over white like Image.from_pixels). Orientation is ignored:
|
|
27
|
+
# QR codes read in any orientation.
|
|
28
|
+
#
|
|
29
|
+
# Pages are the images of the main directory chain; reduced-resolution
|
|
30
|
+
# copies (thumbnails) and transparency masks are skipped. Damaged or
|
|
31
|
+
# missing strips and tiles are left white, like truncated data; only a page
|
|
32
|
+
# without any decodable image data raises ImageError.
|
|
33
|
+
module TIFF
|
|
34
|
+
NEW_SUBFILE_TYPE = 254
|
|
35
|
+
SUBFILE_TYPE = 255
|
|
36
|
+
IMAGE_WIDTH = 256
|
|
37
|
+
IMAGE_LENGTH = 257
|
|
38
|
+
BITS_PER_SAMPLE = 258
|
|
39
|
+
COMPRESSION = 259
|
|
40
|
+
PHOTOMETRIC = 262
|
|
41
|
+
FILL_ORDER = 266
|
|
42
|
+
STRIP_OFFSETS = 273
|
|
43
|
+
SAMPLES_PER_PIXEL = 277
|
|
44
|
+
ROWS_PER_STRIP = 278
|
|
45
|
+
STRIP_BYTE_COUNTS = 279
|
|
46
|
+
PLANAR_CONFIGURATION = 284
|
|
47
|
+
T4_OPTIONS = 292
|
|
48
|
+
PREDICTOR = 317
|
|
49
|
+
COLOR_MAP = 320
|
|
50
|
+
TILE_WIDTH = 322
|
|
51
|
+
TILE_LENGTH = 323
|
|
52
|
+
TILE_OFFSETS = 324
|
|
53
|
+
TILE_BYTE_COUNTS = 325
|
|
54
|
+
INK_SET = 332
|
|
55
|
+
EXTRA_SAMPLES = 338
|
|
56
|
+
SAMPLE_FORMAT = 339
|
|
57
|
+
JPEG_TABLES = 347
|
|
58
|
+
JPEG_INTERCHANGE_FORMAT = 513
|
|
59
|
+
JPEG_INTERCHANGE_FORMAT_LENGTH = 514
|
|
60
|
+
YCBCR_SUBSAMPLING = 530
|
|
61
|
+
X_RESOLUTION = 282
|
|
62
|
+
Y_RESOLUTION = 283
|
|
63
|
+
RESOLUTION_UNIT = 296
|
|
64
|
+
# Tags read by the decoder (a directory keeps no others).
|
|
65
|
+
KNOWN_TAGS = [
|
|
66
|
+
NEW_SUBFILE_TYPE, SUBFILE_TYPE, IMAGE_WIDTH, IMAGE_LENGTH, BITS_PER_SAMPLE, COMPRESSION, PHOTOMETRIC,
|
|
67
|
+
FILL_ORDER, STRIP_OFFSETS, SAMPLES_PER_PIXEL, ROWS_PER_STRIP, STRIP_BYTE_COUNTS, PLANAR_CONFIGURATION,
|
|
68
|
+
T4_OPTIONS, PREDICTOR, COLOR_MAP, TILE_WIDTH, TILE_LENGTH, TILE_OFFSETS, TILE_BYTE_COUNTS, INK_SET,
|
|
69
|
+
EXTRA_SAMPLES, SAMPLE_FORMAT, JPEG_TABLES, JPEG_INTERCHANGE_FORMAT, JPEG_INTERCHANGE_FORMAT_LENGTH,
|
|
70
|
+
YCBCR_SUBSAMPLING, X_RESOLUTION, Y_RESOLUTION, RESOLUTION_UNIT
|
|
71
|
+
].to_h { |tag| [tag, true] }.freeze
|
|
72
|
+
# Directories (pages) followed in one file, and directory entries
|
|
73
|
+
# read in all: crafted files can chain overlapping directories.
|
|
74
|
+
MAX_DIRECTORIES = 65_536
|
|
75
|
+
MAX_ENTRIES = 4_000_000
|
|
76
|
+
# Pixel aspect ratios beyond this (or its inverse) come from broken
|
|
77
|
+
# resolution values and are reported as 1.0.
|
|
78
|
+
MAX_ASPECT = 8.0
|
|
79
|
+
|
|
80
|
+
# Field type -> byte size.
|
|
81
|
+
TYPE_SIZES = {
|
|
82
|
+
1 => 1, 2 => 1, 3 => 2, 4 => 4, 5 => 8, 6 => 1, 7 => 1, 8 => 2, 9 => 4, 10 => 8,
|
|
83
|
+
11 => 4, 12 => 8, 13 => 4, 16 => 8, 17 => 8, 18 => 8
|
|
84
|
+
}.freeze
|
|
85
|
+
# Rational / floating point field type -> unpack directive, little
|
|
86
|
+
# and big endian (rationals: numerator and denominator).
|
|
87
|
+
NUMBER_DIRECTIVES_LE = { 5 => "V2", 10 => "l<2", 11 => "e", 12 => "E" }.freeze
|
|
88
|
+
NUMBER_DIRECTIVES_BE = { 5 => "N2", 10 => "l>2", 11 => "g", 12 => "G" }.freeze
|
|
89
|
+
# Integer field type -> unpack directive, little and big endian.
|
|
90
|
+
INT_DIRECTIVES_LE = {
|
|
91
|
+
1 => "C*", 3 => "v*", 4 => "V*", 6 => "c*", 7 => "C*", 8 => "s<*", 9 => "l<*", 13 => "V*",
|
|
92
|
+
16 => "Q<*", 17 => "q<*", 18 => "Q<*"
|
|
93
|
+
}.freeze
|
|
94
|
+
INT_DIRECTIVES_BE = {
|
|
95
|
+
1 => "C*", 3 => "n*", 4 => "N*", 6 => "c*", 7 => "C*", 8 => "s>*", 9 => "l>*", 13 => "N*",
|
|
96
|
+
16 => "Q>*", 17 => "q>*", 18 => "Q>*"
|
|
97
|
+
}.freeze
|
|
98
|
+
|
|
99
|
+
UNSUPPORTED_COMPRESSION = {
|
|
100
|
+
9 => "JBIG (T.85)", 10 => "JBIG2 (T.43)", 32_766 => "NeXT RLE", 32_771 => "CCITT RLE (word-aligned)",
|
|
101
|
+
32_809 => "ThunderScan RLE", 34_661 => "JBIG", 34_676 => "SGI LogLuv", 34_677 => "SGI LogLuv",
|
|
102
|
+
34_712 => "JPEG 2000", 34_887 => "LERC", 34_892 => "lossy JPEG (DNG)", 34_925 => "LZMA",
|
|
103
|
+
50_000 => "Zstandard", 50_001 => "WebP", 50_002 => "JPEG XL"
|
|
104
|
+
}.freeze
|
|
105
|
+
|
|
106
|
+
WHITE = "\xFF".b
|
|
107
|
+
SOI = "\xFF\xD8".b
|
|
108
|
+
EOI = "\xFF\xD9".b
|
|
109
|
+
# Adobe APP14 segment without its transform byte (marks JPEG data as RGB).
|
|
110
|
+
ADOBE = "\xFF\xEE\x00\x0EAdobe\x00\x64\x00\x00\x00\x00".b
|
|
111
|
+
HEX_DIGITS = "0123456789abcdef".b
|
|
112
|
+
ALL_BYTES = (0..255).to_a.freeze
|
|
113
|
+
# Byte -> byte with its bits reversed (FillOrder 2), as String#tr tables.
|
|
114
|
+
REVERSE_FROM = Image::TR_ESCAPE.call(ALL_BYTES)
|
|
115
|
+
REVERSE_TO = Image::TR_ESCAPE.call(ALL_BYTES.map { |b| format("%08b", b).reverse.to_i(2) })
|
|
116
|
+
# Byte -> its four 2-bit samples as bytes 0..3.
|
|
117
|
+
TWO_BIT = Array.new(256) { |b| [b >> 6, (b >> 4) & 3, (b >> 2) & 3, b & 3].pack("C4").freeze }.freeze
|
|
118
|
+
# Input slice size for Zlib, so that a decompression bomb stops early.
|
|
119
|
+
INFLATE_SLICE = 16_384
|
|
120
|
+
# Largest decompressed strip or tile, and largest buffer of unpacked
|
|
121
|
+
# samples of one, whatever sizes a (malicious) header declares: a
|
|
122
|
+
# chunk is held in memory while it is converted to luminance.
|
|
123
|
+
CHUNK_LIMIT = 1 << 28
|
|
124
|
+
# Unpack directives (little, big endian) of wide samples for predictor 2.
|
|
125
|
+
WORDS = { 16 => %w[v* n*], 32 => %w[V* N*], 64 => %w[Q<* Q>*] }.freeze
|
|
126
|
+
# Unpack directives of floating point samples by [bits, big endian].
|
|
127
|
+
FLOATS = { [32, false] => "e*", [32, true] => "g*", [64, false] => "E*", [64, true] => "G*" }.freeze
|
|
128
|
+
|
|
129
|
+
# An empty binary String with room for +size+ bytes. Hot methods call
|
|
130
|
+
# this rather than String.new(capacity:, encoding:) themselves: YJIT
|
|
131
|
+
# side-exits on that keyword-argument call for the rest of the method.
|
|
132
|
+
module Bytes
|
|
133
|
+
def self.buffer(size)
|
|
134
|
+
String.new(capacity: size, encoding: Encoding::BINARY)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
class << self
|
|
139
|
+
# Decodes page +page+ (0-based; negative counts from the end) to a
|
|
140
|
+
# luminance Image. Raises IndexError for a page that does not exist.
|
|
141
|
+
def decode(bytes, page: 0)
|
|
142
|
+
Container.new(bytes).page(page).decode
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Number of pages.
|
|
146
|
+
def page_count(bytes)
|
|
147
|
+
Container.new(bytes).pages.size
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Yields each page as an Image, decoding it only when it is reached.
|
|
151
|
+
# Returns an Enumerator (sized by page_count) without a block.
|
|
152
|
+
def each_page(bytes)
|
|
153
|
+
return enum_for(:each_page, bytes) { page_count(bytes) } unless block_given?
|
|
154
|
+
|
|
155
|
+
container = Container.new(bytes)
|
|
156
|
+
container.pages.each_index { |i| yield container.page(i).decode }
|
|
157
|
+
nil
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# [width, height] of a page, read from its directory only.
|
|
161
|
+
def dimensions(bytes, page: 0)
|
|
162
|
+
Container.new(bytes).page(page).dimensions
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# What a page's directory says about it, without decoding it:
|
|
166
|
+
# {page:, width:, height:, pixel_aspect:, dpi:}. +pixel_aspect+ is
|
|
167
|
+
# the width of a pixel divided by its height (YResolution /
|
|
168
|
+
# XResolution: 0.48 for a 204 x 98 dpi fax; 1.0 when unknown);
|
|
169
|
+
# +dpi+ is [x, y] (nil without a resolution in inches or cm).
|
|
170
|
+
def info(bytes, page: 0)
|
|
171
|
+
container = Container.new(bytes)
|
|
172
|
+
index = page.is_a?(Integer) && page.negative? ? container.pages.size + page : page
|
|
173
|
+
container.page(page).info(index)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Yields each decodable page as an Image with its info (see #info),
|
|
177
|
+
# decoding one page at a time. Pages that cannot be decoded are
|
|
178
|
+
# skipped (+info[:page]+ tells which page an image is); only a file
|
|
179
|
+
# without any decodable page raises (UnsupportedImageError if a page
|
|
180
|
+
# was unsupported, else ImageError). Returns an Enumerator without a
|
|
181
|
+
# block.
|
|
182
|
+
def each_image(bytes)
|
|
183
|
+
return enum_for(:each_image, bytes) unless block_given?
|
|
184
|
+
|
|
185
|
+
container = Container.new(bytes)
|
|
186
|
+
failure = nil
|
|
187
|
+
decoded = 0
|
|
188
|
+
container.pages.each_index do |i|
|
|
189
|
+
page = begin
|
|
190
|
+
container.page(i)
|
|
191
|
+
rescue Error => e
|
|
192
|
+
failure = pick_failure(failure, e)
|
|
193
|
+
next
|
|
194
|
+
end
|
|
195
|
+
image = begin
|
|
196
|
+
page.decode
|
|
197
|
+
rescue Error => e
|
|
198
|
+
failure = pick_failure(failure, e)
|
|
199
|
+
next
|
|
200
|
+
end
|
|
201
|
+
decoded += 1
|
|
202
|
+
yield image, page.info(i)
|
|
203
|
+
end
|
|
204
|
+
raise failure if decoded.zero? && failure
|
|
205
|
+
|
|
206
|
+
nil
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
private
|
|
210
|
+
|
|
211
|
+
# The error to raise when no page decodes: the first one, or the
|
|
212
|
+
# first UnsupportedImageError.
|
|
213
|
+
def pick_failure(failure, error)
|
|
214
|
+
return error if failure.nil?
|
|
215
|
+
return error if error.is_a?(UnsupportedImageError) && !failure.is_a?(UnsupportedImageError)
|
|
216
|
+
|
|
217
|
+
failure
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# The file header and the chain of image file directories (IFDs).
|
|
222
|
+
class Container
|
|
223
|
+
attr_reader :bytes
|
|
224
|
+
|
|
225
|
+
def initialize(bytes)
|
|
226
|
+
raise ImageError, "expected a String of TIFF data, got #{bytes.class}" unless bytes.is_a?(String)
|
|
227
|
+
|
|
228
|
+
@bytes = bytes.encoding == Encoding::BINARY ? bytes : bytes.b
|
|
229
|
+
raise ImageError, "truncated TIFF header" if @bytes.bytesize < 8
|
|
230
|
+
|
|
231
|
+
case @bytes.byteslice(0, 4)
|
|
232
|
+
when "II*\0" then @le = true
|
|
233
|
+
when "MM\0*" then @le = false
|
|
234
|
+
when "II+\0" then @le = @big = true
|
|
235
|
+
when "MM\0+"
|
|
236
|
+
@le = false
|
|
237
|
+
@big = true
|
|
238
|
+
else raise ImageError, "not a TIFF file"
|
|
239
|
+
end
|
|
240
|
+
@directives = @le ? INT_DIRECTIVES_LE : INT_DIRECTIVES_BE
|
|
241
|
+
if @big
|
|
242
|
+
raise ImageError, "unsupported BigTIFF offset size" unless @bytes.bytesize >= 16 && uint(4, 2) == 8
|
|
243
|
+
|
|
244
|
+
@first = uint(8, 8)
|
|
245
|
+
else
|
|
246
|
+
@first = uint(4, 4)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def little_endian?
|
|
251
|
+
@le
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Directories of the pages, in file order.
|
|
255
|
+
def pages
|
|
256
|
+
@pages ||= begin
|
|
257
|
+
dirs = directories
|
|
258
|
+
raise ImageError, "TIFF file contains no image directory" if dirs.empty?
|
|
259
|
+
|
|
260
|
+
main = dirs.reject(&:subsidiary?)
|
|
261
|
+
main.empty? ? dirs : main
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def page(index)
|
|
266
|
+
dir = pages[index] if index.is_a?(Integer)
|
|
267
|
+
raise IndexError, "TIFF page #{index.inspect} out of range (#{pages.size} pages)" unless dir
|
|
268
|
+
|
|
269
|
+
Page.new(self, dir)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Unsigned little/big endian integer of +size+ bytes at +pos+.
|
|
273
|
+
def uint(pos, size)
|
|
274
|
+
v = 0
|
|
275
|
+
if @le
|
|
276
|
+
i = pos + size - 1
|
|
277
|
+
while i >= pos
|
|
278
|
+
v = (v << 8) | @bytes.getbyte(i)
|
|
279
|
+
i -= 1
|
|
280
|
+
end
|
|
281
|
+
else
|
|
282
|
+
stop = pos + size
|
|
283
|
+
while pos < stop
|
|
284
|
+
v = (v << 8) | @bytes.getbyte(pos)
|
|
285
|
+
pos += 1
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
v
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Integer values of a field (nil for non-integer types or values
|
|
292
|
+
# outside the file), at most +limit+ of them.
|
|
293
|
+
def ints(type, count, pos, limit = nil)
|
|
294
|
+
directive = @directives[type] or return nil
|
|
295
|
+
size = count * TYPE_SIZES[type]
|
|
296
|
+
return nil if pos + size > @bytes.bytesize
|
|
297
|
+
|
|
298
|
+
size = limit * TYPE_SIZES[type] if limit && count > limit
|
|
299
|
+
@bytes.byteslice(pos, size).unpack(directive)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# First value of a numeric field (rationals and floating point
|
|
303
|
+
# included) as a Float, or nil.
|
|
304
|
+
def number(type, count, pos)
|
|
305
|
+
return nil unless count.positive?
|
|
306
|
+
|
|
307
|
+
directive = (@le ? NUMBER_DIRECTIVES_LE : NUMBER_DIRECTIVES_BE)[type]
|
|
308
|
+
return ints(type, 1, pos)&.first&.to_f unless directive
|
|
309
|
+
return nil if pos + TYPE_SIZES[type] > @bytes.bytesize
|
|
310
|
+
|
|
311
|
+
num, den = @bytes.unpack(directive, offset: pos)
|
|
312
|
+
value = den ? (den.zero? ? nil : num.fdiv(den)) : num
|
|
313
|
+
value&.finite? ? value : nil
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Raw bytes of a field.
|
|
317
|
+
def blob(type, count, pos)
|
|
318
|
+
size = count * TYPE_SIZES[type]
|
|
319
|
+
return nil if pos + size > @bytes.bytesize
|
|
320
|
+
|
|
321
|
+
@bytes.byteslice(pos, size)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
private
|
|
325
|
+
|
|
326
|
+
# Follows the IFD chain (stops at loops, offsets outside the file
|
|
327
|
+
# and after MAX_DIRECTORIES directories or MAX_ENTRIES entries).
|
|
328
|
+
def directories
|
|
329
|
+
dirs = []
|
|
330
|
+
seen = {}
|
|
331
|
+
offset = @first
|
|
332
|
+
@entries_left = MAX_ENTRIES
|
|
333
|
+
while offset.positive? && !seen[offset] && dirs.size < MAX_DIRECTORIES
|
|
334
|
+
seen[offset] = true
|
|
335
|
+
dir = read_directory(offset) or break
|
|
336
|
+
dirs << dir
|
|
337
|
+
offset = dir.next_offset
|
|
338
|
+
end
|
|
339
|
+
dirs
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def read_directory(offset)
|
|
343
|
+
size = @bytes.bytesize
|
|
344
|
+
count_size, entry_size, value_size = @big ? [8, 20, 8] : [2, 12, 4]
|
|
345
|
+
return nil if offset + count_size > size
|
|
346
|
+
|
|
347
|
+
count = uint(offset, count_size)
|
|
348
|
+
start = offset + count_size
|
|
349
|
+
return nil if count.zero? || start + count * entry_size > size || (@entries_left -= count).negative?
|
|
350
|
+
|
|
351
|
+
entries = {}
|
|
352
|
+
count.times do |i|
|
|
353
|
+
pos = start + i * entry_size
|
|
354
|
+
tag = uint(pos, 2)
|
|
355
|
+
next unless KNOWN_TAGS[tag]
|
|
356
|
+
|
|
357
|
+
type = uint(pos + 2, 2)
|
|
358
|
+
n = uint(pos + 4, value_size)
|
|
359
|
+
type_size = TYPE_SIZES[type]
|
|
360
|
+
next if entries.key?(tag) || !type_size
|
|
361
|
+
|
|
362
|
+
value_pos = pos + 4 + value_size
|
|
363
|
+
value_pos = uint(value_pos, value_size) if n * type_size > value_size
|
|
364
|
+
entries[tag] = [type, n, value_pos]
|
|
365
|
+
end
|
|
366
|
+
next_pos = start + count * entry_size
|
|
367
|
+
next_offset = next_pos + value_size <= size ? uint(next_pos, value_size) : 0
|
|
368
|
+
Directory.new(self, entries, next_offset)
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# One image file directory: tag -> [type, count, value position].
|
|
373
|
+
class Directory
|
|
374
|
+
attr_reader :next_offset
|
|
375
|
+
|
|
376
|
+
def initialize(container, entries, next_offset)
|
|
377
|
+
@container = container
|
|
378
|
+
@entries = entries
|
|
379
|
+
@next_offset = next_offset
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def include?(tag)
|
|
383
|
+
@entries.key?(tag)
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# Integer values of +tag+ (at most +limit+), or nil.
|
|
387
|
+
def ints(tag, limit = nil)
|
|
388
|
+
entry = @entries[tag] or return nil
|
|
389
|
+
@container.ints(*entry, limit)
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# First integer value of +tag+, or +default+.
|
|
393
|
+
def int(tag, default = nil)
|
|
394
|
+
ints(tag, 1)&.first || default
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# First value of +tag+ as a Float (rationals too), or nil.
|
|
398
|
+
def number(tag)
|
|
399
|
+
entry = @entries[tag] or return nil
|
|
400
|
+
@container.number(*entry)
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# Raw bytes of +tag+ (JPEGTables), or nil.
|
|
404
|
+
def blob(tag)
|
|
405
|
+
entry = @entries[tag] or return nil
|
|
406
|
+
@container.blob(*entry)
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# Reduced-resolution image or transparency mask, not a page.
|
|
410
|
+
def subsidiary?
|
|
411
|
+
int(NEW_SUBFILE_TYPE, 0) & 5 != 0 || int(SUBFILE_TYPE) == 2
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# Decodes one page. Strips and tiles ("chunks") are decompressed to
|
|
416
|
+
# sample rows, normalised to one byte per sample and converted to
|
|
417
|
+
# luminance; bilevel CCITT chunks and colour JPEG chunks yield
|
|
418
|
+
# luminance directly.
|
|
419
|
+
class Page
|
|
420
|
+
attr_reader :width, :height
|
|
421
|
+
|
|
422
|
+
def initialize(container, dir)
|
|
423
|
+
@bytes = container.bytes
|
|
424
|
+
@le = container.little_endian?
|
|
425
|
+
@dir = dir
|
|
426
|
+
@width = dir.int(IMAGE_WIDTH)
|
|
427
|
+
@height = dir.int(IMAGE_LENGTH)
|
|
428
|
+
raise ImageError, "TIFF page without image dimensions" unless @width && @height
|
|
429
|
+
|
|
430
|
+
raise ImageError, "invalid TIFF dimensions #{@width}x#{@height}" unless @width.positive? && @height.positive?
|
|
431
|
+
return if @width * @height <= MAX_PIXELS
|
|
432
|
+
|
|
433
|
+
raise UnsupportedImageError, "TIFF image too large (#{@width}x#{@height}, limit #{MAX_PIXELS} pixels)"
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def dimensions
|
|
437
|
+
[@width, @height]
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# {page:, width:, height:, pixel_aspect:, dpi:} (see TIFF.info).
|
|
441
|
+
def info(index)
|
|
442
|
+
x = @dir.number(X_RESOLUTION)
|
|
443
|
+
y = @dir.number(Y_RESOLUTION)
|
|
444
|
+
x = nil unless x&.positive?
|
|
445
|
+
y = nil unless y&.positive?
|
|
446
|
+
x ||= y
|
|
447
|
+
y ||= x
|
|
448
|
+
aspect = x ? y / x : 1.0
|
|
449
|
+
aspect = 1.0 unless aspect.between?(1 / MAX_ASPECT, MAX_ASPECT)
|
|
450
|
+
scale = case @dir.int(RESOLUTION_UNIT, 2)
|
|
451
|
+
when 2 then 1.0
|
|
452
|
+
when 3 then 2.54
|
|
453
|
+
end
|
|
454
|
+
dpi = [(x * scale).round(2), (y * scale).round(2)] if x && scale
|
|
455
|
+
{ page: index, width: @width, height: @height, pixel_aspect: aspect.round(6), dpi: dpi }
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
def decode
|
|
459
|
+
setup
|
|
460
|
+
@decoded = 0
|
|
461
|
+
data = if @codec == :ojpeg then old_jpeg
|
|
462
|
+
elsif @planes then decode_planar
|
|
463
|
+
else assemble(0, WHITE) { |src, cw, rows| chunk_luma(src, cw, rows) }
|
|
464
|
+
end
|
|
465
|
+
raise ImageError, "TIFF page has no decodable image data" if @decoded.zero?
|
|
466
|
+
|
|
467
|
+
Image.new(@width, @height, data)
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
private
|
|
471
|
+
|
|
472
|
+
# --- parameters ---------------------------------------------------
|
|
473
|
+
|
|
474
|
+
def setup
|
|
475
|
+
d = @dir
|
|
476
|
+
@spp = d.int(SAMPLES_PER_PIXEL, 1)
|
|
477
|
+
raise ImageError, "invalid TIFF samples per pixel #{@spp}" unless @spp.between?(1, 64)
|
|
478
|
+
|
|
479
|
+
depths = d.ints(BITS_PER_SAMPLE, 64) || [1]
|
|
480
|
+
@bps = depths.first
|
|
481
|
+
raise ImageError, "invalid TIFF bits per sample #{@bps}" unless @bps&.between?(1, 64)
|
|
482
|
+
unless depths.first(@spp).uniq.size == 1
|
|
483
|
+
raise UnsupportedImageError, "TIFF images with different bit depths per sample are not supported"
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
@compression = d.int(COMPRESSION, 1)
|
|
487
|
+
@photometric = d.int(PHOTOMETRIC) || default_photometric
|
|
488
|
+
@fill_order = d.int(FILL_ORDER, 1)
|
|
489
|
+
@predictor = d.int(PREDICTOR, 1)
|
|
490
|
+
@sample_format = d.int(SAMPLE_FORMAT, 1)
|
|
491
|
+
setup_layout
|
|
492
|
+
setup_codec
|
|
493
|
+
return if %i[ojpeg fax].include?(@codec)
|
|
494
|
+
|
|
495
|
+
setup_color
|
|
496
|
+
check_chunk_size unless @codec == :jpeg
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# Decompressed chunks and unpacked low-depth samples are held in
|
|
500
|
+
# memory whole (an uncompressed chunk is a slice of the file).
|
|
501
|
+
def check_chunk_size
|
|
502
|
+
nsamp = planar? ? 1 : @spp
|
|
503
|
+
raw = @codec == :none ? 0 : chunk_size(@chunk_w, @chunk_h, nsamp)
|
|
504
|
+
samples = @bps < 8 ? @chunk_w * @chunk_h * nsamp : 0
|
|
505
|
+
return if raw <= CHUNK_LIMIT && samples <= CHUNK_LIMIT
|
|
506
|
+
|
|
507
|
+
raise UnsupportedImageError,
|
|
508
|
+
"TIFF strips or tiles too large (#{@chunk_w}x#{@chunk_h}, #{nsamp} samples of #{@bps} bits per pixel)"
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
def default_photometric
|
|
512
|
+
if [2, 3, 4].include?(@dir.int(COMPRESSION)) then 0
|
|
513
|
+
elsif @spp >= 3 then 2
|
|
514
|
+
elsif @dir.include?(COLOR_MAP) then 3
|
|
515
|
+
else 1
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def setup_layout
|
|
520
|
+
d = @dir
|
|
521
|
+
w = @width
|
|
522
|
+
h = @height
|
|
523
|
+
if d.include?(TILE_WIDTH)
|
|
524
|
+
tw = d.int(TILE_WIDTH)
|
|
525
|
+
tl = d.int(TILE_LENGTH)
|
|
526
|
+
raise ImageError, "invalid TIFF tile size" unless tw&.positive? && tl&.positive?
|
|
527
|
+
|
|
528
|
+
@tiled = true
|
|
529
|
+
@chunk_w = tw
|
|
530
|
+
@chunk_h = tl
|
|
531
|
+
@across = (w + tw - 1) / tw
|
|
532
|
+
@down = (h + tl - 1) / tl
|
|
533
|
+
if @across * @down * tw * tl > 2 * MAX_PIXELS
|
|
534
|
+
raise UnsupportedImageError, "TIFF tiles too large (#{tw}x#{tl} for a #{w}x#{h} image)"
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
@offsets = d.ints(TILE_OFFSETS) || d.ints(STRIP_OFFSETS)
|
|
538
|
+
@counts = d.ints(TILE_BYTE_COUNTS) || d.ints(STRIP_BYTE_COUNTS)
|
|
539
|
+
else
|
|
540
|
+
rps = d.int(ROWS_PER_STRIP, h)
|
|
541
|
+
rps = h unless rps.positive? && rps < h
|
|
542
|
+
@tiled = false
|
|
543
|
+
@chunk_w = w
|
|
544
|
+
@chunk_h = rps
|
|
545
|
+
@across = 1
|
|
546
|
+
@down = (h + rps - 1) / rps
|
|
547
|
+
@offsets = d.ints(STRIP_OFFSETS)
|
|
548
|
+
@counts = d.ints(STRIP_BYTE_COUNTS)
|
|
549
|
+
end
|
|
550
|
+
raise ImageError, "TIFF page without image data offsets" unless @offsets
|
|
551
|
+
|
|
552
|
+
@per_plane = @across * @down
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
def setup_codec
|
|
556
|
+
case @compression
|
|
557
|
+
when 1 then @codec = :none
|
|
558
|
+
when 2, 3, 4 then setup_fax
|
|
559
|
+
when 5 then @codec = :lzw
|
|
560
|
+
when 6 then @codec = :ojpeg
|
|
561
|
+
when 7
|
|
562
|
+
@codec = :jpeg
|
|
563
|
+
@jpeg_tables = @dir.blob(JPEG_TABLES)
|
|
564
|
+
when 8, 32_946 then @codec = :deflate
|
|
565
|
+
when 32_773 then @codec = :packbits
|
|
566
|
+
else
|
|
567
|
+
name = UNSUPPORTED_COMPRESSION.fetch(@compression) { "compression #{@compression}" }
|
|
568
|
+
raise UnsupportedImageError, "#{name} TIFF images are not supported"
|
|
569
|
+
end
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def setup_fax
|
|
573
|
+
raise ImageError, "CCITT-compressed TIFF must be 1 bit per pixel" unless @bps == 1 && @spp == 1
|
|
574
|
+
|
|
575
|
+
@codec = :fax
|
|
576
|
+
t4 = @dir.int(T4_OPTIONS, 0)
|
|
577
|
+
@fax = case @compression
|
|
578
|
+
when 2 then { k: 0, encoded_byte_align: true }
|
|
579
|
+
when 3 then { k: t4 & 1, encoded_byte_align: t4 & 4 != 0, end_of_line: true }
|
|
580
|
+
else { k: -1 }
|
|
581
|
+
end
|
|
582
|
+
@fax[:fill_order] = @fill_order
|
|
583
|
+
@fax[:black_is_1] = @photometric == 1 # coded white runs are 0 bits
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
# Chooses the luminance conversion (@mode), which samples it reads
|
|
587
|
+
# and the per-value mapping applied while unpacking samples.
|
|
588
|
+
def setup_color
|
|
589
|
+
ph = @photometric
|
|
590
|
+
spp = @spp
|
|
591
|
+
extra = @dir.ints(EXTRA_SAMPLES, 64) || []
|
|
592
|
+
@invert = false
|
|
593
|
+
lut = nil
|
|
594
|
+
color = case ph
|
|
595
|
+
when 0, 1
|
|
596
|
+
@invert = ph.zero?
|
|
597
|
+
1
|
|
598
|
+
when 2 then 3
|
|
599
|
+
when 3
|
|
600
|
+
lut = palette
|
|
601
|
+
1
|
|
602
|
+
when 5 then @dir.int(INK_SET, 1) == 1 && spp >= 4 ? 4 : 1
|
|
603
|
+
when 6 then 3
|
|
604
|
+
when 8, 9, 10 then 1 # L* only
|
|
605
|
+
else raise UnsupportedImageError, "TIFF photometric interpretation #{ph} is not supported"
|
|
606
|
+
end
|
|
607
|
+
raise ImageError, "TIFF photometric #{ph} needs #{color} samples per pixel, got #{spp}" if spp < color
|
|
608
|
+
|
|
609
|
+
alpha = spp > color && [1, 2].include?(extra.first) ? spp - extra.size : nil
|
|
610
|
+
alpha = nil if alpha && alpha < color
|
|
611
|
+
@associated = extra.first == 1
|
|
612
|
+
@invert = true if ph == 5 && color == 1 # a single ink: 0 is white
|
|
613
|
+
|
|
614
|
+
@mode = if @codec == :jpeg && spp > 1 && !planar? then :luma
|
|
615
|
+
elsif ph == 6 then planar? ? :single : :ycbcr
|
|
616
|
+
elsif color == 4 then :cmyk
|
|
617
|
+
elsif color == 3 && ph == 2 then alpha ? :rgba : :rgb
|
|
618
|
+
elsif alpha && lut.nil? && [0, 1].include?(ph) then :gray_alpha
|
|
619
|
+
else spp == 1 ? :single : :pick
|
|
620
|
+
end
|
|
621
|
+
setup_planes(alpha)
|
|
622
|
+
setup_samples(lut)
|
|
623
|
+
@adobe = 0 if @mode == :luma && ph == 2 # JPEG data holds RGB, not YCbCr
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
def planar?
|
|
627
|
+
@dir.int(PLANAR_CONFIGURATION, 1) == 2 && @spp > 1
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
# Sample offsets within a pixel (@channels) for the conversion and,
|
|
631
|
+
# for planar data, which planes are needed.
|
|
632
|
+
def setup_planes(alpha)
|
|
633
|
+
used = case @mode
|
|
634
|
+
when :rgb then [0, 1, 2]
|
|
635
|
+
when :rgba then [0, 1, 2, alpha]
|
|
636
|
+
when :cmyk then [0, 1, 2, 3]
|
|
637
|
+
when :gray_alpha then [0, alpha]
|
|
638
|
+
else [0]
|
|
639
|
+
end
|
|
640
|
+
if planar?
|
|
641
|
+
@planes = used
|
|
642
|
+
@channels = (0...used.size).to_a
|
|
643
|
+
@stride = used.size
|
|
644
|
+
@mode = :single if used.size == 1
|
|
645
|
+
else
|
|
646
|
+
@channels = used
|
|
647
|
+
@stride = @spp
|
|
648
|
+
end
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
# Mapping of unpacked sample values to output bytes: scaling of low
|
|
652
|
+
# bit depths, palette lookup, WhiteIsZero inversion, signed samples.
|
|
653
|
+
def setup_samples(lut)
|
|
654
|
+
bps = @bps
|
|
655
|
+
@float = @sample_format == 3
|
|
656
|
+
if @float && ![32, 64].include?(bps)
|
|
657
|
+
raise UnsupportedImageError, "#{bps}-bit floating point TIFF samples are not supported"
|
|
658
|
+
end
|
|
659
|
+
raise UnsupportedImageError, "TIFF palettes with more than 8 bits are not supported" if lut && bps > 8
|
|
660
|
+
if @predictor == 2 && bps != 8 && !WORDS.key?(bps)
|
|
661
|
+
raise UnsupportedImageError, "TIFF predictor 2 with #{bps}-bit samples is not supported"
|
|
662
|
+
end
|
|
663
|
+
unless [1, 2, 3].include?(@predictor)
|
|
664
|
+
raise UnsupportedImageError, "TIFF predictor #{@predictor} is not supported"
|
|
665
|
+
end
|
|
666
|
+
raise UnsupportedImageError, "TIFF predictor 3 needs floating point samples" if @predictor == 3 && !@float
|
|
667
|
+
raise UnsupportedImageError, "JPEG-compressed TIFF must have 8-bit samples" if @codec == :jpeg && bps != 8
|
|
668
|
+
|
|
669
|
+
signed = @sample_format == 2 && bps >= 8
|
|
670
|
+
final = %i[single pick].include?(@mode)
|
|
671
|
+
max = (1 << bps) - 1
|
|
672
|
+
levels = bps < 8 ? max + 1 : 256
|
|
673
|
+
map = Array.new(levels) do |v|
|
|
674
|
+
if lut then lut[v]
|
|
675
|
+
else
|
|
676
|
+
s = bps < 8 ? (v * 255 + max / 2) / max : v
|
|
677
|
+
s ^= 0x80 if signed
|
|
678
|
+
final && @invert ? 255 - s : s
|
|
679
|
+
end
|
|
680
|
+
end
|
|
681
|
+
from = case bps
|
|
682
|
+
when 1 then "01".b
|
|
683
|
+
when 4 then HEX_DIGITS
|
|
684
|
+
else ALL_BYTES.first(levels).pack("C*")
|
|
685
|
+
end
|
|
686
|
+
@fused_rgb = @mode == :rgb && @predictor == 2 && bps == 8 && !planar? && map == ALL_BYTES
|
|
687
|
+
return if map == ALL_BYTES
|
|
688
|
+
|
|
689
|
+
@tr_from = Image::TR_ESCAPE.call(from.bytes)
|
|
690
|
+
@tr_to = Image::TR_ESCAPE.call(map)
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
# Palette entry -> luminance. 16-bit colour map values; files with
|
|
694
|
+
# 8-bit values (all below 256) are accepted like libtiff does.
|
|
695
|
+
def palette
|
|
696
|
+
n = 1 << [@bps, 8].min
|
|
697
|
+
cmap = @dir.ints(COLOR_MAP, 3 * 256)
|
|
698
|
+
raise ImageError, "TIFF palette image without a valid colour map" unless cmap && cmap.size >= 3 * n
|
|
699
|
+
|
|
700
|
+
shift = cmap.first(3 * n).max < 256 ? 0 : 8
|
|
701
|
+
# (Values of the wrong type - signed, 32-bit - are clamped.)
|
|
702
|
+
v = ->(i) { (cmap[i] >> shift).clamp(0, 255) }
|
|
703
|
+
Array.new(n) { |i| Image.luma(v.call(i), v.call(n + i), v.call(2 * n + i)) }
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
# --- chunk layout -------------------------------------------------
|
|
707
|
+
|
|
708
|
+
# Decodes the chunks of one plane into a width * height buffer. The
|
|
709
|
+
# block turns a chunk's data into rows of +cw+ bytes (possibly fewer
|
|
710
|
+
# rows than asked for); missing parts are filled with +fill+.
|
|
711
|
+
def assemble(plane, fill)
|
|
712
|
+
w = @width
|
|
713
|
+
h = @height
|
|
714
|
+
cw = @chunk_w
|
|
715
|
+
ch = @chunk_h
|
|
716
|
+
base = plane * @per_plane
|
|
717
|
+
# Chunks without an offset are missing: no need to visit them.
|
|
718
|
+
count = (@offsets.size - base).clamp(0, @per_plane)
|
|
719
|
+
if @tiled
|
|
720
|
+
out = fill * (w * h)
|
|
721
|
+
count.times do |i|
|
|
722
|
+
data = chunk_data(base + i) or next
|
|
723
|
+
x0 = (i % @across) * cw
|
|
724
|
+
y0 = (i / @across) * ch
|
|
725
|
+
part = yield(data, cw, ch) or next
|
|
726
|
+
vis_w = [cw, w - x0].min
|
|
727
|
+
rows = [part.bytesize / cw, h - y0].min
|
|
728
|
+
rows.times { |r| out.bytesplice((y0 + r) * w + x0, vis_w, part.byteslice(r * cw, vis_w)) }
|
|
729
|
+
end
|
|
730
|
+
else
|
|
731
|
+
out = Bytes.buffer(w * h)
|
|
732
|
+
count.times do |i|
|
|
733
|
+
rows = [ch, h - i * ch].min
|
|
734
|
+
need = rows * w
|
|
735
|
+
data = chunk_data(base + i)
|
|
736
|
+
part = yield(data, w, rows) if data
|
|
737
|
+
if part.nil? || part.empty?
|
|
738
|
+
out << fill * need
|
|
739
|
+
elsif part.bytesize >= need
|
|
740
|
+
out << (part.bytesize == need ? part : part.byteslice(0, need))
|
|
741
|
+
else
|
|
742
|
+
out << part << fill * (need - part.bytesize)
|
|
743
|
+
end
|
|
744
|
+
end
|
|
745
|
+
out << fill * (w * h - out.bytesize)
|
|
746
|
+
end
|
|
747
|
+
out
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
# Bytes of chunk +index+ (shorter when the file is truncated), or nil.
|
|
751
|
+
def chunk_data(index)
|
|
752
|
+
off = @offsets[index]
|
|
753
|
+
size = @bytes.bytesize
|
|
754
|
+
return nil unless off&.positive? && off < size
|
|
755
|
+
|
|
756
|
+
count = @counts&.[](index)
|
|
757
|
+
return nil if count&.<= 0
|
|
758
|
+
|
|
759
|
+
# (64-bit counts may exceed what byteslice accepts.)
|
|
760
|
+
count = size - off if count.nil? || count > size - off
|
|
761
|
+
@bytes.byteslice(off, count)
|
|
762
|
+
end
|
|
763
|
+
|
|
764
|
+
# Luminance rows of one chunk (planar configuration 1).
|
|
765
|
+
def chunk_luma(data, cw, rows)
|
|
766
|
+
case @codec
|
|
767
|
+
when :fax then return fax_chunk(data, cw, rows)
|
|
768
|
+
when :jpeg
|
|
769
|
+
samples = jpeg_chunk(data, cw, rows) or return nil
|
|
770
|
+
return samples if @mode == :luma
|
|
771
|
+
|
|
772
|
+
avail = samples.bytesize / cw
|
|
773
|
+
samples.tr!(@tr_from, @tr_to) if @tr_from
|
|
774
|
+
else
|
|
775
|
+
raw = decompress(data, chunk_size(cw, rows, @spp))
|
|
776
|
+
return ycbcr_luma(raw, cw, rows) if @mode == :ycbcr
|
|
777
|
+
return predicted_rgb_luma(raw, cw, rows) if @fused_rgb
|
|
778
|
+
|
|
779
|
+
samples, avail = normalize(raw, cw, rows, @spp)
|
|
780
|
+
return nil if avail.zero?
|
|
781
|
+
|
|
782
|
+
@decoded += 1
|
|
783
|
+
end
|
|
784
|
+
convert(samples, cw, avail)
|
|
785
|
+
end
|
|
786
|
+
|
|
787
|
+
# Normalised samples of one chunk of one plane (planar configuration 2).
|
|
788
|
+
def plane_samples(data, cw, rows)
|
|
789
|
+
if @codec == :jpeg
|
|
790
|
+
samples = jpeg_chunk(data, cw, rows) or return nil
|
|
791
|
+
samples.tr!(@tr_from, @tr_to) if @tr_from
|
|
792
|
+
return samples
|
|
793
|
+
end
|
|
794
|
+
|
|
795
|
+
samples, avail = normalize(decompress(data, chunk_size(cw, rows, 1)), cw, rows, 1)
|
|
796
|
+
@decoded += 1 if avail.positive?
|
|
797
|
+
samples
|
|
798
|
+
end
|
|
799
|
+
|
|
800
|
+
def decode_planar
|
|
801
|
+
# Single planes hold luminance; otherwise white is the colour
|
|
802
|
+
# samples' white and opaque alpha.
|
|
803
|
+
alpha = @planes.last if %i[rgba gray_alpha].include?(@mode)
|
|
804
|
+
fills = @planes.map { |p| @mode == :single || p == alpha ? WHITE : white_sample }
|
|
805
|
+
planes = @planes.zip(fills).map do |p, fill|
|
|
806
|
+
assemble(p, fill) { |src, cw, rows| plane_samples(src, cw, rows) }
|
|
807
|
+
end
|
|
808
|
+
return planes[0] if @mode == :single
|
|
809
|
+
|
|
810
|
+
# Each row's planes are joined, so that the converters read them
|
|
811
|
+
# as channels +w+ bytes apart. Rows without data are white.
|
|
812
|
+
w = @width
|
|
813
|
+
@channels = Array.new(planes.size) { |i| i * w }
|
|
814
|
+
@stride = 1
|
|
815
|
+
blank = fills.map { |fill| fill * w }.join
|
|
816
|
+
white = WHITE * w
|
|
817
|
+
out = Bytes.buffer(w * @height)
|
|
818
|
+
@height.times do |y|
|
|
819
|
+
row = planes.map { |pl| pl.byteslice(y * w, w) }.join
|
|
820
|
+
if row == blank
|
|
821
|
+
out << white
|
|
822
|
+
else
|
|
823
|
+
convert_row(row, 0, w, out)
|
|
824
|
+
end
|
|
825
|
+
end
|
|
826
|
+
out
|
|
827
|
+
end
|
|
828
|
+
|
|
829
|
+
# The value of a white colour sample before conversion.
|
|
830
|
+
def white_sample
|
|
831
|
+
[0, 5].include?(@photometric) ? "\0".b : WHITE
|
|
832
|
+
end
|
|
833
|
+
|
|
834
|
+
# Expected decompressed size of a chunk.
|
|
835
|
+
def chunk_size(cw, rows, nsamp)
|
|
836
|
+
if @mode == :ycbcr
|
|
837
|
+
sh, sv = subsampling
|
|
838
|
+
((rows + sv - 1) / sv) * ((cw + sh - 1) / sh) * (sh * sv + 2)
|
|
839
|
+
else
|
|
840
|
+
((cw * nsamp * @bps + 7) >> 3) * rows
|
|
841
|
+
end
|
|
842
|
+
end
|
|
843
|
+
|
|
844
|
+
# --- codecs -------------------------------------------------------
|
|
845
|
+
|
|
846
|
+
def decompress(data, expected)
|
|
847
|
+
data = data.tr(REVERSE_FROM, REVERSE_TO) if @fill_order == 2
|
|
848
|
+
case @codec
|
|
849
|
+
when :none then data
|
|
850
|
+
when :lzw then lzw(data, expected)
|
|
851
|
+
when :deflate then inflate(data, expected)
|
|
852
|
+
when :packbits then packbits(data, expected)
|
|
853
|
+
end
|
|
854
|
+
end
|
|
855
|
+
|
|
856
|
+
# TIFF LZW: MSB-first codes with "early change"; data starting with
|
|
857
|
+
# 0x00 and an odd byte is the old LSB-first variant without early
|
|
858
|
+
# change (detected like libtiff does). The kernel is shared with PDF.
|
|
859
|
+
def lzw(data, expected)
|
|
860
|
+
compat = data.getbyte(0) == 0 && data.bytesize > 1 && data.getbyte(1).odd?
|
|
861
|
+
QrReaderKernels.lzw_decode(data, expected, compat ? 0 : 1, compat)
|
|
862
|
+
end
|
|
863
|
+
|
|
864
|
+
# Room to reserve for decompressed data: never what a header claims
|
|
865
|
+
# (String.new(capacity:) commits the memory), the buffer grows.
|
|
866
|
+
def initial_capacity(data, expected)
|
|
867
|
+
guess = 4 * data.bytesize + 4096
|
|
868
|
+
guess < expected ? guess : expected
|
|
869
|
+
end
|
|
870
|
+
|
|
871
|
+
def fax_chunk(data, cw, rows)
|
|
872
|
+
image = CCITT.decode(data, columns: cw, rows: rows, **@fax)
|
|
873
|
+
@decoded += 1
|
|
874
|
+
image.data
|
|
875
|
+
rescue UnsupportedImageError
|
|
876
|
+
raise
|
|
877
|
+
rescue ImageError
|
|
878
|
+
nil # not even the first row decodes: the chunk stays white
|
|
879
|
+
end
|
|
880
|
+
|
|
881
|
+
# Inflates zlib (or raw deflate) data; stops after +expected+ bytes
|
|
882
|
+
# and keeps what was decoded before an error.
|
|
883
|
+
def inflate(data, expected)
|
|
884
|
+
out = Bytes.buffer(initial_capacity(data, expected))
|
|
885
|
+
[Zlib::MAX_WBITS, -Zlib::MAX_WBITS].each do |window_bits|
|
|
886
|
+
zstream = Zlib::Inflate.new(window_bits)
|
|
887
|
+
pos = 0
|
|
888
|
+
while pos < data.bytesize && out.bytesize < expected
|
|
889
|
+
out << zstream.inflate(data.byteslice(pos, INFLATE_SLICE))
|
|
890
|
+
pos += INFLATE_SLICE
|
|
891
|
+
end
|
|
892
|
+
break
|
|
893
|
+
rescue Zlib::Error
|
|
894
|
+
break unless out.empty?
|
|
895
|
+
ensure
|
|
896
|
+
zstream&.reset
|
|
897
|
+
zstream&.close
|
|
898
|
+
end
|
|
899
|
+
out
|
|
900
|
+
end
|
|
901
|
+
|
|
902
|
+
def packbits(data, expected)
|
|
903
|
+
out = Bytes.buffer(initial_capacity(data, expected))
|
|
904
|
+
i = 0
|
|
905
|
+
n = data.bytesize
|
|
906
|
+
while i < n && out.bytesize < expected
|
|
907
|
+
c = data.getbyte(i)
|
|
908
|
+
i += 1
|
|
909
|
+
if c < 128
|
|
910
|
+
out << data.byteslice(i, c + 1)
|
|
911
|
+
i += c + 1
|
|
912
|
+
elsif c > 128
|
|
913
|
+
b = data.byteslice(i, 1)
|
|
914
|
+
out << b * (257 - c)
|
|
915
|
+
i += 1
|
|
916
|
+
end
|
|
917
|
+
end
|
|
918
|
+
out
|
|
919
|
+
end
|
|
920
|
+
|
|
921
|
+
# Luminance (or, for gray JPEGs, sample) rows of a JPEG chunk:
|
|
922
|
+
# JPEGTables merged in, cropped or padded to +cw+ x +rows+.
|
|
923
|
+
def jpeg_chunk(data, cw, rows)
|
|
924
|
+
stream = data
|
|
925
|
+
tables = @jpeg_tables
|
|
926
|
+
if tables&.start_with?(SOI) && stream.start_with?(SOI)
|
|
927
|
+
tables = tables.byteslice(0, tables.bytesize - 2) if tables.end_with?(EOI)
|
|
928
|
+
stream = tables + stream.byteslice(2, stream.bytesize - 2)
|
|
929
|
+
end
|
|
930
|
+
if @adobe && stream.start_with?(SOI)
|
|
931
|
+
stream = SOI + ADOBE + @adobe.chr + stream.byteslice(2, stream.bytesize - 2)
|
|
932
|
+
end
|
|
933
|
+
jw, jh = JPEG.dimensions(stream)
|
|
934
|
+
return nil if jw * jh > cw * @chunk_h # not this chunk's image; also bounds crafted sizes
|
|
935
|
+
|
|
936
|
+
image = JPEG.decode(stream)
|
|
937
|
+
@decoded += 1
|
|
938
|
+
fit(image.data, jw, [jh, rows].min, cw)
|
|
939
|
+
rescue UnsupportedImageError
|
|
940
|
+
raise
|
|
941
|
+
rescue ImageError
|
|
942
|
+
nil
|
|
943
|
+
end
|
|
944
|
+
|
|
945
|
+
# +rows+ rows of +cw+ bytes from a buffer with rows of +w+ bytes.
|
|
946
|
+
def fit(data, w, rows, cw)
|
|
947
|
+
return data.byteslice(0, cw * rows) if w == cw
|
|
948
|
+
|
|
949
|
+
out = Bytes.buffer(cw * rows)
|
|
950
|
+
rows.times do |r|
|
|
951
|
+
out << data.byteslice(r * w, [w, cw].min)
|
|
952
|
+
out << WHITE * (cw - w) if w < cw
|
|
953
|
+
end
|
|
954
|
+
out
|
|
955
|
+
end
|
|
956
|
+
|
|
957
|
+
# Old-style JPEG (compression 6): only the common case of a complete
|
|
958
|
+
# JPEG stream at JPEGInterchangeFormat is supported.
|
|
959
|
+
def old_jpeg
|
|
960
|
+
off = @dir.int(JPEG_INTERCHANGE_FORMAT)
|
|
961
|
+
unless off&.positive? && off < @bytes.bytesize
|
|
962
|
+
raise UnsupportedImageError, "old-style JPEG TIFF images are not supported"
|
|
963
|
+
end
|
|
964
|
+
|
|
965
|
+
length = @dir.int(JPEG_INTERCHANGE_FORMAT_LENGTH)
|
|
966
|
+
length = @bytes.bytesize - off unless length&.positive? && length < @bytes.bytesize - off
|
|
967
|
+
image = JPEG.decode(@bytes.byteslice(off, length))
|
|
968
|
+
@decoded = 1
|
|
969
|
+
w = @width
|
|
970
|
+
rows = [image.height, @height].min
|
|
971
|
+
data = fit(image.data, image.width, rows, w)
|
|
972
|
+
data << WHITE * (w * (@height - rows)) if rows < @height
|
|
973
|
+
data
|
|
974
|
+
rescue UnsupportedImageError
|
|
975
|
+
raise
|
|
976
|
+
rescue ImageError
|
|
977
|
+
raise UnsupportedImageError, "old-style JPEG TIFF images are not supported"
|
|
978
|
+
end
|
|
979
|
+
|
|
980
|
+
# --- samples ------------------------------------------------------
|
|
981
|
+
|
|
982
|
+
# Unpacks the complete rows of a decompressed chunk to one byte per
|
|
983
|
+
# sample (values mapped by setup_samples). Returns [samples, rows].
|
|
984
|
+
def normalize(raw, cw, rows, nsamp)
|
|
985
|
+
bps = @bps
|
|
986
|
+
count = cw * nsamp
|
|
987
|
+
row_bytes = (count * bps + 7) >> 3
|
|
988
|
+
avail = raw.bytesize / row_bytes
|
|
989
|
+
avail = rows if avail > rows
|
|
990
|
+
return ["".b, 0] if avail.zero?
|
|
991
|
+
|
|
992
|
+
raw = raw.byteslice(0, avail * row_bytes) if raw.bytesize != avail * row_bytes
|
|
993
|
+
raw = undo_predictor(raw, row_bytes, avail, nsamp) if @predictor != 1
|
|
994
|
+
samples = case bps
|
|
995
|
+
when 8 then raw
|
|
996
|
+
when 1 then unpack_text(raw.unpack1("B*"), row_bytes * 8, count, avail)
|
|
997
|
+
when 4 then unpack_text(raw.unpack1("H*"), row_bytes * 2, count, avail)
|
|
998
|
+
end
|
|
999
|
+
samples ||= unpack_rows(raw, row_bytes, count, avail)
|
|
1000
|
+
samples.tr!(@tr_from, @tr_to) if @tr_from
|
|
1001
|
+
[samples, avail]
|
|
1002
|
+
end
|
|
1003
|
+
|
|
1004
|
+
# Rows of an unpacked bit/hex digit string trimmed to +count+ samples.
|
|
1005
|
+
def unpack_text(text, per_row, count, rows)
|
|
1006
|
+
text.force_encoding(Encoding::BINARY)
|
|
1007
|
+
return text if per_row == count
|
|
1008
|
+
|
|
1009
|
+
out = Bytes.buffer(count * rows)
|
|
1010
|
+
rows.times { |r| out << text.byteslice(r * per_row, count) }
|
|
1011
|
+
out
|
|
1012
|
+
end
|
|
1013
|
+
|
|
1014
|
+
def unpack_rows(raw, row_bytes, count, rows)
|
|
1015
|
+
bps = @bps
|
|
1016
|
+
out = Bytes.buffer(count * rows)
|
|
1017
|
+
if @float
|
|
1018
|
+
# File byte order, except after predictor 3 (always big-endian).
|
|
1019
|
+
directive = FLOATS[[bps, !@le || @predictor == 3]]
|
|
1020
|
+
rows.times { |r| floats_row(raw.byteslice(r * row_bytes, row_bytes).unpack(directive), out) }
|
|
1021
|
+
elsif bps == 2
|
|
1022
|
+
rows.times { |r| unpack2_row(raw, r * row_bytes, count, out) }
|
|
1023
|
+
elsif (bps & 7).zero?
|
|
1024
|
+
step = bps >> 3
|
|
1025
|
+
msb = @le ? step - 1 : 0
|
|
1026
|
+
rows.times { |r| pick(raw, r * row_bytes + msb, count, step, out) }
|
|
1027
|
+
else
|
|
1028
|
+
rows.times { |r| bits_row(raw, r * row_bytes, count, bps, out) }
|
|
1029
|
+
end
|
|
1030
|
+
out
|
|
1031
|
+
end
|
|
1032
|
+
|
|
1033
|
+
# 2-bit samples (MSB first) -> one byte 0..3 each.
|
|
1034
|
+
def unpack2_row(src, off, n, out)
|
|
1035
|
+
stop = off + (n >> 2)
|
|
1036
|
+
i = off
|
|
1037
|
+
while i < stop
|
|
1038
|
+
out << TWO_BIT[src.getbyte(i)]
|
|
1039
|
+
i += 1
|
|
1040
|
+
end
|
|
1041
|
+
out << TWO_BIT[src.getbyte(stop)].byteslice(0, n & 3) if (n & 3).positive?
|
|
1042
|
+
out
|
|
1043
|
+
end
|
|
1044
|
+
|
|
1045
|
+
# Every +step+-th byte (the most significant byte of wide samples).
|
|
1046
|
+
def pick(src, off, n, step, out)
|
|
1047
|
+
stop = off + n * step
|
|
1048
|
+
while off < stop
|
|
1049
|
+
out << src.getbyte(off)
|
|
1050
|
+
off += step
|
|
1051
|
+
end
|
|
1052
|
+
out
|
|
1053
|
+
end
|
|
1054
|
+
|
|
1055
|
+
# Samples of an odd bit depth (MSB first), reduced to 8 bits if wider.
|
|
1056
|
+
def bits_row(src, off, n, bps, out)
|
|
1057
|
+
pos = off * 8
|
|
1058
|
+
shift = bps > 8 ? bps - 8 : 0
|
|
1059
|
+
i = 0
|
|
1060
|
+
while i < n
|
|
1061
|
+
v = 0
|
|
1062
|
+
need = bps
|
|
1063
|
+
while need.positive?
|
|
1064
|
+
left = 8 - (pos & 7)
|
|
1065
|
+
take = need < left ? need : left
|
|
1066
|
+
v = (v << take) | ((src.getbyte(pos >> 3) >> (left - take)) & ((1 << take) - 1))
|
|
1067
|
+
pos += take
|
|
1068
|
+
need -= take
|
|
1069
|
+
end
|
|
1070
|
+
out << (v >> shift)
|
|
1071
|
+
i += 1
|
|
1072
|
+
end
|
|
1073
|
+
out
|
|
1074
|
+
end
|
|
1075
|
+
|
|
1076
|
+
# Floating point samples (0.0 = black, 1.0 = white) -> bytes.
|
|
1077
|
+
def floats_row(values, out)
|
|
1078
|
+
i = 0
|
|
1079
|
+
n = values.size
|
|
1080
|
+
while i < n
|
|
1081
|
+
v = values[i] * 255 + 0.5
|
|
1082
|
+
out << if v.nan? || v <= 0 then 0
|
|
1083
|
+
elsif v >= 255 then 255
|
|
1084
|
+
else v.to_i
|
|
1085
|
+
end
|
|
1086
|
+
i += 1
|
|
1087
|
+
end
|
|
1088
|
+
out
|
|
1089
|
+
end
|
|
1090
|
+
|
|
1091
|
+
# Returns +raw+ with the predictor undone (in place, or a new String).
|
|
1092
|
+
def undo_predictor(raw, row_bytes, rows, nsamp)
|
|
1093
|
+
bps = @bps
|
|
1094
|
+
if @predictor == 3
|
|
1095
|
+
rows.times do |r|
|
|
1096
|
+
off = r * row_bytes
|
|
1097
|
+
raw.bytesplice(off, row_bytes, float_row(raw.byteslice(off, row_bytes), nsamp, bps >> 3))
|
|
1098
|
+
end
|
|
1099
|
+
elsif bps == 8 && nsamp == 1
|
|
1100
|
+
out = Bytes.buffer(raw.bytesize)
|
|
1101
|
+
rows.times { |r| accumulate_row(raw, r * row_bytes, row_bytes, out) }
|
|
1102
|
+
return out
|
|
1103
|
+
elsif bps == 8
|
|
1104
|
+
rows.times { |r| accumulate_bytes(raw, r * row_bytes, row_bytes, nsamp) }
|
|
1105
|
+
else
|
|
1106
|
+
directive = WORDS[bps][@le ? 0 : 1]
|
|
1107
|
+
mask = (1 << bps) - 1
|
|
1108
|
+
rows.times do |r|
|
|
1109
|
+
off = r * row_bytes
|
|
1110
|
+
values = raw.byteslice(off, row_bytes).unpack(directive)
|
|
1111
|
+
accumulate(values, nsamp, mask)
|
|
1112
|
+
raw.bytesplice(off, row_bytes, values.pack(directive))
|
|
1113
|
+
end
|
|
1114
|
+
end
|
|
1115
|
+
raw
|
|
1116
|
+
end
|
|
1117
|
+
|
|
1118
|
+
# Horizontal differencing on one 8-bit sample per pixel, appended to
|
|
1119
|
+
# +out+ (appending beats setbyte in place by about 1.5x).
|
|
1120
|
+
def accumulate_row(src, off, n, out)
|
|
1121
|
+
acc = 0
|
|
1122
|
+
i = off
|
|
1123
|
+
stop = off + n
|
|
1124
|
+
while i < stop
|
|
1125
|
+
out << (acc = (acc + src.getbyte(i)) & 0xFF)
|
|
1126
|
+
i += 1
|
|
1127
|
+
end
|
|
1128
|
+
out
|
|
1129
|
+
end
|
|
1130
|
+
|
|
1131
|
+
# 8-bit RGB with horizontal differencing (what most writers produce
|
|
1132
|
+
# for colour LZW and Deflate): the predictor is undone while
|
|
1133
|
+
# converting to luminance, three to four times faster than two passes.
|
|
1134
|
+
def predicted_rgb_luma(raw, cw, rows)
|
|
1135
|
+
stride = @spp
|
|
1136
|
+
row_bytes = cw * stride
|
|
1137
|
+
avail = [raw.bytesize / row_bytes, rows].min
|
|
1138
|
+
return nil if avail.zero?
|
|
1139
|
+
|
|
1140
|
+
@decoded += 1
|
|
1141
|
+
out = Bytes.buffer(cw * avail)
|
|
1142
|
+
avail.times { |r| predicted_rgb_row(raw, r * row_bytes, cw, stride, out) }
|
|
1143
|
+
out
|
|
1144
|
+
end
|
|
1145
|
+
|
|
1146
|
+
def predicted_rgb_row(src, off, n, stride, out)
|
|
1147
|
+
r = g = b = 0
|
|
1148
|
+
i = off
|
|
1149
|
+
stop = off + n * stride
|
|
1150
|
+
while i < stop
|
|
1151
|
+
r = (r + src.getbyte(i)) & 0xFF
|
|
1152
|
+
g = (g + src.getbyte(i + 1)) & 0xFF
|
|
1153
|
+
b = (b + src.getbyte(i + 2)) & 0xFF
|
|
1154
|
+
out << ((306 * r + 601 * g + 117 * b + 0x200) >> 10)
|
|
1155
|
+
i += stride
|
|
1156
|
+
end
|
|
1157
|
+
out
|
|
1158
|
+
end
|
|
1159
|
+
|
|
1160
|
+
# Horizontal differencing on bytes (8-bit samples).
|
|
1161
|
+
def accumulate_bytes(raw, off, len, stride)
|
|
1162
|
+
i = off + stride
|
|
1163
|
+
stop = off + len
|
|
1164
|
+
while i < stop
|
|
1165
|
+
raw.setbyte(i, (raw.getbyte(i) + raw.getbyte(i - stride)) & 0xFF)
|
|
1166
|
+
i += 1
|
|
1167
|
+
end
|
|
1168
|
+
end
|
|
1169
|
+
|
|
1170
|
+
def accumulate(values, stride, mask)
|
|
1171
|
+
i = stride
|
|
1172
|
+
n = values.size
|
|
1173
|
+
while i < n
|
|
1174
|
+
values[i] = (values[i] + values[i - stride]) & mask
|
|
1175
|
+
i += 1
|
|
1176
|
+
end
|
|
1177
|
+
end
|
|
1178
|
+
|
|
1179
|
+
# Undoes the floating point predictor: byte differencing over the
|
|
1180
|
+
# row, then the bytes of each sample are gathered from byte planes
|
|
1181
|
+
# (most significant first). The result is big-endian.
|
|
1182
|
+
def float_row(row, stride, size)
|
|
1183
|
+
bytes = row.bytes
|
|
1184
|
+
i = stride
|
|
1185
|
+
n = bytes.size
|
|
1186
|
+
while i < n
|
|
1187
|
+
bytes[i] = (bytes[i] + bytes[i - stride]) & 0xFF
|
|
1188
|
+
i += 1
|
|
1189
|
+
end
|
|
1190
|
+
count = n / size
|
|
1191
|
+
planes = Array.new(size) { |b| bytes[b * count, count] }
|
|
1192
|
+
planes[0].zip(*planes[1..]).flatten!.pack("C*")
|
|
1193
|
+
end
|
|
1194
|
+
|
|
1195
|
+
# Y samples of subsampled YCbCr data (8 bits, data units of
|
|
1196
|
+
# h x v luma samples followed by Cb and Cr).
|
|
1197
|
+
def ycbcr_luma(raw, cw, rows)
|
|
1198
|
+
raise UnsupportedImageError, "#{@bps}-bit YCbCr TIFF images are not supported" unless @bps == 8
|
|
1199
|
+
|
|
1200
|
+
sh, sv = subsampling
|
|
1201
|
+
units = (cw + sh - 1) / sh
|
|
1202
|
+
unit_size = sh * sv + 2
|
|
1203
|
+
unit_rows = [raw.bytesize / (units * unit_size), (rows + sv - 1) / sv].min
|
|
1204
|
+
return nil if unit_rows.zero?
|
|
1205
|
+
|
|
1206
|
+
@decoded += 1
|
|
1207
|
+
out = Bytes.buffer(cw * rows)
|
|
1208
|
+
unit_rows.times do |ur|
|
|
1209
|
+
sv.times do |j|
|
|
1210
|
+
break if ur * sv + j >= rows
|
|
1211
|
+
|
|
1212
|
+
ycbcr_row(raw, ur * units * unit_size + j * sh, sh, unit_size, cw, out)
|
|
1213
|
+
end
|
|
1214
|
+
end
|
|
1215
|
+
out
|
|
1216
|
+
end
|
|
1217
|
+
|
|
1218
|
+
def ycbcr_row(raw, off, sh, unit_size, cw, out)
|
|
1219
|
+
x = 0
|
|
1220
|
+
while x < cw
|
|
1221
|
+
out << raw.getbyte(off + (x / sh) * unit_size + x % sh)
|
|
1222
|
+
x += 1
|
|
1223
|
+
end
|
|
1224
|
+
out
|
|
1225
|
+
end
|
|
1226
|
+
|
|
1227
|
+
def subsampling
|
|
1228
|
+
@subsampling ||= begin
|
|
1229
|
+
sh, sv = @dir.ints(YCBCR_SUBSAMPLING, 2) || [2, 2]
|
|
1230
|
+
sv ||= 2
|
|
1231
|
+
unless [1, 2, 4].include?(sh) && [1, 2, 4].include?(sv)
|
|
1232
|
+
raise ImageError, "invalid TIFF YCbCr subsampling #{sh}x#{sv}"
|
|
1233
|
+
end
|
|
1234
|
+
|
|
1235
|
+
[sh, sv]
|
|
1236
|
+
end
|
|
1237
|
+
end
|
|
1238
|
+
|
|
1239
|
+
# --- luminance ----------------------------------------------------
|
|
1240
|
+
|
|
1241
|
+
def convert(samples, cw, rows)
|
|
1242
|
+
return samples if @mode == :single
|
|
1243
|
+
|
|
1244
|
+
out = Bytes.buffer(cw * rows)
|
|
1245
|
+
stride = @stride
|
|
1246
|
+
rows.times { |r| convert_row(samples, r * cw * stride, cw, out) }
|
|
1247
|
+
out
|
|
1248
|
+
end
|
|
1249
|
+
|
|
1250
|
+
# Appends the luminance of +n+ pixels at +off+ to +out+.
|
|
1251
|
+
def convert_row(src, off, n, out)
|
|
1252
|
+
c = @channels
|
|
1253
|
+
stride = @stride
|
|
1254
|
+
case @mode
|
|
1255
|
+
when :pick then pick(src, off, n, stride, out)
|
|
1256
|
+
when :rgb then rgb_row(src, off, n, out, c[0], c[1], c[2], stride)
|
|
1257
|
+
when :rgba
|
|
1258
|
+
if @associated
|
|
1259
|
+
premultiplied_row(src, off, n, out, c[0], c[1], c[2], c[3], stride)
|
|
1260
|
+
else
|
|
1261
|
+
rgba_row(src, off, n, out, c[0], c[1], c[2], c[3], stride)
|
|
1262
|
+
end
|
|
1263
|
+
when :gray_alpha then gray_alpha_row(src, off, n, out, c[1], stride)
|
|
1264
|
+
when :cmyk then cmyk_row(src, off, n, out, c[0], c[1], c[2], c[3], stride)
|
|
1265
|
+
end
|
|
1266
|
+
end
|
|
1267
|
+
|
|
1268
|
+
def rgb_row(src, off, n, out, ro, go, bo, stride)
|
|
1269
|
+
i = off
|
|
1270
|
+
stop = off + n * stride
|
|
1271
|
+
while i < stop
|
|
1272
|
+
out << ((306 * src.getbyte(i + ro) + 601 * src.getbyte(i + go) + 117 * src.getbyte(i + bo) + 0x200) >> 10)
|
|
1273
|
+
i += stride
|
|
1274
|
+
end
|
|
1275
|
+
out
|
|
1276
|
+
end
|
|
1277
|
+
|
|
1278
|
+
# Unassociated alpha, composited over white like Image.from_pixels.
|
|
1279
|
+
def rgba_row(src, off, n, out, ro, go, bo, ao, stride)
|
|
1280
|
+
i = off
|
|
1281
|
+
stop = off + n * stride
|
|
1282
|
+
while i < stop
|
|
1283
|
+
v = (306 * src.getbyte(i + ro) + 601 * src.getbyte(i + go) + 117 * src.getbyte(i + bo) + 0x200) >> 10
|
|
1284
|
+
a = src.getbyte(i + ao)
|
|
1285
|
+
out << (a == 255 ? v : (v * a + 255 * (255 - a) + 127) / 255)
|
|
1286
|
+
i += stride
|
|
1287
|
+
end
|
|
1288
|
+
out
|
|
1289
|
+
end
|
|
1290
|
+
|
|
1291
|
+
# Associated (premultiplied) alpha: colour + (255 - alpha) of white.
|
|
1292
|
+
def premultiplied_row(src, off, n, out, ro, go, bo, ao, stride)
|
|
1293
|
+
i = off
|
|
1294
|
+
stop = off + n * stride
|
|
1295
|
+
while i < stop
|
|
1296
|
+
v = ((306 * src.getbyte(i + ro) + 601 * src.getbyte(i + go) + 117 * src.getbyte(i + bo) + 0x200) >> 10) +
|
|
1297
|
+
255 - src.getbyte(i + ao)
|
|
1298
|
+
out << (v > 255 ? 255 : v)
|
|
1299
|
+
i += stride
|
|
1300
|
+
end
|
|
1301
|
+
out
|
|
1302
|
+
end
|
|
1303
|
+
|
|
1304
|
+
def gray_alpha_row(src, off, n, out, ao, stride)
|
|
1305
|
+
invert = @invert
|
|
1306
|
+
associated = @associated
|
|
1307
|
+
i = off
|
|
1308
|
+
stop = off + n * stride
|
|
1309
|
+
while i < stop
|
|
1310
|
+
v = src.getbyte(i)
|
|
1311
|
+
a = src.getbyte(i + ao)
|
|
1312
|
+
if associated
|
|
1313
|
+
v = a - v if invert
|
|
1314
|
+
v += 255 - a
|
|
1315
|
+
out << (v > 255 ? 255 : v)
|
|
1316
|
+
else
|
|
1317
|
+
v = 255 - v if invert
|
|
1318
|
+
out << (a == 255 ? v : (v * a + 255 * (255 - a) + 127) / 255)
|
|
1319
|
+
end
|
|
1320
|
+
i += stride
|
|
1321
|
+
end
|
|
1322
|
+
out
|
|
1323
|
+
end
|
|
1324
|
+
|
|
1325
|
+
# Ink amounts (0 = none): R = (255 - C) * (255 - K) / 255, ..., like
|
|
1326
|
+
# the JPEG decoder's Adobe CMYK conversion.
|
|
1327
|
+
def cmyk_row(src, off, n, out, co, mo, yo, ko, stride)
|
|
1328
|
+
i = off
|
|
1329
|
+
stop = off + n * stride
|
|
1330
|
+
while i < stop
|
|
1331
|
+
k = 255 - src.getbyte(i + ko)
|
|
1332
|
+
r = ((255 - src.getbyte(i + co)) * k + 127) / 255
|
|
1333
|
+
g = ((255 - src.getbyte(i + mo)) * k + 127) / 255
|
|
1334
|
+
b = ((255 - src.getbyte(i + yo)) * k + 127) / 255
|
|
1335
|
+
out << ((306 * r + 601 * g + 117 * b + 0x200) >> 10)
|
|
1336
|
+
i += stride
|
|
1337
|
+
end
|
|
1338
|
+
out
|
|
1339
|
+
end
|
|
1340
|
+
end
|
|
1341
|
+
|
|
1342
|
+
private_constant(*constants)
|
|
1343
|
+
end
|
|
1344
|
+
end
|
|
1345
|
+
end
|