1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
// SPDX-FileCopyrightText: Copyright (c) 2026 Mike Li/Mikewolfli/Wei Li(mikewolfli@163.com)
// SPDX-License-Identifier: MIT
//! High-level image wrapper with decode, encode, and transform capabilities.
use crate::image::format::{DecodedImage, ImageData};
use crate::image::{decoder, encoder, transform};
/// High-level image wrapper with decode, encode, and transform capabilities.
#[derive(Debug, Clone)]
pub struct Image {
/// The underlying decoded image data.
pub inner: DecodedImage,
}
impl Image {
/// Creates an empty image for widgets that have no source yet.
pub fn new() -> Self {
Self::from_raw(ImageData::Rgba8(Vec::new()), 0, 0)
}
/// Creates an image from validated raw RGBA8 pixels, refusing a size mismatch.
///
/// This compatibility constructor preserves the historical widget-image API while storing
/// pixels in the canonical `ImageData` representation.
///
/// # Why `Result` rather than a panic
///
/// It used to `assert_eq!` the length against `width * height * 4`. That is a programming
/// error in the *caller's* arithmetic, not an invariant of this type, and a public constructor
/// has no business aborting the host process over it — a decoder that produced a short buffer,
/// or a caller who computed the size with saturating arithmetic, would take the whole
/// application down. `Err` names the mismatch; `expect` at a call site that knows its own
/// buffer is right is one line and reads better than an assertion buried in a library.
pub fn try_from_rgba(data: Vec<u8>, width: u32, height: u32) -> Result<Self, String> {
let expected = width as usize * height as usize * 4;
if data.len() != expected {
return Err(format!(
"Image::try_from_rgba: data length {} does not match {width}x{height} RGBA \
(expected {expected})",
data.len()
));
}
Ok(Self::from_raw(ImageData::Rgba8(data), width, height))
}
/// Decode an image from raw bytes. Auto-detects format.
pub fn from_bytes(data: &[u8]) -> Result<Self, String> {
let decoded = decoder::decode(data)?;
Ok(Self { inner: decoded })
}
/// Decode RGBA8 from bytes without consulting the decode cache.
///
/// This is the *private decode* entry point: a caller that wants its own copy (loading a
/// series of frames, say) gets one. A caller that expects to share a decode **must** use
/// [`crate::image::cache::pixels_rgba8_or_none`], which returns the cached `Arc<Vec<u8>>`
/// directly — the controls' draw paths all go through it.
///
/// There used to be a `from_bytes_rgba8_shared` beside this one. It went to the cache and
/// then copied the pixels into an owning `Image`, so it saved the decode but performed the
/// very copy its name promised to avoid, and nothing in the crate ever called it. An owner
/// that needs both can write
/// `Image::from_raw(ImageData::Rgba8(pixels.to_vec()), w, h)` against the cache entry, which
/// at least says out loud that a copy is happening.
pub fn from_bytes_rgba8(data: &[u8]) -> Result<Self, String> {
let decoded = decoder::decode_to_rgba8(data)?;
Ok(Self { inner: decoded })
}
/// Create from a DecodedImage.
pub fn from_decoded(decoded: DecodedImage) -> Self {
Self { inner: decoded }
}
/// Create from raw pixel data.
pub fn from_raw(data: ImageData, width: u32, height: u32) -> Self {
Self {
inner: DecodedImage::new(crate::image::format::ImageFormat::Rgba8, data, width, height),
}
}
/// Image width in pixels.
pub fn width(&self) -> u32 {
self.inner.width
}
/// Image height in pixels.
pub fn height(&self) -> u32 {
self.inner.height
}
/// Image format.
pub fn format(&self) -> crate::image::format::ImageFormat {
self.inner.format
}
/// Reference to pixel data.
pub fn data(&self) -> &ImageData {
&self.inner.data
}
/// Returns RGBA8 bytes when the underlying image is already RGBA8.
pub fn rgba8_data(&self) -> Option<&[u8]> {
match &self.inner.data {
ImageData::Rgba8(data) => Some(data),
_ => None,
}
}
/// Returns whether the image has no pixels.
pub fn is_empty(&self) -> bool {
self.inner.data.as_bytes().is_empty()
}
/// Loads an image from a file path.
pub fn from_file(path: &str) -> Result<Self, String> {
let data = std::fs::read(path).map_err(|error| {
format!(
"image file '{path}' could not be read: {error} (check that the path exists \
and is readable)"
)
})?;
Self::from_bytes(&data)
}
/// Returns RGBA8 pixel data, converting if needed.
pub fn to_rgba8(&self) -> ImageData {
self.inner.as_rgba8()
}
/// Encode the image to bytes in the specified format.
pub fn encode(&self, format: crate::image::format::ImageFormat) -> Result<Vec<u8>, String> {
encoder::encode(&self.inner, format)
}
/// Resize the image to new dimensions.
pub fn resize(&self, width: u32, height: u32) -> Result<Self, String> {
let rgba = self.to_rgba8();
let resized = transform::resize(rgba, self.inner.width, self.inner.height, width, height)?;
Ok(Self::from_raw(resized, width, height))
}
/// Crop a rectangular region from the image.
pub fn crop(&self, x: u32, y: u32, width: u32, height: u32) -> Result<Self, String> {
let rgba = self.to_rgba8();
let cropped =
transform::crop(rgba, self.inner.width, self.inner.height, x, y, width, height)?;
Ok(Self::from_raw(cropped, width, height))
}
/// Rotate the image by 90, 180, or 270 degrees.
pub fn rotate(&self, degrees: u32) -> Result<Self, String> {
let rgba = self.to_rgba8();
let (rotated, w, h) =
transform::rotate(rgba, self.inner.width, self.inner.height, degrees)?;
Ok(Self::from_raw(rotated, w, h))
}
/// Flip the image horizontally (mirror).
pub fn flip_horizontal(&self) -> Result<Self, String> {
let rgba = self.to_rgba8();
let flipped = transform::flip_horizontal(rgba, self.inner.width, self.inner.height)?;
Ok(Self::from_raw(flipped, self.inner.width, self.inner.height))
}
/// Flip the image vertically.
pub fn flip_vertical(&self) -> Result<Self, String> {
let rgba = self.to_rgba8();
let flipped = transform::flip_vertical(rgba, self.inner.width, self.inner.height)?;
Ok(Self::from_raw(flipped, self.inner.width, self.inner.height))
}
}
crate::impl_default_via_new!(Image);
#[cfg(test)]
mod tests {
use super::*;
/// Helper: create a small test image (2×2 RGBA8 with known pixels).
fn test_image() -> Image {
let data = ImageData::Rgba8(vec![
255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 0, 255,
]);
Image::from_raw(data, 2, 2)
}
#[test]
fn image_from_raw() {
let img = test_image();
assert_eq!(img.width(), 2);
assert_eq!(img.height(), 2);
}
/// A buffer whose length disagrees with its declared size is an `Err`, not an abort.
///
/// The constructor used to `assert_eq!`, so a caller whose decoder produced a short buffer
/// took the host process down — from a library, over an arithmetic mistake the `Err` can
/// describe. This pins the refusal and that the message names the expected length, because a
/// bare "mismatch" would leave the caller to work it out.
#[test]
fn a_size_mismatch_is_refused_rather_than_panicking() {
let error = Image::try_from_rgba(vec![0u8; 3], 1, 1)
.expect_err("three bytes are not one RGBA pixel");
assert!(error.contains("expected 4"), "the error must name the expected length: {error}");
assert!(Image::try_from_rgba(vec![0u8; 4], 1, 1).is_ok());
}
#[test]
fn compatibility_rgba_constructor_exposes_canonical_pixels() {
let img = Image::try_from_rgba(vec![255, 0, 0, 255], 1, 1)
.expect("the buffer matches its declared size");
assert_eq!(img.format(), crate::image::format::ImageFormat::Rgba8);
assert_eq!(img.rgba8_data(), Some(&[255, 0, 0, 255][..]));
assert!(!img.is_empty());
}
#[test]
fn image_width_height() {
let img = Image::from_raw(ImageData::Rgba8(vec![0u8; 16 * 16 * 4]), 16, 16);
assert_eq!(img.width(), 16);
assert_eq!(img.height(), 16);
}
#[test]
fn image_to_rgba8_roundtrip() {
let img = test_image();
let rgba = img.to_rgba8();
assert_eq!(rgba.as_bytes().len(), 16); // 4 pixels * 4 bytes
}
#[test]
fn image_resize_maintains_aspect() {
let img = Image::from_raw(ImageData::Rgba8(vec![0u8; 100 * 100 * 4]), 100, 100);
let resized = img.resize(50, 50).unwrap();
assert_eq!(resized.width(), 50);
assert_eq!(resized.height(), 50);
}
#[test]
fn image_crop_returns_correct_size() {
let img = Image::from_raw(ImageData::Rgba8(vec![0u8; 100 * 100 * 4]), 100, 100);
let cropped = img.crop(10, 10, 20, 30).unwrap();
assert_eq!(cropped.width(), 20);
assert_eq!(cropped.height(), 30);
}
#[test]
fn image_flip_horizontal_works() {
let img = test_image();
let flipped = img.flip_horizontal().unwrap();
assert_eq!(flipped.width(), 2);
assert_eq!(flipped.height(), 2);
}
#[test]
fn image_flip_vertical_works() {
let img = test_image();
let flipped = img.flip_vertical().unwrap();
assert_eq!(flipped.width(), 2);
assert_eq!(flipped.height(), 2);
}
#[test]
fn image_crop_invalid_returns_err() {
let img = Image::from_raw(ImageData::Rgba8(vec![0u8; 10 * 10 * 4]), 10, 10);
assert!(img.crop(5, 5, 10, 10).is_err());
}
#[test]
fn image_resize_zero_returns_err() {
let img = Image::from_raw(ImageData::Rgba8(vec![0u8; 10 * 10 * 4]), 10, 10);
assert!(img.resize(0, 0).is_err());
}
}