class FastImage
Constants
- DefaultTimeout
- LocalFileChunkSize
Attributes
Public Class Methods
# 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
# File lib/fastimage.rb, line 92 def self.parse(location) Addressable::URI.parse(location) rescue Addressable::URI::InvalidURIError raise URI::InvalidURIError end
# File lib/fastimage.rb, line 100 def self.parse_uri(location) (@uri_parser || URI).parse(location) end
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
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
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
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
# File lib/fastimage.rb, line 360 def fetch_using_file_open File.open(@uri) do |s| fetch_using_read(s) end end
# File lib/fastimage.rb, line 247 def fetch_using_http @redirect_count = 0 fetch_using_http_from_parsed_uri end
# 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
# 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
# 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
# File lib/fastimage.rb, line 388 def parse_size @type = parse_type unless @type send("parse_size_for_#{@type}") end
# 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
# File lib/fastimage.rb, line 496 def parse_size_for_gif @stream.read(11)[6..10].unpack('SS') end
# 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
# 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
# File lib/fastimage.rb, line 500 def parse_size_for_png @stream.read(25)[16..24].unpack('NN') end
# File lib/fastimage.rb, line 672 def parse_size_for_psd @stream.read(26).unpack("x14NN").reverse end
# 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
# 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
# File lib/fastimage.rb, line 575 def parse_size_vp8 w, h = @stream.read(10).unpack("@6vv") [w & 0x3fff, h & 0x3fff] end
# 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
# 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
# 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
# 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
# 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