Skip to main content

rlvgl_core/
image.rs

1//! Image descriptor, blit option, and cache-handle primitives.
2//!
3//! These types describe decoded or embedded image pixel data without tying the
4//! core crate to filesystem loading, eviction policy, or a concrete renderer
5//! implementation.
6
7use alloc::vec::Vec;
8
9use crate::widget::{Color, Rect};
10
11/// Pixel layout used by an [`ImageDescriptor`].
12///
13/// This enum is a cross-phase contract between image sources, blit paths,
14/// display drivers, and caches. New variants require a standards action.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub enum PixelFormat {
17    /// 16-bit RGB in 5-6-5 channel layout.
18    Rgb565,
19    /// 32-bit alpha, red, green, blue channel layout.
20    Argb8888,
21    /// 8-bit luminance / grayscale pixels.
22    L8,
23}
24
25impl PixelFormat {
26    /// Return the number of bytes required for one tightly packed pixel.
27    pub const fn bytes_per_pixel(self) -> u32 {
28        match self {
29            PixelFormat::Rgb565 => 2,
30            PixelFormat::Argb8888 => 4,
31            PixelFormat::L8 => 1,
32        }
33    }
34}
35
36/// Pixel storage owned by or borrowed by an [`ImageDescriptor`].
37///
38/// The enum is non-exhaustive so future asset-handle sources can be added
39/// without breaking callers. Consumers should include a wildcard arm when
40/// matching.
41///
42/// # LPAR-09 addition
43///
44/// The [`ImageData::Asset`] variant bridges source-backed images (registered
45/// with an `AssetRegistry`) into the image pipeline. Decoded pixels may or
46/// may not be resident in the cache; callers MUST call
47/// `AssetRegistry::resolve_image` before blitting to ensure decoded data is
48/// present.
49///
50/// # Match exhaustiveness
51///
52/// Because `ImageData` is `#[non_exhaustive]`, external `match` expressions
53/// already require a wildcard arm. The addition of `Asset` is non-breaking.
54#[derive(Debug, Clone, PartialEq, Eq)]
55#[non_exhaustive]
56pub enum ImageData<'a> {
57    /// Borrowed encoded pixel bytes in the descriptor's [`PixelFormat`].
58    Borrowed(&'a [u8]),
59    /// Borrowed [`Color`] pixels used as a safe zero-copy ARGB8888 bridge.
60    ///
61    /// This avoids casting `Color` values to raw bytes; consumers that need
62    /// packed bytes can serialize each color with [`Color::to_argb8888`].
63    BorrowedColors(&'a [Color]),
64    /// Heap-resident encoded pixel bytes in the descriptor's [`PixelFormat`].
65    Owned(Vec<u8>),
66    /// Source-backed image handle registered with an `AssetRegistry`.
67    ///
68    /// The decoded pixels may or may not be resident in the `SlotCache` at
69    /// any moment. Callers MUST call `AssetRegistry::resolve_image(handle)` to
70    /// obtain a descriptor with in-memory pixel data before blitting.
71    ///
72    /// Added by LPAR-09 (Standards Action; LPAR-08 ยง15 amendment filed
73    /// 2026-06-12). Ownership of the `AssetPath` behind this handle lives in
74    /// the `AssetRegistry`, enabling reload after cache eviction.
75    #[cfg(feature = "fs")]
76    Asset(crate::asset::AssetHandle),
77}
78
79impl<'a> ImageData<'a> {
80    /// Return the encoded byte length represented by this data source.
81    ///
82    /// [`ImageData::BorrowedColors`] reports `colors.len() * 4` because each
83    /// color is represented as one ARGB8888 pixel.
84    ///
85    /// [`ImageData::Asset`] returns `0` because decoded pixels are not
86    /// in-memory until `AssetRegistry::resolve_image` is called. This
87    /// follows LVGL's model where an unresolved source has zero in-memory size.
88    pub fn byte_len(&self) -> usize {
89        match self {
90            ImageData::Borrowed(bytes) => bytes.len(),
91            ImageData::BorrowedColors(colors) => {
92                colors.len() * PixelFormat::Argb8888.bytes_per_pixel() as usize
93            }
94            ImageData::Owned(bytes) => bytes.len(),
95            #[cfg(feature = "fs")]
96            ImageData::Asset(_) => 0,
97            // `ImageData` is #[non_exhaustive]; this arm satisfies the compiler
98            // when future variants are added outside the `fs` feature scope.
99            #[allow(unreachable_patterns)]
100            _ => 0,
101        }
102    }
103
104    /// Return `true` when this data source contains no pixels.
105    pub fn is_empty(&self) -> bool {
106        self.byte_len() == 0
107    }
108
109    /// Return borrowed encoded bytes when this source is byte-addressable.
110    ///
111    /// [`ImageData::BorrowedColors`] and [`ImageData::Asset`] return `None`
112    /// because neither exposes in-memory bytes directly.
113    pub fn as_bytes(&self) -> Option<&[u8]> {
114        match self {
115            ImageData::Borrowed(bytes) => Some(bytes),
116            ImageData::Owned(bytes) => Some(bytes),
117            ImageData::BorrowedColors(_) => None,
118            #[cfg(feature = "fs")]
119            ImageData::Asset(_) => None,
120            #[allow(unreachable_patterns)]
121            _ => None,
122        }
123    }
124
125    /// Return borrowed color pixels when this source is the safe color bridge.
126    pub fn as_color_slice(&self) -> Option<&[Color]> {
127        match self {
128            ImageData::BorrowedColors(colors) => Some(colors),
129            ImageData::Borrowed(_) | ImageData::Owned(_) => None,
130            #[cfg(feature = "fs")]
131            ImageData::Asset(_) => None,
132            #[allow(unreachable_patterns)]
133            _ => None,
134        }
135    }
136}
137
138/// Description of an image's dimensions, format, storage, and row stride.
139#[derive(Debug, Clone, PartialEq, Eq)]
140pub struct ImageDescriptor<'a> {
141    /// Pixel layout used by the image data.
142    pub format: PixelFormat,
143    /// Width in pixels.
144    pub width: u16,
145    /// Height in pixels.
146    pub height: u16,
147    /// Pixel bytes or safe borrowed color pixels.
148    pub data: ImageData<'a>,
149    /// Bytes per row; `None` means tightly packed rows.
150    pub stride: Option<u32>,
151}
152
153impl<'a> ImageDescriptor<'a> {
154    /// Create an image descriptor from explicit parts.
155    pub const fn new(
156        format: PixelFormat,
157        width: u16,
158        height: u16,
159        data: ImageData<'a>,
160        stride: Option<u32>,
161    ) -> Self {
162        Self {
163            format,
164            width,
165            height,
166            data,
167            stride,
168        }
169    }
170
171    /// Create a tightly packed descriptor over borrowed encoded pixel bytes.
172    pub const fn borrowed(format: PixelFormat, width: u16, height: u16, data: &'a [u8]) -> Self {
173        Self::new(format, width, height, ImageData::Borrowed(data), None)
174    }
175
176    /// Create a tightly packed descriptor over owned encoded pixel bytes.
177    pub fn owned(format: PixelFormat, width: u16, height: u16, data: Vec<u8>) -> Self {
178        Self::new(format, width, height, ImageData::Owned(data), None)
179    }
180
181    /// Create a zero-copy ARGB8888 descriptor over borrowed [`Color`] pixels.
182    ///
183    /// The descriptor uses [`ImageData::BorrowedColors`] instead of exposing
184    /// raw bytes because `Color` does not define a stable memory layout. The
185    /// effective representation is still ARGB8888: consumers serialize each
186    /// color through [`Color::to_argb8888`] when byte output is needed.
187    pub const fn from_color_slice(pixels: &'a [Color], width: u16, height: u16) -> Self {
188        Self::new(
189            PixelFormat::Argb8888,
190            width,
191            height,
192            ImageData::BorrowedColors(pixels),
193            None,
194        )
195    }
196
197    /// Return `(width, height)` in pixels.
198    pub const fn dimensions(&self) -> (u16, u16) {
199        (self.width, self.height)
200    }
201
202    /// Return the bytes per row for tightly packed rows.
203    pub const fn tightly_packed_stride(&self) -> u32 {
204        self.width as u32 * self.format.bytes_per_pixel()
205    }
206
207    /// Return the effective bytes per row.
208    pub fn stride_bytes(&self) -> u32 {
209        match self.stride {
210            Some(stride) => stride,
211            None => self.tightly_packed_stride(),
212        }
213    }
214
215    /// Return `true` when rows are tightly packed with no padding bytes.
216    pub fn is_tightly_packed(&self) -> bool {
217        match self.stride {
218            Some(stride) => stride == self.tightly_packed_stride(),
219            None => true,
220        }
221    }
222}
223
224/// Options applied when blitting an [`ImageDescriptor`].
225#[derive(Debug, Clone, Copy, PartialEq, Eq)]
226pub struct BlitOpts {
227    /// Optional tint color applied before the image is written.
228    pub recolor: Option<Color>,
229    /// Tint blend factor where `0` disables tint and `255` applies full tint.
230    pub recolor_alpha: u8,
231    /// Horizontal scale as fixed point where `256` is `1.0x`.
232    pub scale_x: u16,
233    /// Vertical scale as fixed point where `256` is `1.0x`.
234    pub scale_y: u16,
235    /// Clockwise rotation in degrees; `0` disables rotation.
236    pub rotation_deg: i16,
237    /// Pivot offset from the destination rectangle's top-left corner.
238    pub pivot: (i16, i16),
239    /// Additional local clip rectangle.
240    pub clip: Option<Rect>,
241}
242
243impl Default for BlitOpts {
244    fn default() -> Self {
245        Self {
246            recolor: None,
247            recolor_alpha: 0,
248            scale_x: 256,
249            scale_y: 256,
250            rotation_deg: 0,
251            pivot: (0, 0),
252            clip: None,
253        }
254    }
255}
256
257/// Opaque token identifying an image registered with an [`ImageCache`].
258#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
259pub struct CacheHandle(u32);
260
261impl CacheHandle {
262    /// Create a cache handle from a raw token supplied by a cache.
263    pub const fn new(raw: u32) -> Self {
264        Self(raw)
265    }
266
267    /// Return the raw token value.
268    pub const fn as_u32(self) -> u32 {
269        self.0
270    }
271}
272
273/// Minimal image-cache access contract.
274///
275/// LPAR-08 defines handle registration and lookup. Concrete storage limits,
276/// eviction policy, and asset reload behavior are owned by later image-source
277/// phases.
278pub trait ImageCache<'a> {
279    /// Return the descriptor associated with `handle`, if it is still cached.
280    fn get(&self, handle: CacheHandle) -> Option<&ImageDescriptor<'a>>;
281
282    /// Register `descriptor` with the cache and return its handle.
283    fn put(&mut self, descriptor: ImageDescriptor<'a>) -> CacheHandle;
284}