1use gamut_core::{Error, Result};
13
14use super::bool_coder::{BoolDecoder, BoolEncoder};
15use super::tokens::{self, CoeffProbs, DEFAULT_COEFF_PROBS};
16
17pub const VP8_KEYFRAME_START_CODE: [u8; 3] = [0x9d, 0x01, 0x2a];
19
20pub const UNCOMPRESSED_CHUNK_LEN: usize = 10;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
25pub struct Segmentation {
26 pub enabled: bool,
28 pub update_map: bool,
30 pub abs_delta: bool,
33 pub quantizer: [i8; 4],
35 pub filter_strength: [i8; 4],
37 pub tree_probs: [u8; 3],
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
43pub struct LoopFilterParams {
44 pub simple: bool,
46 pub level: u8,
48 pub sharpness: u8,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
54pub struct QuantIndices {
55 pub y_ac: u8,
57 pub y_dc_delta: i8,
59 pub y2_dc_delta: i8,
61 pub y2_ac_delta: i8,
63 pub uv_dc_delta: i8,
65 pub uv_ac_delta: i8,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub struct Vp8FrameHeader {
73 pub width: u16,
75 pub height: u16,
77 pub horizontal_scale: u8,
79 pub vertical_scale: u8,
81 pub version: u8,
83 pub color_space: u8,
85 pub clamp_required: bool,
87 pub segmentation: Segmentation,
89 pub loop_filter: LoopFilterParams,
91 pub token_partitions: u8,
93 pub quant: QuantIndices,
95 pub refresh_entropy_probs: bool,
97 pub mb_no_skip_coeff: bool,
99 pub prob_skip_false: u8,
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub struct UncompressedChunk {
106 pub is_key_frame: bool,
108 pub version: u8,
110 pub show_frame: bool,
112 pub first_partition_size: u32,
114 pub width: u16,
116 pub height: u16,
118 pub horizontal_scale: u8,
120 pub vertical_scale: u8,
122}
123
124fn log2_partitions(count: u8) -> u32 {
126 debug_assert!(
127 matches!(count, 1 | 2 | 4 | 8),
128 "token partition count must be 1, 2, 4, or 8"
129 );
130 u32::from(count).trailing_zeros()
131}
132
133pub fn write_uncompressed_chunk(
137 header: &Vp8FrameHeader,
138 first_partition_size: u32,
139 out: &mut Vec<u8>,
140) {
141 let tag = (u32::from(header.version) << 1) | (1 << 4) | (first_partition_size << 5);
143 out.push((tag & 0xff) as u8);
144 out.push(((tag >> 8) & 0xff) as u8);
145 out.push(((tag >> 16) & 0xff) as u8);
146 out.extend_from_slice(&VP8_KEYFRAME_START_CODE);
147 let h = u32::from(header.width) | (u32::from(header.horizontal_scale) << 14);
148 out.push((h & 0xff) as u8);
149 out.push(((h >> 8) & 0xff) as u8);
150 let v = u32::from(header.height) | (u32::from(header.vertical_scale) << 14);
151 out.push((v & 0xff) as u8);
152 out.push(((v >> 8) & 0xff) as u8);
153}
154
155pub fn read_uncompressed_chunk(data: &[u8]) -> Result<UncompressedChunk> {
162 if data.len() < 3 {
163 return Err(Error::InvalidInput("VP8: truncated frame tag"));
164 }
165 let tag = u32::from(data[0]) | (u32::from(data[1]) << 8) | (u32::from(data[2]) << 16);
166 let is_key_frame = (tag & 1) == 0;
167 if !is_key_frame {
168 return Err(Error::Unsupported(
169 "VP8: only intra key frames are supported",
170 ));
171 }
172 if data.len() < UNCOMPRESSED_CHUNK_LEN {
173 return Err(Error::InvalidInput("VP8: truncated key-frame header"));
174 }
175 if data[3..6] != VP8_KEYFRAME_START_CODE {
176 return Err(Error::InvalidInput("VP8: bad key-frame start code"));
177 }
178 let hsc = u32::from(data[6]) | (u32::from(data[7]) << 8);
179 let vsc = u32::from(data[8]) | (u32::from(data[9]) << 8);
180 Ok(UncompressedChunk {
181 is_key_frame,
182 version: ((tag >> 1) & 0x7) as u8,
183 show_frame: (tag >> 4) & 1 != 0,
184 first_partition_size: (tag >> 5) & 0x7_FFFF,
185 width: (hsc & 0x3FFF) as u16,
186 horizontal_scale: (hsc >> 14) as u8,
187 height: (vsc & 0x3FFF) as u16,
188 vertical_scale: (vsc >> 14) as u8,
189 })
190}
191
192fn write_delta(enc: &mut BoolEncoder, delta: i8) {
194 if delta == 0 {
195 enc.put_flag(false);
196 } else {
197 enc.put_flag(true);
198 enc.put_literal(u32::from(delta.unsigned_abs()), 4);
199 enc.put_flag(delta < 0);
200 }
201}
202
203fn read_delta(dec: &mut BoolDecoder) -> i8 {
205 if dec.get_flag() {
206 let magnitude = dec.get_literal(4) as i8;
207 if dec.get_flag() {
208 -magnitude
209 } else {
210 magnitude
211 }
212 } else {
213 0
214 }
215}
216
217fn write_quant_indices(enc: &mut BoolEncoder, quant: &QuantIndices) {
219 enc.put_literal(u32::from(quant.y_ac), 7);
220 write_delta(enc, quant.y_dc_delta);
221 write_delta(enc, quant.y2_dc_delta);
222 write_delta(enc, quant.y2_ac_delta);
223 write_delta(enc, quant.uv_dc_delta);
224 write_delta(enc, quant.uv_ac_delta);
225}
226
227fn read_quant_indices(dec: &mut BoolDecoder) -> QuantIndices {
229 QuantIndices {
230 y_ac: dec.get_literal(7) as u8,
231 y_dc_delta: read_delta(dec),
232 y2_dc_delta: read_delta(dec),
233 y2_ac_delta: read_delta(dec),
234 uv_dc_delta: read_delta(dec),
235 uv_ac_delta: read_delta(dec),
236 }
237}
238
239fn write_update_segmentation(enc: &mut BoolEncoder, seg: &Segmentation) {
242 enc.put_flag(seg.update_map);
243 let update_data = seg.abs_delta || seg.quantizer != [0; 4] || seg.filter_strength != [0; 4];
244 enc.put_flag(update_data);
245 if update_data {
246 enc.put_flag(seg.abs_delta); for q in seg.quantizer {
248 write_segment_feature(enc, q, 7);
249 }
250 for f in seg.filter_strength {
251 write_segment_feature(enc, f, 6);
252 }
253 }
254 if seg.update_map {
255 for p in seg.tree_probs {
256 if p == 255 {
257 enc.put_flag(false); } else {
259 enc.put_flag(true);
260 enc.put_literal(u32::from(p), 8);
261 }
262 }
263 }
264}
265
266fn write_segment_feature(enc: &mut BoolEncoder, value: i8, bits: u32) {
268 if value == 0 {
269 enc.put_flag(false);
270 } else {
271 enc.put_flag(true);
272 enc.put_literal(u32::from(value.unsigned_abs()), bits);
273 enc.put_flag(value < 0);
274 }
275}
276
277fn read_update_segmentation(dec: &mut BoolDecoder) -> Segmentation {
279 let update_map = dec.get_flag();
280 let mut seg = Segmentation {
281 enabled: true,
282 update_map,
283 tree_probs: [255; 3],
284 ..Segmentation::default()
285 };
286 if dec.get_flag() {
287 seg.abs_delta = dec.get_flag();
288 for q in &mut seg.quantizer {
289 *q = read_segment_feature(dec, 7);
290 }
291 for f in &mut seg.filter_strength {
292 *f = read_segment_feature(dec, 6);
293 }
294 }
295 if update_map {
296 for p in &mut seg.tree_probs {
297 if dec.get_flag() {
298 *p = dec.get_literal(8) as u8;
299 }
300 }
301 }
302 seg
303}
304
305fn read_segment_feature(dec: &mut BoolDecoder, bits: u32) -> i8 {
307 if dec.get_flag() {
308 let mag = dec.get_literal(bits) as i8;
309 if dec.get_flag() { -mag } else { mag }
310 } else {
311 0
312 }
313}
314
315pub fn write_frame_header(enc: &mut BoolEncoder, header: &Vp8FrameHeader) {
320 enc.put_literal(u32::from(header.color_space), 1);
321 enc.put_flag(!header.clamp_required); enc.put_flag(header.segmentation.enabled);
323 if header.segmentation.enabled {
324 write_update_segmentation(enc, &header.segmentation);
325 }
326 enc.put_flag(header.loop_filter.simple); enc.put_literal(u32::from(header.loop_filter.level), 6);
328 enc.put_literal(u32::from(header.loop_filter.sharpness), 3);
329 enc.put_flag(false); enc.put_literal(log2_partitions(header.token_partitions), 2);
331 write_quant_indices(enc, &header.quant);
332 enc.put_flag(header.refresh_entropy_probs);
333 tokens::write_coeff_prob_updates(enc, &DEFAULT_COEFF_PROBS, &DEFAULT_COEFF_PROBS);
334 enc.put_flag(header.mb_no_skip_coeff);
335 if header.mb_no_skip_coeff {
336 enc.put_literal(u32::from(header.prob_skip_false), 8);
337 }
338}
339
340pub fn read_frame_header(
348 chunk: &UncompressedChunk,
349 dec: &mut BoolDecoder,
350) -> Result<(Vp8FrameHeader, CoeffProbs)> {
351 let color_space = dec.get_literal(1) as u8;
352 let clamp_required = !dec.get_flag();
353 let segmentation = if dec.get_flag() {
354 read_update_segmentation(dec)
355 } else {
356 Segmentation::default()
357 };
358 let loop_filter = LoopFilterParams {
359 simple: dec.get_flag(),
360 level: dec.get_literal(6) as u8,
361 sharpness: dec.get_literal(3) as u8,
362 };
363 if dec.get_flag() {
364 return Err(Error::Unsupported(
365 "VP8: loop-filter adjustments not yet supported",
366 ));
367 }
368 let token_partitions = 1u8 << dec.get_literal(2);
369 let quant = read_quant_indices(dec);
370 let refresh_entropy_probs = dec.get_flag();
371 let mut coeff_probs = DEFAULT_COEFF_PROBS;
372 tokens::read_coeff_prob_updates(dec, &mut coeff_probs);
373 let mb_no_skip_coeff = dec.get_flag();
374 let prob_skip_false = if mb_no_skip_coeff {
375 dec.get_literal(8) as u8
376 } else {
377 0
378 };
379 let header = Vp8FrameHeader {
380 width: chunk.width,
381 height: chunk.height,
382 horizontal_scale: chunk.horizontal_scale,
383 vertical_scale: chunk.vertical_scale,
384 version: chunk.version,
385 color_space,
386 clamp_required,
387 segmentation,
388 loop_filter,
389 token_partitions,
390 quant,
391 refresh_entropy_probs,
392 mb_no_skip_coeff,
393 prob_skip_false,
394 };
395 Ok((header, coeff_probs))
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401
402 fn sample_header() -> Vp8FrameHeader {
403 Vp8FrameHeader {
404 width: 176,
405 height: 144,
406 horizontal_scale: 0,
407 vertical_scale: 0,
408 version: 0,
409 color_space: 0,
410 clamp_required: true,
411 segmentation: Segmentation::default(),
412 loop_filter: LoopFilterParams {
413 simple: false,
414 level: 0,
415 sharpness: 0,
416 },
417 token_partitions: 1,
418 quant: QuantIndices::default(),
419 refresh_entropy_probs: true,
420 mb_no_skip_coeff: false,
421 prob_skip_false: 0,
422 }
423 }
424
425 fn roundtrip(header: &Vp8FrameHeader) {
427 let mut enc = BoolEncoder::new();
428 write_frame_header(&mut enc, header);
429 let part0 = enc.finish();
430 let mut stream = Vec::new();
431 write_uncompressed_chunk(header, part0.len() as u32, &mut stream);
432 stream.extend_from_slice(&part0);
433
434 let chunk = read_uncompressed_chunk(&stream).expect("chunk");
435 assert!(chunk.is_key_frame);
436 assert_eq!((chunk.width, chunk.height), (header.width, header.height));
437 assert_eq!(chunk.first_partition_size as usize, part0.len());
438
439 let end = UNCOMPRESSED_CHUNK_LEN + chunk.first_partition_size as usize;
440 let mut dec = BoolDecoder::new(&stream[UNCOMPRESSED_CHUNK_LEN..end]);
441 let (decoded, probs) = read_frame_header(&chunk, &mut dec).expect("header");
442 assert_eq!(&decoded, header);
443 assert_eq!(
444 probs, DEFAULT_COEFF_PROBS,
445 "minimal header carries no prob updates"
446 );
447 }
448
449 #[test]
450 fn minimal_header_round_trips() {
451 roundtrip(&sample_header());
452 }
453
454 #[test]
455 fn dimensions_and_scale_round_trip() {
456 for (w, h, hs, vs) in [
457 (1u16, 1u16, 0u8, 0u8),
458 (16, 16, 0, 0),
459 (16383, 1, 3, 0),
460 (17, 9, 1, 2),
461 ] {
462 let mut header = sample_header();
463 header.width = w;
464 header.height = h;
465 header.horizontal_scale = hs;
466 header.vertical_scale = vs;
467 roundtrip(&header);
468 }
469 }
470
471 #[test]
472 fn quant_filter_and_flags_round_trip() {
473 let mut header = sample_header();
474 header.quant = QuantIndices {
475 y_ac: 100,
476 y_dc_delta: 7,
477 y2_dc_delta: -8,
478 y2_ac_delta: 15,
479 uv_dc_delta: -1,
480 uv_ac_delta: 0,
481 };
482 header.loop_filter = LoopFilterParams {
483 simple: true,
484 level: 47,
485 sharpness: 5,
486 };
487 header.color_space = 1;
488 header.clamp_required = false;
489 header.refresh_entropy_probs = false;
490 header.version = 3;
491 roundtrip(&header);
492 }
493
494 #[test]
495 fn skip_probability_round_trips() {
496 let mut header = sample_header();
497 header.mb_no_skip_coeff = true;
498 header.prob_skip_false = 210;
499 roundtrip(&header);
500 }
501
502 #[test]
503 fn partition_counts_round_trip() {
504 for count in [1u8, 2, 4, 8] {
505 let mut header = sample_header();
506 header.token_partitions = count;
507 roundtrip(&header);
508 }
509 }
510
511 #[test]
512 fn rejects_inter_frame_and_bad_start_code() {
513 assert!(matches!(
515 read_uncompressed_chunk(&[0x01, 0, 0, 0x9d, 0x01, 0x2a, 0, 0, 0, 0]),
516 Err(Error::Unsupported(_))
517 ));
518 assert!(matches!(
520 read_uncompressed_chunk(&[0x00, 0, 0, 0x9d, 0x01, 0x2b, 16, 0, 16, 0]),
521 Err(Error::InvalidInput(_))
522 ));
523 assert!(read_uncompressed_chunk(&[0x00, 0, 0]).is_err());
525 }
526
527 #[test]
528 fn rejects_unsupported_lf_adjust() {
529 let chunk = UncompressedChunk {
530 is_key_frame: true,
531 version: 0,
532 show_frame: true,
533 first_partition_size: 0,
534 width: 16,
535 height: 16,
536 horizontal_scale: 0,
537 vertical_scale: 0,
538 };
539 let mut lf = BoolEncoder::new();
541 lf.put_literal(0, 1);
542 lf.put_flag(true);
543 lf.put_flag(false);
544 lf.put_flag(false);
545 lf.put_literal(0, 6);
546 lf.put_literal(0, 3);
547 lf.put_flag(true);
548 let bytes = lf.finish();
549 assert!(matches!(
550 read_frame_header(&chunk, &mut BoolDecoder::new(&bytes)),
551 Err(Error::Unsupported(_))
552 ));
553 }
554
555 #[test]
556 fn segmentation_round_trips() {
557 let mut header = sample_header();
558 header.segmentation = Segmentation {
559 enabled: true,
560 update_map: true,
561 abs_delta: false,
562 quantizer: [-8, -2, 5, 12],
563 filter_strength: [0; 4],
564 tree_probs: [120, 200, 64],
565 };
566 let chunk = UncompressedChunk {
567 is_key_frame: true,
568 version: 0,
569 show_frame: true,
570 first_partition_size: 0,
571 width: header.width,
572 height: header.height,
573 horizontal_scale: 0,
574 vertical_scale: 0,
575 };
576 let mut enc = BoolEncoder::new();
577 write_frame_header(&mut enc, &header);
578 let bytes = enc.finish();
579 let (decoded, _) = read_frame_header(&chunk, &mut BoolDecoder::new(&bytes)).unwrap();
580 assert_eq!(decoded.segmentation, header.segmentation);
581 }
582}