# Image to C Array

> 把 PNG、JPG 或 SVG 轉成 OLED、TFT、電子紙用的 C 陣列，涵蓋 LVGL、Adafruit GFX、u8g2 與 TFT_eSPI，也能把 framebuffer 傾印讀回成一張看得見的圖。

- URL: https://bytescope.dev/zh-Hant/image-to-c-array/
- Locale: zh-Hant · Also: [en](https://bytescope.dev/en/image-to-c-array.md) · [ja](https://bytescope.dev/ja/image-to-c-array.md)
- Runs: 圖片不會離開你的瀏覽器——每一個像素都在本機轉換。
- Requires: Any modern browser (Chrome, Edge, Firefox, Safari), desktop and mobile.
- Updated: 2026-09-08

## What it does

### 顯示器真正要的那份陣列

顯示驅動不吃 PNG。它吃的是一串位元組，而且要照它的控制器讀取的方式排好，其中每一項都得有人決定：一個像素一個位元還是十六個位元？最左邊那個像素放在第 7 位還是第 0 位？一列接一列，還是切成八個像素高的頁？紅色在高五位還是低五位？把圖丟進來、選好格式，位元組就照著那個排法打包——`mono`、`gray2`、`gray4`、`gray8`、LVGL 要的 `alpha1` 到 `alpha8` 遮罩，以及 `rgb565`、`rgb888` 和 `argb8888`。控制項旁邊那張預覽不是把原圖縮小：它是把打包好的陣列再讀回來畫的，所以你看到的就是面板會顯示的。

### 一個像素只有一個位元，這個位元怎麼花

128x64 的 OLED 有 8,192 個像素，每個一個位元，所以整張圖的成敗就在黑白分界線切在哪裡。線稿和圖示用單純的門檻就對了，滑桿就是在移動那條線。照片則要把誤差散開：Floyd–Steinberg 把誤差擴散給旁邊的像素，做出報紙印刷那種顆粒感；Bayer 4x4 的規則矩陣則給你一片整齊的網紋，在小面板上往往更好認。亮度和對比在切之前作用，`invert` 在切之後，而且每一個都會在你拖動的當下重畫預覽——因為一個圖示撐不撐得住一個位元，只能用它實際會顯示的尺寸看出來。

### 把 framebuffer 讀回成一張圖

反方向這件事沒人做，而它正好回答你手上真正的問題：這份陣列到底對不對？貼一段 C 陣列（`0x` 的位元組、十進位、`\x` 跳脫、一整塊裸的十六進位，有沒有大括號、註解和 `#define` 行都無所謂），或是丟一份從快閃記憶體挖出來的 `.bin`，它就會被畫成圖。接著拉寬度滑桿。寬度不對的 framebuffer 會歪成一道道斜條紋，而寬度一對，整張圖就當場立起來——這是辨認來路不明傾印的標準手法。它同樣可以拿來檢查自己的輸出：轉一個圖示，把陣列貼回來，如果出來的東西看起來就是你的圖示，那位元順序和每列補的位元都對了。讀不出來的 token 會連同位置一起報出來，不會被吞掉，所以貼上的陣列打錯一個字，賠掉的是一個值，不是後面整份。

### 樣板、位元組順序，還有那個一定會咬人的

同樣一串位元組，看是誰要編譯它，包裝就不一樣：素的 `static const uint8_t` 陣列、附 `_WIDTH` 和 `_HEIGHT` 巨集的 Adafruit GFX `PROGMEM` 陣列、用 `#define` 寫尺寸的 u8g2 XBM、LVGL v8 的 `lv_img_dsc_t` 和 v9 的 `lv_image_dsc_t`、丟給 TFT_eSPI `pushImage()` 的 `uint16_t` 陣列，還有旁邊直接把 `framebuf` 建構式寫好的 MicroPython `bytes` 字面值。會咬人的是 TFT_eSPI：它讀 16 位元像素時先讀高位元組，所以只有這個樣板要把 RGB565 用大端序打包，其他一律小端序。在這裡你選樣板，位元組順序就跟著走——一份編得乾乾淨淨、顏色卻整個錯掉的陣列，是沒有人喜歡花時間去找的臭蟲。整套都在這個分頁的 Web Worker 裡跑：不上傳、不用帳號，還沒公開的產品美術素材不會離開你的機器。

## Limits

Does not emit LVGL's packed binary .bin image header (it writes the C struct literal a project compiles), does not generate bitmap fonts, does not decode animated GIF past the first frame, and does not dither RGB565, RGB888 or ARGB8888 per channel.

## FAQ

### SSD1306 的 OLED 要設成什麼？

格式選 `mono`、像素排列選分頁、位元順序選 LSB 在前。SSD1306 和 SH1106 把記憶體存成八個像素高的直向頁，一欄一個位元組，該頁最上面那個像素放在第 0 位——分頁加上 LSB 在前產生的正好就是這個。如果你老老實實逐列排一份陣列送過去，畫面看起來會像被切成八條交錯的片段，原因就在這裡。要是你不是直接寫控制器，而是餵給 u8g2 的 XBM 輔助函式，那就改成逐列加 MSB 在前。

### 為什麼送進 TFT_eSPI 顏色整個跑掉？

幾乎都是位元組順序。`pushImage()` 讀 16 位元緩衝區時先讀高位元組，所以低位元組在前打包的 RGB565，會把純紅 `0xf800` 變成 `0x00f8`——一個暗藍灰。陣列編得過、大小也對，只有顏色是錯的。在這裡選 TFT_eSPI 樣板，它會替你用大端序打包，並且把這件事寫出來。如果是從別處抄來的陣列顏色亂掉，把它丟進反向檢視器切換位元組順序，圖會立刻告訴你原本是用哪一種寫的。

### 可以產 LVGL 的二進位圖檔嗎？

不行，而且不會假裝可以。這裡吐出來的是 C 原始碼：像素陣列，加上一個欄位都有名字的 `lv_img_dsc_t`（v8）或 `lv_image_dsc_t`（v9）結構體字面值——就是你加進專案裡編譯的那種東西。帶自有標頭的 `.bin` 是另一種產物，與其憑印象亂猜，這裡選擇不做。色彩格式那一欄也留給你：你給什麼 token 就原樣寫出去，留空就會得到一個 `UNKNOWN` 加一行 `TODO`，絕不會替你發明一個常數。

### 我有一份快閃記憶體傾印，但完全不知道圖多寬。

把 `.bin` 丟進反向檢視器，猜一個像素格式，然後把寬度掃描從 1 往上拉。只要寬度不對，每一列就會固定偏移一點，直線邊緣因此變成斜線，畫面就歪掉——直到寬度剛好等於真正的 stride，所有東西同時對齊。接著調整位移量，把像素前面那段標頭跳過去。資料不夠長在這裡不算錯：後面沒有位元組的像素會畫成透明並計數，所以半截的傾印照樣掃得動，工具不會拒絕顯示。

### 我的圖會被上傳嗎？

不會。檔案由你自己的瀏覽器解碼，像素的量化和打包在這個分頁的 Web Worker 裡做完，分頁一關就全部沒了。沒有上傳步驟、沒有帳號，這頁也沒有伺服器端。這件事在這裡比在多數工具上更要緊：還沒發表的產品那一整套圖示，正是那種「別貼到線上轉檔網站」的檔案。

## Related tools

- [MicroPython Web IDE](https://bytescope.dev/zh-Hant/micropython.md) — 把 framebuf 的位元組直接貼進跑 MicroPython 的板子。
- [ESP Firmware Flasher](https://bytescope.dev/zh-Hant/esp-flasher.md) — 把帶著這個陣列的韌體透過 USB 燒進 ESP32 或 ESP8266。
- [Hex Viewer / Editor](https://bytescope.dev/zh-Hant/hex-editor.md) — 把原始位元組打開，找到要改的欄位直接改。
- [Image Format Converter](https://bytescope.dev/zh-Hant/image-convert.md) — 在圖變成陣列之前，先改尺寸或換編碼。
