use std::ffi::c_int;
use std::os::raw::c_void;
use std::ptr;
use std::slice;
use libwebp_sys::*;
pub fn decode_webp_rgb(data: &[u8]) -> Result<(u32, u32, Vec<u8>), String> {
let mut width: c_int = 0;
let mut height: c_int = 0;
let ok = unsafe { WebPGetInfo(data.as_ptr(), data.len(), &mut width, &mut height) };
if ok == 0 {
return Err("WebPGetInfo failed: invalid WebP data".into());
}
if width <= 0 || height <= 0 {
return Err(format!("Invalid dimensions: {width}x{height}"));
}
let w = width as u32;
let h = height as u32;
let ptr = unsafe { WebPDecodeRGB(data.as_ptr(), data.len(), &mut width, &mut height) };
if ptr.is_null() {
return Err("WebPDecodeRGB failed".into());
}
let len = (w as usize) * (h as usize) * 3;
let pixels = unsafe { slice::from_raw_parts(ptr, len) }.to_vec();
unsafe { WebPFree(ptr as *mut c_void) };
Ok((w, h, pixels))
}
pub fn build_webp_config(
lossless: bool,
quality: f32,
method: u32,
near_lossless: u32,
) -> Result<WebPConfig, String> {
let mut config = WebPConfig::new_with_preset(WebPPreset::WEBP_PRESET_DEFAULT, quality)
.map_err(|()| "WebPConfigInit failed")?;
config.lossless = if lossless { 1 } else { 0 };
config.quality = quality;
config.method = method as c_int;
if lossless {
config.near_lossless = near_lossless as c_int;
}
let valid = unsafe { WebPValidateConfig(&config) };
if valid == 0 {
return Err("WebPValidateConfig failed: invalid config".into());
}
Ok(config)
}
pub fn encode_webp_rgb(
pixels: &[u8],
width: u32,
height: u32,
config: &WebPConfig,
) -> Result<Vec<u8>, String> {
let expected_len = (width as usize) * (height as usize) * 3;
if pixels.len() != expected_len {
return Err(format!(
"pixel buffer length mismatch: expected {expected_len}, got {}",
pixels.len()
));
}
unsafe {
let mut picture: WebPPicture = std::mem::zeroed();
if WebPPictureInitInternal(&mut picture, WEBP_ENCODER_ABI_VERSION as c_int) == 0 {
return Err("WebPPictureInit failed".into());
}
picture.width = width as c_int;
picture.height = height as c_int;
picture.use_argb = if config.lossless != 0 { 1 } else { 0 };
let mut writer: WebPMemoryWriter = std::mem::zeroed();
WebPMemoryWriterInit(&mut writer);
picture.writer = Some(WebPMemoryWrite);
picture.custom_ptr = &mut writer as *mut WebPMemoryWriter as *mut c_void;
let stride = (width * 3) as c_int;
if WebPPictureImportRGB(&mut picture, pixels.as_ptr(), stride) == 0 {
WebPPictureFree(&mut picture);
WebPMemoryWriterClear(&mut writer);
return Err("WebPPictureImportRGB failed".into());
}
let ok = WebPEncode(config, &mut picture);
if ok == 0 {
let err_code = picture.error_code;
WebPPictureFree(&mut picture);
WebPMemoryWriterClear(&mut writer);
return Err(format!("WebPEncode failed: error code {:?}", err_code));
}
let output = slice::from_raw_parts(writer.mem, writer.size).to_vec();
WebPPictureFree(&mut picture);
WebPMemoryWriterClear(&mut writer);
Ok(output)
}
}
pub fn mux_webp(
bitstream: &[u8],
exif: Option<&[u8]>,
icc: Option<&[u8]>,
xmp: Option<&[u8]>,
) -> Result<Vec<u8>, String> {
if exif.is_none() && icc.is_none() && xmp.is_none() {
return Ok(bitstream.to_vec());
}
unsafe {
let mux = WebPMuxNew();
if mux.is_null() {
return Err("WebPMuxNew failed".into());
}
let image_data = WebPData {
bytes: bitstream.as_ptr(),
size: bitstream.len(),
};
let rc = WebPMuxSetImage(mux, &image_data, 1);
if rc != WebPMuxError::WEBP_MUX_OK {
WebPMuxDelete(mux);
return Err(format!("WebPMuxSetImage failed: {:?}", rc));
}
if let Some(data) = exif {
let chunk_data = WebPData {
bytes: data.as_ptr(),
size: data.len(),
};
let rc = WebPMuxSetChunk(mux, b"EXIF".as_ptr() as *const i8, &chunk_data, 1);
if rc != WebPMuxError::WEBP_MUX_OK {
WebPMuxDelete(mux);
return Err(format!("WebPMuxSetChunk(EXIF) failed: {:?}", rc));
}
}
if let Some(data) = icc {
let chunk_data = WebPData {
bytes: data.as_ptr(),
size: data.len(),
};
let rc = WebPMuxSetChunk(mux, b"ICCP".as_ptr() as *const i8, &chunk_data, 1);
if rc != WebPMuxError::WEBP_MUX_OK {
WebPMuxDelete(mux);
return Err(format!("WebPMuxSetChunk(ICCP) failed: {:?}", rc));
}
}
if let Some(data) = xmp {
let chunk_data = WebPData {
bytes: data.as_ptr(),
size: data.len(),
};
let rc = WebPMuxSetChunk(mux, b"XMP ".as_ptr() as *const i8, &chunk_data, 1);
if rc != WebPMuxError::WEBP_MUX_OK {
WebPMuxDelete(mux);
return Err(format!("WebPMuxSetChunk(XMP) failed: {:?}", rc));
}
}
let mut output_data: WebPData = WebPData {
bytes: ptr::null(),
size: 0,
};
let rc = WebPMuxAssemble(mux, &mut output_data);
if rc != WebPMuxError::WEBP_MUX_OK {
WebPMuxDelete(mux);
return Err(format!("WebPMuxAssemble failed: {:?}", rc));
}
let result = slice::from_raw_parts(output_data.bytes, output_data.size).to_vec();
WebPDataClear(&mut output_data);
WebPMuxDelete(mux);
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_config_lossy() {
let config = build_webp_config(false, 75.0, 4, 100).expect("config should be valid");
assert_eq!(config.lossless, 0);
assert!((config.quality - 75.0).abs() < f32::EPSILON);
}
#[test]
fn test_build_config_lossless() {
let config = build_webp_config(true, 75.0, 4, 80).expect("config should be valid");
assert_eq!(config.lossless, 1);
assert_eq!(config.near_lossless, 80);
}
#[test]
fn test_lossless_roundtrip() {
let pixels: Vec<u8> = vec![
255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255, ];
let config = build_webp_config(true, 75.0, 4, 100).unwrap();
let encoded = encode_webp_rgb(&pixels, 2, 2, &config).expect("encode");
assert!(!encoded.is_empty());
let (w, h, decoded) = decode_webp_rgb(&encoded).expect("decode");
assert_eq!(w, 2);
assert_eq!(h, 2);
assert_eq!(decoded, pixels, "lossless round-trip should be exact");
}
#[test]
fn test_lossy_encode_decode() {
let pixels: Vec<u8> = vec![128; 4 * 4 * 3];
let config = build_webp_config(false, 50.0, 4, 100).unwrap();
let encoded = encode_webp_rgb(&pixels, 4, 4, &config).expect("encode");
assert!(!encoded.is_empty());
let (w, h, decoded) = decode_webp_rgb(&encoded).expect("decode");
assert_eq!(w, 4);
assert_eq!(h, 4);
assert_eq!(decoded.len(), 4 * 4 * 3);
}
#[test]
fn test_mux_with_exif() {
let pixels: Vec<u8> = vec![100; 2 * 2 * 3];
let config = build_webp_config(true, 75.0, 0, 100).unwrap();
let bitstream = encode_webp_rgb(&pixels, 2, 2, &config).unwrap();
let fake_exif = b"Exif\x00\x00test_exif_data";
let muxed = mux_webp(&bitstream, Some(fake_exif), None, None).expect("mux");
assert_eq!(&muxed[0..4], b"RIFF");
assert_eq!(&muxed[8..12], b"WEBP");
let has_exif = muxed.windows(4).any(|w| w == b"EXIF");
assert!(has_exif, "muxed output should contain EXIF chunk");
}
#[test]
fn test_mux_with_icc() {
let pixels: Vec<u8> = vec![100; 2 * 2 * 3];
let config = build_webp_config(true, 75.0, 0, 100).unwrap();
let bitstream = encode_webp_rgb(&pixels, 2, 2, &config).unwrap();
let fake_icc = b"fake_icc_profile_data";
let muxed = mux_webp(&bitstream, None, Some(fake_icc), None).expect("mux");
let has_iccp = muxed.windows(4).any(|w| w == b"ICCP");
assert!(has_iccp, "muxed output should contain ICCP chunk");
}
#[test]
fn test_mux_with_xmp() {
let pixels: Vec<u8> = vec![100; 2 * 2 * 3];
let config = build_webp_config(true, 75.0, 0, 100).unwrap();
let bitstream = encode_webp_rgb(&pixels, 2, 2, &config).unwrap();
let fake_xmp = b"<x:xmpmeta>test</x:xmpmeta>";
let muxed = mux_webp(&bitstream, None, None, Some(fake_xmp)).expect("mux");
let has_xmp = muxed.windows(4).any(|w| w == b"XMP ");
assert!(has_xmp, "muxed output should contain XMP chunk");
}
#[test]
fn test_mux_no_metadata_returns_original() {
let data = b"some_data";
let result = mux_webp(data, None, None, None).unwrap();
assert_eq!(result, data);
}
#[test]
fn test_decode_invalid_data() {
let result = decode_webp_rgb(b"not a webp");
assert!(result.is_err());
}
#[test]
fn test_encode_buffer_mismatch() {
let pixels = vec![0u8; 10]; let config = build_webp_config(false, 75.0, 4, 100).unwrap();
let result = encode_webp_rgb(&pixels, 2, 2, &config);
assert!(result.is_err());
}
}