img2lv
Convert common static images (PNG, JPEG, WebP, BMP, etc.) to and from LVGL v9 image data.
Supports Node.js and Browser (via WebAssembly). Compatibility with other JS runtimes such as Deno and Bun has not been tested.
You can try out the features of this repository online at https://laride.github.io/img2lv/.
Installation
npm install img2lv
# or
yarn add img2lv
# or
pnpm add img2lv
The package ships prebuilt native binaries for major platforms. In environments where a native binary is unavailable, it automatically falls back to a WASI-based WebAssembly build.
Usage
Node.js
import { readFileSync, writeFileSync } from 'node:fs'
import { imageToBin, imageToC, lvglToPng } from 'img2lv'
const png = readFileSync('icon.png')
// Convert to LVGL binary format — only `cf` is required, everything else is optional
const bin = imageToBin(png, { cf: 'ARGB8888' })
writeFileSync('icon.bin', bin)
// Convert to LVGL C source file with LZ4 compression
const cSource = imageToC(png, 'icon.png', null, { cf: 'RGB565', compress: 'LZ4' })
writeFileSync('icon.c', cSource)
// Convert LVGL binary back to PNG
const pngOut = lvglToPng(bin)
writeFileSync('icon_out.png', pngOut)
Browser
The browser entry point loads the WASM build automatically:
import { imageToBin } from 'img2lv'
const file = await fetch('/icon.png').then((r) => r.arrayBuffer())
const bin = imageToBin(Buffer.from(file), { cf: 'ARGB8888' })
The browser WASM build uses shared WebAssembly.Memory, which requires SharedArrayBuffer. Modern browsers only expose SharedArrayBuffer in a cross-origin isolated context. Your page must be served with these response headers:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
If you control the server or dev server, configure the headers there. For example, with Vite:
// vite.config.js
export default {
server: {
headers: {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
},
},
preview: {
headers: {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
},
},
}
On static hosts that do not let you set custom headers (e.g. GitHub Pages), use a client-side workaround such as coi-serviceworker. Place coi-serviceworker.min.js next to your index.html and load it before your app:
<script src="coi-serviceworker.min.js"></script>
The script registers a service worker that injects the required COOP/COEP headers. The page reloads once on first visit; after that, crossOriginIsolated should be true.
You will also need a Buffer polyfill in the browser (e.g. the buffer package) and assign it to globalThis.Buffer before calling the library.
C array parsing (isCArray = true) is not available in the browser build.
API
API documentation generated from doc comments is available at https://laride.github.io/img2lv/doc/.
imageToBin(input, options)
Convert an encoded image to LVGL binary format.
| Parameter | Type | Description |
|---|---|---|
input |
Buffer |
Encoded image bytes (PNG, JPEG, WebP, BMP, etc.) |
options.cf |
string |
Required. Color format (see table below), or 'AUTO' / 'OPTIMIZED' |
options.background |
number |
Background color as 24-bit RGB (e.g. 0xFF0000). Default: 0 |
options.align |
number |
Stride alignment in bytes (any positive integer). Default: 1 |
options.premultiply |
boolean |
Pre-multiply RGB channels with alpha. Default: false |
options.compress |
string |
'NONE', 'RLE', or 'LZ4'. Default: 'NONE' |
options.rgb565Dither |
boolean |
Enable ordered dithering for RGB565 variants. Default: false |
options.nemaGfx |
boolean |
Byte-swap palette indices for NEMA GFX accelerator compatibility. Default: false |
Returns: Buffer — LVGL binary data (12-byte header + pixel data)
Input constraints:
- Image width and height must both be at least
1. - LVGL stores
w,h, andstridein 16-bit header fields. In practice this means width, height, and the final aligned stride must all fit within65535. - Very wide images may still be rejected even when
width <= 65535, because higher-bpp formats and largealignvalues can push the computed stride past the LVGL header limit.
imageToC(input, inputName, outputName, options)
Convert an encoded image to a LVGL C source file.
| Parameter | Type | Description |
|---|---|---|
input |
Buffer |
Encoded image bytes |
inputName |
string |
Original filename (used for variable naming) |
outputName |
string | null |
Override variable name, or null to derive from filename |
options |
ConvertOptions |
Same options as imageToBin |
Returns: string — C source code containing the lv_image_dsc_t descriptor
The same dimension and stride limits from imageToBin() apply here.
lvglToPng(input, options?)
Convert LVGL image data back to PNG.
| Parameter | Type | Description |
|---|---|---|
input |
Buffer |
LVGL binary data, or UTF-8 encoded C source |
options.isCArray |
boolean |
true if input is a C array from imageToC. Default: false |
Returns: Buffer — PNG file bytes
C array parsing is intended for testing and debugging only and should not be relied on in production.
lvglToRgba(input, options?)
Decode LVGL image data to raw RGBA pixels.
| Parameter | Type | Description |
|---|---|---|
input |
Buffer |
LVGL binary data, or UTF-8 encoded C source |
options.isCArray |
boolean |
true if input is a C array from imageToC. Default: false |
Returns: Buffer — Raw RGBA pixel data (4 bytes per pixel, row-major)
lvglWidth(input, options?) / lvglHeight(input, options?)
Get image dimensions from LVGL data. Accept the same options as lvglToPng.
Returns: number
Supported Color Formats
| Format | Description |
|---|---|
I1 / I2 / I4 / I8 |
Indexed with 2 / 4 / 16 / 256 color palette |
A1 / A2 / A4 / A8 |
Alpha-only |
AL88 |
8-bit luminance + 8-bit alpha |
L8 |
8-bit grayscale |
RGB565 |
16-bit RGB (little-endian) |
RGB565_SWAPPED |
16-bit RGB (big-endian) |
RGB565A8 |
16-bit RGB + separate alpha plane |
ARGB8565 |
16-bit RGB + inline alpha byte |
RGB888 |
24-bit RGB |
ARGB8888 |
32-bit RGBA |
XRGB8888 |
32-bit RGB (alpha forced to 0xFF) |
ARGB8888_PREMULTIPLIED |
32-bit premultiplied RGBA |
AUTO |
Match the Python reference behavior for automatic indexed selection |
OPTIMIZED |
Automatically compress indexed palettes when that produces a smaller LVGL indexed tier |
AUTO vs OPTIMIZED
AUTOis intended to stay compatible with the Python reference converter. In particular, for source indexed PNG files it preserves the original palette/index stream and selects the LVGL indexed tier from the source palette size.OPTIMIZEDis specific to this Rust/JS project and does not exist in the Python reference converter.OPTIMIZEDmay rewrite indexed palettes when that can reduce the LVGL output size. If the resulting indexed tier is not smaller than the original one, the original palette is kept.
Supported Platforms
Prebuilt native binaries are provided for:
- Windows — x64, ARM64
- macOS — x64, ARM64 (Apple Silicon)
- Linux — x64 (glibc / musl), ARM64 (glibc / musl)
- FreeBSD — x64
A WebAssembly (WASI) fallback is included for all other environments, including browsers.
Building from Source
Prerequisites
Development
# Install dependencies
yarn install
# Build native addon (debug)
yarn build:debug
# Build native addon (release)
yarn build
# Run tests
yarn test
# Format code
yarn format
# Lint
yarn lint # JS
cargo clippy # Rust
Tip: Before submitting a PR, make sure to run at least one build, format the code, and run both the JS and Rust lint checks.
Cross-compilation
To build for a specific target:
yarn build --target aarch64-apple-darwin
yarn build --target x86_64-unknown-linux-musl -x
yarn build --target wasm32-wasip1-threads
For musl targets, Zig and cargo-zigbuild are required.
Reference Compatibility
This project’s conversion logic is based on the official LVGL Python conversion script, which can be found in the reference/ directory.
Most behaviors are intentionally kept compatible with that version, but there are also a few implementation differences in this project:
AUTOfor non-indexed inputs follows the Rust implementation’s own palette analysis path instead of the Python script’spngquant-based conversion flow.OPTIMIZEDis specific to this project and does not exist in the Python reference converter. It may rewrite indexed palettes when a smaller LVGL indexed tier can be used without changing the visible result.- Fully transparent RGB variants may be normalized or merged during indexed conversion so visually identical transparent pixels do not consume multiple palette entries.
ARGB8888_PREMULTIPLIEDis decoded back to straight-alpha RGBA/PNG by un-premultiplying the stored RGB channels, instead of reproducing the Python script’s decode behavior byte-for-byte.- Premultiplication math is not identical in every format. In particular, some Rust paths use
/ 255where the Python script uses>> 8, sopremultiply=trueoutput is not guaranteed to be byte-identical to the reference implementation.
License
MIT