1use gamut_color::{Bt601Range, Yuv420};
9use gamut_core::{Dimensions, EncodeImage, ImageRef, Result, Rgb8, Rgba8};
10use gamut_riff::{FourCc, Vp8xHeader, write_extended, write_simple_lossless, write_simple_lossy};
11
12use crate::alpha;
13use crate::config::{WebpConfig, WebpMode};
14use crate::vp8::frame::encode_frame;
15use crate::vp8l::encoder::encode as encode_vp8l;
16use crate::vp8l::transform::make_argb;
17
18fn quality_to_quant(quality: u8) -> u8 {
21 let q = u32::from(quality.min(100));
22 ((100 - q) * 127 / 100) as u8
23}
24
25#[derive(Debug, Clone, Default)]
30pub struct WebpEncoder {
31 config: WebpConfig,
33}
34
35impl WebpEncoder {
36 #[must_use]
38 pub fn new() -> Self {
39 Self::default()
40 }
41
42 #[must_use]
44 pub fn lossless() -> Self {
45 Self {
46 config: WebpConfig {
47 mode: WebpMode::Lossless,
48 ..WebpConfig::default()
49 },
50 }
51 }
52
53 #[must_use]
55 pub fn lossy(quality: u8) -> Self {
56 Self {
57 config: WebpConfig {
58 mode: WebpMode::Lossy,
59 quality,
60 },
61 }
62 }
63
64 #[must_use]
66 pub fn config(&self) -> WebpConfig {
67 self.config
68 }
69
70 fn encode_rgb8_inner(
73 &self,
74 pixels: &[u8],
75 dims: Dimensions,
76 out: &mut Vec<u8>,
77 ) -> Result<usize> {
78 match self.config.mode {
79 WebpMode::Lossless => {
80 let argb: Vec<u32> = pixels
81 .chunks_exact(3)
82 .map(|p| make_argb(0xff, p[0], p[1], p[2]))
83 .collect();
84 let bitstream = encode_vp8l(&argb, dims)?;
85 let file = write_simple_lossless(&bitstream);
86 let written = file.len();
87 out.extend_from_slice(&file);
88 Ok(written)
89 }
90 WebpMode::Lossy => {
91 let yuv = Yuv420::from_rgb8(pixels, dims.width, dims.height, Bt601Range::Limited)?;
93 let (payload, _recon) = encode_frame(&yuv, quality_to_quant(self.config.quality));
94 let file = write_simple_lossy(&payload);
95 let written = file.len();
96 out.extend_from_slice(&file);
97 Ok(written)
98 }
99 }
100 }
101
102 fn encode_rgba8_inner(
107 &self,
108 pixels: &[u8],
109 dims: Dimensions,
110 out: &mut Vec<u8>,
111 ) -> Result<usize> {
112 let file = match self.config.mode {
113 WebpMode::Lossless => {
114 let argb: Vec<u32> = pixels
115 .chunks_exact(4)
116 .map(|p| make_argb(p[3], p[0], p[1], p[2]))
117 .collect();
118 write_simple_lossless(&encode_vp8l(&argb, dims)?)
119 }
120 WebpMode::Lossy => {
121 let rgb: Vec<u8> = pixels
122 .chunks_exact(4)
123 .flat_map(|p| [p[0], p[1], p[2]])
124 .collect();
125 let yuv = Yuv420::from_rgb8(&rgb, dims.width, dims.height, Bt601Range::Limited)?;
126 let (vp8, _) = encode_frame(&yuv, quality_to_quant(self.config.quality));
127 if pixels.chunks_exact(4).all(|p| p[3] == 0xff) {
128 write_simple_lossy(&vp8)
129 } else {
130 let alpha: Vec<u8> = pixels.chunks_exact(4).map(|p| p[3]).collect();
131 let alph =
132 alpha::write_alph(&alpha, dims.width as usize, dims.height as usize)?;
133 let header = Vp8xHeader {
134 alpha: true,
135 canvas_width: dims.width,
136 canvas_height: dims.height,
137 ..Default::default()
138 };
139 write_extended(&header, &[(FourCc::ALPH, &alph), (FourCc::VP8, &vp8)])
140 }
141 }
142 };
143 let written = file.len();
144 out.extend_from_slice(&file);
145 Ok(written)
146 }
147}
148
149impl EncodeImage<Rgb8> for WebpEncoder {
150 fn encode_image(&self, image: ImageRef<'_, Rgb8>, out: &mut Vec<u8>) -> Result<usize> {
151 self.encode_rgb8_inner(image.as_samples(), image.dimensions(), out)
152 }
153}
154
155impl EncodeImage<Rgba8> for WebpEncoder {
156 fn encode_image(&self, image: ImageRef<'_, Rgba8>, out: &mut Vec<u8>) -> Result<usize> {
157 self.encode_rgba8_inner(image.as_samples(), image.dimensions(), out)
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use gamut_core::{DecodeImage, ImageBuf};
165
166 fn dims(w: u32, h: u32) -> Dimensions {
167 Dimensions {
168 width: w,
169 height: h,
170 }
171 }
172
173 #[test]
174 fn constructors_select_mode() {
175 assert_eq!(WebpEncoder::new().config().mode, WebpMode::Lossless);
176 assert_eq!(WebpEncoder::lossless().config().mode, WebpMode::Lossless);
177 let lossy = WebpEncoder::lossy(40);
178 assert_eq!(lossy.config().mode, WebpMode::Lossy);
179 assert_eq!(lossy.config().quality, 40);
180 }
181
182 #[test]
183 fn rejects_mismatched_buffer_length() {
184 assert!(ImageRef::<Rgb8>::new(&[0u8; 10], dims(2, 2)).is_err());
186 }
187
188 #[test]
189 fn lossless_encodes_a_valid_webp_file() {
190 let mut out = Vec::new();
193 let rgb = [0x10, 0x20, 0x30].repeat(4);
194 let written = WebpEncoder::lossless()
195 .encode_image(ImageRef::<Rgb8>::new(&rgb, dims(2, 2)).unwrap(), &mut out)
196 .expect("encode");
197 assert_eq!(written, out.len());
198 assert_eq!(&out[0..4], b"RIFF");
199
200 let decoded: ImageBuf<Rgb8> = crate::WebpDecoder::new()
201 .decode_image(&out)
202 .expect("decode");
203 assert_eq!(decoded.dimensions(), dims(2, 2));
204 assert_eq!(decoded.as_samples(), rgb.as_slice());
205 }
206
207 #[test]
208 fn lossy_encodes_a_decodable_webp_file() {
209 let mut out = Vec::new();
212 let rgb = [40u8, 80, 120].repeat(16 * 16);
213 let written = WebpEncoder::lossy(60)
214 .encode_image(ImageRef::<Rgb8>::new(&rgb, dims(16, 16)).unwrap(), &mut out)
215 .expect("lossy encode");
216 assert_eq!(written, out.len());
217 assert_eq!(&out[0..4], b"RIFF");
218 let decoded: ImageBuf<Rgb8> = crate::WebpDecoder::new()
219 .decode_image(&out)
220 .expect("decode");
221 assert_eq!(decoded.dimensions(), dims(16, 16));
222 assert_eq!(decoded.as_samples().len(), 16 * 16 * 3);
223 }
224
225 #[test]
226 fn lossy_rgba_round_trips_alpha_exactly() {
227 let (w, h) = (32u32, 24u32);
230 let rgba: Vec<u8> = (0..(w * h) as usize)
231 .flat_map(|i| {
232 let (x, y) = (i as u32 % w, i as u32 / w);
233 [
234 (x * 7) as u8,
235 (y * 9) as u8,
236 (x ^ y) as u8,
237 ((x * 5 + y * 3) & 0xff) as u8,
238 ]
239 })
240 .collect();
241 let mut file = Vec::new();
242 WebpEncoder::lossy(75)
243 .encode_image(
244 ImageRef::<Rgba8>::new(&rgba, dims(w, h)).unwrap(),
245 &mut file,
246 )
247 .expect("rgba encode");
248 assert_eq!(&file[0..4], b"RIFF");
249
250 let decoded: ImageBuf<Rgba8> = crate::WebpDecoder::new()
251 .decode_image(&file)
252 .expect("rgba decode");
253 assert_eq!(decoded.dimensions(), dims(w, h));
254 let dec_alpha: Vec<u8> = decoded.as_samples().chunks_exact(4).map(|p| p[3]).collect();
255 let src_alpha: Vec<u8> = rgba.chunks_exact(4).map(|p| p[3]).collect();
256 assert_eq!(dec_alpha, src_alpha, "alpha must round-trip losslessly");
257 }
258
259 #[test]
260 fn opaque_rgba_uses_the_simple_lossy_format() {
261 use gamut_riff::{RiffReader, WebpChunkId};
262 let rgba = [120u8, 60, 200, 0xff].repeat(16 * 16);
263 let mut file = Vec::new();
264 WebpEncoder::lossy(60)
265 .encode_image(
266 ImageRef::<Rgba8>::new(&rgba, dims(16, 16)).unwrap(),
267 &mut file,
268 )
269 .expect("rgba encode");
270 let ids: Vec<_> = RiffReader::new(&file)
272 .unwrap()
273 .map(|c| WebpChunkId::from(c.unwrap().fourcc))
274 .collect();
275 assert_eq!(ids, vec![WebpChunkId::Vp8]);
276 }
277
278 #[test]
279 fn encode_image_is_object_safe() {
280 let mut out = Vec::new();
281 let rgb = [7u8, 8, 9];
282 let enc: &dyn EncodeImage<Rgb8> = &WebpEncoder::new();
283 let written = enc
284 .encode_image(ImageRef::<Rgb8>::new(&rgb, dims(1, 1)).unwrap(), &mut out)
285 .expect("encode via trait");
286 assert_eq!(written, out.len());
287 assert_eq!(&out[0..4], b"RIFF");
288 }
289}