Skip to main content

webpkit/
image.rs

1//! Public image model: validated [`Dimensions`], the byte-order [`PixelLayout`],
2//! sidecar [`Metadata`], and the owned [`Image`] / borrowed [`ImageRef`]
3//! containers.
4//!
5//! Internally pixels are native `u32` ARGB (`0xAARRGGBB`); the conversion to and
6//! from a caller's byte layout happens only here, so the rest of the codec never
7//! deals with channel order.
8
9use crate::error::{Error, Result};
10use crate::prelude::*;
11
12/// Largest image side any WebP bitstream can express, in pixels.
13///
14/// The VP8L header stores each side as `dimension - 1` in 14 bits, so a valid
15/// dimension is `1..=MAX_DIMENSION` (`1 << 14` = 16384). The lossy `VP8` format
16/// tops out slightly lower (16383); callers that need the tighter bound check it
17/// themselves. This is the single source of truth for [`Dimensions`] validation.
18pub const MAX_DIMENSION: u32 = 1 << 14;
19
20/// A validated image size: both sides lie in `1..=16384`.
21#[derive(Clone, Copy, PartialEq, Eq, Debug)]
22pub struct Dimensions {
23    width: u32,
24    height: u32,
25}
26
27impl Dimensions {
28    /// Create dimensions, validating both sides are in `1..=16384`.
29    ///
30    /// # Errors
31    ///
32    /// [`Error::InvalidDimensions`] if either side is `0` or exceeds `16384`.
33    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    /// The width in pixels.
43    #[must_use]
44    pub const fn width(self) -> u32 {
45        self.width
46    }
47
48    /// The height in pixels.
49    #[must_use]
50    pub const fn height(self) -> u32 {
51        self.height
52    }
53
54    /// The total pixel count (`width * height`), widened so it never overflows.
55    #[must_use]
56    pub fn pixel_count(self) -> u64 {
57        u64::from(self.width) * u64::from(self.height)
58    }
59}
60
61/// The byte order of a pixel buffer at the API boundary.
62#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
63pub enum PixelLayout {
64    /// `R, G, B, A` (the default).
65    #[default]
66    Rgba8,
67    /// `A, R, G, B`.
68    Argb8,
69    /// `B, G, R, A`.
70    Bgra8,
71}
72
73impl PixelLayout {
74    /// Pack a native ARGB pixel (`0xAARRGGBB`) into this layout's four bytes.
75    #[must_use]
76    pub const fn pack(self, argb: u32) -> [u8; 4] {
77        // Native `0xAARRGGBB` little-endian bytes are `[B, G, R, A]`.
78        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    /// Unpack this layout's four bytes into a native ARGB pixel (`0xAARRGGBB`).
87    #[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    /// Byte offset of the alpha lane within a 4-byte pixel: `Rgba8`/`Bgra8` -> 3,
98    /// `Argb8` -> 0.
99    #[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/// Pack native ARGB pixels into a byte buffer in `layout` order.
109#[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/// Unpack a `layout`-ordered byte buffer into native ARGB pixels.
119#[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/// Whether any pixel in a native-ARGB buffer is non-opaque.
128#[must_use]
129pub fn argb_has_alpha(argb: &[u32]) -> bool {
130    argb.iter().any(|&p| p >> 24 != 0xff)
131}
132
133/// Optional sidecar metadata carried by a WebP extended (`VP8X`) container.
134#[derive(Clone, PartialEq, Eq, Debug, Default)]
135pub struct Metadata {
136    /// ICC color profile (`ICCP` chunk).
137    pub icc_profile: Option<Vec<u8>>,
138    /// Exif metadata (`EXIF` chunk).
139    pub exif: Option<Vec<u8>>,
140    /// XMP metadata (`XMP ` chunk).
141    pub xmp: Option<Vec<u8>>,
142}
143
144impl Metadata {
145    /// Empty metadata (no ICC/Exif/XMP).
146    #[must_use]
147    pub const fn none() -> Self {
148        Self {
149            icc_profile: None,
150            exif: None,
151            xmp: None,
152        }
153    }
154
155    /// Whether no metadata is present (so a bare `VP8L` file suffices).
156    #[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    /// Fold `self` (a per-field *override*), the `inherited` source metadata, and a
162    /// [`MetadataPolicy`] into the effective metadata to embed. Per field: an
163    /// override value in `self` wins; otherwise the inherited value is kept, gated
164    /// by the policy. ICC is never gated — it is inherited (or replaced) but never
165    /// dropped; only the privacy-bearing Exif/XMP sidecars are gated.
166    #[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/// How an encoder treats the metadata inherited from a source [`Image`].
193///
194/// The ICC color profile is preserved under *every* policy — a WebP we emit never
195/// silently loses color-correctness — so a policy governs only the
196/// privacy-bearing Exif/XMP sidecars.
197#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
198#[non_exhaustive]
199pub enum MetadataPolicy {
200    /// Preserve all inherited metadata: ICC, Exif, and XMP. The default — kinder
201    /// than `cwebp`, which strips metadata by default.
202    #[default]
203    Preserve,
204    /// Preserve the ICC color profile but strip the privacy-bearing Exif and XMP
205    /// sidecars (they can embed GPS, timestamps, and device IDs).
206    StripPrivate,
207}
208
209/// A decoded image: pixels in a chosen [`PixelLayout`] plus size, alpha, and
210/// sidecar [`Metadata`].
211#[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    /// Assemble an image from already-validated parts (internal constructor).
222    #[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    /// Attach (or replace) the sidecar metadata, builder-style — used to surface
240    /// `VP8X` container metadata recovered alongside a decoded image so a
241    /// decode → re-encode round trip preserves it.
242    #[must_use]
243    pub fn with_metadata(mut self, metadata: Metadata) -> Self {
244        self.metadata = metadata;
245        self
246    }
247
248    /// The image dimensions.
249    #[must_use]
250    pub const fn dimensions(&self) -> Dimensions {
251        self.dims
252    }
253
254    /// The width in pixels.
255    #[must_use]
256    pub const fn width(&self) -> u32 {
257        self.dims.width()
258    }
259
260    /// The height in pixels.
261    #[must_use]
262    pub const fn height(&self) -> u32 {
263        self.dims.height()
264    }
265
266    /// The byte order of [`Self::as_bytes`].
267    #[must_use]
268    pub const fn layout(&self) -> PixelLayout {
269        self.layout
270    }
271
272    /// The pixel bytes in [`Self::layout`] order (`width * height * 4` bytes).
273    #[must_use]
274    pub fn as_bytes(&self) -> &[u8] {
275        &self.pixels
276    }
277
278    /// Consume the image and return its pixel bytes.
279    #[must_use]
280    pub fn into_pixels(self) -> Vec<u8> {
281        self.pixels
282    }
283
284    /// Whether any pixel is non-opaque.
285    #[must_use]
286    pub const fn has_alpha(&self) -> bool {
287        self.has_alpha
288    }
289
290    /// The sidecar metadata (empty if the source was a bare `VP8L` file).
291    #[must_use]
292    pub const fn metadata(&self) -> &Metadata {
293        &self.metadata
294    }
295
296    /// Borrow this image as an [`ImageRef`] for re-encoding.
297    #[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    /// Overwrite the alpha lane of every pixel from a `width * height` alpha plane
307    /// (one byte per pixel), and set [`Self::has_alpha`] from the plane.
308    ///
309    /// # Errors
310    ///
311    /// [`Error::PixelBufferMismatch`] if `alpha.len() != width * height`.
312    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/// A borrowed view of pixels to encode: size, layout, and a byte slice.
326#[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    /// Borrow `pixels` (in `layout` order) as an image of `dims`.
335    ///
336    /// # Errors
337    ///
338    /// [`Error::PixelBufferMismatch`] if `pixels.len() != width * height * 4`.
339    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    /// The image dimensions.
351    #[must_use]
352    pub const fn dimensions(self) -> Dimensions {
353        self.dims
354    }
355
356    /// The byte order of [`Self::as_bytes`].
357    #[must_use]
358    pub const fn layout(self) -> PixelLayout {
359        self.layout
360    }
361
362    /// The borrowed pixel bytes.
363    #[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        /// For every layout, `pack`/`unpack` are mutual inverses in both
379        /// directions (the single channel-order boundary of the whole codec).
380        #[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        // A distinctive ARGB pixel: A=0x11, R=0x22, G=0x33, B=0x44.
409        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        // Byte order is exactly as documented.
414        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        // Alpha is the high byte (`>> 24`): an all-opaque buffer has none.
443        assert!(!argb_has_alpha(&[0xFF00_0000, 0xFFAA_BBCC]));
444        // A non-opaque pixel is detected (pins `!=`, not `==`).
445        assert!(argb_has_alpha(&[0xFF00_0000, 0x0000_0000]));
446        // A pixel opaque in the high byte but 0xFF in the low byte is still opaque
447        // — kills `>> -> <<`, which would inspect the low byte instead.
448        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(); // 2 * 3 * 4
455        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); // kills `height -> 1`
464        assert_eq!(img.as_bytes(), &pixels[..]);
465        assert_eq!(img.into_pixels(), pixels); // kills `into_pixels -> vec![..]`
466    }
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    /// A source carrying all three sidecars.
479    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    /// The single source of truth for the `Metadata::resolve` fold: policy
488    /// (Preserve/StripPrivate) × field (icc/exif/xmp) × override presence. The
489    /// encoder configs delegate here, so their tests only check the delegation.
490    #[test]
491    fn resolve_truth_table() {
492        // Preserve (no override): every inherited field is kept as-is.
493        let inherited = all_three();
494        assert_eq!(
495            Metadata::none().resolve(&inherited, MetadataPolicy::Preserve),
496            inherited,
497        );
498
499        // StripPrivate (no override): ICC kept, Exif/XMP dropped.
500        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        // Override wins per-slot under any policy: exif override beats inherited.
506        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        // An explicit override survives StripPrivate, but a non-overridden private
519        // slot is still dropped; ICC is never gated.
520        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        // ICC: a `None` override never nulls the inherited profile; a `Some`
530        // override replaces it.
531        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        // A present-but-empty blob (`Some(vec![])`) is a real value: the `.or_else`
555        // short-circuits on any `Some`, so it wins and is not normalized to `None`,
556        // under either policy — and it still upgrades to VP8X (non-empty).
557        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        // A present-but-empty *inherited* value survives under Preserve.
571        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    /// The alpha lane lands at the layout's byte offset (`3`/`0`/`3`), the other
602    /// three channels are untouched, and a mixed plane flips `has_alpha` on.
603    #[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            // Four fully-opaque pixels with distinct non-alpha channels.
613            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; // opaque alpha lane
621            }
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    /// An all-`0xff` plane still overwrites the lane but leaves `has_alpha` off.
644    #[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    /// A plane whose length is not `width * height` is rejected.
659    #[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}