Skip to content

Commit

Permalink
Incorporate lazy property fetching
Browse files Browse the repository at this point in the history
  • Loading branch information
sdsykes committed Apr 6, 2024
1 parent b50c834 commit a5e4052
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 40 deletions.
108 changes: 71 additions & 37 deletions lib/fastimage/fastimage.rb
Expand Up @@ -21,8 +21,6 @@

class FastImage
include FastImageParsing

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

attr_reader :bytes_read

Expand Down Expand Up @@ -153,7 +151,7 @@ def self.size(uri, options={})
# If set to true causes an exception to be raised if the image type cannot be found for any reason.
#
def self.type(uri, options={})
new(uri, options.merge(:type_only=>true)).type
new(uri, options).type
end

# Returns a boolean value indicating the image is animated.
Expand Down Expand Up @@ -181,38 +179,73 @@ def self.type(uri, options={})
# 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
new(uri, options).animated
end

def initialize(uri, options={})
@uri = uri
@options = {
:type_only => false,
:timeout => DefaultTimeout,
:raise_on_failure => false,
:proxy => nil,
:http_header => {}
}.merge(options)
end

@property = if @options[:animated_only]
:animated
elsif @options[:type_only]
:type
else
:size
def type
@property = :type
fetch unless defined?(@type)
@type
end

def size
@property = :size
begin
fetch unless defined?(@size)
rescue CannotParseImage
end

raise BadImageURI if uri.nil?
raise SizeNotFound if @options[:raise_on_failure] && !@size

@size
end

def orientation
size unless defined?(@size)
@orientation ||= 1 if @size
end

def width
size && @size[0]
end

def height
size && @size[1]
end

def animated
@property = :animated
fetch unless defined?(@animated)
@animated
end

def content_length
@property = :content_length
fetch unless defined?(@content_length)
@content_length
end

@type, @state = nil
# find an appropriate method to fetch the image according to the passed parameter
def fetch
raise BadImageURI if @uri.nil?

if uri.respond_to?(:read)
fetch_using_read(uri)
elsif uri.start_with?('data:')
fetch_using_base64(uri)
if @uri.respond_to?(:read)
fetch_using_read(@uri)
elsif @uri.start_with?('data:')
fetch_using_base64(@uri)
else
begin
@parsed_uri = URI.parse(uri)
@parsed_uri = URI.parse(@uri)
rescue URI::InvalidURIError
fetch_using_file_open
else
Expand All @@ -230,19 +263,11 @@ def initialize(uri, options={})
Errno::ENETUNREACH, ImageFetchFailure, Net::HTTPBadResponse, EOFError, Errno::ENOENT,
OpenSSL::SSL::SSLError
raise ImageFetchFailure if @options[:raise_on_failure]
rescue UnknownImageType, BadImageURI
rescue UnknownImageType, BadImageURI, CannotParseImage
raise if @options[:raise_on_failure]
rescue CannotParseImage
if @options[:raise_on_failure]
if @property == :size
raise SizeNotFound
else
raise ImageFetchFailure
end
end


ensure
uri.rewind if uri.respond_to?(:rewind)
@uri.rewind if @uri.respond_to?(:rewind)

end

Expand Down Expand Up @@ -287,6 +312,7 @@ def fetch_using_http_from_parsed_uri
raise ImageFetchFailure unless res.is_a?(Net::HTTPSuccess)

@content_length = res.content_length
break if @property == :content_length

read_fiber = Fiber.new do
res.read_body do |str|
Expand Down Expand Up @@ -349,6 +375,8 @@ def setup_http
end

def fetch_using_read(readable)
return @content_length = readable.size if @property == :content_length && readable.respond_to?(:size)

readable.rewind if readable.respond_to?(:rewind)
# Pathnames respond to read, but always return the first
# chunk of the file unlike an IO (even though the
Expand Down Expand Up @@ -376,7 +404,8 @@ def fetch_using_read(readable)
end

def fetch_using_file_open
@content_length = File.size?(@uri)
return @content_length = File.size?(@uri) if @property == :content_length

File.open(@uri) do |s|
fetch_using_read(s)
end
Expand All @@ -388,15 +417,25 @@ def fetch_using_base64(uri)
rescue
raise CannotParseImage
end
@content_length = decoded.size

