use rlvgl_decomp::lvgl::LV_IMAGE_HEADER_MAGIC;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutKind {
Bin,
C,
Rust,
}
pub fn sanitize_ident(raw: &str) -> String {
let mut out = String::with_capacity(raw.len());
for ch in raw.chars() {
if ch.is_ascii_alphanumeric() || ch == '_' {
out.push(ch);
} else {
out.push('_');
}
}
if out.is_empty() {
return "image".to_string();
}
if out.chars().next().is_some_and(|c| c.is_ascii_digit()) {
out.insert(0, '_');
}
out
}
fn format_bytes(bytes: &[u8], indent: usize) -> String {
let pad = " ".repeat(indent);
let mut out = String::with_capacity(bytes.len() * 6);
for (i, b) in bytes.iter().enumerate() {
if i % 16 == 0 {
if i != 0 {
out.push('\n');
}
out.push_str(&pad);
} else {
out.push(' ');
}
out.push_str(&format!("0x{b:02x},"));
}
out
}
pub fn emit_bytes(kind: OutKind, name: &str, bytes: &[u8]) -> String {
let id = sanitize_ident(name);
match kind {
OutKind::C => {
format!(
"// Generated by rlvgl-creator. Do not edit.\n\
#include <stddef.h>\n\
#include <stdint.h>\n\n\
const uint8_t {id}[{n}] = {{\n{body}\n}};\n\
const size_t {id}_len = {n};\n",
id = id,
n = bytes.len(),
body = format_bytes(bytes, 4),
)
}
OutKind::Rust => {
let up = id.to_ascii_uppercase();
format!(
"// Generated by rlvgl-creator. Do not edit.\n\n\
/// Embedded image blob ({n} bytes).\n\
pub static {up}: [u8; {n}] = [\n{body}\n];\n\
/// Length of [`{up}`] in bytes.\n\
pub const {up}_LEN: usize = {n};\n",
up = up,
n = bytes.len(),
body = format_bytes(bytes, 4),
)
}
OutKind::Bin => unreachable!("emit_bytes is only for C/Rust output"),
}
}
#[allow(clippy::too_many_arguments)]
pub fn emit_lvgl(
kind: OutKind,
name: &str,
w: u16,
h: u16,
cf_name: &str,
cf_code: u8,
stride: usize,
map: &[u8],
) -> String {
let id = sanitize_ident(name);
match kind {
OutKind::C => {
format!(
"// Generated by rlvgl-creator. Do not edit.\n\
#include \"lvgl.h\"\n\n\
#ifndef LV_ATTRIBUTE_MEM_ALIGN\n\
#define LV_ATTRIBUTE_MEM_ALIGN\n\
#endif\n\n\
static const LV_ATTRIBUTE_MEM_ALIGN uint8_t {id}_map[] = {{\n{body}\n}};\n\n\
const lv_image_dsc_t {id} = {{\n\
\x20 .header.magic = LV_IMAGE_HEADER_MAGIC,\n\
\x20 .header.cf = {cf},\n\
\x20 .header.flags = 0,\n\
\x20 .header.w = {w},\n\
\x20 .header.h = {h},\n\
\x20 .header.stride = {stride},\n\
\x20 .data = {id}_map,\n\
\x20 .data_size = sizeof({id}_map),\n\
}};\n",
id = id,
cf = cf_name,
w = w,
h = h,
stride = stride,
body = format_bytes(map, 4),
)
}
OutKind::Rust => {
let up = id.to_ascii_uppercase();
format!(
"// Generated by rlvgl-creator. Do not edit.\n\n\
/// LVGL image width in pixels.\n\
pub const {up}_WIDTH: u16 = {w};\n\
/// LVGL image height in pixels.\n\
pub const {up}_HEIGHT: u16 = {h};\n\
/// LVGL `lv_color_format_t` code ({cf_name} = 0x{cf_code:02x}).\n\
pub const {up}_CF: u8 = 0x{cf_code:02x};\n\
/// Row stride in bytes.\n\
pub const {up}_STRIDE: u16 = {stride};\n\
/// Pixel data in LVGL stored byte order ({n} bytes). Magic byte for\n\
/// the on-wire header is 0x{magic:02x} (`LV_IMAGE_HEADER_MAGIC`).\n\
pub static {up}_MAP: [u8; {n}] = [\n{body}\n];\n",
up = up,
w = w,
h = h,
cf_name = cf_name,
cf_code = cf_code,
stride = stride,
n = map.len(),
magic = LV_IMAGE_HEADER_MAGIC,
body = format_bytes(map, 4),
)
}
OutKind::Bin => unreachable!("emit_lvgl is only for C/Rust output"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sanitize_handles_leading_digit_and_symbols() {
assert_eq!(sanitize_ident("3-fox.png"), "_3_fox_png");
assert_eq!(sanitize_ident("logo"), "logo");
assert_eq!(sanitize_ident(""), "image");
}
#[test]
fn c_byte_array_has_size_and_length() {
let s = emit_bytes(OutKind::C, "blob", &[0xDE, 0xAD, 0xBE, 0xEF]);
assert!(s.contains("const uint8_t blob[4] = {"));
assert!(s.contains("0xde, 0xad, 0xbe, 0xef,"));
assert!(s.contains("const size_t blob_len = 4;"));
}
#[test]
fn rust_byte_array_uppercases_ident() {
let s = emit_bytes(OutKind::Rust, "my-icon", &[0x01, 0x02]);
assert!(s.contains("pub static MY_ICON: [u8; 2] = ["));
assert!(s.contains("pub const MY_ICON_LEN: usize = 2;"));
}
#[test]
fn lvgl_c_descriptor_is_well_formed() {
let s = emit_lvgl(
OutKind::C,
"icon",
2,
1,
"LV_COLOR_FORMAT_RGB565",
0x12,
4,
&[0, 0xF8, 0, 0xF8],
);
assert!(s.contains("static const LV_ATTRIBUTE_MEM_ALIGN uint8_t icon_map[] = {"));
assert!(s.contains("const lv_image_dsc_t icon = {"));
assert!(s.contains(".header.cf = LV_COLOR_FORMAT_RGB565,"));
assert!(s.contains(".header.w = 2,"));
assert!(s.contains(".header.stride = 4,")); assert!(s.contains(".data = icon_map,"));
}
#[test]
fn lvgl_rust_emits_dim_and_cf_constants() {
let s = emit_lvgl(
OutKind::Rust,
"icon",
2,
1,
"LV_COLOR_FORMAT_ARGB8888",
0x10,
8,
&[0; 8],
);
assert!(s.contains("pub const ICON_WIDTH: u16 = 2;"));
assert!(s.contains("pub const ICON_CF: u8 = 0x10;")); assert!(s.contains("pub const ICON_STRIDE: u16 = 8;")); assert!(s.contains("pub static ICON_MAP: [u8; 8] = ["));
}
#[test]
fn lvgl_c_descriptor_for_a8_alpha() {
let s = emit_lvgl(
OutKind::C,
"ic",
4,
2,
"LV_COLOR_FORMAT_A8",
0x0E,
4,
&[0; 8],
);
assert!(s.contains(".header.cf = LV_COLOR_FORMAT_A8,"));
assert!(s.contains(".header.stride = 4,")); }
}