1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
170pub struct LosslessEncodingOptions {
171 pub optimization_level: u8,
178}
179
180impl Default for LosslessEncodingOptions {
181 fn default() -> Self {
183 Self {
184 optimization_level: DEFAULT_OPTIMIZATION_LEVEL,
185 }
186 }
187}
188
189impl ColorCache {
190 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 fn key(&self, argb: u32) -> usize {
204 ((argb.wrapping_mul(COLOR_CACHE_HASH_MUL)) >> self.hash_shift) as usize
205 }
206
207 fn lookup(&self, argb: u32) -> Option<usize> {
209 let key = self.key(argb);
210 (self.colors[key] == argb).then_some(key)
211 }
212
213 fn insert(&mut self, argb: u32) {
215 let key = self.key(argb);
216 self.colors[key] = argb;
217 }
218}
219
220fn 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
246fn 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
256fn 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
342fn 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
351fn rgba_has_alpha(rgba: &[u8]) -> bool {
353 rgba.chunks_exact(4).any(|pixel| pixel[3] != 0xff)
354}
355
356fn 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::*;