# Copyright (c) 2004 Keisuke Minami # License: Ruby's # http://raa.ruby-lang.org/project/image_size/ class ImageSize # Image Type Constants module Type OTHER = "OTHER" GIF = "GIF" PNG = "PNG" JPEG = "JPEG" BMP = "BMP" PPM = "PPM" # PPM is like PBM, PGM, & XV PBM = "PBM" PGM = "PGM" # XV = "XV" XBM = "XBM" TIFF = "TIFF" XPM = "XPM" PSD = "PSD" PCX = "PCX" SWF = "SWF" end JpegCodeCheck = [ "\xc0", "\xc1", "\xc2", "\xc3", "\xc5", "\xc6", "\xc7", "\xc9", "\xca", "\xcb", "\xcd", "\xce", "\xcf", ] # image type list def ImageSize.type_list Type.constants end # receive image & make size # argument is image String or IO def initialize(img_data, img_type = nil) @img_data = img_data.dup @img_wedth = nil @img_height = nil if @img_data.is_a?(IO) @img_top = @img_data.read(512) @img_data.seek(0, 0) # define Singleton-method definition to IO (byte, offset) def @img_data.read_o(length = 1, offset = nil) self.seek(offset, 0) if offset ret = self.read(length) raise "cannot read!!" unless ret ret end elsif @img_data.is_a?(String) @img_top = @img_data[0, 512] # define Singleton-method definition to String (byte, offset) def @img_data.read_o(length = 1, offset = nil) @img_offset = 0 if !(defined?(@img_offset)) @img_offset = offset if offset ret = self[@img_offset, length] @img_offset += length ret end else raise "argument class error!! #{img_data.type}" end if img_type.nil? @img_type = check_type() else match = false Type.constants.each do |t| match = true if img_type == t end raise("img_type is failed. #{img_type}\n") if match == false @img_type = img_type end eval("@img_width, @img_height = measure_" + @img_type + "()") if @img_type != Type::OTHER end # get parameter def get_type; @img_type; end def get_height if @img_type == Type::OTHER then nil else @img_height end end def get_width if @img_type == Type::OTHER then nil else @img_width end end def get_size if get_height && get_width {:width => get_width, :height => get_height} else nil end end def mime_type case @img_type when 'SWF' ret = 'application/x-shockwave-flash' when 'PBM' ref = 'image/x-portable-bitmap' when 'PGM' ret = 'image/x-portable-graymap' when 'PPM' ret = 'image/x-portable-pixmap' when 'XBM' ret = 'image/x-xbitmap' when 'XPM' ret = 'image/x-xpixmap' when 'GIF', 'PNG', 'JPEG', 'BMP', 'TIFF' ret = "image/#{@img_type.downcase}" else ret = 'application/octet-stream' end ret end def check_type() if /<\s*[a-z]+/i =~ @img_top then Type::OTHER # html? elsif @img_top =~ /^GIF8[7,9]a/ then Type::GIF elsif @img_top[0, 8] == "\x89PNG\x0d\x0a\x1a\x0a" then Type::PNG elsif @img_top[0, 2] == "\xFF\xD8" then Type::JPEG elsif @img_top[0, 2] == 'BM' then Type::BMP elsif @img_top =~ /^P[1-7]/ then Type::PPM elsif @img_top =~ /\#define\s+\S+\s+\d+/ then Type::XBM elsif @img_top[0, 4] == "MM\x00\x2a" then Type::TIFF elsif @img_top[0, 4] == "II\x2a\x00" then Type::TIFF elsif @img_top =~ /\/\* XPM \*\// then Type::XPM elsif @img_top[0, 4] == "8BPS" then Type::PSD elsif @img_top[0, 3] == "FWS" then Type::SWF elsif @img_top[0] == 10 then Type::PCX else Type::OTHER end end private(:check_type) def measure_GIF() @img_data.read_o(6) @img_data.read_o(4).unpack('vv') end private(:measure_GIF) def measure_PNG() @img_data.read_o(12) raise "This file is not PNG." unless @img_data.read_o(4) == "IHDR" @img_data.read_o(8).unpack('NN') end private(:measure_PNG) def measure_JPEG() c_marker = "\xFF" # Section marker. @img_data.read_o(2) while(true) marker, code, length = @img_data.read_o(4).unpack('aan') raise "JPEG marker not found!" if marker != c_marker if JpegCodeCheck.include?(code) height, width = @img_data.read_o(5).unpack('xnn') return([width, height]) end @img_data.read_o(length - 2) end end private(:measure_JPEG) def measure_BMP() @img_data.read_o(26).unpack("x18VV"); end private(:measure_BMP) def measure_PPM() header = @img_data.read_o(1024) header.gsub!(/^\#[^\n\r]*/m, "") header =~ /^(P[1-6])\s+?(\d+)\s+?(\d+)/m width = $2.to_i; height = $3.to_i case $1 when "P1", "P4" then @img_type = "PBM" when "P2", "P5" then @img_type = "PGM" when "P3", "P6" then @img_type = "PPM" # when "P7" # @img_type = "XV" # header =~ /IMGINFO:(\d+)x(\d+)/m # width = $1.to_i; height = $2.to_i end [width, height] end private(:measure_PPM) alias :measure_PGM :measure_PPM private(:measure_PGM) alias :measure_PBM :measure_PPM private(:measure_PBM) def measure_XBM() @img_data.read_o(1024) =~ /^\#define\s*\S*\s*(\d+)\s*\n\#define\s*\S*\s*(\d+)/mi [$1.to_i, $2.to_i] end private(:measure_XBM) def measure_XPM() width = height = nil while(line = @img_data.read_o(1024)) if line =~ /"\s*(\d+)\s+(\d+)(\s+\d+\s+\d+){1,2}\s*"/m width = $1.to_i; height = $2.to_i break end end [width, height] end private(:measure_XPM) def measure_PSD() @img_data.read_o(26).unpack("x14NN") end private(:measure_PSD) def measure_TIFF() endian = if (@img_data.read_o(4) =~ /II\x2a\x00/o) then 'v' else 'n' end # 'v' little-endian 'n' default to big-endian packspec = [ nil, # nothing (shouldn't happen) 'C', # BYTE (8-bit unsigned integer) nil, # ASCII endian, # SHORT (16-bit unsigned integer) endian.upcase, # LONG (32-bit unsigned integer) nil, # RATIONAL 'c', # SBYTE (8-bit signed integer) nil, # UNDEFINED endian, # SSHORT (16-bit unsigned integer) endian.upcase, # SLONG (32-bit unsigned integer) ] offset = @img_data.read_o(4).unpack(endian.upcase)[0] # Get offset to IFD ifd = @img_data.read_o(2, offset) num_dirent = ifd.unpack(endian)[0] # Make it useful offset += 2 num_dirent = offset + (num_dirent * 12); # Calc. maximum offset of IFD ifd = width = height = nil while(width.nil? || height.nil?) ifd = @img_data.read_o(12, offset) # Get first directory entry break if (ifd.nil? || (offset > num_dirent)) offset += 12 tag = ifd.unpack(endian)[0] # ...and decode its tag type = ifd[2, 2].unpack(endian)[0] # ...and the data type # Check the type for sanity. next if (type > packspec.size + 0) || (packspec[type].nil?) if tag == 0x0100 # Decode the value width = ifd[8, 4].unpack(packspec[type])[0] elsif tag == 0x0101 # Decode the value height = ifd[8, 4].unpack(packspec[type])[0] end end raise "#{if width.nil? then 'width not defined.' end} #{if height.nil? then 'height not defined.' end}" if width.nil? || height.nil? [width, height] end private(:measure_TIFF) def measure_PCX() header = @img_data.read_o(128) head_part = header.unpack('C4S4') width = head_part[6] - head_part[4] + 1 height = head_part[7] - head_part[5] + 1 [width, height] end private(:measure_PCX) def measure_SWF() header = @img_data.read_o(9) raise("This file is not SWF.") unless header.unpack('a3')[0] == 'FWS' bit_length = Integer("0b#{header.unpack('@8B5')}") header << @img_data.read_o(bit_length*4/8+1) str = header.unpack("@8B#{5+bit_length*4}")[0] last = 5 x_min = Integer("0b#{str[last,bit_length]}") x_max = Integer("0b#{str[(last += bit_length),bit_length]}") y_min = Integer("0b#{str[(last += bit_length),bit_length]}") y_max = Integer("0b#{str[(last += bit_length),bit_length]}") width = (x_max - x_min)/20 height = (y_max - y_min)/20 [width, height] end private(:measure_PCX) end if __FILE__ == $0 print "TypeList: #{ImageSize.type_list.inspect}\n" ARGV.each do |file| print "#{file} (string)\n" open(file, "rb") do |fh| img = ImageSize.new(fh) print <<-EOF type: #{img.get_type.inspect} width: #{img.get_width.inspect} height: #{img.get_height.inspect} EOF end end end