Last active
April 12, 2022 16:59
-
-
Save aholmes/08a1f83ab2530d4065967268e6baffe5 to your computer and use it in GitHub Desktop.
Generate a random black-and-white bitmap.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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