1use otf_pixels_core::{
14 EncodeOptions, Encoder, ImageDescriptor, PixelFormat, PixelsError, Result, Sink,
15};
16
17#[derive(Debug)]
19pub struct WebPEncoder {
20 state: Option<State>,
22 options: EncodeOptions,
23 icc: Option<Vec<u8>>,
25 exif: Option<Vec<u8>>,
27}
28
29impl Default for WebPEncoder {
30 fn default() -> Self {
31 Self::new()
32 }
33}
34
35#[derive(Debug)]
37struct State {
38 descriptor: ImageDescriptor,
39 pixels: Vec<u8>,
42 rows_written: u32,
43}
44
45impl WebPEncoder {
46 #[must_use]
48 pub fn new() -> Self {
49 Self::from_options(&EncodeOptions::default())
50 }
51
52 #[must_use]
55 pub const fn from_options(options: &EncodeOptions) -> Self {
56 Self {
57 state: None,
58 options: *options,
59 icc: None,
60 exif: None,
61 }
62 }
63}
64
65fn check_format(format: PixelFormat) -> Result<()> {
67 match format {
68 PixelFormat::Gray8 | PixelFormat::GrayA8 | PixelFormat::Rgb8 | PixelFormat::Rgba8 => Ok(()),
69 other => Err(PixelsError::unsupported(format!(
70 "WebP encoding needs an 8-bit format; got {other}. Convert first."
71 ))),
72 }
73}
74
75impl Encoder for WebPEncoder {
76 fn set_icc_profile(&mut self, profile: Option<&[u8]>) -> Result<()> {
77 if self.state.is_some() {
78 return Err(PixelsError::invalid_argument(
79 "profile",
80 "the ICC profile must be set before write_header",
81 ));
82 }
83 self.icc = profile.map(<[u8]>::to_vec);
84 Ok(())
85 }
86
87 fn set_exif(&mut self, exif: Option<&[u8]>) -> Result<()> {
88 if self.state.is_some() {
89 return Err(PixelsError::invalid_argument(
90 "exif",
91 "the EXIF block must be set before write_header",
92 ));
93 }
94 self.exif = exif.map(<[u8]>::to_vec);
95 Ok(())
96 }
97
98 fn write_header(&mut self, desc: &ImageDescriptor, _sink: &mut dyn Sink) -> Result<()> {
99 if self.state.is_some() {
100 return Err(PixelsError::invalid_argument(
101 "descriptor",
102 "write_header called more than once",
103 ));
104 }
105 check_format(desc.pixel)?;
106 const MAX: u32 = 16_383;
109 if desc.width > MAX || desc.height > MAX {
110 return Err(PixelsError::unsupported(format!(
111 "WebP dimensions are at most {MAX}; {}x{} does not fit",
112 desc.width, desc.height
113 )));
114 }
115 let capacity = desc
116 .byte_len()
117 .ok_or_else(|| PixelsError::malformed("webp", "image size overflows"))?;
118
119 self.state = Some(State {
122 descriptor: *desc,
123 pixels: Vec::with_capacity(capacity),
124 rows_written: 0,
125 });
126 Ok(())
127 }
128
129 fn write_row(&mut self, row: &[u8], _sink: &mut dyn Sink) -> Result<()> {
130 let Some(state) = self.state.as_mut() else {
131 return Err(PixelsError::invalid_argument(
132 "row",
133 "write_row called before write_header",
134 ));
135 };
136 let expected = state.descriptor.row_bytes();
137 if row.len() != expected {
138 return Err(PixelsError::invalid_argument(
139 "row",
140 format!("row is {} bytes, expected {expected}", row.len()),
141 ));
142 }
143 if state.rows_written >= state.descriptor.height {
144 return Err(PixelsError::invalid_argument(
145 "row",
146 format!("more than {} rows written", state.descriptor.height),
147 ));
148 }
149 state.pixels.extend_from_slice(row);
150 state.rows_written += 1;
151 Ok(())
152 }
153
154 fn finish(&mut self, sink: &mut dyn Sink) -> Result<()> {
155 let Some(state) = self.state.as_mut() else {
156 return Err(PixelsError::invalid_argument(
157 "sink",
158 "finish called before write_header",
159 ));
160 };
161 if state.rows_written < state.descriptor.height {
162 return Err(PixelsError::malformed(
163 "webp",
164 format!(
165 "{} of {} rows were written",
166 state.rows_written, state.descriptor.height
167 ),
168 ));
169 }
170
171 let options = self.options;
172 let effort = options.effort.unwrap_or(DEFAULT_EFFORT).min(6);
173 let lossless = options.lossless || options.near_lossless;
174 let (body, alpha) = if lossless {
175 let near = options.near_lossless.then_some(options.quality);
176 encode_lossless(state, effort, near)
177 } else {
178 encode_lossy(state, options.quality, effort, options.alpha_quality)?
179 };
180 let (width, height) = (state.descriptor.width, state.descriptor.height);
181 let needs_vp8x = alpha && !lossless;
183 let file = container(
184 width,
185 height,
186 &body,
187 alpha,
188 needs_vp8x,
189 self.icc.as_deref(),
190 self.exif.as_deref(),
191 );
192 sink.write_all(&file)?;
193 sink.flush()
194 }
195}
196
197const DEFAULT_EFFORT: u8 = 4;
200
201fn chunk(out: &mut Vec<u8>, kind: &[u8; 4], payload: &[u8]) {
203 out.extend_from_slice(kind);
204 out.extend_from_slice(&(payload.len() as u32).to_le_bytes());
205 out.extend_from_slice(payload);
206 if payload.len() % 2 == 1 {
207 out.push(0);
208 }
209}
210
211fn riff(body: &[u8]) -> Vec<u8> {
213 let mut out = Vec::with_capacity(body.len() + 12);
214 out.extend_from_slice(b"RIFF");
215 out.extend_from_slice(&(body.len() as u32 + 4).to_le_bytes());
216 out.extend_from_slice(b"WEBP");
217 out.extend_from_slice(body);
218 out
219}
220
221fn encode_lossy(
224 state: &State,
225 quality: u8,
226 effort: u8,
227 alpha_quality: u8,
228) -> Result<(Vec<u8>, bool)> {
229 let (width, height) = (
230 state.descriptor.width as usize,
231 state.descriptor.height as usize,
232 );
233 let channels = state.descriptor.pixel.channels();
234 let (planes, alpha) = crate::yuv::from_rgb(&state.pixels, channels, width, height);
235 let mut params = crate::vp8::encode::Params::with_quality(quality.clamp(1, 100));
236 params.intra4 = effort >= 2;
237 let vp8 = crate::vp8::encode::encode(&planes, params)?;
238 let mut alpha = alpha.filter(|a| a.iter().any(|&v| v != 255));
241 let mut body = Vec::new();
242 if let Some(alpha) = &mut alpha {
243 if alpha_quality < 100 {
244 crate::preprocess::quantize_levels(
245 alpha,
246 crate::preprocess::alpha_levels(alpha_quality),
247 );
248 }
249 chunk(
250 &mut body,
251 b"ALPH",
252 &encode_alpha(alpha, width, height, effort),
253 );
254 }
255 chunk(&mut body, b"VP8 ", &vp8);
256 Ok((body, alpha.is_some()))
257}
258
259fn container(
263 width: u32,
264 height: u32,
265 image: &[u8],
266 alpha: bool,
267 needs_vp8x: bool,
268 icc: Option<&[u8]>,
269 exif: Option<&[u8]>,
270) -> Vec<u8> {
271 if !needs_vp8x && icc.is_none() && exif.is_none() {
272 return riff(image);
273 }
274 let flags = if icc.is_some() { 0x20 } else { 0 }
277 | if alpha { 0x10 } else { 0 }
278 | if exif.is_some() { 0x08 } else { 0 };
279 let mut vp8x = vec![flags, 0, 0, 0];
280 vp8x.extend_from_slice(&(width - 1).to_le_bytes()[..3]);
281 vp8x.extend_from_slice(&(height - 1).to_le_bytes()[..3]);
282 let mut body = Vec::new();
283 chunk(&mut body, b"VP8X", &vp8x);
284 if let Some(profile) = icc {
285 chunk(&mut body, b"ICCP", profile);
286 }
287 body.extend_from_slice(image);
288 if let Some(exif) = exif {
289 chunk(&mut body, b"EXIF", exif);
290 }
291 riff(&body)
292}
293
294fn to_argb(pixels: &[u8], channels: usize) -> Vec<u32> {
297 pixels
298 .chunks_exact(channels)
299 .map(|p| {
300 let (rgb, alpha) = match *p {
301 [g] => ([g, g, g], 255),
302 [g, a] => ([g, g, g], a),
303 [r, g, b] => ([r, g, b], 255),
304 [r, g, b, a, ..] => ([r, g, b], a),
305 _ => ([0; 3], 255),
306 };
307 u32::from_be_bytes([alpha, rgb[0], rgb[1], rgb[2]])
308 })
309 .collect()
310}
311
312fn encode_lossless(state: &State, effort: u8, near_lossless: Option<u8>) -> (Vec<u8>, bool) {
315 let (width, height) = (
316 state.descriptor.width as usize,
317 state.descriptor.height as usize,
318 );
319 let argb = to_argb(&state.pixels, state.descriptor.pixel.channels());
320 let mut tuning = crate::vp8l_encode::Tuning::for_effort(effort);
321 if let Some(quality) = near_lossless.filter(|_| width >= 64 || height >= 64) {
323 tuning = tuning.near_lossless(quality);
324 }
325 let has_alpha = argb.iter().any(|&p| p >> 24 != 0xff);
326 let mut body = Vec::new();
327 chunk(
328 &mut body,
329 b"VP8L",
330 &crate::vp8l_encode::encode(&argb, width, height, has_alpha, tuning),
331 );
332 (body, has_alpha)
333}
334
335fn encode_alpha(alpha: &[u8], width: usize, height: usize, effort: u8) -> Vec<u8> {
338 let argb: Vec<u32> = alpha
339 .iter()
340 .map(|&a| 0xff00_0000 | (u32::from(a) << 8))
341 .collect();
342 let mut w = crate::vp8l_encode::BitWriter::default();
343 crate::vp8l_encode::write_image_stream(
344 &mut w,
345 &argb,
346 width,
347 height,
348 crate::vp8l_encode::Tuning::for_effort(effort),
349 );
350 let mut out = vec![1]; out.extend_from_slice(&w.finish());
352 out
353}
354
355#[cfg(test)]
356#[allow(
357 clippy::unwrap_used,
358 clippy::indexing_slicing,
359 reason = "tests operate on known-good values and assert shapes directly"
360)]
361mod tests {
362 use super::*;
363 use crate::preprocess::{alpha_levels, quantize_levels};
364 use otf_pixels_core::ErrorCode;
365
366 #[test]
367 fn unsupported_pixel_formats_are_refused_at_the_header() {
368 for format in [
369 PixelFormat::Gray16,
370 PixelFormat::Rgb16,
371 PixelFormat::Rgba16,
372 PixelFormat::RgbF32,
373 ] {
374 let descriptor = ImageDescriptor::new(4, 4, format).unwrap();
375 let mut sink = Vec::new();
376 let error = WebPEncoder::new()
377 .write_header(&descriptor, &mut sink)
378 .unwrap_err();
379 assert_eq!(error.code(), ErrorCode::Unsupported, "{format}");
380 assert!(sink.is_empty(), "{format}: bytes were written anyway");
381 }
382 }
383
384 #[test]
385 fn the_encoder_contract_is_enforced() {
386 let descriptor = ImageDescriptor::new(4, 4, PixelFormat::Rgb8).unwrap();
387 let row = vec![0_u8; descriptor.row_bytes()];
388
389 let mut encoder = WebPEncoder::new();
390 let mut sink = Vec::new();
391 assert_eq!(
392 encoder.write_row(&row, &mut sink).unwrap_err().code(),
393 ErrorCode::InvalidArgument
394 );
395 assert_eq!(
396 encoder.finish(&mut sink).unwrap_err().code(),
397 ErrorCode::InvalidArgument
398 );
399
400 encoder.write_header(&descriptor, &mut sink).unwrap();
401 assert_eq!(
402 encoder
403 .write_header(&descriptor, &mut sink)
404 .unwrap_err()
405 .code(),
406 ErrorCode::InvalidArgument
407 );
408
409 encoder.write_row(&row, &mut sink).unwrap();
411 assert_eq!(
412 encoder.finish(&mut sink).unwrap_err().code(),
413 ErrorCode::Malformed
414 );
415 assert!(sink.is_empty());
416 }
417
418 #[test]
419 fn oversized_images_are_refused() {
420 let descriptor = ImageDescriptor::new(20_000, 4, PixelFormat::Rgb8).unwrap();
421 let error = WebPEncoder::new()
422 .write_header(&descriptor, &mut Vec::new())
423 .unwrap_err();
424 assert_eq!(error.code(), ErrorCode::Unsupported, "{error}");
425 }
426
427 fn encode(descriptor: &ImageDescriptor, pixels: &[u8], options: EncodeOptions) -> Vec<u8> {
429 let mut encoder = WebPEncoder::from_options(&options);
430 let mut out = Vec::new();
431 encoder.write_header(descriptor, &mut out).unwrap();
432 for row in pixels.chunks_exact(descriptor.row_bytes()) {
433 encoder.write_row(row, &mut out).unwrap();
434 }
435 encoder.finish(&mut out).unwrap();
436 out
437 }
438
439 fn decode(bytes: &[u8]) -> Vec<u8> {
441 use otf_pixels_core::{Decoder, Limits};
442 let mut decoder = crate::WebPDecoder::new(bytes, Limits::default()).unwrap();
443 let descriptor = decoder.descriptor();
444 let mut row = vec![0_u8; descriptor.row_bytes()];
445 let mut out = Vec::new();
446 for _ in 0..descriptor.height {
447 decoder.read_row(&mut row).unwrap();
448 out.extend_from_slice(&row);
449 }
450 out
451 }
452
453 fn noisy(width: u32, height: u32) -> (ImageDescriptor, Vec<u8>) {
455 let descriptor = ImageDescriptor::new(width, height, PixelFormat::Rgba8).unwrap();
456 let mut seed = 99_u32;
457 let mut pixels = Vec::new();
458 for _y in 0..height {
459 for x in 0..width {
460 for _ in 0..3 {
461 seed = seed.wrapping_mul(1_103_515_245).wrapping_add(12_345);
462 pixels.push((seed >> 16) as u8);
463 }
464 pixels.push((x * 255 / (width - 1)) as u8);
465 }
466 }
467 (descriptor, pixels)
468 }
469
470 fn distinct(values: impl Iterator<Item = u8>) -> usize {
471 let mut seen = [false; 256];
472 values.for_each(|v| seen[usize::from(v)] = true);
473 seen.iter().filter(|&&s| s).count()
474 }
475
476 #[test]
477 fn alpha_levels_follow_libwebp() {
478 assert_eq!(alpha_levels(0), 2);
479 assert_eq!(alpha_levels(50), 12);
480 assert_eq!(alpha_levels(70), 16);
481 assert_eq!(alpha_levels(71), 24);
482 assert_eq!(alpha_levels(99), 248);
483 }
484
485 #[test]
486 fn alpha_is_reduced_to_its_levels_keeping_the_ends() {
487 let mut ramp: Vec<u8> = (0..=255).collect();
488 quantize_levels(&mut ramp, 4);
489 assert!(distinct(ramp.iter().copied()) <= 4);
490 assert_eq!((ramp[0], ramp[255]), (0, 255), "the ends are fixed");
491 let mut few = vec![3, 3, 200, 9];
493 quantize_levels(&mut few, 4);
494 assert_eq!(few, [3, 3, 200, 9]);
495 }
496
497 #[test]
498 fn a_lower_alpha_quality_keeps_fewer_alpha_levels() {
499 let (descriptor, pixels) = noisy(64, 32);
500 let alpha = |quality| {
501 let options = EncodeOptions::default()
502 .with_alpha_quality(quality)
503 .unwrap();
504 let decoded = decode(&encode(&descriptor, &pixels, options));
505 distinct(decoded.chunks_exact(4).map(|p| p[3]))
506 };
507 assert_eq!(alpha(100), 64, "lossless alpha keeps every level");
508 assert!(alpha(0) <= 2);
509 assert!(alpha(50) <= 12);
510 }
511
512 #[test]
513 fn near_lossless_stays_close_and_is_exact_at_full_quality() {
514 let (descriptor, pixels) = noisy(80, 70);
515 let options = EncodeOptions::default().with_near_lossless(true);
516 let bytes = encode(&descriptor, &pixels, options);
517 assert!(
518 bytes.windows(4).any(|w| w == b"VP8L"),
519 "near-lossless is lossless"
520 );
521 let decoded = decode(&bytes);
522 assert_ne!(decoded, pixels, "quality 80 changes something");
523 for (got, want) in decoded.chunks_exact(4).zip(pixels.chunks_exact(4)) {
526 assert!(
527 got.iter().zip(want).all(|(&g, &w)| g.abs_diff(w) <= 2),
528 "{got:?} {want:?}"
529 );
530 }
531 let exact = EncodeOptions::with_quality(100)
532 .unwrap()
533 .with_near_lossless(true);
534 assert_eq!(decode(&encode(&descriptor, &pixels, exact)), pixels);
535 let (small, small_pixels) = noisy(32, 32);
537 assert_eq!(
538 decode(&encode(&small, &small_pixels, options)),
539 small_pixels
540 );
541 }
542
543 #[test]
544 fn every_effort_decodes_and_the_default_is_four() {
545 let (descriptor, pixels) = noisy(48, 40);
546 for lossless in [false, true] {
547 let base = EncodeOptions::default().with_lossless(lossless);
548 let default = encode(&descriptor, &pixels, base);
549 assert_eq!(
550 default,
551 encode(&descriptor, &pixels, base.with_effort(4).unwrap())
552 );
553 for effort in 0..=10 {
554 let bytes = encode(&descriptor, &pixels, base.with_effort(effort).unwrap());
555 let decoded = decode(&bytes);
556 if lossless {
557 assert_eq!(decoded, pixels, "effort {effort}");
558 } else {
559 assert_eq!(decoded.len(), pixels.len());
560 }
561 }
562 }
563 let lossy = EncodeOptions::default();
566 assert_ne!(
567 encode(&descriptor, &pixels, lossy.with_effort(0).unwrap()),
568 encode(&descriptor, &pixels, lossy)
569 );
570 }
571}