Optimal RGB16 BMP editing in Python

ImageManager class, to manage and abstract Image type.

It allows automatic management of the next pixel to be updated with a defined set of constraints :

  • Value of y : TOP/BOTTOM
  • Value of x : LEFT/RIGHT
  • Direction : HORIZONTAL/VERTICAL
from struct import pack,unpack
class ImageManager:
    # Fill x, or y first
    HORIZONTAL = 0
    VERTICAL = 1
    # Start y = 0, or y = height-1
    TOP = 0
    BOTTOM = 1
    # Start x = 0, or x = width-1
    LEFT = 0
    RIGHT = 1
    def __init__(self, _name, _width, _height, _background_color = int('0x0000', 16), _x_mode = LEFT, _y_mode = TOP, _direction_mode = HORIZONTAL):
        self.name = _name
        self.width = _width
        self.height = _height
        self.background_color = _background_color
        self.x_mode = _x_mode
        self.y_mode = _y_mode
        self.direction_mode = _direction_mode
        self.is_finished = False
        self.x = 0 if self.x_mode == ImageManager.TOP else (self.width - 1)
        self.y = 0 if self.y_mode == ImageManager.LEFT else (self.height - 1)
        
        self.reset()
    # Initialize a new Image with specified background
    def reset(self):
        self.image = Bitmap(self.width, self.height, self.background_color)
    # Update corresponding pixel in memory
    def update(self, _x, _y, _pixel):
        self.image.set_pixel(_x, _y, _pixel)
    # Update only corresponding byte in already existing Image file
    def raw_update(self, _x, _y, _pixel):
        self.image.write_pixel(_x, _y, _pixel, self.name +".bmp")
    # Manage next pixel using specified configuration
    def add(self, _pixel):
        if ( self.is_finished is True ):
            raise Exception("Image size limit exceeded.")
        else:            
            self.update(self.x, self.y, _pixel)
            if ( self.direction_mode == ImageManager.HORIZONTAL ):
                isNextRow = False
                if ( self.x_mode == ImageManager.LEFT):
                    if ( self.x == (self.width-1) ):
                        self.x = 0
                        isNextRow = True
                    else:
                        self.x = self.x + 1
                else:
                    if ( self.x == 0 ):
                        self.x = (self.width-1)
                        isNextRow = True
                    else:
                        self.x = self.x - 1
                if ( isNextRow == True ):
                    if ( self.y_mode == ImageManager.TOP):
                        self.y = self.y + 1
                    else:
                        self.y = self.y - 1
            else:
                isNextColumn = False
                if ( self.y_mode == ImageManager.TOP):
                    if ( self.y == (self.height-1) ):
                        self.y = 0
                        isNextColumn = True
                    else:
                        self.y = self.y + 1
                else:
                    if ( self.y == 0 ):
                        self.y = self.height - 1
                        isNextColumn = True
                    else:
                        self.y = self.y - 1
                if ( isNextColumn == True ):
                    if ( self.x_mode == ImageManager.LEFT):
                        self.x = self.x + 1
                    else:
                        self.x = self.x - 1
            if (
                (self.x_mode == ImageManager.TOP and self.y_mode == ImageManager.LEFT and self.x == (self.width-1) and self.y == (self.height-1)) or
                (self.x_mode == ImageManager.TOP and self.y_mode == ImageManager.RIGHT and self.x == (self.width-1) and self.y == 0) or
                (self.x_mode == ImageManager.BOTTOM and self.y_mode == ImageManager.RIGHT and self.x == 0 and self.y == 0) or
                (self.x_mode == ImageManager.BOTTOM and self.y_mode == ImageManager.LEFT and self.x == 0 and self.y == (self.height-1))      
            ):
                self.is_finished = True
            
    def render(self):
        self.image.write( self.name +".bmp" )

Bitmap class, to render a RGB16 bitmap or update an existing bitmap with a raw byte change.

