ByteScope

Image to C Array

Both directions: a picture into an array your firmware compiles, and an array back into a picture you can actually look at.

Your images never leave your browser — every pixel is converted locally.

Source image

Drop a PNG, JPG, SVG, GIF or WebP here

or click to browse. The file is decoded in the page and never uploaded.

Up to 4096 px on a side. A panel is almost always far smaller than the artwork drawn for it, and scaling here is what decides how much detail one bit has to carry.

Pixel format

Dither
Pixel order
Bit order
Byte order

Output preview

Not the source scaled down: this is the packed array read back out, so the dither pattern, the bit order and the row padding are the ones being emitted.

C source

Nothing to show yet.
Nothing to show yet.

Read an array back into a picture

Paste a C array or drop a raw dump, then drag the width until the picture stops shearing. This is how an unknown framebuffer is identified, and it is also the fastest way to prove your own array came out right.

Byte order for 16- and 32-bit tokens

Only applies to tokens wider than a byte, such as the uint16_t values a TFT_eSPI array is written with.

or drop a .bin dump

a flash region, an SD card sector, anything with pixels in it.

A framebuffer read at the wrong width shears into diagonal stripes. Drag until they straighten out; that width is the real one.

Pixel order
Bit order
Byte order

Nothing loaded yet.

About this tool

The array your display actually wants

A display driver does not take a PNG. It takes a run of bytes laid out exactly the way its controller reads them, and every part of that layout is a decision somebody has to make: one bit per pixel or sixteen, the leftmost pixel at bit 7 or at bit 0, rows one after another or eight-pixel-tall pages, red in the top five bits or the bottom five. Drop an image here, pick the format, and the bytes are packed to match — mono, gray2, gray4, gray8, the alpha1 to alpha8 masks LVGL wants, rgb565, rgb888 and argb8888. The preview beside the controls is not the source image scaled down; it is the packed array read back out again, so what you are looking at is what the panel will show.

One bit per pixel, and how to spend it

A 128x64 OLED gives you 8,192 pixels and one bit each, so the whole picture comes down to where the cut between black and white falls. For line art and icons a plain threshold is the right answer, and the slider moves it. A photograph needs the error spread around instead: Floyd–Steinberg diffuses it into the neighbouring pixels and gives the grainy newsprint look, while an ordered Bayer 4x4 matrix gives a regular cross-hatch that often reads more cleanly on a small panel. Brightness and contrast are applied before the cut, invert after it, and each of them redraws the preview as you drag — because the only way to know whether an icon survives one bit is to look at it at the size it will be shown.

Reading a framebuffer back into a picture

The other direction is the one nobody builds, and it answers the question you actually have: is this array right? Paste a C array — 0x bytes, plain decimal, \x escapes, a bare hex blob, with or without braces, comments and #define lines — or drop a raw .bin you pulled out of flash, and it is drawn as an image. Then drag the width slider. A framebuffer read at the wrong width shears into diagonal stripes, and the moment the width is right the picture snaps into place; that is the standard way to identify an unknown dump. It works just as well as a check on your own output: convert an icon, paste the array back, and if what comes out looks like your icon then the bit order and the row padding are right. A token that cannot be read is reported with its position rather than swallowed, so one typo in a pasted array costs one value and never the rest of the file.

Templates, byte order, and the one that bites

The same bytes are wrapped differently depending on what is going to compile them: a plain static const uint8_t array, an Adafruit GFX PROGMEM array with _WIDTH and _HEIGHT macros, a u8g2 XBM with #define dimensions, an lv_img_dsc_t for LVGL v8 or an lv_image_dsc_t for v9, a uint16_t array for TFT_eSPI's pushImage(), or a MicroPython bytes literal with the framebuf constructor written out beside it. TFT_eSPI is the one that catches people: it reads each 16-bit pixel high byte first, so RGB565 has to be packed big-endian for that template and little-endian for every other one. Pick the template and the byte order follows it, because an array that compiles cleanly and displays in the wrong colours is a bug nobody enjoys finding. All of it runs in a Web Worker in this tab: no upload, no account, and the artwork for the product you have not announced yet stays on your machine.

Frequently asked questions

What settings does an SSD1306 OLED need?

Format mono, pixel order set to pages, and LSB first. An SSD1306 or SH1106 stores its memory as eight-pixel-tall vertical pages, one byte per column, with the topmost pixel of the page in bit 0 — that is exactly what page order plus LSB first produces, and it is why a straightforward row-by-row array comes out looking like eight interleaved slices of your image. If you are feeding u8g2's XBM helpers rather than writing to the controller directly, use rows and MSB first instead.

Why does my TFT_eSPI image come out the wrong colour?

Byte order, almost always. pushImage() reads a 16-bit buffer with the high byte first, so RGB565 packed low-byte-first turns pure red 0xf800 into 0x00f8, a dark blue-grey — the array compiles, the size is right, and only the colours are wrong. Selecting the TFT_eSPI template here packs big-endian for you and says so. If you are pasting an array from somewhere else and the colours look scrambled, load it into the reverse viewer and switch the byte order there; the picture will tell you immediately which one it was written with.

Can it produce LVGL's binary image file?

No, and it will not pretend to. What it emits is C source: the pixel array plus an lv_img_dsc_t (v8) or lv_image_dsc_t (v9) struct literal with named fields, which is what you add to a project and compile. The packed .bin form with its own header is a different artefact and is not implemented here rather than being guessed at. The colour-format field is also left to you — pass the token your build expects and it is written through verbatim; leave it empty and you get an UNKNOWN token with a TODO beside it, never an invented constant.

I have a flash dump and no idea how wide the image is.

Drop the .bin into the reverse viewer, guess the pixel format, and drag the width scan from 1 upwards. Any width but the right one shifts each row by a constant amount, which turns straight edges into diagonals, so the display shears — until the width matches the real stride and everything lines up at once. Then adjust the offset to skip whatever header sits in front of the pixels. Short data is never an error here: pixels with no bytes behind them are drawn transparent and counted, so you can scan a partial dump without the tool refusing to show it.

Is my image uploaded anywhere?

No. The file is decoded by your own browser, the pixels are quantized and packed in a Web Worker inside this tab, and everything is dropped when you close it. There is no upload step, no account and no server side to this page. That matters more here than on most tools: the icon set for an unreleased product is exactly the kind of file people are told not to paste into an online converter.