use crate::config::{EncodeStats, EncoderConfig, Preset};
use crate::error::{EncodingError, Error, Result};
use crate::ffi::mem_writer::MemWriter;
use crate::ffi::picture::Picture;
use crate::types::{EncodePixel, PixelLayout, YuvPlanesRef};
use alloc::vec::Vec;
use enough::Stop;
use imgref::ImgRef;
use whereat::*;
struct StopContext<'a, S: Stop> {
stop: &'a S,
}
extern "C" fn progress_hook<S: Stop>(
_percent: core::ffi::c_int,
picture: *const libwebp_sys::WebPPicture,
) -> core::ffi::c_int {
let ctx = unsafe { &*((*picture).user_data as *const StopContext<S>) };
if ctx.stop.should_stop() {
0 } else {
1 }
}
pub(crate) fn encode_with_config_stats(
data: &[u8],
width: u32,
height: u32,
bpp: u8,
config: &EncoderConfig,
) -> Result<(Vec<u8>, EncodeStats)> {
validate_dimensions(width, height)?;
validate_buffer_size(data.len(), width, height, bpp as u32)?;
let webp_config = config.to_libwebp()?;
let mut picture = Picture::new()?;
picture.inner_mut().width = width as i32;
picture.inner_mut().height = height as i32;
picture.inner_mut().use_argb = 1;
let mut stats = core::mem::MaybeUninit::<libwebp_sys::WebPAuxStats>::zeroed();
picture.inner_mut().stats = stats.as_mut_ptr();
let import_ok = if bpp == 4 {
unsafe {
libwebp_sys::WebPPictureImportRGBA(
picture.as_mut_ptr(),
data.as_ptr(),
(width * 4) as i32,
)
}
} else {
unsafe {
libwebp_sys::WebPPictureImportRGB(
picture.as_mut_ptr(),
data.as_ptr(),
(width * 3) as i32,
)
}
};
if import_ok == 0 {
return Err(at!(Error::EncodeFailed(EncodingError::OutOfMemory)));
}
let mut writer = MemWriter::new();
picture.inner_mut().writer = Some(libwebp_sys::WebPMemoryWrite);
picture.inner_mut().custom_ptr = writer.as_mut_ptr() as *mut _;
let ok = unsafe { libwebp_sys::WebPEncode(&webp_config, picture.as_mut_ptr()) };
let result = if ok == 0 {
let error = EncodingError::from(picture.inner_mut().error_code as i32);
Err(at!(Error::EncodeFailed(error)))
} else {
let webp_data = writer.to_vec();
let stats_val = unsafe { stats.assume_init() };
let encode_stats = EncodeStats::from_libwebp(&stats_val);
Ok((webp_data, encode_stats))
};
#[cfg(feature = "icc")]
if let Ok((mut webp_data, stats)) = result {
if let Some(ref icc) = config.icc_profile {
webp_data = crate::mux::embed_icc(&webp_data, icc)?;
}
if let Some(ref exif) = config.exif_data {
webp_data = crate::mux::embed_exif(&webp_data, exif)?;
}
if let Some(ref xmp) = config.xmp_data {
webp_data = crate::mux::embed_xmp(&webp_data, xmp)?;
}
return Ok((webp_data, stats));
}
result
}
pub(crate) fn encode_with_config_stoppable<S: Stop>(
data: &[u8],
width: u32,
height: u32,
bpp: u8,
config: &EncoderConfig,
stop: &S,
) -> Result<Vec<u8>> {
validate_dimensions(width, height)?;
validate_buffer_size(data.len(), width, height, bpp as u32)?;
stop.check().map_err(|reason| at!(Error::Stopped(reason)))?;
let webp_config = config.to_libwebp()?;
let mut picture = Picture::new()?;
picture.inner_mut().width = width as i32;
picture.inner_mut().height = height as i32;
picture.inner_mut().use_argb = 1;
let import_ok = if bpp == 4 {
unsafe {
libwebp_sys::WebPPictureImportRGBA(
picture.as_mut_ptr(),
data.as_ptr(),
(width * 4) as i32,
)
}
} else {
unsafe {
libwebp_sys::WebPPictureImportRGB(
picture.as_mut_ptr(),
data.as_ptr(),
(width * 3) as i32,
)
}
};
if import_ok == 0 {
return Err(at!(Error::EncodeFailed(EncodingError::OutOfMemory)));
}
let mut writer = MemWriter::new();
picture.inner_mut().writer = Some(libwebp_sys::WebPMemoryWrite);
picture.inner_mut().custom_ptr = writer.as_mut_ptr() as *mut _;
let ctx = StopContext { stop };
picture.inner_mut().progress_hook = Some(progress_hook::<S>);
picture.inner_mut().user_data = &ctx as *const _ as *mut _;
let ok = unsafe { libwebp_sys::WebPEncode(&webp_config, picture.as_mut_ptr()) };
let result = if ok == 0 {
let error_code = picture.inner_mut().error_code as i32;
if error_code == 10 {
if let Err(reason) = stop.check() {
return Err(at!(Error::Stopped(reason)));
}
Err(at!(Error::EncodeFailed(EncodingError::UserAbort)))
} else {
Err(at!(Error::EncodeFailed(EncodingError::from(error_code))))
}
} else {
Ok(writer.to_vec())
};
#[cfg(feature = "icc")]
if let Ok(mut webp_data) = result {
if let Some(ref icc) = config.icc_profile {
webp_data = crate::mux::embed_icc(&webp_data, icc)?;
}
if let Some(ref exif) = config.exif_data {
webp_data = crate::mux::embed_exif(&webp_data, exif)?;
}
if let Some(ref xmp) = config.xmp_data {
webp_data = crate::mux::embed_xmp(&webp_data, xmp)?;
}
return Ok(webp_data);
}
result
}
pub struct Encoder<'a> {
data: EncoderInput<'a>,
width: u32,
height: u32,
config: EncoderConfig,
#[cfg(feature = "icc")]
icc_profile: Option<&'a [u8]>,
}
enum EncoderInput<'a> {
Rgba { data: &'a [u8], stride_bytes: u32 },
Bgra { data: &'a [u8], stride_bytes: u32 },
Rgb { data: &'a [u8], stride_bytes: u32 },
Bgr { data: &'a [u8], stride_bytes: u32 },
Argb { data: &'a [u32], stride_pixels: u32 },
Yuv(YuvPlanesRef<'a>),
}
impl EncoderInput<'_> {
fn requires_exact(&self) -> bool {
match self {
EncoderInput::Argb { .. } => true,
EncoderInput::Yuv(planes) => planes.a.is_some(),
EncoderInput::Rgba { .. }
| EncoderInput::Bgra { .. }
| EncoderInput::Rgb { .. }
| EncoderInput::Bgr { .. } => false,
}
}
}
impl<'a> Encoder<'a> {
#[must_use]
pub fn new_rgba(data: &'a [u8], width: u32, height: u32) -> Self {
Self {
data: EncoderInput::Rgba {
data,
stride_bytes: width.saturating_mul(4),
},
width,
height,
config: EncoderConfig::default(),
#[cfg(feature = "icc")]
icc_profile: None,
}
}
#[must_use]
pub fn new_rgba_stride(data: &'a [u8], width: u32, height: u32, stride_bytes: u32) -> Self {
Self {
data: EncoderInput::Rgba { data, stride_bytes },
width,
height,
config: EncoderConfig::default(),
#[cfg(feature = "icc")]
icc_profile: None,
}
}
#[must_use]
pub fn new_bgra(data: &'a [u8], width: u32, height: u32) -> Self {
Self {
data: EncoderInput::Bgra {
data,
stride_bytes: width.saturating_mul(4),
},
width,
height,
config: EncoderConfig::default(),
#[cfg(feature = "icc")]
icc_profile: None,
}
}
#[must_use]
pub fn new_bgra_stride(data: &'a [u8], width: u32, height: u32, stride_bytes: u32) -> Self {
Self {
data: EncoderInput::Bgra { data, stride_bytes },
width,
height,
config: EncoderConfig::default(),
#[cfg(feature = "icc")]
icc_profile: None,
}
}
#[must_use]
pub fn new_rgb(data: &'a [u8], width: u32, height: u32) -> Self {
Self {
data: EncoderInput::Rgb {
data,
stride_bytes: width.saturating_mul(3),
},
width,
height,
config: EncoderConfig::default(),
#[cfg(feature = "icc")]
icc_profile: None,
}
}
#[must_use]
pub fn new_rgb_stride(data: &'a [u8], width: u32, height: u32, stride_bytes: u32) -> Self {
Self {
data: EncoderInput::Rgb { data, stride_bytes },
width,
height,
config: EncoderConfig::default(),
#[cfg(feature = "icc")]
icc_profile: None,
}
}
#[must_use]
pub fn new_bgr(data: &'a [u8], width: u32, height: u32) -> Self {
Self {
data: EncoderInput::Bgr {
data,
stride_bytes: width.saturating_mul(3),
},
width,
height,
config: EncoderConfig::default(),
#[cfg(feature = "icc")]
icc_profile: None,
}
}
#[must_use]
pub fn new_bgr_stride(data: &'a [u8], width: u32, height: u32, stride_bytes: u32) -> Self {
Self {
data: EncoderInput::Bgr { data, stride_bytes },
width,
height,
config: EncoderConfig::default(),
#[cfg(feature = "icc")]
icc_profile: None,
}
}
#[must_use]
pub fn new_yuv(planes: YuvPlanesRef<'a>) -> Self {
let width = planes.width;
let height = planes.height;
Self {
data: EncoderInput::Yuv(planes),
width,
height,
config: EncoderConfig::default(),
#[cfg(feature = "icc")]
icc_profile: None,
}
}
#[must_use]
pub fn new_argb(data: &'a [u32], width: u32, height: u32) -> Self {
Self {
data: EncoderInput::Argb {
data,
stride_pixels: width,
},
width,
height,
config: EncoderConfig::default(),
#[cfg(feature = "icc")]
icc_profile: None,
}
}
#[must_use]
pub fn new_argb_stride(data: &'a [u32], width: u32, height: u32, stride_pixels: u32) -> Self {
Self {
data: EncoderInput::Argb {
data,
stride_pixels,
},
width,
height,
config: EncoderConfig::default(),
#[cfg(feature = "icc")]
icc_profile: None,
}
}
#[must_use]
pub fn from_img<P: EncodePixel>(img: ImgRef<'a, P>) -> Self {
let bpp = P::LAYOUT.bytes_per_pixel();
let data = unsafe {
core::slice::from_raw_parts(
img.buf().as_ptr() as *const u8,
img.buf().len().saturating_mul(bpp),
)
};
let stride_bytes_usize = img.stride().saturating_mul(bpp);
let stride_bytes = u32::try_from(stride_bytes_usize).unwrap_or(u32::MAX);
Self::from_pixels_internal(
data,
img.width() as u32,
img.height() as u32,
stride_bytes,
P::LAYOUT,
)
}
#[must_use]
pub fn from_pixels<P: EncodePixel>(pixels: &'a [P], width: u32, height: u32) -> Self {
let bpp = P::LAYOUT.bytes_per_pixel();
let data = unsafe {
core::slice::from_raw_parts(
pixels.as_ptr() as *const u8,
pixels.len().saturating_mul(bpp),
)
};
let stride_bytes = width * bpp as u32;
Self::from_pixels_internal(data, width, height, stride_bytes, P::LAYOUT)
}
#[must_use]
pub fn from_pixels_stride<P: EncodePixel>(
pixels: &'a [P],
width: u32,
height: u32,
stride_pixels: u32,
) -> Self {
let bpp = P::LAYOUT.bytes_per_pixel();
let data = unsafe {
core::slice::from_raw_parts(
pixels.as_ptr() as *const u8,
pixels.len().saturating_mul(bpp),
)
};
let stride_bytes =
u32::try_from((stride_pixels as u64).saturating_mul(bpp as u64)).unwrap_or(u32::MAX);
Self::from_pixels_internal(data, width, height, stride_bytes, P::LAYOUT)
}
fn from_pixels_internal(
data: &'a [u8],
width: u32,
height: u32,
stride_bytes: u32,
format: PixelLayout,
) -> Self {
let input = match format {
PixelLayout::Rgba => EncoderInput::Rgba { data, stride_bytes },
PixelLayout::Bgra => EncoderInput::Bgra { data, stride_bytes },
PixelLayout::Rgb => EncoderInput::Rgb { data, stride_bytes },
PixelLayout::Bgr => EncoderInput::Bgr { data, stride_bytes },
};
Self {
data: input,
width,
height,
config: EncoderConfig::default(),
#[cfg(feature = "icc")]
icc_profile: None,
}
}
#[must_use]
pub fn quality(mut self, quality: f32) -> Self {
self.config = self.config.quality(quality);
self
}
#[must_use]
pub fn preset(mut self, preset: Preset) -> Self {
self.config = self.config.preset(preset);
self
}
#[must_use]
pub fn lossless(mut self, lossless: bool) -> Self {
self.config = self.config.lossless(lossless);
self
}
#[must_use]
pub fn method(mut self, method: u8) -> Self {
self.config = self.config.method(method);
self
}
#[must_use]
pub fn near_lossless(mut self, value: u8) -> Self {
self.config = self.config.near_lossless(value);
self
}
#[must_use]
pub fn alpha_quality(mut self, quality: u8) -> Self {
self.config = self.config.alpha_quality(quality);
self
}
#[must_use]
pub fn exact(mut self, exact: bool) -> Self {
self.config = self.config.exact(exact);
self
}
#[must_use]
pub fn target_size(mut self, size: u32) -> Self {
self.config = self.config.target_size(size);
self
}
#[must_use]
pub fn sharp_yuv(mut self, enable: bool) -> Self {
self.config = self.config.sharp_yuv(enable);
self
}
#[must_use]
pub fn config(mut self, config: EncoderConfig) -> Self {
self.config = config;
self
}
#[cfg(feature = "icc")]
#[must_use]
pub fn icc_profile(mut self, profile: &'a [u8]) -> Self {
self.icc_profile = Some(profile);
self
}
pub fn encode<S: Stop>(self, stop: S) -> Result<Vec<u8>> {
validate_dimensions(self.width, self.height)?;
stop.check().map_err(|reason| at!(Error::Stopped(reason)))?;
let mut webp_config = self.config.to_libwebp()?;
if self.data.requires_exact() {
webp_config.exact = 1;
}
let mut picture = Picture::new()?;
picture.inner_mut().width = self.width as i32;
picture.inner_mut().height = self.height as i32;
let import_ok = match &self.data {
EncoderInput::Rgba { data, stride_bytes } => {
validate_buffer_size_stride(data.len(), self.width, self.height, *stride_bytes, 4)?;
picture.inner_mut().use_argb = 1;
unsafe {
libwebp_sys::WebPPictureImportRGBA(
picture.as_mut_ptr(),
data.as_ptr(),
*stride_bytes as i32,
)
}
}
EncoderInput::Bgra { data, stride_bytes } => {
validate_buffer_size_stride(data.len(), self.width, self.height, *stride_bytes, 4)?;
picture.inner_mut().use_argb = 1;
unsafe {
libwebp_sys::WebPPictureImportBGRA(
picture.as_mut_ptr(),
data.as_ptr(),
*stride_bytes as i32,
)
}
}
EncoderInput::Rgb { data, stride_bytes } => {
validate_buffer_size_stride(data.len(), self.width, self.height, *stride_bytes, 3)?;
picture.inner_mut().use_argb = 1;
unsafe {
libwebp_sys::WebPPictureImportRGB(
picture.as_mut_ptr(),
data.as_ptr(),
*stride_bytes as i32,
)
}
}
EncoderInput::Bgr { data, stride_bytes } => {
validate_buffer_size_stride(data.len(), self.width, self.height, *stride_bytes, 3)?;
picture.inner_mut().use_argb = 1;
unsafe {
libwebp_sys::WebPPictureImportBGR(
picture.as_mut_ptr(),
data.as_ptr(),
*stride_bytes as i32,
)
}
}
EncoderInput::Argb {
data,
stride_pixels,
} => {
let min_len = (*stride_pixels as usize).saturating_mul(self.height as usize);
if data.len() < min_len {
return Err(at!(Error::InvalidInput(alloc::format!(
"ARGB buffer too small: got {} pixels, expected {}",
data.len(),
min_len
))));
}
if *stride_pixels < self.width {
return Err(at!(Error::InvalidInput(alloc::format!(
"ARGB stride too small: got {}, minimum {}",
stride_pixels,
self.width
))));
}
crate::ffi::validate::stride_fits_i32(
*stride_pixels as usize,
"Encoder::new_argb_stride",
)?;
let pic = picture.inner_mut();
pic.use_argb = 1;
pic.argb = data.as_ptr() as *mut u32;
pic.argb_stride = *stride_pixels as i32;
1 }
EncoderInput::Yuv(planes) => {
validate_yuv_planes(planes)?;
let pic = picture.inner_mut();
pic.use_argb = 0;
pic.colorspace = if planes.a.is_some() {
libwebp_sys::WebPEncCSP::WEBP_YUV420A
} else {
libwebp_sys::WebPEncCSP::WEBP_YUV420
};
pic.y = planes.y.as_ptr() as *mut _;
pic.u = planes.u.as_ptr() as *mut _;
pic.v = planes.v.as_ptr() as *mut _;
pic.y_stride = planes.y_stride as i32;
pic.uv_stride = planes.u_stride as i32;
if let Some(a) = &planes.a {
pic.a = a.as_ptr() as *mut _;
pic.a_stride = planes.a_stride as i32;
}
1 }
};
if import_ok == 0 {
return Err(at!(Error::EncodeFailed(EncodingError::OutOfMemory)));
}
let mut writer = MemWriter::new();
picture.inner_mut().writer = Some(libwebp_sys::WebPMemoryWrite);
picture.inner_mut().custom_ptr = writer.as_mut_ptr() as *mut _;
let ctx = StopContext { stop: &stop };
picture.inner_mut().progress_hook = Some(progress_hook::<S>);
picture.inner_mut().user_data = &ctx as *const _ as *mut _;
let ok = unsafe { libwebp_sys::WebPEncode(&webp_config, picture.as_mut_ptr()) };
if ok == 0 {
let error_code = picture.inner_mut().error_code as i32;
if error_code == 10 {
if let Err(reason) = stop.check() {
return Err(at!(Error::Stopped(reason)));
}
Err(at!(Error::EncodeFailed(EncodingError::UserAbort)))
} else {
Err(at!(Error::EncodeFailed(EncodingError::from(error_code))))
}
} else {
let webp_data = writer.to_vec();
#[cfg(feature = "icc")]
if let Some(icc) = self.icc_profile {
return crate::mux::embed_icc(&webp_data, icc);
}
Ok(webp_data)
}
}
pub fn encode_owned<S: Stop>(self, stop: S) -> Result<crate::WebPData> {
validate_dimensions(self.width, self.height)?;
stop.check().map_err(|reason| at!(Error::Stopped(reason)))?;
let mut webp_config = self.config.to_libwebp()?;
if self.data.requires_exact() {
webp_config.exact = 1;
}
let mut picture = Picture::new()?;
picture.inner_mut().width = self.width as i32;
picture.inner_mut().height = self.height as i32;
let import_ok = self.import_pixels(picture.inner_mut())?;
if import_ok == 0 {
return Err(at!(Error::EncodeFailed(EncodingError::OutOfMemory)));
}
let mut writer = MemWriter::new();
picture.inner_mut().writer = Some(libwebp_sys::WebPMemoryWrite);
picture.inner_mut().custom_ptr = writer.as_mut_ptr() as *mut _;
let ctx = StopContext { stop: &stop };
picture.inner_mut().progress_hook = Some(progress_hook::<S>);
picture.inner_mut().user_data = &ctx as *const _ as *mut _;
let ok = unsafe { libwebp_sys::WebPEncode(&webp_config, picture.as_mut_ptr()) };
if ok == 0 {
let error_code = picture.inner_mut().error_code as i32;
if error_code == 10 {
if let Err(reason) = stop.check() {
return Err(at!(Error::Stopped(reason)));
}
return Err(at!(Error::EncodeFailed(EncodingError::UserAbort)));
}
return Err(at!(Error::EncodeFailed(EncodingError::from(error_code))));
}
#[cfg(feature = "icc")]
if self.icc_profile.is_some() {
return Err(at!(Error::InvalidConfig(
"ICC profile embedding not supported with encode_owned(), use encode() instead"
.into()
)));
}
Ok(writer.into_webp_data())
}
pub fn encode_into<S: Stop>(self, stop: S, output: &mut Vec<u8>) -> Result<()> {
let data = self.encode_owned(stop)?;
output.extend_from_slice(&data);
Ok(())
}
#[cfg(feature = "std")]
pub fn encode_to_writer<S: Stop, W: std::io::Write>(
self,
stop: S,
mut writer: W,
) -> Result<()> {
let data = self.encode_owned(stop)?;
writer
.write_all(&data)
.map_err(|e| at!(Error::IoError(e.to_string())))?;
Ok(())
}
fn import_pixels(&self, picture: &mut libwebp_sys::WebPPicture) -> Result<i32> {
let import_ok = match &self.data {
EncoderInput::Rgba { data, stride_bytes } => {
validate_buffer_size_stride(data.len(), self.width, self.height, *stride_bytes, 4)?;
picture.use_argb = 1;
unsafe {
libwebp_sys::WebPPictureImportRGBA(picture, data.as_ptr(), *stride_bytes as i32)
}
}
EncoderInput::Bgra { data, stride_bytes } => {
validate_buffer_size_stride(data.len(), self.width, self.height, *stride_bytes, 4)?;
picture.use_argb = 1;
unsafe {
libwebp_sys::WebPPictureImportBGRA(picture, data.as_ptr(), *stride_bytes as i32)
}
}
EncoderInput::Rgb { data, stride_bytes } => {
validate_buffer_size_stride(data.len(), self.width, self.height, *stride_bytes, 3)?;
picture.use_argb = 1;
unsafe {
libwebp_sys::WebPPictureImportRGB(picture, data.as_ptr(), *stride_bytes as i32)
}
}
EncoderInput::Bgr { data, stride_bytes } => {
validate_buffer_size_stride(data.len(), self.width, self.height, *stride_bytes, 3)?;
picture.use_argb = 1;
unsafe {
libwebp_sys::WebPPictureImportBGR(picture, data.as_ptr(), *stride_bytes as i32)
}
}
EncoderInput::Argb {
data,
stride_pixels,
} => {
let min_len = (*stride_pixels as usize).saturating_mul(self.height as usize);
if data.len() < min_len {
return Err(at!(Error::InvalidInput(alloc::format!(
"ARGB buffer too small: got {} pixels, expected {}",
data.len(),
min_len
))));
}
if *stride_pixels < self.width {
return Err(at!(Error::InvalidInput(alloc::format!(
"ARGB stride too small: got {}, minimum {}",
stride_pixels,
self.width
))));
}
crate::ffi::validate::stride_fits_i32(
*stride_pixels as usize,
"Encoder::new_argb_stride",
)?;
picture.use_argb = 1;
picture.argb = data.as_ptr() as *mut u32;
picture.argb_stride = *stride_pixels as i32;
1
}
EncoderInput::Yuv(planes) => {
validate_yuv_planes(planes)?;
picture.use_argb = 0;
picture.colorspace = if planes.a.is_some() {
libwebp_sys::WebPEncCSP::WEBP_YUV420A
} else {
libwebp_sys::WebPEncCSP::WEBP_YUV420
};
picture.y = planes.y.as_ptr() as *mut _;
picture.u = planes.u.as_ptr() as *mut _;
picture.v = planes.v.as_ptr() as *mut _;
picture.y_stride = planes.y_stride as i32;
picture.uv_stride = planes.u_stride as i32;
if let Some(a) = &planes.a {
picture.a = a.as_ptr() as *mut _;
picture.a_stride = planes.a_stride as i32;
}
1
}
};
Ok(import_ok)
}
}
pub(crate) fn validate_dimensions(width: u32, height: u32) -> Result<()> {
const MAX_DIMENSION: u32 = 16383;
if width == 0 || height == 0 {
return Err(at!(Error::InvalidInput(
"width and height must be non-zero".into(),
)));
}
if width > MAX_DIMENSION || height > MAX_DIMENSION {
return Err(at!(Error::InvalidInput(alloc::format!(
"dimensions exceed maximum ({} x {})",
MAX_DIMENSION,
MAX_DIMENSION
))));
}
Ok(())
}
pub(crate) fn validate_buffer_size(size: usize, width: u32, height: u32, bpp: u32) -> Result<()> {
let expected = (width as usize)
.saturating_mul(height as usize)
.saturating_mul(bpp as usize);
if size < expected {
return Err(at!(Error::InvalidInput(alloc::format!(
"buffer too small: got {}, expected {}",
size,
expected
))));
}
Ok(())
}
pub(crate) fn validate_yuv_planes(planes: &YuvPlanesRef<'_>) -> Result<()> {
let width = planes.width as usize;
let height = planes.height as usize;
let uv_width = width.div_ceil(2);
let uv_height = height.div_ceil(2);
if planes.y_stride < width {
return Err(at!(Error::InvalidInput(alloc::format!(
"Y stride too small: got {}, minimum {}",
planes.y_stride,
width
))));
}
if planes.u_stride < uv_width {
return Err(at!(Error::InvalidInput(alloc::format!(
"U stride too small: got {}, minimum {}",
planes.u_stride,
uv_width
))));
}
if planes.v_stride < uv_width {
return Err(at!(Error::InvalidInput(alloc::format!(
"V stride too small: got {}, minimum {}",
planes.v_stride,
uv_width
))));
}
if planes.u_stride != planes.v_stride {
return Err(at!(Error::InvalidInput(alloc::format!(
"U and V strides must match (got u={}, v={}); libwebp uses a single uv_stride",
planes.u_stride,
planes.v_stride
))));
}
crate::ffi::validate::stride_fits_i32(planes.y_stride, "YuvPlanes::y_stride")?;
crate::ffi::validate::stride_fits_i32(planes.u_stride, "YuvPlanes::uv_stride")?;
let y_min = planes.y_stride.saturating_mul(height);
if planes.y.len() < y_min {
return Err(at!(Error::InvalidInput(alloc::format!(
"Y plane too short: got {}, expected at least {} (y_stride {} × height {})",
planes.y.len(),
y_min,
planes.y_stride,
height
))));
}
let u_min = planes.u_stride.saturating_mul(uv_height);
if planes.u.len() < u_min {
return Err(at!(Error::InvalidInput(alloc::format!(
"U plane too short: got {}, expected at least {} (u_stride {} × uv_height {})",
planes.u.len(),
u_min,
planes.u_stride,
uv_height
))));
}
let v_min = planes.v_stride.saturating_mul(uv_height);
if planes.v.len() < v_min {
return Err(at!(Error::InvalidInput(alloc::format!(
"V plane too short: got {}, expected at least {} (v_stride {} × uv_height {})",
planes.v.len(),
v_min,
planes.v_stride,
uv_height
))));
}
if let Some(a) = planes.a {
if planes.a_stride < width {
return Err(at!(Error::InvalidInput(alloc::format!(
"A stride too small: got {}, minimum {}",
planes.a_stride,
width
))));
}
crate::ffi::validate::stride_fits_i32(planes.a_stride, "YuvPlanes::a_stride")?;
let a_min = planes.a_stride.saturating_mul(height);
if a.len() < a_min {
return Err(at!(Error::InvalidInput(alloc::format!(
"A plane too short: got {}, expected at least {} (a_stride {} × height {})",
a.len(),
a_min,
planes.a_stride,
height
))));
}
}
Ok(())
}
pub(crate) fn validate_buffer_size_stride(
size: usize,
width: u32,
height: u32,
stride_bytes: u32,
bpp: u32,
) -> Result<()> {
crate::ffi::validate::stride_fits_i32(stride_bytes as usize, "validate_buffer_size_stride")?;
let min_stride = (width as usize).saturating_mul(bpp as usize);
if (stride_bytes as usize) < min_stride {
return Err(at!(Error::InvalidInput(alloc::format!(
"stride too small: got {}, minimum {}",
stride_bytes,
min_stride
))));
}
let expected = (stride_bytes as usize).saturating_mul(height as usize);
if size < expected {
return Err(at!(Error::InvalidInput(alloc::format!(
"buffer too small: got {}, expected {} (stride {} × height {})",
size,
expected,
stride_bytes,
height
))));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_dimensions() {
assert!(validate_dimensions(0, 100).is_err());
assert!(validate_dimensions(100, 0).is_err());
assert!(validate_dimensions(20000, 100).is_err());
assert!(validate_dimensions(100, 100).is_ok());
}
#[test]
fn test_validate_buffer_size() {
assert!(validate_buffer_size(100, 10, 10, 4).is_err());
assert!(validate_buffer_size(400, 10, 10, 4).is_ok());
assert!(validate_buffer_size(500, 10, 10, 4).is_ok());
}
}