class FastImage

Constants

DefaultTimeout
LocalFileChunkSize

Attributes

bytes_read[R]
content_length[R]
orientation[R]
size[R]
type[R]

Public Class Methods

new(uri, options={}) click to toggle source
# File lib/fastimage.rb, line 182
def initialize(uri, options={})
  @uri = uri
  @options = {
    :type_only        => false,
    :timeout          => DefaultTimeout,
    :raise_on_failure => false,
    :proxy            => nil,
    :http_header      => {}
  }.merge(options)

  @property = @options[:type_only] ? :type : :size

  @type, @state = nil

  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)
    rescue URI::InvalidURIError
      fetch_using_file_open
    else
      if @parsed_uri.scheme == "http" || @parsed_uri.scheme == "https"
        fetch_using_http
      else
        fetch_using_file_open
      end
    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]
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)

end
size(uri, options={}) click to toggle source

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, TIFF, ICO, CUR, PNG, PSD, SVG and WEBP 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)
=> [16, 16]
FastImage.size("http://stephensykes.com/images/squareBlue.icns", :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.

# File lib/fastimage.rb, line 136
def self.size(uri, options={})
  new(uri, options).size
end
type(uri, options={}) click to toggle source

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://stephensykes.com/does_not_exist")
=> nil
File.open("/some/local/file.gif", "r") {|io| FastImage.type(io)}
=> :gif
FastImage.type("test/fixtures/test.tiff")
=> :tiff
FastImage.type("test/fixtures/test.psd")
=> :psd

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.

# File lib/fastimage.rb, line 178
def self.type(uri, options={})
  new(uri, options.merge(:type_only=>true)).type
end

Private Instance Methods

fetch_using_base64(uri) click to toggle source
# File lib/fastimage.rb, line 389
def fetch_using_base64(uri)
  data = uri.split(',')[1]
  fetch_using_read StringIO.new(Base64.decode64(data))
end
fetch_using_file_open() click to toggle source
# File lib/fastimage.rb, line 355
def fetch_using_file_open
  @content_length = File.size?(@uri)
  File.open(@uri) do |s|
    fetch_using_read(s)
  end
end
fetch_using_http() click to toggle source
# File lib/fastimage.rb, line 239
def fetch_using_http
  @redirect_count = 0

  fetch_using_http_from_parsed_uri
end
fetch_using_http_from_parsed_uri() click to toggle source
# File lib/fastimage.rb, line 245
def fetch_using_http_from_parsed_uri
  http_header = {'Accept-Encoding' => 'identity'}.merge(@options[:http_header])

  setup_http
  @http.request_get(@parsed_uri.request_uri, http_header) do |res|
    if res.is_a?(Net::HTTPRedirection) && @redirect_count < 4
      @redirect_count += 1
      begin
        newly_parsed_uri = URI.parse(res['Location'])
        # The new location may be relative - check for that
        if protocol_relative_url?(res['Location'])
          @parsed_uri = URI.parse("#{@parsed_uri.scheme}:#{res['Location']}")
        elsif newly_parsed_uri.scheme != "http" && newly_parsed_uri.scheme != "https"
          @parsed_uri.path = res['Location']
        else
          @parsed_uri = newly_parsed_uri
        end
      rescue URI::InvalidURIError
      else
        fetch_using_http_from_parsed_uri
        break
      end
    end

    raise ImageFetchFailure unless res.is_a?(Net::HTTPSuccess)

    @content_length = res.content_length

    read_fiber = Fiber.new do
      res.read_body do |str|
        Fiber.yield str
      end
    end

    case res['content-encoding']
    when 'deflate', 'gzip', 'x-gzip'
      begin
        gzip = Zlib::GzipReader.new(FiberStream.new(read_fiber))
      rescue FiberError, Zlib::GzipFile::Error
        raise CannotParseImage
      end

      read_fiber = Fiber.new do
        while data = gzip.readline
          Fiber.yield data
        end
      end
    end

    parse_packets FiberStream.new(read_fiber)

    break  # needed to actively quit out of the fetch
  end
end
fetch_using_read(readable) click to toggle source
# File lib/fastimage.rb, line 331
def fetch_using_read(readable)
  # Pathnames respond to read, but always return the first
  # chunk of the file unlike an IO (even though the
  # docuementation for it refers to IO). Need to supply
  # an offset in this case.
  if readable.is_a?(Pathname)
    read_fiber = Fiber.new do
      offset = 0
      while str = readable.read(LocalFileChunkSize, offset)
        Fiber.yield str
        offset += LocalFileChunkSize
      end
    end
  else
    read_fiber = Fiber.new do
      while str = readable.read(LocalFileChunkSize)
        Fiber.yield str
      end
    end
  end

  parse_packets FiberStream.new(read_fiber)
end
parse_packets(stream) click to toggle source
# File lib/fastimage.rb, line 362
def parse_packets(stream)
  @stream = stream

  begin
    result = send("parse_#{@property}")
    if result
      # extract exif orientation if it was found
      if @property == :size && result.size == 3
        @orientation = result.pop
      else
        @orientation = 1
      end

      instance_variable_set("@#{@property}", result)
    else
      raise CannotParseImage
    end
  rescue FiberError
    raise CannotParseImage
  end
end
parse_size() click to toggle source
# File lib/fastimage.rb, line 384
def parse_size
  @type = parse_type unless @type
  send("parse_size_for_#{@type}")
end
parse_size_for_bmp() click to toggle source
# File lib/fastimage.rb, line 566
def parse_size_for_bmp
  d = @stream.read(32)[14..28]
  header = d.unpack("C")[0]

  result = if header == 40
             d[4..-1].unpack('l<l<')
           else
             d[4..8].unpack('SS')
           end

  # ImageHeight is expressed in pixels. The absolute value is necessary because ImageHeight can be negative
  [result.first, result.last.abs]
end
parse_size_for_cur()
Alias for: parse_size_for_ico
parse_size_for_gif() click to toggle source
# File lib/fastimage.rb, line 515
def parse_size_for_gif
  @stream.read(11)[6..10].unpack('SS')
end
parse_size_for_ico() click to toggle source
# File lib/fastimage.rb, line 508
def parse_size_for_ico
  icons = @stream.read(6)[4..5].unpack('v').first
  sizes = icons.times.map { @stream.read(16).unpack('C2').map { |x| x == 0 ? 256 : x } }.sort_by { |w,h| w * h }
  sizes.last
end
Also aliased as: parse_size_for_cur
parse_size_for_jpeg() click to toggle source
# File lib/fastimage.rb, line 523
def parse_size_for_jpeg
  exif = nil
  loop do
    @state = case @state
    when nil
      @stream.skip(2)
      :started
    when :started
      @stream.read_byte == 0xFF ? :sof : :started
    when :sof
      case @stream.read_byte
      when 0xe1 # APP1
        skip_chars = @stream.read_int - 2
        data = @stream.read(skip_chars)
        io = StringIO.new(data)
        if io.read(4) == "Exif"
          io.read(2)
          exif = Exif.new(IOStream.new(io)) rescue nil
        end
        :started
      when 0xe0..0xef
        :skipframe
      when 0xC0..0xC3, 0xC5..0xC7, 0xC9..0xCB, 0xCD..0xCF
        :readsize
      when 0xFF
        :sof
      else
        :skipframe
      end
    when :skipframe
      skip_chars = @stream.read_int - 2
      @stream.skip(skip_chars)
      :started
    when :readsize
      @stream.skip(3)
      height = @stream.read_int
      width = @stream.read_int
      width, height = height, width if exif && exif.rotated?
      return [width, height, exif ? exif.orientation : 1]
    end
  end
end
parse_size_for_png() click to toggle source
# File lib/fastimage.rb, line 519
def parse_size_for_png
  @stream.read(25)[16..24].unpack('NN')
end
parse_size_for_psd() click to toggle source
# File lib/fastimage.rb, line 697
def parse_size_for_psd
  @stream.read(26).unpack("x14NN").reverse
end
parse_size_for_svg() click to toggle source
# File lib/fastimage.rb, line 771
def parse_size_for_svg
  svg = Svg.new(@stream)
  svg.width_and_height
end
parse_size_for_tiff() click to toggle source
# File lib/fastimage.rb, line 688
def parse_size_for_tiff
  exif = Exif.new(@stream)
  if exif.rotated?
    [exif.height, exif.width, exif.orientation]
  else
    [exif.width, exif.height, exif.orientation]
  end
end
parse_size_for_webp() click to toggle source
# File lib/fastimage.rb, line 580
def parse_size_for_webp
  vp8 = @stream.read(16)[12..15]
  _len = @stream.read(4).unpack("V")
  case vp8
  when "VP8 "
    parse_size_vp8
  when "VP8L"
    parse_size_vp8l
  when "VP8X"
    parse_size_vp8x
  else
    nil
  end
end
parse_size_vp8() click to toggle source
# File lib/fastimage.rb, line 595
def parse_size_vp8
  w, h = @stream.read(10).unpack("@6vv")
  [w & 0x3fff, h & 0x3fff]
end
parse_size_vp8l() click to toggle source
# File lib/fastimage.rb, line 600
def parse_size_vp8l
  @stream.skip(1) # 0x2f
  b1, b2, b3, b4 = @stream.read(4).bytes.to_a
  [1 + (((b2 & 0x3f) << 8) | b1), 1 + (((b4 & 0xF) << 10) | (b3 << 2) | ((b2 & 0xC0) >> 6))]
end
parse_size_vp8x() click to toggle source
# File lib/fastimage.rb, line 606
def parse_size_vp8x
  flags = @stream.read(4).unpack("C")[0]
  b1, b2, b3, b4, b5, b6 = @stream.read(6).unpack("CCCCCC")
  width, height = 1 + b1 + (b2 << 8) + (b3 << 16), 1 + b4 + (b5 << 8) + (b6 << 16)

  if flags & 8 > 0 # exif
    # parse exif for orientation
    # TODO: find or create test images for this
  end

  return [width, height]
end
parse_type() click to toggle source
# File lib/fastimage.rb, line 470
def parse_type
  parsed_type = case @stream.peek(2)
  when "BM"
    :bmp
  when "GI"
    :gif
  when 0xff.chr + 0xd8.chr
    :jpeg
  when 0x89.chr + "P"
    :png
  when "II", "MM"
    :tiff
  when '8B'
    :psd
  when "\0\0"
    # ico has either a 1 (for ico format) or 2 (for cursor) at offset 3
    case @stream.peek(3).bytes.to_a.last
    when 1 then :ico
    when 2 then :cur
    end
  when "RI"
    :webp if @stream.peek(12)[8..11] == "WEBP"
  when "<s"
    :svg
  when /<[?!]/
    # Peek 10 more chars each time, and if end of file is reached just raise
    # unknown. We assume the <svg tag cannot be within 10 chars of the end of
    # the file, and is within the first 250 chars.
    begin
      :svg if (1..25).detect {|n| @stream.peek(10 * n).include?("<svg")}
    rescue FiberError
      nil
    end
  end

  parsed_type or raise UnknownImageType
end
protocol_relative_url?(url) click to toggle source
# File lib/fastimage.rb, line 300
def protocol_relative_url?(url)
  url.start_with?("//")
end
proxy_uri() click to toggle source
# File lib/fastimage.rb, line 304
def proxy_uri
  begin
    if @options[:proxy]
      proxy = URI.parse(@options[:proxy])
    else
      proxy = ENV['http_proxy'] && ENV['http_proxy'] != "" ? URI.parse(ENV['http_proxy']) : nil
    end
  rescue URI::InvalidURIError
    proxy = nil
  end
  proxy
end
setup_http() click to toggle source
# File lib/fastimage.rb, line 317
def setup_http
  proxy = proxy_uri

  if proxy
    @http = Net::HTTP::Proxy(proxy.host, proxy.port).new(@parsed_uri.host, @parsed_uri.port)
  else
    @http = Net::HTTP.new(@parsed_uri.host, @parsed_uri.port)
  end
  @http.use_ssl = (@parsed_uri.scheme == "https")
  @http.verify_mode = OpenSSL::SSL::VERIFY_NONE
  @http.open_timeout = @options[:timeout]
  @http.read_timeout = @options[:timeout]
end