1use std::borrow::Cow;
2#[cfg(feature = "image")]
3use std::path::Path;
4
5#[cfg(feature = "image")]
6use image::{DynamicImage, RgbImage, RgbaImage};
7
8use crate::{
9 color::{rgb_to_xyz, xyz_to_lab, Lab, D65},
10 image::{error::ImageError, Pixel, RGBA_CHANNELS},
11 math::normalize,
12 Filter,
13 FloatNumber,
14 ImageResult,
15};
16
17#[derive(Debug)]
41pub struct ImageData<'a> {
42 width: u32,
43 height: u32,
44 data: Cow<'a, [u8]>,
45}
46
47impl<'a> ImageData<'a> {
48 pub fn new(width: u32, height: u32, data: &'a [u8]) -> ImageResult<Self> {
61 let expected_length = (width * height) as usize * RGBA_CHANNELS;
62 if data.len() != expected_length {
63 return Err(ImageError::UnexpectedLength {
64 expected: expected_length,
65 actual: data.len(),
66 });
67 }
68
69 Ok(Self {
70 width,
71 height,
72 data: Cow::Borrowed(data),
73 })
74 }
75
76 #[cfg(feature = "image")]
89 pub fn load<P>(path: P) -> ImageResult<Self>
90 where
91 P: AsRef<Path>,
92 {
93 let image = image::open(path).map_err(ImageError::from)?;
94 Self::try_from(&image)
95 }
96
97 #[must_use]
102 pub fn is_empty(&self) -> bool {
103 self.data.is_empty()
104 }
105
106 #[inline]
111 #[must_use]
112 pub fn width(&self) -> u32 {
113 self.width
114 }
115
116 #[inline]
121 #[must_use]
122 pub fn height(&self) -> u32 {
123 self.height
124 }
125
126 #[must_use]
131 pub fn area(&self) -> usize {
132 self.width as usize * self.height as usize
133 }
134
135 #[must_use]
142 pub fn data(&self) -> &[u8] {
143 &self.data
144 }
145
146 #[allow(dead_code)]
151 pub(crate) fn pixels<'b, T>(&'b self) -> impl Iterator<Item = Pixel<T>> + 'b
152 where
153 T: FloatNumber + 'b,
154 {
155 self.data
156 .chunks_exact(RGBA_CHANNELS)
157 .enumerate()
158 .map(move |(index, rgba)| self.chunk_to_pixel(index, rgba))
159 }
160
161 pub(crate) fn pixels_with_filter<'b, T, F>(
173 &'b self,
174 filter: &'b F,
175 ) -> impl Iterator<Item = (Pixel<T>, bool)> + 'b
176 where
177 T: FloatNumber + 'b,
178 F: Filter,
179 {
180 self.data
181 .chunks_exact(RGBA_CHANNELS)
182 .enumerate()
183 .map(move |(index, chunk)| {
184 (
185 self.chunk_to_pixel::<T>(index, chunk),
186 filter.test(&[chunk[0], chunk[1], chunk[2], chunk[3]]),
187 )
188 })
189 }
190
191 #[inline(always)]
203 #[must_use]
204 fn chunk_to_pixel<T>(&self, index: usize, chunk: &[u8]) -> Pixel<T>
205 where
206 T: FloatNumber,
207 {
208 let (x, y, z) = rgb_to_xyz::<T>(chunk[0], chunk[1], chunk[2]);
209 let (l, a, b) = xyz_to_lab::<T, D65>(x, y, z);
210
211 let coord_x = T::from_usize((index % self.width as usize) + 1);
212 let coord_y = T::from_usize((index / self.width as usize) + 1);
213
214 let width_f = T::from_u32(self.width);
215 let height_f = T::from_u32(self.height);
216
217 [
218 Lab::<T>::normalize_l(l),
219 Lab::<T>::normalize_a(a),
220 Lab::<T>::normalize_b(b),
221 normalize(coord_x, T::zero(), width_f),
222 normalize(coord_y, T::zero(), height_f),
223 ]
224 }
225}
226
227#[cfg(feature = "image")]
228impl TryFrom<&DynamicImage> for ImageData<'_> {
229 type Error = ImageError;
230
231 fn try_from(image: &DynamicImage) -> Result<Self, Self::Error> {
232 match image {
233 DynamicImage::ImageRgb8(image) => Ok(Self::from(image)),
234 DynamicImage::ImageRgba8(image) => Ok(Self::from(image)),
235 _ => Err(ImageError::UnsupportedFormat),
236 }
237 }
238}
239
240#[cfg(feature = "image")]
241impl From<&RgbImage> for ImageData<'_> {
242 fn from(image: &RgbImage) -> Self {
243 let (width, height) = image.dimensions();
244 let size = (width * height) as usize;
245 let data = image.pixels().fold(
246 Vec::with_capacity(size * RGBA_CHANNELS),
247 |mut pixels, pixel| {
248 pixels.extend_from_slice(&[pixel[0], pixel[1], pixel[2], 255]);
249 pixels
250 },
251 );
252 Self {
253 width,
254 height,
255 data: data.into(),
256 }
257 }
258}
259
260#[cfg(feature = "image")]
261impl From<&RgbaImage> for ImageData<'_> {
262 fn from(image: &RgbaImage) -> Self {
263 let (width, height) = image.dimensions();
264 let data = image.to_vec();
265 Self {
266 width,
267 height,
268 data: data.into(),
269 }
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276 use crate::{assert_approx_eq, Rgba};
277
278 #[test]
279 fn test_new() {
280 let pixels = [
282 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, ];
287
288 let actual = ImageData::new(2, 2, &pixels);
290
291 assert!(actual.is_ok());
293
294 let image_data = actual.unwrap();
295 assert!(!image_data.is_empty());
296 assert_eq!(image_data.width(), 2);
297 assert_eq!(image_data.height(), 2);
298 assert_eq!(image_data.area(), 4);
299 assert_eq!(image_data.data(), &pixels);
300 }
301
302 #[test]
303 fn test_new_empty_data() {
304 let pixels = [];
306
307 let actual = ImageData::new(0, 0, &pixels);
309
310 assert!(actual.is_ok());
312
313 let image_data = actual.unwrap();
314 assert!(image_data.is_empty());
315 assert_eq!(image_data.width(), 0);
316 assert_eq!(image_data.height(), 0);
317 assert_eq!(image_data.area(), 0);
318 assert_eq!(image_data.data(), &pixels);
319 }
320
321 #[test]
322 fn test_new_unexpected_length() {
323 let pixels = [255, 255, 255, 255];
325
326 let actual = ImageData::new(2, 2, &pixels);
328
329 assert!(actual.is_err());
331
332 let error = actual.unwrap_err();
333 assert_eq!(
334 error.to_string(),
335 "Unexpected data length - expected 16, got 4"
336 );
337 }
338
339 #[cfg(feature = "image")]
340 #[test]
341 fn test_load_supported_format() {
342 let actual = ImageData::load("../../gfx/parrots_rgba8.png");
344
345 assert!(actual.is_ok());
347
348 let image_data = actual.unwrap();
349 assert!(!image_data.is_empty());
350 assert_eq!(image_data.width(), 150);
351 assert_eq!(image_data.height(), 150);
352 assert_eq!(image_data.area(), 150 * 150);
353 assert_eq!(image_data.data().len(), 150 * 150 * 4);
354 }
355
356 #[cfg(feature = "image")]
357 #[test]
358 fn test_load_unsupported_format() {
359 let actual = ImageData::load("../../gfx/parrots_la16.png");
361
362 assert!(actual.is_err());
364
365 let error = actual.unwrap_err();
366 assert_eq!(error.to_string(), "Unsupported image format or color type");
367 }
368
369 #[cfg(all(feature = "image", not(target_os = "windows")))]
370 #[test]
371 fn test_load_unknown_path() {
372 let actual = ImageData::load("../../gfx/unknown.png");
374
375 assert!(actual.is_err());
377
378 let error = actual.unwrap_err();
379 assert_eq!(
380 error.to_string(),
381 "Failed to load image from file: No such file or directory (os error 2)"
382 );
383 }
384
385 #[cfg(all(feature = "image", target_os = "windows"))]
386 #[test]
387 fn test_load_unknown_path_windows() {
388 let actual = ImageData::load("../../gfx/unknown.png");
390
391 assert!(actual.is_err());
393
394 let error = actual.unwrap_err();
395 assert_eq!(
396 error.to_string(),
397 "Failed to load image from file: The system cannot find the file specified. (os error 2)"
398 );
399 }
400
401 #[cfg(all(feature = "image", not(target_os = "windows")))]
402 #[test]
403 fn test_load_invalid_file() {
404 let actual = ImageData::load("../../gfx/colors/invalid.jpg");
406
407 assert!(actual.is_err());
409
410 let error = actual.unwrap_err();
411 assert_eq!(
412 error.to_string(),
413 "Failed to load image from file: No such file or directory (os error 2)"
414 );
415 }
416
417 #[cfg(all(feature = "image", target_os = "windows"))]
418 #[test]
419 fn test_load_invalid_file_windows() {
420 let actual = ImageData::load("../../gfx/colors/invalid.jpg");
422
423 assert!(actual.is_err());
425
426 let error = actual.unwrap_err();
427 assert_eq!(
428 error.to_string(),
429 "Failed to load image from file: The system cannot find the file specified. (os error 2)"
430 );
431 }
432
433 #[test]
434 fn test_pixels_iter() {
435 let pixels = [
437 255, 0, 0, 255, 0, 0, 0, 0, 255, 255, 0, 255, 0, 0, 0, 0, ];
442 let image_data = ImageData::new(2, 2, &pixels).unwrap();
443
444 let actual: Vec<_> = image_data.pixels::<f64>().collect();
446
447 assert_eq!(actual.len(), 4);
449
450 let pixel = actual[0];
451 assert_approx_eq!(pixel[0], 0.532371);
452 assert_approx_eq!(pixel[1], 0.816032);
453 assert_approx_eq!(pixel[2], 0.765488);
454 assert_approx_eq!(pixel[3], 0.5);
455 assert_approx_eq!(pixel[4], 0.5);
456
457 let pixel = actual[1];
458 assert_approx_eq!(pixel[0], 0.0);
459 assert_approx_eq!(pixel[1], 0.501960);
460 assert_approx_eq!(pixel[2], 0.501960);
461 assert_approx_eq!(pixel[3], 1.0);
462 assert_approx_eq!(pixel[4], 0.5);
463
464 let pixel = actual[2];
465 assert_approx_eq!(pixel[0], 0.971385);
466 assert_approx_eq!(pixel[1], 0.417402);
467 assert_approx_eq!(pixel[2], 0.872457);
468 assert_approx_eq!(pixel[3], 0.5);
469 assert_approx_eq!(pixel[4], 1.0);
470
471 let pixel = actual[3];
472 assert_approx_eq!(pixel[0], 0.0);
473 assert_approx_eq!(pixel[1], 0.501960);
474 assert_approx_eq!(pixel[2], 0.501960);
475 assert_approx_eq!(pixel[3], 1.0);
476 assert_approx_eq!(pixel[4], 1.0);
477 }
478
479 #[test]
480 fn test_pixels_with_filter() {
481 let data = [
483 255, 0, 0, 255, 0, 0, 0, 0, 255, 255, 0, 255, 0, 0, 0, 0, ];
488 let image_data = ImageData::new(2, 2, &data).unwrap();
489
490 let (pixels, mask) = image_data
492 .pixels_with_filter::<f64, _>(&|rgba: &Rgba| rgba[3] != 0)
493 .fold(
494 (Vec::new(), Vec::new()),
495 |(mut pixels, mut mask), (pixel, m)| {
496 pixels.push(pixel);
497 mask.push(m);
498 (pixels, mask)
499 },
500 );
501
502 assert_eq!(pixels.len(), 4);
504 assert_eq!(mask.len(), 4);
505 assert_eq!(mask, vec![true, false, true, false]);
506 }
507}