import struct
from typing import NamedTuple, Optional
class BMPHeader(NamedTuple):
"""BMP file header structure - matches C BMP_Header"""
magic_number: bytes # 2 bytes: 'B', 'M'
file_size: int # 4 bytes: uint32_t
unused1: int # 2 bytes: uint16_t
unused2: int # 2 bytes: uint16_t
offset: int # 4 bytes: uint32_t
class DIBHeader(NamedTuple):
"""DIB header structure - matches C DIB_Header"""
header_size: int # 4 bytes: uint32_t
width: int # 4 bytes: uint32_t
height: int # 4 bytes: uint32_t
color_planes: int # 2 bytes: uint16_t
depth: int # 2 bytes: uint16_t
compression_algorithm: int # 4 bytes: uint32_t
pixel_array_size: int # 4 bytes: uint32_t
horizontal_resolution: int # 4 bytes: uint32_t
vertical_resolution: int # 4 bytes: uint32_t
palette_colors_count: int # 4 bytes: uint32_t
important_colors_count: int # 4 bytes: uint32_t
class BMPImage:
"""Complete BMP image structure - matches C BMP_Image"""
def __init__(self, bmp_header: BMPHeader, dib_header: DIBHeader, pixel_data: bytes):
self.bmp_header = bmp_header
self.dib_header = dib_header
self.pixel_data = pixel_data
# Struct format strings (little-endian, packed)
# '<' means little-endian, no padding between fields
BMP_HEADER_FORMAT = '<2sI2HI' # 2s=2 chars, I=uint32, H=uint16
DIB_HEADER_FORMAT = '<3I2H6I' # I=uint32, H=uint16
# Calculate sizes
BMP_HEADER_SIZE = struct.calcsize(BMP_HEADER_FORMAT) # 14 bytes
DIB_HEADER_SIZE = struct.calcsize(DIB_HEADER_FORMAT) # 40 bytes
def read_bmp_header(data: bytes, offset: int = 0) -> BMPHeader:
"""Read BMP header from bytes"""
values = struct.unpack_from(BMP_HEADER_FORMAT, data, offset)
return BMPHeader(
magic_number=values[0],
file_size=values[1],
unused1=values[2],
unused2=values[3],
offset=values[4]
)
def write_bmp_header(header: BMPHeader) -> bytes:
"""Write BMP header to bytes"""
return struct.pack(BMP_HEADER_FORMAT,
header.magic_number,
header.file_size,
header.unused1,
header.unused2,
header.offset
)
def read_dib_header(data: bytes, offset: int = 0) -> DIBHeader:
"""Read DIB header from bytes"""
values = struct.unpack_from(DIB_HEADER_FORMAT, data, offset)
return DIBHeader(
header_size=values[0],
width=values[1],
height=values[2],
color_planes=values[3],
depth=values[4],
compression_algorithm=values[5],
pixel_array_size=values[6],
horizontal_resolution=values[7],
vertical_resolution=values[8],
palette_colors_count=values[9],
important_colors_count=values[10]
)
def write_dib_header(header: DIBHeader) -> bytes:
"""Write DIB header to bytes"""
return struct.pack(DIB_HEADER_FORMAT,
header.header_size,
header.width,
header.height,
header.color_planes,
header.depth,
header.compression_algorithm,
header.pixel_array_size,
header.horizontal_resolution,
header.vertical_resolution,
header.palette_colors_count,
header.important_colors_count
)
def read_bmp_file(filename: str) -> BMPImage:
"""Read a complete BMP file"""
with open(filename, 'rb') as f:
data = f.read()
# Read headers
bmp_header = read_bmp_header(data, 0)
dib_header = read_dib_header(data, BMP_HEADER_SIZE)
# Validate magic number
if bmp_header.magic_number != b'BM':
raise ValueError("Invalid BMP file: magic number not 'BM'")
# Read pixel data
pixel_data = data[bmp_header.offset:]
return BMPImage(bmp_header, dib_header, pixel_data)
def write_bmp_file(filename: str, image: BMPImage):
"""Write a complete BMP file"""
with open(filename, 'wb') as f:
f.write(write_bmp_header(image.bmp_header))
f.write(write_dib_header(image.dib_header))
# Write any padding between headers and pixel data if needed
current_pos = BMP_HEADER_SIZE + DIB_HEADER_SIZE
padding_size = image.bmp_header.offset - current_pos
if padding_size > 0:
f.write(b'\x00' * padding_size)
f.write(image.pixel_data)
def bmp_image_to_bytes(image: BMPImage) -> bytes:
"""Convert BMPImage to bytes representation"""
# Write BMP header
result = write_bmp_header(image.bmp_header)
# Write DIB header
result += write_dib_header(image.dib_header)
# Write any padding between headers and pixel data if needed
current_pos = BMP_HEADER_SIZE + DIB_HEADER_SIZE
padding_size = image.bmp_header.offset - current_pos
if padding_size > 0:
result += b'\x00' * padding_size
# Write pixel data
result += image.pixel_data
return result
def update_bmp_file_size(image: BMPImage) -> BMPImage:
"""Update the file_size field to match actual size"""
actual_size = len(bmp_image_to_bytes(image))
updated_header = image.bmp_header._replace(file_size=actual_size)
return BMPImage(updated_header, image.dib_header, image.pixel_data)