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}