Skip to content

Commit

Permalink
Add check for animated GIFs
Browse files Browse the repository at this point in the history
  • Loading branch information
nbianca committed May 21, 2020
1 parent bad9027 commit c59b45e
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 4 deletions.
113 changes: 109 additions & 4 deletions lib/fastimage.rb
Expand Up @@ -70,7 +70,7 @@ module URI
end

class FastImage
attr_reader :size, :type, :content_length, :orientation
attr_reader :size, :type, :content_length, :orientation, :animated

attr_reader :bytes_read

Expand Down Expand Up @@ -179,6 +179,34 @@ def self.type(uri, options={})
new(uri, options.merge(:type_only=>true)).type
end

# Returns an positive value indicating the image is animated.
# It will return nil if the image could not be fetched, or if the image type was not recognised.
#
# By default there is a timeout of 2 seconds for opening and reading from a remote server.
# This can be changed by passing a :timeout => number_of_seconds in the options.
#
# If you wish FastImage to raise if it cannot find the type of the image for any reason, then pass
# :raise_on_failure => true in the options.
#
# === Example
#
# require 'fastimage'
#
# FastImage.animated("test/fixtures/test.gif")
# => false
# FastImage.animated("test/fixtures/animated.gif")
# => true
#
# === Supported options
# [:timeout]
# Overrides the default timeout of 2 seconds. Applies both to reading from and opening the http connection.
# [:raise_on_failure]
# If set to true causes an exception to be raised if the image type cannot be found for any reason.
#
def self.animated?(uri, options={})
new(uri, options.merge(:animated_only=>true)).animated
end

def initialize(uri, options={})
@uri = uri
@options = {
Expand All @@ -189,7 +217,13 @@ def initialize(uri, options={})
:http_header => {}
}.merge(options)

@property = @options[:type_only] ? :type : :size
@property = if @options[:animated_only]
:animated
elsif @options[:type_only]
:type
else
:size
end

@type, @state = nil

Expand Down Expand Up @@ -369,7 +403,7 @@ def parse_packets(stream)

begin
result = send("parse_#{@property}")
if result
if result != nil
# extract exif orientation if it was found
if @property == :size && result.size == 3
@orientation = result.pop
Expand All @@ -391,6 +425,11 @@ def parse_size
send("parse_size_for_#{@type}")
end

def parse_animated
@type = parse_type unless @type
@type == :gif ? send("parse_animated_for_#{@type}") : nil
end

def fetch_using_base64(uri)
data = uri.split(',')[1]
decoded = Base64.decode64(data)
Expand Down Expand Up @@ -524,8 +563,69 @@ def parse_size_for_ico
end
alias_method :parse_size_for_cur, :parse_size_for_ico

class Gif # :nodoc:
def initialize(stream)
@stream = stream
end

def width_and_height
@stream.read(11)[6..10].unpack('SS')
end

# Checks if a delay between frames exists and if it does, then the GIFs is
# animated
def animated?
delay = 0

@stream.read(10) # "GIF" + version (3) + width (2) + height (2)

fields = @stream.read(3).unpack("C")[0] # fields (1) + bg color (1) + pixel ratio (1)

# Skip Global Color Table if it exists
if fields & 0x80
# 2 * (depth + 1) colors, each occupying 3 bytes (RGB)
@stream.skip(3 * 2 ** ((fields & 0x7) + 1))
end

loop do
block_type = @stream.read(1).unpack("C")[0]
if block_type == 0x21
extension_type = @stream.read(1).unpack("C")[0]
size = @stream.read(1).unpack("C")[0]
if extension_type == 0xF9
delay = @stream.read(4).unpack("CSC")[1] # fields (1) + delay (2) + transparent index (1)
break
elsif extension_type == 0xFF
@stream.skip(size) # application ID (8) + version (3)
else
return # unrecognized extension
end
skip_sub_blocks
else
return # unrecognized block
end
end

delay > 0
end

private

def skip_sub_blocks
loop do
size = @stream.read(1).unpack("C")[0]
if size == 0
break
else
@stream.skip(size)
end
end
end
end

def parse_size_for_gif
@stream.read(11)[6..10].unpack('SS')
gif = Gif.new(@stream)
gif.width_and_height
end

def parse_size_for_png
Expand Down Expand Up @@ -785,4 +885,9 @@ def parse_size_for_svg
svg = Svg.new(@stream)
svg.width_and_height
end

def parse_animated_for_gif
gif = Gif.new(@stream)
gif.animated?
end
end
Binary file added test/fixtures/animated.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions test/test.rb
Expand Up @@ -14,6 +14,7 @@
"test_coreheader.bmp"=>[:bmp, [40, 27]],
"test_v5header.bmp"=>[:bmp, [40, 27]],
"test.gif"=>[:gif, [17, 32]],
"animated.gif"=>[:gif, [400, 400]],
"test.jpg"=>[:jpeg, [882, 470]],
"test.png"=>[:png, [30, 20]],
"test2.jpg"=>[:jpeg, [250, 188]],
Expand Down Expand Up @@ -102,6 +103,12 @@ def test_should_report_size_correctly
end
end

def test_should_report_animated_correctly
assert_equal nil, FastImage.animated?(TestUrl + "test.png")
assert_equal false, FastImage.animated?(TestUrl + "test.gif")
assert_equal true, FastImage.animated?(TestUrl + "animated.gif")
end

def test_should_return_nil_on_fetch_failure
assert_nil FastImage.size(TestUrl + "does_not_exist")
end
Expand Down

0 comments on commit c59b45e

Please sign in to comment.