How different pixel formats encode color at the binary level. Compare RGB332, RGB565, RGB888, and ARGB8888 layouts, extraction, and conversion.
Every pixel format is defined by two things: how many bits are allocated to each color channel, and the order they appear in memory. I have worked with embedded displays, canvas APIs, and image codecs — each uses a different pixel format, and getting the bit layout wrong produces corrupted or shifted colors every single time.
Here is a quick comparison of the major pixel formats. The bit allocations show how many bits each channel receives, which directly determines the color precision of that channel.
When I was working on a retro game emulator, understanding pixel format layouts was essential. The difference between RGB565 and ARGB1555 is just a few bits per channel, but getting it wrong produces completely wrong colors.
| Format | Total Bits | R Bits | G Bits | B Bits | A Bits | Colors |
|---|---|---|---|---|---|---|
| RGB332 | 8 | 3 | 3 | 2 | 0 | 256 |
| RGB565 | 16 | 5 | 6 | 5 | 0 | 65,536 |
| RGB888 | 24 | 8 | 8 | 8 | 0 | 16.7M |
| ARGB8888 | 32 | 8 | 8 | 8 | 8 | 16.7M + alpha |
The key pattern is that each format uses a fixed total bit budget per pixel. Lower-bit-depth formats sacrifice color precision for memory savings. RGB332 gives you only 256 colors but uses 1 byte per pixel. RGB565 gives 65,536 colors at 2 bytes per pixel — a great trade-off for embedded systems. RGB888 and ARGB8888 deliver true color at 3-4 bytes per pixel.
RGB332 is the most compact RGB format at 8 bits per pixel. Red and Green each get 3 bits (8 levels each), and Blue gets 2 bits (4 levels), for a total of 8 × 8 × 4 = 256 possible colors. The blue channel gets fewer bits because the human eye is less sensitive to blue variations — a deliberate design choice I have seen made again and again in low-memory graphics systems.
I encountered RGB332 on a small OLED display module for a weather station project. With only 256 colors, gradients were visibly banded, but the display data fit in a tiny buffer. The extraction pattern — shift right by the combined width of the lower channels, mask by 2^bits - 1 — works for any packed pixel format.
// JavaScript: extract RGB332 channels and convert to 8-bit each
function rgb332ToRgb888(pixel) {
const r3 = (pixel >> 5) & 0x07; // 3-bit red
const g3 = (pixel >> 2) & 0x07; // 3-bit green
const b2 = pixel & 0x03; // 2-bit blue
// Scale to 0-255
return {
r: (r3 * 255) / 7, // scale 0-7 0-255
g: (g3 * 255) / 7,
b: (b2 * 255) / 3
};
}
RGB565 is the most widely used 16-bit pixel format in embedded systems, LCD controllers, and older graphics hardware. It allocates 5 bits to Red, 6 bits to Green (the human eye is most sensitive to green), and 5 bits to Blue. This asymmetry gives better perceived quality than a symmetric 5-5-5 allocation while still fitting neatly into 16 bits.
The extraction formulas for RGB565 are the pattern I use most often in embedded work. I have these memorized:
The masks 0x1F (binary 11111, decimal 31) and 0x3F (binary 111111, decimal 63) correspond to the range of each channel. The scaling formula value * 255 / maxValue maps the reduced-range channel to an 8-bit display. Note that 31 * 255 / 31 = 255 and 0 * 255 / 31 = 0, so the endpoints are preserved correctly.
// JavaScript: RGB565 ↔ RGB888 conversion
function rgb565ToRgb888(pixel) {
const r5 = (pixel >> 11) & 0x1F;
const g6 = (pixel >> 5) & 0x3F;
const b5 = pixel & 0x1F;
return {
r: Math.round(r5 * 255 / 31),
g: Math.round(g6 * 255 / 63),
b: Math.round(b5 * 255 / 31)
};
}
function rgb888ToRgb565(r, g, b) {
const r5 = (r >> 3) & 0x1F; // keep top 5 bits
const g6 = (g >> 2) & 0x3F; // keep top 6 bits
const b5 = (b >> 3) & 0x1F; // keep top 5 bits
return (r5 << 11) | (g6 << 5) | b5;
}
RGB888 is what we mean by "true color" — 8 bits per channel, 16.7 million colors. Each pixel occupies exactly 3 bytes, with 256 levels per channel. This is the standard for modern displays, JPEG/PNG images, and most web graphics (CSS rgb()).
One practical issue with RGB888 in memory: with 3 bytes per pixel, rows do not align to 4-byte boundaries naturally. A 4-pixel-wide row is 12 bytes (aligned), but a 5-pixel-wide row is 15 bytes — requiring 1 byte of padding per row in formats like BMP. This is why many graphics APIs internally prefer 32-bit formats even when no alpha is needed — the 4-byte alignment simplifies memory access.
// JavaScript: pack/unpack RGB888 (stored as 0xRRGGBB)
function rgb888Unpack(color) {
return {
r: (color >> 16) & 0xFF,
g: (color >> 8) & 0xFF,
b: color & 0xFF
};
}
function rgb888Pack(r, g, b) {
return ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF);
}
ARGB8888 adds an 8-bit alpha channel to the three RGB channels, making 32 bits per pixel. This is the native format for Android graphics, .NET GDI+, WebGL textures, and most modern GPU framebuffers. The 4-byte alignment makes it efficient for hardware rendering.
I consider ARGB8888 the "universal" pixel format because it is what most GPUs natively use. Even when an image is stored as 24-bit RGB, it is often expanded to ARGB8888 in the GPU framebuffer. The trade-off is memory — a 1920×1080 framebuffer at ARGB8888 consumes about 8.3 MB (1920 × 1080 × 4), compared to 6.2 MB for RGB888.
// JavaScript: ARGB8888 pack/unpack
function argbUnpack(color) {
return {
a: (color >>> 24) & 0xFF, // >>> to avoid sign extension
r: (color >> 16) & 0xFF,
g: (color >> 8) & 0xFF,
b: color & 0xFF
};
}
function argbPack(a, r, g, b) {
return ((a & 0xFF) << 24) | ((r & 0xFF) << 16) |
((g & 0xFF) << 8) | (b & 0xFF);
}
Format conversion between different pixel depths is a common task in graphics programming. The general principle is: when converting from lower depth to higher depth (e.g., RGB565 to RGB888), you scale each channel value; when converting from higher to lower (e.g., RGB888 to RGB565), you drop the least significant bits.
Upconversion increases color precision by scaling each channel. The 5-bit red value (0-31) becomes 8-bit (0-255) using multiplication: r8 = (r5 * 255) / 31. With integer arithmetic, the efficient form is r8 = (r5 * 255 + 15) / 31.
Downconversion discards the least significant bits: r5 = r8 >> 3 (keep top 5 bits), g6 = g8 >> 2 (keep top 6 bits), b5 = b8 >> 3 (keep top 5 bits). The lost bits cause quantized banding, particularly visible in smooth gradients.
The downconversion errors are small (within 2-5 units per channel in my testing) but become noticeable as posterization in smooth gradients. For most embedded applications this is acceptable given the 50% memory savings versus full RGB888.
// JavaScript: convert an entire buffer from RGB565 to RGB888
function convertBufferRgb565ToRgb888(src565, width, height) {
const dst888 = new Uint8Array(width * height * 3);
for (let i = 0; i < src565.length; i++) {
const p = src565[i];
const r5 = (p >> 11) & 0x1F;
const g6 = (p >> 5) & 0x3F;
const b5 = p & 0x1F;
dst888[i * 3] = Math.round(r5 * 255 / 31);
dst888[i * 3 + 1] = Math.round(g6 * 255 / 63);
dst888[i * 3 + 2] = Math.round(b5 * 255 / 31);
}
return dst888;
}
RGB332: Legacy systems or extremely memory-constrained devices (256 colors)
RGB565: Embedded displays, LCD controllers, framebuffers (best quality-per-bit ratio)
RGB888: Standard images, web graphics, print (full color precision)
ARGB8888: GPU rendering, compositing, alpha transparency (hardware-native format)
Pixel formats connect directly to color masking, bitmap operations, and ARGB manipulation. These guides cover the full picture.
RGB332 uses 8 bits total: 3 bits for Red (0-7), 3 bits for Green (0-7), 2 bits for Blue (0-3). It supports 256 colors. RGB565 uses 16 bits: 5 bits for Red (0-31), 6 bits for Green (0-63), 5 bits for Blue (0-31). It supports 65,536 colors and is widely used in embedded displays and low-cost LCD controllers.
To extract channels from RGB565: R = (pixel >> 11) & 0x1F, G = (pixel >> 5) & 0x3F, B = pixel & 0x1F. Each extracted value is 5 or 6 bits. To convert to 8-bit for display, scale: R8 = (R * 255) / 31, G8 = (G * 255) / 63, B8 = (B * 255) / 31.
RGB888 is standard 24-bit true color with 8 bits per channel (Red, Green, Blue), supporting 16.7 million colors. ARGB8888 is 32-bit with an additional 8-bit alpha channel, supporting the same color range plus transparency. The difference is one extra byte for alpha (bits 31-24) and 4-byte alignment which is more efficient for modern GPUs.
RGB565 is popular in embedded systems because it balances color quality and memory usage. At 16 bits per pixel, a 320x240 display uses only 150 KB of framebuffer memory (vs 300 KB for RGB888). The human eye is less sensitive to blue, so allocating only 5 bits to red and blue but 6 bits to green maximizes perceived quality.
To pack RGB888 into RGB565, reduce each 8-bit channel to the appropriate bit depth using right shifts: R5 = (R >> 3) & 0x1F (top 5 bits), G6 = (G >> 2) & 0x3F (top 6 bits), B5 = (B >> 3) & 0x1F (top 5 bits). Then pack: rgb565 = (R5 << 11) | (G6 << 5) | B5.