class Bmp

This class contains the methods to read bmp files It supports 1,2,4,8,16,24 bits per pixel and RLE compression

Attributes

height[R]
width[R]

Public Class Methods

new(path) click to toggle source

getters

# File lib/bmp.rb, line 11
def initialize(path)
  puts "Inside bmp constructor"
  file_descriptor = IO.sysopen(path)
  @raw_data = IO.new(file_descriptor)
  @signature = @raw_data.sysread(2)
  @size = @raw_data.sysread(4).unpack('L')[0] # Because little endian format
  @raw_data.sysseek(10)
  @offset = @raw_data.sysread(4).unpack('L')[0]
  @info_header_size = @raw_data.sysread(4).unpack('L')[0]
  @width = @raw_data.sysread(4).unpack('L')[0]
  @height = @raw_data.sysread(4).unpack('L')[0]
  @planes = @raw_data.sysread(2).unpack('S')[0]
  @bits_per_pixel = @raw_data.sysread(2).unpack('S')[0]
  @numcolors = 2**@bits_per_pixel
  @compression = @raw_data.sysread(4).unpack('L')[0]
  @compressed_size = @raw_data.sysread(4).unpack('L')[0]
  @x_pixels_per_m = @raw_data.sysread(4).unpack('L')[0]
  @y_pixels_per_m = @raw_data.sysread(4).unpack('L')[0]
  @colors_used = @raw_data.sysread(4).unpack('L')[0]
  @num_of_imp_colors = @raw_data.sysread(4).unpack('L')[0]
  read_to_palette if @bits_per_pixel <= 8
  read_masks if @bits_per_pixel >= 16
  puts @raw_data.pos.to_s
end
populate_colors(array) click to toggle source
# File lib/bmp.rb, line 353
def self.populate_colors(array)
  colors = Hash.new
  height = array.length
  #raise exception if empty array
  width = array[0].length
  index = 0
  #creating a hashmap of unique colors for palette
  (1...height).each do |i|
    (1...width).each do |j|
      #debug
      #if(array[i][j].class.to_s != "Array")
      #  puts "******"
      #  puts array[i][j]
      #  puts i
      #  puts j
      #  puts "******"
      #end
      if(colors.key?(array[i][j]))
        next
      else
        colors.store(array[i][j], index)
        index += 1
      end
      if(index > 255)
        print "ERROR"
        return
        #raise exception

      end
    end
  end
  colors
end
write_to_bmp(name, array, bpp, compression_type) click to toggle source
# File lib/bmp.rb, line 387
def self.write_to_bmp(name, array, bpp, compression_type)
  height = array.length
  #raise exception if empty array
  width = array[0].length
  filestep = ((width*3 + 3) & -(4))
  zeropad = "\0\0\0\0"
  bmp_header_size = 40

  if(bpp<=8)
    palette_size = 1024
    header_size = 14 + bmp_header_size + palette_size
    ############
    colors = populate_colors(array)
    
    (1..colors.length).each do |i|
      puts colors.keys[i-1].length

    end
    ####debug###
    #IO.write("palette2.txt", colors)
    ############
    file_size = height*filestep

    fileEntity = File.new(name, "w")
    if fileEntity
      size = [file_size+header_size]
      offset = [54+1024]
      x_pixels_per_m = 2000
      y_pixels_per_m = 2000
      fileEntity.syswrite([19778].pack('S')) #signature - "BM"
      fileEntity.syswrite(size.pack('L')) #size
      fileEntity.syswrite([0].pack('L')) #random 4 bytes ###check if accounting for palette
      fileEntity.syswrite(offset.pack('L')) #offset
      fileEntity.syswrite([bmp_header_size].pack('L')) #header size
      fileEntity.syswrite([width].pack('L')) #width
      fileEntity.syswrite([height].pack('L')) #height
      fileEntity.syswrite([1].pack('S')) #planes
      fileEntity.syswrite([bpp].pack('S')) #bits per pixel
      fileEntity.syswrite([compression_type].pack('L')) #compression
      fileEntity.syswrite([0].pack('L')) #compressed size
      fileEntity.syswrite([x_pixels_per_m].pack('L')) #x_pixels_per_m
      fileEntity.syswrite([y_pixels_per_m].pack('L')) #y_pixels_per_m
      fileEntity.syswrite([0].pack('L')) #number of important colors
      fileEntity.syswrite([0].pack('L')) #number of important colors
      (1..colors.length).each do |i| #writes palette
        fileEntity.syswrite([colors.keys[i-1][0]].pack('C')) #b
        fileEntity.syswrite([colors.keys[i-1][1]].pack('C')) #g
        fileEntity.syswrite([colors.keys[i-1][2]].pack('C')) #r
        #fileEntity.syswrite([colors.keys[i-1][3]].pack('C'))
        fileEntity.syswrite([0].pack('C')) #random
      end
      (1..547).each do |i| #writes palette
        fileEntity.syswrite([0].pack('C')) #random
      end
      #starts writing the pixel info
      (1...height).each do |i|
        (1...width).each do |j|
          fileEntity.syswrite([colors[array[height-i][j-1]]].pack('C'))

        end
        (1..((width*2) % 4)).each do
          fileEntity.syswrite([0].pack('C'))
        end
      end
      fileEntity.syswrite([0].pack('C'))
      fileEntity.syswrite([1].pack('C'))
    end
    print "Successfully created bmp file"
  else

  end

