Skip to main content

webpx/
types.rs

1//! Core types for image data representation.
2
3use alloc::vec::Vec;
4#[cfg(any(feature = "encode", feature = "decode"))]
5use rgb::alt::{BGR8, BGRA8};
6#[cfg(any(feature = "encode", feature = "decode"))]
7use rgb::{RGB8, RGBA8};
8use whereat::*;
9
10/// Pixel layout describing channel order (implementation detail).
11#[cfg(any(feature = "encode", feature = "decode"))]
12#[doc(hidden)]
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum PixelLayout {
15    /// RGBA - 4 bytes per pixel (red, green, blue, alpha)
16    Rgba,
17    /// BGRA - 4 bytes per pixel (blue, green, red, alpha) - Windows/GPU native
18    Bgra,
19    /// RGB - 3 bytes per pixel (red, green, blue)
20    Rgb,
21    /// BGR - 3 bytes per pixel (blue, green, red) - OpenCV native
22    Bgr,
23}
24
25#[cfg(any(feature = "encode", feature = "decode"))]
26impl PixelLayout {
27    /// Bytes per pixel for this layout.
28    #[must_use]
29    pub const fn bytes_per_pixel(self) -> usize {
30        match self {
31            PixelLayout::Rgba | PixelLayout::Bgra => 4,
32            PixelLayout::Rgb | PixelLayout::Bgr => 3,
33        }
34    }
35
36    /// Whether this layout has an alpha channel.
37    #[must_use]
38    pub const fn has_alpha(self) -> bool {
39        matches!(self, PixelLayout::Rgba | PixelLayout::Bgra)
40    }
41}
42
43/// Sealed marker trait for pixel types that can be encoded.
44///
45/// This trait is an implementation detail and should not be referenced directly.
46/// Use concrete types like [`RGB8`], [`RGBA8`], [`BGR8`], [`BGRA8`] with
47/// [`Encoder::from_pixels`](crate::Encoder::from_pixels).
48#[cfg(feature = "encode")]
49#[doc(hidden)]
50pub trait EncodePixel: Copy + 'static + private::Sealed {
51    /// The pixel layout corresponding to this type.
52    const LAYOUT: PixelLayout;
53}
54
55#[cfg(feature = "encode")]
56impl EncodePixel for RGBA8 {
57    const LAYOUT: PixelLayout = PixelLayout::Rgba;
58}
59
60#[cfg(feature = "encode")]
61impl EncodePixel for BGRA8 {
62    const LAYOUT: PixelLayout = PixelLayout::Bgra;
63}
64
65#[cfg(feature = "encode")]
66impl EncodePixel for RGB8 {
67    const LAYOUT: PixelLayout = PixelLayout::Rgb;
68}
69
70#[cfg(feature = "encode")]
71impl EncodePixel for BGR8 {
72    const LAYOUT: PixelLayout = PixelLayout::Bgr;
73}
74
75/// Sealed marker trait for pixel types that can be decoded into.
76///
77/// This trait is an implementation detail and should not be referenced directly.
78/// Use concrete types like [`RGB8`], [`RGBA8`], [`BGR8`], [`BGRA8`] with
79/// decode functions.
80#[cfg(feature = "decode")]
81#[doc(hidden)]
82pub trait DecodePixel: Copy + 'static + private::Sealed {
83    /// The pixel layout corresponding to this type.
84    const LAYOUT: PixelLayout;
85
86    /// Decode WebP data into a newly allocated buffer.
87    ///
88    /// # Safety
89    /// This calls libwebp FFI functions.
90    #[doc(hidden)]
91    fn decode_new(data: &[u8]) -> Option<(*mut u8, i32, i32)>;
92
93    /// Decode WebP data into an existing buffer.
94    ///
95    /// # Safety
96    /// - `output` must be a valid pointer to a buffer of at least `output_len` bytes
97    /// - The buffer must remain valid for the duration of the call
98    #[doc(hidden)]
99    unsafe fn decode_into(data: &[u8], output: *mut u8, output_len: usize, stride: i32) -> bool;
100}
101
102#[cfg(feature = "decode")]
103impl DecodePixel for RGBA8 {
104    const LAYOUT: PixelLayout = PixelLayout::Rgba;
105
106    fn decode_new(data: &[u8]) -> Option<(*mut u8, i32, i32)> {
107        let mut width: i32 = 0;
108        let mut height: i32 = 0;
109        let ptr = unsafe {
110            libwebp_sys::WebPDecodeRGBA(data.as_ptr(), data.len(), &mut width, &mut height)
111        };
112        if ptr.is_null() {
113            None
114        } else {
115            Some((ptr, width, height))
116        }
117    }
118
119    unsafe fn decode_into(data: &[u8], output: *mut u8, output_len: usize, stride: i32) -> bool {
120        // SAFETY: Caller guarantees output is valid for output_len bytes
121        let result = unsafe {
122            libwebp_sys::WebPDecodeRGBAInto(data.as_ptr(), data.len(), output, output_len, stride)
123        };
124        !result.is_null()
125    }
126}
127
128#[cfg(feature = "decode")]
129impl DecodePixel for BGRA8 {
130    const LAYOUT: PixelLayout = PixelLayout::Bgra;
131
132    fn decode_new(data: &[u8]) -> Option<(*mut u8, i32, i32)> {
133        let mut width: i32 = 0;
134        let mut height: i32 = 0;
135        let ptr = unsafe {
136            libwebp_sys::WebPDecodeBGRA(data.as_ptr(), data.len(), &mut width, &mut height)
137        };
138        if ptr.is_null() {
139            None
140        } else {
141            Some((ptr, width, height))
142        }
143    }
144
145    unsafe fn decode_into(data: &[u8], output: *mut u8, output_len: usize, stride: i32) -> bool {
146        // SAFETY: Caller guarantees output is valid for output_len bytes
147        let result = unsafe {
148            libwebp_sys::WebPDecodeBGRAInto(data.as_ptr(), data.len(), output, output_len, stride)
149        };
150        !result.is_null()
151    }
152}
153
154#[cfg(feature = "decode")]
155impl DecodePixel for RGB8 {
156    const LAYOUT: PixelLayout = PixelLayout::Rgb;
157
158    fn decode_new(data: &[u8]) -> Option<(*mut u8, i32, i32)> {
159        let mut width: i32 = 0;
160        let mut height: i32 = 0;
161        let ptr = unsafe {
162            libwebp_sys::WebPDecodeRGB(data.as_ptr(), data.len(), &mut width, &mut height)
163        };
164        if ptr.is_null() {
165            None
166        } else {
167            Some((ptr, width, height))
168        }
169    }
170
171    unsafe fn decode_into(data: &[u8], output: *mut u8, output_len: usize, stride: i32) -> bool {
172        // SAFETY: Caller guarantees output is valid for output_len bytes
173        let result = unsafe {
174            libwebp_sys::WebPDecodeRGBInto(data.as_ptr(), data.len(), output, output_len, stride)
175        };
176        !result.is_null()
177    }
178}
179
180#[cfg(feature = "decode")]
181impl DecodePixel for BGR8 {
182    const LAYOUT: PixelLayout = PixelLayout::Bgr;
183
184    fn decode_new(data: &[u8]) -> Option<(*mut u8, i32, i32)> {
185        let mut width: i32 = 0;
186        let mut height: i32 = 0;
187        let ptr = unsafe {
188            libwebp_sys::WebPDecodeBGR(data.as_ptr(), data.len(), &mut width, &mut height)
189        };
190        if ptr.is_null() {
191            None
192        } else {
193            Some((ptr, width, height))
194        }
195    }
196
197    unsafe fn decode_into(data: &[u8], output: *mut u8, output_len: usize, stride: i32) -> bool {
198        // SAFETY: Caller guarantees output is valid for output_len bytes
199        let result = unsafe {
200            libwebp_sys::WebPDecodeBGRInto(data.as_ptr(), data.len(), output, output_len, stride)
201        };
202        !result.is_null()
203    }
204}
205
206#[cfg(any(feature = "encode", feature = "decode"))]
207mod private {
208    use super::*;
209
210    pub trait Sealed {}
211    impl Sealed for RGBA8 {}
212    impl Sealed for BGRA8 {}
213    impl Sealed for RGB8 {}
214    impl Sealed for BGR8 {}
215}
216
217/// Information about a WebP image.
218#[derive(Debug, Clone, PartialEq, Eq)]
219#[non_exhaustive]
220pub struct ImageInfo {
221    /// Image width in pixels.
222    pub width: u32,
223    /// Image height in pixels.
224    pub height: u32,
225    /// Whether the image has an alpha channel.
226    pub has_alpha: bool,
227    /// Whether the image is animated.
228    pub has_animation: bool,
229    /// Number of frames (1 for static images).
230    pub frame_count: u32,
231    /// Bitstream format (lossy or lossless).
232    pub format: BitstreamFormat,
233}
234
235impl ImageInfo {
236    /// Get info from WebP data without decoding.
237    pub fn from_webp(data: &[u8]) -> crate::Result<Self> {
238        let mut width: i32 = 0;
239        let mut height: i32 = 0;
240
241        let result =
242            unsafe { libwebp_sys::WebPGetInfo(data.as_ptr(), data.len(), &mut width, &mut height) };
243
244        if result == 0 {
245            return Err(at!(crate::Error::InvalidWebP));
246        }
247
248        // Get more detailed features. Only `assume_init` after `WebPGetFeatures`
249        // returns `VP8_STATUS_OK` — on failure libwebp may not have written all
250        // fields, and `MaybeUninit::assume_init` on uninitialized memory is UB
251        // even for plain integer structs.
252        let mut features = core::mem::MaybeUninit::<libwebp_sys::WebPBitstreamFeatures>::zeroed();
253        let status = unsafe {
254            libwebp_sys::WebPGetFeatures(data.as_ptr(), data.len(), features.as_mut_ptr())
255        };
256        if status != libwebp_sys::VP8StatusCode::VP8_STATUS_OK {
257            return Err(at!(crate::Error::DecodeFailed(
258                crate::error::DecodingError::from(status as i32),
259            )));
260        }
261        let features = unsafe { features.assume_init() };
262
263        let format = match features.format {
264            0 => BitstreamFormat::Undefined,
265            1 => BitstreamFormat::Lossy,
266            2 => BitstreamFormat::Lossless,
267            _ => BitstreamFormat::Undefined,
268        };
269
270        Ok(ImageInfo {
271            width: width as u32,
272            height: height as u32,
273            has_alpha: features.has_alpha != 0,
274            has_animation: features.has_animation != 0,
275            frame_count: if features.has_animation != 0 { 0 } else { 1 }, // Animation frame count needs demux
276            format,
277        })
278    }
279}
280
281/// Bitstream format.
282#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
283#[non_exhaustive]
284pub enum BitstreamFormat {
285    /// Format not determined.
286    #[default]
287    Undefined,
288    /// Lossy compression (VP8).
289    Lossy,
290    /// Lossless compression (VP8L).
291    Lossless,
292}
293
294/// Color mode for output.
295#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
296#[non_exhaustive]
297pub enum ColorMode {
298    /// RGBA (8 bits per channel, 32 bits per pixel).
299    #[default]
300    Rgba,
301    /// BGRA (8 bits per channel, 32 bits per pixel).
302    Bgra,
303    /// ARGB (8 bits per channel, 32 bits per pixel).
304    Argb,
305    /// RGB (8 bits per channel, 24 bits per pixel).
306    Rgb,
307    /// BGR (8 bits per channel, 24 bits per pixel).
308    Bgr,
309    /// YUV420 (separate Y, U, V planes).
310    Yuv420,
311    /// YUVA420 (YUV420 with alpha plane).
312    Yuva420,
313}
314
315impl ColorMode {
316    /// Bytes per pixel for packed formats.
317    pub fn bytes_per_pixel(self) -> Option<usize> {
318        match self {
319            ColorMode::Rgba | ColorMode::Bgra | ColorMode::Argb => Some(4),
320            ColorMode::Rgb | ColorMode::Bgr => Some(3),
321            ColorMode::Yuv420 | ColorMode::Yuva420 => None, // Planar
322        }
323    }
324
325    /// Whether this mode has an alpha channel.
326    pub fn has_alpha(self) -> bool {
327        matches!(
328            self,
329            ColorMode::Rgba | ColorMode::Bgra | ColorMode::Argb | ColorMode::Yuva420
330        )
331    }
332
333    /// Whether this is a planar YUV format.
334    pub fn is_yuv(self) -> bool {
335        matches!(self, ColorMode::Yuv420 | ColorMode::Yuva420)
336    }
337}
338
339/// YUV plane data for planar formats.
340#[derive(Debug, Clone)]
341pub struct YuvPlanes {
342    /// Y (luma) plane data.
343    pub y: Vec<u8>,
344    /// Y plane stride in bytes.
345    pub y_stride: usize,
346    /// U (chroma blue) plane data.
347    pub u: Vec<u8>,
348    /// U plane stride in bytes.
349    pub u_stride: usize,
350    /// V (chroma red) plane data.
351    pub v: Vec<u8>,
352    /// V plane stride in bytes.
353    pub v_stride: usize,
354    /// Alpha plane data (optional).
355    pub a: Option<Vec<u8>>,
356    /// Alpha plane stride in bytes.
357    pub a_stride: usize,
358    /// Image width.
359    pub width: u32,
360    /// Image height.
361    pub height: u32,
362}
363
364impl YuvPlanes {
365    /// Create new YUV planes with the given dimensions.
366    ///
367    /// Allocates planes for YUV420 format where U and V are half the resolution.
368    ///
369    /// Returns `None` if the dimensions exceed libwebp's 16383×16383 limit
370    /// or would otherwise overflow plane size calculations on 32-bit usize.
371    /// For valid encode-side dimensions, prefer this over the panic-on-overflow
372    /// `vec!` macro.
373    #[must_use]
374    pub fn new_checked(width: u32, height: u32, with_alpha: bool) -> Option<Self> {
375        // Mirror the encoder's MAX_DIMENSION (libwebp's WEBP_MAX_DIMENSION).
376        const MAX_DIMENSION: u32 = 16383;
377        if width == 0 || height == 0 || width > MAX_DIMENSION || height > MAX_DIMENSION {
378            return None;
379        }
380
381        let y_stride = width as usize;
382        let uv_stride = (width as usize).div_ceil(2);
383        let uv_height = (height as usize).div_ceil(2);
384
385        // saturating_mul guards 32-bit usize even though MAX_DIMENSION should
386        // prevent overflow at the dimension cap.
387        let y_size = y_stride.saturating_mul(height as usize);
388        let uv_size = uv_stride.saturating_mul(uv_height);
389
390        Some(Self {
391            y: alloc::vec![0u8; y_size],
392            y_stride,
393            u: alloc::vec![0u8; uv_size],
394            u_stride: uv_stride,
395            v: alloc::vec![0u8; uv_size],
396            v_stride: uv_stride,
397            a: if with_alpha {
398                Some(alloc::vec![0u8; y_size])
399            } else {
400                None
401            },
402            a_stride: y_stride,
403            width,
404            height,
405        })
406    }
407
408    /// Create new YUV planes with the given dimensions.
409    ///
410    /// Allocates planes for YUV420 format where U and V are half the resolution.
411    ///
412    /// # Panics
413    ///
414    /// Panics if `width` or `height` is zero or exceeds 16383. For a
415    /// non-panicking version, use [`Self::new_checked`].
416    pub fn new(width: u32, height: u32, with_alpha: bool) -> Self {
417        Self::new_checked(width, height, with_alpha)
418            .expect("YuvPlanes::new: dimensions out of range (1..=16383)")
419    }
420
421    /// Get the U plane dimensions (half width/height for YUV420).
422    pub fn uv_dimensions(&self) -> (u32, u32) {
423        (self.width.div_ceil(2), self.height.div_ceil(2))
424    }
425}
426
427/// Reference to YUV planes (borrowed version).
428#[derive(Debug, Clone, Copy)]
429pub struct YuvPlanesRef<'a> {
430    /// Y (luma) plane data.
431    pub y: &'a [u8],
432    /// Y plane stride in bytes.
433    pub y_stride: usize,
434    /// U (chroma blue) plane data.
435    pub u: &'a [u8],
436    /// U plane stride in bytes.
437    pub u_stride: usize,
438    /// V (chroma red) plane data.
439    pub v: &'a [u8],
440    /// V plane stride in bytes.
441    pub v_stride: usize,
442    /// Alpha plane data (optional).
443    pub a: Option<&'a [u8]>,
444    /// Alpha plane stride in bytes.
445    pub a_stride: usize,
446    /// Image width.
447    pub width: u32,
448    /// Image height.
449    pub height: u32,
450}
451
452impl<'a> From<&'a YuvPlanes> for YuvPlanesRef<'a> {
453    fn from(planes: &'a YuvPlanes) -> Self {
454        Self {
455            y: &planes.y,
456            y_stride: planes.y_stride,
457            u: &planes.u,
458            u_stride: planes.u_stride,
459            v: &planes.v,
460            v_stride: planes.v_stride,
461            a: planes.a.as_deref(),
462            a_stride: planes.a_stride,
463            width: planes.width,
464            height: planes.height,
465        }
466    }
467}
468
469/// Owned WebP data that wraps libwebp's native memory allocation.
470///
471/// This type provides zero-copy access to encoded WebP data by directly
472/// holding libwebp's allocated buffer. The memory is freed when dropped.
473///
474/// Use this when you want to avoid the copy from libwebp's internal buffer
475/// to a `Vec<u8>`.
476///
477/// # Example
478///
479/// ```rust,no_run
480/// use webpx::{Encoder, Unstoppable};
481///
482/// let rgba = vec![255u8; 100 * 100 * 4];
483/// let webp_data = Encoder::new_rgba(&rgba, 100, 100)
484///     .quality(85.0)
485///     .encode_owned(Unstoppable)?;
486///
487/// // Access the data without copying
488/// let bytes: &[u8] = &webp_data;
489/// println!("Encoded {} bytes", bytes.len());
490///
491/// // Or convert to Vec when needed (copies)
492/// let vec: Vec<u8> = webp_data.into();
493/// # Ok::<(), webpx::At<webpx::Error>>(())
494/// ```
495pub struct WebPData {
496    ptr: *mut u8,
497    len: usize,
498}
499
500// SAFETY: The data is heap-allocated and owned exclusively
501unsafe impl Send for WebPData {}
502unsafe impl Sync for WebPData {}
503
504impl WebPData {
505    /// Create a new WebPData from a raw pointer and length.
506    ///
507    /// # Safety
508    ///
509    /// - `ptr` must be a valid pointer allocated by libwebp's memory allocator
510    /// - `len` must be the exact size of the allocation
511    /// - The caller transfers ownership of the memory to this struct
512    #[cfg(feature = "encode")]
513    #[must_use]
514    pub(crate) unsafe fn from_raw(ptr: *mut u8, len: usize) -> Self {
515        Self { ptr, len }
516    }
517
518    /// Returns the length of the encoded data in bytes.
519    #[must_use]
520    pub fn len(&self) -> usize {
521        self.len
522    }
523
524    /// Returns true if the data is empty.
525    #[must_use]
526    pub fn is_empty(&self) -> bool {
527        self.len == 0
528    }
529
530    /// Returns a slice of the encoded data.
531    #[must_use]
532    pub fn as_slice(&self) -> &[u8] {
533        if self.ptr.is_null() || self.len == 0 {
534            &[]
535        } else {
536            // SAFETY: ptr is valid and len is correct per from_raw contract
537            unsafe { core::slice::from_raw_parts(self.ptr, self.len) }
538        }
539    }
540}
541
542impl Drop for WebPData {
543    fn drop(&mut self) {
544        if !self.ptr.is_null() {
545            // SAFETY: ptr was allocated by libwebp's WebPMemoryWriter
546            unsafe {
547                libwebp_sys::WebPFree(self.ptr as *mut core::ffi::c_void);
548            }
549        }
550    }
551}
552
553impl core::ops::Deref for WebPData {
554    type Target = [u8];
555
556    fn deref(&self) -> &Self::Target {
557        self.as_slice()
558    }
559}
560
561impl AsRef<[u8]> for WebPData {
562    fn as_ref(&self) -> &[u8] {
563        self.as_slice()
564    }
565}
566
567impl From<WebPData> for Vec<u8> {
568    fn from(data: WebPData) -> Self {
569        data.as_slice().to_vec()
570    }
571}
572
573impl core::fmt::Debug for WebPData {
574    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
575        f.debug_struct("WebPData")
576            .field("len", &self.len)
577            .finish_non_exhaustive()
578    }
579}