rlvgl 0.2.4

A modular, idiomatic Rust reimplementation of the LVGL graphics library for embedded and simulator use.
Documentation
//! Source-array emitters for image blobs.
//!
//! Produces C and Rust source that embeds an image directly in the binary —
//! the cheapest path on all-RAM SoCs, where there is no filesystem and the
//! decode-from-flash dance buys nothing. Two shapes:
//!
//! * [`emit_bytes`] — a raw `uint8_t[]` / `[u8; N]` of an opaque blob (used
//!   for the rlvgl `RLEC` blob, which firmware feeds to
//!   `rlvgl_decomp::parse_rle_blob`).
//! * [`emit_lvgl`] — the canonical LVGL compiled-in image: a pixel-data array
//!   plus a ready-to-use `lv_image_dsc_t` descriptor (C) or dimension/format
//!   constants (Rust), so an LVGL build can point at it with zero runtime
//!   parsing.
//!
//! All output is deterministic (fixed 16-bytes-per-line formatting, no
//! timestamps) so it is reproducible and diff-friendly.

use rlvgl_decomp::lvgl::LV_IMAGE_HEADER_MAGIC;

/// Output container for an emitted image blob.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutKind {
    /// Raw binary blob written verbatim to the output file.
    Bin,
    /// C source: a byte array (and `lv_image_dsc_t` for the LVGL path).
    C,
    /// Rust source: a `[u8; N]` static (and dimension constants for LVGL).
    Rust,
}

/// Sanitize an arbitrary string into a valid C / Rust identifier.
///
/// Non-alphanumeric characters become `_`; a leading digit is prefixed with
/// `_`. An empty result falls back to `image`.
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
}

/// Format a byte slice as `0xNN, ` groups, 16 per line, each line indented by
/// `indent` spaces. No trailing newline.
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
}

/// Emit an opaque blob as a C or Rust byte array.
///
/// C emits `const uint8_t <name>[N]` plus `const size_t <name>_len`; Rust
/// emits `pub static <NAME>: [u8; N]` plus `pub const <NAME>_LEN: usize`.
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"),
    }
}

/// Emit an LVGL image as a compiled-in C `lv_image_dsc_t` (+ pixel map) or a
/// Rust pixel-map array with dimension/format constants.
///
/// `map` is the uncompressed pixel-data section in LVGL stored byte order.
#[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,")); // 2px * 2 bytes
        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;")); // ARGB8888
        assert!(s.contains("pub const ICON_STRIDE: u16 = 8;")); // 2px * 4 bytes
        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,")); // A8: width bytes
    }
}