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