1#![deny(unsafe_code)]
9#![warn(missing_docs)]
10
11use std::ops::{Index, IndexMut};
12
13#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
15pub enum ImageError {
16 #[error("image channel count must be greater than zero")]
18 ZeroChannels,
19 #[error("image dimensions overflow addressable memory")]
21 DimensionOverflow,
22 #[error("image storage is too short: need at least {required} elements, found {found}")]
24 StorageTooShort {
25 required: usize,
27 found: usize,
29 },
30 #[error("row stride {stride} is smaller than packed row width {minimum}")]
32 InvalidStride {
33 stride: usize,
35 minimum: usize,
37 },
38 #[error("plane stride {stride} is smaller than one plane span {minimum}")]
40 InvalidPlaneStride {
41 stride: usize,
43 minimum: usize,
45 },
46 #[error("color space {color_space:?} requires {expected} channels, image has {found}")]
48 MetadataChannelMismatch {
49 color_space: ColorSpace,
51 expected: usize,
53 found: usize,
55 },
56 #[error("region ({x}, {y}, {region_width}, {region_height}) exceeds image bounds {image_width}x{image_height}")]
58 InvalidRegion {
59 x: usize,
61 y: usize,
63 region_width: usize,
65 region_height: usize,
67 image_width: usize,
69 image_height: usize,
71 },
72 #[error("pixel ({x}, {y}) is outside image bounds {width}x{height}")]
74 OutOfBounds {
75 x: usize,
77 y: usize,
79 width: usize,
81 height: usize,
83 },
84}
85
86#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
88pub enum ImageLayout {
89 #[default]
91 Interleaved,
92 Planar,
94}
95
96#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
98pub enum ColorSpace {
99 #[default]
101 Unknown,
102 Gray,
104 Rgb,
106 Bgr,
108 Rgba,
110 Bgra,
112 LinearRgb,
114 Hsv,
116 Depth,
118 Label,
120}
121
122impl ColorSpace {
123 #[must_use]
125 pub const fn required_channels(self) -> Option<usize> {
126 match self {
127 Self::Unknown => None,
128 Self::Gray | Self::Depth | Self::Label => Some(1),
129 Self::Rgb | Self::Bgr | Self::LinearRgb | Self::Hsv => Some(3),
130 Self::Rgba | Self::Bgra => Some(4),
131 }
132 }
133}
134
135#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
137pub enum ColorRange {
138 #[default]
140 Unspecified,
141 Full,
143 Limited,
145}
146
147#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
149pub enum AlphaMode {
150 #[default]
152 None,
153 Straight,
155 Premultiplied,
157}
158
159#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
161pub struct ImageMetadata {
162 pub color_space: ColorSpace,
164 pub color_range: ColorRange,
166 pub alpha_mode: AlphaMode,
168}
169
170impl ImageMetadata {
171 pub fn validate<const CHANNELS: usize>(self) -> Result<(), ImageError> {
173 if CHANNELS == 0 {
174 return Err(ImageError::ZeroChannels);
175 }
176 if let Some(expected) = self.color_space.required_channels() {
177 if expected != CHANNELS {
178 return Err(ImageError::MetadataChannelMismatch {
179 color_space: self.color_space,
180 expected,
181 found: CHANNELS,
182 });
183 }
184 }
185 Ok(())
186 }
187}
188
189#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
191pub struct ImageRegion {
192 pub x: usize,
194 pub y: usize,
196 pub width: usize,
198 pub height: usize,
200}
201
202impl ImageRegion {
203 #[must_use]
205 pub const fn new(x: usize, y: usize, width: usize, height: usize) -> Self {
206 Self { x, y, width, height }
207 }
208
209 fn validate(self, image_width: usize, image_height: usize) -> Result<(), ImageError> {
210 let end_x = self.x.checked_add(self.width).ok_or(ImageError::DimensionOverflow)?;
211 let end_y = self.y.checked_add(self.height).ok_or(ImageError::DimensionOverflow)?;
212 if end_x > image_width || end_y > image_height {
213 return Err(ImageError::InvalidRegion {
214 x: self.x,
215 y: self.y,
216 region_width: self.width,
217 region_height: self.height,
218 image_width,
219 image_height,
220 });
221 }
222 Ok(())
223 }
224}
225
226fn packed_len<const CHANNELS: usize>(width: usize, height: usize) -> Result<usize, ImageError> {
227 if CHANNELS == 0 {
228 return Err(ImageError::ZeroChannels);
229 }
230 width
231 .checked_mul(height)
232 .and_then(|value| value.checked_mul(CHANNELS))
233 .ok_or(ImageError::DimensionOverflow)
234}
235
236fn strided_span(height: usize, row_stride: usize, packed_row: usize) -> Result<usize, ImageError> {
237 if height == 0 {
238 return Ok(0);
239 }
240 row_stride
241 .checked_mul(height - 1)
242 .and_then(|offset| offset.checked_add(packed_row))
243 .ok_or(ImageError::DimensionOverflow)
244}
245
246#[derive(Clone, Debug, PartialEq, Eq)]
248pub struct Image<T, const CHANNELS: usize> {
249 width: usize,
250 height: usize,
251 data: Vec<T>,
252 metadata: ImageMetadata,
253}
254
255impl<T, const CHANNELS: usize> Image<T, CHANNELS> {
256 pub fn try_new(width: usize, height: usize, data: Vec<T>) -> Result<Self, ImageError> {
258 let required = packed_len::<CHANNELS>(width, height)?;
259 if data.len() != required {
260 return Err(ImageError::StorageTooShort { required, found: data.len() });
261 }
262 Ok(Self { width, height, data, metadata: ImageMetadata::default() })
263 }
264
265 pub fn try_new_with_metadata(
267 width: usize,
268 height: usize,
269 data: Vec<T>,
270 metadata: ImageMetadata,
271 ) -> Result<Self, ImageError> {
272 metadata.validate::<CHANNELS>()?;
273 let mut image = Self::try_new(width, height, data)?;
274 image.metadata = metadata;
275 Ok(image)
276 }
277
278 #[must_use]
280 pub const fn width(&self) -> usize {
281 self.width
282 }
283
284 #[must_use]
286 pub const fn height(&self) -> usize {
287 self.height
288 }
289
290 #[must_use]
292 pub const fn row_stride(&self) -> usize {
293 self.width * CHANNELS
294 }
295
296 #[must_use]
298 pub const fn layout(&self) -> ImageLayout {
299 ImageLayout::Interleaved
300 }
301
302 #[must_use]
304 pub const fn metadata(&self) -> ImageMetadata {
305 self.metadata
306 }
307
308 pub fn set_metadata(&mut self, metadata: ImageMetadata) -> Result<(), ImageError> {
310 metadata.validate::<CHANNELS>()?;
311 self.metadata = metadata;
312 Ok(())
313 }
314
315 #[must_use]
317 pub fn as_slice(&self) -> &[T] {
318 &self.data
319 }
320
321 #[must_use]
323 pub fn as_mut_slice(&mut self) -> &mut [T] {
324 &mut self.data
325 }
326
327 #[must_use]
329 pub fn view(&self) -> ImageView<'_, T, CHANNELS> {
330 ImageView {
331 width: self.width,
332 height: self.height,
333 row_stride: self.row_stride(),
334 data: &self.data,
335 metadata: self.metadata,
336 }
337 }
338
339 #[must_use]
341 pub fn view_mut(&mut self) -> ImageViewMut<'_, T, CHANNELS> {
342 ImageViewMut {
343 width: self.width,
344 height: self.height,
345 row_stride: self.width * CHANNELS,
346 data: &mut self.data,
347 metadata: self.metadata,
348 }
349 }
350
351 #[must_use]
353 pub fn get(&self, x: usize, y: usize) -> Option<&[T; CHANNELS]> {
354 self.view().get(x, y)
355 }
356
357 #[must_use]
359 pub fn get_mut(&mut self, x: usize, y: usize) -> Option<&mut [T; CHANNELS]> {
360 if x >= self.width || y >= self.height {
361 return None;
362 }
363 let offset = (y * self.width + x) * CHANNELS;
364 self.data.get_mut(offset..offset + CHANNELS)?.try_into().ok()
365 }
366
367 #[must_use]
369 pub fn into_vec(self) -> Vec<T> {
370 self.data
371 }
372}
373
374impl<T: Clone, const CHANNELS: usize> Image<T, CHANNELS> {
375 pub fn from_pixel(
377 width: usize,
378 height: usize,
379 pixel: [T; CHANNELS],
380 ) -> Result<Self, ImageError> {
381 let required = packed_len::<CHANNELS>(width, height)?;
382 let mut data = Vec::with_capacity(required);
383 for _ in 0..width.saturating_mul(height) {
384 data.extend_from_slice(&pixel);
385 }
386 Self::try_new(width, height, data)
387 }
388}
389
390impl<T, const CHANNELS: usize> Index<(usize, usize)> for Image<T, CHANNELS> {
391 type Output = [T; CHANNELS];
392
393 fn index(&self, (x, y): (usize, usize)) -> &Self::Output {
394 self.get(x, y).expect("image index out of bounds")
395 }
396}
397
398impl<T, const CHANNELS: usize> IndexMut<(usize, usize)> for Image<T, CHANNELS> {
399 fn index_mut(&mut self, (x, y): (usize, usize)) -> &mut Self::Output {
400 self.get_mut(x, y).expect("image index out of bounds")
401 }
402}
403
404#[derive(Clone, Copy, Debug)]
406pub struct ImageView<'a, T, const CHANNELS: usize> {
407 width: usize,
408 height: usize,
409 row_stride: usize,
410 data: &'a [T],
411 metadata: ImageMetadata,
412}
413
414impl<'a, T, const CHANNELS: usize> ImageView<'a, T, CHANNELS> {
415 pub fn new(
419 width: usize,
420 height: usize,
421 row_stride: usize,
422 data: &'a [T],
423 ) -> Result<Self, ImageError> {
424 Self::new_with_metadata(width, height, row_stride, data, ImageMetadata::default())
425 }
426
427 pub fn new_with_metadata(
429 width: usize,
430 height: usize,
431 row_stride: usize,
432 data: &'a [T],
433 metadata: ImageMetadata,
434 ) -> Result<Self, ImageError> {
435 metadata.validate::<CHANNELS>()?;
436 let packed_row = width.checked_mul(CHANNELS).ok_or(ImageError::DimensionOverflow)?;
437 if row_stride < packed_row {
438 return Err(ImageError::InvalidStride { stride: row_stride, minimum: packed_row });
439 }
440 let required = if height == 0 {
441 0
442 } else {
443 row_stride
444 .checked_mul(height - 1)
445 .and_then(|offset| offset.checked_add(packed_row))
446 .ok_or(ImageError::DimensionOverflow)?
447 };
448 if data.len() < required {
449 return Err(ImageError::StorageTooShort { required, found: data.len() });
450 }
451 Ok(Self { width, height, row_stride, data, metadata })
452 }
453
454 #[must_use]
456 pub const fn width(self) -> usize {
457 self.width
458 }
459
460 #[must_use]
462 pub const fn height(self) -> usize {
463 self.height
464 }
465
466 #[must_use]
468 pub const fn row_stride(self) -> usize {
469 self.row_stride
470 }
471
472 #[must_use]
474 pub const fn layout(self) -> ImageLayout {
475 ImageLayout::Interleaved
476 }
477
478 #[must_use]
480 pub const fn metadata(self) -> ImageMetadata {
481 self.metadata
482 }
483
484 #[must_use]
486 pub fn get(self, x: usize, y: usize) -> Option<&'a [T; CHANNELS]> {
487 if x >= self.width || y >= self.height {
488 return None;
489 }
490 let offset = y * self.row_stride + x * CHANNELS;
491 self.data.get(offset..offset + CHANNELS)?.try_into().ok()
492 }
493
494 #[must_use]
496 pub fn row(self, y: usize) -> Option<&'a [T]> {
497 if y >= self.height {
498 return None;
499 }
500 let start = y * self.row_stride;
501 self.data.get(start..start + self.width * CHANNELS)
502 }
503
504 pub fn subview(self, region: ImageRegion) -> Result<Self, ImageError> {
506 region.validate(self.width, self.height)?;
507 if region.width == 0 || region.height == 0 {
508 return Ok(Self {
509 width: region.width,
510 height: region.height,
511 row_stride: self.row_stride,
512 data: &self.data[..0],
513 metadata: self.metadata,
514 });
515 }
516 let start = region.y * self.row_stride + region.x * CHANNELS;
517 let span = (region.height - 1) * self.row_stride + region.width * CHANNELS;
518 Ok(Self {
519 width: region.width,
520 height: region.height,
521 row_stride: self.row_stride,
522 data: &self.data[start..start + span],
523 metadata: self.metadata,
524 })
525 }
526}
527
528#[derive(Debug)]
530pub struct ImageViewMut<'a, T, const CHANNELS: usize> {
531 width: usize,
532 height: usize,
533 row_stride: usize,
534 data: &'a mut [T],
535 metadata: ImageMetadata,
536}
537
538impl<'a, T, const CHANNELS: usize> ImageViewMut<'a, T, CHANNELS> {
539 pub fn new(
541 width: usize,
542 height: usize,
543 row_stride: usize,
544 data: &'a mut [T],
545 ) -> Result<Self, ImageError> {
546 Self::new_with_metadata(width, height, row_stride, data, ImageMetadata::default())
547 }
548
549 pub fn new_with_metadata(
551 width: usize,
552 height: usize,
553 row_stride: usize,
554 data: &'a mut [T],
555 metadata: ImageMetadata,
556 ) -> Result<Self, ImageError> {
557 metadata.validate::<CHANNELS>()?;
558 let packed_row = width.checked_mul(CHANNELS).ok_or(ImageError::DimensionOverflow)?;
559 if row_stride < packed_row {
560 return Err(ImageError::InvalidStride { stride: row_stride, minimum: packed_row });
561 }
562 let required = strided_span(height, row_stride, packed_row)?;
563 if data.len() < required {
564 return Err(ImageError::StorageTooShort { required, found: data.len() });
565 }
566 Ok(Self { width, height, row_stride, data, metadata })
567 }
568
569 #[must_use]
571 pub const fn width(&self) -> usize {
572 self.width
573 }
574
575 #[must_use]
577 pub const fn height(&self) -> usize {
578 self.height
579 }
580
581 #[must_use]
583 pub const fn row_stride(&self) -> usize {
584 self.row_stride
585 }
586
587 #[must_use]
589 pub const fn metadata(&self) -> ImageMetadata {
590 self.metadata
591 }
592
593 pub fn set_metadata(&mut self, metadata: ImageMetadata) -> Result<(), ImageError> {
595 metadata.validate::<CHANNELS>()?;
596 self.metadata = metadata;
597 Ok(())
598 }
599
600 #[must_use]
602 pub fn as_view(&self) -> ImageView<'_, T, CHANNELS> {
603 ImageView {
604 width: self.width,
605 height: self.height,
606 row_stride: self.row_stride,
607 data: self.data,
608 metadata: self.metadata,
609 }
610 }
611
612 #[must_use]
614 pub fn get(&self, x: usize, y: usize) -> Option<&[T; CHANNELS]> {
615 self.as_view().get(x, y)
616 }
617
618 #[must_use]
620 pub fn get_mut(&mut self, x: usize, y: usize) -> Option<&mut [T; CHANNELS]> {
621 if x >= self.width || y >= self.height {
622 return None;
623 }
624 let offset = y * self.row_stride + x * CHANNELS;
625 self.data.get_mut(offset..offset + CHANNELS)?.try_into().ok()
626 }
627
628 #[must_use]
630 pub fn row_mut(&mut self, y: usize) -> Option<&mut [T]> {
631 if y >= self.height {
632 return None;
633 }
634 let start = y * self.row_stride;
635 self.data.get_mut(start..start + self.width * CHANNELS)
636 }
637
638 #[must_use]
643 pub fn as_mut_slice(&mut self) -> &mut [T] {
644 self.data
645 }
646
647 pub fn subview(
649 &mut self,
650 region: ImageRegion,
651 ) -> Result<ImageViewMut<'_, T, CHANNELS>, ImageError> {
652 region.validate(self.width, self.height)?;
653 if region.width == 0 || region.height == 0 {
654 return Ok(ImageViewMut {
655 width: region.width,
656 height: region.height,
657 row_stride: self.row_stride,
658 data: &mut self.data[..0],
659 metadata: self.metadata,
660 });
661 }
662 let start = region.y * self.row_stride + region.x * CHANNELS;
663 let span = (region.height - 1) * self.row_stride + region.width * CHANNELS;
664 Ok(ImageViewMut {
665 width: region.width,
666 height: region.height,
667 row_stride: self.row_stride,
668 data: &mut self.data[start..start + span],
669 metadata: self.metadata,
670 })
671 }
672}
673
674#[derive(Clone, Debug, PartialEq, Eq)]
678pub struct PlanarImage<T, const CHANNELS: usize> {
679 width: usize,
680 height: usize,
681 data: Vec<T>,
682 metadata: ImageMetadata,
683}
684
685impl<T, const CHANNELS: usize> PlanarImage<T, CHANNELS> {
686 pub fn try_new(width: usize, height: usize, data: Vec<T>) -> Result<Self, ImageError> {
688 let required = packed_len::<CHANNELS>(width, height)?;
689 if data.len() != required {
690 return Err(ImageError::StorageTooShort { required, found: data.len() });
691 }
692 Ok(Self { width, height, data, metadata: ImageMetadata::default() })
693 }
694
695 pub fn try_new_with_metadata(
697 width: usize,
698 height: usize,
699 data: Vec<T>,
700 metadata: ImageMetadata,
701 ) -> Result<Self, ImageError> {
702 metadata.validate::<CHANNELS>()?;
703 let mut image = Self::try_new(width, height, data)?;
704 image.metadata = metadata;
705 Ok(image)
706 }
707
708 #[must_use]
710 pub const fn width(&self) -> usize {
711 self.width
712 }
713
714 #[must_use]
716 pub const fn height(&self) -> usize {
717 self.height
718 }
719
720 #[must_use]
722 pub const fn layout(&self) -> ImageLayout {
723 ImageLayout::Planar
724 }
725
726 #[must_use]
728 pub const fn row_stride(&self) -> usize {
729 self.width
730 }
731
732 #[must_use]
734 pub const fn plane_stride(&self) -> usize {
735 self.width * self.height
736 }
737
738 #[must_use]
740 pub const fn metadata(&self) -> ImageMetadata {
741 self.metadata
742 }
743
744 pub fn set_metadata(&mut self, metadata: ImageMetadata) -> Result<(), ImageError> {
746 metadata.validate::<CHANNELS>()?;
747 self.metadata = metadata;
748 Ok(())
749 }
750
751 #[must_use]
753 pub fn as_slice(&self) -> &[T] {
754 &self.data
755 }
756
757 #[must_use]
759 pub fn as_mut_slice(&mut self) -> &mut [T] {
760 &mut self.data
761 }
762
763 #[must_use]
765 pub fn view(&self) -> PlanarImageView<'_, T, CHANNELS> {
766 PlanarImageView {
767 width: self.width,
768 height: self.height,
769 row_stride: self.width,
770 plane_stride: self.width * self.height,
771 data: &self.data,
772 metadata: self.metadata,
773 }
774 }
775
776 #[must_use]
778 pub fn get(&self, channel: usize, x: usize, y: usize) -> Option<&T> {
779 self.view().get(channel, x, y)
780 }
781
782 #[must_use]
784 pub fn get_mut(&mut self, channel: usize, x: usize, y: usize) -> Option<&mut T> {
785 if channel >= CHANNELS || x >= self.width || y >= self.height {
786 return None;
787 }
788 let offset = channel * self.width * self.height + y * self.width + x;
789 self.data.get_mut(offset)
790 }
791
792 #[must_use]
794 pub fn into_vec(self) -> Vec<T> {
795 self.data
796 }
797}
798
799#[derive(Clone, Copy, Debug)]
801pub struct PlanarImageView<'a, T, const CHANNELS: usize> {
802 width: usize,
803 height: usize,
804 row_stride: usize,
805 plane_stride: usize,
806 data: &'a [T],
807 metadata: ImageMetadata,
808}
809
810impl<'a, T, const CHANNELS: usize> PlanarImageView<'a, T, CHANNELS> {
811 pub fn new(
813 width: usize,
814 height: usize,
815 row_stride: usize,
816 plane_stride: usize,
817 data: &'a [T],
818 ) -> Result<Self, ImageError> {
819 Self::new_with_metadata(
820 width,
821 height,
822 row_stride,
823 plane_stride,
824 data,
825 ImageMetadata::default(),
826 )
827 }
828
829 pub fn new_with_metadata(
831 width: usize,
832 height: usize,
833 row_stride: usize,
834 plane_stride: usize,
835 data: &'a [T],
836 metadata: ImageMetadata,
837 ) -> Result<Self, ImageError> {
838 metadata.validate::<CHANNELS>()?;
839 if row_stride < width {
840 return Err(ImageError::InvalidStride { stride: row_stride, minimum: width });
841 }
842 let plane_span = strided_span(height, row_stride, width)?;
843 if plane_stride < plane_span {
844 return Err(ImageError::InvalidPlaneStride {
845 stride: plane_stride,
846 minimum: plane_span,
847 });
848 }
849 let required = if width == 0 || height == 0 {
850 0
851 } else {
852 plane_stride
853 .checked_mul(CHANNELS - 1)
854 .and_then(|offset| offset.checked_add(plane_span))
855 .ok_or(ImageError::DimensionOverflow)?
856 };
857 if data.len() < required {
858 return Err(ImageError::StorageTooShort { required, found: data.len() });
859 }
860 Ok(Self { width, height, row_stride, plane_stride, data, metadata })
861 }
862
863 #[must_use]
865 pub const fn width(self) -> usize {
866 self.width
867 }
868
869 #[must_use]
871 pub const fn height(self) -> usize {
872 self.height
873 }
874
875 #[must_use]
877 pub const fn row_stride(self) -> usize {
878 self.row_stride
879 }
880
881 #[must_use]
883 pub const fn plane_stride(self) -> usize {
884 self.plane_stride
885 }
886
887 #[must_use]
889 pub const fn layout(self) -> ImageLayout {
890 ImageLayout::Planar
891 }
892
893 #[must_use]
895 pub const fn metadata(self) -> ImageMetadata {
896 self.metadata
897 }
898
899 #[must_use]
901 pub fn get(self, channel: usize, x: usize, y: usize) -> Option<&'a T> {
902 if channel >= CHANNELS || x >= self.width || y >= self.height {
903 return None;
904 }
905 self.data.get(channel * self.plane_stride + y * self.row_stride + x)
906 }
907
908 #[must_use]
910 pub fn pixel(self, x: usize, y: usize) -> Option<[T; CHANNELS]>
911 where
912 T: Copy,
913 {
914 if x >= self.width || y >= self.height {
915 return None;
916 }
917 Some(std::array::from_fn(|channel| {
918 *self.get(channel, x, y).expect("validated planar coordinate")
919 }))
920 }
921
922 pub fn subview(self, region: ImageRegion) -> Result<Self, ImageError> {
924 region.validate(self.width, self.height)?;
925 if region.width == 0 || region.height == 0 {
926 return Ok(Self {
927 width: region.width,
928 height: region.height,
929 row_stride: self.row_stride,
930 plane_stride: self.plane_stride,
931 data: &self.data[..0],
932 metadata: self.metadata,
933 });
934 }
935 let start = region.y * self.row_stride + region.x;
936 let span = (CHANNELS - 1) * self.plane_stride
937 + (region.height - 1) * self.row_stride
938 + region.width;
939 Ok(Self {
940 width: region.width,
941 height: region.height,
942 row_stride: self.row_stride,
943 plane_stride: self.plane_stride,
944 data: &self.data[start..start + span],
945 metadata: self.metadata,
946 })
947 }
948}
949
950pub type GrayImage<T> = Image<T, 1>;
952pub type RgbImage<T> = Image<T, 3>;
954
955#[cfg(test)]
956mod tests {
957 use super::{
958 AlphaMode, ColorRange, ColorSpace, Image, ImageError, ImageMetadata, ImageRegion,
959 ImageView, ImageViewMut, PlanarImage, PlanarImageView,
960 };
961
962 #[test]
963 fn packed_image_indexes_pixels() {
964 let mut image = Image::<u8, 3>::try_new(2, 1, vec![1, 2, 3, 4, 5, 6]).unwrap();
965 assert_eq!(image[(1, 0)], [4, 5, 6]);
966 image[(0, 0)] = [7, 8, 9];
967 assert_eq!(image.as_slice(), &[7, 8, 9, 4, 5, 6]);
968 }
969
970 #[test]
971 fn strided_view_skips_padding() {
972 let data = [1_u16, 2, 99, 3, 4];
973 let view = ImageView::<u16, 1>::new(2, 2, 3, &data).unwrap();
974 assert_eq!(view.get(0, 1), Some(&[3]));
975 assert_eq!(view.row(0), Some(&[1, 2][..]));
976 }
977
978 #[test]
979 fn rejects_short_storage() {
980 assert_eq!(
981 Image::<u8, 1>::try_new(2, 2, vec![0; 3]).unwrap_err(),
982 ImageError::StorageTooShort { required: 4, found: 3 }
983 );
984 }
985
986 #[test]
987 fn mutable_roi_updates_only_selected_pixels() {
988 let mut data = [0_u8, 1, 2, 99, 3, 4, 5];
989 let mut view = ImageViewMut::<u8, 1>::new(3, 2, 4, &mut data).unwrap();
990 {
991 let mut roi = view.subview(ImageRegion::new(1, 0, 2, 2)).unwrap();
992 *roi.get_mut(0, 0).unwrap() = [10];
993 *roi.get_mut(1, 1).unwrap() = [20];
994 }
995 assert_eq!(data, [0, 10, 2, 99, 3, 4, 20]);
996 }
997
998 #[test]
999 fn immutable_roi_preserves_parent_stride() {
1000 let data = [0_u8, 1, 2, 99, 3, 4, 5];
1001 let view = ImageView::<u8, 1>::new(3, 2, 4, &data).unwrap();
1002 let roi = view.subview(ImageRegion::new(1, 0, 2, 2)).unwrap();
1003 assert_eq!(roi.row_stride(), 4);
1004 assert_eq!(roi.get(0, 0), Some(&[1]));
1005 assert_eq!(roi.get(1, 1), Some(&[5]));
1006 assert!(view.subview(ImageRegion::new(2, 1, 2, 1)).is_err());
1007 }
1008
1009 #[test]
1010 fn planar_image_reads_channels_and_subviews() {
1011 let mut image = PlanarImage::<u8, 3>::try_new(
1013 2,
1014 2,
1015 vec![1, 2, 3, 4, 10, 20, 30, 40, 100, 110, 120, 130],
1016 )
1017 .unwrap();
1018 assert_eq!(image.view().pixel(1, 1), Some([4, 40, 130]));
1019 *image.get_mut(1, 0, 1).unwrap() = 31;
1020 let roi = image.view().subview(ImageRegion::new(0, 1, 2, 1)).unwrap();
1021 assert_eq!(roi.pixel(0, 0), Some([3, 31, 120]));
1022 assert_eq!(roi.pixel(1, 0), Some([4, 40, 130]));
1023 }
1024
1025 #[test]
1026 fn planar_view_honors_row_and_plane_padding() {
1027 let data = [1_u8, 2, 99, 3, 4, 88, 77, 10, 20, 99, 30, 40];
1028 let view = PlanarImageView::<u8, 2>::new(2, 2, 3, 7, &data).unwrap();
1029 assert_eq!(view.pixel(0, 1), Some([3, 30]));
1030 assert_eq!(view.pixel(1, 1), Some([4, 40]));
1031 }
1032
1033 #[test]
1034 fn validates_color_metadata_channel_count() {
1035 let metadata = ImageMetadata {
1036 color_space: ColorSpace::Rgb,
1037 color_range: ColorRange::Full,
1038 alpha_mode: AlphaMode::None,
1039 };
1040 let image = Image::<u8, 3>::try_new_with_metadata(1, 1, vec![1, 2, 3], metadata).unwrap();
1041 assert_eq!(image.metadata(), metadata);
1042 assert!(matches!(
1043 Image::<u8, 1>::try_new_with_metadata(1, 1, vec![1], metadata),
1044 Err(ImageError::MetadataChannelMismatch { .. })
1045 ));
1046 }
1047}