Skip to main content

gufo_webp/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::io::{Cursor, Read, Seek};
4use std::ops::Range;
5use std::slice::SliceIndex;
6
7use gufo_common::error::ErrorWithData;
8use gufo_common::image::ImageMetadata;
9
10pub const RIFF_MAGIC_BYTES: &[u8] = b"RIFF";
11pub const WEBP_MAGIC_BYTES: &[u8] = b"WEBP";
12
13#[derive(Debug, Clone)]
14pub struct WebP {
15    data: Vec<u8>,
16    chunks: Vec<RawChunk>,
17}
18
19impl ImageMetadata for WebP {
20    fn exif(&self) -> Vec<Vec<u8>> {
21        let Some(exif) = self.exif_data() else {
22            return vec![];
23        };
24
25        vec![exif.to_vec()]
26    }
27}
28
29/// Representation of a WEBP image
30impl WebP {
31    /// Returns WEBP image representation
32    ///
33    /// * `data`: WEBP image data starting with RIFF magic byte
34    pub fn new(data: Vec<u8>) -> Result<Self, ErrorWithData<Error>> {
35        match Self::find_chunks(&data) {
36            Ok(chunks) => Ok(Self { chunks, data }),
37            Err(err) => Err(ErrorWithData::new(err, data)),
38        }
39    }
40
41    pub fn is_filetype(data: &[u8]) -> bool {
42        data.starts_with(RIFF_MAGIC_BYTES) && data.get(8..12) == Some(WEBP_MAGIC_BYTES)
43    }
44
45    pub fn into_inner(self) -> Vec<u8> {
46        self.data
47    }
48
49    pub fn get(&self, index: impl SliceIndex<[u8], Output = [u8]>) -> Option<&[u8]> {
50        self.data.get(index)
51    }
52
53    /// Returns all chunks
54    pub fn chunks(&self) -> Vec<Chunk<'_>> {
55        self.chunks.iter().map(|x| x.chunk(self)).collect()
56    }
57
58    fn exif_data(&self) -> Option<&[u8]> {
59        self.chunks
60            .iter()
61            .find(|x| x.four_cc == FourCC::EXIF)
62            .and_then(|x| self.get(x.payload.clone()))
63    }
64
65    /// List all chunks in the data
66    fn find_chunks(data: &[u8]) -> Result<Vec<RawChunk>, Error> {
67        let mut cur = Cursor::new(data);
68
69        // Riff magic bytes
70        let riff_magic_bytes = &mut [0; WEBP_MAGIC_BYTES.len()];
71        cur.read_exact(riff_magic_bytes)
72            .map_err(|_| Error::UnexpectedEof)?;
73        if riff_magic_bytes != RIFF_MAGIC_BYTES {
74            return Err(Error::RiffMagicBytesMissing(*riff_magic_bytes));
75        }
76
77        // File length
78        let file_length_data = &mut [0; 4];
79        cur.read_exact(file_length_data)
80            .map_err(|_| Error::UnexpectedEof)?;
81        let file_length = u32::from_le_bytes(*file_length_data);
82
83        // Exif magic bytes
84        let webp_magic_bytes = &mut [0; WEBP_MAGIC_BYTES.len()];
85        cur.read_exact(webp_magic_bytes)
86            .map_err(|_| Error::UnexpectedEof)?;
87        if webp_magic_bytes != WEBP_MAGIC_BYTES {
88            return Err(Error::WebpMagicBytesMissing(*webp_magic_bytes));
89        }
90
91        let mut chunks = Vec::new();
92        loop {
93            // Next 4 bytes are chunk FourCC (chunk type)
94            let four_cc_data = &mut [0; 4];
95            cur.read_exact(four_cc_data)
96                .map_err(|_| Error::UnexpectedEof)?;
97            let four_cc = FourCC::from(u32::from_le_bytes(*four_cc_data));
98
99            // First 4 bytes are chunk size
100            let size_data = &mut [0; 4];
101            cur.read_exact(size_data)
102                .map_err(|_| Error::UnexpectedEof)?;
103            let size = u32::from_le_bytes(*size_data);
104
105            // Next is the payload
106            let payload_start: usize = cur
107                .position()
108                .try_into()
109                .map_err(|_| Error::PositionTooLarge)?;
110            let payload_end = payload_start
111                .checked_add(size as usize)
112                .ok_or(Error::PositionTooLarge)?;
113            let payload = payload_start..payload_end;
114
115            let chunk = RawChunk { four_cc, payload };
116
117            // Jump to end of payload
118            cur.set_position(payload_end as u64);
119
120            // If odd, jump over 1 byte padding
121            if size % 2 != 0 {
122                cur.seek(std::io::SeekFrom::Current(1))
123                    .map_err(|_| Error::UnexpectedEof)?;
124            }
125
126            chunks.push(chunk);
127
128            if cur.position() >= file_length.into() {
129                break;
130            }
131        }
132
133        Ok(chunks)
134    }
135}
136
137#[derive(Debug, Clone)]
138pub struct RawChunk {
139    four_cc: FourCC,
140    payload: Range<usize>,
141}
142
143impl RawChunk {
144    fn chunk<'a>(&self, webp: &'a WebP) -> Chunk<'a> {
145        Chunk {
146            four_cc: self.four_cc,
147            payload: self.payload.clone(),
148            webp,
149        }
150    }
151}
152
153#[derive(Debug, Clone)]
154pub struct Chunk<'a> {
155    four_cc: FourCC,
156    payload: Range<usize>,
157    webp: &'a WebP,
158}
159
160impl<'a> Chunk<'a> {
161    pub fn four_cc(&self) -> FourCC {
162        self.four_cc
163    }
164
165    pub fn payload(&self) -> &[u8] {
166        self.webp
167            .data
168            .get(self.payload.clone())
169            .expect("Unreachable: Chunk must be part of the data")
170    }
171}
172
173#[derive(Debug, Clone, thiserror::Error)]
174pub enum Error {
175    #[error("RIFF magic bytes missing: {0:?}")]
176    RiffMagicBytesMissing([u8; 4]),
177    #[error("WEBP magic bytes missing: {0:?}")]
178    WebpMagicBytesMissing([u8; 4]),
179    #[error("Unexpected end of file")]
180    UnexpectedEof,
181    #[error("Position too large")]
182    PositionTooLarge,
183}
184
185gufo_common::utils::convertible_enum!(
186    #[repr(u32)]
187    #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
188    #[non_exhaustive]
189    #[allow(non_camel_case_types)]
190    /// Type of a chunk
191    ///
192    /// The value is stored as little endian [`u32`] of the original byte
193    /// string.
194    pub enum FourCC {
195        /// Information about features used in the file
196        VP8X = b(b"VP8X"),
197        /// Embedded ICC color profile
198        ICCP = b(b"ICCP"),
199        /// Global parameters of the animation.
200        ANIM = b(b"ANIM"),
201
202        /// Information about a single frame
203        ANMF = b(b"ANMF"),
204        /// Alpha data for this frame (only with [`VP8`](Self::VP8))
205        ALPH = b(b"ALPH"),
206        /// Lossy data for this frame
207        VP8 = b(b"VP8 "),
208        /// Lossless data for this frame
209        VP8L = b(b"VP8L"),
210
211        EXIF = b(b"EXIF"),
212        XMP = b(b"XMP "),
213    }
214);
215
216impl FourCC {
217    /// Returns the byte string of the chunk
218    pub fn bytes(self) -> [u8; 4] {
219        u32::to_le_bytes(self.into())
220    }
221}
222
223/// Convert bytes to u32
224const fn b(d: &[u8; 4]) -> u32 {
225    u32::from_le_bytes(*d)
226}