class ImageSize
Determine image format and size
Experimental, not yet part of stable API
It adds ability to fetch image meta from HTTP server while downloading only needed chunks if the server recognises Range header, otherwise fetches only required amount of data
Constants
- EMF_SMAX
- EMF_UMAX
- HEIF_WALKER
- JP2_WALKER
- JPEG_CODE_CHECK
- MEDIA_TYPES
- SVG_R
- XML_R
Attributes
Image format
Image height
Image height
Image width
Image width
Public Class Methods
Source
# File lib/image_size.rb, line 52 def self.chunk_size @chunk_size || 4096 end
Size of chunk to use by IO and URI readers
Source
# File lib/image_size.rb, line 57 def self.chunk_size=(chunk_size) unless chunk_size.nil? || (chunk_size.is_a?(Integer) && chunk_size > 0) fail ArgumentError, "chunk_size should be a positive Integer or nil, got #{chunk_size}" end @chunk_size = chunk_size end
Set size of chunk to use by IO and URI readers
Source
# File lib/image_size.rb, line 45 def self.dpi=(dpi) fail ArgumentError, "dpi should be nil or positive, got #{dpi}" unless dpi.nil? || dpi > 0 @dpi = dpi ? dpi.to_f : nil end
Used for svg and emf
Source
# File lib/image_size/uri_reader.rb, line 152 def self.max_redirects @max_redirects || 5 end
Maximum number of redirects
Source
# File lib/image_size/uri_reader.rb, line 157 def self.max_redirects=(max_redirects) unless max_redirects.nil? || (max_redirects.is_a?(Integer) && max_redirects >= 0) fail ArgumentError, "max_redirects should be 0, a positive Integer or nil, got #{max_redirects}" end @max_redirects = max_redirects end
Set maximum number of redirects
Source
# File lib/image_size.rb, line 66 def initialize(data) Reader.open(data) do |ir| @format = detect_format(ir) @width, @height = send("size_of_#{@format}", ir) if @format @byte_size = ir.byte_size end end
Given image as any class responding to read and eof? or data as String, finds its format and dimensions
Source
# File lib/image_size.rb, line 35 def self.path(path) new(Pathname.new(path)) end
Given path to image finds its format, width and height
Source
# File lib/image_size/uri_reader.rb, line 166 def self.uri_checker @uri_checker || proc{ |_uri| } end
Hook to call before making every request
Source
# File lib/image_size/uri_reader.rb, line 171 def self.uri_checker=(uri_checker) unless uri_checker.nil? || uri_checker.respond_to?(:call) fail ArgumentError, "uri_checker should respond to call or be nil, got #{uri_checker}" end @uri_checker = uri_checker end
Set hook to call before making every request
Source
# File lib/image_size/uri_reader.rb, line 147 def self.url(url) new(url.is_a?(URI) ? url : URI(url)) end
Public Instance Methods
Source
# File lib/image_size.rb, line 93 def media_type media_types.first end
Media type (formerly known as a MIME type)
Source
# File lib/image_size.rb, line 101 def media_types MEDIA_TYPES.fetch(format, []) end
All media types:
-
commonly used and official like for apng and ico
-
main and compatible like for heic and pnm (pbm, pgm, ppm)
-
multiple unregistered like for mng
Source
# File lib/image_size.rb, line 88 def size Size.new([width, height]) if format end
get image width and height as an array which to_s method returns “#{width}x#{height}”
Private Instance Methods
Source
# File lib/image_size.rb, line 109 def detect_format(ir) head = ir[0, 1024] case when head.nil? || head.empty? then nil when head[0, 6] =~ /\AGIF8[79]a\z/ then :gif when head[0, 8] == "\211PNG\r\n\032\n" then detect_png_type(ir) when head[0, 8] == "\212MNG\r\n\032\n" then :mng when head[0, 2] == "\377\330" then :jpeg when head[0, 2] == 'BM' then :bmp when head[0, 3] =~ /\AP([1-6]\s|7\n)\z/ then detect_pnm_type(ir) when head =~ /\#define\s+\S+\s+\d+/ then :xbm when %W[II*\0 MM\0*].include?(head[0, 4]) then :tiff when head =~ %r{/\* XPM \*/} then :xpm when head[0, 4] == '8BPS' then :psd when head[0, 3] =~ /\A[FC]WS\z/ then :swf when head =~ SVG_R || (head =~ XML_R && ir[0, 4096] =~ SVG_R) then :svg when head[0, 2] =~ /\n[\0-\5]/ then :pcx when head[0, 12] =~ /\ARIFF(?m:....)WEBP\z/ then :webp when head[0, 4] == "\0\0\1\0" then :ico when head[0, 4] == "\0\0\2\0" then :cur when head[0, 12] == "\0\0\0\fjP \r\n\207\n" then detect_jpeg2000_type(ir) when head[0, 4] == "\377O\377Q" then :j2c when head[0, 4] == "\1\0\0\0" && head[40, 4] == ' EMF' then :emf when head[4, 8] =~ /\Aftypavi[fs]\z/ then :avif when head[4, 8] =~ /\Aftyp(hei[cs]|mif[12]|msf1)\z/ then :heic end end
Source
# File lib/image_size.rb, line 159 def detect_jpeg2000_type(ir) return unless ir[16, 4] == 'ftyp' # using xl-box would be weird, but doesn't seem to contradict specification skip = ir[12, 4] == "\0\0\0\1" ? 16 : 8 case ir[skip + 12, 4] when 'jp2 ' then :jp2 when 'jpx ' then :jpx end end
Source
# File lib/image_size.rb, line 137 def detect_png_type(ir) offset = 8 loop do type = ir[offset + 4, 4] break if ['IDAT', 'IEND', nil].include?(type) return :apng if type == 'acTL' length = ir.unpack1(offset, 4, 'N') offset += length + 8 + 4 end :png end
Source
# File lib/image_size.rb, line 150 def detect_pnm_type(ir) case ir[0, 2] when 'P1', 'P4' then :pbm when 'P2', 'P5' then :pgm when 'P3', 'P6' then :ppm when 'P7' then :pam end end
Source
# File lib/image_size.rb, line 210 def size_of_bmp(ir) header_size = ir.unpack1(14, 4, 'V') if header_size == 12 ir.unpack(18, 4, 'vv') else ir.unpack(18, 8, 'VV').map do |n| if n > 0x7fff_ffff 0x1_0000_0000 - n # absolute value of converted to signed else n end end end end
Source
# File lib/image_size.rb, line 385 def size_of_emf(ir) left, top, right, bottom = if RUBY_VERSION < '1.9' ir.unpack(24, 16, 'V*').map{ |u| u < EMF_SMAX ? u : u - EMF_UMAX } else ir.unpack(24, 16, 'L<*') end dpi = self.class.dpi [right - left + 1, bottom - top + 1].map do |n| (n.to_f * dpi / 2540).round end end
Source
# File lib/image_size.rb, line 170 def size_of_gif(ir) ir.unpack(6, 4, 'vv') end
Source
# File lib/image_size.rb, line 403 def size_of_heif(ir) pitm = nil ipma = nil ispes = {} claps = {} irots = {} HEIF_WALKER.recurse(ir) do |box, _path| case box.type when 'hdlr' fail FormatError, "hdlr box too small (#{box.data_size})" if box.data_size < 8 return nil unless ir[box.data_offset + 4, 4] == 'pict' when 'pitm' fail FormatError, 'second pitm box encountered' if pitm pitm = box.version == 0 ? ir.unpack1(box.data_offset, 2, 'n') : ir.unpack1(box.data_offset, 4, 'N') when 'ipma' stream = ir.stream(box.data_offset) property_index_16b = (box.flags & 1) == 1 ipma ||= {} stream.unpack1(4, 'N').times do item_id = box.version == 0 ? stream.unpack1(2, 'n') : stream.unpack1(4, 'N') ipma[item_id] ||= Array.new(stream.unpack1(1, 'C')) do property_index_16b ? stream.unpack1(2, 'n') & 0x7fff : stream.unpack1(1, 'C') & 0x7f end end when 'ispe' ispes[box.index] ||= ir.unpack(box.data_offset, 8, 'NN') when 'clap' width_n, width_d, height_n, height_d = ir.unpack(box.data_offset, 16, 'N4') claps[box.index] ||= [Rational(width_n, width_d).round, Rational(height_n, height_d).round] when 'irot' irots[box.index] ||= ir.unpack1(box.data_offset, 1, 'C') & 0b11 end end return unless ipma properties = ipma[pitm || ipma.keys.min] return unless properties dimensions = claps.values_at(*properties).compact.first || ispes.values_at(*properties).compact.first return unless dimensions irot = irots.values_at(*properties).compact.first if irot && irot.odd? dimensions.reverse else dimensions end end
Source
# File lib/image_size.rb, line 349 def size_of_ico(ir) ir.unpack(6, 2, 'CC').map{ |v| v.zero? ? 256 : v } end
Source
# File lib/image_size.rb, line 378 def size_of_j2c(ir) ir.unpack(8, 8, 'NN') end
Source
# File lib/image_size.rb, line 371 def size_of_jp2(ir) JP2_WALKER.recurse(ir) do |box| return ir.unpack(box.data_offset, 8, 'NN').reverse if box.type == 'ihdr' end end
Source
# File lib/image_size.rb, line 193 def size_of_jpeg(ir) section_marker = "\xFF" offset = 2 loop do offset += 1 until [nil, section_marker].include? ir[offset, 1] offset += 1 until section_marker != ir[offset + 1, 1] fail FormatError, 'EOF in JPEG' unless ir[offset, 1] code, length = ir.unpack(offset, 4, 'xCn') offset += 4 return ir.unpack(offset + 1, 4, 'nn').reverse if JPEG_CODE_CHECK.include?(code) offset += length - 2 end end
Source
# File lib/image_size.rb, line 174 def size_of_mng(ir) fail FormatError, 'MHDR not in place for MNG' unless ir[12, 4] == 'MHDR' ir.unpack(16, 8, 'NN') end
Source
# File lib/image_size.rb, line 233 def size_of_pam(ir) width = height = nil offset = 3 until width && height if ir[offset, 1] == '#' offset += 1 until ["\n", '', nil].include?(ir[offset, 1]) offset += 1 else chunk = ir[offset, 32] case chunk when /\AWIDTH (\d+)\n/ width = Regexp.last_match[1].to_i when /\AHEIGHT (\d+)\n/ height = Regexp.last_match[1].to_i when /\AENDHDR\n/ break when /\A(?:DEPTH|MAXVAL) \d+\n/, /\ATUPLTYPE \S+\n/ # ignore else fail FormatError, "Unexpected data in PAM header: #{chunk.inspect}" end offset += Regexp.last_match[0].length end end [width, height] end
Source
# File lib/image_size.rb, line 312 def size_of_pcx(ir) parts = ir.unpack(4, 8, 'v4') [parts[2] - parts[0] + 1, parts[3] - parts[1] + 1] end
Source
# File lib/image_size.rb, line 180 def size_of_png(ir) fail FormatError, 'IHDR not in place for PNG' unless ir[12, 4] == 'IHDR' ir.unpack(16, 8, 'NN') end
Source
# File lib/image_size.rb, line 225 def size_of_ppm(ir) header = ir[0, 1024] header.gsub!(/^\#[^\n\r]*/m, '') header.match(/^(?:P[1-6])\s+?(\d+)\s+?(\d+)/m)[1..2].map(&:to_i) end
Source
# File lib/image_size.rb, line 277 def size_of_psd(ir) ir.unpack(14, 8, 'NN').reverse end
Source
# File lib/image_size.rb, line 326 def size_of_svg(ir) attributes = {} svg_tag = ir[0, 1024][SVG_R, 1] || ir[0, 4096][SVG_R, 1] svg_tag.scan(/(\S+)=(?:'([^']*)'|"([^"]*)"|([^'"\s]*))/) do |name, v0, v1, v2| attributes[name] = v0 || v1 || v2 end dpi = self.class.dpi [attributes['width'], attributes['height']].map do |length| next unless length pixels = case length.downcase.strip[/(?:em|ex|px|in|cm|mm|pt|pc|%)\z/] when 'em', 'ex', '%' then nil when 'in' then length.to_f * dpi when 'cm' then length.to_f * dpi / 2.54 when 'mm' then length.to_f * dpi / 25.4 when 'pt' then length.to_f * dpi / 72 when 'pc' then length.to_f * dpi / 6 else length.to_f end pixels.round if pixels end end
Source
# File lib/image_size.rb, line 317 def size_of_swf(ir) value_bit_length = ir.unpack1(8, 1, 'B5').to_i(2) bit_length = (value_bit_length * 4) + 5 rect_bits = ir.unpack1(8, (bit_length / 8) + 1, "B#{bit_length}") values = rect_bits[5..-1].unpack("a#{value_bit_length}" * 4).map{ |bits| bits.to_i(2) } x_min, x_max, y_min, y_max = values [(x_max - x_min) / 20, (y_max - y_min) / 20] end
Source
# File lib/image_size.rb, line 281 def size_of_tiff(ir) endian2b = ir.fetch(0, 4) == "II*\000" ? 'v' : 'n' endian4b = endian2b.upcase packspec = [nil, 'C', nil, endian2b, endian4b, nil, 'c', nil, endian2b, endian4b] offset = ir.unpack1(4, 4, endian4b) num_dirent = ir.unpack1(offset, 2, endian2b) offset += 2 num_dirent = offset + (num_dirent * 12) width = height = nil until width && height ifd = ir.fetch(offset, 12) fail FormatError, 'Reached end of directory entries in TIFF' if offset > num_dirent tag, type = ifd.unpack(endian2b * 2) offset += 12 next unless packspec[type] value = ifd[8, 4].unpack(packspec[type])[0] case tag when 0x0100 width = value when 0x0101 height = value end end [width, height] end
Source
# File lib/image_size.rb, line 354 def size_of_webp(ir) case ir.fetch(12, 4) when 'VP8 ' ir.unpack(26, 4, 'vv').map{ |v| v & 0x3fff } when 'VP8L' n = ir.unpack1(21, 4, 'V') [(n & 0x3fff) + 1, ((n >> 14) & 0x3fff) + 1] when 'VP8X' w16, w8, h16, h8 = ir.unpack(24, 6, 'vCvC') [(w16 | (w8 << 16)) + 1, (h16 | (h8 << 16)) + 1] end end
Source
# File lib/image_size.rb, line 260 def size_of_xbm(ir) ir[0, 1024].match(/^\#define\s*\S*\s*(\d+)\s*\n\#define\s*\S*\s*(\d+)/mi)[1..2].map(&:to_i) end
Source
# File lib/image_size.rb, line 264 def size_of_xpm(ir) length = 1024 loop do data = ir[0, length] m = data.match(/"\s*(\d+)\s+(\d+)(?:\s+\d+\s+\d+){1,2}\s*"/m) return m[1..2].map(&:to_i) if m fail FormatError, 'XPM size not found' if data.length != length length += 1024 end end