class FastImage

def self.size(uri, options={})


If set to true causes an exception to be raised if the image size cannot be found for any reason.
[:raise_on_failure]
Overrides the default timeout of 2 seconds. Applies both to reading from and opening the http connection.
[:timeout]
=== Supported options

=> raises FastImage::SizeNotFound
FastImage.size("http://stephensykes.com/images/faulty.jpg", :raise_on_failure=>true)
=> raises FastImage::ImageFetchFailure
FastImage.size("http://stephensykes.com/favicon.ico", :raise_on_failure=>true, :timeout=>0.01)
=> raises FastImage::UnknownImageType
FastImage.size("http://stephensykes.com/favicon.ico", :raise_on_failure=>true)
=> raises FastImage::ImageFetchFailure
FastImage.size("http://pennysmalls.com/does_not_exist", :raise_on_failure=>true)
=> nil
FastImage.size("http://pennysmalls.com/does_not_exist")
=> [882, 470]
FastImage.size("test/fixtures/test.jpg")
=> [512, 512]
FastImage.size("http://www-ece.rice.edu/~wakin/images/lena512.bmp")
=> [500, 375]
FastImage.size("http://farm4.static.flickr.com/3023/3047236863_9dce98b836.jpg")
=> [16, 16]
FastImage.size("http://stephensykes.com/images/pngimage")
=> [266, 56]
FastImage.size("http://stephensykes.com/images/ss.com_x.gif")

require 'fastimage'

=== Example

FastImage knows about GIF, JPEG, BMP and PNG files.

:raise_on_failure => true in the options.
If you wish FastImage to raise if it cannot size the image for any reason, then pass

This can be changed by passing a :timeout => number_of_seconds in the options.
By default there is a timeout of 2 seconds for opening and reading from a remote server.

It will return nil if the image could not be fetched, or if the image type was not recognised.
Returns an array containing the width and height of the image.
def self.size(uri, options={})
  new(uri, options).size
end

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.
[:raise_on_failure]
Overrides the default timeout of 2 seconds. Applies both to reading from and opening the http connection.
[:timeout]
=== Supported options

=> nil
FastImage.type("http://pennysmalls.com/does_not_exist")
=> :jpeg
FastImage.type("test/fixtures/test.jpg")
=> :bmp
FastImage.type("http://www-ece.rice.edu/~wakin/images/lena512.bmp")
=> :jpeg
FastImage.type("http://farm4.static.flickr.com/3023/3047236863_9dce98b836.jpg")
=> :png
FastImage.type("http://stephensykes.com/images/pngimage")
=> :gif
FastImage.type("http://stephensykes.com/images/ss.com_x.gif")

require 'fastimage'

=== Example

:raise_on_failure => true in the options.
If you wish FastImage to raise if it cannot find the type of the image for any reason, then pass

This can be changed by passing a :timeout => number_of_seconds in the options.
By default there is a timeout of 2 seconds for opening and reading from a remote server.

It will return nil if the image could not be fetched, or if the image type was not recognised.
Returns an symbol indicating the image type fetched from a uri.
def self.type(uri, options={})
  new(uri, options.merge(:type_only=>true)).type
end

def fetch_using_http

def fetch_using_http
  setup_http
  @http.request_get(@parsed_uri.request_uri) do |res|
    raise ImageFetchFailure unless res.is_a?(Net::HTTPSuccess)
    res.read_body do |str|
      break if parse_packet(str)
    end
  end
end

def fetch_using_open_uri

def fetch_using_open_uri
  open(@uri) do |s|
    while str = s.read(LocalFileChunkSize)
      break if parse_packet(str)
    end
  end
end

def get_byte

def get_byte
  get_chars(1).unpack("C")[0]
end

def get_chars(n)

def get_chars(n)
  if @strpos + n - 1 >= @str.size
    @unused_str = @str[@strpos..-1]
    raise MoreCharsNeeded
  else
    result = @str[@strpos..(@strpos + n - 1)]
    @strpos += n
    result
  end
end

def initialize(uri, options={})

def initialize(uri, options={})
  @property = options[:type_only] ? :type : :size
  @timeout = options[:timeout] || DefaultTimeout
  @uri = uri
  begin
    @parsed_uri = URI.parse(uri)
  rescue URI::InvalidURIError
    fetch_using_open_uri
  else
    if @parsed_uri.scheme == "http" || @parsed_uri.scheme == "https"
      fetch_using_http
    else
      fetch_using_open_uri
    end
  end
  raise SizeNotFound if options[:raise_on_failure] && @property == :size && !@size
rescue Timeout::Error, SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ECONNRESET, 
  ImageFetchFailure, Net::HTTPBadResponse, EOFError, Errno::ENOENT
  raise ImageFetchFailure if options[:raise_on_failure]
rescue NoMethodError  # 1.8.7p248 can raise this due to a net/http bug
  raise ImageFetchFailure if options[:raise_on_failure]
rescue UnknownImageType
  raise UnknownImageType if options[:raise_on_failure]
end

def parse_packet(str)


returns true once result is achieved
def parse_packet(str)
  @str = (@unused_str || "") + str
  @strpos = 0
  begin
    result = send("parse_#{@property}")
    if result 
      instance_variable_set("@#{@property}", result)
      true
    end
  rescue MoreCharsNeeded
    false
  end
end

def parse_size

def parse_size
  @type = parse_type unless @type
  @strpos = 0
  send("parse_size_for_#{@type}")
end

def parse_size_for_bmp

def parse_size_for_bmp
  d = get_chars(29)[14..28]
  d.unpack("C")[0] == 40 ? d[4..-1].unpack('LL') : d[4..8].unpack('SS')
end

def parse_size_for_gif

def parse_size_for_gif
  get_chars(11)[6..10].unpack('SS')
end

def parse_size_for_jpeg

def parse_size_for_jpeg
  loop do
    @state = case @state
    when nil
      get_chars(2)
      :started
    when :started
      get_byte == 0xFF ? :sof : :started          
    when :sof
      c = get_byte
      if (0xe0..0xef).include?(c)
        :skipframe
      elsif [0xC0..0xC3, 0xC5..0xC7, 0xC9..0xCB, 0xCD..0xCF].detect {|r| r.include? c}
        :readsize
      else
        :skipframe
      end
    when :skipframe
      @skip_chars = read_int(get_chars(2)) - 2
      :do_skip
    when :do_skip
      get_chars(@skip_chars)
      :started
    when :readsize
      s = get_chars(7)
      return [read_int(s[5..6]), read_int(s[3..4])]
    end
  end
end

def parse_size_for_png

def parse_size_for_png
  get_chars(25)[16..24].unpack('NN')
end

def parse_type

def parse_type
  case get_chars(2)
  when "BM"
    :bmp
  when "GI"
    :gif
  when 0xff.chr + 0xd8.chr
    :jpeg
  when 0x89.chr + "P"
    :png
  else
    raise UnknownImageType
  end
end

def read_int(str)

def read_int(str)
  size_bytes = str.unpack("CC")
  (size_bytes[0] << 8) + size_bytes[1]
end

def setup_http

def setup_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 = @timeout
end