Adafruit GFX drawBitmap vs drawXBitmap: Which Should You Use?
Adafruit_GFX exposes two near-identical methods: drawBitmap() and drawXBitmap(). The signatures match. The names differ by one letter. And if you swap them, your image displays mirrored or scrambled. This post explains the actual difference (it's the bit order) and how to pick the right output mode in image2cpp.
The one-line difference
drawBitmap reads pixels MSB-first. drawXBitmap reads pixels LSB-first. That is the entire technical difference. Everything else — coordinates, colors, dimensions — is identical.
What MSB vs LSB means here
Each byte stores 8 horizontal pixels. The question is: which pixel goes into bit 7 (the most-significant bit) and which goes into bit 0 (the least-significant bit)?
| Pixel 0 (leftmost) | Pixel 7 (rightmost) | |
|---|---|---|
| drawBitmap (MSB-first) | bit 7 (0x80) | bit 0 (0x01) |
| drawXBitmap (LSB-first, XBM) | bit 0 (0x01) | bit 7 (0x80) |
So the same row "11110000" (left half white, right half black) is:
- MSB:
0xF0 - LSB / XBM:
0x0F
If you feed an MSB array to drawXBitmap (or vice-versa), every byte's bits are reversed and your image looks mirrored within each 8-pixel chunk — a distinctive "stripey" corruption that confused you the first time it happened.
Why two formats exist at all
The XBM format dates back to X11 in 1989. It's a plain C source file that any X-based Unix tool could include and compile directly. GIMP and ImageMagick can export XBM in one click. Adafruit added drawXBitmap so users could paste GIMP-exported XBM data without converting bit order.
MSB-first is the natural form when you think of a byte as a left-to-right binary number, which is why most homegrown converters (including the original javl/image2cpp default) emit MSB.
Picking the right output in image2cpp
In pixel.hjlabs.in/converter, the relevant dropdowns are:
- Code output format: "Arduino code" (MSB) vs "Arduino code, single bitmap (XBM)" (LSB)
- Draw mode: Horizontal vs Vertical (covered below)
Rule of thumb:
- Using Adafruit_GFX's
drawBitmap→ pick "Arduino code, Horizontal, MSB" - Using U8g2's
drawXBM→ pick "XBM (Horizontal, LSB)" - Using U8g2's page-mode draw → pick "Vertical, 1 bit per pixel, LSB"
Code: side-by-side comparison
// MSB-first bitmap, drawBitmap
const unsigned char arrowMsb [] PROGMEM = {
0x10, 0x38, 0x7C, 0xFE, 0x10, 0x10, 0x10, 0x10
};
display.drawBitmap(0, 0, arrowMsb, 8, 8, SSD1306_WHITE);
// Same arrow as XBM (LSB-first), drawXBitmap
static const unsigned char arrow_bits [] PROGMEM = {
0x08, 0x1C, 0x3E, 0x7F, 0x08, 0x08, 0x08, 0x08
};
display.drawXBitmap(0, 0, arrow_bits, 8, 8, SSD1306_WHITE);
Both calls draw the same upward arrow at (0, 0). Note how every byte is bit-reversed between the two arrays.
Vertical vs Horizontal byte direction
This is a separate axis of confusion. drawBitmap and drawXBitmap in Adafruit_GFX both expect horizontal packing — bytes increment left-to-right within a row, then row-by-row. U8g2's drawBitmap() in page mode, however, expects vertical packing — each byte is 8 pixels tall, and bytes go left-to-right then page-by-page. The SSD1306 itself stores its frame buffer vertically, which is why U8g2 page-mode is so memory-efficient.
If your image looks like horizontal stripes shifted by 8 pixels, you have horizontal data being read as vertical (or vice versa).
Other GFX bitmap methods you should know
| Method | Bit order | Color depth | Use for |
|---|---|---|---|
| drawBitmap | MSB | 1-bit | Standard mono bitmaps |
| drawXBitmap | LSB (XBM) | 1-bit | GIMP-exported XBM |
| drawGrayscaleBitmap | 1 byte/pixel | 8-bit grayscale | Grayscale displays |
| drawRGBBitmap | 2 bytes/pixel | RGB565 | Color TFT displays |
| drawBitmap (mask version) | MSB | 1-bit + mask | Sprite blitting with transparency |
Performance: any difference?
Negligible. Both methods walk the array byte by byte and shift one bit at a time. The compiler generates near-identical assembly. If you're hammering the screen at 60+ FPS and need every cycle, you'd hand-roll a tile blitter anyway.
Picking by library
| Library | Recommended call | image2cpp output |
|---|---|---|
| Adafruit_SSD1306 / Adafruit_GFX | drawBitmap | Horizontal, MSB |
| Adafruit_GFX (XBM) | drawXBitmap | XBM (LSB) |
| U8g2 (full buffer) | drawBitmap or drawXBM | Match the call you use |
| U8g2 (page buffer) | drawBitmap (vertical) | Vertical, LSB |
| TFT_eSPI | drawBitmap | Horizontal, MSB (mono) or RGB565 |
Quick debugging chart
- Image is mirrored within each 8-pixel chunk: wrong MSB/LSB. Swap drawBitmap ↔ drawXBitmap.
- Image is rotated 90° or stripey: wrong vertical/horizontal. Toggle the byte direction.
- Image is upside-down or fully mirrored: SSD1306 rotation. Set
display.setRotation(2). - Image is shifted right by N pixels: width not divisible by 8 and converter zero-padded. Round width to a multiple of 8 in image2cpp.
Bottom line
Use drawBitmap with image2cpp's default Arduino output. Use drawXBitmap only when consuming XBM files from GIMP or older codebases. The "X" in drawXBitmap is for "X11", not "extended" or "extra". Test both modes side-by-side in image2cpp with the live preview — the visual feedback makes the difference instant.
More from hjLabs.in: og.hjlabs.in for project social previews, and enhance.hjlabs.in if your source PNG needs upscaling before conversion.
Try image2cpp now — free, in browser
Drop in a PNG, JPG or BMP. Get a paste-ready C++ array for Arduino, ESP32 or RP2040. 100% client-side. Learn more about image2cpp or jump straight to the tool.
Open the converter →