1use super::bitstream::*;
4use super::predict::*;
5use super::*;
6use crate::encoder::writer::ByteWriter;
7
8fn build_vp8_frame(
10 width: usize,
11 height: usize,
12 partition0: &[u8],
13 token_partition: &[u8],
14) -> Result<Vec<u8>, EncoderError> {
15 if partition0.len() > MAX_PARTITION0_LENGTH {
16 return Err(EncoderError::Bitstream("VP8 partition 0 overflow"));
17 }
18
19 let payload_size = 10usize
20 .checked_add(partition0.len())
21 .and_then(|size| size.checked_add(token_partition.len()))
22 .ok_or(EncoderError::InvalidParam("encoded output is too large"))?;
23
24 let mut data = ByteWriter::with_capacity(payload_size);
25 let frame_bits = ((partition0.len() as u32) << 5) | (1 << 4);
26 data.write_u24_le(frame_bits);
27 data.write_bytes(&[0x9d, 0x01, 0x2a]);
28 data.write_u16_le(width as u16);
29 data.write_u16_le(height as u16);
30 data.write_bytes(partition0);
31 data.write_bytes(token_partition);
32 Ok(data.into_bytes())
33}
34
35fn build_candidate_vp8_frame(
37 width: usize,
38 height: usize,
39 mb_width: usize,
40 mb_height: usize,
41 candidate: &EncodedLossyCandidate,
42 filter: &FilterConfig,
43) -> Result<Vec<u8>, EncoderError> {
44 let segment = segment_with_uniform_filter(&candidate.segment, filter.level);
45 let partition0 = encode_partition0(
46 mb_width,
47 mb_height,
48 candidate.base_quant,
49 &segment,
50 filter,
51 &candidate.probabilities,
52 &candidate.modes,
53 );
54 build_vp8_frame(width, height, &partition0, &candidate.token_partition)
55}
56
57fn encode_lossy_candidate(
59 source: &Planes,
60 mb_width: usize,
61 mb_height: usize,
62 profile: &LossySearchProfile,
63 segment: &SegmentConfig,
64) -> Result<EncodedLossyCandidate, EncoderError> {
65 let segment_quants = build_segment_quantizers(segment);
66 let (token_partition, probabilities, modes) = if profile.update_probabilities {
67 let mut stats = [[[[0u32; NUM_PROBAS]; NUM_CTX]; NUM_BANDS]; NUM_TYPES];
68 let (initial_partition, _, initial_modes) = encode_token_partition(
69 source,
70 mb_width,
71 mb_height,
72 profile,
73 segment,
74 &segment_quants,
75 &COEFFS_PROBA0,
76 Some(&mut stats),
77 );
78 let probabilities = finalize_token_probabilities(&stats);
79 if probabilities == COEFFS_PROBA0 {
80 (initial_partition, probabilities, initial_modes)
81 } else {
82 let (token_partition, _, modes) = encode_token_partition(
83 source,
84 mb_width,
85 mb_height,
86 profile,
87 segment,
88 &segment_quants,
89 &probabilities,
90 None,
91 );
92 (token_partition, probabilities, modes)
93 }
94 } else {
95 let (token_partition, _, modes) = encode_token_partition(
96 source,
97 mb_width,
98 mb_height,
99 profile,
100 segment,
101 &segment_quants,
102 &COEFFS_PROBA0,
103 None,
104 );
105 (token_partition, COEFFS_PROBA0, modes)
106 };
107 Ok(EncodedLossyCandidate {
108 base_quant: segment.quantizer[0],
109 segment: segment.clone(),
110 probabilities,
111 modes,
112 token_partition,
113 })
114}
115
116fn finalize_lossy_candidate(
118 width: usize,
119 height: usize,
120 source: &Planes,
121 mb_width: usize,
122 mb_height: usize,
123 base_quant: i32,
124 optimization_level: u8,
125 candidate: &EncodedLossyCandidate,
126) -> Result<Vec<u8>, EncoderError> {
127 let mb_count = mb_width * mb_height;
128 if !use_exhaustive_filter_search(optimization_level, mb_count) {
129 let filter = heuristic_filter(base_quant);
130 return build_candidate_vp8_frame(width, height, mb_width, mb_height, candidate, &filter);
131 }
132
133 let filters = filter_candidates(base_quant);
134 let mut best = None;
135 for filter in &filters {
136 let vp8 = build_candidate_vp8_frame(width, height, mb_width, mb_height, candidate, filter)?;
137 let distortion = yuv_sse(source, width, height, &vp8)?;
138 let replace = match &best {
139 Some((best_distortion, best_len, _)) => {
140 distortion < *best_distortion
141 || (distortion == *best_distortion && vp8.len() < *best_len)
142 }
143 None => true,
144 };
145 if replace {
146 best = Some((distortion, vp8.len(), vp8));
147 }
148 }
149
150 best.map(|(_, _, vp8)| vp8).ok_or(EncoderError::Bitstream(
151 "lossy filter search produced no output",
152 ))
153}
154
155pub fn encode_lossy_rgba_to_vp8_with_options(
157 width: usize,
158 height: usize,
159 rgba: &[u8],
160 options: &LossyEncodingOptions,
161) -> Result<Vec<u8>, EncoderError> {
162 validate_rgba(width, height, rgba)?;
163 validate_options(options)?;
164
165 let mb_width = (width + 15) >> 4;
166 let mb_height = (height + 15) >> 4;
167 let base_quant = base_quantizer_from_quality(options.quality);
168 let profile = lossy_search_profile(options.optimization_level);
169 let source = rgba_to_yuv420(width, height, rgba, mb_width, mb_height);
170 let candidates = build_segment_candidates(
171 &source,
172 mb_width,
173 mb_height,
174 base_quant,
175 options.optimization_level,
176 );
177 let mut best = None;
178 for segment in &candidates {
179 let candidate = encode_lossy_candidate(&source, mb_width, mb_height, &profile, segment)?;
180 let vp8 = finalize_lossy_candidate(
181 width,
182 height,
183 &source,
184 mb_width,
185 mb_height,
186 base_quant,
187 options.optimization_level,
188 &candidate,
189 )?;
190 let replace = match &best {
191 Some((best_bytes, _)) => vp8.len() < *best_bytes,
192 None => true,
193 };
194 if replace {
195 best = Some((vp8.len(), vp8));
196 }
197 }
198
199 best.map(|(_, vp8)| vp8).ok_or(EncoderError::Bitstream(
200 "lossy candidate search produced no output",
201 ))
202}
203
204pub fn encode_lossy_rgba_to_vp8(
206 width: usize,
207 height: usize,
208 rgba: &[u8],
209) -> Result<Vec<u8>, EncoderError> {
210 encode_lossy_rgba_to_vp8_with_options(width, height, rgba, &LossyEncodingOptions::default())
211}
212
213pub fn encode_lossy_rgba_to_webp_with_options(
215 width: usize,
216 height: usize,
217 rgba: &[u8],
218 options: &LossyEncodingOptions,
219) -> Result<Vec<u8>, EncoderError> {
220 encode_lossy_rgba_to_webp_with_options_and_exif(width, height, rgba, options, None)
221}
222
223pub fn encode_lossy_rgba_to_webp_with_options_and_exif(
225 width: usize,
226 height: usize,
227 rgba: &[u8],
228 options: &LossyEncodingOptions,
229 exif: Option<&[u8]>,
230) -> Result<Vec<u8>, EncoderError> {
231 let vp8 = encode_lossy_rgba_to_vp8_with_options(width, height, rgba, options)?;
232 wrap_still_webp(
233 StillImageChunk {
234 fourcc: *b"VP8 ",
235 payload: &vp8,
236 width,
237 height,
238 has_alpha: false,
239 },
240 exif,
241 )
242}
243
244pub fn encode_lossy_rgba_to_webp(
246 width: usize,
247 height: usize,
248 rgba: &[u8],
249) -> Result<Vec<u8>, EncoderError> {
250 encode_lossy_rgba_to_webp_with_options(width, height, rgba, &LossyEncodingOptions::default())
251}
252
253pub fn encode_lossy_image_to_webp_with_options(
255 image: &ImageBuffer,
256 options: &LossyEncodingOptions,
257) -> Result<Vec<u8>, EncoderError> {
258 encode_lossy_image_to_webp_with_options_and_exif(image, options, None)
259}
260
261pub fn encode_lossy_image_to_webp_with_options_and_exif(
263 image: &ImageBuffer,
264 options: &LossyEncodingOptions,
265 exif: Option<&[u8]>,
266) -> Result<Vec<u8>, EncoderError> {
267 encode_lossy_rgba_to_webp_with_options_and_exif(
268 image.width,
269 image.height,
270 &image.rgba,
271 options,
272 exif,
273 )
274}
275
276pub fn encode_lossy_image_to_webp(image: &ImageBuffer) -> Result<Vec<u8>, EncoderError> {
278 encode_lossy_image_to_webp_with_options(image, &LossyEncodingOptions::default())
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284 use crate::decoder::decode_lossy_vp8_to_yuv;
285
286 fn sample_rgba() -> (usize, usize, Vec<u8>) {
287 let width = 19;
288 let height = 17;
289 let mut rgba = vec![0u8; width * height * 4];
290 for y in 0..height {
291 for x in 0..width {
292 let offset = (y * width + x) * 4;
293 rgba[offset] = (x as u8).saturating_mul(12);
294 rgba[offset + 1] = (y as u8).saturating_mul(13);
295 rgba[offset + 2] = ((x + y) as u8).saturating_mul(7);
296 rgba[offset + 3] = 0xff;
297 }
298 }
299 (width, height, rgba)
300 }
301
302 #[test]
303 fn internal_reconstruction_matches_decoder_output() {
304 let (width, height, rgba) = sample_rgba();
305 let mb_width = (width + 15) >> 4;
306 let mb_height = (height + 15) >> 4;
307 let options = LossyEncodingOptions::default();
308 let base_quant = base_quantizer_from_quality(options.quality);
309 let profile = lossy_search_profile(options.optimization_level);
310 let source = rgba_to_yuv420(width, height, &rgba, mb_width, mb_height);
311 let segment = disabled_segment_config(mb_width * mb_height, clipped_quantizer(base_quant));
312 let candidate =
313 encode_lossy_candidate(&source, mb_width, mb_height, &profile, &segment).unwrap();
314 let partition0 = encode_partition0(
315 mb_width,
316 mb_height,
317 base_quant as u8,
318 &segment,
319 &FilterConfig {
320 simple: false,
321 level: 0,
322 sharpness: 0,
323 },
324 &candidate.probabilities,
325 &candidate.modes,
326 );
327 let vp8 = build_vp8_frame(width, height, &partition0, &candidate.token_partition).unwrap();
328 let decoded = decode_lossy_vp8_to_yuv(&vp8).unwrap();
329 let (_, reconstructed, _) = encode_token_partition(
330 &source,
331 mb_width,
332 mb_height,
333 &profile,
334 &segment,
335 &build_segment_quantizers(&segment),
336 &candidate.probabilities,
337 None,
338 );
339 assert_eq!(decoded.y, reconstructed.y);
340 assert_eq!(decoded.u, reconstructed.u);
341 assert_eq!(decoded.v, reconstructed.v);
342 }
343
344 #[test]
345 fn mode_search_prefers_vertical_prediction_for_repeated_top_rows() {
346 let mb_width = 1;
347 let mb_height = 2;
348 let mut source = empty_reconstructed_planes(mb_width, mb_height);
349 let mut reconstructed = empty_reconstructed_planes(mb_width, mb_height);
350
351 for row in 0..16 {
352 for col in 0..16 {
353 let value = (col as u8).saturating_mul(9);
354 reconstructed.y[row * reconstructed.y_stride + col] = value;
355 source.y[(16 + row) * source.y_stride + col] = value;
356 }
357 }
358
359 for row in 0..8 {
360 for col in 0..8 {
361 let u = (32 + col * 7) as u8;
362 let v = (96 + col * 5) as u8;
363 reconstructed.u[row * reconstructed.uv_stride + col] = u;
364 reconstructed.v[row * reconstructed.uv_stride + col] = v;
365 source.u[(8 + row) * source.uv_stride + col] = u;
366 source.v[(8 + row) * source.uv_stride + col] = v;
367 }
368 }
369
370 let quant = build_quant_matrices(base_quantizer_from_quality(90));
371 let rd = build_rd_multipliers(&quant);
372 let profile = lossy_search_profile(MAX_LOSSY_OPTIMIZATION_LEVEL);
373 let top_modes = [B_DC_PRED; 4];
374 let left_modes = [B_DC_PRED; 4];
375 let top_context = NonZeroContext::default();
376 let left_context = NonZeroContext::default();
377 let mode = choose_macroblock_mode(
378 &source,
379 &mut reconstructed,
380 0,
381 1,
382 &profile,
383 &quant,
384 &rd,
385 &COEFFS_PROBA0,
386 &top_context,
387 &left_context,
388 &top_modes,
389 &left_modes,
390 );
391 assert!(matches!(mode.luma, V_PRED | B_PRED));
392 assert_eq!(mode.chroma, V_PRED);
393 }
394
395 #[test]
396 fn segment_candidates_include_segmented_plan_for_mixed_activity() {
397 let width = 64;
398 let height = 32;
399 let mb_width = (width + 15) >> 4;
400 let mb_height = (height + 15) >> 4;
401 let mut rgba = vec![0u8; width * height * 4];
402 for y in 0..height {
403 for x in 0..width {
404 let offset = (y * width + x) * 4;
405 let (r, g, b) = if x < width / 2 {
406 (0x80, 0x80, 0x80)
407 } else {
408 (
409 ((x * 17 + y * 3) & 0xff) as u8,
410 ((x * 5 + y * 11) & 0xff) as u8,
411 ((x * 13 + y * 7) & 0xff) as u8,
412 )
413 };
414 rgba[offset] = r;
415 rgba[offset + 1] = g;
416 rgba[offset + 2] = b;
417 rgba[offset + 3] = 0xff;
418 }
419 }
420
421 let source = rgba_to_yuv420(width, height, &rgba, mb_width, mb_height);
422 let candidates = build_segment_candidates(
423 &source,
424 mb_width,
425 mb_height,
426 13,
427 MAX_LOSSY_OPTIMIZATION_LEVEL,
428 );
429
430 assert!(candidates.iter().any(|candidate| candidate.use_segment));
431 assert!(candidates
432 .iter()
433 .filter(|candidate| candidate.use_segment)
434 .any(|candidate| candidate.segments.iter().any(|&segment| segment != 0)));
435 }
436
437 #[test]
438 fn segment_candidates_can_use_more_than_two_segments() {
439 let width = 96;
440 let height = 64;
441 let mb_width = (width + 15) >> 4;
442 let mb_height = (height + 15) >> 4;
443 let mut rgba = vec![0u8; width * height * 4];
444 for y in 0..height {
445 for x in 0..width {
446 let offset = (y * width + x) * 4;
447 let band = x / 24;
448 let value = match band {
449 0 => 96,
450 1 => ((x * 3 + y * 5) & 0xff) as u8,
451 2 => ((x * 9 + y * 13) & 0xff) as u8,
452 _ => ((x * 17 + y * 29) & 0xff) as u8,
453 };
454 rgba[offset] = value;
455 rgba[offset + 1] = value.wrapping_add((band * 17) as u8);
456 rgba[offset + 2] = value.wrapping_add((band * 33) as u8);
457 rgba[offset + 3] = 0xff;
458 }
459 }
460
461 let source = rgba_to_yuv420(width, height, &rgba, mb_width, mb_height);
462 let candidates = build_segment_candidates(
463 &source,
464 mb_width,
465 mb_height,
466 13,
467 MAX_LOSSY_OPTIMIZATION_LEVEL,
468 );
469
470 assert!(candidates.iter().any(|candidate| {
471 candidate.use_segment && candidate.segments.iter().copied().max().unwrap_or(0) >= 2
472 }));
473 }
474}