Skip to content

Instantly share code, notes, and snippets.

@aholmes
Last active April 12, 2022 16:59
Show Gist options
  • Save aholmes/08a1f83ab2530d4065967268e6baffe5 to your computer and use it in GitHub Desktop.
Save aholmes/08a1f83ab2530d4065967268e6baffe5 to your computer and use it in GitHub Desktop.
Generate a random black-and-white bitmap.
from math import trunc
from struct import pack
from typing import List
from random import random
def main():
# some hard-coded image attributes
# the width and height are 50 pixels
img_width = 50
img_height = img_width
# the bitmap will be a 24-bit color image https://en.wikipedia.org/wiki/Color_depth#True_color_(24-bit)
img_colordepth = 24
# some constants derived from the formula found here https://en.wikipedia.org/wiki/BMP_file_format#Pixel_storage
# row_bytes is the number of bytes it takes to store all img_width pixels @ img_colordepth-bit color (50 pixels @ 24-bit)
# you could also do (width * depth / 8) because (width * depth) is the number of _bits_ required, and there are 8 bits in a byte
row_bytes = trunc(img_width * img_colordepth / 32 * 4)
# pad_bytes is the remainder of bytes to fill in for each "row"
# this is non-zero when the number of bytes in row_bytes is not a multiple of 4
# From wiki: "Padding bytes (not necessarily 0) must be appended to the end of the rows in
# order to bring up the length of the rows to a multiple of four bytes."
pad_bytes = (4 - row_bytes) % 4
# other known constants
# the bitmap file header is always 14 bytes
file_header_size = 14
# the type of bitmap we're drawing - BITMAPINFOHEADER - is 40 bytes long,
# without any "extra bit masks" used
dib_header_size = 40
header = b""
# bitmap file header - 14 bytes https://en.wikipedia.org/wiki/BMP_file_format#Bitmap_file_header
header += pack("<B", ord("B")) # literal 'B' - 1 byte
header += pack("<B", ord("M")) # literal 'M' - 1 byte
header += pack("<L", 0) # filesize = 0 - 4 bytes
header += pack("<H", 0) # application specific - 2 bytes
header += pack("<H", 0) # application specific - 2 bytes
# After the file header and the DBI information header, this is the
# number of bytes, from the start of the file, after which the bitmap
# data can be found.
# That means every byte after (file_header_size + dib_header_size)
# contains pixel color data.
header += pack(
"<L", file_header_size + dib_header_size
) # image data address - 4 bytes
# DIB information header https://en.wikipedia.org/wiki/BMP_file_format#DIB_header_(bitmap_information_header)
# this is a BITMAPINFOHEADER
header += pack("<L", dib_header_size) # size of the header - 4 bytes
header += pack("<L", img_width) # width in pixels - 4 bytes
header += pack("<L", img_height) # height in pixels - 4 bytes
header += pack("<H", 1) # color planes - 2 bytes
header += pack("<H", img_colordepth) # color depth - 2 bytes
header += pack("<L", 0) # compression (none) - 4 bytes
header += pack("<L", 0) # image size - 4 bytes
header += pack("<L", 0) # horizontal resolution - 4 bytes
header += pack("<L", 0) # vertical resolution - 4 bytes
header += pack("<L", 0) # palette - 4 bytes
header += pack("<L", 0) # important colors - 4 bytes
# every `width` pixels is a new row. each pixel is a "column" in that row.
# https://en.wikipedia.org/wiki/BMP_file_format#Pixel_storage
# This looks like:
# [
# [
# [0,0,0], [0,0,0], [0,0,0], ... # there are img_width (50) of these pixel color arrays here
# ],
# [
# [0,0,0], [0,0,0], [0,0,0], ...
# ], # there are img_height (50) of these arrays
# ]
img_pixel_array: List[List[List[int]]] = [
[get_color() for _ in range(img_width)] for _ in range(img_height)
]
# `pixels` is a long sequence of 32-bit DWORDs representing the RGB of each pixel.
# Each "row" is padded with `pad_bytes` to ensure subsequent "rows" are read correctly
# by whatever is displaying the image, which uses the image height/width defined above
# to correctly read the pixels out of the file.
#
# In essence, if we were drawing a 1x1 black bitmap (keeping in mind every pixel is 32 bits (4 bytes)),
# we would write data like this to that file.
# Note: these are the integer values - the spaces are for visual purposes only, and are not part of the bitmap data.
# "|" separates each pixel - again just for visual purposes.
# I've denoted every 4 bytes with `^----^` - notice how the pixel values can span across multiple 32-bit DWORDs?
# That is why the data is padded.
# 0 0 0 | 0
# ^-------^
# or 1x1 all white:
# 255 255 255 | 0
# ^-------------^
# or 2x2 all black :
# 0 0 0 | 0 0 0 | 0 0
# ^-------^ ^-------^
# or 3x3 all black:
# 0 0 0 | 0 0 0 | 0 0 0 | 0 0 0
# ^-------^ ^-------^ ^-------^
pixels = b""
for row in img_pixel_array:
for pixel in row:
red = pixel[0]
green = pixel[1]
blue = pixel[2]
# order is blue, green, red because wiki says so
# "In most cases, each entry in the color table occupies 4 bytes, in the order blue, green, red"
pixels += pack("<BBB", blue, green, red)
for _ in range(pad_bytes):
pixels += pack("<B", 0)
with open("test.bmp", "wb") as file:
file.write(header + pixels)
def get_color():
black_or_white = round(random()) * 255
return [black_or_white, black_or_white, black_or_white]
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment