Skip to main content

webp_rust/encoder/lossless/
mod.rs

1//! Shared state and search profiles for the lossless `VP8L` encoder.
2
3use std::cmp::Reverse;
4use std::collections::{BinaryHeap, HashMap, HashSet};
5
6use crate::encoder::bit_writer::BitWriter;
7use crate::encoder::container::{wrap_still_webp, StillImageChunk};
8use crate::encoder::huffman::{compress_huffman_tree, HuffmanCode};
9use crate::encoder::EncoderError;
10use crate::ImageBuffer;
11
12const MAX_WEBP_DIMENSION: usize = 1 << 14;
13const MAX_CACHE_BITS: usize = 11;
14const MIN_LENGTH: usize = 4;
15const MAX_LENGTH: usize = 4096;
16const MIN_TRANSFORM_BITS: usize = 2;
17const GLOBAL_CROSS_COLOR_TRANSFORM_BITS: usize = 9;
18const GLOBAL_PREDICTOR_TRANSFORM_BITS: usize = 9;
19const GLOBAL_PREDICTOR_MODE: u8 = 11;
20const CROSS_COLOR_TRANSFORM_BITS: usize = 5;
21const PREDICTOR_TRANSFORM_BITS: usize = 5;
22const MAX_OPTIMIZATION_LEVEL: u8 = 9;
23const DEFAULT_OPTIMIZATION_LEVEL: u8 = 6;
24const NUM_PREDICTOR_MODES: u8 = 14;
25const NUM_LITERAL_CODES: usize = 256;
26const NUM_LENGTH_CODES: usize = 24;
27const NUM_DISTANCE_CODES: usize = 40;
28const NUM_CODE_LENGTH_CODES: usize = 19;
29const NUM_HISTOGRAM_PARTITIONS: usize = 4;
30const MIN_HUFFMAN_BITS: usize = 2;
31const NUM_HUFFMAN_BITS: usize = 3;
32const COLOR_CACHE_HASH_MUL: u32 = 0x1e35_a7bd;
33const MATCH_HASH_BITS: usize = 15;
34const MATCH_HASH_SIZE: usize = 1 << MATCH_HASH_BITS;
35const MATCH_CHAIN_DEPTH_LEVEL1: usize = 4;
36const MATCH_CHAIN_DEPTH_LEVEL2: usize = 8;
37const MATCH_CHAIN_DEPTH_LEVEL3: usize = 16;
38const MATCH_CHAIN_DEPTH_LEVEL4: usize = 32;
39const MATCH_CHAIN_DEPTH_LEVEL5: usize = 64;
40const MATCH_CHAIN_DEPTH_LEVEL6: usize = 128;
41const MATCH_CHAIN_DEPTH_LEVEL7: usize = 192;
42const MAX_FALLBACK_DISTANCE: usize = (1 << 20) - 120;
43const APPROX_LITERAL_COST_BITS: isize = 32;
44const APPROX_CACHE_COST_BITS: isize = 8;
45const APPROX_COPY_LENGTH_SYMBOL_BITS: isize = 8;
46const APPROX_COPY_DISTANCE_SYMBOL_BITS: isize = 8;
47const CODE_LENGTH_CODE_ORDER: [usize; NUM_CODE_LENGTH_CODES] = [
48    17, 18, 0, 1, 2, 3, 4, 5, 16, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
49];
50const PLANE_TO_CODE_LUT: [u8; 128] = [
51    96, 73, 55, 39, 23, 13, 5, 1, 255, 255, 255, 255, 255, 255, 255, 255, 101, 78, 58, 42, 26, 16,
52    8, 2, 0, 3, 9, 17, 27, 43, 59, 79, 102, 86, 62, 46, 32, 20, 10, 6, 4, 7, 11, 21, 33, 47, 63,
53    87, 105, 90, 70, 52, 37, 28, 18, 14, 12, 15, 19, 29, 38, 53, 71, 91, 110, 99, 82, 66, 48, 35,
54    30, 24, 22, 25, 31, 36, 49, 67, 83, 100, 115, 108, 94, 76, 64, 50, 44, 40, 34, 41, 45, 51, 65,
55    77, 95, 109, 118, 113, 103, 92, 80, 68, 60, 56, 54, 57, 61, 69, 81, 93, 104, 114, 119, 116,
56    111, 106, 97, 88, 84, 74, 72, 75, 85, 89, 98, 107, 112, 117,
57];
58
59#[derive(Debug, Clone, Copy)]
60enum Token {
61    Literal(u32),
62    Cache(usize),
63    Copy { distance: usize, length: usize },
64}
65
66#[derive(Debug, Clone, Copy)]
67struct PrefixCode {
68    symbol: usize,
69    extra_bits: usize,
70    extra_value: usize,
71}
72
73#[derive(Debug, Clone, Copy)]
74struct CrossColorTransform {
75    green_to_red: i8,
76    green_to_blue: i8,
77    red_to_blue: i8,
78}
79
80#[derive(Debug, Clone)]
81struct ColorCache {
82    colors: Vec<u32>,
83    hash_shift: u32,
84}
85
86#[derive(Debug, Clone)]
87struct TransformPlan {
88    use_subtract_green: bool,
89    cross_bits: Option<usize>,
90    cross_width: usize,
91    cross_image: Vec<u32>,
92    predictor_bits: Option<usize>,
93    predictor_width: usize,
94    predictor_image: Vec<u32>,
95    predicted: Vec<u32>,
96}
97
98#[derive(Debug, Clone)]
99struct PaletteCandidate {
100    palette: Vec<u32>,
101    packed_width: usize,
102    packed_indices: Vec<u32>,
103}
104
105#[derive(Debug, Clone, Copy)]
106struct TokenBuildOptions {
107    color_cache_bits: usize,
108    match_chain_depth: usize,
109    use_window_offsets: bool,
110    window_offset_limit: usize,
111    lazy_matching: bool,
112    use_traceback: bool,
113    traceback_max_candidates: usize,
114}
115
116#[derive(Debug, Clone, Copy)]
117enum TracebackStep {
118    Literal,
119    Cache { key: usize },
120    Copy { distance: usize, length: usize },
121}
122
123#[derive(Debug, Clone)]
124struct TracebackCostModel {
125    literal: Vec<usize>,
126    red: Vec<usize>,
127    blue: Vec<usize>,
128    alpha: Vec<usize>,
129    distance: Vec<usize>,
130    length_cost_intervals: Vec<(usize, usize, usize)>,
131}
132
133type HistogramSet = [Vec<u32>; 5];
134
135#[derive(Debug, Clone)]
136struct HuffmanGroupCodes {
137    green: HuffmanCode,
138    red: HuffmanCode,
139    blue: HuffmanCode,
140    alpha: HuffmanCode,
141    dist: HuffmanCode,
142}
143
144#[derive(Debug, Clone)]
145struct MetaHuffmanPlan {
146    huffman_bits: usize,
147    huffman_xsize: usize,
148    assignments: Vec<usize>,
149    groups: Vec<HuffmanGroupCodes>,
150}
151
152#[derive(Debug, Clone)]
153struct HistogramCandidate {
154    histograms: HistogramSet,
155    weight: usize,
156}
157
158#[derive(Debug, Clone, Copy)]
159struct LosslessSearchProfile {
160    transform_search_level: u8,
161    match_search_level: u8,
162    entropy_search_level: u8,
163    use_color_cache: bool,
164    shortlist_keep: usize,
165    early_stop_ratio_percent: usize,
166}
167
168/// Lossless encoder tuning knobs.
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
170pub struct LosslessEncodingOptions {
171    /// Compression effort from `0` to `9`.
172    ///
173    /// - `0`: fastest path, raw-only search
174    /// - `1..=3`: fast presets that should still beat PNG on typical images
175    /// - `4..=6`: balanced presets
176    /// - `7..=9`: increasingly heavy search, with `9` enabling the slowest trials
177    pub optimization_level: u8,
178}
179
180impl Default for LosslessEncodingOptions {
181    /// Returns the balanced default lossless settings.
182    fn default() -> Self {
183        Self {
184            optimization_level: DEFAULT_OPTIMIZATION_LEVEL,
185        }
186    }
187}
188
189impl ColorCache {
190    /// Allocates a lossless color cache with the requested hash width.
191    fn new(hash_bits: usize) -> Result<Self, EncoderError> {
192        if !(1..=MAX_CACHE_BITS).contains(&hash_bits) {
193            return Err(EncoderError::InvalidParam("invalid VP8L color cache size"));
194        }
195        let size = 1usize << hash_bits;
196        Ok(Self {
197            colors: vec![0; size],
198            hash_shift: (32 - hash_bits) as u32,
199        })
200    }
201
202    /// Computes the cache slot for a packed ARGB pixel.
203    fn key(&self, argb: u32) -> usize {
204        ((argb.wrapping_mul(COLOR_CACHE_HASH_MUL)) >> self.hash_shift) as usize
205    }
206
207    /// Returns the cache key when the pixel is already present.
208    fn lookup(&self, argb: u32) -> Option<usize> {
209        let key = self.key(argb);
210        (self.colors[key] == argb).then_some(key)
211    }
212
213    /// Inserts or replaces one packed ARGB pixel in the cache.
214    fn insert(&mut self, argb: u32) {
215        let key = self.key(argb);
216        self.colors[key] = argb;
217    }
218}
219
220/// Validates rgba.
221fn validate_rgba(width: usize, height: usize, rgba: &[u8]) -> Result<(), EncoderError> {
222    if width == 0 || height == 0 {
223        return Err(EncoderError::InvalidParam(
224            "image dimensions must be non-zero",
225        ));
226    }
227    if width > MAX_WEBP_DIMENSION || height > MAX_WEBP_DIMENSION {
228        return Err(EncoderError::InvalidParam(
229            "image dimensions exceed VP8L limits",
230        ));
231    }
232
233    let expected_len = width
234        .checked_mul(height)
235        .and_then(|pixels| pixels.checked_mul(4))
236        .ok_or(EncoderError::InvalidParam("image dimensions overflow"))?;
237    if rgba.len() != expected_len {
238        return Err(EncoderError::InvalidParam(
239            "RGBA buffer length does not match dimensions",
240        ));
241    }
242
243    Ok(())
244}
245
246/// Validates options.
247fn validate_options(options: &LosslessEncodingOptions) -> Result<(), EncoderError> {
248    if options.optimization_level > MAX_OPTIMIZATION_LEVEL {
249        return Err(EncoderError::InvalidParam(
250            "lossless optimization level must be in 0..=9",
251        ));
252    }
253    Ok(())
254}
255
256/// Builds the lossless search profile for a given optimization level.
257fn lossless_search_profile(optimization_level: u8) -> LosslessSearchProfile {
258    match optimization_level {
259        0 => LosslessSearchProfile {
260            transform_search_level: 0,
261            match_search_level: 0,
262            entropy_search_level: 0,
263            use_color_cache: false,
264            shortlist_keep: 1,
265            early_stop_ratio_percent: 100,
266        },
267        1 => LosslessSearchProfile {
268            transform_search_level: 1,
269            match_search_level: 1,
270            entropy_search_level: 0,
271            use_color_cache: false,
272            shortlist_keep: 2,
273            early_stop_ratio_percent: 104,
274        },
275        2 => LosslessSearchProfile {
276            transform_search_level: 2,
277            match_search_level: 2,
278            entropy_search_level: 1,
279            use_color_cache: true,
280            shortlist_keep: 2,
281            early_stop_ratio_percent: 106,
282        },
283        3 => LosslessSearchProfile {
284            transform_search_level: 3,
285            match_search_level: 2,
286            entropy_search_level: 1,
287            use_color_cache: true,
288            shortlist_keep: 3,
289            early_stop_ratio_percent: 108,
290        },
291        4 => LosslessSearchProfile {
292            transform_search_level: 4,
293            match_search_level: 3,
294            entropy_search_level: 2,
295            use_color_cache: true,
296            shortlist_keep: 3,
297            early_stop_ratio_percent: 110,
298        },
299        5 => LosslessSearchProfile {
300            transform_search_level: 5,
301            match_search_level: 4,
302            entropy_search_level: 2,
303            use_color_cache: true,
304            shortlist_keep: 4,
305            early_stop_ratio_percent: 112,
306        },
307        6 => LosslessSearchProfile {
308            transform_search_level: 6,
309            match_search_level: 4,
310            entropy_search_level: 3,
311            use_color_cache: true,
312            shortlist_keep: 4,
313            early_stop_ratio_percent: 115,
314        },
315        7 => LosslessSearchProfile {
316            transform_search_level: 7,
317            match_search_level: 5,
318            entropy_search_level: 4,
319            use_color_cache: true,
320            shortlist_keep: 5,
321            early_stop_ratio_percent: 118,
322        },
323        8 => LosslessSearchProfile {
324            transform_search_level: 7,
325            match_search_level: 6,
326            entropy_search_level: 5,
327            use_color_cache: true,
328            shortlist_keep: 6,
329            early_stop_ratio_percent: 122,
330        },
331        _ => LosslessSearchProfile {
332            transform_search_level: 7,
333            match_search_level: 7,
334            entropy_search_level: 6,
335            use_color_cache: true,
336            shortlist_keep: 8,
337            early_stop_ratio_percent: 128,
338        },
339    }
340}
341
342/// Expands a lossless optimization level into candidate search profiles.
343fn lossless_candidate_profiles(optimization_level: u8) -> Vec<LosslessSearchProfile> {
344    match optimization_level {
345        8 => vec![lossless_search_profile(7)],
346        9 => vec![lossless_search_profile(7)],
347        _ => vec![lossless_search_profile(optimization_level)],
348    }
349}
350
351/// Returns whether the RGBA input contains any non-opaque pixels.
352fn rgba_has_alpha(rgba: &[u8]) -> bool {
353    rgba.chunks_exact(4).any(|pixel| pixel[3] != 0xff)
354}
355
356/// Reorders RGBA bytes into packed ARGB pixels.
357fn rgba_to_argb(rgba: &[u8]) -> Vec<u32> {
358    rgba.chunks_exact(4)
359        .map(|pixel| {
360            ((pixel[3] as u32) << 24)
361                | ((pixel[0] as u32) << 16)
362                | ((pixel[1] as u32) << 8)
363                | pixel[2] as u32
364        })
365        .collect()
366}
367
368mod api;
369mod entropy;
370mod plans;
371mod tokens;
372
373pub use api::*;