How pixels, color depth, and image data work at the binary level. Understanding bitmap memory layout is essential for low-level graphics programming and image processing.
A bitmap (or raster image) is a memory-mapped grid of pixels. At the binary level, there is no concept of "image" — just a contiguous block of memory where every N bits represent one pixel. I internalized this when I wrote my first BMP decoder from scratch: a bitmap is literally just an array of numbers arranged in a specific order, and the order is everything.
Every bitmap has three fundamental properties at the bit level:
I used bit-packed bitmaps to track memory page allocation in a hobby OS kernel I was building. Setting and clearing individual bits with OR and AND was the most efficient way to manage which pages were free.
For a 24-bit RGB bitmap, each pixel is exactly 3 bytes (blue, green, red in BMP convention). For a 4-bit bitmap, two pixels fit in each byte. For a 1-bit bitmap, eight pixels fit in each byte. The bit-level access patterns differ drastically between these formats, and understanding the layout is the difference between working code and corrupted images.
Bitmaps store pixels in row-major order: all pixels of row 0 come first, then all pixels of row 1, and so on. To access pixel (x, y), you compute the byte offset from the start of the pixel data array.
For 24-bit bitmaps, the triple-byte stride (x * 3) is important. Unlike 32-bit or 8-bit formats where the multiplication aligns naturally to byte boundaries, the 3-byte stride requires offset calculations that do not map to power-of-two alignment. I remember spending an afternoon debugging a corrupted output because I was using width * 3 as the stride instead of computing the padded stride properly.
// JavaScript: read pixel (x, y) from a 24-bit RGB raw buffer (BMP order)
function getPixel24(buffer, width, height, x, y) {
const bpp = 24;
const stride = ((width * bpp + 31) >> 5) << 2; // padded to 4 bytes
const offset = y * stride + x * 3;
// BMP stores in B, G, R order
const b = buffer[offset];
const g = buffer[offset + 1];
const r = buffer[offset + 2];
// Pack into 0xRRGGBB
return (r << 16) | (g << 8) | b;
}
The BMP file format stores pixel data in Blue-Green-Red order at the byte level. When reading a BMP, the first byte of each pixel is blue, the second is green, and the third is red. This differs from most other formats (PNG, JPEG) which use RGB order. Always check the byte order before processing raw pixel data.
Stride is the number of bytes from the start of one row to the start of the next. Most bitmap formats require each row to start at a memory address that is a multiple of 4 bytes. This alignment requirement means that the raw data for a row may have padding bytes at the end.
I ran into this the hard way when I assumed width * 3 was the row size for a 24-bit BMP. A 5-pixel-wide image at 24-bit is 15 bytes per row, but 15 is not divisible by 4. The BMP spec requires 4-byte alignment, so the actual stride is 16 bytes, with 1 padding byte at the end of each row. My code was reading garbage pixels because it was not skipping that padding.
The bitwise formula ((width * bpp + 31) >> 5) << 2 rounds the total bits per row up to the next multiple of 32 bits (4 bytes), then divides by 8 to get bytes. This is a clean, branchless computation that works for any width and bit depth.
// JavaScript: compute stride for any bit depth
function computeStride(width, bpp) {
const bitsPerRow = width * bpp;
const alignedBits = (bitsPerRow + 31) & ~31; // round up to 32-bit boundary
return alignedBits / 8; // convert bits to bytes
}
console.log(computeStride(5, 24)); // Output: 16
console.log(computeStride(10, 24)); // Output: 32
A 1-bit bitmap (like a black-and-white fax or a 1-bit icon) packs 8 pixels into each byte. Each bit represents one pixel: 1 for white (or foreground), 0 for black (or background). The pixel packing order is typically most-significant-bit first, meaning pixel 0 in a row occupies bit 7 of the first byte.
To access individual pixels in a 1-bit bitmap, you need to identify the correct byte and then extract the specific bit. The extraction uses the same bitwise masking technique as color channel extraction, but at the single-bit level.
// JavaScript: get pixel from 1-bit bitmap
function getPixel1Bit(buffer, width, height, x, y) {
const stride = computeStride(width, 1);
const byteIndex = y * stride + Math.floor(x / 8);
const bitIndex = 7 - (x % 8); // MSB-first ordering
return (buffer[byteIndex] >> bitIndex) & 1;
}
// Set pixel in 1-bit bitmap (0 or 1)
function setPixel1Bit(buffer, width, height, x, y, value) {
const stride = computeStride(width, 1);
const byteIndex = y * stride + Math.floor(x / 8);
const bitIndex = 7 - (x % 8);
if (value) {
buffer[byteIndex] |= (1 << bitIndex); // set bit
} else {
buffer[byteIndex] &= ~(1 << bitIndex); // clear bit
}
}
8-bit and 4-bit bitmaps use a color palette (or color table). Instead of storing RGB values directly per pixel, each pixel stores an index into a palette of predefined colors. The palette is a table where each entry is an RGB or ARGB color value. This reduces memory usage dramatically for images with limited colors.
The palette approach means a 256-color 8-bit image uses only 1 byte per pixel for the data, plus 256 × 4 bytes = 1024 bytes for the palette. For a 100×100 image, that is 10,000 bytes of pixel data + 1024 bytes palette = 11,024 bytes. The same image in 24-bit true color would be 30,000 bytes. When you are processing thousands of frames, this savings adds up fast.
// JavaScript: convert 8-bit indexed pixel to RGB using palette
function indexedToRgb(indexedBuffer, palette, width, height) {
const stride = computeStride(width, 8);
const rgb = new Uint8Array(width * height * 3);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const idx = indexedBuffer[y * stride + x];
const out = (y * width + x) * 3;
rgb[out] = palette[idx][0]; // R
rgb[out + 1] = palette[idx][1]; // G
rgb[out + 2] = palette[idx][2]; // B
}
}
return rgb;
}
Converting a true-color image to indexed color requires palette quantization — finding the best set of colors to represent the image. Algorithms like Median Cut, Octree Quantization, and K-Means determine which 256 (or 16) colors best approximate the original. All of them use bitwise operations extensively for histogram computation and color distance measurement.
Bitmap operations connect directly to color channels, masking, and pixel format conversion. Explore these related topics.
A bitmap is a memory-mapped grid where each pixel is represented by one or more bits. At the lowest level, a bitmap is just a contiguous array of binary data. A 1-bit bitmap stores each pixel as either 0 (black) or 1 (white). An 8-bit bitmap stores 256 possible values per pixel, and a 24-bit bitmap stores RGB color with 8 bits per channel.
Bits per pixel (bpp) defines how many bits of data are used to represent a single pixel. Common values are 1 bpp (black and white), 8 bpp (256 colors or grayscale), 16 bpp (high color), 24 bpp (true color), and 32 bpp (true color with alpha). Higher bpp means more colors but larger file sizes.
In a raw bitmap, pixels are stored sequentially row by row (row-major order), starting from the bottom-left corner in formats like BMP. Each row is padded to a multiple of 4 bytes. For a 24-bit bitmap, each pixel occupies 3 bytes (B, G, R order in BMP). For a 32-bit bitmap, each pixel occupies 4 bytes (B, G, R, A).
Stride is the number of bytes per row including padding. The formula is: stride = ((width * bpp + 31) >> 5) << 2. This rounds up to the next multiple of 4 bytes, which is the standard alignment requirement for BMP and many other image formats. In code: stride = (((width * bpp) + 31) & ~31) / 8.
To access pixel (x, y) in a raw bitmap array, compute offset = y * stride + x * (bpp / 8). For a 24-bit BMP, offset = y * stride + x * 3. The pixel data starts at this offset, with bytes in B, G, R order (BMP convention). For 32-bit, offset = y * stride + x * 4, with B, G, R, A ordering.