U8g2 Image Converter
Turn a PNG, BMP, or GIF into a 1-bit XBMP array for an SSD1306 or any other monochrome display U8g2 drives, and see it on a simulated screen before you flash anything.
Runs in the browser. Free account, no install.
What the converter does
Lopaka is a graphics editor for embedded displays. Import an image into a U8g2 project and it becomes a one-bit bitmap layer on a canvas the size of your display, where you can move it, crop it, redraw individual pixels with the Paint tool, and put text and shapes around it. The code panel keeps pace: it declares the array and emits the drawXBMP() call for wherever the layer currently sits.
That is the part a plain converter leaves to you. Getting the bytes is one step; knowing that the logo is 4 pixels too low, or that the label collides with it at this font size, normally takes a compile and a flash. Here it takes dragging the layer.
How a monochrome bitmap actually works
One bit per pixel, not one byte
An SSD1306 panel has one bit of state per pixel: lit or dark. There is no grey and no colour, so a bitmap for it stores one bit per pixel too, eight pixels packed into every byte, row after row. A 128x64 screen is 1,024 bytes in that form. Store the same picture as one byte per pixel and it becomes 8,192 bytes, which on a classic AVR board is more RAM than you have.
XBM, XBMP, and where PROGMEM comes in
XBM is that packed one-bit format, and it is what U8g2 expects from drawXBM(). XBMP is the same data declared in flash rather than RAM: with Declare as PROGMEM enabled, Lopaka marks the array U8X8_PROGMEM and calls drawXBMP(), which reads it from program memory. On an Arduino target that is usually what you want, because bitmaps are constant and RAM is the scarce resource. The ESP-IDF template emits a plain uint8_t array and drawXBM() instead.
Threshold, dithering, and inversion
A source image usually has 256 shades per channel and the display has two, so something has to decide which pixels survive. A plain threshold sends everything above a cutoff to white and everything below to black, which is right for logos and line art with hard edges. Dithering instead spreads the rounding error into neighbouring pixels, so a gradient reads as texture rather than a band. Brightness and contrast move the cutoff before either happens, and Grayscale first helps when a colour image has hues of similar perceived brightness, like red on white.
| Dithering mode | When to use it |
|---|---|
| Floyd-Steinberg | Detailed photos and smooth gradients |
| Atkinson | A lighter, high-contrast dither that works well for small displays |
| Bayer 8x8 | A regular pixel pattern with predictable texture |
| No dithering | Clean line art, logos, and solid fills |
Two separate inversion controls exist because there are two separate problems. Invert palette swaps black and white before dithering, for when the converter is treating your background as the subject. Invert result flips the finished bitmap, for when the preview is right but the panel renders it backwards.
Displays and boards
U8g2 is a monochrome graphics library covering a long list of OLED and LCD controllers, and Lopaka targets its graphical API. The familiar ones are SSD1306, SH1106, and SSD1309, which is most of what ships on a small I2C or SPI OLED module. Check the upstream constructor list to confirm your exact panel.
Lopaka does not detect or drive hardware. It generates the drawing code; you pick the U8g2 constructor that matches your controller, resolution, bus, and pins, then call begin() and run the refresh loop yourself. Set the Lopaka screen size to the same logical resolution as that constructor and the canvas coordinates will match the panel.
Converting an image
- Create a project, select U8g2 as the platform, and set the screen size to your display resolution.
- Open the Image tool and drop in a PNG, BMP, or GIF, or paste an existing XBMP byte array.
- Crop and resize, then set brightness, contrast, and dithering while the preview updates.
- Import the bitmap, position it on the canvas, and add text or shapes around it.
- Copy the generated source, which declares the array and calls drawXBMP at that position.
What the output looks like
A 16x16 icon placed at x 8, y 8 with a label beside it, on a screen named Status. This is the Arduino (Cpp) template, so the array is declared PROGMEM and drawn with drawXBMP.
static const unsigned char image_wifi_bits[] U8X8_PROGMEM = {0x00,0x00,0x00,0x00,0xf8,0x1f,0xfc,0x3f,0x0e,0x70,0x03,0xc0,0x00,0x00,0xf0,0x0f,0xf8,0x1f,0x1c,0x38,0x06,0x60,0x00,0x00,0xc0,0x03,0xe0,0x07,0xc0,0x03,0x00,0x00};
void drawStatus(void) {
u8g2.setFontMode(1);
u8g2.setBitmapMode(1);
// wifi
u8g2.drawXBMP(8, 8, 16, 16, image_wifi_bits);
// label
u8g2.setFont(u8g2_font_6x10_tr);
u8g2.drawStr(32, 21, "Connected");
}The same screen exported with the ESP-IDF (C) template. The bitmap bytes are identical; only the declaration and the API around them change.
static const uint8_t image_wifi_bits[] = {0x00,0x00,0x00,0x00,0xf8,0x1f,0xfc,0x3f,0x0e,0x70,0x03,0xc0,0x00,0x00,0xf0,0x0f,0xf8,0x1f,0x1c,0x38,0x06,0x60,0x00,0x00,0xc0,0x03,0xe0,0x07,0xc0,0x03,0x00,0x00};
void drawStatus(void) {
u8g2_ClearBuffer(&u8g2);
u8g2_SetBitmapMode(&u8g2, 1);
u8g2_SetFontMode(&u8g2, 1);
// wifi
u8g2_DrawXBM(&u8g2, 8, 8, 16, 16, image_wifi_bits);
// label
u8g2_SetFont(&u8g2, u8g2_font_6x10_tr);
u8g2_DrawStr(&u8g2, 32, 21, "Connected");
u8g2_SendBuffer(&u8g2);
}The font in both samples is one of U8g2's built-in symbols, so it needs no downloaded header. A font outside that list is exported as a header you download alongside the sketch.
Questions
How do I convert an image to a U8g2 bitmap?
Import the image into a U8g2 project. Lopaka converts it to a 1-bit XBM array, shows it on a canvas the size of your display, and generates the declaration plus the drawXBMP call that puts it at the position you dragged it to.
What is XBMP and why does U8g2 use it?
XBM packs one pixel into one bit, eight pixels per byte, row by row. A monochrome OLED has exactly one bit of state per pixel, so the array is a direct copy of what the panel shows. XBMP is the same data declared in flash with U8X8_PROGMEM, which is why Arduino builds pair it with drawXBMP instead of drawXBM.
Which displays does this work with?
Any monochrome display U8g2 itself supports, including SSD1306, SH1106, and SSD1309 OLEDs. Lopaka generates the drawing code; you pick the U8g2 constructor for your controller, bus, and pins, and call begin() and the refresh loop yourself.
What does dithering do to a photograph?
A monochrome display has no grey, so every pixel has to end up black or white. Dithering scatters the error across neighbouring pixels so a gradient reads as a texture instead of a hard edge. Floyd-Steinberg and Atkinson suit photos; logos and line art usually look cleaner with dithering off.
Can I use my existing bitmap arrays?
Yes. The import wizard accepts a raw XBMP byte array pasted from existing firmware. Give it the width and height, flip Swap bytes if the preview looks scrambled, and it becomes an editable layer you can redraw pixel by pixel with the Paint tool.
Does it generate ESP-IDF code as well as Arduino?
Yes. The U8g2 platform has two templates: Arduino (Cpp), which emits U8X8_PROGMEM arrays and u8g2.drawXBMP(), and ESP-IDF (C), which emits plain uint8_t arrays and u8g2_DrawXBM() against the C interface. Switch between them in Code Settings.
Is it free?
Yes. Converting images and generating U8g2 code are part of the free Basic plan, along with the community icon library and a couple of cloud projects. Plus adds higher limits, custom font import, the full icon and animation libraries, and file export, but nothing on this page needs it.
Try it on your own image
Create a free account and the editor opens on a blank screen at your display size.