1use crate::error::{Error, Result};
10use crate::prelude::*;
11
12pub const MAX_DIMENSION: u32 = 1 << 14;
19
20#[derive(Clone, Copy, PartialEq, Eq, Debug)]
22pub struct Dimensions {
23 width: u32,
24 height: u32,
25}
26
27impl Dimensions {
28 pub fn new(width: u32, height: u32) -> Result<Self> {
34 let valid = |side: u32| (1..=MAX_DIMENSION).contains(&side);
35 if valid(width) && valid(height) {
36 Ok(Self { width, height })
37 } else {
38 Err(Error::InvalidDimensions)
39 }
40 }
41
42 #[must_use]
44 pub const fn width(self) -> u32 {
45 self.width
46 }
47
48 #[must_use]
50 pub const fn height(self) -> u32 {
51 self.height
52 }
53
54 #[must_use]
56 pub fn pixel_count(self) -> u64 {
57 u64::from(self.width) * u64::from(self.height)
58 }
59}
60
61#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
63pub enum PixelLayout {
64 #[default]
66 Rgba8,
67 Argb8,
69 Bgra8,
71}
72
73impl PixelLayout {
74 #[must_use]
76 pub const fn pack(self, argb: u32) -> [u8; 4] {
77 let [b, g, r, a] = argb.to_le_bytes();
79 match self {
80 Self::Rgba8 => [r, g, b, a],
81 Self::Argb8 => [a, r, g, b],
82 Self::Bgra8 => [b, g, r, a],
83 }
84 }
85
86 #[must_use]
88 pub const fn unpack(self, px: [u8; 4]) -> u32 {
89 let (r, g, b, a) = match self {
90 Self::Rgba8 => (px[0], px[1], px[2], px[3]),
91 Self::Argb8 => (px[1], px[2], px[3], px[0]),
92 Self::Bgra8 => (px[2], px[1], px[0], px[3]),
93 };
94 u32::from_le_bytes([b, g, r, a])
95 }
96
97 #[must_use]
100 pub const fn alpha_byte_offset(self) -> usize {
101 match self {
102 Self::Rgba8 | Self::Bgra8 => 3,
103 Self::Argb8 => 0,
104 }
105 }
106}
107
108#[must_use]
110pub fn pack_pixels(layout: PixelLayout, argb: &[u32]) -> Vec<u8> {
111 let mut out = Vec::with_capacity(argb.len() * 4);
112 for &pixel in argb {
113 out.extend_from_slice(&layout.pack(pixel));
114 }
115 out
116}
117
118#[must_use]
120pub fn unpack_pixels(layout: PixelLayout, bytes: &[u8]) -> Vec<u32> {
121 bytes
122 .chunks_exact(4)
123 .map(|c| layout.unpack([c[0], c[1], c[2], c[3]]))
124 .collect()
125}
126
127#[must_use]
129pub fn argb_has_alpha(argb: &[u32]) -> bool {
130 argb.iter().any(|&p| p >> 24 != 0xff)
131}
132
133#[derive(Clone, PartialEq, Eq, Debug, Default)]
135pub struct Metadata {
136 pub icc_profile: Option<Vec<u8>>,
138 pub exif: Option<Vec<u8>>,
140 pub xmp: Option<Vec<u8>>,
142}
143
144impl Metadata {
145 #[must_use]
147 pub const fn none() -> Self {
148 Self {
149 icc_profile: None,
150 exif: None,
151 xmp: None,
152 }
153 }
154
155 #[must_use]
157 pub const fn is_empty(&self) -> bool {
158 self.icc_profile.is_none() && self.exif.is_none() && self.xmp.is_none()
159 }
160
161 #[must_use]
167 pub fn resolve(&self, inherited: &Self, policy: MetadataPolicy) -> Self {
168 let keep_private = matches!(policy, MetadataPolicy::Preserve);
169 Self {
170 icc_profile: self
171 .icc_profile
172 .clone()
173 .or_else(|| inherited.icc_profile.clone()),
174 exif: self.exif.clone().or_else(|| {
175 if keep_private {
176 inherited.exif.clone()
177 } else {
178 None
179 }
180 }),
181 xmp: self.xmp.clone().or_else(|| {
182 if keep_private {
183 inherited.xmp.clone()
184 } else {
185 None
186 }
187 }),
188 }
189 }
190}
191
192#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
198#[non_exhaustive]
199pub enum MetadataPolicy {
200 #[default]
203 Preserve,
204 StripPrivate,
207}
208
209#[derive(Clone, PartialEq, Eq, Debug)]
212pub struct Image {
213 dims: Dimensions,
214 layout: PixelLayout,
215 pixels: Vec<u8>,
216 has_alpha: bool,
217 metadata: Metadata,
218}
219
220impl Image {
221 #[must_use]
223 pub const fn from_parts(
224 dims: Dimensions,
225 layout: PixelLayout,
226 pixels: Vec<u8>,
227 has_alpha: bool,
228 metadata: Metadata,
229 ) -> Self {
230 Self {
231 dims,
232 layout,
233 pixels,
234 has_alpha,
235 metadata,
236 }
237 }
238
239 #[must_use]
243 pub fn with_metadata(mut self, metadata: Metadata) -> Self {
244 self.metadata = metadata;
245 self
246 }
247
248 #[must_use]
250 pub const fn dimensions(&self) -> Dimensions {
251 self.dims
252 }
253
254 #[must_use]
256 pub const fn width(&self) -> u32 {
257 self.dims.width()
258 }
259
260 #[must_use]
262 pub const fn height(&self) -> u32 {
263 self.dims.height()
264 }
265
266 #[must_use]
268 pub const fn layout(&self) -> PixelLayout {
269 self.layout
270 }
271
272 #[must_use]
274 pub fn as_bytes(&self) -> &[u8] {
275 &self.pixels
276 }
277
278 #[must_use]
280 pub fn into_pixels(self) -> Vec<u8> {
281 self.pixels
282 }
283
284 #[must_use]
286 pub const fn has_alpha(&self) -> bool {
287 self.has_alpha
288 }
289
290 #[must_use]
292 pub const fn metadata(&self) -> &Metadata {
293 &self.metadata
294 }
295
296 #[must_use]
298 pub fn as_image_ref(&self) -> ImageRef<'_> {
299 ImageRef {
300 dims: self.dims,
301 layout: self.layout,
302 pixels: &self.pixels,
303 }
304 }
305
306 pub fn apply_alpha_plane(&mut self, alpha: &[u8]) -> Result<()> {
313 if alpha.len() as u64 != self.dims.pixel_count() {
314 return Err(Error::PixelBufferMismatch);
315 }
316 let off = self.layout.alpha_byte_offset();
317 for (px, &a) in self.pixels.chunks_exact_mut(4).zip(alpha) {
318 px[off] = a;
319 }
320 self.has_alpha = alpha.iter().any(|&a| a != 0xff);
321 Ok(())
322 }
323}
324
325#[derive(Clone, Copy, Debug)]
327pub struct ImageRef<'a> {
328 dims: Dimensions,
329 layout: PixelLayout,
330 pixels: &'a [u8],
331}
332
333impl<'a> ImageRef<'a> {
334 pub fn new(dims: Dimensions, layout: PixelLayout, pixels: &'a [u8]) -> Result<Self> {
340 if pixels.len() as u64 != dims.pixel_count() * 4 {
341 return Err(Error::PixelBufferMismatch);
342 }
343 Ok(Self {
344 dims,
345 layout,
346 pixels,
347 })
348 }
349
350 #[must_use]
352 pub const fn dimensions(self) -> Dimensions {
353 self.dims
354 }
355
356 #[must_use]
358 pub const fn layout(self) -> PixelLayout {
359 self.layout
360 }
361
362 #[must_use]
364 pub const fn as_bytes(self) -> &'a [u8] {
365 self.pixels
366 }
367}
368
369#[cfg(test)]
370mod tests {
371 use proptest::prelude::*;
372
373 use super::{
374 Dimensions, Image, Metadata, MetadataPolicy, PixelLayout, pack_pixels, unpack_pixels,
375 };
376
377 proptest! {
378 #[test]
381 fn pixel_layout_pack_unpack_round_trips(
382 argb in any::<u32>(),
383 px in any::<[u8; 4]>(),
384 layout in prop_oneof![
385 Just(PixelLayout::Rgba8),
386 Just(PixelLayout::Argb8),
387 Just(PixelLayout::Bgra8),
388 ],
389 ) {
390 prop_assert_eq!(layout.unpack(layout.pack(argb)), argb);
391 prop_assert_eq!(layout.pack(layout.unpack(px)), px);
392 }
393 }
394 use crate::error::Error;
395
396 #[test]
397 fn dimensions_validate_range() {
398 assert!(Dimensions::new(0, 4).is_err());
399 assert!(Dimensions::new(4, 0).is_err());
400 assert!(Dimensions::new(16385, 1).is_err());
401 let d = Dimensions::new(16384, 2).unwrap();
402 assert_eq!((d.width(), d.height()), (16384, 2));
403 assert_eq!(d.pixel_count(), 32768);
404 }
405
406 #[test]
407 fn layout_pack_unpack_round_trips() {
408 let argb = 0x1122_3344u32;
410 for layout in [PixelLayout::Rgba8, PixelLayout::Argb8, PixelLayout::Bgra8] {
411 assert_eq!(layout.unpack(layout.pack(argb)), argb);
412 }
413 assert_eq!(PixelLayout::Rgba8.pack(argb), [0x22, 0x33, 0x44, 0x11]);
415 assert_eq!(PixelLayout::Argb8.pack(argb), [0x11, 0x22, 0x33, 0x44]);
416 assert_eq!(PixelLayout::Bgra8.pack(argb), [0x44, 0x33, 0x22, 0x11]);
417 }
418
419 #[test]
420 fn buffer_pack_unpack_round_trips() {
421 let argb: Vec<u32> = (0..64u32).map(|v| v.wrapping_mul(0x0104_5197)).collect();
422 for layout in [PixelLayout::Rgba8, PixelLayout::Argb8, PixelLayout::Bgra8] {
423 let bytes = pack_pixels(layout, &argb);
424 assert_eq!(bytes.len(), argb.len() * 4);
425 assert_eq!(unpack_pixels(layout, &bytes), argb);
426 }
427 }
428
429 #[test]
430 fn image_ref_checks_buffer_length() {
431 let dims = Dimensions::new(2, 2).unwrap();
432 assert_eq!(
433 super::ImageRef::new(dims, PixelLayout::Rgba8, &[0u8; 15]).unwrap_err(),
434 Error::PixelBufferMismatch
435 );
436 assert!(super::ImageRef::new(dims, PixelLayout::Rgba8, &[0u8; 16]).is_ok());
437 }
438
439 #[test]
440 fn argb_has_alpha_reads_the_high_byte() {
441 use super::argb_has_alpha;
442 assert!(!argb_has_alpha(&[0xFF00_0000, 0xFFAA_BBCC]));
444 assert!(argb_has_alpha(&[0xFF00_0000, 0x0000_0000]));
446 assert!(!argb_has_alpha(&[0xFF00_00FF]));
449 }
450
451 #[test]
452 fn image_reports_dimensions_and_pixels_verbatim() {
453 let dims = Dimensions::new(2, 3).unwrap();
454 let pixels: Vec<u8> = (0..24).collect(); let img = Image::from_parts(
456 dims,
457 PixelLayout::Rgba8,
458 pixels.clone(),
459 false,
460 Metadata::none(),
461 );
462 assert_eq!(img.width(), 2);
463 assert_eq!(img.height(), 3); assert_eq!(img.as_bytes(), &pixels[..]);
465 assert_eq!(img.into_pixels(), pixels); }
467
468 #[test]
469 fn metadata_emptiness() {
470 assert!(Metadata::none().is_empty());
471 let with_icc = Metadata {
472 icc_profile: Some(vec![1, 2, 3]),
473 ..Metadata::none()
474 };
475 assert!(!with_icc.is_empty());
476 }
477
478 fn all_three() -> Metadata {
480 Metadata {
481 icc_profile: Some(vec![10]),
482 exif: Some(vec![20]),
483 xmp: Some(vec![30]),
484 }
485 }
486
487 #[test]
491 fn resolve_truth_table() {
492 let inherited = all_three();
494 assert_eq!(
495 Metadata::none().resolve(&inherited, MetadataPolicy::Preserve),
496 inherited,
497 );
498
499 let stripped = Metadata::none().resolve(&inherited, MetadataPolicy::StripPrivate);
501 assert_eq!(stripped.icc_profile.as_deref(), Some(&[10][..]));
502 assert_eq!(stripped.exif, None);
503 assert_eq!(stripped.xmp, None);
504
505 let exif_override = Metadata {
507 exif: Some(vec![2]),
508 ..Metadata::none()
509 };
510 assert_eq!(
511 exif_override
512 .resolve(&inherited, MetadataPolicy::Preserve)
513 .exif
514 .as_deref(),
515 Some(&[2][..]),
516 );
517
518 let exif99 = Metadata {
521 exif: Some(vec![99]),
522 ..Metadata::none()
523 };
524 let resolved = exif99.resolve(&inherited, MetadataPolicy::StripPrivate);
525 assert_eq!(resolved.exif.as_deref(), Some(&[99][..]));
526 assert_eq!(resolved.xmp, None);
527 assert_eq!(resolved.icc_profile.as_deref(), Some(&[10][..]));
528
529 let icc_only = Metadata {
532 icc_profile: Some(vec![10]),
533 ..Metadata::none()
534 };
535 assert_eq!(
536 Metadata::none()
537 .resolve(&icc_only, MetadataPolicy::Preserve)
538 .icc_profile
539 .as_deref(),
540 Some(&[10][..]),
541 );
542 let icc_replace = Metadata {
543 icc_profile: Some(vec![77]),
544 ..Metadata::none()
545 };
546 assert_eq!(
547 icc_replace
548 .resolve(&icc_only, MetadataPolicy::Preserve)
549 .icc_profile
550 .as_deref(),
551 Some(&[77][..]),
552 );
553
554 let empty_exif = Metadata {
558 exif: Some(vec![]),
559 ..Metadata::none()
560 };
561 let e_preserve = empty_exif.resolve(&Metadata::none(), MetadataPolicy::Preserve);
562 assert_eq!(e_preserve.exif, Some(vec![]));
563 assert!(!e_preserve.is_empty());
564 assert_eq!(
565 empty_exif
566 .resolve(&Metadata::none(), MetadataPolicy::StripPrivate)
567 .exif,
568 Some(vec![]),
569 );
570 let inherited_empty_xmp = Metadata {
572 xmp: Some(vec![]),
573 ..Metadata::none()
574 };
575 assert_eq!(
576 Metadata::none()
577 .resolve(&inherited_empty_xmp, MetadataPolicy::Preserve)
578 .xmp,
579 Some(vec![]),
580 );
581 }
582
583 #[test]
584 fn image_accessors_and_borrow() {
585 let dims = Dimensions::new(2, 1).unwrap();
586 let img = Image::from_parts(
587 dims,
588 PixelLayout::Rgba8,
589 vec![1, 2, 3, 255, 4, 5, 6, 0],
590 true,
591 Metadata::none(),
592 );
593 assert_eq!((img.width(), img.height()), (2, 1));
594 assert!(img.has_alpha());
595 assert_eq!(img.layout(), PixelLayout::Rgba8);
596 let borrowed = img.as_image_ref();
597 assert_eq!(borrowed.as_bytes(), img.as_bytes());
598 assert_eq!(borrowed.dimensions(), dims);
599 }
600
601 #[test]
604 fn apply_alpha_plane_writes_alpha_lane() {
605 for (layout, off) in [
606 (PixelLayout::Rgba8, 3usize),
607 (PixelLayout::Argb8, 0usize),
608 (PixelLayout::Bgra8, 3usize),
609 ] {
610 assert_eq!(layout.alpha_byte_offset(), off);
611 let dims = Dimensions::new(2, 2).unwrap();
612 let bases = [10u8, 14, 18, 22];
614 let mut pixels = vec![0u8; 16];
615 for (px, &base) in pixels.chunks_exact_mut(4).zip(bases.iter()) {
616 px[0] = base;
617 px[1] = base + 1;
618 px[2] = base + 2;
619 px[3] = base + 3;
620 px[off] = 0xff; }
622 let original = pixels.clone();
623 let mut img = Image::from_parts(dims, layout, pixels, false, Metadata::none());
624 let plane = [0x00u8, 0x80, 0xff, 0x40];
625 img.apply_alpha_plane(&plane).unwrap();
626 for (i, (px, orig)) in img
627 .as_bytes()
628 .chunks_exact(4)
629 .zip(original.chunks_exact(4))
630 .enumerate()
631 {
632 assert_eq!(px[off], plane[i], "alpha lane at offset {off}");
633 for (b, (&got, &want)) in px.iter().zip(orig).enumerate() {
634 if b != off {
635 assert_eq!(got, want, "channel {b} untouched");
636 }
637 }
638 }
639 assert!(img.has_alpha(), "mixed plane flips has_alpha on");
640 }
641 }
642
643 #[test]
645 fn apply_alpha_plane_all_opaque_keeps_flag_false() {
646 for layout in [PixelLayout::Rgba8, PixelLayout::Argb8, PixelLayout::Bgra8] {
647 let dims = Dimensions::new(2, 2).unwrap();
648 let mut img = Image::from_parts(dims, layout, vec![0u8; 16], false, Metadata::none());
649 img.apply_alpha_plane(&[0xffu8; 4]).unwrap();
650 assert!(!img.has_alpha());
651 let off = layout.alpha_byte_offset();
652 for px in img.as_bytes().chunks_exact(4) {
653 assert_eq!(px[off], 0xff);
654 }
655 }
656 }
657
658 #[test]
660 fn apply_alpha_plane_length_mismatch() {
661 let dims = Dimensions::new(2, 2).unwrap();
662 let mut img = Image::from_parts(
663 dims,
664 PixelLayout::Rgba8,
665 vec![0u8; 16],
666 false,
667 Metadata::none(),
668 );
669 assert_eq!(
670 img.apply_alpha_plane(&[0u8; 3]).unwrap_err(),
671 Error::PixelBufferMismatch
672 );
673 assert_eq!(
674 img.apply_alpha_plane(&[0u8; 5]).unwrap_err(),
675 Error::PixelBufferMismatch
676 );
677 }
678}