class Bitmap:
    def __init__(self, width, height, background_color):
        self.TYPE = 19778 # Bitmap signature : BM
        self.RESERVED_1 = 0 
        self.RESERVED_2 = 0
        self.DATA_OFFSET = 54 # Data starting offset
        self.SIZE = self.DATA_OFFSET + width * 2 * height
        # BITMAPINFOHEADER
        self.HEADER_SIZE = 40 # BITMAPINFOHEADER size
        self.WIDTH = width
        self.HEIGHT = height
        self.PLANES = 1 # Must be 1
        self.BPP = 16 # Number of bits per pixel
        self.COMPRESSION = 0 # Compression method, 0 = BI_RGB (no compression)
        self.IMAGE_SIZE = 0 # BI_RGB, dummy zero is allowed
        self.H_RESOLUTION = 0 # Horizontal pixel per meter
        self.V_RESOLUTION = 0 # Vertical pixel per meter
        self.COLORS_IN_PALETTE = 0 # Number of colors in color palette, 0 default to 2^n
        self.IMPORTANT_COLOR = 0 # Number of important color, 0 is every color is important
        # Out of spec, utility
        self.background_color = background_color
        self.clear()
    def clear(self):
        self.data = [pack('H', self.background_color)] * self.WIDTH * self.HEIGHT
    def set_pixel(self, x, y, color):
        if ( x < 0 or y < 0 or x > (self.WIDTH-1) or y > (self.HEIGHT-1) ):
            raise ValueError("["+ x +";"+ y +"] is out of range.")
        self.data[y * self.WIDTH + x] = pack('<H', color)
    def write_pixel(self, x, y, color, file):
        if ( x < 0 or y < 0 or x > (self.WIDTH-1) or y > (self.HEIGHT-1) ):
            raise ValueError("["+ str(x) +";"+ str(y) +"] is out of range.")
        with open(file, 'rb+') as f:
            f.seek( self.DATA_OFFSET, 0)
            f.seek( (self.HEIGHT-1-y)*self.HEIGHT*(self.BPP/8) + x * (self.BPP/8), 1)
            f.write( pack('<H', color) )   
    def write(self, file):
        with open(file, 'wb') as f:
            # Writing BITMAPFILEHEADER
            f.write(pack('<HLHHL', 
                    self.TYPE, 
                    self.SIZE, 
                    self.RESERVED_1, 
                    self.RESERVED_2, 
                    self.DATA_OFFSET)) 
            # Writing BITMAPINFO
            f.write(pack('<LLLHHLLLLLL', 
                    self.HEADER_SIZE, 
                    self.WIDTH, 
                    self.HEIGHT, 
                    self.PLANES, 
                    self.BPP,
                    self.COMPRESSION, # No compression
                    self.IMAGE_SIZE, # Raw size, as it is a BI_RGB a dummy 0 can be set
                    self.H_RESOLUTION, # Horizontal pixel per meter
                    self.V_RESOLUTION, # Vertical pixel per meter
                    self.COLORS_IN_PALETTE, # Number of color, default will be 2^n
                    self.IMPORTANT_COLOR  # Important color
                    ))
            # Writing data
            for pixel in self.data:
                tarte = unpack('<H', pixel)
                f.write(pixel)
            # Padding data
            for i in range( (4 - ( (self.WIDTH*3) % 4) ) % 4):
                f.write( pack('B', 0) )

Small example :

# R G B
red = int('0x7C00', 16)
white = int('0x7FFF', 16)
# In BMP, TOP and BOTTOM are reverse !
myImage = ImageManager("test", 200,200, int('0x03E0', 16), ImageManager.LEFT, ImageManager.TOP, ImageManager.HORIZONTAL)
i = 0
while ( i < 500 ):
    myImage.add(red)
    i = i + 1
# Save image
myImage.render()
myImage.raw_update(0, 0, white)
myImage.raw_update(0, 19, white)
myImage.raw_update(19, 0, white)
myImage.raw_update(19, 19, white)
x = 10
while ( x < 20 ):
    y = 10
    while ( y < 20 ):
        myImage.raw_update(x, y, white)
        y = y + 1 
    x = x + 1

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.