1use std::io::{self, Write};
3
4use quick_error::quick_error;
5
6use crate::{
7 lossless::{encode_frame_lossless, EncoderParams},
8 lossy::encoder::encode_frame_lossy,
9};
10
11#[derive(Copy, Clone, Debug, PartialEq, Eq)]
17pub enum ColorType {
18 L8,
20 La8,
22 Rgb8,
24 Rgba8,
26}
27
28impl ColorType {
29 fn has_alpha(self) -> bool {
30 self == ColorType::La8 || self == ColorType::Rgba8
31 }
32}
33
34quick_error! {
35 #[derive(Debug)]
37 #[non_exhaustive]
38 pub enum EncodingError {
39 IoError(err: io::Error) {
41 from()
42 display("IO error: {}", err)
43 source(err)
44 }
45
46 InvalidDimensions {
48 display("Invalid dimensions")
49 }
50 }
51}
52
53fn encode_alpha_lossless<W: Write>(
60 mut writer: W,
61 data: &[u8],
62 width: u32,
63 height: u32,
64 color: ColorType,
65) -> Result<(), EncodingError> {
66 let bytes_per_pixel = match color {
67 ColorType::La8 => 2,
68 ColorType::Rgba8 => 4,
69 _ => unreachable!(),
70 };
71 if width == 0 || width > 16384 || height == 0 || height > 16384 {
72 return Err(EncodingError::InvalidDimensions);
73 }
74
75 let preprocessing = 0u8;
76 let filtering_method = 0u8;
77 let compression_method = 1u8;
80
81 let initial_byte = preprocessing << 4 | filtering_method << 2 | compression_method;
82
83 writer.write_all(&[initial_byte])?;
84
85 let alpha_data: Vec<u8> = data
87 .iter()
88 .skip(bytes_per_pixel - 1)
89 .step_by(bytes_per_pixel)
90 .copied()
91 .collect();
92
93 debug_assert_eq!(alpha_data.len(), (width * height) as usize);
94
95 encode_frame_lossless(
96 writer,
97 &alpha_data,
98 width,
99 height,
100 ColorType::L8,
101 EncoderParams::default(),
102 true,
103 )?;
104
105 Ok(())
106}
107
108const fn chunk_size(inner_bytes: usize) -> u32 {
109 if inner_bytes % 2 == 1 {
110 (inner_bytes + 1) as u32 + 8
111 } else {
112 inner_bytes as u32 + 8
113 }
114}
115
116fn write_chunk<W: Write>(mut w: W, name: &[u8], data: &[u8]) -> io::Result<()> {
117 debug_assert!(name.len() == 4);
118
119 w.write_all(name)?;
120 w.write_all(&(data.len() as u32).to_le_bytes())?;
121 w.write_all(data)?;
122 if data.len() % 2 == 1 {
123 w.write_all(&[0])?;
124 }
125 Ok(())
126}
127
128pub struct WebPEncoder<W> {
130 writer: W,
131 icc_profile: Vec<u8>,
132 exif_metadata: Vec<u8>,
133 xmp_metadata: Vec<u8>,
134 params: EncoderParams,
135}
136
137impl<W: Write> WebPEncoder<W> {
138 pub fn new(w: W) -> Self {
145 Self {
146 writer: w,
147 icc_profile: Vec::new(),
148 exif_metadata: Vec::new(),
149 xmp_metadata: Vec::new(),
150 params: EncoderParams::default(),
151 }
152 }
153
154 pub fn set_icc_profile(&mut self, icc_profile: Vec<u8>) {
156 self.icc_profile = icc_profile;
157 }
158
159 pub fn set_exif_metadata(&mut self, exif_metadata: Vec<u8>) {
161 self.exif_metadata = exif_metadata;
162 }
163
164 pub fn set_xmp_metadata(&mut self, xmp_metadata: Vec<u8>) {
166 self.xmp_metadata = xmp_metadata;
167 }
168
169 pub fn set_params(&mut self, params: EncoderParams) {
171 self.params = params;
172 }
173
174 pub fn encode(
180 mut self,
181 data: &[u8],
182 width: u32,
183 height: u32,
184 color: ColorType,
185 ) -> Result<(), EncodingError> {
186 let mut frame = Vec::new();
187
188 let lossy_with_alpha = self.params.use_lossy && color.has_alpha();
189
190 let frame_chunk = if self.params.use_lossy {
191 encode_frame_lossy(
192 &mut frame,
193 data,
194 width,
195 height,
196 color,
197 self.params.lossy_quality,
198 )?;
199 b"VP8 "
200 } else {
201 encode_frame_lossless(&mut frame, data, width, height, color, self.params, false)?;
202 b"VP8L"
203 };
204
205 let use_simple_container = self.icc_profile.is_empty()
208 && self.exif_metadata.is_empty()
209 && self.xmp_metadata.is_empty()
210 && !lossy_with_alpha;
211
212 if use_simple_container {
213 self.writer.write_all(b"RIFF")?;
214 self.writer
215 .write_all(&(chunk_size(frame.len()) + 4).to_le_bytes())?;
216 self.writer.write_all(b"WEBP")?;
217 write_chunk(&mut self.writer, frame_chunk, &frame)?;
218 } else {
219 let mut total_bytes = 22 + chunk_size(frame.len());
220 if !self.icc_profile.is_empty() {
221 total_bytes += chunk_size(self.icc_profile.len());
222 }
223 if !self.exif_metadata.is_empty() {
224 total_bytes += chunk_size(self.exif_metadata.len());
225 }
226 if !self.xmp_metadata.is_empty() {
227 total_bytes += chunk_size(self.xmp_metadata.len());
228 }
229
230 let alpha_chunk_data = if lossy_with_alpha {
231 let mut alpha_chunk = Vec::new();
232 encode_alpha_lossless(&mut alpha_chunk, data, width, height, color)?;
233
234 total_bytes += chunk_size(alpha_chunk.len());
235 Some(alpha_chunk)
236 } else {
237 None
238 };
239
240 let mut flags = 0;
241 if !self.xmp_metadata.is_empty() {
242 flags |= 1 << 2;
243 }
244 if !self.exif_metadata.is_empty() {
245 flags |= 1 << 3;
246 }
247 if color.has_alpha() {
248 flags |= 1 << 4;
249 }
250 if !self.icc_profile.is_empty() {
251 flags |= 1 << 5;
252 }
253
254 self.writer.write_all(b"RIFF")?;
255 self.writer.write_all(&total_bytes.to_le_bytes())?;
256 self.writer.write_all(b"WEBP")?;
257
258 let mut vp8x = Vec::new();
259 vp8x.write_all(&[flags])?; vp8x.write_all(&[0; 3])?; vp8x.write_all(&(width - 1).to_le_bytes()[..3])?; vp8x.write_all(&(height - 1).to_le_bytes()[..3])?; write_chunk(&mut self.writer, b"VP8X", &vp8x)?;
264
265 if !self.icc_profile.is_empty() {
266 write_chunk(&mut self.writer, b"ICCP", &self.icc_profile)?;
267 }
268
269 if let Some(alpha_chunk) = alpha_chunk_data {
270 write_chunk(&mut self.writer, b"ALPH", &alpha_chunk)?;
271 }
272
273 write_chunk(&mut self.writer, frame_chunk, &frame)?;
274
275 if !self.exif_metadata.is_empty() {
276 write_chunk(&mut self.writer, b"EXIF", &self.exif_metadata)?;
277 }
278
279 if !self.xmp_metadata.is_empty() {
280 write_chunk(&mut self.writer, b"XMP ", &self.xmp_metadata)?;
281 }
282 }
283
284 Ok(())
285 }
286}
287
288#[cfg(test)]
289mod tests {
290 use rand::RngCore;
291
292 use super::*;
293
294 #[test]
295 fn write_webp() {
296 let mut img = vec![0; 256 * 256 * 4];
297 rand::thread_rng().fill_bytes(&mut img);
298
299 let mut output = Vec::new();
300 WebPEncoder::new(&mut output)
301 .encode(&img, 256, 256, crate::ColorType::Rgba8)
302 .unwrap();
303
304 let mut decoder = crate::WebPDecoder::new(std::io::Cursor::new(output)).unwrap();
305 let mut img2 = vec![0; 256 * 256 * 4];
306 decoder.read_image(&mut img2).unwrap();
307 assert_eq!(img, img2);
308 }
309
310 #[test]
311 fn write_webp_exif() {
312 let mut img = vec![0; 256 * 256 * 3];
313 rand::thread_rng().fill_bytes(&mut img);
314
315 let mut exif = vec![0; 10];
316 rand::thread_rng().fill_bytes(&mut exif);
317
318 let mut output = Vec::new();
319 let mut encoder = WebPEncoder::new(&mut output);
320 encoder.set_exif_metadata(exif.clone());
321 encoder
322 .encode(&img, 256, 256, crate::ColorType::Rgb8)
323 .unwrap();
324
325 let mut decoder = crate::WebPDecoder::new(std::io::Cursor::new(output)).unwrap();
326
327 let mut img2 = vec![0; 256 * 256 * 3];
328 decoder.read_image(&mut img2).unwrap();
329 assert_eq!(img, img2);
330
331 let exif2 = decoder.exif_metadata().unwrap();
332 assert_eq!(Some(exif), exif2);
333 }
334
335 #[test]
350 fn roundtrip_libwebp_lossy_segmented() {
351 let (w, h): (u32, u32) = (200, 150);
352 let mut img = vec![0u8; (w * h * 3) as usize];
353 for y in 0..h {
354 for x in 0..w {
355 let i = ((y * w + x) * 3) as usize;
356 let (r, g, b) = if x < w / 2 {
357 let v = ((x * 255) / w.max(1)) as u8;
359 (v, v.wrapping_add((y % 255) as u8), 128)
360 } else {
361 let v = if (x / 4 + y / 4) % 2 == 0 { 20 } else { 235 };
363 (v, v, v)
364 };
365 img[i] = r;
366 img[i + 1] = g;
367 img[i + 2] = b;
368 }
369 }
370
371 for lossy_quality in [1u8, 40, 75, 95, 100] {
372 let mut output = Vec::new();
373 let mut encoder = WebPEncoder::new(&mut output);
374 encoder.set_params(EncoderParams {
375 use_lossy: true,
376 lossy_quality,
377 ..Default::default()
378 });
379 encoder
380 .encode(&img, w, h, crate::ColorType::Rgb8)
381 .unwrap_or_else(|e| panic!("encode failed at lossy_quality={lossy_quality}: {e}"));
382
383 let decoded = webp::Decoder::new(&output).decode().unwrap_or_else(|| {
384 panic!("libwebp failed to decode our bitstream at lossy_quality={lossy_quality}")
385 });
386 assert_eq!(decoded.width(), w);
387 assert_eq!(decoded.height(), h);
388 }
389 }
390
391 #[test]
392 fn roundtrip_libwebp() {
393 roundtrip_libwebp_params(EncoderParams::default());
394 roundtrip_libwebp_params(EncoderParams {
395 use_predictor_transform: false,
396 ..Default::default()
397 });
398 }
399
400 fn roundtrip_libwebp_params(params: EncoderParams) {
401 println!("Testing {params:?}");
402
403 let mut img = vec![0; 256 * 256 * 4];
404 rand::thread_rng().fill_bytes(&mut img);
405
406 let mut output = Vec::new();
407 let mut encoder = WebPEncoder::new(&mut output);
408 encoder.set_params(params.clone());
409 encoder
410 .encode(&img[..256 * 256 * 3], 256, 256, crate::ColorType::Rgb8)
411 .unwrap();
412 let decoded = webp::Decoder::new(&output).decode().unwrap();
413 assert_eq!(img[..256 * 256 * 3], *decoded);
414
415 let mut output = Vec::new();
416 let mut encoder = WebPEncoder::new(&mut output);
417 encoder.set_params(params.clone());
418 encoder
419 .encode(&img, 256, 256, crate::ColorType::Rgba8)
420 .unwrap();
421 let decoded = webp::Decoder::new(&output).decode().unwrap();
422 assert_eq!(img, *decoded);
423
424 let mut output = Vec::new();
425 let mut encoder = WebPEncoder::new(&mut output);
426 encoder.set_params(params.clone());
427 encoder.set_icc_profile(vec![0; 10]);
428 encoder
429 .encode(&img, 256, 256, crate::ColorType::Rgba8)
430 .unwrap();
431 let decoded = webp::Decoder::new(&output).decode().unwrap();
432 assert_eq!(img, *decoded);
433
434 let mut output = Vec::new();
435 let mut encoder = WebPEncoder::new(&mut output);
436 encoder.set_params(params.clone());
437 encoder.set_exif_metadata(vec![0; 10]);
438 encoder
439 .encode(&img, 256, 256, crate::ColorType::Rgba8)
440 .unwrap();
441 let decoded = webp::Decoder::new(&output).decode().unwrap();
442 assert_eq!(img, *decoded);
443
444 let mut output = Vec::new();
445 let mut encoder = WebPEncoder::new(&mut output);
446 encoder.set_params(params);
447 encoder.set_xmp_metadata(vec![0; 7]);
448 encoder.set_icc_profile(vec![0; 8]);
449 encoder.set_icc_profile(vec![0; 9]);
450 encoder
451 .encode(&img, 256, 256, crate::ColorType::Rgba8)
452 .unwrap();
453 let decoded = webp::Decoder::new(&output).decode().unwrap();
454 assert_eq!(img, *decoded);
455 }
456}