Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add API to check if a GIF is animated #114

Merged
merged 1 commit into from Jun 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
115 changes: 111 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 a boolean 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,13 @@ def parse_size
send("parse_size_for_#{@type}")
end

def parse_animated
@type = parse_type unless @type
return nil if @type == nil

@type == :gif ? send("parse_animated_for_#{@type}") : false
end

def fetch_using_base64(uri)
data = uri.split(',')[1]
decoded = Base64.decode64(data)
Expand Down Expand Up @@ -524,8 +565,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 +887,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