end

Public Instance Methods

decompress_RLE4() click to toggle source
# File lib/bmp.rb, line 192
def decompress_RLE4
  a = Array.new(height)
  (1..@height).each do |j|
    i = 0
    row_array = []
    len = 0
    loop do

      first_byte = @raw_data.sysread(1).unpack('C')[0]
      second_byte = @raw_data.sysread(1).unpack('C')[0]
      if first_byte != 0
        pixel_two = second_byte & 15
        pixel_one = second_byte >> 4
        rolling_array = [pixel_one, pixel_two]
        k = 0
        (1..first_byte).each do
          row_array.push(@palette[rolling_array[k]])
          k = k ^ 1
          len += 1
        end
      elsif second_byte > 2
        sz = (((second_byte + 1)>>1) + 1) & (~1)
        if second_byte%2 == 1
          temp = 0
          (1..sz).each do
            len += 2
            two_pixels =  @raw_data.sysread(1).unpack('C')[0]
            pixel_two = two_pixels & 15
            pixel_one = two_pixels >> 4
            if temp<second_byte
              row_array.push(@palette[pixel_one])
              temp += 1
            end
            if temp<second_byte
              row_array.push(@palette[pixel_two])
              temp += 1
            end
          end
        else
          (1..sz).each do
            len += 2
            two_pixels =  @raw_data.sysread(1).unpack('C')[0]
            pixel_two = two_pixels & 15
            pixel_one = two_pixels >> 4
            row_array.push(@palette[pixel_one])
            row_array.push(@palette[pixel_two])
          end
        end
      elsif second_byte == 1
        break
      end
      break if len >= @width

    end
    a[@height - j] = row_array
  end
  puts @raw_data.pos.to_s
  return a
end
decompress_RLE8() click to toggle source
# File lib/bmp.rb, line 252
def decompress_RLE8
  a = Array.new(@height)
  @raw_data.sysseek(@offset)
  (1..@height).each do |j|
    i = 0
    row_array = []
    len = 0
    loop do

      first_byte = @raw_data.sysread(1).unpack('C')[0]
      second_byte = @raw_data.sysread(1).unpack('C')[0]
      if first_byte != 0
        (1..first_byte).each do
          row_array.push(@palette[second_byte])
          len += 1
        end
      elsif second_byte > 2
        sz = (second_byte+1) & (~1)

        if second_byte %2 ==1
          (1..(sz-1)).each do
            len += 1
            pixel_val =  @raw_data.sysread(1).unpack('C')[0]
            row_array.push(@palette[pixel_val])
          end
          garbage = @raw_data.sysread(1).unpack('C')[0]
        else
          (1..sz).each do
            len += 1
            pixel_val =  @raw_data.sysread(1).unpack('C')[0]
            row_array.push(@palette[pixel_val])
          end
        end
      elsif second_byte == 1
        #break
      elsif second_byte == 2
        puts "Error"
      end
      break if len >= @width

    end
    a[@height - j] = row_array
  end

  return a
