use super::{
chunk_total_size, copy_chunk, read_all_chunks, read_chunk_header, skip_chunk_data,
validate_riff_header, write_chunk, write_riff_header, RiffChunk, CHUNK_HEADER_SIZE,
RIFF_HEADER_SIZE,
};
use crate::core::error::{XmpError, XmpResult};
use crate::core::metadata::XmpMeta;
use crate::files::handler::{FileHandler, XmpOptions};
use std::io::{Read, Seek, SeekFrom, Write};
const WEBP_SIGNATURE: &[u8; 4] = b"WEBP";
const XMP_CHUNK_ID: &[u8; 4] = b"XMP ";
const VP8X_CHUNK_ID: &[u8; 4] = b"VP8X";
const VP8_CHUNK_ID: &[u8; 4] = b"VP8 ";
const VP8L_CHUNK_ID: &[u8; 4] = b"VP8L";
const VP8X_XMP_FLAG: u8 = 0x04;
#[derive(Debug, Clone, Copy, Default)]
pub struct WebpHandler;
impl FileHandler for WebpHandler {
fn can_handle<R: Read + Seek>(&self, reader: &mut R) -> XmpResult<bool> {
let pos = reader.stream_position()?;
let file_len = reader.seek(SeekFrom::End(0))?;
reader.seek(SeekFrom::Start(pos))?;
if file_len < 12 {
return Ok(false);
}
match validate_riff_header(reader) {
Ok(form_type) => {
reader.seek(SeekFrom::Start(pos))?;
Ok(&form_type == WEBP_SIGNATURE)
}
Err(_) => {
reader.seek(SeekFrom::Start(pos))?;
Ok(false)
}
}
}
fn read_xmp<R: Read + Seek>(
&self,
reader: &mut R,
_options: &XmpOptions,
) -> XmpResult<Option<XmpMeta>> {
Self::read_xmp(reader)
}
fn write_xmp<R: Read + Seek, W: Write + Seek>(
&self,
reader: &mut R,
writer: &mut W,
meta: &XmpMeta,
) -> XmpResult<()> {
Self::write_xmp(reader, writer, meta)
}
fn format_name(&self) -> &'static str {
"WebP"
}
fn extensions(&self) -> &'static [&'static str] {
&["webp"]
}
}
impl WebpHandler {
pub fn read_xmp<R: Read + Seek>(mut reader: R) -> XmpResult<Option<XmpMeta>> {
let form_type = validate_riff_header(&mut reader)?;
if &form_type != WEBP_SIGNATURE {
return Err(XmpError::BadValue("Not a valid WebP file".to_string()));
}
reader.seek(SeekFrom::Start(RIFF_HEADER_SIZE))?;
while let Ok(chunk) = read_chunk_header(&mut reader) {
if chunk.id == *XMP_CHUNK_ID {
let mut xmp_data = vec![0u8; chunk.size as usize];
reader.read_exact(&mut xmp_data)?;
let xmp_str = String::from_utf8(xmp_data)
.map_err(|e| XmpError::ParseError(format!("Invalid UTF-8 in XMP: {}", e)))?;
return XmpMeta::parse(&xmp_str).map(Some);
}
skip_chunk_data(&mut reader, chunk.size)?;
}
Ok(None)
}
pub fn write_xmp<R: Read + Seek, W: Write + Seek>(
mut reader: R,
mut writer: W,
meta: &XmpMeta,
) -> XmpResult<()> {
let form_type = validate_riff_header(&mut reader)?;
if &form_type != WEBP_SIGNATURE {
return Err(XmpError::BadValue("Not a valid WebP file".to_string()));
}
let xmp_packet = meta.serialize_packet()?;
let xmp_bytes = xmp_packet.as_bytes();
let chunks = read_all_chunks(&mut reader)?;
let xmp_chunk = chunks.iter().find(|c| c.id == *XMP_CHUNK_ID);
let vp8x_chunk = chunks.iter().find(|c| c.id == *VP8X_CHUNK_ID);
let old_xmp_size = xmp_chunk.map(|c| c.total_size()).unwrap_or(0);
let new_xmp_size = chunk_total_size(xmp_bytes.len() as u32);
reader.seek(SeekFrom::Start(4))?;
let mut old_file_size_bytes = [0u8; 4];
reader.read_exact(&mut old_file_size_bytes)?;
let old_file_size = u32::from_le_bytes(old_file_size_bytes);
let new_file_size = if xmp_chunk.is_some() {
old_file_size - old_xmp_size as u32 + new_xmp_size as u32
} else {
let vp8x_addition = if vp8x_chunk.is_none() {
chunk_total_size(10) as u32
} else {
0
};
old_file_size + new_xmp_size as u32 + vp8x_addition
};
write_riff_header(&mut writer, new_file_size, WEBP_SIGNATURE)?;
let needs_vp8x = vp8x_chunk.is_none();
let mut xmp_written = false;
let mut vp8x_written = false;
for chunk in &chunks {
if chunk.id == *XMP_CHUNK_ID {
continue; }
if chunk.id == *VP8X_CHUNK_ID {
reader.seek(SeekFrom::Start(chunk.offset + CHUNK_HEADER_SIZE))?;
let mut vp8x_data = vec![0u8; chunk.size as usize];
reader.read_exact(&mut vp8x_data)?;
if !vp8x_data.is_empty() {
vp8x_data[0] |= VP8X_XMP_FLAG;
}
write_chunk(&mut writer, VP8X_CHUNK_ID, &vp8x_data)?;
vp8x_written = true;
write_chunk(&mut writer, XMP_CHUNK_ID, xmp_bytes)?;
xmp_written = true;
continue;
}
if needs_vp8x
&& !vp8x_written
&& (chunk.id == *VP8_CHUNK_ID || chunk.id == *VP8L_CHUNK_ID)
{
let (width, height) = Self::read_image_dimensions(&mut reader, chunk)?;
Self::write_vp8x_chunk(&mut writer, width, height, VP8X_XMP_FLAG)?;
vp8x_written = true;
write_chunk(&mut writer, XMP_CHUNK_ID, xmp_bytes)?;
xmp_written = true;
}
copy_chunk(&mut reader, &mut writer, chunk)?;
}
if !xmp_written {
write_chunk(&mut writer, XMP_CHUNK_ID, xmp_bytes)?;
}
Ok(())
}
fn write_vp8x_chunk<W: Write>(
writer: &mut W,
width: u32,
height: u32,
flags: u8,
) -> XmpResult<()> {
let mut data = [0u8; 10];
data[0] = flags;
let w = width.saturating_sub(1) & 0xFFFFFF;
data[4] = w as u8;
data[5] = (w >> 8) as u8;
data[6] = (w >> 16) as u8;
let h = height.saturating_sub(1) & 0xFFFFFF;
data[7] = h as u8;
data[8] = (h >> 8) as u8;
data[9] = (h >> 16) as u8;
write_chunk(writer, VP8X_CHUNK_ID, &data)
}
fn read_image_dimensions<R: Read + Seek>(
reader: &mut R,
chunk: &RiffChunk,
) -> XmpResult<(u32, u32)> {
reader.seek(SeekFrom::Start(chunk.offset + CHUNK_HEADER_SIZE))?;
if chunk.id == *VP8_CHUNK_ID {
let mut header = [0u8; 10];
reader.read_exact(&mut header)?;
if header[3] == 0x9D && header[4] == 0x01 && header[5] == 0x2A {
let width = u16::from_le_bytes([header[6], header[7]]) & 0x3FFF;
let height = u16::from_le_bytes([header[8], header[9]]) & 0x3FFF;
return Ok((width as u32, height as u32));
}
} else if chunk.id == *VP8L_CHUNK_ID {
let mut header = [0u8; 5];
reader.read_exact(&mut header)?;
if header[0] == 0x2F {
let bits = u32::from_le_bytes([header[1], header[2], header[3], header[4]]);
let width = (bits & 0x3FFF) + 1;
let height = ((bits >> 14) & 0x3FFF) + 1;
return Ok((width, height));
}
}
Ok((1, 1)) }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::namespace::ns;
use crate::types::value::XmpValue;
use std::io::Cursor;
fn create_minimal_webp() -> Vec<u8> {
let mut webp = Vec::new();
webp.extend_from_slice(b"RIFF");
let vp8l_data: Vec<u8> = vec![
0x2F, 0x00, 0x00, 0x00, 0x00, 0x10, 0x07, 0x10, 0x11, 0x11, 0x88, 0x88, 0x08, 0x08,
];
let file_size = 4 + 8 + vp8l_data.len();
webp.extend_from_slice(&(file_size as u32).to_le_bytes());
webp.extend_from_slice(WEBP_SIGNATURE);
webp.extend_from_slice(VP8L_CHUNK_ID);
webp.extend_from_slice(&(vp8l_data.len() as u32).to_le_bytes());
webp.extend_from_slice(&vp8l_data);
webp
}
#[test]
fn test_can_handle_webp() {
let handler = WebpHandler;
let webp_data = create_minimal_webp();
let mut reader = Cursor::new(webp_data);
assert!(handler.can_handle(&mut reader).unwrap());
}
#[test]
fn test_can_handle_non_webp() {
let handler = WebpHandler;
let non_webp_data = vec![0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07];
let mut reader = Cursor::new(non_webp_data);
assert!(!handler.can_handle(&mut reader).unwrap());
}
#[test]
fn test_read_xmp_no_xmp() {
let webp_data = create_minimal_webp();
let reader = Cursor::new(webp_data);
let result = WebpHandler::read_xmp(reader).unwrap();
assert!(result.is_none());
}
#[test]
fn test_write_and_read_xmp() {
let webp_data = create_minimal_webp();
let reader = Cursor::new(webp_data);
let mut writer = Cursor::new(Vec::new());
let mut meta = XmpMeta::new();
meta.set_property(ns::DC, "title", XmpValue::String("Test WebP".to_string()))
.unwrap();
WebpHandler::write_xmp(reader, &mut writer, &meta).unwrap();
writer.set_position(0);
let result = WebpHandler::read_xmp(writer).unwrap();
assert!(result.is_some());
let read_meta = result.unwrap();
let title = read_meta.get_property(ns::DC, "title");
assert!(matches!(title, Some(XmpValue::String(s)) if s == "Test WebP"));
}
#[test]
fn test_format_info() {
let handler = WebpHandler;
assert_eq!(handler.format_name(), "WebP");
assert_eq!(handler.extensions(), &["webp"]);
}
}