From a5e4052dd59c7d43e3676e0552e4c9292342b5a9 Mon Sep 17 00:00:00 2001 From: Stephen Sykes Date: Sat, 6 Apr 2024 15:52:46 +0300 Subject: [PATCH] Incorporate lazy property fetching --- lib/fastimage/fastimage.rb | 108 ++++++++++++------ lib/fastimage/fastimage_parsing/jpeg.rb | 3 +- .../fastimage_parsing/type_parser.rb | 3 +- test/test.rb | 33 +++++- 4 files changed, 107 insertions(+), 40 deletions(-) diff --git a/lib/fastimage/fastimage.rb b/lib/fastimage/fastimage.rb index 20c40a3..61f8b41 100644 --- a/lib/fastimage/fastimage.rb +++ b/lib/fastimage/fastimage.rb @@ -21,8 +21,6 @@ class FastImage include FastImageParsing - - attr_reader :size, :type, :content_length, :orientation, :animated attr_reader :bytes_read @@ -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. @@ -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 @@ -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 @@ -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| @@ -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 @@ -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 @@ -388,7 +417,7 @@ def fetch_using_base64(uri) rescue raise CannotParseImage end - @content_length = decoded.size + fetch_using_read StringIO.new(decoded) end @@ -396,7 +425,17 @@ 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 @@ -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 diff --git a/lib/fastimage/fastimage_parsing/jpeg.rb b/lib/fastimage/fastimage_parsing/jpeg.rb index ef79f24..fb2eeb5 100644 --- a/lib/fastimage/fastimage_parsing/jpeg.rb +++ b/lib/fastimage/fastimage_parsing/jpeg.rb @@ -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 diff --git a/lib/fastimage/fastimage_parsing/type_parser.rb b/lib/fastimage/fastimage_parsing/type_parser.rb index be854be..2b6b21b 100644 --- a/lib/fastimage/fastimage_parsing/type_parser.rb +++ b/lib/fastimage/fastimage_parsing/type_parser.rb @@ -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" @@ -65,4 +66,4 @@ def type parsed_type or raise FastImage::UnknownImageType end end -end \ No newline at end of file +end diff --git a/test/test.rb b/test/test.rb index 9de064c..40d2144 100644 --- a/test/test.rb +++ b/test/test.rb @@ -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 @@ -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 @@ -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 @@ -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