fetch_using_read StringIO.new(decoded)
end

def parse_packets(stream)
@stream = stream

begin
result = send("parse_#{@property}")
@type = TypeParser.new(@stream).type unless defined?(@type)

result = case @property
when :type
@type
when :size
parse_size
when :animated
parse_animated
end

if result != nil
# extract exif orientation if it was found
if @property == :size && result.size == 3
Expand All @@ -414,12 +453,7 @@ def parse_packets(stream)
end
end

def parse_type
TypeParser.new(@stream).type
end

def parser_class
@type ||= parse_type
klass = Parsers[@type]
raise UnknownImageType unless klass
klass
Expand Down
3 changes: 2 additions & 1 deletion lib/fastimage/fastimage_parsing/jpeg.rb
Expand Up @@ -6,8 +6,9 @@ class IOStream < SimpleDelegator # :nodoc:
class Jpeg < ImageBase # :nodoc:
def dimensions
exif = nil
state = nil
loop do
@state = case @state
state = case state
when nil
@stream.skip(2)
:started
Expand Down
3 changes: 2 additions & 1 deletion lib/fastimage/fastimage_parsing/type_parser.rb
Expand Up @@ -4,6 +4,7 @@ def initialize(stream)
@stream = stream
end

# type will use peek to get enough bytes to determing the type of the image
def type
parsed_type = case @stream.peek(2)
when "BM"
Expand Down Expand Up @@ -65,4 +66,4 @@ def type
parsed_type or raise FastImage::UnknownImageType
end
end
end
end
33 changes: 32 additions & 1 deletion test/test.rb
Expand Up @@ -139,6 +139,14 @@ def test_should_report_animated_correctly
assert_equal true, FastImage.animated?(TestUrl + "avif/red_green_flash.avif")
end

def test_should_report_multiple_properties
fi = FastImage.new(File.join(FixturePath, "animated.gif"))
assert_equal :gif, fi.type
assert_equal [400, 400], fi.size
assert_equal true, fi.animated
assert_equal 1001718, fi.content_length
end

def test_should_return_nil_on_fetch_failure
assert_nil FastImage.size(TestUrl + "does_not_exist")
end
Expand Down Expand Up @@ -437,6 +445,13 @@ def test_content_length
FakeWeb.register_uri(:get, url, :body => File.join(FixturePath, "test.jpg"), :content_length => 52)

assert_equal 52, FastImage.new(url).content_length

assert_equal 322, FastImage.new(File.join(FixturePath, "test.png")).content_length
assert_equal 322, FastImage.new(Pathname.new(File.join(FixturePath, "test.png"))).content_length

string = File.read(File.join(FixturePath, "test.png"))
stringio = StringIO.new(string)
assert_equal 322, FastImage.new(stringio).content_length
end

def test_content_length_not_provided
Expand Down Expand Up @@ -473,7 +488,7 @@ def test_should_raise_when_handling_invalid_ico_files
def test_should_support_data_uri_scheme_images
assert_equal DataUriImageInfo[0], FastImage.type(DataUriImage)
assert_equal DataUriImageInfo[1], FastImage.size(DataUriImage)
assert_raises(FastImage::ImageFetchFailure) do
assert_raises(FastImage::CannotParseImage) do
FastImage.type("data:", :raise_on_failure => true)
end
end
Expand Down Expand Up @@ -504,4 +519,20 @@ def test_raises_when_uri_is_nil_and_raise_on_failure_is_set
FastImage.size(nil, :raise_on_failure => true)
end
end

def test_width
assert_equal 30, FastImage.new(TestUrl + "test.png").width
assert_equal nil, FastImage.new(TestUrl + "does_not_exist").width
end

def test_height
assert_equal 20, FastImage.new(TestUrl + "test.png").height
assert_equal nil, FastImage.new(TestUrl + "does_not_exist").height
end

def test_content_length_after_size
fi = FastImage.new(File.join(FixturePath, "test.png"))
fi.size
assert_equal 322, fi.content_length
end
end

0 comments on commit a5e4052

Please sign in to comment.