class FastImage

Constants

LocalFileChunkSize
VERSION

Attributes

bytes_read[R]
orientation[R]
path[R]
size[R]
source[R]
type[R]

Public Class Methods

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

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

  @type, @state = nil

  if @source.respond_to?(:read)
    @path = @source.path if @source.respond_to? :path
    fetch_using_read
  else
    @path = @source
    fetch_using_file_open
  end

  raise SizeNotFound if @options[:raise_on_failure] && @property == :size && !@size

rescue ImageFetchFailure, EOFError, Errno::ENOENT, Errno::EISDIR
  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
  source.rewind if source.respond_to?(:rewind)
end
size(source, 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.

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("example.gif")
=> [266, 56]
FastImage.size("does_not_exist")
=> nil
FastImage.size("does_not_exist", :raise_on_failure => true)
=> raises FastImage::ImageFetchFailure
FastImage.size("example.png", :raise_on_failure => true)
=> [16, 16]
FastImage.size("app.icns", :raise_on_failure=>true)
=> raises FastImage::UnknownImageType
FastImage.size("faulty.jpg", :raise_on_failure=>true)
=> raises FastImage::SizeNotFound

Supported options

: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 92
def self.size(source, options={})
  new(source, options).size
end
type(source, options={}) click to toggle source

Returns an symbol indicating the image type located at source. It will return nil if the image could not be fetched, or if the image type was not recognised.

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("example.gif")
=> :gif
FastImage.type("image.png")
=> :png
FastImage.type("photo.jpg")
=> :jpeg
FastImage.type("lena512.bmp")
=> :bmp
FastImage.type("does_not_exist")
=> nil
File.open("file.gif", "r") {|io| FastImage.type(io)}
=> :gif
FastImage.type("test/fixtures/test.tiff")
=> :tiff
FastImage.type("test/fixtures/test.psd")
=> :psd

Supported options

: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 128
def self.type(source, options={})
  new(source, options.merge(:type_only=>true)).type
end

Private Instance Methods

fetch_using_file_open() click to toggle source
# File lib/fastimage.rb, line 198
def fetch_using_file_open
  File.open(@source) do |file|
    fetch_using_read(file)
  end
end
fetch_using_read(readable = @source) click to toggle source
# File lib/fastimage.rb, line 174
def fetch_using_read(readable = @source)
  # 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 204
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 226
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 407
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 355
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 348
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 363
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)
          new_exif = Exif.new(IOStream.new(io)) rescue nil
          exif ||= new_exif # only use the first APP1 segment
        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 359
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 538
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 612
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 529
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 421
def parse_size_for_webp
  vp8 = @stream.read(16)[12..15]
  @stream.read(4).unpack("V") # len
  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 436
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 441
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 447
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 307
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"
    case @stream.peek(11)[8..10]
    when "APC", "CR\002"
      nil  # do not recognise CRW or CR2 as tiff
    else
      :tiff
    end
  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', /<[?!]/
    # 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, CannotParseImage
      nil
    end
  end

  parsed_type or raise UnknownImageType
end