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
- SVG_R
- XML_R
Attributes
format[R]
Image format
h[R]
Image height
height[R]
Image height
w[R]
Image width
width[R]
Image width
Public Class Methods
dpi()
click to toggle source
Used for svg
# File lib/image_size.rb, line 39 def self.dpi @dpi || 72 end
dpi=(dpi)
click to toggle source
Used for svg
# File lib/image_size.rb, line 44 def self.dpi=(dpi) @dpi = dpi.to_f end
new(data)
click to toggle source
Given image as any class responding to read and eof? or data as String, finds its format and dimensions
# File lib/image_size.rb, line 49 def initialize(data) Reader.open(data) do |ir| @format = detect_format(ir) @width, @height = send("size_of_#{@format}", ir) if @format end end
path(path)
click to toggle source
Given path to image finds its format, width and height
# File lib/image_size.rb, line 34 def self.path(path) new(Pathname.new(path)) end
url(url)
click to toggle source
# File lib/image_size/uri_reader.rb, line 123 def self.url(url) new(url.is_a?(URI) ? url : URI(url)) end
Public Instance Methods
size()
click to toggle source
get image width and height as an array which to_s method returns “#{width}x#{height}”
# File lib/image_size.rb, line 68 def size Size.new([width, height]) if format end
Private Instance Methods
detect_format(ir)
click to toggle source
# File lib/image_size.rb, line 76 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
detect_jpeg2000_type(ir)
click to toggle source
# File lib/image_size.rb, line 126 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
detect_png_type(ir)
click to toggle source
# File lib/image_size.rb, line 104 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
detect_pnm_type(ir)
click to toggle source
# File lib/image_size.rb, line 117 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
size_of_bmp(ir)
click to toggle source
# File lib/image_size.rb, line 183 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
size_of_emf(ir)
click to toggle source
# File lib/image_size.rb, line 359 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
size_of_gif(ir)
click to toggle source
# File lib/image_size.rb, line 137 def size_of_gif(ir) ir.unpack(6, 4, 'vv') end
size_of_heif(ir)
click to toggle source
# File lib/image_size.rb, line 377 def size_of_heif(ir) pitm = nil ipma = nil ispes = {} claps = {} irots = {} HEIF_WALKER.recurse(ir) do |box, _path| case box.type when 'hdlr' raise 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' raise 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
Also aliased as: size_of_avif, size_of_heic
size_of_ico(ir)
click to toggle source
# File lib/image_size.rb, line 323 def size_of_ico(ir) ir.unpack(6, 2, 'CC').map{ |v| v.zero? ? 256 : v } end
Also aliased as: size_of_cur
size_of_j2c(ir)
click to toggle source
# File lib/image_size.rb, line 352 def size_of_j2c(ir) ir.unpack(8, 8, 'NN') end
size_of_jp2(ir)
click to toggle source
# File lib/image_size.rb, line 345 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
Also aliased as: size_of_jpx
size_of_jpeg(ir)
click to toggle source
# File lib/image_size.rb, line 164 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] raise FormatError, 'EOF in JPEG' if ir[offset, 1].nil? code, length = ir.unpack(offset, 4, 'xCn') offset += 4 if JPEG_CODE_CHECK.include?(code) return ir.unpack(offset + 1, 4, 'nn').reverse end offset += length - 2 end end
size_of_mng(ir)
click to toggle source
# File lib/image_size.rb, line 141 def size_of_mng(ir) unless ir[12, 4] == 'MHDR' raise FormatError, 'MHDR not in place for MNG' end ir.unpack(16, 8, 'NN') end
size_of_pam(ir)
click to toggle source
# File lib/image_size.rb, line 207 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 = $1.to_i when /\AHEIGHT (\d+)\n/ height = $1.to_i when /\AENDHDR\n/ break when /\A(?:DEPTH|MAXVAL) \d+\n/, /\ATUPLTYPE \S+\n/ # ignore else raise FormatError, "Unexpected data in PAM header: #{chunk.inspect}" end offset += $&.length end end [width, height] end
size_of_pcx(ir)
click to toggle source
# File lib/image_size.rb, line 286 def size_of_pcx(ir) parts = ir.unpack(4, 8, 'v4') [parts[2] - parts[0] + 1, parts[3] - parts[1] + 1] end
size_of_png(ir)
click to toggle source
# File lib/image_size.rb, line 149 def size_of_png(ir) unless ir[12, 4] == 'IHDR' raise FormatError, 'IHDR not in place for PNG' end ir.unpack(16, 8, 'NN') end
Also aliased as: size_of_apng
size_of_ppm(ir)
click to toggle source
# File lib/image_size.rb, line 198 def size_of_ppm(ir) header = ir[0, 1024] header.gsub!(/^\#[^\n\r]*/m, '') header =~ /^(P[1-6])\s+?(\d+)\s+?(\d+)/m [$2.to_i, $3.to_i] end
Also aliased as: size_of_pbm, size_of_pgm
size_of_psd(ir)
click to toggle source
# File lib/image_size.rb, line 251 def size_of_psd(ir) ir.unpack(14, 8, 'NN').reverse end
size_of_svg(ir)
click to toggle source
# File lib/image_size.rb, line 300 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
size_of_swf(ir)
click to toggle source
# File lib/image_size.rb, line 291 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
size_of_tiff(ir)
click to toggle source
# File lib/image_size.rb, line 255 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) raise FormatError, 'Reached end of directory entries in TIFF' if offset > num_dirent tag, type = ifd.unpack(endian2b * 2) offset += 12 next if packspec[type].nil? value = ifd[8, 4].unpack(packspec[type])[0] case tag when 0x0100 width = value when 0x0101 height = value end end [width, height] end
size_of_webp(ir)
click to toggle source
# File lib/image_size.rb, line 328 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
size_of_xbm(ir)
click to toggle source
# File lib/image_size.rb, line 234 def size_of_xbm(ir) ir[0, 1024] =~ /^\#define\s*\S*\s*(\d+)\s*\n\#define\s*\S*\s*(\d+)/mi [$1.to_i, $2.to_i] end
size_of_xpm(ir)
click to toggle source
# File lib/image_size.rb, line 239 def size_of_xpm(ir) length = 1024 until (data = ir[0, length]) =~ /"\s*(\d+)\s+(\d+)(\s+\d+\s+\d+){1,2}\s*"/m if data.length != length raise FormatError, 'XPM size not found' end length += 1024 end [$1.to_i, $2.to_i] end