bmpencode.rb 4.74 KB
Newer Older
Cool Fire's avatar
Cool Fire committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161
#!/usr/bin/env ruby
require 'optparse'

# Option parsing
inputfile  = ''
outputfile = ''
width      = 0
height     = 0
padding    = 0
decode     = false
  
ARGV.options do |opts|
  opts.separator 'When no filenames are provided STDIO is used'
  opts.separator 'When no width is provided a the square root of the input size is used.'
  opts.on('-i', '--input=filename',  'Read input from file')             { |val| inputfile  = val }
  opts.on('-o', '--output=filename', 'Write output to file')             { |val| outputfile = val }
  opts.on('-w', '--width=value',     'Make image "value" pixels wide')   { |val| width = val.to_i }
  opts.on('-p', '--padding',         'Pad pixel rows with 0-bytes to ensure all data is in pixel values') { padding = 1 }
  opts.on('-d', '--decode',          'Decode bmp back to original file') { decode = true }
  opts.parse!
end

# Start reading input
data   = ''
output = ''
if(inputfile == '')
  data = ARGF.read
else
  data = IO.binread(inputfile)
end

if(decode)
  # Decoding

  # Read headers we care about
  data    = data.bytes
  padsize = data[6..7].pack('C*').unpack('S_')[0]
  padding = data[8..9].pack('C*').unpack('S_')[0]
  width   = data[18..21].pack('C*').unpack('I')[0]

  # Calculate end of data
  dend   = data.size - padsize - 1

  # Calculate row padding
  rowpadsize = (width * 3) % 4
  if(rowpadsize > 0)
    rowpadsize = 4 - rowpadsize
  end

  # Decode
  if(padding == 1 && rowpadsize != 0)

    pc = 0
    data[54..-1].pack('C*').each_byte { |b|
      pc +=1
      if(pc > width * 3)
        if(pc == (width * 3) + rowpadsize)
          pc = 0
        end
        
        next
      else
        output.concat([b].pack('C*'))
      end

    }
    dend   = -1 - padsize
    output = output.bytes[0..dend].pack('C*')
  else
    output = data[54..dend].pack('C*')
  end
else
  # Encoding

  # Add padding if needed
  padsize = data.bytesize % 4
  if(padsize > 0)
    padsize = 4 - padsize
  end
  data = data.concat([0x00].pack('C*') * padsize)

  # Calculate image dimensions
  if(width == 0)
    width  = Math.sqrt(data.bytesize / 3).ceil()
    height = width
  else
    height = ((data.bytesize / 3) / width.to_f).ceil()
  end

  # Calculate extra bytes required for row padding
  rowpadsize = (width * 3) % 4
  if(rowpadsize > 0)
    rowpadsize = 4 - rowpadsize
  end
  totalrowpad = 0
  if(padding == 1)
    totalrowpad = rowpadsize * height
  end

  # Construct BMP header
  bfType      = [0x42, 0x4D]                  # BMP specification type
  bfSize      = [54 + data.bytesize + totalrowpad] # File size
  bfReserved1 = [padsize]                     # Reserved, always 0 [abused to store number of padding bytes added for alignment]
  bfReserved2 = [padding]                     # Reserved, always 0 [abused to store if extra row padding was used]
  bfOffBits   = [0x36, 0x00, 0x00, 0x00]      # Offset to start of image data

  # Construct image header
  biSize          = [0x28, 0x00, 0x00, 0x00] # Image header size [always 40 bytes here]
  biWidth         = [width]                  # Image width in px
  biHeight        = [height]                 # Image height in px
  biPlanes        = [0x01, 0x00]             # BMP planes [Always 1]
  biBitCount      = [0x18, 0x00]             # Bits per pixel [Always 24 here]
  biCompression   = [0x00, 0x00, 0x00, 0x00] # Compression [Always no compression here]
  biSizeImage     = [0x00, 0x00, 0x00, 0x00] # Image size [May be 0 because no compression is used]
  biXPelsPerMeter = [0x13, 0x0B, 0x00, 0x00] # Preferred resolution in pixels per meter
  biYPelsPerMeter = [0x13, 0x0B, 0x00, 0x00] # Preferred resolution in pixels per meter
  biClrUsed       = [0x00, 0x00, 0x00, 0x00] # Number of Color Map entities used [unused here]
  biClrImportant  = [0x00, 0x00, 0x00, 0x00] # Number of signifcant colors [unused here]

  # Print headers
  output.concat(bfType.pack('C*'))
  output.concat(bfSize.pack('I'))
  output.concat(bfReserved1.pack('S_'))
  output.concat(bfReserved2.pack('S_'))
  output.concat(bfOffBits.pack('C*'))

  output.concat(biSize.pack('C*'))
  output.concat(biWidth.pack('I'))
  output.concat(biHeight.pack('I'))
  output.concat(biPlanes.pack('C*'))
  output.concat(biBitCount.pack('C*'))
  output.concat(biCompression.pack('C*'))
  output.concat(biSizeImage.pack('I'))
  output.concat(biXPelsPerMeter.pack('C*'))
  output.concat(biYPelsPerMeter.pack('C*'))
  output.concat(biClrUsed.pack('C*'))
  output.concat(biClrImportant.pack('C*'))

  # Print content
  if(padding == 1)
    pc = 0
    data.each_byte { |b|
      pc += 1
      output.concat([b].pack('C*'))

      if(pc == width * 3)
        output.concat([0x00].pack('C*') * rowpadsize)
        pc = 0
      end
    }
  else
    output.concat(data)
  end
end

# Write out results
if(outputfile == '')
  print output
else
  IO.binwrite(outputfile, output)
end