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 194
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

  if uri.respond_to?(:read)
    fetch_using_read(uri)
  else
    begin
      @parsed_uri = self.class.parse_uri(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,
       Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ECONNRESET, Errno::ENOENT,
       Net::HTTPBadResponse,
       SocketError, EOFError, IOError, NoMethodError,
       ImageFetchFailure
  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
parse(location) click to toggle source
# File lib/fastimage.rb, line 92
def self.parse(location)
  Addressable::URI.parse(location)
rescue Addressable::URI::InvalidURIError
  raise URI::InvalidURIError
end
parse_uri(location) click to toggle source
# File lib/fastimage.rb, line 100
def self.parse_uri(location)
  (@uri_parser || URI).parse(location)
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 148
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 190
def self.type(uri, options={})
  new(uri, options.merge(:type_only=>true)).type
end
uri_parser=(parser) click to toggle source

Parser object should respond to parse and raise a URI::InvalidURIError if something goes wrong

# File lib/fastimage.rb, line 84
def self.uri_parser=(parser)
  @uri_parser = parser
end
use_addressable_uri_parser() click to toggle source

Helper that sets URI parsing to use the Addressable gem

# File lib/fastimage.rb, line 89
def self.use_addressable_uri_parser
  require 'addressable/uri'
  self.uri_parser = Class.new do
    def self.parse(location)
      Addressable::URI.parse(location)
    rescue Addressable::URI::InvalidURIError
      raise URI::InvalidURIError
    end
  end
end

Private Instance Methods

fetch_using_file_open() click to toggle source
# File lib/fastimage.rb, line 360
def fetch_using_file_open
  File.open(@uri) do |s|
    fetch_using_read(s)
  end
end
fetch_using_http() click to toggle source
# File lib/fastimage.rb, line 247
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 253
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 = self.class.parse_uri(res['Location'])
        # The new location may be relative - check for that
        if 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 336
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 366
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 388
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 546
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 496
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 489
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 504
def parse_size_for_jpeg
  loop do
    @state = case @state
    when nil
      @stream.read(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.read(skip_chars)
      :started
    when :readsize
      _s = @stream.read(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 500
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 672
def parse_size_for_psd
  @stream.read(26).unpack("x14NN").reverse
end
parse_size_for_tiff() click to toggle source
# File lib/fastimage.rb, line 663
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 560
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 575
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 580
def parse_size_vp8l
  @stream.read(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 586
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 450
def parse_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"
    if @stream.peek(12)[8..11] == "WEBP"
      :webp
    else
      raise UnknownImageType
    end
  when "<s"
    :svg
  when "<?"
    (10..200).step(10).each do |length|
      characters = @stream.peek(length) rescue nil
      raise UnknownImageType if characters.nil?
      return :svg if characters.include?("<svg")
    end
  else
    raise UnknownImageType
  end
end
proxy_uri() click to toggle source
# File lib/fastimage.rb, line 306
def proxy_uri
  begin
    if @options[:proxy]
      proxy = self.class.parse_uri(@options[:proxy])
    else
      proxy = ENV['http_proxy'] && ENV['http_proxy'] != "" ? self.class.parse_uri(ENV['http_proxy']) : nil
    end
  rescue URI::InvalidURIError
    proxy = nil
  end
  proxy
end
setup_http() click to toggle source
# File lib/fastimage.rb, line 319
def setup_http
  proxy = proxy_uri
  use_ssl = (@parsed_uri.scheme == "https")
  port = @parsed_uri.port || (use_ssl ? 443 : 80)

  if proxy
    @http = Net::HTTP::Proxy(proxy.host, proxy.port).new(@parsed_uri.host, port)
  else
    @http = Net::HTTP.new(@parsed_uri.host, port)
  end

  @http.use_ssl = use_ssl
  @http.verify_mode = OpenSSL::SSL::VERIFY_NONE
  @http.open_timeout = @options[:timeout]
  @http.read_timeout = @options[:timeout]
end