fastimage 2.3.1 → 2.4.0
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/README.md +5 -1
- data/lib/fastimage/fastimage.rb +471 -0
- data/lib/fastimage/fastimage_parsing/avif.rb +12 -0
- data/lib/fastimage/fastimage_parsing/bmp.rb +17 -0
- data/lib/fastimage/fastimage_parsing/exif.rb +76 -0
- data/lib/fastimage/fastimage_parsing/fiber_stream.rb +58 -0
- data/lib/fastimage/fastimage_parsing/gif.rb +63 -0
- data/lib/fastimage/fastimage_parsing/heic.rb +8 -0
- data/lib/fastimage/fastimage_parsing/ico.rb +9 -0
- data/lib/fastimage/fastimage_parsing/image_base.rb +17 -0
- data/lib/fastimage/fastimage_parsing/iso_bmff.rb +176 -0
- data/lib/fastimage/fastimage_parsing/jpeg.rb +52 -0
- data/lib/fastimage/fastimage_parsing/jxl.rb +13 -0
- data/lib/fastimage/fastimage_parsing/jxlc.rb +75 -0
- data/lib/fastimage/fastimage_parsing/png.rb +26 -0
- data/lib/fastimage/fastimage_parsing/psd.rb +7 -0
- data/lib/fastimage/fastimage_parsing/stream_util.rb +19 -0
- data/lib/fastimage/fastimage_parsing/svg.rb +69 -0
- data/lib/fastimage/fastimage_parsing/tiff.rb +16 -0
- data/lib/fastimage/fastimage_parsing/type_parser.rb +69 -0
- data/lib/fastimage/fastimage_parsing/webp.rb +60 -0
- data/lib/fastimage/version.rb +1 -1
- data/lib/fastimage.rb +3 -1078
- metadata +23 -6
data/lib/fastimage.rb
CHANGED
@@ -9,7 +9,7 @@
|
|
9
9
|
# No external libraries such as ImageMagick are used here, this is a very lightweight solution to
|
10
10
|
# finding image information.
|
11
11
|
#
|
12
|
-
# FastImage knows about GIF, JPEG, BMP, TIFF, ICO, CUR, PNG, PSD, SVG and
|
12
|
+
# FastImage knows about GIF, JPEG, BMP, TIFF, ICO, CUR, PNG, HEIC/HEIF, AVIF, PSD, SVG, WEBP and JXL files.
|
13
13
|
#
|
14
14
|
# FastImage can also read files from the local filesystem by supplying the path instead of a uri.
|
15
15
|
# In this case FastImage reads the file in chunks of 256 bytes until
|
@@ -59,9 +59,10 @@ require 'net/https'
|
|
59
59
|
require 'delegate'
|
60
60
|
require 'pathname'
|
61
61
|
require 'zlib'
|
62
|
-
require 'base64'
|
63
62
|
require 'uri'
|
64
63
|
require 'stringio'
|
64
|
+
|
65
|
+
require_relative 'fastimage/fastimage'
|
65
66
|
require_relative 'fastimage/version'
|
66
67
|
|
67
68
|
# see http://stackoverflow.com/questions/5208851/i/41048816#41048816
|
@@ -70,1079 +71,3 @@ if RUBY_VERSION < "2.2"
|
|
70
71
|
DEFAULT_PARSER = Parser.new(:HOSTNAME => "(?:(?:[a-zA-Z\\d](?:[-\\_a-zA-Z\\d]*[a-zA-Z\\d])?)\\.)*(?:[a-zA-Z](?:[-\\_a-zA-Z\\d]*[a-zA-Z\\d])?)\\.?")
|
71
72
|
end
|
72
73
|
end
|
73
|
-
|
74
|
-
class FastImage
|
75
|
-
attr_reader :size, :type, :content_length, :orientation, :animated
|
76
|
-
|
77
|
-
attr_reader :bytes_read
|
78
|
-
|
79
|
-
class FastImageException < StandardError # :nodoc:
|
80
|
-
end
|
81
|
-
class UnknownImageType < FastImageException # :nodoc:
|
82
|
-
end
|
83
|
-
class ImageFetchFailure < FastImageException # :nodoc:
|
84
|
-
end
|
85
|
-
class SizeNotFound < FastImageException # :nodoc:
|
86
|
-
end
|
87
|
-
class CannotParseImage < FastImageException # :nodoc:
|
88
|
-
end
|
89
|
-
class BadImageURI < FastImageException # :nodoc:
|
90
|
-
end
|
91
|
-
|
92
|
-
DefaultTimeout = 2 unless const_defined?(:DefaultTimeout)
|
93
|
-
|
94
|
-
LocalFileChunkSize = 256 unless const_defined?(:LocalFileChunkSize)
|
95
|
-
|
96
|
-
SUPPORTED_IMAGE_TYPES = [:bmp, :gif, :jpeg, :png, :tiff, :psd, :heic, :heif, :webp, :svg, :ico, :cur].freeze
|
97
|
-
|
98
|
-
# Returns an array containing the width and height of the image.
|
99
|
-
# It will return nil if the image could not be fetched, or if the image type was not recognised.
|
100
|
-
#
|
101
|
-
# By default there is a timeout of 2 seconds for opening and reading from a remote server.
|
102
|
-
# This can be changed by passing a :timeout => number_of_seconds in the options.
|
103
|
-
#
|
104
|
-
# If you wish FastImage to raise if it cannot size the image for any reason, then pass
|
105
|
-
# :raise_on_failure => true in the options.
|
106
|
-
#
|
107
|
-
# FastImage knows about GIF, JPEG, BMP, TIFF, ICO, CUR, PNG, PSD, SVG and WEBP files.
|
108
|
-
#
|
109
|
-
# === Example
|
110
|
-
#
|
111
|
-
# require 'fastimage'
|
112
|
-
#
|
113
|
-
# FastImage.size("https://switchstep.com/images/ios.gif")
|
114
|
-
# => [196, 283]
|
115
|
-
# FastImage.size("http://switchstep.com/images/ss_logo.png")
|
116
|
-
# => [300, 300]
|
117
|
-
# FastImage.size("https://upload.wikimedia.org/wikipedia/commons/0/09/Jpeg_thumb_artifacts_test.jpg")
|
118
|
-
# => [1280, 800]
|
119
|
-
# FastImage.size("https://eeweb.engineering.nyu.edu/~yao/EL5123/image/lena_gray.bmp")
|
120
|
-
# => [512, 512]
|
121
|
-
# FastImage.size("test/fixtures/test.jpg")
|
122
|
-
# => [882, 470]
|
123
|
-
# FastImage.size("http://switchstep.com/does_not_exist")
|
124
|
-
# => nil
|
125
|
-
# FastImage.size("http://switchstep.com/does_not_exist", :raise_on_failure=>true)
|
126
|
-
# => raises FastImage::ImageFetchFailure
|
127
|
-
# FastImage.size("http://switchstep.com/images/favicon.ico", :raise_on_failure=>true)
|
128
|
-
# => [16, 16]
|
129
|
-
# FastImage.size("http://switchstep.com/foo.ics", :raise_on_failure=>true)
|
130
|
-
# => raises FastImage::UnknownImageType
|
131
|
-
# FastImage.size("http://switchstep.com/images/favicon.ico", :raise_on_failure=>true, :timeout=>0.01)
|
132
|
-
# => raises FastImage::ImageFetchFailure
|
133
|
-
# FastImage.size("http://switchstep.com/images/faulty.jpg", :raise_on_failure=>true)
|
134
|
-
# => raises FastImage::SizeNotFound
|
135
|
-
#
|
136
|
-
# === Supported options
|
137
|
-
# [:timeout]
|
138
|
-
# Overrides the default timeout of 2 seconds. Applies both to reading from and opening the http connection.
|
139
|
-
# [:raise_on_failure]
|
140
|
-
# If set to true causes an exception to be raised if the image size cannot be found for any reason.
|
141
|
-
#
|
142
|
-
def self.size(uri, options={})
|
143
|
-
new(uri, options).size
|
144
|
-
end
|
145
|
-
|
146
|
-
# Returns an symbol indicating the image type fetched from a uri.
|
147
|
-
# It will return nil if the image could not be fetched, or if the image type was not recognised.
|
148
|
-
#
|
149
|
-
# By default there is a timeout of 2 seconds for opening and reading from a remote server.
|
150
|
-
# This can be changed by passing a :timeout => number_of_seconds in the options.
|
151
|
-
#
|
152
|
-
# If you wish FastImage to raise if it cannot find the type of the image for any reason, then pass
|
153
|
-
# :raise_on_failure => true in the options.
|
154
|
-
#
|
155
|
-
# === Example
|
156
|
-
#
|
157
|
-
# require 'fastimage'
|
158
|
-
#
|
159
|
-
# FastImage.type("https://switchstep.com/images/ios.gif")
|
160
|
-
# => :gif
|
161
|
-
# FastImage.type("http://switchstep.com/images/ss_logo.png")
|
162
|
-
# => :png
|
163
|
-
# FastImage.type("https://upload.wikimedia.org/wikipedia/commons/0/09/Jpeg_thumb_artifacts_test.jpg")
|
164
|
-
# => :jpeg
|
165
|
-
# FastImage.type("https://eeweb.engineering.nyu.edu/~yao/EL5123/image/lena_gray.bmp")
|
166
|
-
# => :bmp
|
167
|
-
# FastImage.type("test/fixtures/test.jpg")
|
168
|
-
# => :jpeg
|
169
|
-
# FastImage.type("http://switchstep.com/does_not_exist")
|
170
|
-
# => nil
|
171
|
-
# File.open("/some/local/file.gif", "r") {|io| FastImage.type(io)}
|
172
|
-
# => :gif
|
173
|
-
# FastImage.type("test/fixtures/test.tiff")
|
174
|
-
# => :tiff
|
175
|
-
# FastImage.type("test/fixtures/test.psd")
|
176
|
-
# => :psd
|
177
|
-
#
|
178
|
-
# === Supported options
|
179
|
-
# [:timeout]
|
180
|
-
# Overrides the default timeout of 2 seconds. Applies both to reading from and opening the http connection.
|
181
|
-
# [:raise_on_failure]
|
182
|
-
# If set to true causes an exception to be raised if the image type cannot be found for any reason.
|
183
|
-
#
|
184
|
-
def self.type(uri, options={})
|
185
|
-
new(uri, options.merge(:type_only=>true)).type
|
186
|
-
end
|
187
|
-
|
188
|
-
# Returns a boolean value indicating the image is animated.
|
189
|
-
# It will return nil if the image could not be fetched, or if the image type was not recognised.
|
190
|
-
#
|
191
|
-
# By default there is a timeout of 2 seconds for opening and reading from a remote server.
|
192
|
-
# This can be changed by passing a :timeout => number_of_seconds in the options.
|
193
|
-
#
|
194
|
-
# If you wish FastImage to raise if it cannot find the type of the image for any reason, then pass
|
195
|
-
# :raise_on_failure => true in the options.
|
196
|
-
#
|
197
|
-
# === Example
|
198
|
-
#
|
199
|
-
# require 'fastimage'
|
200
|
-
#
|
201
|
-
# FastImage.animated?("test/fixtures/test.gif")
|
202
|
-
# => false
|
203
|
-
# FastImage.animated?("test/fixtures/animated.gif")
|
204
|
-
# => true
|
205
|
-
#
|
206
|
-
# === Supported options
|
207
|
-
# [:timeout]
|
208
|
-
# Overrides the default timeout of 2 seconds. Applies both to reading from and opening the http connection.
|
209
|
-
# [:raise_on_failure]
|
210
|
-
# If set to true causes an exception to be raised if the image type cannot be found for any reason.
|
211
|
-
#
|
212
|
-
def self.animated?(uri, options={})
|
213
|
-
new(uri, options.merge(:animated_only=>true)).animated
|
214
|
-
end
|
215
|
-
|
216
|
-
def initialize(uri, options={})
|
217
|
-
@uri = uri
|
218
|
-
@options = {
|
219
|
-
:type_only => false,
|
220
|
-
:timeout => DefaultTimeout,
|
221
|
-
:raise_on_failure => false,
|
222
|
-
:proxy => nil,
|
223
|
-
:http_header => {}
|
224
|
-
}.merge(options)
|
225
|
-
|
226
|
-
@property = if @options[:animated_only]
|
227
|
-
:animated
|
228
|
-
elsif @options[:type_only]
|
229
|
-
:type
|
230
|
-
else
|
231
|
-
:size
|
232
|
-
end
|
233
|
-
|
234
|
-
raise BadImageURI if uri.nil?
|
235
|
-
|
236
|
-
@type, @state = nil
|
237
|
-
|
238
|
-
if uri.respond_to?(:read)
|
239
|
-
fetch_using_read(uri)
|
240
|
-
elsif uri.start_with?('data:')
|
241
|
-
fetch_using_base64(uri)
|
242
|
-
else
|
243
|
-
begin
|
244
|
-
@parsed_uri = URI.parse(uri)
|
245
|
-
rescue URI::InvalidURIError
|
246
|
-
fetch_using_file_open
|
247
|
-
else
|
248
|
-
if @parsed_uri.scheme == "http" || @parsed_uri.scheme == "https"
|
249
|
-
fetch_using_http
|
250
|
-
else
|
251
|
-
fetch_using_file_open
|
252
|
-
end
|
253
|
-
end
|
254
|
-
end
|
255
|
-
|
256
|
-
raise SizeNotFound if @options[:raise_on_failure] && @property == :size && !@size
|
257
|
-
|
258
|
-
rescue Timeout::Error, SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ECONNRESET,
|
259
|
-
Errno::ENETUNREACH, ImageFetchFailure, Net::HTTPBadResponse, EOFError, Errno::ENOENT,
|
260
|
-
OpenSSL::SSL::SSLError
|
261
|
-
raise ImageFetchFailure if @options[:raise_on_failure]
|
262
|
-
rescue UnknownImageType, BadImageURI
|
263
|
-
raise if @options[:raise_on_failure]
|
264
|
-
rescue CannotParseImage
|
265
|
-
if @options[:raise_on_failure]
|
266
|
-
if @property == :size
|
267
|
-
raise SizeNotFound
|
268
|
-
else
|
269
|
-
raise ImageFetchFailure
|
270
|
-
end
|
271
|
-
end
|
272
|
-
|
273
|
-
ensure
|
274
|
-
uri.rewind if uri.respond_to?(:rewind)
|
275
|
-
|
276
|
-
end
|
277
|
-
|
278
|
-
private
|
279
|
-
|
280
|
-
def fetch_using_http
|
281
|
-
@redirect_count = 0
|
282
|
-
|
283
|
-
fetch_using_http_from_parsed_uri
|
284
|
-
end
|
285
|
-
|
286
|
-
# Some invalid locations need escaping
|
287
|
-
def escaped_location(location)
|
288
|
-
begin
|
289
|
-
URI(location)
|
290
|
-
rescue URI::InvalidURIError
|
291
|
-
::URI::DEFAULT_PARSER.escape(location)
|
292
|
-
else
|
293
|
-
location
|
294
|
-
end
|
295
|
-
end
|
296
|
-
|
297
|
-
def fetch_using_http_from_parsed_uri
|
298
|
-
http_header = {'Accept-Encoding' => 'identity'}.merge(@options[:http_header])
|
299
|
-
|
300
|
-
setup_http
|
301
|
-
@http.request_get(@parsed_uri.request_uri, http_header) do |res|
|
302
|
-
if res.is_a?(Net::HTTPRedirection) && @redirect_count < 4
|
303
|
-
@redirect_count += 1
|
304
|
-
begin
|
305
|
-
location = res['Location']
|
306
|
-
raise ImageFetchFailure if location.nil? || location.empty?
|
307
|
-
|
308
|
-
@parsed_uri = URI.join(@parsed_uri, escaped_location(location))
|
309
|
-
rescue URI::InvalidURIError
|
310
|
-
else
|
311
|
-
fetch_using_http_from_parsed_uri
|
312
|
-
break
|
313
|
-
end
|
314
|
-
end
|
315
|
-
|
316
|
-
raise ImageFetchFailure unless res.is_a?(Net::HTTPSuccess)
|
317
|
-
|
318
|
-
@content_length = res.content_length
|
319
|
-
|
320
|
-
read_fiber = Fiber.new do
|
321
|
-
res.read_body do |str|
|
322
|
-
Fiber.yield str
|
323
|
-
end
|
324
|
-
nil
|
325
|
-
end
|
326
|
-
|
327
|
-
case res['content-encoding']
|
328
|
-
when 'deflate', 'gzip', 'x-gzip'
|
329
|
-
begin
|
330
|
-
gzip = Zlib::GzipReader.new(FiberStream.new(read_fiber))
|
331
|
-
rescue FiberError, Zlib::GzipFile::Error
|
332
|
-
raise CannotParseImage
|
333
|
-
end
|
334
|
-
|
335
|
-
read_fiber = Fiber.new do
|
336
|
-
while data = gzip.readline
|
337
|
-
Fiber.yield data
|
338
|
-
end
|
339
|
-
nil
|
340
|
-
end
|
341
|
-
end
|
342
|
-
|
343
|
-
parse_packets FiberStream.new(read_fiber)
|
344
|
-
|
345
|
-
break # needed to actively quit out of the fetch
|
346
|
-
end
|
347
|
-
end
|
348
|
-
|
349
|
-
def protocol_relative_url?(url)
|
350
|
-
url.start_with?("//")
|
351
|
-
end
|
352
|
-
|
353
|
-
def proxy_uri
|
354
|
-
begin
|
355
|
-
if @options[:proxy]
|
356
|
-
proxy = URI.parse(@options[:proxy])
|
357
|
-
else
|
358
|
-
proxy = ENV['http_proxy'] && ENV['http_proxy'] != "" ? URI.parse(ENV['http_proxy']) : nil
|
359
|
-
end
|
360
|
-
rescue URI::InvalidURIError
|
361
|
-
proxy = nil
|
362
|
-
end
|
363
|
-
proxy
|
364
|
-
end
|
365
|
-
|
366
|
-
def setup_http
|
367
|
-
proxy = proxy_uri
|
368
|
-
|
369
|
-
if proxy
|
370
|
-
@http = Net::HTTP::Proxy(proxy.host, proxy.port, proxy.user, proxy.password).new(@parsed_uri.host, @parsed_uri.port)
|
371
|
-
else
|
372
|
-
@http = Net::HTTP.new(@parsed_uri.host, @parsed_uri.port)
|
373
|
-
end
|
374
|
-
@http.use_ssl = (@parsed_uri.scheme == "https")
|
375
|
-
@http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
376
|
-
@http.open_timeout = @options[:timeout]
|
377
|
-
@http.read_timeout = @options[:timeout]
|
378
|
-
end
|
379
|
-
|
380
|
-
def fetch_using_read(readable)
|
381
|
-
readable.rewind if readable.respond_to?(:rewind)
|
382
|
-
# Pathnames respond to read, but always return the first
|
383
|
-
# chunk of the file unlike an IO (even though the
|
384
|
-
# docuementation for it refers to IO). Need to supply
|
385
|
-
# an offset in this case.
|
386
|
-
if readable.is_a?(Pathname)
|
387
|
-
read_fiber = Fiber.new do
|
388
|
-
offset = 0
|
389
|
-
while str = readable.read(LocalFileChunkSize, offset)
|
390
|
-
Fiber.yield str
|
391
|
-
offset += LocalFileChunkSize
|
392
|
-
end
|
393
|
-
nil
|
394
|
-
end
|
395
|
-
else
|
396
|
-
read_fiber = Fiber.new do
|
397
|
-
while str = readable.read(LocalFileChunkSize)
|
398
|
-
Fiber.yield str
|
399
|
-
end
|
400
|
-
nil
|
401
|
-
end
|
402
|
-
end
|
403
|
-
|
404
|
-
parse_packets FiberStream.new(read_fiber)
|
405
|
-
end
|
406
|
-
|
407
|
-
def fetch_using_file_open
|
408
|
-
@content_length = File.size?(@uri)
|
409
|
-
File.open(@uri) do |s|
|
410
|
-
fetch_using_read(s)
|
411
|
-
end
|
412
|
-
end
|
413
|
-
|
414
|
-
def parse_packets(stream)
|
415
|
-
@stream = stream
|
416
|
-
|
417
|
-
begin
|
418
|
-
result = send("parse_#{@property}")
|
419
|
-
if result != nil
|
420
|
-
# extract exif orientation if it was found
|
421
|
-
if @property == :size && result.size == 3
|
422
|
-
@orientation = result.pop
|
423
|
-
else
|
424
|
-
@orientation = 1
|
425
|
-
end
|
426
|
-
|
427
|
-
instance_variable_set("@#{@property}", result)
|
428
|
-
else
|
429
|
-
raise CannotParseImage
|
430
|
-
end
|
431
|
-
rescue FiberError
|
432
|
-
raise CannotParseImage
|
433
|
-
end
|
434
|
-
end
|
435
|
-
|
436
|
-
def parse_size
|
437
|
-
@type = parse_type unless @type
|
438
|
-
send("parse_size_for_#{@type}")
|
439
|
-
end
|
440
|
-
|
441
|
-
def parse_animated
|
442
|
-
@type = parse_type unless @type
|
443
|
-
%i(gif png webp avif).include?(@type) ? send("parse_animated_for_#{@type}") : nil
|
444
|
-
end
|
445
|
-
|
446
|
-
def fetch_using_base64(uri)
|
447
|
-
decoded = begin
|
448
|
-
Base64.decode64(uri.split(',')[1])
|
449
|
-
rescue
|
450
|
-
raise CannotParseImage
|
451
|
-
end
|
452
|
-
@content_length = decoded.size
|
453
|
-
fetch_using_read StringIO.new(decoded)
|
454
|
-
end
|
455
|
-
|
456
|
-
module StreamUtil # :nodoc:
|
457
|
-
def read_byte
|
458
|
-
read(1)[0].ord
|
459
|
-
end
|
460
|
-
|
461
|
-
def read_int
|
462
|
-
read(2).unpack('n')[0]
|
463
|
-
end
|
464
|
-
|
465
|
-
def read_string_int
|
466
|
-
value = []
|
467
|
-
while read(1) =~ /(\d)/
|
468
|
-
value << $1
|
469
|
-
end
|
470
|
-
value.join.to_i
|
471
|
-
end
|
472
|
-
end
|
473
|
-
|
474
|
-
class FiberStream # :nodoc:
|
475
|
-
include StreamUtil
|
476
|
-
attr_reader :pos
|
477
|
-
|
478
|
-
# read_fiber should return nil if it no longer has anything to return when resumed
|
479
|
-
# so the result of the whole Fiber block should be set to be nil in case yield is no
|
480
|
-
# longer called
|
481
|
-
def initialize(read_fiber)
|
482
|
-
@read_fiber = read_fiber
|
483
|
-
@pos = 0
|
484
|
-
@strpos = 0
|
485
|
-
@str = ''
|
486
|
-
end
|
487
|
-
|
488
|
-
# Peeking beyond the end of the input will raise
|
489
|
-
def peek(n)
|
490
|
-
while @strpos + n > @str.size
|
491
|
-
unused_str = @str[@strpos..-1]
|
492
|
-
|
493
|
-
new_string = @read_fiber.resume
|
494
|
-
raise CannotParseImage if !new_string
|
495
|
-
# we are dealing with bytes here, so force the encoding
|
496
|
-
new_string.force_encoding("ASCII-8BIT") if new_string.respond_to? :force_encoding
|
497
|
-
|
498
|
-
@str = unused_str + new_string
|
499
|
-
@strpos = 0
|
500
|
-
end
|
501
|
-
|
502
|
-
@str[@strpos, n]
|
503
|
-
end
|
504
|
-
|
505
|
-
def read(n)
|
506
|
-
result = peek(n)
|
507
|
-
@strpos += n
|
508
|
-
@pos += n
|
509
|
-
result
|
510
|
-
end
|
511
|
-
|
512
|
-
def skip(n)
|
513
|
-
discarded = 0
|
514
|
-
fetched = @str[@strpos..-1].size
|
515
|
-
while n > fetched
|
516
|
-
discarded += @str[@strpos..-1].size
|
517
|
-
new_string = @read_fiber.resume
|
518
|
-
raise CannotParseImage if !new_string
|
519
|
-
|
520
|
-
new_string.force_encoding("ASCII-8BIT") if new_string.respond_to? :force_encoding
|
521
|
-
|
522
|
-
fetched += new_string.size
|
523
|
-
@str = new_string
|
524
|
-
@strpos = 0
|
525
|
-
end
|
526
|
-
@strpos = @strpos + n - discarded
|
527
|
-
@pos += n
|
528
|
-
end
|
529
|
-
end
|
530
|
-
|
531
|
-
class IOStream < SimpleDelegator # :nodoc:
|
532
|
-
include StreamUtil
|
533
|
-
end
|
534
|
-
|
535
|
-
def parse_type
|
536
|
-
parsed_type = case @stream.peek(2)
|
537
|
-
when "BM"
|
538
|
-
:bmp
|
539
|
-
when "GI"
|
540
|
-
:gif
|
541
|
-
when 0xff.chr + 0xd8.chr
|
542
|
-
:jpeg
|
543
|
-
when 0x89.chr + "P"
|
544
|
-
:png
|
545
|
-
when "II", "MM"
|
546
|
-
case @stream.peek(11)[8..10]
|
547
|
-
when "APC", "CR\002"
|
548
|
-
nil # do not recognise CRW or CR2 as tiff
|
549
|
-
else
|
550
|
-
:tiff
|
551
|
-
end
|
552
|
-
when '8B'
|
553
|
-
:psd
|
554
|
-
when "\0\0"
|
555
|
-
case @stream.peek(3).bytes.to_a.last
|
556
|
-
when 0
|
557
|
-
# http://www.ftyps.com/what.html
|
558
|
-
case @stream.peek(12)[4..-1]
|
559
|
-
when "ftypavif"
|
560
|
-
:avif
|
561
|
-
when "ftypavis"
|
562
|
-
:avif
|
563
|
-
when "ftypheic"
|
564
|
-
:heic
|
565
|
-
when "ftypmif1"
|
566
|
-
:heif
|
567
|
-
end
|
568
|
-
# ico has either a 1 (for ico format) or 2 (for cursor) at offset 3
|
569
|
-
when 1 then :ico
|
570
|
-
when 2 then :cur
|
571
|
-
end
|
572
|
-
when "RI"
|
573
|
-
:webp if @stream.peek(12)[8..11] == "WEBP"
|
574
|
-
when "<s"
|
575
|
-
:svg if @stream.peek(4) == "<svg"
|
576
|
-
when /\s\s|\s<|<[?!]/, 0xef.chr + 0xbb.chr
|
577
|
-
# Peek 10 more chars each time, and if end of file is reached just raise
|
578
|
-
# unknown. We assume the <svg tag cannot be within 10 chars of the end of
|
579
|
-
# the file, and is within the first 1000 chars.
|
580
|
-
begin
|
581
|
-
:svg if (1..100).detect {|n| @stream.peek(10 * n).include?("<svg")}
|
582
|
-
rescue FiberError, CannotParseImage
|
583
|
-
nil
|
584
|
-
end
|
585
|
-
end
|
586
|
-
|
587
|
-
parsed_type or raise UnknownImageType
|
588
|
-
end
|
589
|
-
|
590
|
-
def parse_size_for_ico
|
591
|
-
icons = @stream.read(6)[4..5].unpack('v').first
|
592
|
-
sizes = icons.times.map { @stream.read(16).unpack('C2').map { |x| x == 0 ? 256 : x } }.sort_by { |w,h| w * h }
|
593
|
-
sizes.last
|
594
|
-
end
|
595
|
-
alias_method :parse_size_for_cur, :parse_size_for_ico
|
596
|
-
|
597
|
-
# HEIC/AVIF are a special case of the general ISO_BMFF format, in which all data is encapsulated in typed boxes,
|
598
|
-
# with a mandatory ftyp box that is used to indicate particular file types. Is composed of nested "boxes". Each
|
599
|
-
# box has a header composed of
|
600
|
-
# - Size (32 bit integer)
|
601
|
-
# - Box type (4 chars)
|
602
|
-
# - Extended size: only if size === 1, the type field is followed by 64 bit integer of extended size
|
603
|
-
# - Payload: Type-dependent
|
604
|
-
class IsoBmff # :nodoc:
|
605
|
-
def initialize(stream)
|
606
|
-
@stream = stream
|
607
|
-
end
|
608
|
-
|
609
|
-
def width_and_height
|
610
|
-
@rotation = 0
|
611
|
-
@max_size = nil
|
612
|
-
@primary_box = nil
|
613
|
-
@ipma_boxes = []
|
614
|
-
@ispe_boxes = []
|
615
|
-
@final_size = nil
|
616
|
-
|
617
|
-
catch :finish do
|
618
|
-
read_boxes!
|
619
|
-
end
|
620
|
-
|
621
|
-
if [90, 270].include?(@rotation)
|
622
|
-
@final_size.reverse
|
623
|
-
else
|
624
|
-
@final_size
|
625
|
-
end
|
626
|
-
end
|
627
|
-
|
628
|
-
private
|
629
|
-
|
630
|
-
# Format specs: https://www.loc.gov/preservation/digital/formats/fdd/fdd000525.shtml
|
631
|
-
|
632
|
-
# If you need to inspect a heic/heif file, use
|
633
|
-
# https://gpac.github.io/mp4box.js/test/filereader.html
|
634
|
-
def read_boxes!(max_read_bytes = nil)
|
635
|
-
end_pos = max_read_bytes.nil? ? nil : @stream.pos + max_read_bytes
|
636
|
-
index = 0
|
637
|
-
|
638
|
-
loop do
|
639
|
-
return if end_pos && @stream.pos >= end_pos
|
640
|
-
|
641
|
-
box_type, box_size = read_box_header!
|
642
|
-
|
643
|
-
case box_type
|
644
|
-
when "meta"
|
645
|
-
handle_meta_box(box_size)
|
646
|
-
when "pitm"
|
647
|
-
handle_pitm_box(box_size)
|
648
|
-
when "ipma"
|
649
|
-
handle_ipma_box(box_size)
|
650
|
-
when "hdlr"
|
651
|
-
handle_hdlr_box(box_size)
|
652
|
-
when "iprp", "ipco"
|
653
|
-
read_boxes!(box_size)
|
654
|
-
when "irot"
|
655
|
-
handle_irot_box
|
656
|
-
when "ispe"
|
657
|
-
handle_ispe_box(box_size, index)
|
658
|
-
when "mdat"
|
659
|
-
@stream.skip(box_size)
|
660
|
-
else
|
661
|
-
@stream.skip(box_size)
|
662
|
-
end
|
663
|
-
|
664
|
-
index += 1
|
665
|
-
end
|
666
|
-
end
|
667
|
-
|
668
|
-
def handle_irot_box
|
669
|
-
@rotation = (read_uint8! & 0x3) * 90
|
670
|
-
end
|
671
|
-
|
672
|
-
def handle_ispe_box(box_size, index)
|
673
|
-
throw :finish if box_size < 12
|
674
|
-
|
675
|
-
data = @stream.read(box_size)
|
676
|
-
width, height = data[4...12].unpack("N2")
|
677
|
-
@ispe_boxes << { index: index, size: [width, height] }
|
678
|
-
end
|
679
|
-
|
680
|
-
def handle_hdlr_box(box_size)
|
681
|
-
throw :finish if box_size < 12
|
682
|
-
|
683
|
-
data = @stream.read(box_size)
|
684
|
-
throw :finish if data[8...12] != "pict"
|
685
|
-
end
|
686
|
-
|
687
|
-
def handle_ipma_box(box_size)
|
688
|
-
@stream.read(3)
|
689
|
-
flags3 = read_uint8!
|
690
|
-
entries_count = read_uint32!
|
691
|
-
|
692
|
-
entries_count.times do
|
693
|
-
id = read_uint16!
|
694
|
-
essen_count = read_uint8!
|
695
|
-
|
696
|
-
essen_count.times do
|
697
|
-
property_index = read_uint8! & 0x7F
|
698
|
-
|
699
|
-
if flags3 & 1 == 1
|
700
|
-
property_index = (property_index << 7) + read_uint8!
|
701
|
-
end
|
702
|
-
|
703
|
-
@ipma_boxes << { id: id, property_index: property_index - 1 }
|
704
|
-
end
|
705
|
-
end
|
706
|
-
end
|
707
|
-
|
708
|
-
def handle_pitm_box(box_size)
|
709
|
-
data = @stream.read(box_size)
|
710
|
-
@primary_box = data[4...6].unpack("S>")[0]
|
711
|
-
end
|
712
|
-
|
713
|
-
def handle_meta_box(box_size)
|
714
|
-
throw :finish if box_size < 4
|
715
|
-
|
716
|
-
@stream.read(4)
|
717
|
-
read_boxes!(box_size - 4)
|
718
|
-
|
719
|
-
throw :finish if !@primary_box
|
720
|
-
|
721
|
-
primary_indices = @ipma_boxes
|
722
|
-
.select { |box| box[:id] == @primary_box }
|
723
|
-
.map { |box| box[:property_index] }
|
724
|
-
|
725
|
-
ispe_box = @ispe_boxes.find do |box|
|
726
|
-
primary_indices.include?(box[:index])
|
727
|
-
end
|
728
|
-
|
729
|
-
if ispe_box
|
730
|
-
@final_size = ispe_box[:size]
|
731
|
-
end
|
732
|
-
|
733
|
-
throw :finish
|
734
|
-
end
|
735
|
-
|
736
|
-
def read_box_header!
|
737
|
-
size = read_uint32!
|
738
|
-
type = @stream.read(4)
|
739
|
-
size = read_uint64! - 8 if size == 1
|
740
|
-
[type, size - 8]
|
741
|
-
end
|
742
|
-
|
743
|
-
def read_uint8!
|
744
|
-
@stream.read(1).unpack("C")[0]
|
745
|
-
end
|
746
|
-
|
747
|
-
def read_uint16!
|
748
|
-
@stream.read(2).unpack("S>")[0]
|
749
|
-
end
|
750
|
-
|
751
|
-
def read_uint32!
|
752
|
-
@stream.read(4).unpack("N")[0]
|
753
|
-
end
|
754
|
-
|
755
|
-
def read_uint64!
|
756
|
-
@stream.read(8).unpack("Q>")[0]
|
757
|
-
end
|
758
|
-
end
|
759
|
-
|
760
|
-
def parse_size_for_avif
|
761
|
-
bmff = IsoBmff.new(@stream)
|
762
|
-
bmff.width_and_height
|
763
|
-
end
|
764
|
-
|
765
|
-
def parse_size_for_heic
|
766
|
-
bmff = IsoBmff.new(@stream)
|
767
|
-
bmff.width_and_height
|
768
|
-
end
|
769
|
-
|
770
|
-
def parse_size_for_heif
|
771
|
-
bmff = IsoBmff.new(@stream)
|
772
|
-
bmff.width_and_height
|
773
|
-
end
|
774
|
-
|
775
|
-
class Gif # :nodoc:
|
776
|
-
def initialize(stream)
|
777
|
-
@stream = stream
|
778
|
-
end
|
779
|
-
|
780
|
-
def width_and_height
|
781
|
-
@stream.read(11)[6..10].unpack('SS')
|
782
|
-
end
|
783
|
-
|
784
|
-
# Checks if a delay between frames exists and if it does, then the GIFs is
|
785
|
-
# animated
|
786
|
-
def animated?
|
787
|
-
frames = 0
|
788
|
-
|
789
|
-
# "GIF" + version (3) + width (2) + height (2)
|
790
|
-
@stream.skip(10)
|
791
|
-
|
792
|
-
# fields (1) + bg color (1) + pixel ratio (1)
|
793
|
-
fields = @stream.read(3).unpack("CCC")[0]
|
794
|
-
if fields & 0x80 != 0 # Global Color Table
|
795
|
-
# 2 * (depth + 1) colors, each occupying 3 bytes (RGB)
|
796
|
-
@stream.skip(3 * 2 ** ((fields & 0x7) + 1))
|
797
|
-
end
|
798
|
-
|
799
|
-
loop do
|
800
|
-
block_type = @stream.read(1).unpack("C")[0]
|
801
|
-
|
802
|
-
if block_type == 0x21 # Graphic Control Extension
|
803
|
-
# extension type (1) + size (1)
|
804
|
-
size = @stream.read(2).unpack("CC")[1]
|
805
|
-
@stream.skip(size)
|
806
|
-
skip_sub_blocks
|
807
|
-
elsif block_type == 0x2C # Image Descriptor
|
808
|
-
frames += 1
|
809
|
-
return true if frames > 1
|
810
|
-
|
811
|
-
# left position (2) + top position (2) + width (2) + height (2) + fields (1)
|
812
|
-
fields = @stream.read(9).unpack("SSSSC")[4]
|
813
|
-
if fields & 0x80 != 0 # Local Color Table
|
814
|
-
# 2 * (depth + 1) colors, each occupying 3 bytes (RGB)
|
815
|
-
@stream.skip(3 * 2 ** ((fields & 0x7) + 1))
|
816
|
-
end
|
817
|
-
|
818
|
-
@stream.skip(1) # LZW min code size (1)
|
819
|
-
skip_sub_blocks
|
820
|
-
else
|
821
|
-
break # unrecognized block
|
822
|
-
end
|
823
|
-
end
|
824
|
-
|
825
|
-
false
|
826
|
-
end
|
827
|
-
|
828
|
-
private
|
829
|
-
|
830
|
-
def skip_sub_blocks
|
831
|
-
loop do
|
832
|
-
size = @stream.read(1).unpack("C")[0]
|
833
|
-
if size == 0
|
834
|
-
break
|
835
|
-
else
|
836
|
-
@stream.skip(size)
|
837
|
-
end
|
838
|
-
end
|
839
|
-
end
|
840
|
-
end
|
841
|
-
|
842
|
-
def parse_size_for_gif
|
843
|
-
gif = Gif.new(@stream)
|
844
|
-
gif.width_and_height
|
845
|
-
end
|
846
|
-
|
847
|
-
def parse_size_for_png
|
848
|
-
@stream.read(25)[16..24].unpack('NN')
|
849
|
-
end
|
850
|
-
|
851
|
-
def parse_size_for_jpeg
|
852
|
-
exif = nil
|
853
|
-
loop do
|
854
|
-
@state = case @state
|
855
|
-
when nil
|
856
|
-
@stream.skip(2)
|
857
|
-
:started
|
858
|
-
when :started
|
859
|
-
@stream.read_byte == 0xFF ? :sof : :started
|
860
|
-
when :sof
|
861
|
-
case @stream.read_byte
|
862
|
-
when 0xe1 # APP1
|
863
|
-
skip_chars = @stream.read_int - 2
|
864
|
-
data = @stream.read(skip_chars)
|
865
|
-
io = StringIO.new(data)
|
866
|
-
if io.read(4) == "Exif"
|
867
|
-
io.read(2)
|
868
|
-
new_exif = Exif.new(IOStream.new(io)) rescue nil
|
869
|
-
exif ||= new_exif # only use the first APP1 segment
|
870
|
-
end
|
871
|
-
:started
|
872
|
-
when 0xe0..0xef
|
873
|
-
:skipframe
|
874
|
-
when 0xC0..0xC3, 0xC5..0xC7, 0xC9..0xCB, 0xCD..0xCF
|
875
|
-
:readsize
|
876
|
-
when 0xFF
|
877
|
-
:sof
|
878
|
-
else
|
879
|
-
:skipframe
|
880
|
-
end
|
881
|
-
when :skipframe
|
882
|
-
skip_chars = @stream.read_int - 2
|
883
|
-
@stream.skip(skip_chars)
|
884
|
-
:started
|
885
|
-
when :readsize
|
886
|
-
@stream.skip(3)
|
887
|
-
height = @stream.read_int
|
888
|
-
width = @stream.read_int
|
889
|
-
width, height = height, width if exif && exif.rotated?
|
890
|
-
return [width, height, exif ? exif.orientation : 1]
|
891
|
-
end
|
892
|
-
end
|
893
|
-
end
|
894
|
-
|
895
|
-
def parse_size_for_bmp
|
896
|
-
d = @stream.read(32)[14..28]
|
897
|
-
header = d.unpack("C")[0]
|
898
|
-
|
899
|
-
result = if header == 12
|
900
|
-
d[4..8].unpack('SS')
|
901
|
-
else
|
902
|
-
d[4..-1].unpack('l<l<')
|
903
|
-
end
|
904
|
-
|
905
|
-
# ImageHeight is expressed in pixels. The absolute value is necessary because ImageHeight can be negative
|
906
|
-
[result.first, result.last.abs]
|
907
|
-
end
|
908
|
-
|
909
|
-
def parse_size_for_webp
|
910
|
-
vp8 = @stream.read(16)[12..15]
|
911
|
-
_len = @stream.read(4).unpack("V")
|
912
|
-
case vp8
|
913
|
-
when "VP8 "
|
914
|
-
parse_size_vp8
|
915
|
-
when "VP8L"
|
916
|
-
parse_size_vp8l
|
917
|
-
when "VP8X"
|
918
|
-
parse_size_vp8x
|
919
|
-
else
|
920
|
-
nil
|
921
|
-
end
|
922
|
-
end
|
923
|
-
|
924
|
-
def parse_size_vp8
|
925
|
-
w, h = @stream.read(10).unpack("@6vv")
|
926
|
-
[w & 0x3fff, h & 0x3fff]
|
927
|
-
end
|
928
|
-
|
929
|
-
def parse_size_vp8l
|
930
|
-
@stream.skip(1) # 0x2f
|
931
|
-
b1, b2, b3, b4 = @stream.read(4).bytes.to_a
|
932
|
-
[1 + (((b2 & 0x3f) << 8) | b1), 1 + (((b4 & 0xF) << 10) | (b3 << 2) | ((b2 & 0xC0) >> 6))]
|
933
|
-
end
|
934
|
-
|
935
|
-
def parse_size_vp8x
|
936
|
-
flags = @stream.read(4).unpack("C")[0]
|
937
|
-
b1, b2, b3, b4, b5, b6 = @stream.read(6).unpack("CCCCCC")
|
938
|
-
width, height = 1 + b1 + (b2 << 8) + (b3 << 16), 1 + b4 + (b5 << 8) + (b6 << 16)
|
939
|
-
|
940
|
-
if flags & 8 > 0 # exif
|
941
|
-
# parse exif for orientation
|
942
|
-
# TODO: find or create test images for this
|
943
|
-
end
|
944
|
-
|
945
|
-
return [width, height]
|
946
|
-
end
|
947
|
-
|
948
|
-
class Exif # :nodoc:
|
949
|
-
attr_reader :width, :height, :orientation
|
950
|
-
|
951
|
-
def initialize(stream)
|
952
|
-
@stream = stream
|
953
|
-
@width, @height, @orientation = nil
|
954
|
-
parse_exif
|
955
|
-
end
|
956
|
-
|
957
|
-
def rotated?
|
958
|
-
@orientation >= 5
|
959
|
-
end
|
960
|
-
|
961
|
-
private
|
962
|
-
|
963
|
-
def get_exif_byte_order
|
964
|
-
byte_order = @stream.read(2)
|
965
|
-
case byte_order
|
966
|
-
when 'II'
|
967
|
-
@short, @long = 'v', 'V'
|
968
|
-
when 'MM'
|
969
|
-
@short, @long = 'n', 'N'
|
970
|
-
else
|
971
|
-
raise CannotParseImage
|
972
|
-
end
|
973
|
-
end
|
974
|
-
|
975
|
-
def parse_exif_ifd
|
976
|
-
tag_count = @stream.read(2).unpack(@short)[0]
|
977
|
-
tag_count.downto(1) do
|
978
|
-
type = @stream.read(2).unpack(@short)[0]
|
979
|
-
@stream.read(6)
|
980
|
-
data = @stream.read(2).unpack(@short)[0]
|
981
|
-
case type
|
982
|
-
when 0x0100 # image width
|
983
|
-
@width = data
|
984
|
-
when 0x0101 # image height
|
985
|
-
@height = data
|
986
|
-
when 0x0112 # orientation
|
987
|
-
@orientation = data
|
988
|
-
end
|
989
|
-
if @width && @height && @orientation
|
990
|
-
return # no need to parse more
|
991
|
-
end
|
992
|
-
@stream.read(2)
|
993
|
-
end
|
994
|
-
end
|
995
|
-
|
996
|
-
def parse_exif
|
997
|
-
@start_byte = @stream.pos
|
998
|
-
|
999
|
-
get_exif_byte_order
|
1000
|
-
|
1001
|
-
@stream.read(2) # 42
|
1002
|
-
|
1003
|
-
offset = @stream.read(4).unpack(@long)[0]
|
1004
|
-
if @stream.respond_to?(:skip)
|
1005
|
-
@stream.skip(offset - 8)
|
1006
|
-
else
|
1007
|
-
@stream.read(offset - 8)
|
1008
|
-
end
|
1009
|
-
|
1010
|
-
parse_exif_ifd
|
1011
|
-
|
1012
|
-
@orientation ||= 1
|
1013
|
-
end
|
1014
|
-
|
1015
|
-
end
|
1016
|
-
|
1017
|
-
def parse_size_for_tiff
|
1018
|
-
exif = Exif.new(@stream)
|
1019
|
-
if exif.rotated?
|
1020
|
-
[exif.height, exif.width, exif.orientation]
|
1021
|
-
else
|
1022
|
-
[exif.width, exif.height, exif.orientation]
|
1023
|
-
end
|
1024
|
-
end
|
1025
|
-
|
1026
|
-
def parse_size_for_psd
|
1027
|
-
@stream.read(26).unpack("x14NN").reverse
|
1028
|
-
end
|
1029
|
-
|
1030
|
-
class Svg # :nodoc:
|
1031
|
-
def initialize(stream)
|
1032
|
-
@stream = stream
|
1033
|
-
@width, @height, @ratio, @viewbox_width, @viewbox_height = nil
|
1034
|
-
parse_svg
|
1035
|
-
end
|
1036
|
-
|
1037
|
-
def width_and_height
|
1038
|
-
if @width && @height
|
1039
|
-
[@width, @height]
|
1040
|
-
elsif @width && @ratio
|
1041
|
-
[@width, @width / @ratio]
|
1042
|
-
elsif @height && @ratio
|
1043
|
-
[@height * @ratio, @height]
|
1044
|
-
elsif @viewbox_width && @viewbox_height
|
1045
|
-
[@viewbox_width, @viewbox_height]
|
1046
|
-
else
|
1047
|
-
nil
|
1048
|
-
end
|
1049
|
-
end
|
1050
|
-
|
1051
|
-
private
|
1052
|
-
|
1053
|
-
def parse_svg
|
1054
|
-
attr_name = []
|
1055
|
-
state = nil
|
1056
|
-
|
1057
|
-
while (char = @stream.read(1)) && state != :stop do
|
1058
|
-
case char
|
1059
|
-
when "="
|
1060
|
-
if attr_name.join =~ /width/i
|
1061
|
-
@stream.read(1)
|
1062
|
-
@width = @stream.read_string_int
|
1063
|
-
return if @height
|
1064
|
-
elsif attr_name.join =~ /height/i
|
1065
|
-
@stream.read(1)
|
1066
|
-
@height = @stream.read_string_int
|
1067
|
-
return if @width
|
1068
|
-
elsif attr_name.join =~ /viewbox/i
|
1069
|
-
values = attr_value.split(/\s/)
|
1070
|
-
if values[2].to_f > 0 && values[3].to_f > 0
|
1071
|
-
@ratio = values[2].to_f / values[3].to_f
|
1072
|
-
@viewbox_width = values[2].to_i
|
1073
|
-
@viewbox_height = values[3].to_i
|
1074
|
-
end
|
1075
|
-
end
|
1076
|
-
when /\w/
|
1077
|
-
attr_name << char
|
1078
|
-
when "<"
|
1079
|
-
attr_name = [char]
|
1080
|
-
when ">"
|
1081
|
-
state = :stop if state == :started
|
1082
|
-
else
|
1083
|
-
state = :started if attr_name.join == "<svg"
|
1084
|
-
attr_name.clear
|
1085
|
-
end
|
1086
|
-
end
|
1087
|
-
end
|
1088
|
-
|
1089
|
-
def attr_value
|
1090
|
-
@stream.read(1)
|
1091
|
-
|
1092
|
-
value = []
|
1093
|
-
while @stream.read(1) =~ /([^"])/
|
1094
|
-
value << $1
|
1095
|
-
end
|
1096
|
-
value.join
|
1097
|
-
end
|
1098
|
-
end
|
1099
|
-
|
1100
|
-
def parse_size_for_svg
|
1101
|
-
svg = Svg.new(@stream)
|
1102
|
-
svg.width_and_height
|
1103
|
-
end
|
1104
|
-
|
1105
|
-
def parse_animated_for_gif
|
1106
|
-
gif = Gif.new(@stream)
|
1107
|
-
gif.animated?
|
1108
|
-
end
|
1109
|
-
|
1110
|
-
def parse_animated_for_png
|
1111
|
-
# Signature (8) + IHDR chunk (4 + 4 + 13 + 4)
|
1112
|
-
@stream.read(33)
|
1113
|
-
|
1114
|
-
loop do
|
1115
|
-
length = @stream.read(4).unpack("L>")[0]
|
1116
|
-
type = @stream.read(4)
|
1117
|
-
|
1118
|
-
case type
|
1119
|
-
when "acTL"
|
1120
|
-
return true
|
1121
|
-
when "IDAT"
|
1122
|
-
return false
|
1123
|
-
end
|
1124
|
-
|
1125
|
-
@stream.skip(length + 4)
|
1126
|
-
end
|
1127
|
-
end
|
1128
|
-
|
1129
|
-
def parse_animated_for_webp
|
1130
|
-
vp8 = @stream.read(16)[12..15]
|
1131
|
-
_len = @stream.read(4).unpack("V")
|
1132
|
-
case vp8
|
1133
|
-
when "VP8 "
|
1134
|
-
false
|
1135
|
-
when "VP8L"
|
1136
|
-
false
|
1137
|
-
when "VP8X"
|
1138
|
-
flags = @stream.read(4).unpack("C")[0]
|
1139
|
-
flags & 2 > 0
|
1140
|
-
else
|
1141
|
-
nil
|
1142
|
-
end
|
1143
|
-
end
|
1144
|
-
|
1145
|
-
def parse_animated_for_avif
|
1146
|
-
@stream.peek(12)[4..-1] == "ftypavis"
|
1147
|
-
end
|
1148
|
-
end
|