#!/usr/bin/env python # -*- coding: utf-8 -*- """Converting 24 bit per pixel BMP to 16 bit per pixel. BMP's with 16 bit per pixel seem to be a little bit exotic but used by devices like mobile phones or portable media players with 65K color displays. From GIMP source:: /* Valid bitpdepthis 1, 4, 8, 16, 24, 32 */ /* 16 is awful, we should probably shoot whoever invented it */ """ from __future__ import division import struct import sys __author__ = "Marc 'BlackJack' Rintsch, modified by Ulrich Pfeifer" __version__ = '0.1.0' __date__ = '$Date: Fri Jun 8 20:12:51 CEST 2007$' __revision__ = '$Rev: compressed-3 r4 $' class BMPError(Exception): """Error for problems with BMP file.""" pass class BMP(object): """BMP image. A `BMP` object can load and save very simple BMP images. It is very restrictive; just uncompressed 24 bits per pixel BMPs can be loaded. HEADER_FMT : `str` Format in `struct` notation of the beginning of the BMP header structure including all relevant fields. HEADER_SIZE : `int` Size of the header structure described by `HEADER_FMT` in bytes. signature : `str` of length two BMP signature. Must be ``BM``. file_size : `int` Size of the complete image file including header(s) and image data. data_offset : `int` Offset into the file where the image data starts. info_size : `int` Size of the info header structure. It's 40 bytes for most BMPs. width, height : `int` Dimensions of the image in pixels. planes : `int` No idea!? Has to be 1. bit_count : `int` Number of bits per pixel. compression : `int` Compression method. 0 means no compression. data_size : `int` Size of the (compressed) data in bytes. If no compression is used this field may be set to 0. remaining_header : `str` Data between first part of the header and the image data. bytes_per_row : `int` Number of bytes per pixel row in the image data. data : `str` The image data. """ HEADER_FMT_24 = '< 2s i 4x i iii hh ii iiii' HEADER_FMT_16 = '< 2s i 4x i iii hh ii iiii iii' # ('BM', 78914, 66, 40, 176, -224, 1, 16, 3, 78848, 0, 0, 0, 0, 63488, 2016, 31) HEADER_SIZE_24 = struct.calcsize(HEADER_FMT_24) HEADER_SIZE_16 = struct.calcsize(HEADER_FMT_16) def __init__(self, fileobj): """Create a `BMP` object from the content of `fileobj`. Raises `BMPError` if the BMP in `fileobj` is not an uncompressed 24 bits per pixel BMP. """ (self.signature, self.file_size, self.data_offset, self.info_size, self.width, self.height, self.planes, self.bit_count, self.compression, self.data_size, self.hres, self.vres, self.hcols, self.icols, ) = struct.unpack(self.HEADER_FMT_24, fileobj.read(self.HEADER_SIZE_24)) if self.signature != 'BM': raise BMPError('not a BMP') if self.planes != 1: raise BMPError('image has more than one plane') if self.bit_count != 24: raise BMPError('not a 24 bpp image') if self.compression != 0: raise BMPError('compressed image') if self.data_offset > self.HEADER_SIZE_24: self.remaining_header = fileobj.read( self.data_offset - self.HEADER_SIZE_24) print("remaining_header", self.data_offset - self.HEADER_SIZE_24) if self.data_offset < self.HEADER_SIZE_24: raise BMPError('Header extended into data') self.data = fileobj.read() self.bytes_per_row = None self._calculate_bytes_per_row() def _calculate_bytes_per_row(self): """Calculate the bytes per row. Row data in BMP images is padded to 32 bit boundaries. """ self.bytes_per_row = ((self.width * self.bit_count - 1) // 32) * 4 + 4 def iter_rows(self): """Iterate over the data of the rows.""" for i in xrange(self.height): offset = i * self.bytes_per_row yield self.data[offset:offset+self.bytes_per_row] def _iter_pixels(self, rowdata): """Iterate over RGB values in `rowdata`. Returns an iterator over tuples with red, green and blue values as `int`\s between 0 and 255. """ for i in xrange(self.width): yield map(ord, rowdata[i*3:i*3+3]) def convert_to_16_bit(self): """Convert image to 16 bits per pixel.""" assert self.bit_count in (24, 16) if self.bit_count == 16: return # # Convert image data. # new_data = list() for rowdata in self.iter_rows(): new_row = list() for red, green, blue in self._iter_pixels(rowdata): # 5-6-5 new_row.append( ((blue >> 3) << 11) + ((green >> 2) << 5) + (red >> 3)) # 5-5-5 #new_row.append( ((red >> 3) << 10) # + ((green >> 3) << 5) # + (blue >> 3)) new_data.insert(0,new_row) # # Adjust header information and pack image data. # self.bit_count = 16 self.data_size = 0 self._calculate_bytes_per_row() row_fmt = ( 'H' * self.width + 'x' * (self.bytes_per_row // 2 - self.width)) self.data = ''.join(struct.pack(row_fmt, *row) for row in new_data) self.file_size = self.data_offset + len(self.data) + 12 self.data_size = self.file_size - self.HEADER_SIZE_16; @classmethod def load(cls, filename): """Load a BMP file and return a `BMP` object.""" bmp_file = open(filename, 'rb') result = cls(bmp_file) bmp_file.close() return result def save(self, filename): """Save `BMP` object to a file.""" bmp_file = open(filename, 'wb') # 5-6-5 self.red = 0xf800 self.green = 0x07e0 self.blue = 0x001f # 5-5-5 #self.red = 0x7c00 #self.green = 0x03e0 #self.blue = 0x001f self.compression = 3 header = struct.pack(self.HEADER_FMT_16, self.signature, self.file_size, self.data_offset+12, self.info_size, self.width, self.height, self.planes, self.bit_count, self.compression, self.data_size, self.hres, self.vres, self.hcols, self.icols, self.red, self.green, self.blue, ) bmp_file.write(header) bmp_file.write(self.data) bmp_file.close() def main(): """Main function.""" if len(sys.argv) < 3: print 'usage: bmp16.py input.bmp output.bmp' sys.exit() input_name, output_name = sys.argv[1:3] try: bmp = BMP.load(input_name) bmp.convert_to_16_bit() bmp.save(output_name) except BMPError, error: print error if __name__ == '__main__': main()