Skip to main content

webp_rust/encoder/lossy/
mod.rs

1//! Shared state and search profiles for the lossy `VP8` encoder.
2
3use crate::decoder::decode_lossy_vp8_to_yuv;
4use crate::decoder::quant::{AC_TABLE, DC_TABLE};
5use crate::decoder::tree::{BMODES_PROBA, COEFFS_PROBA0, COEFFS_UPDATE_PROBA, Y_MODES_INTRA4};
6use crate::decoder::vp8i::{
7    B_DC_PRED, B_HD_PRED, B_HE_PRED, B_HU_PRED, B_LD_PRED, B_PRED, B_RD_PRED, B_TM_PRED, B_VE_PRED,
8    B_VL_PRED, B_VR_PRED, DC_PRED, H_PRED, MB_FEATURE_TREE_PROBS, NUM_BANDS, NUM_BMODES, NUM_CTX,
9    NUM_MB_SEGMENTS, NUM_PROBAS, NUM_TYPES, TM_PRED, V_PRED,
10};
11use crate::encoder::container::{wrap_still_webp, StillImageChunk};
12use crate::encoder::vp8_bool_writer::Vp8BoolWriter;
13use crate::encoder::EncoderError;
14use crate::ImageBuffer;
15
16const MAX_WEBP_DIMENSION: usize = 1 << 14;
17const MAX_PARTITION0_LENGTH: usize = (1 << 19) - 1;
18const YUV_FIX: i32 = 16;
19const YUV_HALF: i32 = 1 << (YUV_FIX - 1);
20const VP8_TRANSFORM_AC3_C1: i32 = 20_091;
21const VP8_TRANSFORM_AC3_C2: i32 = 35_468;
22
23const CAT3: [u8; 4] = [173, 148, 140, 0];
24const CAT4: [u8; 5] = [176, 155, 140, 135, 0];
25const CAT5: [u8; 6] = [180, 157, 141, 134, 130, 0];
26const CAT6: [u8; 12] = [254, 254, 243, 230, 196, 177, 153, 140, 133, 130, 129, 0];
27const ZIGZAG: [usize; 16] = [0, 1, 4, 8, 5, 2, 3, 6, 9, 12, 13, 10, 7, 11, 14, 15];
28const BANDS: [usize; 17] = [0, 1, 2, 3, 6, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6, 7, 0];
29
30type CoeffProbTables = [[[[u8; NUM_PROBAS]; NUM_CTX]; NUM_BANDS]; NUM_TYPES];
31type CoeffStats = [[[[u32; NUM_PROBAS]; NUM_CTX]; NUM_BANDS]; NUM_TYPES];
32
33const DEFAULT_LOSSY_OPTIMIZATION_LEVEL: u8 = 0;
34const MAX_LOSSY_OPTIMIZATION_LEVEL: u8 = 9;
35
36/// Lossy encoder tuning knobs.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub struct LossyEncodingOptions {
39    /// Quality from `0` to `100`.
40    pub quality: u8,
41    /// Search effort from `0` to `9`.
42    ///
43    /// The default `0` favors fast encode speed. `9` enables the heaviest
44    /// search profile currently implemented.
45    pub optimization_level: u8,
46}
47
48impl Default for LossyEncodingOptions {
49    fn default() -> Self {
50        Self {
51            quality: 90,
52            optimization_level: DEFAULT_LOSSY_OPTIMIZATION_LEVEL,
53        }
54    }
55}
56
57#[derive(Debug, Clone, Copy, Default)]
58struct NonZeroContext {
59    nz: u8,
60    nz_dc: u8,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64struct MacroblockMode {
65    luma: u8,
66    sub_luma: [u8; 16],
67    chroma: u8,
68    segment: u8,
69    skip: bool,
70}
71
72#[derive(Debug, Clone, Copy)]
73struct QuantMatrices {
74    y1: [u16; 2],
75    y2: [u16; 2],
76    uv: [u16; 2],
77}
78
79#[derive(Debug, Clone, Copy)]
80struct RdMultipliers {
81    i16: u32,
82    i4: u32,
83    uv: u32,
84    mode: u32,
85}
86
87#[derive(Debug, Clone)]
88struct Planes {
89    y_stride: usize,
90    uv_stride: usize,
91    y: Vec<u8>,
92    u: Vec<u8>,
93    v: Vec<u8>,
94}
95
96#[derive(Debug, Clone)]
97struct SegmentConfig {
98    use_segment: bool,
99    update_map: bool,
100    quantizer: [u8; NUM_MB_SEGMENTS],
101    filter_strength: [i8; NUM_MB_SEGMENTS],
102    probs: [u8; MB_FEATURE_TREE_PROBS],
103    segments: Vec<u8>,
104}
105
106#[derive(Debug, Clone, Copy)]
107struct FilterConfig {
108    simple: bool,
109    level: u8,
110    sharpness: u8,
111}
112
113#[derive(Debug, Clone)]
114struct EncodedLossyCandidate {
115    base_quant: u8,
116    segment: SegmentConfig,
117    probabilities: CoeffProbTables,
118    modes: Vec<MacroblockMode>,
119    token_partition: Vec<u8>,
120}
121
122#[derive(Debug, Clone, Copy)]
123struct LossySearchProfile {
124    fast_mode_search: bool,
125    allow_i4x4: bool,
126    refine_i16: bool,
127    refine_i4_search: bool,
128    refine_i4_final: bool,
129    refine_chroma: bool,
130    refine_y2: bool,
131    update_probabilities: bool,
132}
133
134/// Validates rgba.
135fn validate_rgba(width: usize, height: usize, rgba: &[u8]) -> Result<(), EncoderError> {
136    if width == 0 || height == 0 {
137        return Err(EncoderError::InvalidParam(
138            "image dimensions must be non-zero",
139        ));
140    }
141    if width > MAX_WEBP_DIMENSION || height > MAX_WEBP_DIMENSION {
142        return Err(EncoderError::InvalidParam(
143            "image dimensions exceed VP8 limits",
144        ));
145    }
146    let expected_len = width
147        .checked_mul(height)
148        .and_then(|pixels| pixels.checked_mul(4))
149        .ok_or(EncoderError::InvalidParam("image dimensions overflow"))?;
150    if rgba.len() != expected_len {
151        return Err(EncoderError::InvalidParam(
152            "RGBA buffer length does not match dimensions",
153        ));
154    }
155    if rgba.chunks_exact(4).any(|pixel| pixel[3] != 0xff) {
156        return Err(EncoderError::InvalidParam(
157            "lossy encoder does not support alpha yet",
158        ));
159    }
160    Ok(())
161}
162
163/// Validates options.
164fn validate_options(options: &LossyEncodingOptions) -> Result<(), EncoderError> {
165    if options.quality > 100 {
166        return Err(EncoderError::InvalidParam(
167            "lossy quality must be in 0..=100",
168        ));
169    }
170    if options.optimization_level > MAX_LOSSY_OPTIMIZATION_LEVEL {
171        return Err(EncoderError::InvalidParam(
172            "lossy optimization level must be in 0..=9",
173        ));
174    }
175    Ok(())
176}
177
178/// Converts a user quality value into the base VP8 quantizer.
179fn base_quantizer_from_quality(quality: u8) -> i32 {
180    (((100 - quality as i32) * 127) + 50) / 100
181}
182
183/// Builds quant matrices.
184fn build_quant_matrices(base_q: i32) -> QuantMatrices {
185    let q = base_q.clamp(0, 127) as usize;
186    QuantMatrices {
187        y1: [DC_TABLE[q] as u16, AC_TABLE[q]],
188        y2: [
189            (DC_TABLE[q] as u16) * 2,
190            ((AC_TABLE[q] as u32 * 101_581) >> 16).max(8) as u16,
191        ],
192        uv: [DC_TABLE[q.min(117)] as u16, AC_TABLE[q]],
193    }
194}
195
196/// Builds rd multipliers.
197fn build_rd_multipliers(quant: &QuantMatrices) -> RdMultipliers {
198    let q_i4 = u32::from(quant.y1[1].max(8));
199    let q_i16 = u32::from(quant.y2[1].max(8));
200    let q_uv = u32::from(quant.uv[1].max(8));
201    RdMultipliers {
202        i16: ((3 * q_i16 * q_i16).max(128)) >> 0,
203        i4: ((3 * q_i4 * q_i4).max(128)) >> 7,
204        uv: ((3 * q_uv * q_uv).max(128)) >> 6,
205        mode: (q_i4 * q_i4).max(128) >> 7,
206    }
207}
208
209/// Internal helper for clipped quantizer.
210fn clipped_quantizer(value: i32) -> u8 {
211    value.clamp(0, 127) as u8
212}
213
214/// Internal helper for filter candidates.
215fn filter_candidates(base_quant: i32) -> Vec<FilterConfig> {
216    let mut levels = vec![
217        0u8,
218        clipped_quantizer((base_quant + 1) / 2).min(63),
219        clipped_quantizer(base_quant).min(63),
220        clipped_quantizer((base_quant * 3 + 1) / 2).min(63),
221        clipped_quantizer(base_quant * 2).min(63),
222    ];
223    levels.sort_unstable();
224    levels.dedup();
225    levels
226        .into_iter()
227        .map(|level| FilterConfig {
228            simple: false,
229            level,
230            sharpness: 0,
231        })
232        .collect()
233}
234
235/// Internal helper for heuristic filter.
236fn heuristic_filter(base_quant: i32) -> FilterConfig {
237    let level = if base_quant <= 10 {
238        0
239    } else {
240        clipped_quantizer((base_quant * 3 + 2) / 4).min(63)
241    };
242    FilterConfig {
243        simple: false,
244        level,
245        sharpness: 0,
246    }
247}
248
249/// Builds the lossy search profile for a given optimization level.
250fn lossy_search_profile(optimization_level: u8) -> LossySearchProfile {
251    match optimization_level {
252        0 => LossySearchProfile {
253            fast_mode_search: true,
254            allow_i4x4: false,
255            refine_i16: false,
256            refine_i4_search: false,
257            refine_i4_final: false,
258            refine_chroma: false,
259            refine_y2: false,
260            update_probabilities: false,
261        },
262        1 | 2 => LossySearchProfile {
263            fast_mode_search: false,
264            allow_i4x4: false,
265            refine_i16: false,
266            refine_i4_search: false,
267            refine_i4_final: false,
268            refine_chroma: false,
269            refine_y2: false,
270            update_probabilities: true,
271        },
272        3 | 4 => LossySearchProfile {
273            fast_mode_search: false,
274            allow_i4x4: true,
275            refine_i16: false,
276            refine_i4_search: false,
277            refine_i4_final: false,
278            refine_chroma: false,
279            refine_y2: false,
280            update_probabilities: true,
281        },
282        5 => LossySearchProfile {
283            fast_mode_search: false,
284            allow_i4x4: true,
285            refine_i16: false,
286            refine_i4_search: false,
287            refine_i4_final: false,
288            refine_chroma: true,
289            refine_y2: false,
290            update_probabilities: true,
291        },
292        6 => LossySearchProfile {
293            fast_mode_search: false,
294            allow_i4x4: true,
295            refine_i16: true,
296            refine_i4_search: false,
297            refine_i4_final: true,
298            refine_chroma: true,
299            refine_y2: false,
300            update_probabilities: true,
301        },
302        7 => LossySearchProfile {
303            fast_mode_search: false,
304            allow_i4x4: true,
305            refine_i16: true,
306            refine_i4_search: true,
307            refine_i4_final: true,
308            refine_chroma: true,
309            refine_y2: false,
310            update_probabilities: true,
311        },
312        _ => LossySearchProfile {
313            fast_mode_search: false,
314            allow_i4x4: true,
315            refine_i16: true,
316            refine_i4_search: true,
317            refine_i4_final: true,
318            refine_chroma: true,
319            refine_y2: true,
320            update_probabilities: true,
321        },
322    }
323}
324
325/// Returns whether the current lossy effort level should exhaustively search segments.
326fn use_exhaustive_segment_search(optimization_level: u8) -> bool {
327    optimization_level >= 9
328}
329
330/// Returns whether the current lossy effort level should exhaustively search loop filters.
331fn use_exhaustive_filter_search(optimization_level: u8, mb_count: usize) -> bool {
332    if optimization_level >= 9 {
333        return true;
334    }
335    if optimization_level >= 6 {
336        return mb_count < 2_048;
337    }
338    mb_count < 1_024
339}
340
341/// Internal helper for segment with uniform filter.
342fn segment_with_uniform_filter(segment: &SegmentConfig, level: u8) -> SegmentConfig {
343    let mut filtered = segment.clone();
344    if filtered.use_segment {
345        filtered.filter_strength[..].fill(level as i8);
346    }
347    filtered
348}
349
350/// Looks up a probability from a pair of neighboring context flags.
351fn get_proba(a: usize, b: usize) -> u8 {
352    let total = a + b;
353    if total == 0 {
354        255
355    } else {
356        ((255 * a + total / 2) / total) as u8
357    }
358}
359
360/// Builds segment quantizers.
361fn build_segment_quantizers(segment: &SegmentConfig) -> [QuantMatrices; NUM_MB_SEGMENTS] {
362    std::array::from_fn(|index| build_quant_matrices(segment.quantizer[index] as i32))
363}
364
365/// Internal helper for disabled segment config.
366fn disabled_segment_config(mb_count: usize, base_quant: u8) -> SegmentConfig {
367    SegmentConfig {
368        use_segment: false,
369        update_map: false,
370        quantizer: [base_quant; NUM_MB_SEGMENTS],
371        filter_strength: [0; NUM_MB_SEGMENTS],
372        probs: [255; MB_FEATURE_TREE_PROBS],
373        segments: vec![0; mb_count],
374    }
375}
376
377/// Internal helper for rgb to y.
378fn rgb_to_y(r: u8, g: u8, b: u8) -> u8 {
379    let luma = 16_839 * r as i32 + 33_059 * g as i32 + 6_420 * b as i32;
380    ((luma + YUV_HALF + (16 << YUV_FIX)) >> YUV_FIX) as u8
381}
382
383/// Clamps uv.
384fn clip_uv(value: i32, rounding: i32) -> u8 {
385    let uv = (value + rounding + (128 << (YUV_FIX + 2))) >> (YUV_FIX + 2);
386    uv.clamp(0, 255) as u8
387}
388
389/// Internal helper for rgb to u.
390fn rgb_to_u(r: i32, g: i32, b: i32) -> u8 {
391    clip_uv(-9_719 * r - 19_081 * g + 28_800 * b, YUV_HALF << 2)
392}
393
394/// Internal helper for rgb to v.
395fn rgb_to_v(r: i32, g: i32, b: i32) -> u8 {
396    clip_uv(28_800 * r - 24_116 * g - 4_684 * b, YUV_HALF << 2)
397}
398
399/// Internal helper for rgba to yuv420.
400fn rgba_to_yuv420(
401    width: usize,
402    height: usize,
403    rgba: &[u8],
404    mb_width: usize,
405    mb_height: usize,
406) -> Planes {
407    let y_stride = mb_width * 16;
408    let uv_stride = mb_width * 8;
409    let y_height = mb_height * 16;
410    let uv_height = mb_height * 8;
411    let mut y = vec![0u8; y_stride * y_height];
412    let mut u = vec![0u8; uv_stride * uv_height];
413    let mut v = vec![0u8; uv_stride * uv_height];
414
415    for py in 0..y_height {
416        let src_y = py.min(height - 1);
417        for px in 0..y_stride {
418            let src_x = px.min(width - 1);
419            let offset = (src_y * width + src_x) * 4;
420            y[py * y_stride + px] = rgb_to_y(rgba[offset], rgba[offset + 1], rgba[offset + 2]);
421        }
422    }
423
424    for py in 0..uv_height {
425        for px in 0..uv_stride {
426            let mut sum_r = 0i32;
427            let mut sum_g = 0i32;
428            let mut sum_b = 0i32;
429            for dy in 0..2 {
430                let src_y = (py * 2 + dy).min(height - 1);
431                for dx in 0..2 {
432                    let src_x = (px * 2 + dx).min(width - 1);
433                    let offset = (src_y * width + src_x) * 4;
434                    sum_r += rgba[offset] as i32;
435                    sum_g += rgba[offset + 1] as i32;
436                    sum_b += rgba[offset + 2] as i32;
437                }
438            }
439            u[py * uv_stride + px] = rgb_to_u(sum_r, sum_g, sum_b);
440            v[py * uv_stride + px] = rgb_to_v(sum_r, sum_g, sum_b);
441        }
442    }
443
444    Planes {
445        y_stride,
446        uv_stride,
447        y,
448        u,
449        v,
450    }
451}
452
453/// Internal helper for empty reconstructed planes.
454fn empty_reconstructed_planes(mb_width: usize, mb_height: usize) -> Planes {
455    let y_stride = mb_width * 16;
456    let uv_stride = mb_width * 8;
457    let y_height = mb_height * 16;
458    let uv_height = mb_height * 8;
459    Planes {
460        y_stride,
461        uv_stride,
462        y: vec![0; y_stride * y_height],
463        u: vec![0; uv_stride * uv_height],
464        v: vec![0; uv_stride * uv_height],
465    }
466}
467
468/// Internal helper for macroblock activity.
469fn macroblock_activity(source: &Planes, mb_x: usize, mb_y: usize) -> u32 {
470    let x0 = mb_x * 16;
471    let y0 = mb_y * 16;
472    let mut activity = 0u32;
473
474    for row in 0..16 {
475        let row_offset = (y0 + row) * source.y_stride + x0;
476        let pixels = &source.y[row_offset..row_offset + 16];
477        for col in 1..16 {
478            activity += pixels[col].abs_diff(pixels[col - 1]) as u32;
479        }
480        if row > 0 {
481            let prev_offset = (y0 + row - 1) * source.y_stride + x0;
482            let prev = &source.y[prev_offset..prev_offset + 16];
483            for col in 0..16 {
484                activity += pixels[col].abs_diff(prev[col]) as u32;
485            }
486        }
487    }
488
489    activity
490}
491
492/// Builds segment probs.
493fn build_segment_probs(counts: &[usize; NUM_MB_SEGMENTS]) -> [u8; MB_FEATURE_TREE_PROBS] {
494    [
495        get_proba(counts[0] + counts[1], counts[2] + counts[3]),
496        get_proba(counts[0], counts[1]),
497        get_proba(counts[2], counts[3]),
498    ]
499}
500
501/// Builds segment config.
502fn build_segment_config(
503    activities: &[u32],
504    sorted_activities: &[u32],
505    flat_percent: usize,
506    flat_delta: i32,
507    detail_delta: i32,
508    base_quant: i32,
509) -> Option<SegmentConfig> {
510    if activities.len() < 8 {
511        return None;
512    }
513    let flat_count = (activities.len() * flat_percent / 100).clamp(1, activities.len() - 1);
514    let threshold = sorted_activities[flat_count - 1];
515
516    let mut segments = vec![0u8; activities.len()];
517    let mut counts = [0usize; NUM_MB_SEGMENTS];
518    for (index, &activity) in activities.iter().enumerate() {
519        let segment = if activity <= threshold { 0 } else { 1 };
520        segments[index] = segment;
521        counts[segment as usize] += 1;
522    }
523    if counts[0] == 0 || counts[1] == 0 {
524        return None;
525    }
526
527    let quant0 = clipped_quantizer(base_quant + flat_delta);
528    let quant1 = clipped_quantizer(base_quant + detail_delta);
529    if quant0 == quant1 {
530        return None;
531    }
532
533    let probs = build_segment_probs(&counts);
534    let update_map = probs.iter().any(|&prob| prob != 255);
535    if !update_map {
536        return None;
537    }
538
539    let mut quantizer = [quant0; NUM_MB_SEGMENTS];
540    quantizer[1] = quant1;
541    Some(SegmentConfig {
542        use_segment: true,
543        update_map,
544        quantizer,
545        filter_strength: [0; NUM_MB_SEGMENTS],
546        probs,
547        segments,
548    })
549}
550
551/// Builds multi segment config.
552fn build_multi_segment_config(
553    activities: &[u32],
554    sorted_activities: &[u32],
555    percentiles: &[usize],
556    deltas: &[i32],
557    base_quant: i32,
558) -> Option<SegmentConfig> {
559    let segment_count = deltas.len();
560    if !(2..=NUM_MB_SEGMENTS).contains(&segment_count) || percentiles.len() + 1 != segment_count {
561        return None;
562    }
563
564    let mut thresholds = Vec::with_capacity(percentiles.len());
565    for &percentile in percentiles {
566        let split = (activities.len() * percentile / 100).clamp(1, activities.len() - 1);
567        thresholds.push(sorted_activities[split - 1]);
568    }
569    thresholds.sort_unstable();
570
571    let mut segments = vec![0u8; activities.len()];
572    let mut counts = [0usize; NUM_MB_SEGMENTS];
573    for (index, &activity) in activities.iter().enumerate() {
574        let segment = thresholds.partition_point(|&threshold| activity > threshold);
575        segments[index] = segment as u8;
576        counts[segment] += 1;
577    }
578
579    if counts[..segment_count].iter().any(|&count| count == 0) {
580        return None;
581    }
582
583    let mut quantizer = [clipped_quantizer(base_quant); NUM_MB_SEGMENTS];
584    let mut distinct = false;
585    for (index, &delta) in deltas.iter().enumerate() {
586        quantizer[index] = clipped_quantizer(base_quant + delta);
587        if index > 0 && quantizer[index] != quantizer[index - 1] {
588            distinct = true;
589        }
590    }
591    if !distinct {
592        return None;
593    }
594
595    let probs = build_segment_probs(&counts);
596    let update_map = probs.iter().any(|&prob| prob != 255);
597    if !update_map {
598        return None;
599    }
600
601    Some(SegmentConfig {
602        use_segment: true,
603        update_map,
604        quantizer,
605        filter_strength: [0; NUM_MB_SEGMENTS],
606        probs,
607        segments,
608    })
609}
610
611/// Builds segment candidates.
612fn build_segment_candidates(
613    source: &Planes,
614    mb_width: usize,
615    mb_height: usize,
616    base_quant: i32,
617    optimization_level: u8,
618) -> Vec<SegmentConfig> {
619    let mb_count = mb_width * mb_height;
620    let mut candidates = vec![disabled_segment_config(
621        mb_count,
622        clipped_quantizer(base_quant),
623    )];
624    if mb_count < 8 || optimization_level == 0 {
625        return candidates;
626    }
627
628    let mut activities = Vec::with_capacity(mb_count);
629    for mb_y in 0..mb_height {
630        for mb_x in 0..mb_width {
631            activities.push(macroblock_activity(source, mb_x, mb_y));
632        }
633    }
634    let mut sorted = activities.clone();
635    sorted.sort_unstable();
636
637    if !use_exhaustive_segment_search(optimization_level) && mb_count >= 1_024 {
638        if let Some(config) = build_segment_config(&activities, &sorted, 65, 12, -2, base_quant) {
639            return vec![config];
640        }
641        return candidates;
642    }
643
644    let two_segment_presets: &[(usize, i32, i32)] = if optimization_level <= 2 {
645        &[(65usize, 12i32, -2i32)]
646    } else if mb_count >= 2_048 && !use_exhaustive_segment_search(optimization_level) {
647        &[(65usize, 12i32, -2i32), (55, 10, 0)]
648    } else {
649        &[(55usize, 10i32, 0i32), (65, 12, -2), (45, 8, 0)]
650    };
651    for &(flat_percent, flat_delta, detail_delta) in two_segment_presets {
652        if let Some(config) = build_segment_config(
653            &activities,
654            &sorted,
655            flat_percent,
656            flat_delta,
657            detail_delta,
658            base_quant,
659        ) {
660            candidates.push(config);
661        }
662    }
663
664    if optimization_level >= 4
665        && (use_exhaustive_segment_search(optimization_level) || mb_count < 2_048)
666    {
667        for (percentiles, deltas) in [
668            (&[35usize, 72usize][..], &[12i32, 4i32, -4i32][..]),
669            (
670                &[25usize, 50usize, 78usize][..],
671                &[16i32, 8i32, 1i32, -7i32][..],
672            ),
673            (
674                &[30usize, 58usize, 84usize][..],
675                &[18i32, 10i32, 2i32, -8i32][..],
676            ),
677        ] {
678            if let Some(config) =
679                build_multi_segment_config(&activities, &sorted, percentiles, deltas, base_quant)
680            {
681                candidates.push(config);
682            }
683        }
684    }
685
686    candidates
687}
688
689mod api;
690mod bitstream;
691mod predict;
692
693pub use api::*;