#!/usr/bin/ruby require 'stringio' class Bitmap class Header SIGNATURE = "BM" SIZE = 14 PACK_STRING = "a2Vv2V" def initialize @file_size = 0 @offset = 0 end attr_accessor :offset attr_accessor :file_size def read(bytes) sign, @file_size, reserved1, reserved2, @offset \ = bytes.unpack(PACK_STRING) if (sign != SIGNATURE) \ or (reserved1 != 0) \ or (reserved2 != 0) then return nil end return self end def to_s return [ SIGNATURE, @file_size, 0, 0, # 予約領域 @offset].pack(PACK_STRING) end end class InfoHeader SIZE = 40 COMPRESSION_TYPE = [ :BI_RGB, :BI_RLE8, :BI_RLE4, :BI_BITFIELDS, :BI_JPEG, :BI_PNG] PACK_STRING = "V3v2V6" def initialize @width = 0 @height = 0 @plane = 1 @bits = 24 @compression = 0 @data_size = 0 @resolution_landscape = 72 @resolution_portrait = 72 @colors = 256 @important_colors = 256 end attr_accessor :width, :height, :plane, :bits, :resolution_landscape, :resolution_portrait, :colors, :important_colors attr_reader :compression, :data_size def compression=(v) if v.kind_of?(String) then @compression = COMPRESSION_TYPE.index(v.intern) \ || COMPRESSION_TYPE.index(:BI_RGB) else @compression = v end end def bytes_per_pixel case @bits when 1 then return 0.125 when 4 then return 0.5 else return (@bits + 7) / 8 end end def size return (self.class)::SIZE end def read(bytes) len, \ @width, @height, \ @plane, @bits, \ @compression, @data_size, \ @resolution_landscape, @resolution_portrait, \ @colors, @important_colors \ = bytes.unpack(PACK_STRING) if (@data_size == 0) && (@compression == 0) then # たまにサイズが0で困る @data_size = (((bytes_per_pixel * @width).ceil + 3) / 4) * 4 * @height end if len != size then return nil end return self end def to_s case @compression when 0 # 非圧縮の場合は4vytesにアライメントしている必要あり。 @data_size = (((bytes_per_pixel * @width).ceil + 3) / 4) * 4 * @height end return [ size, @width, @height, @plane, @bits, @compression, @data_size, @resolution_landscape, @resolution_portrait, @colors, @important_colors].pack(PACK_STRING) end end class InfoHeaderV4 < InfoHeader SIZE = 108 PACK_STRING = "V5C12C12C12V3" def initialize super @red_mask = 0 @blue_mask = 0 @green_mask = 0 @alpha_mask = 0 @color_space = 0 @red_ciexyz = '0' * 12 @green_ciexyz = '0' * 12 @blue_ciexyz = '0' * 12 @red_gamma = 0 @green_gamma = 0 @blue_gamma = 0 end attr_accessor :red_mask, :blue_mask, :green_mask, :alpha_mask, :color_space, :red_ciexyz, :green_ciexyz, :blue_ciexyz, :red_gamma, :green_gamma, :blue_gamma def read(bytes) return nil unless super(bytes[0, InfoHeader::SIZE]) @red_mask, @blue_mask, @green_mask, @alpha_mask, \ @color_space, \ @red_ciexyz, @green_ciexyz, @blue_ciexyz, \ @red_gamma, @green_gamma, @blue_gamma \ = bytes[InfoHeader::SIZE, -1].unpack(PACK_STRING) return self end def to_s return super + [ @red_mask, @blue_mask, @green_mask, @alpha_mask, \ @color_space, \ @red_ciexyz, @green_ciexyz, @blue_ciexyz, \ @red_gamma, @green_gamma, @blue_gamma].pack(PACK_STRING) end end class InfoHeaderV5 < InfoHeaderV4 SIZE = 124 PACK_STRING = "V4" def initialize super @rendering = 1 @profile_start = 0 @profile_size = 0 end attr_accessor :rendering, :profile_start, :profile_size def read(bytes) return nil unless super(bytes[0, InfoHeaderV4::SIZE]) @rendering, @profile_start, @profile_size, reserved \ = bytes[InfoHeaderV4::SIZE, -1].unpack(PACK_STRING) return nil unless reserved == 0 return self end def to_s return super + [ @rendering, @profile_start, @profile_size, 0].pack(PACK_STRING) end end def initialize @header = Header::new @info_header = InfoHeader::new @pixels = [] @color_table = nil end attr_accessor :header, :info_header, :pixels, :color_table [:width, :height, :bits].each{|item| eval(<<-__EVAL_STRING__) def #{item.to_s} return @info_header.#{item.to_s} end def #{item.to_s}=(x) return @info_header.#{item.to_s} = x end __EVAL_STRING__ } def [](x, y) return @pixels[y * width + x] end def []=(x, y, v) return @pixels[y * width + x] = v end def flip_vertical! # 先にheightとwidthが指定されていること new_pixels = [] @info_header.height.times{|i| start_pixel = i * @info_header.width end_pixel = (i + 1) * @info_header.width - 1 new_pixels.unshift(*(@pixels[start_pixel..end_pixel])) } @pixels = new_pixels return self end def flip_horizontal! # 先にheightとwidthが指定されていること @info_header.height.times{|i| start_pixel = i * @info_header.width end_pixel = (i + 1) * @info_header.width - 1 @pixels[start_pixel..end_pixel] = @pixels[start_pixel..end_pixel].reverse } return self end def Bitmap::read(io) bitmap = Bitmap::new read_bytes = 0 return nil unless bitmap.header.read(io.read(Header::SIZE)) read_bytes += Header::SIZE infoheader_len_bytes = io.read(4) infoheader_len = infoheader_len_bytes.unpack("V").first case infoheader_len when InfoHeader::SIZE # nothing to do, default when InfoHeaderV4::SIZE bitmap.info_header = InfoHeaderV4::new when InfoHeaderV5::SIZE bitmap.info_header = InfoHeaderV5::new else return nil # unspported end return nil unless bitmap.info_header.read( infoheader_len_bytes + io.read(infoheader_len - 4)) read_bytes += infoheader_len # カラーテーブルの読み込み if bitmap.header.offset != read_bytes then bitmap.color_table = [] color_table = StringIO::new(io.read(bitmap.header.offset - read_bytes)) bytes_per_table = 4 unless bitmap.info_header.kind_of?(InfoHeader) then # Windows系は4bytesカラーマップ # OS/2系、3bytesカラーマップ bytes_per_table = 3 end unpack_str = "C#{bytes_per_table}" (color_table.size / bytes_per_table).times{|i| bitmap.color_table << color_table.read(bytes_per_table).unpack(unpack_str) } read_bytes = bitmap.header.offset end # 非圧縮のみとりあえずサポート return nil unless bitmap.info_header.compression \ == InfoHeader::COMPRESSION_TYPE.index(:BI_RGB) pixels = StringIO::new(io.read(bitmap.info_header.data_size)) height = bitmap.info_header.height width = bitmap.info_header.width bytes_per_pixel = bitmap.info_header.bytes_per_pixel unpack_str = "C#{bytes_per_pixel}" mod = (bytes_per_pixel * width) % 4 if bytes_per_pixel >= 1 then height.times{|i| width.times{|j| bitmap.pixels << pixels.read(bytes_per_pixel).unpack(unpack_str) } pixels.read(4 - mod) unless mod == 0 } else # TODO: 1,4bits / pixel end return bitmap end def to_s ret = @info_header.to_s @header.offset = Header::SIZE + @info_header.size @header.file_size = @header.offset + @info_header.data_size if bits <= 8 then # カラーテーブルが必要 color_tables = (1 << bits) color_table_size = color_tables * 4 unless @color_table then # 標準のカラーテーブルを作って足しておく、満足がいかない @color_table = [] color = 0 # http://ja.wikipedia.org/wiki/HSV%E8%89%B2%E7%A9%BA%E9%96%93 h = 0.0 # 0-360 s = 1.0 # 0-1 v = 1.0 # 0-1 h_step, v_step, s_step \ = *(bits == 8 ? [360 / 30, 1.0 / 4, 1.0 / 2.125] : \ (bits == 4 ? [360 / 7.5, 1.0 / 2, 1.0 / 1] : \ (bits == 2 ? [360 / 3, 1.0 / 1, 1.0 / 1] : [360, 1, 0]))) color_tables.times{|i| h_i = (h / 60).to_i % 6 f = h / 60 - h_i p = v * (1.0 - s) q = v * (1.0 - f * s) t = v * (1.0 - (1 - f) * s) case h_i when 0; r, g, b = v, t, p when 1; r, g, b = q, v, p when 2; r, g, b = p, v, t when 3; r, g, b = p, q, v when 4; r, g, b = t, p, v else; r, g, b = v, p, q end #p color.to_s(2) @color_table << [(b * 0xFF).to_i, (g * 0xFF).to_i, (r * 0xFF).to_i, 0] h += h_step if h >= 360 then h -= 360 s -= s_step end if s <= 0 then s = 1.0 v -= v_step end } end @header.file_size += color_table_size @header.offset += color_table_size ret << @color_table.flatten.pack("C*") end ret = @header.to_s + ret @info_header.height.times{|i| start_pixel = i * @info_header.width end_pixel = (i + 1) * @info_header.width - 1 bytes = @pixels[start_pixel..end_pixel].collect{|pixel| pixel.to_a}.flatten # TODO: 1,4bits / pixel ret << bytes.pack("C*") unless (mod = bytes.size % 4) == 0 then ret << ([0] * (4 - mod)).pack("C*") end } return ret end end if __FILE__ == $0 then bitmap = Bitmap::new bitmap.width = 200 bitmap.height = 200 bitmap.bits = 8 bitmap.height.times{|i| bitmap.width.times{|j| bitmap.pixels << [(i + j) % 256] } } bitmap.to_s #p bitmap open("hoge.bmp", "w"){|io| io.print bitmap } open("hoge2.bmp", "w"){|io| io.print bitmap.flip_vertical! } open("hoge3.bmp", "w"){|io| io.print bitmap.flip_horizontal! } open("hoge.bmp", "r"){|io| open("hoge4.bmp", "w"){|io2| io2.print Bitmap::read(io) } } end