#![allow(
clippy::cast_possible_truncation,
reason = "channel indices are masked to 8 bits, prefix symbols index alphabets \
bounded by the format, and pixel counts fit the validated 14-bit VP8L \
dimensions, so every narrowing cast here is value-preserving"
)]
use crate::lossless::bit_io::writer::BitWriter;
use crate::lossless::color_cache::ColorCache;
use crate::lossless::constants::{
ALPHABET_SIZE, COLOR_INDEXING_TRANSFORM, CROSS_COLOR_TRANSFORM, MAX_CACHE_BITS,
MIN_TRANSFORM_BITS, NUM_LENGTH_CODES, NUM_LITERAL_CODES, NUM_TRANSFORM_BITS,
PREDICTOR_TRANSFORM, SUBTRACT_GREEN_TRANSFORM, VP8L_IMAGE_SIZE_BITS, VP8L_MAGIC_BYTE,
VP8L_VERSION_BITS, subsample_size,
};
use crate::lossless::histogram::Histogram;
use crate::lossless::huffman::build::build_code_lengths;
use crate::lossless::huffman::canonical::emit_codes;
use crate::lossless::huffman::serialize::write_huffman_code;
use crate::lossless::lz77::{PlaneCodeMap, prefix_encode};
use crate::lossless::prelude::*;
use crate::lossless::transform::{cross_color, palette, predictor, subtract_green};
use crate::lossless::vp8l::backref::{Resolved, Token, parse, parse_lz77, parse_optimal, resolve};
use crate::lossless::vp8l::meta::{self, MetaPlan};
use crate::lossless::work::work;
const MAX_MAIN_CODE_LENGTH: u32 = 15;
const _: () = assert!(crate::lossless::constants::MAX_ALLOWED_CODE_LENGTH == 15);
const TRANSFORM_TYPE_BITS: u32 = 2;
const CACHE_BITS_FIELD_WIDTH: u32 = 4;
const NUM_COLORS_FIELD_WIDTH: u32 = 8;
enum TransformPlan {
Predictor { bits: u32, tile_data: Vec<u32> },
CrossColor { bits: u32, tile_data: Vec<u32> },
SubtractGreen,
ColorIndexing { num_colors: u32, colormap: Vec<u32> },
}
#[must_use]
pub(crate) fn encode(width: u32, height: u32, argb: &[u32]) -> Vec<u8> {
encode_with(width, height, argb, true)
}
#[must_use]
pub(crate) fn encode_with(width: u32, height: u32, argb: &[u32], use_lz77: bool) -> Vec<u8> {
let alpha_used = argb.iter().any(|&p| p >> 24 != 0xff);
let tier0 = encode_stream(width, height, alpha_used, argb, false, use_lz77);
let tier1 = encode_stream(width, height, alpha_used, argb, true, use_lz77);
if tier1.len() < tier0.len() {
tier1
} else {
tier0
}
}
const VP8L_HEADER_BYTES: usize = 5;
const _: () = assert!(
8 + 2 * VP8L_IMAGE_SIZE_BITS as usize + 1 + VP8L_VERSION_BITS as usize == VP8L_HEADER_BYTES * 8
);
#[must_use]
pub(crate) fn encode_alpha_stream(alpha: &[u8], width: u32, height: u32) -> Vec<u8> {
let argb: Vec<u32> = alpha.iter().map(|&a| u32::from(a) << 8).collect();
let mut full = encode(width, height, &argb);
full.split_off(VP8L_HEADER_BYTES)
}
fn encode_stream(
width: u32,
height: u32,
alpha_used: bool,
argb: &[u32],
use_subtract_green: bool,
use_lz77: bool,
) -> Vec<u8> {
let owned;
let pixels: &[u32] = if use_subtract_green {
let mut copy = argb.to_vec();
subtract_green::forward(&mut copy);
owned = copy;
&owned
} else {
argb
};
let sg = [TransformPlan::SubtractGreen];
let plans: &[TransformPlan] = if use_subtract_green { &sg } else { &[] };
if !use_lz77 {
let tokens_literal = parse(pixels, false);
let model_literal = RefModel::new(&tokens_literal, pixels, width);
return emit_stream(
width,
height,
alpha_used,
plans,
0,
&model_literal,
&model_literal.histogram(0),
);
}
search_lz77(width, height, alpha_used, plans, width, pixels)
}
fn search_lz77(
header_width: u32,
header_height: u32,
alpha_used: bool,
plans: &[TransformPlan],
working_width: u32,
pixels: &[u32],
) -> Vec<u8> {
let emit = |cache_bits, model: &RefModel<'_>, histogram: &Histogram| {
emit_stream(
header_width,
header_height,
alpha_used,
plans,
cache_bits,
model,
histogram,
)
};
let mut best = {
let tokens_literal = parse(pixels, false);
let model_literal = RefModel::new(&tokens_literal, pixels, working_width);
let (cache_literal, hist_literal) = best_cache_bits(&model_literal);
let mut best = emit(0, &model_literal, &model_literal.histogram(0));
if cache_literal > 0 {
keep_smaller(
&mut best,
emit(cache_literal, &model_literal, &hist_literal),
);
}
best
};
let (tokens_lz77, chain) = parse_lz77(pixels);
{
let model_lz77 = RefModel::new(&tokens_lz77, pixels, working_width);
let (cache_lz77, hist_lz77) = best_cache_bits(&model_lz77);
keep_smaller(&mut best, emit(cache_lz77, &model_lz77, &hist_lz77));
}
let tokens_dp = parse_optimal(pixels, working_width, &tokens_lz77, &chain);
drop(tokens_lz77);
drop(chain);
{
let model_dp = RefModel::new(&tokens_dp, pixels, working_width);
let (cache_dp, hist_dp) = best_cache_bits(&model_dp);
keep_smaller(&mut best, emit(cache_dp, &model_dp, &hist_dp));
}
best
}
fn search_lz77_best(
header_width: u32,
header_height: u32,
alpha_used: bool,
plans: &[TransformPlan],
working_width: u32,
pixels: &[u32],
) -> Vec<u8> {
let emit = |cache_bits, model: &RefModel<'_>, histogram: &Histogram| {
emit_stream(
header_width,
header_height,
alpha_used,
plans,
cache_bits,
model,
histogram,
)
};
let mut best = {
let tokens_literal = parse(pixels, false);
let model_literal = RefModel::new(&tokens_literal, pixels, working_width);
let (cache_literal, hist_literal) = best_cache_bits(&model_literal);
let mut best = emit(0, &model_literal, &model_literal.histogram(0));
if cache_literal > 0 {
keep_smaller(
&mut best,
emit(cache_literal, &model_literal, &hist_literal),
);
}
best
};
let (tokens_lz77, chain) = parse_lz77(pixels);
let model_lz77 = RefModel::new(&tokens_lz77, pixels, working_width);
let (cache_lz77, hist_lz77) = best_cache_bits(&model_lz77);
keep_smaller(&mut best, emit(cache_lz77, &model_lz77, &hist_lz77));
let tokens_dp = parse_optimal(pixels, working_width, &tokens_lz77, &chain);
drop(chain);
let model_dp = RefModel::new(&tokens_dp, pixels, working_width);
let (cache_dp, hist_dp) = best_cache_bits(&model_dp);
keep_smaller(&mut best, emit(cache_dp, &model_dp, &hist_dp));
let ysize = pixels.len() as u32 / working_width;
if let Some(plan) = meta::plan(&tokens_lz77, pixels, working_width, ysize, cache_lz77) {
keep_smaller(
&mut best,
emit_stream_meta(
header_width,
header_height,
alpha_used,
plans,
cache_lz77,
&model_lz77,
&plan,
),
);
}
if let Some(plan_dp) = meta::plan(&tokens_dp, pixels, working_width, ysize, cache_dp) {
keep_smaller(
&mut best,
emit_stream_meta(
header_width,
header_height,
alpha_used,
plans,
cache_dp,
&model_dp,
&plan_dp,
),
);
}
best
}
const TRANSFORM_TILE_BITS_SWEEP: [u32; 4] = [2, 3, 4, 5];
const _: () = {
let mut i = 0;
while i < TRANSFORM_TILE_BITS_SWEEP.len() {
assert!(
TRANSFORM_TILE_BITS_SWEEP[i] >= MIN_TRANSFORM_BITS
&& TRANSFORM_TILE_BITS_SWEEP[i] < MIN_TRANSFORM_BITS + (1 << NUM_TRANSFORM_BITS)
);
i += 1;
}
};
#[derive(Clone, Copy)]
enum BestTask {
Floor,
Palette,
Predictor(u32),
CrossColor(u32),
}
fn build_best_tasks() -> Vec<BestTask> {
let mut tasks = vec![BestTask::Floor, BestTask::Palette];
tasks.extend(
TRANSFORM_TILE_BITS_SWEEP
.iter()
.map(|&b| BestTask::Predictor(b)),
);
tasks.extend(
TRANSFORM_TILE_BITS_SWEEP
.iter()
.map(|&b| BestTask::CrossColor(b)),
);
tasks
}
fn run_best_task(
task: BestTask,
width: u32,
height: u32,
alpha_used: bool,
argb: &[u32],
) -> Vec<Vec<u8>> {
match task {
BestTask::Floor => floor_streams(width, height, alpha_used, argb),
BestTask::Palette => palette_streams(width, height, alpha_used, argb),
BestTask::Predictor(bits) => predictor_streams(width, height, alpha_used, argb, bits),
BestTask::CrossColor(bits) => cross_color_streams(width, height, alpha_used, argb, bits),
}
}
fn floor_streams(width: u32, height: u32, alpha_used: bool, argb: &[u32]) -> Vec<Vec<u8>> {
let floor = encode_with(width, height, argb, true);
let raw_meta = search_lz77_best(width, height, alpha_used, &[], width, argb);
let mut sg = argb.to_vec();
subtract_green::forward(&mut sg);
let sg_meta = search_lz77_best(
width,
height,
alpha_used,
&[TransformPlan::SubtractGreen],
width,
&sg,
);
vec![floor, raw_meta, sg_meta]
}
fn palette_streams(width: u32, height: u32, alpha_used: bool, argb: &[u32]) -> Vec<Vec<u8>> {
if let Some(p) = palette::forward(argb, width) {
let working_width = subsample_size(width, p.bits);
let plans = [TransformPlan::ColorIndexing {
num_colors: p.num_colors,
colormap: p.colormap,
}];
vec![search_lz77_best(
width,
height,
alpha_used,
&plans,
working_width,
&p.bundled,
)]
} else {
Vec::new()
}
}
fn predictor_streams(
width: u32,
height: u32,
alpha_used: bool,
argb: &[u32],
bits: u32,
) -> Vec<Vec<u8>> {
let (residual, tile_data) = predictor::forward(argb, width, height, bits);
let alone = search_lz77_best(
width,
height,
alpha_used,
&[TransformPlan::Predictor {
bits,
tile_data: tile_data.clone(),
}],
width,
&residual,
);
let mut residual_sg = residual;
subtract_green::forward(&mut residual_sg);
let with_sg = search_lz77_best(
width,
height,
alpha_used,
&[
TransformPlan::Predictor { bits, tile_data },
TransformPlan::SubtractGreen,
],
width,
&residual_sg,
);
vec![alone, with_sg]
}
fn cross_color_streams(
width: u32,
height: u32,
alpha_used: bool,
argb: &[u32],
bits: u32,
) -> Vec<Vec<u8>> {
let (stored, tile_data) = cross_color::forward(argb, width, height, bits);
let alone = search_lz77_best(
width,
height,
alpha_used,
&[TransformPlan::CrossColor {
bits,
tile_data: tile_data.clone(),
}],
width,
&stored,
);
let mut stored_sg = stored;
subtract_green::forward(&mut stored_sg);
let with_sg = search_lz77_best(
width,
height,
alpha_used,
&[
TransformPlan::CrossColor { bits, tile_data },
TransformPlan::SubtractGreen,
],
width,
&stored_sg,
);
vec![alone, with_sg]
}
fn keep_smallest(streams: Vec<Vec<u8>>) -> Option<Vec<u8>> {
let mut streams = streams.into_iter();
let mut best = streams.next()?;
for candidate in streams {
keep_smaller(&mut best, candidate);
}
Some(best)
}
fn reduce_task_minima(minima: impl IntoIterator<Item = Option<Vec<u8>>>) -> Vec<u8> {
minima
.into_iter()
.flatten()
.reduce(|mut best, candidate| {
keep_smaller(&mut best, candidate);
best
})
.unwrap_or_default()
}
#[cfg(not(feature = "rayon"))]
fn evaluate_best_tasks(
tasks: Vec<BestTask>,
width: u32,
height: u32,
alpha_used: bool,
argb: &[u32],
) -> Vec<u8> {
reduce_task_minima(
tasks
.into_iter()
.map(|t| keep_smallest(run_best_task(t, width, height, alpha_used, argb))),
)
}
#[cfg(feature = "rayon")]
fn evaluate_best_tasks(
tasks: Vec<BestTask>,
width: u32,
height: u32,
alpha_used: bool,
argb: &[u32],
) -> Vec<u8> {
use rayon::prelude::*;
let minima: Vec<Option<Vec<u8>>> = tasks
.into_par_iter()
.map(|t| keep_smallest(run_best_task(t, width, height, alpha_used, argb)))
.collect();
reduce_task_minima(minima)
}
#[must_use]
pub(crate) fn encode_best(width: u32, height: u32, argb: &[u32]) -> Vec<u8> {
let alpha_used = argb.iter().any(|&p| p >> 24 != 0xff);
evaluate_best_tasks(build_best_tasks(), width, height, alpha_used, argb)
}
fn keep_smaller(best: &mut Vec<u8>, candidate: Vec<u8>) {
if candidate.len() < best.len() {
*best = candidate;
}
}
#[derive(Clone, Copy)]
struct CopyCost {
length_symbol: u32,
length_bits: u32,
dist_symbol: u32,
dist_bits: u32,
}
pub(crate) struct RefModel<'a> {
tokens: &'a [Token],
pixels: &'a [u32],
width: u32,
copies: Vec<CopyCost>,
}
impl<'a> RefModel<'a> {
pub(crate) fn new(tokens: &'a [Token], pixels: &'a [u32], width: u32) -> Self {
let plane_map = PlaneCodeMap::new(width);
let copies = tokens
.iter()
.filter_map(|&token| match token {
Token::Copy { length, distance } => {
let (length_symbol, length_bits, _) = prefix_encode(length);
let plane_code = plane_map.plane_code(distance);
let (dist_symbol, dist_bits, _) = prefix_encode(plane_code);
Some(CopyCost {
length_symbol,
length_bits,
dist_symbol,
dist_bits,
})
},
Token::Literal(_) => None,
})
.collect();
Self {
tokens,
pixels,
width,
copies,
}
}
pub(crate) fn histogram(&self, cache_bits: u32) -> Histogram {
let cache_codes = if cache_bits > 0 {
1usize << cache_bits
} else {
0
};
work!(HistogramAlloc);
let mut histogram = Histogram::new(ALPHABET_SIZE[0] + cache_codes);
let mut cache = (cache_bits > 0).then(|| {
work!(ColorCacheAlloc);
ColorCache::new(cache_bits)
});
self.accumulate_into(cache_bits, &mut histogram, cache.as_mut());
histogram
}
fn accumulate_into(
&self,
cache_bits: u32,
histogram: &mut Histogram,
mut cache: Option<&mut ColorCache>,
) {
work!(HistogramPass);
let mut pos = 0usize;
let mut next_copy = 0usize;
for &token in self.tokens {
match token {
Token::Literal(argb) => {
match cache.as_deref_mut() {
None => histogram.add_literal(argb),
Some(cache) => {
let key = ColorCache::index(argb, cache_bits);
if cache.get(key) == argb {
histogram.add_cache(key as u16);
} else {
histogram.add_literal(argb);
}
cache.insert(argb);
},
}
pos += 1;
},
Token::Copy { length, .. } => {
let cost = self.copies[next_copy];
next_copy += 1;
histogram.add_length(cost.length_symbol, cost.length_bits);
histogram.add_distance(cost.dist_symbol, cost.dist_bits);
if let Some(cache) = cache.as_deref_mut() {
for pixel in &self.pixels[pos..pos + length as usize] {
cache.insert(*pixel);
}
}
pos += length as usize;
},
}
}
}
}
fn best_cache_bits(model: &RefModel<'_>) -> (u32, Histogram) {
let mut hist = Histogram::new(ALPHABET_SIZE[0] + (1usize << MAX_CACHE_BITS));
let mut cache = ColorCache::new(MAX_CACHE_BITS);
let mut best_bits = 0;
work!(CacheBitsBuild);
hist.reset();
model.accumulate_into(0, &mut hist, None);
let mut best_cost = hist.estimate_bits();
let mut best_hist = hist.snapshot_truncated(ALPHABET_SIZE[0]);
for bits in 1..=MAX_CACHE_BITS {
work!(CacheBitsBuild);
hist.reset();
cache.reset(bits);
model.accumulate_into(bits, &mut hist, Some(&mut cache));
let cost = hist.estimate_bits();
if cost < best_cost {
best_cost = cost;
best_bits = bits;
best_hist = hist.snapshot_truncated(ALPHABET_SIZE[0] + (1usize << bits));
}
}
(best_bits, best_hist)
}
fn emit_stream(
header_width: u32,
header_height: u32,
alpha_used: bool,
plans: &[TransformPlan],
cache_bits: u32,
model: &RefModel<'_>,
histogram: &Histogram,
) -> Vec<u8> {
let mut bw = BitWriter::new();
write_header(&mut bw, header_width, header_height, alpha_used);
write_transforms(&mut bw, plans, header_width, header_height);
write_color_cache(&mut bw, cache_bits);
bw.write_bits(0, 1); emit_coded_pixels(&mut bw, cache_bits, model, histogram);
bw.into_bytes()
}
fn emit_stream_meta(
header_width: u32,
header_height: u32,
alpha_used: bool,
plans: &[TransformPlan],
cache_bits: u32,
model: &RefModel<'_>,
plan: &MetaPlan,
) -> Vec<u8> {
let mut bw = BitWriter::new();
write_header(&mut bw, header_width, header_height, alpha_used);
write_transforms(&mut bw, plans, header_width, header_height);
write_color_cache(&mut bw, cache_bits);
bw.write_bits(1, 1); bw.write_bits(plan.bits - MIN_TRANSFORM_BITS, NUM_TRANSFORM_BITS); let entropy_pixels: Vec<u32> = plan.groups.iter().map(|&g| u32::from(g) << 8).collect();
emit_subimage(&mut bw, plan.entropy_xsize, &entropy_pixels);
emit_coded_pixels_meta(&mut bw, cache_bits, model, plan);
bw.into_bytes()
}
fn emit_coded_pixels_meta(
bw: &mut BitWriter,
cache_bits: u32,
model: &RefModel<'_>,
plan: &MetaPlan,
) {
let prefix_sets: Vec<[Prefix; 5]> = plan
.group_histograms
.iter()
.map(|h| {
[
Prefix::from_histogram(h.green()),
Prefix::from_histogram(h.red()),
Prefix::from_histogram(h.blue()),
Prefix::from_histogram(h.alpha()),
Prefix::from_histogram(h.dist()),
]
})
.collect();
for set in &prefix_sets {
for prefix in set {
write_huffman_code(bw, &prefix.lengths);
}
}
emit_tokens_meta(
bw,
model.tokens,
model.pixels,
cache_bits,
model.width,
&prefix_sets,
plan,
);
}
fn emit_tokens_meta(
bw: &mut BitWriter,
tokens: &[Token],
pixels: &[u32],
cache_bits: u32,
width: u32,
prefix_sets: &[[Prefix; 5]],
plan: &MetaPlan,
) {
resolve(tokens, pixels, cache_bits, width, |pos, unit| {
let x = pos as u32 % width;
let y = pos as u32 / width;
let block = ((y >> plan.bits) * plan.entropy_xsize + (x >> plan.bits)) as usize;
let [green, red, blue, alpha, dist] = &prefix_sets[plan.groups[block] as usize];
match unit {
Resolved::Literal(argb) => {
green.emit(bw, ((argb >> 8) & 0xff) as usize);
red.emit(bw, ((argb >> 16) & 0xff) as usize);
blue.emit(bw, (argb & 0xff) as usize);
alpha.emit(bw, ((argb >> 24) & 0xff) as usize);
},
Resolved::Copy {
length_symbol,
length_extra,
dist_symbol,
dist_extra,
} => {
green.emit(bw, NUM_LITERAL_CODES + length_symbol as usize);
bw.write_bits(length_extra.0, length_extra.1);
dist.emit(bw, dist_symbol as usize);
bw.write_bits(dist_extra.0, dist_extra.1);
},
Resolved::Cache(key) => {
green.emit(bw, NUM_LITERAL_CODES + NUM_LENGTH_CODES + usize::from(key));
},
}
});
}
fn emit_coded_pixels(
bw: &mut BitWriter,
cache_bits: u32,
model: &RefModel<'_>,
histogram: &Histogram,
) {
let prefixes = [
Prefix::from_histogram(histogram.green()),
Prefix::from_histogram(histogram.red()),
Prefix::from_histogram(histogram.blue()),
Prefix::from_histogram(histogram.alpha()),
Prefix::from_histogram(histogram.dist()),
];
for prefix in &prefixes {
write_huffman_code(bw, &prefix.lengths);
}
emit_tokens(
bw,
model.tokens,
model.pixels,
cache_bits,
model.width,
&prefixes,
);
}
fn emit_subimage(bw: &mut BitWriter, width: u32, pixels: &[u32]) {
write_color_cache(bw, 0);
let tokens = parse(pixels, false);
let model = RefModel::new(&tokens, pixels, width);
emit_coded_pixels(bw, 0, &model, &model.histogram(0));
}
fn write_header(bw: &mut BitWriter, width: u32, height: u32, alpha_used: bool) {
bw.write_bits(u32::from(VP8L_MAGIC_BYTE), 8);
bw.write_bits(width - 1, VP8L_IMAGE_SIZE_BITS);
bw.write_bits(height - 1, VP8L_IMAGE_SIZE_BITS);
bw.write_bits(u32::from(alpha_used), 1);
bw.write_bits(0, VP8L_VERSION_BITS);
}
fn write_transforms(bw: &mut BitWriter, plans: &[TransformPlan], width: u32, _height: u32) {
for plan in plans {
bw.write_bits(1, 1); match plan {
TransformPlan::Predictor { bits, tile_data } => {
bw.write_bits(PREDICTOR_TRANSFORM, TRANSFORM_TYPE_BITS);
bw.write_bits(*bits - MIN_TRANSFORM_BITS, NUM_TRANSFORM_BITS);
emit_subimage(bw, subsample_size(width, *bits), tile_data);
},
TransformPlan::CrossColor { bits, tile_data } => {
bw.write_bits(CROSS_COLOR_TRANSFORM, TRANSFORM_TYPE_BITS);
bw.write_bits(*bits - MIN_TRANSFORM_BITS, NUM_TRANSFORM_BITS);
emit_subimage(bw, subsample_size(width, *bits), tile_data);
},
TransformPlan::SubtractGreen => {
bw.write_bits(SUBTRACT_GREEN_TRANSFORM, TRANSFORM_TYPE_BITS);
},
TransformPlan::ColorIndexing {
num_colors,
colormap,
} => {
bw.write_bits(COLOR_INDEXING_TRANSFORM, TRANSFORM_TYPE_BITS);
bw.write_bits(*num_colors - 1, NUM_COLORS_FIELD_WIDTH);
emit_subimage(bw, *num_colors, colormap);
},
}
}
bw.write_bits(0, 1); }
fn write_color_cache(bw: &mut BitWriter, cache_bits: u32) {
if cache_bits > 0 {
bw.write_bits(1, 1);
bw.write_bits(cache_bits, CACHE_BITS_FIELD_WIDTH);
} else {
bw.write_bits(0, 1);
}
}
fn emit_tokens(
bw: &mut BitWriter,
tokens: &[Token],
pixels: &[u32],
cache_bits: u32,
width: u32,
prefixes: &[Prefix; 5],
) {
let [green, red, blue, alpha, dist] = prefixes;
resolve(tokens, pixels, cache_bits, width, |_pos, unit| match unit {
Resolved::Literal(argb) => {
green.emit(bw, ((argb >> 8) & 0xff) as usize);
red.emit(bw, ((argb >> 16) & 0xff) as usize);
blue.emit(bw, (argb & 0xff) as usize);
alpha.emit(bw, ((argb >> 24) & 0xff) as usize);
},
Resolved::Copy {
length_symbol,
length_extra,
dist_symbol,
dist_extra,
} => {
green.emit(bw, NUM_LITERAL_CODES + length_symbol as usize);
bw.write_bits(length_extra.0, length_extra.1);
dist.emit(bw, dist_symbol as usize);
bw.write_bits(dist_extra.0, dist_extra.1);
},
Resolved::Cache(key) => {
green.emit(bw, NUM_LITERAL_CODES + NUM_LENGTH_CODES + usize::from(key));
},
});
}
struct Prefix {
lengths: Vec<u32>,
codes: Vec<(u32, u32)>,
}
impl Prefix {
fn from_histogram(histogram: &[u32]) -> Self {
let lengths = build_code_lengths(histogram, MAX_MAIN_CODE_LENGTH);
let codes = emit_codes(&lengths);
Self { lengths, codes }
}
fn emit(&self, bw: &mut BitWriter, symbol: usize) {
let (code, len) = self.codes[symbol];
bw.write_bits(code, len);
}
}
#[cfg(test)]
mod tests {
use super::{
RefModel, TransformPlan, best_cache_bits, emit_stream, emit_stream_meta, encode,
encode_best, encode_stream,
};
use crate::lossless::transform::predictor;
use crate::lossless::vp8l::backref::parse;
use crate::lossless::vp8l::decode::decode;
use crate::lossless::vp8l::meta;
fn argb(a: u32, r: u32, g: u32, b: u32) -> u32 {
(a << 24) | (r << 16) | (g << 8) | b
}
fn assert_round_trip(width: u32, height: u32, pixels: &[u32]) {
let payload = encode(width, height, pixels);
let decoded = decode(&payload).expect("our encoder must emit decodable VP8L");
assert_eq!((decoded.width, decoded.height), (width, height));
assert_eq!(decoded.argb.as_slice(), pixels);
}
fn alpha_used(pixels: &[u32]) -> bool {
pixels.iter().any(|&p| p >> 24 != 0xff)
}
fn literal_baseline(width: u32, height: u32, pixels: &[u32]) -> Vec<u8> {
let tokens = parse(pixels, false);
let model = RefModel::new(&tokens, pixels, width);
emit_stream(
width,
height,
alpha_used(pixels),
&[],
0,
&model,
&model.histogram(0),
)
}
#[test]
fn round_trips_a_1x1_pixel() {
assert_round_trip(1, 1, &[argb(255, 50, 100, 200)]);
}
#[test]
fn solid_image_has_empty_pixel_data() {
let color = argb(255, 10, 20, 30);
let small = encode(1, 1, &[color]);
let large = encode(4, 4, &[color; 16]);
assert_eq!(small.len(), large.len());
assert_round_trip(1, 1, &[color]);
assert_round_trip(4, 4, &[color; 16]);
}
#[test]
fn round_trips_a_gradient() {
let pixels: Vec<u32> = (0..64u32).map(|v| argb(255, v * 2, v, 255 - v)).collect();
assert_round_trip(8, 8, &pixels);
}
#[test]
fn round_trips_all_transparent_preserving_rgb() {
let pixels: Vec<u32> = (0..16u32).map(|v| argb(0, v, 63 - v, v * 3)).collect();
assert!(alpha_used(&pixels));
assert_round_trip(4, 4, &pixels);
}
#[test]
fn round_trips_a_single_row() {
let pixels: Vec<u32> = (0..20u32).map(|v| argb(255, v, 100, 200 - v)).collect();
assert_round_trip(20, 1, &pixels);
}
#[test]
fn round_trips_a_single_column() {
let pixels: Vec<u32> = (0..20u32).map(|v| argb(255, 5, v, v)).collect();
assert_round_trip(1, 20, &pixels);
}
#[test]
fn tier1_chosen_when_subtract_green_shrinks() {
let pixels: Vec<u32> = (0..32u32).map(|v| argb(255, v, v, v)).collect();
let au = alpha_used(&pixels);
let tier0 = encode_stream(32, 1, au, &pixels, false, true);
let tier1 = encode_stream(32, 1, au, &pixels, true, true);
assert!(
tier1.len() < tier0.len(),
"subtract-green must shrink a grayscale ramp"
);
assert_eq!(encode(32, 1, &pixels), tier1);
assert_round_trip(32, 1, &pixels);
}
#[test]
fn tier0_chosen_when_only_green_varies() {
let pixels: Vec<u32> = (0..32u32).map(|v| argb(255, 0x40, v, 0x80)).collect();
let au = alpha_used(&pixels);
let tier0 = encode_stream(32, 1, au, &pixels, false, true);
let tier1 = encode_stream(32, 1, au, &pixels, true, true);
assert!(tier0.len() <= tier1.len());
assert_eq!(encode(32, 1, &pixels), tier0);
assert_round_trip(32, 1, &pixels);
}
#[test]
fn lz77_shrinks_a_repeating_pattern() {
let tile = [
argb(255, 10, 20, 30),
argb(255, 40, 50, 60),
argb(255, 70, 80, 90),
argb(255, 100, 110, 120),
];
let pixels: Vec<u32> = tile.iter().cycle().take(256).copied().collect();
let full = encode(64, 4, &pixels);
let baseline = literal_baseline(64, 4, &pixels);
assert!(
full.len() < baseline.len(),
"LZ77 must shrink a repeating pattern: {} vs {}",
full.len(),
baseline.len()
);
assert_round_trip(64, 4, &pixels);
}
#[test]
fn cost_model_enables_cache_for_a_scattered_palette() {
let pixels: Vec<u32> = (0..1024u32)
.map(|i| {
let (x, y) = (i % 32, i / 32);
let c = (x.wrapping_mul(5).wrapping_add(y.wrapping_mul(11))) % 12;
argb(255, c * 20 + 8, c * 13 + 17, c * 7 + 29)
})
.collect();
let tokens = parse(&pixels, false);
let (bits, _) = best_cache_bits(&RefModel::new(&tokens, &pixels, 32));
assert!(
bits > 0,
"cost model must enable a cache for a scattered palette"
);
assert_round_trip(32, 32, &pixels);
}
#[test]
fn forced_cache_stream_round_trips() {
let a = argb(255, 1, 2, 3);
let b = argb(255, 250, 240, 230);
let pixels: Vec<u32> = (0..40u32).map(|i| if i % 2 == 0 { a } else { b }).collect();
let tokens = parse(&pixels, false);
let model = RefModel::new(&tokens, &pixels, 40);
let stream = emit_stream(40, 1, false, &[], 6, &model, &model.histogram(6));
let decoded = decode(&stream).expect("a cache-coded stream must decode");
assert_eq!((decoded.width, decoded.height), (40, 1));
assert_eq!(decoded.argb, pixels);
}
#[test]
fn predictor_subimage_stream_round_trips() {
let width = 4u32;
let height = 4u32;
let source: Vec<u32> = (0..16u32)
.map(|v| argb(255, v * 4, v * 2, 100 + v))
.collect();
let bits = 2u32;
let (residual, tile_data) = predictor::forward(&source, width, height, bits);
let tokens = parse(&residual, false);
let model = RefModel::new(&tokens, &residual, width);
let plans = [TransformPlan::Predictor { bits, tile_data }];
let stream = emit_stream(
width,
height,
alpha_used(&source),
&plans,
0,
&model,
&model.histogram(0),
);
let decoded = decode(&stream).expect("a predictor sub-image stream must decode");
assert_eq!((decoded.width, decoded.height), (width, height));
assert_eq!(decoded.argb, source);
}
#[test]
fn never_regresses_the_literal_baseline() {
let cases: [(u32, u32, Vec<u32>); 3] = [
(
8,
8,
(0..64u32).map(|v| argb(255, v, v * 3, v * 7)).collect(),
),
(16, 1, vec![argb(255, 5, 5, 5); 16]),
(
4,
4,
(0..16u32).map(|v| argb(255, v % 3, v % 3, v % 3)).collect(),
),
];
for (w, h, pixels) in cases {
assert!(encode(w, h, &pixels).len() <= literal_baseline(w, h, &pixels).len());
assert_round_trip(w, h, &pixels);
}
}
fn assert_best_round_trip(width: u32, height: u32, pixels: &[u32]) {
let payload = encode_best(width, height, pixels);
let decoded = decode(&payload).expect("encode_best must emit decodable VP8L");
assert_eq!((decoded.width, decoded.height), (width, height));
assert_eq!(decoded.argb.as_slice(), pixels);
}
#[test]
fn best_never_regresses_the_floor() {
let cases: [(u32, u32, Vec<u32>); 3] = [
(
8,
8,
(0..64u32).map(|v| argb(255, v, v * 3, v * 7)).collect(),
),
(16, 1, vec![argb(255, 5, 5, 5); 16]),
(
4,
4,
(0..16u32).map(|v| argb(255, v % 3, v % 3, v % 3)).collect(),
),
];
for (w, h, pixels) in cases {
assert!(encode_best(w, h, &pixels).len() <= encode(w, h, &pixels).len());
assert_best_round_trip(w, h, &pixels);
}
}
#[test]
fn best_palette_beats_the_literal_baseline_on_a_small_palette() {
let colors = [
argb(255, 10, 20, 30),
argb(255, 200, 40, 60),
argb(255, 70, 220, 90),
argb(255, 100, 110, 240),
argb(255, 33, 66, 99),
argb(255, 240, 240, 10),
argb(255, 15, 250, 250),
argb(255, 250, 15, 130),
];
let pixels: Vec<u32> = (0..256usize)
.map(|i| colors[(i * 7 + i / 16) % colors.len()])
.collect();
let best = encode_best(16, 16, &pixels);
let baseline = literal_baseline(16, 16, &pixels);
assert!(
best.len() < baseline.len(),
"palette Best must beat the literal baseline: {} vs {}",
best.len(),
baseline.len()
);
assert_best_round_trip(16, 16, &pixels);
}
#[test]
fn best_predictor_shrinks_a_smooth_gradient() {
let pixels: Vec<u32> = (0..256u32)
.map(|i| {
let v = (i % 16 + i / 16) * 8;
argb(255, v, v, v)
})
.collect();
let best = encode_best(16, 16, &pixels);
let balanced = encode(16, 16, &pixels);
assert!(
best.len() < balanced.len(),
"predictor Best must shrink a smooth gradient below Balanced: {} vs {}",
best.len(),
balanced.len()
);
assert_best_round_trip(16, 16, &pixels);
}
#[test]
fn best_cross_color_helps_a_correlated_image() {
let pixels: Vec<u32> = (0..64u32)
.map(|i| {
let g = i * 2;
argb(255, g >> 1, g, g >> 2)
})
.collect();
let best = encode_best(8, 8, &pixels);
let balanced = encode(8, 8, &pixels);
assert!(
best.len() < balanced.len(),
"cross-color Best must beat Balanced on a correlated image: {} vs {}",
best.len(),
balanced.len()
);
assert_best_round_trip(8, 8, &pixels);
}
#[test]
fn emit_meta_two_groups_round_trips() {
let mut pixels = Vec::with_capacity(256);
for y in 0..16u32 {
for x in 0..16u32 {
pixels.push(if y < 8 {
argb(255, (x * 16 + y) & 0xff, x & 7, 0)
} else {
argb(255, 0, x & 7, (x * 16 + (y - 8)) & 0xff)
});
}
}
let tokens = crate::lossless::vp8l::backref::parse(&pixels, true);
let plan =
meta::plan(&tokens, &pixels, 16, 16, 0).expect("regional image should plan >=2 groups");
assert!(
plan.group_histograms.len() >= 2,
"expected >=2 groups, got {}",
plan.group_histograms.len()
);
let model = RefModel::new(&tokens, &pixels, 16);
let bytes = emit_stream_meta(16, 16, alpha_used(&pixels), &[], 0, &model, &plan);
let decoded = decode(&bytes).expect("meta stream must decode");
assert_eq!((decoded.width, decoded.height), (16, 16));
assert_eq!(decoded.argb.as_slice(), pixels.as_slice());
}
#[test]
fn alpha_stream_round_trips_headerless() {
use crate::lossless::vp8l::decode::decode_alpha_stream;
let cases: [(u32, u32, Vec<u8>); 3] = [
(4, 4, vec![0x80u8; 16]),
(8, 2, (0..16u32).map(|v| (v * 15) as u8).collect()),
(
5,
5,
(0..25u32)
.map(|v| (v.wrapping_mul(37) ^ 0x5a) as u8)
.collect(),
),
];
for (w, h, alpha) in cases {
let stream = super::encode_alpha_stream(&alpha, w, h);
let decoded = decode_alpha_stream(&stream, w, h).expect("alpha stream must decode");
assert_eq!(decoded, alpha, "{w}x{h} alpha round-trip");
}
}
#[test]
fn encode_best_output_is_byte_stable() {
const EXPECTED: [u64; 4] = [
0xaa74_a8dc_ab4f_7a97,
0x0e16_a604_b5f2_a267,
0x2401_f6da_e437_c4b3,
0xa5c5_1ec6_3637_64c7,
];
let got: [u64; 4] =
byte_stability_cases().map(|(w, h, pixels)| fnv1a64(&encode_best(w, h, &pixels)));
assert_eq!(
got, EXPECTED,
"encode_best output bytes drifted from the committed golden"
);
}
#[test]
fn task_minima_fold_is_index_canonical() {
use super::{keep_smallest, reduce_task_minima};
assert_eq!(
keep_smallest(vec![vec![3u8; 4], vec![4u8; 4]]),
Some(vec![3u8; 4])
);
assert_eq!(keep_smallest(Vec::new()), None);
let families: Vec<Vec<Vec<u8>>> = vec![
vec![vec![0u8; 5], vec![1u8; 5]], Vec::new(), vec![vec![2u8; 5]],
vec![vec![9u8; 3]], ];
let minima: Vec<Option<Vec<u8>>> = families.into_iter().map(keep_smallest).collect();
assert_eq!(minima[1], None);
assert_eq!(reduce_task_minima(minima), vec![9u8; 3]);
let ties: Vec<Vec<Vec<u8>>> = vec![vec![vec![7u8; 4]], vec![vec![8u8; 4]]];
let ties_minima: Vec<Option<Vec<u8>>> = ties.into_iter().map(keep_smallest).collect();
assert_eq!(reduce_task_minima(ties_minima), vec![7u8; 4]);
}
#[cfg(feature = "rayon")]
#[test]
fn serial_and_parallel_evaluation_agree() {
use super::{
build_best_tasks, evaluate_best_tasks, keep_smallest, reduce_task_minima, run_best_task,
};
for (w, h, pixels) in byte_stability_cases() {
let alpha = pixels.iter().any(|&p| p >> 24 != 0xff);
let serial = reduce_task_minima(
build_best_tasks()
.into_iter()
.map(|t| keep_smallest(run_best_task(t, w, h, alpha, &pixels))),
);
let parallel = evaluate_best_tasks(build_best_tasks(), w, h, alpha, &pixels);
assert_eq!(
serial, parallel,
"rayon evaluation must equal serial, byte-for-byte"
);
assert_eq!(parallel, encode_best(w, h, &pixels));
}
}
fn fnv1a64(bytes: &[u8]) -> u64 {
let mut h = 0xcbf2_9ce4_8422_2325u64;
for &b in bytes {
h ^= u64::from(b);
h = h.wrapping_mul(0x100_0000_01b3);
}
h
}
fn byte_stability_cases() -> [(u32, u32, Vec<u32>); 4] {
let solid = vec![argb(255, 10, 20, 30); 16];
let gradient: Vec<u32> = (0..64u32).map(|v| argb(255, v * 2, v, 255 - v)).collect();
let scattered: Vec<u32> = (0..1024u32)
.map(|i| {
let (x, y) = (i % 32, i / 32);
let c = (x * 5 + y * 11) % 12;
argb(255, c * 20 + 8, c * 13 + 17, c * 7 + 29)
})
.collect();
let tile = [
argb(255, 10, 20, 30),
argb(255, 40, 50, 60),
argb(255, 70, 80, 90),
argb(255, 100, 110, 120),
];
let repeating: Vec<u32> = tile.iter().cycle().take(256).copied().collect();
[
(4, 4, solid),
(8, 8, gradient),
(32, 32, scattered),
(64, 4, repeating),
]
}
#[test]
fn search_lz77_returns_the_literal_cache_winner() {
use super::{search_lz77, search_lz77_best};
use crate::lossless::vp8l::backref::{parse_lz77, parse_optimal};
let mut state = 3u64 | 1;
let pixels: Vec<u32> = (0..256usize)
.map(|_| {
state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
let c = (state >> 40) as u32 % 16;
argb(255, c * 20 + 8, c * 13 + 17, c * 7 + 29)
})
.collect();
let (w, h) = (16u32, 16u32);
let au = alpha_used(&pixels);
let tokens_lit = parse(&pixels, false);
let model_lit = RefModel::new(&tokens_lit, &pixels, w);
let (bits, hist) = best_cache_bits(&model_lit);
assert!(bits > 0, "the cost model must enable a cache here");
let lit_cache = emit_stream(w, h, au, &[], bits, &model_lit, &hist);
let lit_zero = emit_stream(w, h, au, &[], 0, &model_lit, &model_lit.histogram(0));
assert!(
lit_cache.len() < lit_zero.len(),
"the cache must shrink the stream"
);
let (tokens_lz, chain) = parse_lz77(&pixels);
let model_lz = RefModel::new(&tokens_lz, &pixels, w);
let (b_lz, h_lz) = best_cache_bits(&model_lz);
assert!(emit_stream(w, h, au, &[], b_lz, &model_lz, &h_lz).len() > lit_cache.len());
let tokens_dp = parse_optimal(&pixels, w, &tokens_lz, &chain);
let model_dp = RefModel::new(&tokens_dp, &pixels, w);
let (b_dp, h_dp) = best_cache_bits(&model_dp);
assert!(emit_stream(w, h, au, &[], b_dp, &model_dp, &h_dp).len() > lit_cache.len());
assert_eq!(search_lz77(w, h, au, &[], w, &pixels), lit_cache);
assert_eq!(search_lz77_best(w, h, au, &[], w, &pixels), lit_cache);
assert_round_trip(w, h, &pixels);
}
#[test]
fn search_lz77_best_meta_shot_shrinks_a_bimodal_image() {
use super::{search_lz77, search_lz77_best};
let (w, h) = (64u32, 32u32);
let mut pixels = Vec::with_capacity((w * h) as usize);
for y in 0..h {
for x in 0..w {
pixels.push(if y < h / 2 {
argb(255, 10, 20, 30)
} else {
argb(255, (x * 3) & 0xff, (x * 7 + y) & 0xff, (y * 5) & 0xff)
});
}
}
let au = alpha_used(&pixels);
let with_meta = search_lz77_best(w, h, au, &[], w, &pixels);
let no_meta = search_lz77(w, h, au, &[], w, &pixels);
assert!(
with_meta.len() < no_meta.len(),
"meta-Huffman must shrink the bimodal image: {} vs {}",
with_meta.len(),
no_meta.len()
);
let decoded = decode(&with_meta).expect("meta-winning stream must decode");
assert_eq!((decoded.width, decoded.height), (w, h));
assert_eq!(decoded.argb.as_slice(), pixels.as_slice());
}
#[test]
fn emit_meta_varying_alpha_round_trips() {
let mut pixels = Vec::with_capacity(256);
for y in 0..16u32 {
for x in 0..16u32 {
let a = 1 + ((x * 5 + y * 3) & 0x3f);
pixels.push(if y < 8 {
argb(a, (x * 16 + y) & 0xff, x & 7, 0)
} else {
argb(a, 0, x & 7, (x * 16 + (y - 8)) & 0xff)
});
}
}
assert!(pixels.iter().any(|&p| p >> 24 != pixels[0] >> 24));
let tokens = crate::lossless::vp8l::backref::parse(&pixels, true);
let plan =
meta::plan(&tokens, &pixels, 16, 16, 0).expect("regional image should plan >=2 groups");
assert!(plan.group_histograms.len() >= 2);
let model = RefModel::new(&tokens, &pixels, 16);
let bytes = emit_stream_meta(16, 16, alpha_used(&pixels), &[], 0, &model, &plan);
let decoded = decode(&bytes).expect("meta stream must decode");
assert_eq!((decoded.width, decoded.height), (16, 16));
assert_eq!(decoded.argb.as_slice(), pixels.as_slice());
}
#[test]
fn emit_meta_cache_reference_uses_the_correct_symbol() {
use crate::lossless::vp8l::backref::{Resolved, resolve};
let top = [
argb(255, 10, 20, 30),
argb(255, 200, 40, 60),
argb(255, 70, 220, 90),
argb(255, 100, 110, 240),
];
let bottom = [
argb(255, 33, 66, 99),
argb(255, 240, 240, 10),
argb(255, 15, 250, 250),
argb(255, 250, 15, 130),
];
let mut pixels = Vec::with_capacity(256);
for y in 0..16u32 {
for x in 0..16u32 {
let palette = if y < 8 { &top } else { &bottom };
pixels.push(palette[((x / 2) % 4) as usize]);
}
}
let cache_bits = 8u32;
let tokens = parse(&pixels, false);
let mut cache_hit_count = 0usize;
resolve(&tokens, &pixels, cache_bits, 16, |_pos, unit| {
if matches!(unit, Resolved::Cache(_)) {
cache_hit_count += 1;
}
});
assert!(
cache_hit_count > 0,
"the meta cache branch must be exercised"
);
let plan = meta::plan(&tokens, &pixels, 16, 16, cache_bits)
.expect("two disjoint-palette halves must plan >=2 groups");
assert!(
plan.group_histograms.len() >= 2,
"expected >=2 groups, got {}",
plan.group_histograms.len()
);
let model = RefModel::new(&tokens, &pixels, 16);
let bytes = emit_stream_meta(16, 16, alpha_used(&pixels), &[], cache_bits, &model, &plan);
let decoded = decode(&bytes).expect("meta cache stream must decode");
assert_eq!((decoded.width, decoded.height), (16, 16));
assert_eq!(decoded.argb.as_slice(), pixels.as_slice());
}
#[test]
fn cross_color_streams_are_nonempty_and_round_trip() {
use super::cross_color_streams;
let pixels: Vec<u32> = (0..64u32)
.map(|i| {
let g = i * 2;
argb(255, g >> 1, g, g >> 2)
})
.collect();
let streams = cross_color_streams(8, 8, alpha_used(&pixels), &pixels, 3);
assert_eq!(
streams.len(),
2,
"cross-color family must emit two candidates"
);
for stream in &streams {
let decoded = decode(stream).expect("each cross-color stream must decode");
assert_eq!((decoded.width, decoded.height), (8, 8));
assert_eq!(decoded.argb.as_slice(), pixels.as_slice());
}
}
#[test]
fn cross_color_transform_tile_bits_field_round_trips() {
use crate::lossless::transform::cross_color;
let width = 32u32;
let height = 8u32;
let source: Vec<u32> = (0..(width * height))
.map(|i| {
let x = i % width;
let g = (i * 2) & 0x7f;
if x < 16 {
argb(255, g >> 1, g, g >> 2)
} else {
argb(255, g, g, 0)
}
})
.collect();
let bits = 2u32;
let (stored, tile_data) = cross_color::forward(&source, width, height, bits);
assert!(
tile_data
.iter()
.collect::<std::collections::BTreeSet<_>>()
.len()
> 1,
"the tile grid must carry >1 distinct multiplier for the field to matter"
);
let tokens = parse(&stored, false);
let model = RefModel::new(&tokens, &stored, width);
let plans = [TransformPlan::CrossColor { bits, tile_data }];
let stream = emit_stream(
width,
height,
alpha_used(&source),
&plans,
0,
&model,
&model.histogram(0),
);
let decoded = decode(&stream).expect("a cross-color sub-image stream must decode");
assert_eq!((decoded.width, decoded.height), (width, height));
assert_eq!(decoded.argb, source);
}
#[test]
fn encode_output_is_byte_stable() {
const EXPECTED: [u64; 4] = [
0xaa74_a8dc_ab4f_7a97,
0xa071_c300_9622_384f,
0x8055_bafc_bbc8_6673,
0x2ac6_6ad5_3623_697f,
];
for ((w, h, pixels), &expected) in byte_stability_cases().into_iter().zip(&EXPECTED) {
assert_eq!(
fnv1a64(&encode(w, h, &pixels)),
expected,
"encode({w}x{h}) output bytes drifted from the committed golden"
);
}
}
}
#[cfg(test)]
mod proptests {
use super::{RefModel, best_cache_bits, emit_stream_meta, encode, encode_best, encode_with};
use crate::lossless::constants::{ALPHABET_SIZE, MAX_CACHE_BITS};
use crate::lossless::histogram::Histogram;
use crate::lossless::vp8l::backref::{Resolved, Token, parse, resolve};
use crate::lossless::vp8l::decode::decode;
use crate::lossless::vp8l::meta;
use proptest::prelude::*;
fn alpha_used(pixels: &[u32]) -> bool {
pixels.iter().any(|&p| p >> 24 != 0xff)
}
fn reference_accumulate(
tokens: &[Token],
pixels: &[u32],
cache_bits: u32,
width: u32,
) -> Histogram {
let cache_codes = if cache_bits > 0 {
1usize << cache_bits
} else {
0
};
let mut histogram = Histogram::new(ALPHABET_SIZE[0] + cache_codes);
resolve(tokens, pixels, cache_bits, width, |_pos, unit| match unit {
Resolved::Literal(argb) => histogram.add_literal(argb),
Resolved::Copy {
length_symbol,
length_extra,
dist_symbol,
dist_extra,
} => {
histogram.add_length(length_symbol, length_extra.1);
histogram.add_distance(dist_symbol, dist_extra.1);
},
Resolved::Cache(key) => histogram.add_cache(key),
});
histogram
}
fn seeded_pixels(seed: u64, count: usize, palette: u32) -> Vec<u32> {
let mut state = seed | 1;
(0..count)
.map(|_| {
state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
(state >> 40) as u32 % palette
})
.collect()
}
fn structured_pixels(variant: u8, seed: u64, width: u32, height: u32) -> Vec<u32> {
let count = (width * height) as usize;
match variant % 3 {
0 => {
let palette = [
0xff10_2030u32,
0xff20_4030,
0xff70_a0c0,
0xffa0_3060,
0xff30_c090,
0xfff0_f010,
];
seeded_pixels(seed, count, palette.len() as u32)
.into_iter()
.map(|idx| palette[idx as usize])
.collect()
},
1 => (0..count as u32)
.map(|i| {
let v = ((i % width + i / width) * 4) & 0xff;
0xff00_0000 | (v << 16) | (v << 8) | v
})
.collect(),
_ => (0..count as u32)
.map(|i| {
let g = (i * 2) & 0x7f;
0xff00_0000 | ((g >> 1) << 16) | (g << 8) | (g >> 2)
})
.collect(),
}
}
proptest! {
#[test]
fn encode_decode_round_trip(
width in 1u32..=16,
height in 1u32..=16,
seed in any::<u64>(),
palette in 1u32..=8,
) {
let count = (width * height) as usize;
let pixels = seeded_pixels(seed, count, palette);
let payload = encode(width, height, &pixels);
let decoded = decode(&payload).expect("encoder must emit decodable VP8L");
prop_assert_eq!(decoded.width, width);
prop_assert_eq!(decoded.height, height);
prop_assert_eq!(&decoded.argb, &pixels);
let fast = encode_with(width, height, &pixels, false);
let decoded_fast = decode(&fast).expect("Fast encoder must emit decodable VP8L");
prop_assert_eq!(decoded_fast.width, width);
prop_assert_eq!(decoded_fast.height, height);
prop_assert_eq!(decoded_fast.argb, pixels);
}
#[test]
fn encode_best_round_trip(
width in 1u32..=16,
height in 1u32..=16,
seed in any::<u64>(),
variant in 0u8..3,
) {
let pixels = structured_pixels(variant, seed, width, height);
let payload = encode_best(width, height, &pixels);
let decoded = decode(&payload).expect("encode_best must emit decodable VP8L");
prop_assert_eq!(decoded.width, width);
prop_assert_eq!(decoded.height, height);
prop_assert_eq!(decoded.argb, pixels);
}
#[test]
fn encode_best_is_repeatable(width in 1u32..=16, height in 1u32..=16, seed in any::<u64>(), variant in 0u8..3) {
let pixels = structured_pixels(variant, seed, width, height);
let a = encode_best(width, height, &pixels);
for _ in 0..3 {
prop_assert_eq!(&encode_best(width, height, &pixels), &a);
}
}
#[test]
fn meta_emit_decodes_to_source(
width in 2u32..=16, height in 2u32..=16, seed in any::<u64>(), variant in 0u8..3,
) {
let pixels = structured_pixels(variant, seed, width, height);
let tokens = crate::lossless::vp8l::backref::parse(&pixels, true);
if let Some(plan) = meta::plan(&tokens, &pixels, width, height, 0) {
let model = RefModel::new(&tokens, &pixels, width);
let bytes = emit_stream_meta(width, height, alpha_used(&pixels), &[], 0, &model, &plan);
let decoded = decode(&bytes).expect("meta emit must decode");
prop_assert_eq!(decoded.width, width);
prop_assert_eq!(decoded.height, height);
prop_assert_eq!(decoded.argb, pixels);
}
}
#[test]
fn ref_model_matches_accumulate(
width in 1u32..=16,
seed in any::<u64>(),
len in 1usize..=200,
palette in 1u32..=8,
use_lz77 in any::<bool>(),
) {
let pixels = seeded_pixels(seed, len, palette);
let tokens = parse(&pixels, use_lz77);
let model = RefModel::new(&tokens, &pixels, width);
for cache_bits in 0..=MAX_CACHE_BITS {
prop_assert_eq!(
model.histogram(cache_bits),
reference_accumulate(&tokens, &pixels, cache_bits, width),
"histogram mismatch at cache_bits={}",
cache_bits
);
}
}
#[test]
fn best_cache_bits_reuse_matches_fresh(
width in 1u32..=16,
seed in any::<u64>(),
len in 1usize..=200,
palette in 1u32..=8,
use_lz77 in any::<bool>(),
) {
let pixels = seeded_pixels(seed, len, palette);
let tokens = parse(&pixels, use_lz77);
let model = RefModel::new(&tokens, &pixels, width);
let mut fresh_bits = 0;
let mut best = model.histogram(0).estimate_bits();
for bits in 1..=MAX_CACHE_BITS {
let cost = model.histogram(bits).estimate_bits();
if cost < best {
best = cost;
fresh_bits = bits;
}
}
prop_assert_eq!(best_cache_bits(&model).0, fresh_bits);
}
#[test]
fn best_cache_bits_histogram_matches_fresh(
width in 1u32..=16,
seed in any::<u64>(),
len in 1usize..=200,
palette in 1u32..=8,
use_lz77 in any::<bool>(),
) {
let pixels = seeded_pixels(seed, len, palette);
let tokens = parse(&pixels, use_lz77);
let model = RefModel::new(&tokens, &pixels, width);
let (bits, hist) = best_cache_bits(&model);
prop_assert_eq!(hist, model.histogram(bits));
}
}
}