Skip to main content

webpx/
types.rs

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