solution_248a.py

#!/usr/bin/python3
# ===============================================================
# interpret/display BMP file header information
# and a few pixels
# ===============================================================

# ---------------------------------------------------------------
# ---- display raw bytes
# ---------------------------------------------------------------

def display_raw_bytes(offset:int,length:int,
                      bary:bytearray) -> None:

    line_cnt = 0                # line byte count
    line_max = 10               # max bytes per line
    idx      = offset           # byte array index
    
    while True:

        if idx > offset+length:
            if idx != 0: print()
            break
        
        print(f'{bary[idx]:#x}, ',end='')

        idx += 1

        if line_cnt > line_max:
            print()
            line_cnt = 0
            continue

        line_cnt += 1

# ---------------------------------------------------------------
# ---- display 3 byte pixel
# ---------------------------------------------------------------

def display_3_byte_pixel(r:int,g:int,b:int,idx=None)-> None:
    if idx is None:
        print(f'r=x{r:02x}, g=x{g:02x}, b=x{b:02x}')
    else:
        print(f'byte[{idx:03}] r=x{r:02x}, g=x{g:02x}, b=x{b:02x}')

# ---------------------------------------------------------------
# ---- calculate row padding
# ----
# ---- Note: The pixel format is defined by the DIB header or
# ----       extra bit masks. Each row in the Pixel array is
# ----       padded to a multiple of 4 bytes in size.
# ---------------------------------------------------------------

def calculate_row_padding(width):

    match (width * 3) % 4:
        case 3:
            return 1
        case 2:
            return 2
        case 1:
            return 3

    return 0

# ---------------------------------------------------------------
# ---- display 3 byte (24 bit) pixels
# ----
# ---- Note: The pixel format is defined by the DIB header or
# ----       extra bit masks. Each row in the Pixel array is
# ----       padded to a multiple of 4 bytes in size.
# ----
# ---- input:
# ---- offset  offset to the first pixel
# ---- count   number if pixels todisplay
# ---- width   image width (pixels)
# ---- bary    BMP byte array
# ----
# ---------------------------------------------------------------

def display_24bit_pixels(offset:int, count:int, width:int,
                         bary:bytearray)-> None:

    byt_idx       = offset    # byte array index
    row_pix_cnt   = 0         # pixels displayed per row count
    total_pix_cnt = 0         # total pixels displayed

    # byte array index padding for the end of each row
    padding = calculate_row_padding(width)

    i = 0
    count = 54
    
    while total_pix_cnt < count:

        # ---- display RGB pixel values

        b = int.from_bytes(bary[byt_idx  :byt_idx+1],'little')
        g = int.from_bytes(bary[byt_idx+1:byt_idx+2],'little')
        r = int.from_bytes(bary[byt_idx+2:byt_idx+3],'little')

        i += 1
        print(f'[{i:02}] ',end='')
        display_3_byte_pixel(r,g,b,byt_idx)

        total_pix_cnt += 1

        row_pix_cnt += 1

        byt_idx += 3

        # --- end of row?
        
        if row_pix_cnt >= width:

            i = 0

            row_pix_cnt = 0
            
            byt_idx += padding
 
            print(f'add padding ({padding}) ' +
                  f'idx={byt_idx} cnt={row_pix_cnt}')

    print()
    print(f'total_pix_count={total_pix_cnt} count={count}')

# ---------------------------------------------------------------
# ---- DIB header name
# ---------------------------------------------------------------

def dib_header_name(dib_header_size:int) -> str:

    match dib_header_size:
        case 12:
            return 'BITMAPCOREHEADER'
        case 64:
            return 'OS22XBITMAPHEADER'
        case 16:
            return 'OS22XBITMAPHEADER'
        case 40:
            return 'BITMAPINFOHEADER'
        case 52:
            return 'BITMAPV2INFOHEADER'
        case 56:
            return 'BITMAPV3INFOHEADER'
        case 108:
            return 'BITMAPV4HEADER'
        case 124:
            return 'BITMAPV5HEADER'

    return 'unknown'

# ---------------------------------------------------------------
# ---- DIB bits per pixel (palette entry information)
# ---------------------------------------------------------------

def dib_bits_per_pixel(bits_per_pixel:int) -> str:

    match bits_per_pixel:
        case 1:
            return 'monochrome palette. NumColors = 1'
        case 4:
            return '4bit palletized. NumColors = 16' 
        case 8:
            return '8bit palletized. NumColors = 256' 
        case 16:
            return '16bit RGB. NumColors = 65536'
        case 24:
            return '24bit RGB. NumColors = 16M'

    return 'unknown'

# ---------------------------------------------------------------
# ---- DIB compression method
# ---------------------------------------------------------------

def dib_compression_method(compression_method:int) -> str:

    match compression_method:
        case 0:
            return 'BI_RGB - no compression'
        case 1:
            return 'BI_RLE8 8bit RLE encoding'
        case 2:
            return 'BI_RLE4 4bit RLE encoding'
        
    return 'unknown'            

# ---------------------------------------------------------------
# ---- DIB important colors
# ---------------------------------------------------------------

def dib_important_colors(important_colors:int) -> str:

    if important_colors == 0: return 'all'
    return f'{important_colors} important colors'
    
# ---------------------------------------------------------------
# ---- display BMP file header
# ----
# ----  the common DIB format is the BITMAPINFOHEADER header
# ---------------------------------------------------------------

