1use otf_pixels_core::{
10 EncodeOptions, Encoder, ImageDescriptor, PixelFormat, PixelsError, Result, Sink,
11};
12
13#[derive(Debug)]
15pub struct WebPEncoder {
16 state: Option<State>,
18 options: EncodeOptions,
19 icc: Option<Vec<u8>>,
21}
22
23impl Default for WebPEncoder {
24 fn default() -> Self {
25 Self::new()
26 }
27}
28
29#[derive(Debug)]
31struct State {
32 descriptor: ImageDescriptor,
33 pixels: Vec<u8>,
36 rows_written: u32,
37}
38
39impl WebPEncoder {
40 #[must_use]
42 pub fn new() -> Self {
43 Self::from_options(&EncodeOptions::default())
44 }
45
46 #[must_use]
49 pub const fn from_options(options: &EncodeOptions) -> Self {
50 Self {
51 state: None,
52 options: *options,
53 icc: None,
54 }
55 }
56}
57
58fn check_format(format: PixelFormat) -> Result<()> {
60 match format {
61 PixelFormat::Gray8 | PixelFormat::GrayA8 | PixelFormat::Rgb8 | PixelFormat::Rgba8 => Ok(()),
62 other => Err(PixelsError::unsupported(format!(
63 "WebP encoding needs an 8-bit format; got {other}. Convert first."
64 ))),
65 }
66}
67
68impl Encoder for WebPEncoder {
69 fn set_icc_profile(&mut self, profile: Option<&[u8]>) -> Result<()> {
70 if self.state.is_some() {
71 return Err(PixelsError::invalid_argument(
72 "profile",
73 "the ICC profile must be set before write_header",
74 ));
75 }
76 self.icc = profile.map(<[u8]>::to_vec);
77 Ok(())
78 }
79
80 fn write_header(&mut self, desc: &ImageDescriptor, _sink: &mut dyn Sink) -> Result<()> {
81 if self.state.is_some() {
82 return Err(PixelsError::invalid_argument(
83 "descriptor",
84 "write_header called more than once",
85 ));
86 }
87 check_format(desc.pixel)?;
88 const MAX: u32 = 16_383;
91 if desc.width > MAX || desc.height > MAX {
92 return Err(PixelsError::unsupported(format!(
93 "WebP dimensions are at most {MAX}; {}x{} does not fit",
94 desc.width, desc.height
95 )));
96 }
97 let capacity = desc
98 .byte_len()
99 .ok_or_else(|| PixelsError::malformed("webp", "image size overflows"))?;
100
101 self.state = Some(State {
104 descriptor: *desc,
105 pixels: Vec::with_capacity(capacity),
106 rows_written: 0,
107 });
108 Ok(())
109 }
110
111 fn write_row(&mut self, row: &[u8], _sink: &mut dyn Sink) -> Result<()> {
112 let Some(state) = self.state.as_mut() else {
113 return Err(PixelsError::invalid_argument(
114 "row",
115 "write_row called before write_header",
116 ));
117 };
118 let expected = state.descriptor.row_bytes();
119 if row.len() != expected {
120 return Err(PixelsError::invalid_argument(
121 "row",
122 format!("row is {} bytes, expected {expected}", row.len()),
123 ));
124 }
125 if state.rows_written >= state.descriptor.height {
126 return Err(PixelsError::invalid_argument(
127 "row",
128 format!("more than {} rows written", state.descriptor.height),
129 ));
130 }
131 state.pixels.extend_from_slice(row);
132 state.rows_written += 1;
133 Ok(())
134 }
135
136 fn finish(&mut self, sink: &mut dyn Sink) -> Result<()> {
137 let Some(state) = self.state.as_mut() else {
138 return Err(PixelsError::invalid_argument(
139 "sink",
140 "finish called before write_header",
141 ));
142 };
143 if state.rows_written < state.descriptor.height {
144 return Err(PixelsError::malformed(
145 "webp",
146 format!(
147 "{} of {} rows were written",
148 state.rows_written, state.descriptor.height
149 ),
150 ));
151 }
152
153 let (body, alpha) = if self.options.lossless {
154 encode_lossless(state)
155 } else {
156 encode_lossy(state, self.options.quality)?
157 };
158 let (width, height) = (state.descriptor.width, state.descriptor.height);
159 let needs_vp8x = alpha && !self.options.lossless;
161 let file = container(width, height, &body, alpha, needs_vp8x, self.icc.as_deref());
162 sink.write_all(&file)?;
163 sink.flush()
164 }
165}
166
167fn chunk(out: &mut Vec<u8>, kind: &[u8; 4], payload: &[u8]) {
169 out.extend_from_slice(kind);
170 out.extend_from_slice(&(payload.len() as u32).to_le_bytes());
171 out.extend_from_slice(payload);
172 if payload.len() % 2 == 1 {
173 out.push(0);
174 }
175}
176
177fn riff(body: &[u8]) -> Vec<u8> {
179 let mut out = Vec::with_capacity(body.len() + 12);
180 out.extend_from_slice(b"RIFF");
181 out.extend_from_slice(&(body.len() as u32 + 4).to_le_bytes());
182 out.extend_from_slice(b"WEBP");
183 out.extend_from_slice(body);
184 out
185}
186
187fn encode_lossy(state: &State, quality: u8) -> Result<(Vec<u8>, bool)> {
190 let (width, height) = (
191 state.descriptor.width as usize,
192 state.descriptor.height as usize,
193 );
194 let channels = state.descriptor.pixel.channels();
195 let (planes, alpha) = crate::yuv::from_rgb(&state.pixels, channels, width, height);
196 let vp8 = crate::vp8::encode::encode(
197 &planes,
198 crate::vp8::encode::Params::with_quality(quality.clamp(1, 100)),
199 )?;
200 let alpha = alpha.filter(|a| a.iter().any(|&v| v != 255));
203 let mut body = Vec::new();
204 if let Some(alpha) = &alpha {
205 chunk(&mut body, b"ALPH", &encode_alpha(alpha, width, height));
206 }
207 chunk(&mut body, b"VP8 ", &vp8);
208 Ok((body, alpha.is_some()))
209}
210
211fn container(
215 width: u32,
216 height: u32,
217 image: &[u8],
218 alpha: bool,
219 needs_vp8x: bool,
220 icc: Option<&[u8]>,
221) -> Vec<u8> {
222 if !needs_vp8x && icc.is_none() {
223 return riff(image);
224 }
225 let flags = if icc.is_some() { 0x20 } else { 0 } | if alpha { 0x10 } else { 0 };
227 let mut vp8x = vec![flags, 0, 0, 0];
228 vp8x.extend_from_slice(&(width - 1).to_le_bytes()[..3]);
229 vp8x.extend_from_slice(&(height - 1).to_le_bytes()[..3]);
230 let mut body = Vec::new();
231 chunk(&mut body, b"VP8X", &vp8x);
232 if let Some(profile) = icc {
233 chunk(&mut body, b"ICCP", profile);
234 }
235 body.extend_from_slice(image);
236 riff(&body)
237}
238
239fn to_argb(pixels: &[u8], channels: usize) -> Vec<u32> {
242 pixels
243 .chunks_exact(channels)
244 .map(|p| {
245 let (rgb, alpha) = match *p {
246 [g] => ([g, g, g], 255),
247 [g, a] => ([g, g, g], a),
248 [r, g, b] => ([r, g, b], 255),
249 [r, g, b, a, ..] => ([r, g, b], a),
250 _ => ([0; 3], 255),
251 };
252 u32::from_be_bytes([alpha, rgb[0], rgb[1], rgb[2]])
253 })
254 .collect()
255}
256
257fn encode_lossless(state: &State) -> (Vec<u8>, bool) {
260 let (width, height) = (
261 state.descriptor.width as usize,
262 state.descriptor.height as usize,
263 );
264 let argb = to_argb(&state.pixels, state.descriptor.pixel.channels());
265 let has_alpha = argb.iter().any(|&p| p >> 24 != 0xff);
266 let mut body = Vec::new();
267 chunk(
268 &mut body,
269 b"VP8L",
270 &crate::vp8l_encode::encode(&argb, width, height, has_alpha),
271 );
272 (body, has_alpha)
273}
274
275fn encode_alpha(alpha: &[u8], width: usize, height: usize) -> Vec<u8> {
278 let argb: Vec<u32> = alpha
279 .iter()
280 .map(|&a| 0xff00_0000 | (u32::from(a) << 8))
281 .collect();
282 let mut w = crate::vp8l_encode::BitWriter::default();
283 crate::vp8l_encode::write_image_stream(&mut w, &argb, width, height);
284 let mut out = vec![1]; out.extend_from_slice(&w.finish());
286 out
287}
288
289#[cfg(test)]
290#[allow(
291 clippy::unwrap_used,
292 clippy::indexing_slicing,
293 reason = "tests operate on known-good values and assert shapes directly"
294)]
295mod tests {
296 use super::*;
297 use otf_pixels_core::ErrorCode;
298
299 #[test]
300 fn unsupported_pixel_formats_are_refused_at_the_header() {
301 for format in [
302 PixelFormat::Gray16,
303 PixelFormat::Rgb16,
304 PixelFormat::Rgba16,
305 PixelFormat::RgbF32,
306 ] {
307 let descriptor = ImageDescriptor::new(4, 4, format).unwrap();
308 let mut sink = Vec::new();
309 let error = WebPEncoder::new()
310 .write_header(&descriptor, &mut sink)
311 .unwrap_err();
312 assert_eq!(error.code(), ErrorCode::Unsupported, "{format}");
313 assert!(sink.is_empty(), "{format}: bytes were written anyway");
314 }
315 }
316
317 #[test]
318 fn the_encoder_contract_is_enforced() {
319 let descriptor = ImageDescriptor::new(4, 4, PixelFormat::Rgb8).unwrap();
320 let row = vec![0_u8; descriptor.row_bytes()];
321
322 let mut encoder = WebPEncoder::new();
323 let mut sink = Vec::new();
324 assert_eq!(
325 encoder.write_row(&row, &mut sink).unwrap_err().code(),
326 ErrorCode::InvalidArgument
327 );
328 assert_eq!(
329 encoder.finish(&mut sink).unwrap_err().code(),
330 ErrorCode::InvalidArgument
331 );
332
333 encoder.write_header(&descriptor, &mut sink).unwrap();
334 assert_eq!(
335 encoder
336 .write_header(&descriptor, &mut sink)
337 .unwrap_err()
338 .code(),
339 ErrorCode::InvalidArgument
340 );
341
342 encoder.write_row(&row, &mut sink).unwrap();
344 assert_eq!(
345 encoder.finish(&mut sink).unwrap_err().code(),
346 ErrorCode::Malformed
347 );
348 assert!(sink.is_empty());
349 }
350
351 #[test]
352 fn oversized_images_are_refused() {
353 let descriptor = ImageDescriptor::new(20_000, 4, PixelFormat::Rgb8).unwrap();
354 let error = WebPEncoder::new()
355 .write_header(&descriptor, &mut Vec::new())
356 .unwrap_err();
357 assert_eq!(error.code(), ErrorCode::Unsupported, "{error}");
358 }
359}