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