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