# FastImage finds the size or type of an image given its uri.# It is careful to only fetch and parse as much of the image as is needed to determine the result.# It does this by using a feature of Net::HTTP that yields strings from the resource being fetched# as soon as the packets arrive.## No external libraries such as ImageMagick are used here, this is a very lightweight solution to # finding image information.## FastImage knows about GIF, JPEG, BMP and PNG files.## FastImage can also read files from the local filesystem by supplying the path instead of a uri.# In this case FastImage uses the open-uri library to read the file in chunks of 256 bytes until# it has enough. This is possibly a useful bandwidth-saving feature if the file is on a network# attached disk rather than truly local.## === Examples# require 'fastimage'## FastImage.size("http://stephensykes.com/images/ss.com_x.gif")# => [266, 56]# FastImage.type("http://stephensykes.com/images/pngimage")# => :png# FastImage.type("/some/local/file.gif")# => :gif## === References# * http://snippets.dzone.com/posts/show/805# * http://www.anttikupila.com/flash/getting-jpg-dimensions-with-as3-without-loading-the-entire-file/# * http://pennysmalls.com/2008/08/19/find-jpeg-dimensions-fast-in-ruby/# * http://imagesize.rubyforge.org/#require'net/https'require'open-uri'classFastImageattr_reader:size,:typeclassFastImageException<StandardError# :nodoc:endclassMoreCharsNeeded<FastImageException# :nodoc:endclassUnknownImageType<FastImageException# :nodoc:endclassImageFetchFailure<FastImageException# :nodoc:endclassSizeNotFound<FastImageException# :nodoc:endDefaultTimeout=2LocalFileChunkSize=256# Returns an array containing the width and height of the image.# 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 size the image for any reason, then pass# :raise_on_failure => true in the options.## FastImage knows about GIF, JPEG, BMP and PNG files.## === Example## require 'fastimage'## FastImage.size("http://stephensykes.com/images/ss.com_x.gif")# => [266, 56]# FastImage.size("http://stephensykes.com/images/pngimage")# => [16, 16]# FastImage.size("http://farm4.static.flickr.com/3023/3047236863_9dce98b836.jpg")# => [500, 375]# FastImage.size("http://www-ece.rice.edu/~wakin/images/lena512.bmp")# => [512, 512]# FastImage.size("test/fixtures/test.jpg")# => [882, 470]# FastImage.size("http://pennysmalls.com/does_not_exist")# => nil# FastImage.size("http://pennysmalls.com/does_not_exist", :raise_on_failure=>true)# => raises FastImage::ImageFetchFailure# FastImage.size("http://stephensykes.com/favicon.ico", :raise_on_failure=>true)# => raises FastImage::UnknownImageType# FastImage.size("http://stephensykes.com/favicon.ico", :raise_on_failure=>true, :timeout=>0.01)# => raises FastImage::ImageFetchFailure# FastImage.size("http://stephensykes.com/images/faulty.jpg", :raise_on_failure=>true)# => raises FastImage::SizeNotFound## === 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 size cannot be found for any reason.#defself.size(uri,options={})new(uri,options).sizeend# Returns an symbol indicating the image type fetched from a uri.# 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.type("http://stephensykes.com/images/ss.com_x.gif")# => :gif# FastImage.type("http://stephensykes.com/images/pngimage")# => :png# FastImage.type("http://farm4.static.flickr.com/3023/3047236863_9dce98b836.jpg")# => :jpeg# FastImage.type("http://www-ece.rice.edu/~wakin/images/lena512.bmp")# => :bmp# FastImage.type("test/fixtures/test.jpg")# => :jpeg# FastImage.type("http://pennysmalls.com/does_not_exist")# => nil## === 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.#defself.type(uri,options={})new(uri,options.merge(:type_only=>true)).typeenddefinitialize(uri,options={})@property=options[:type_only]?:type::size@timeout=options[:timeout]||DefaultTimeout@uri=uribegin@parsed_uri=URI.parse(uri)rescueURI::InvalidURIErrorfetch_using_open_urielseif@parsed_uri.scheme=="http"||@parsed_uri.scheme=="https"fetch_using_httpelsefetch_using_open_uriendendraiseSizeNotFoundifoptions[:raise_on_failure]&&@property==:size&&!@sizerescueTimeout::Error,SocketError,Errno::ECONNREFUSED,Errno::EHOSTUNREACH,Errno::ECONNRESET,ImageFetchFailure,Net::HTTPBadResponse,EOFError,Errno::ENOENTraiseImageFetchFailureifoptions[:raise_on_failure]rescueNoMethodError# 1.8.7p248 can raise this due to a net/http bugraiseImageFetchFailureifoptions[:raise_on_failure]rescueUnknownImageTyperaiseUnknownImageTypeifoptions[:raise_on_failure]endprivatedeffetch_using_httpsetup_http@http.request_get(@parsed_uri.request_uri)do|res|raiseImageFetchFailureunlessres.is_a?(Net::HTTPSuccess)res.read_bodydo|str|breakifparse_packet(str)endendenddefsetup_http@http=Net::HTTP.new(@parsed_uri.host,@parsed_uri.port)@http.use_ssl=(@parsed_uri.scheme=="https")@http.verify_mode=OpenSSL::SSL::VERIFY_NONE@http.open_timeout=@timeout@http.read_timeout=@timeoutenddeffetch_using_open_uriopen(@uri)do|s|whilestr=s.read(LocalFileChunkSize)breakifparse_packet(str)endendend# returns true once result is achieved#defparse_packet(str)@str=(@unused_str||"")+str@strpos=0beginresult=send("parse_#{@property}")ifresultinstance_variable_set("@#{@property}",result)trueendrescueMoreCharsNeededfalseendenddefparse_size@type=parse_typeunless@type@strpos=0send("parse_size_for_#{@type}")enddefget_chars(n)if@strpos+n-1>=@str.size@unused_str=@str[@strpos..-1]raiseMoreCharsNeededelseresult=@str[@strpos..(@strpos+n-1)]@strpos+=nresultendenddefget_byteget_chars(1).unpack("C")[0]enddefread_int(str)size_bytes=str.unpack("CC")(size_bytes[0]<<8)+size_bytes[1]enddefparse_typecaseget_chars(2)when"BM":bmpwhen"GI":gifwhen0xff.chr+0xd8.chr:jpegwhen0x89.chr+"P":pngelseraiseUnknownImageTypeendenddefparse_size_for_gifget_chars(11)[6..10].unpack('SS')enddefparse_size_for_pngget_chars(25)[16..24].unpack('NN')enddefparse_size_for_jpegloopdo@state=case@statewhennilget_chars(2):startedwhen:startedget_byte==0xFF?:sof::startedwhen:sofc=get_byteif(0xe0..0xef).include?(c):skipframeelsif[0xC0..0xC3,0xC5..0xC7,0xC9..0xCB,0xCD..0xCF].detect{|r|r.include?c}:readsizeelse:skipframeendwhen:skipframe@skip_chars=read_int(get_chars(2))-2:do_skipwhen:do_skipget_chars(@skip_chars):startedwhen:readsizes=get_chars(7)return[read_int(s[5..6]),read_int(s[3..4])]endendenddefparse_size_for_bmpd=get_chars(29)[14..28]d.unpack("C")[0]==40?d[4..-1].unpack('LL'):d[4..8].unpack('SS')endend