end
describe() click to toggle source
# File lib/bmp.rb, line 59
def describe
  puts 'Header Data'
  puts "Signature: #{@signature}"
  puts "Size: #{@size}"
  puts "Offset: #{@offset}"
  puts 'InfoHeader Data'
  puts "Infoheader size: #{@info_header_size}"
  puts "Width: #{@width}"
  puts "Height: #{@height}"
  puts "Planes: #{@planes}"
  puts "Bits per pixel = #{@bits_per_pixel}"
  puts "Compression: #{@compression}"
  if @compression.zero?
    puts 'Compression: None'
  elsif @compression == 1
    puts 'Compression: BI_RLE8 8 bit encoding'
  elsif @compression == 2
    puts 'Compression: BI_RLE4 4 bit encoding'
  elsif @compression == 3
    puts 'Compression: BI_BITFIELDS'
  end
  puts "Compressed image size: #{@compressed_size}"
  puts "Pixel density on x-axis (per m): #{@x_pixels_per_m}"
  puts "Pixel density on y-axis (per m): #{@y_pixels_per_m}"
  puts "Used colors (in color palette): #{@colors_used}"
  puts "Number of important colors: #{@num_of_imp_colors} (0 if all are important)"
end
read_from_palette() click to toggle source
# File lib/bmp.rb, line 101
def read_from_palette
  a = Array.new(@height)
  if @bits_per_pixel == 1
    row_length = ((@width / 32).floor * 32 + 32) / 8
    (1..@height).each do |i|
      colors = []
      row_length_data = ''
      (1..row_length).each do
        row_length_data += @raw_data.sysread(1).unpack('C')[0].to_s
      end
      (1..@width).each do |j|
        colors[j - 1] = @palette[row_length_data[j].to_i]
      end
      a[@height - i] = colors
    end
  elsif @bits_per_pixel == 2
    (1..@height).each do |i|
      row_array = []
      row_length = ((@width / 32).floor * 32 + 32) / 8
      count = 0
      (1..row_length).each do |_j|
        break if count > @width

        byteval = @raw_data.sysread(1).unpack('C')[0]
        fourth = byteval & 3
        byteval >> 2
        third = byteval & 3
        byteval >> 2
        second = byteval & 3
        byteval >> 2
        first = byteval
        row_array.push(@palette[first])
        count += 1
        break if count >= @width

        row_array.push(@palette[second])
        count += 1
        break if count >= @width

        row_array.push(@palette[third])
        count += 1
        break if count >= @width

        row_array.push(@palette[fourth])
        count += 1
        break if count >= @width
      end
      a[@height - i] = row_array
    end
  elsif @bits_per_pixel == 4
    if @compression != 0
      a = decompress_RLE4
      return a
    end
    (1..@height).each do |i|
      row_array = []
      row_length = ((@width * 4 / 32).floor * 32 + 32) / 8
      count = 0
      (1..row_length).each do |_j|
        break if count >= @width

        byteval = @raw_data.sysread(1).unpack('C')[0]
        second = byteval & 15
        byteval >> 4
        first = byteval
        row_array.push(@palette[first])
        count += 1
        break if count >= @width

        row_array.push(@palette[second])
        count += 1
      end
      a[@height - i] = row_array
    end
  elsif @bits_per_pixel == 8
    if @compression != 0
      a = decompress_RLE8
      return a
    end
    (1..@height).each do |i|
      row_array = Array.new(@width)
      (1..@width).each do |j|
        row_array[j - 1] = @palette[@raw_data.sysread(1).unpack('C')[0]]
      end
      @raw_data.sysread((@width * 3) % 4) # zeros are appended to rows of pixels if the image width is not a multiple of 4, this ignores them.
      a[@height - i] = row_array
    end
  end
  a