def display_bmp_header(file_path,bmp_bytes:str) -> None:

    print('---- BMP Header -------------------------------')

    char1     = chr(int.from_bytes(bmp_bytes[0:1],'little'))
    char2     = chr(int.from_bytes(bmp_bytes[1:2],'little'))
    width     = int.from_bytes(bmp_bytes[18:22],'little')
    height    = int.from_bytes(bmp_bytes[22:26],'little')
    file_size = int.from_bytes(bmp_bytes[2:6],'little')
    offset    = int.from_bytes(bmp_bytes[10:14],'little')
    
    print(f'file               = {file_path}')
    print(f'file type          = {char1}{char2}')
    print(f'width              = {width}')
    print(f'height             = {height}')
    print(f'file size          = {file_size}')
    print(f'pixels offset      = {offset}')

    print('---- DIB Header -------------------------------')

    dib_header_size       = int.from_bytes(bmp_bytes[14:18],
                                          'little')
    dib_width             = int.from_bytes(bmp_bytes[18:22],
                                          'little')
    dib_height            = int.from_bytes(bmp_bytes[22:26],
                                          'little')
    color_planes          = int.from_bytes(bmp_bytes[26:28],
                                          'little')
    bits_per_pixel        = int.from_bytes(bmp_bytes[28:30],
                                          'little')
    compression_method    = int.from_bytes(bmp_bytes[30:34],
                                          'little')
    raw_image_size        = int.from_bytes(bmp_bytes[34:38],
                                          'little')
    horizontal_resolution = int.from_bytes(bmp_bytes[38:42],
                                          'little',signed=True)
    vertical_resolution   = int.from_bytes(bmp_bytes[42:46],
                                          'little',signed=True)
    number_of_colors      = int.from_bytes(bmp_bytes[46:50],
                                          'little')
    important_colors      = int.from_bytes(bmp_bytes[50:54],
                                          'little')

    compression = dib_compression_method(compression_method)
    header_name = dib_header_name(dib_header_size)
    pixel_bits  = dib_bits_per_pixel(bits_per_pixel)
    num_important_colors = dib_important_colors(important_colors)
    
    print(f'DIB header size    = {dib_header_size}')
    print(f'DIB header name    = {header_name}')
    print(f'DIB width          = {dib_width}')
    print(f'DIB height         = {dib_height}')
    print(f'color planes       = {color_planes}')
    print(f'bits per pixel     = {bits_per_pixel}')
    print(f'                     {pixel_bits}')
    print(f'compression method = {compression_method}')
    print(f'                     {compression}')
    print(f'raw image size     = {raw_image_size}')
    print(f'horizontal res     = {horizontal_resolution}')
    print(f'vertical res       = {vertical_resolution}')
    print(f'number of colors   = {number_of_colors}')
    print(f'important colors   = {important_colors}')
    print(f'                     {num_important_colors}')

    if bits_per_pixel < 8:

        print('---- Color Table ------------------------------')

        print('bits per pixel ({bits_per_pixel}) < 8')

        red_intensity   = int.from_bytes(bmp_bytes[55:56],'little')
        green_intensity = int.from_bytes(bmp_bytes[56:57],'little')
        blue_intensity  = int.from_bytes(bmp_bytes[57:58],'little')
     
        print(f'red intensity        {red_intensity}')
        print(f'green intensity      {green_intensity}')
        print(f'blue intensity       {blue_intensity}')
                                          
# ---------------------------------------------------------------
# ---- open bmp and read it directly into a byte array
# ----
# ---- It is not necessary to read in the complete file
# ---- if you are only accessing the headers. For example,
# ---- read the first 1000 bytes?
# ----
# ----   def load_bmp_to_array(bmp_file_path):
# ----       f = open(bmp_file_path,'rb')
# ----       bmp_bytes = f.read(1000)
# ----       close(f)
# ----       return bmp_bytes
# ----
# ---------------------------------------------------------------
    
def load_bmp_to_array(bmp_file_path:str) -> bytearray:

    with open(bmp_file_path,'rb') as file:
        bmp_bytes = file.read()
    return bmp_bytes

# ---------------------------------------------------------------
# ---- main
# ---------------------------------------------------------------

if __name__ == '__main__':
    
    bmp_file_paths = [ 'bmp_all_red.bmp',
             ##'mona_lisa_1.bmp', 'mona_lisa_original.bmp',
             ##'bmp_test_file_1.bmp','bmp_test_file_2.bmp',
             ##'bmp_test_file_3.bmp'
             ]

    for file_path in bmp_file_paths:

        print()

        bmp_bytes = load_bmp_to_array(file_path)

        display_bmp_header(file_path,bmp_bytes)

        # ---- assumption: no compression and 24 bit pixels
        
        offset = int.from_bytes(bmp_bytes[10:14],'little')
        height = int.from_bytes(bmp_bytes[22:26],'little')
        width  = int.from_bytes(bmp_bytes[18:22],'little')

        pixels_to_display = 30

        print()
        print(f'---- raw bytes   offset={offset}')
        print()
        display_raw_bytes(offset,pixels_to_display,bmp_bytes)

        print()
        print(f'---- RGB Pixels  offset={offset}')
        print()
        display_24bit_pixels(offset,pixels_to_display,width,bmp_bytes)
        
    print()