From 422fced8e89ec788b592363387412d1a57fc8ae8 Mon Sep 17 00:00:00 2001 From: Sophie Herold Date: Mon, 19 Aug 2024 23:18:51 +0200 Subject: [PATCH] Add gufo-webp --- Cargo.toml | 1 + gufo-png/src/lib.rs | 6 +- gufo-webp/Cargo.toml | 13 +++ gufo-webp/examples/webp-dump.rs | 17 +++ gufo-webp/src/lib.rs | 187 ++++++++++++++++++++++++++++++++ 5 files changed, 220 insertions(+), 4 deletions(-) create mode 100644 gufo-webp/Cargo.toml create mode 100644 gufo-webp/examples/webp-dump.rs create mode 100644 gufo-webp/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index c9b79b9..7592150 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ members = [ "gufo-exif", "gufo-jpeg", "gufo-png", + "gufo-webp", "gufo-xmp", "tests", ] diff --git a/gufo-png/src/lib.rs b/gufo-png/src/lib.rs index 8c6f21a..86fcbe1 100644 --- a/gufo-png/src/lib.rs +++ b/gufo-png/src/lib.rs @@ -1,7 +1,5 @@ -use std::{ - io::{Cursor, Read, Seek}, - ops::Range, -}; +use std::io::{Cursor, Read, Seek}; +use std::ops::Range; use miniz_oxide::inflate::DecompressError; diff --git a/gufo-webp/Cargo.toml b/gufo-webp/Cargo.toml new file mode 100644 index 0000000..bf467ab --- /dev/null +++ b/gufo-webp/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "gufo-webp" +version.workspace = true +license.workspace = true +edition.workspace = true +rust-version.workspace = true +repository.workspace = true + +[dependencies] +gufo-common.workspace = true + +[lints] +workspace = true diff --git a/gufo-webp/examples/webp-dump.rs b/gufo-webp/examples/webp-dump.rs new file mode 100644 index 0000000..d0ede7d --- /dev/null +++ b/gufo-webp/examples/webp-dump.rs @@ -0,0 +1,17 @@ +fn main() { + let path = std::env::args() + .nth(1) + .expect("First agument must be a path."); + let data = std::fs::read(path).unwrap(); + let webp = gufo_webp::WebP::new(data).unwrap(); + + for chunk in webp.chunks() { + match chunk.four_cc() { + gufo_webp::FourCC::Unknown(unknown) => println!( + "Unknown({})", + String::from_utf8_lossy(&u32::to_le_bytes(unknown)) + ), + chunk_type => println!("{chunk_type:?}"), + } + } +} diff --git a/gufo-webp/src/lib.rs b/gufo-webp/src/lib.rs new file mode 100644 index 0000000..3b2a11d --- /dev/null +++ b/gufo-webp/src/lib.rs @@ -0,0 +1,187 @@ +use std::io::{Cursor, Read, Seek}; +use std::ops::Range; + +pub const RIFF_MAGIC_BYTES: &[u8] = b"RIFF"; +pub const WEBP_MAGIC_BYTES: &[u8] = b"WEBP"; + +#[derive(Debug, Clone)] +pub struct WebP { + data: Vec, + chunks: Vec, +} + +/// Representation of a WEBP image +impl WebP { + /// Returns WEBP image representation + /// + /// * `data`: WEBP image data starting with RIFF magic byte + pub fn new(data: Vec) -> Result { + let chunks = Self::find_chunks(&data)?; + + Ok(Self { chunks, data }) + } + + /// Returns all chunks + pub fn chunks(&self) -> Vec { + self.chunks.iter().map(|x| x.chunk(self)).collect() + } + + /// List all chunks in the data + fn find_chunks(data: &[u8]) -> Result, Error> { + let mut cur = Cursor::new(data); + + // Riff magic bytes + let riff_magic_bytes = &mut [0; WEBP_MAGIC_BYTES.len()]; + cur.read_exact(riff_magic_bytes) + .map_err(|_| Error::UnexpectedEof)?; + if riff_magic_bytes != RIFF_MAGIC_BYTES { + return Err(Error::RiffMagicBytesMissing(*riff_magic_bytes)); + } + + // File length + let file_length_data = &mut [0; 4]; + cur.read_exact(file_length_data) + .map_err(|_| Error::UnexpectedEof)?; + let file_length = u32::from_le_bytes(*file_length_data); + + // Exif magic bytes + let webp_magic_bytes = &mut [0; WEBP_MAGIC_BYTES.len()]; + cur.read_exact(webp_magic_bytes) + .map_err(|_| Error::UnexpectedEof)?; + if webp_magic_bytes != WEBP_MAGIC_BYTES { + return Err(Error::WebpMagicBytesMissing(*webp_magic_bytes)); + } + + let mut chunks = Vec::new(); + loop { + // Next 4 bytes are chunk FourCC (chunk type) + let four_cc_data = &mut [0; 4]; + cur.read_exact(four_cc_data) + .map_err(|_| Error::UnexpectedEof)?; + let four_cc = FourCC::from(u32::from_le_bytes(*four_cc_data)); + + // First 4 bytes are chunk size + let size_data = &mut [0; 4]; + cur.read_exact(size_data) + .map_err(|_| Error::UnexpectedEof)?; + let size = u32::from_le_bytes(*size_data); + + // Next is the payload + let payload_start: usize = cur + .position() + .try_into() + .map_err(|_| Error::PositionTooLarge)?; + let payload_end = payload_start + .checked_add(size as usize) + .ok_or(Error::PositionTooLarge)?; + let payload = payload_start..payload_end; + + let chunk = RawChunk { four_cc, payload }; + + // Jump to end of payload + cur.set_position(payload_end as u64); + + // If odd, jump over 1 byte padding + if size % 2 != 0 { + cur.seek(std::io::SeekFrom::Current(1)) + .map_err(|_| Error::UnexpectedEof)?; + } + + chunks.push(chunk); + + if cur.position() >= file_length.into() { + break; + } + } + + Ok(chunks) + } +} + +#[derive(Debug, Clone)] +pub struct RawChunk { + four_cc: FourCC, + payload: Range, +} + +impl RawChunk { + fn chunk<'a>(&self, webp: &'a WebP) -> Chunk<'a> { + Chunk { + four_cc: self.four_cc, + payload: self.payload.clone(), + webp, + } + } + + pub fn total_len(&self) -> usize { + self.payload.len().checked_add(8).unwrap() + } +} + +#[derive(Debug, Clone)] +pub struct Chunk<'a> { + four_cc: FourCC, + payload: Range, + webp: &'a WebP, +} + +impl<'a> Chunk<'a> { + pub fn four_cc(&self) -> FourCC { + self.four_cc + } + + pub fn payload(&self) -> &[u8] { + self.webp.data.get(self.payload.clone()).unwrap() + } +} + +#[derive(Debug, Clone)] +pub enum Error { + RiffMagicBytesMissing([u8; 4]), + WebpMagicBytesMissing([u8; 4]), + UnexpectedEof, + PositionTooLarge, +} + +gufo_common::utils::convertible_enum!( + #[repr(u32)] + #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] + #[non_exhaustive] + #[allow(non_camel_case_types)] + /// Type of a chunk + /// + /// The value is stored as little endian [`u32`] of the original byte + /// string. + pub enum FourCC { + /// Information about features used in the file + VP8X = b(b"VP8X"), + /// Embedded ICC color profile + ICCP = b(b"ICCP"), + /// Global parameters of the animation. + ANIM = b(b"ANIM"), + + /// Information about a single frame + ANMF = b(b"ANMF"), + /// Alpha data for this frame (only with [`VP8`](Self::VP8)) + ALPH = b(b"ALPH"), + /// Lossy data for this frame + VP8 = b(b"VP8 "), + /// Lossless data for this frame + VP8L = b(b"VP8L"), + + EXIF = b(b"EXIF"), + XMP = b(b"XMP "), + } +); + +impl FourCC { + /// Returns the byte string of the chunk + pub fn bytes(self) -> [u8; 4] { + u32::to_le_bytes(self.into()) + } +} + +/// Convert bytes to u32 +const fn b(d: &[u8; 4]) -> u32 { + u32::from_le_bytes(*d) +}