end
read_from_pixel_array() click to toggle source
# File lib/bmp.rb, line 299
def read_from_pixel_array
  a = Array.new(@height)
  if @bits_per_pixel == 16 # works for rgb555 format, not rgb565
    (1..@height).each do |i|
      row_array = Array.new(@width)
      (1..@width).each do |j|
        bindata = @raw_data.sysread(2).unpack('S')[0]
        #bindata = bindata>>1
        b = (bindata<<3) & 0xf8

        g = (bindata>>2) & 0xf8
        #bindata = bindata >> 6
        r = (bindata>>7) & 0xf8
        row_array[j - 1] = [b, g, r]
      end
      @raw_data.sysread((@width * 3) % 4) # zeros are appended to rows of pixels if the image width is not a multiple of 4, this ignores them.
      a[@height - i] = row_array
    end

  elsif @bits_per_pixel == 24
    (1..@height).each do |i|
      row_array = Array.new(@width)
      (1..@width).each do |j|
        b = @raw_data.sysread(1).unpack('C')[0]
        g = @raw_data.sysread(1).unpack('C')[0]
        r = @raw_data.sysread(1).unpack('C')[0]
        row_array[j - 1] = [b, g, r]
      end
      @raw_data.sysread((@width * 3) % 4) # zeros are appended to rows of pixels if the image width is not a multiple of 4, this ignores them.
      a[@height - i] = row_array
    end
  elsif @bits_per_pixel == 32 # not tested with an image yet, maybe order of colors is wrong
    (1..@height).each do |i|
      row_array = Array.new(@width)
      (1..@width).each do |j|
        b = @raw_data.sysread(1).unpack('C')[0]
        g = @raw_data.sysread(1).unpack('C')[0]
        r = @raw_data.sysread(1).unpack('C')[0]
        alpha = @raw_data.sysread(1).unpack('C')[0]
        row_array[j - 1] = [ b, g, r, alpha] # should be changed based on requirement
      end
      @raw_data.sysread((@width * 3) % 4) # zeros are appended to rows of pixels if the image width is not a multiple of 4, this ignores them.
      a[@height - i] = row_array
    end
  end
  a
end
read_masks() click to toggle source
# File lib/bmp.rb, line 50
def read_masks
  if @compression != 0
    @redmask = @raw_data.sysread(4).unpack('N')[0]
    @greenmask = @raw_data.sysread(4).unpack('N')[0]
    @bluemask = @raw_data.sysread(4).unpack('N')[0]
    @alphachannel = @raw_data.sysread(4).unpack('N')[0]
  end
end
read_to_array() click to toggle source
# File lib/bmp.rb, line 87
def read_to_array
  @raw_data.sysseek(@offset)
  a = Array.new(@height)

  if @bits_per_pixel <= 8
    a = read_from_palette
  elsif @bits_per_pixel > 8
    a = read_from_pixel_array
    # puts "Location of pointer after reading: #{@raw_data.pos}"
  end

  a
end
read_to_nmatrix() click to toggle source

will do once the bug is fixed

# File lib/bmp.rb, line 348
def read_to_nmatrix
  result = [] # NMatrix.new([3, 3],dtype: :int64)
  result
end
read_to_palette() click to toggle source
# File lib/bmp.rb, line 36
def read_to_palette
  @palette = []
  (1..@colors_used).each do |i|
    b = @raw_data.sysread(1).unpack('C')[0]
    g = @raw_data.sysread(1).unpack('C')[0]
    r = @raw_data.sysread(1).unpack('C')[0]
    temp = @raw_data.sysread(1).unpack('C')
    @palette[i - 1] = [b, g, r]
    #puts "#{r},#{g},#{b}"
  end
  #IO.write("results/palette.txt", @palette.join("\n"))
end