use alloc::vec::Vec;
use super::op_format::OpCategory;
use super::registry::{CodecFormats, FormatEntry};
use crate::PixelDescriptor;
use crate::negotiate::{
ConversionCost, Provenance, conversion_cost_with_provenance, suitability_loss, weighted_score,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum LossBucket {
Lossless,
NearLossless,
LowLoss,
Moderate,
High,
}
impl LossBucket {
pub fn from_model_loss(loss: u16) -> Self {
if loss <= 10 {
Self::Lossless
} else if loss <= 50 {
Self::NearLossless
} else if loss <= 150 {
Self::LowLoss
} else if loss <= 400 {
Self::Moderate
} else {
Self::High
}
}
pub fn max_loss(self) -> u16 {
match self {
Self::Lossless => 10,
Self::NearLossless => 50,
Self::LowLoss => 150,
Self::Moderate => 400,
Self::High => u16::MAX,
}
}
}
#[derive(Clone, Copy, Debug)]
pub enum QualityThreshold {
Lossless,
SubPerceptual,
NearLossless,
MaxBucket(LossBucket),
}
impl QualityThreshold {
fn max_loss(self) -> u16 {
match self {
Self::Lossless => 0,
Self::SubPerceptual => 10,
Self::NearLossless => 50,
Self::MaxBucket(bucket) => bucket.max_loss(),
}
}
}
#[derive(Clone, Debug)]
pub struct ConversionPath {
pub source_format: PixelDescriptor,
pub working_format: PixelDescriptor,
pub output_format: PixelDescriptor,
pub source_to_working: ConversionCost,
pub working_suitability: u16,
pub working_to_output: ConversionCost,
pub total_score: u32,
pub total_loss: u16,
pub proven_lossless: bool,
}
impl ConversionPath {
pub fn loss_bucket(&self) -> LossBucket {
LossBucket::from_model_loss(self.total_loss)
}
}
pub fn optimal_path(
source: PixelDescriptor,
provenance: Provenance,
operation: OpCategory,
output: PixelDescriptor,
threshold: QualityThreshold,
) -> Option<ConversionPath> {
let intent = operation.to_intent();
let candidates = operation.candidate_working_formats(source);
let max_loss = threshold.max_loss();
let mut best: Option<ConversionPath> = None;
for working in candidates {
let s2w = conversion_cost_with_provenance(source, working, provenance);
let suit = suitability_loss(working, intent);
let w2o = conversion_cost_with_provenance(
working,
output,
provenance_after_operation(provenance, working),
);
let total_loss = s2w.loss.saturating_add(suit).saturating_add(w2o.loss);
if total_loss > max_loss {
continue;
}
let total_effort = s2w.effort as u32 + w2o.effort as u32;
let total_score = weighted_score(total_effort, total_loss as u32 + suit as u32, intent);
let path = ConversionPath {
source_format: source,
working_format: working,
output_format: output,
source_to_working: s2w,
working_suitability: suit,
working_to_output: w2o,
total_score,
total_loss,
proven_lossless: false,
};
match &best {
Some(current) if path.total_score < current.total_score => best = Some(path),
None => best = Some(path),
_ => {}
}
}
best
}
fn provenance_after_operation(original: Provenance, working: PixelDescriptor) -> Provenance {
Provenance::with_origin(working.channel_type(), original.origin_primaries)
}
#[derive(Clone, Debug)]
pub struct PathEntry {
pub source_codec: &'static str,
pub source_format: PixelDescriptor,
pub source_effective_bits: u8,
pub operation: OpCategory,
pub output_codec: &'static str,
pub output_format: PixelDescriptor,
pub path: Option<ConversionPath>,
}
pub fn generate_path_matrix(
source_codecs: &[&CodecFormats],
operations: &[OpCategory],
output_codecs: &[&CodecFormats],
threshold: QualityThreshold,
) -> Vec<PathEntry> {
let mut entries = Vec::new();
for source_codec in source_codecs {
for source_entry in source_codec.decode_outputs {
let provenance = provenance_from_entry(source_entry);
for &operation in operations {
for output_codec in output_codecs {
for output_entry in output_codec.encode_inputs {
let path = optimal_path(
source_entry.descriptor,
provenance,
operation,
output_entry.descriptor,
threshold,
);
entries.push(PathEntry {
source_codec: source_codec.name,
source_format: source_entry.descriptor,
source_effective_bits: source_entry.effective_bits,
operation,
output_codec: output_codec.name,
output_format: output_entry.descriptor,
path,
});
}
}
}
}
}
entries
}
fn provenance_from_entry(entry: &FormatEntry) -> Provenance {
use crate::ChannelType;
let origin_depth = if entry.effective_bits <= 8 {
ChannelType::U8
} else if entry.effective_bits <= 16 {
ChannelType::U16
} else {
ChannelType::F32
};
Provenance::with_origin_depth(origin_depth)
}
#[derive(Clone, Debug, Default)]
pub struct MatrixStats {
pub total_triples: usize,
pub paths_found: usize,
pub no_path: usize,
pub by_bucket: [usize; 5],
pub distinct_working_formats: usize,
}
pub fn matrix_stats(entries: &[PathEntry]) -> MatrixStats {
use alloc::collections::BTreeSet;
let mut stats = MatrixStats {
total_triples: entries.len(),
..Default::default()
};
let mut working_formats = BTreeSet::new();
for entry in entries {
match &entry.path {
Some(path) => {
stats.paths_found += 1;
let bucket_idx = match path.loss_bucket() {
LossBucket::Lossless => 0,
LossBucket::NearLossless => 1,
LossBucket::LowLoss => 2,
LossBucket::Moderate => 3,
LossBucket::High => 4,
};
stats.by_bucket[bucket_idx] += 1;
let wf = path.working_format;
let alpha_byte = match wf.alpha() {
None => 0u8,
Some(a) => a as u8,
};
let key = (
wf.channel_type() as u8,
wf.layout() as u8,
alpha_byte,
wf.transfer() as u8,
wf.primaries as u8,
);
working_formats.insert(key);
}
None => stats.no_path += 1,
}
}
stats.distinct_working_formats = working_formats.len();
stats
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pipeline::registry;
use crate::{AlphaMode, ChannelType, TransferFunction};
#[test]
fn passthrough_identity_is_lossless() {
let src = PixelDescriptor::RGB8_SRGB;
let provenance = Provenance::from_source(src);
let path = optimal_path(
src,
provenance,
OpCategory::Passthrough,
src,
QualityThreshold::Lossless,
);
assert!(
path.is_some(),
"passthrough identity should always find a path"
);
let path = path.unwrap();
assert_eq!(path.working_format, src);
assert_eq!(path.total_loss, 0);
}
#[test]
fn resize_sharp_uses_f32_linear() {
let src = PixelDescriptor::RGB8_SRGB;
let provenance = Provenance::from_source(src);
let path = optimal_path(
src,
provenance,
OpCategory::ResizeSharp,
PixelDescriptor::RGB8_SRGB,
QualityThreshold::MaxBucket(LossBucket::Moderate),
);
assert!(path.is_some());
let path = path.unwrap();
assert_eq!(path.working_format.channel_type(), ChannelType::F32);
assert_eq!(path.working_format.transfer(), TransferFunction::Linear);
}
#[test]
fn jpeg_to_jpeg_passthrough() {
let src = PixelDescriptor::RGB8_SRGB;
let provenance = Provenance::with_origin_depth(ChannelType::U8);
let path = optimal_path(
src,
provenance,
OpCategory::Passthrough,
PixelDescriptor::RGB8_SRGB,
QualityThreshold::Lossless,
);
assert!(path.is_some());
assert_eq!(path.unwrap().total_loss, 0);
}
#[test]
fn composite_uses_premultiplied() {
let src = PixelDescriptor::RGBA8_SRGB;
let provenance = Provenance::from_source(src);
let path = optimal_path(
src,
provenance,
OpCategory::Composite,
PixelDescriptor::RGBA8_SRGB,
QualityThreshold::MaxBucket(LossBucket::Moderate),
);
assert!(path.is_some());
let path = path.unwrap();
assert_eq!(path.working_format.alpha(), Some(AlphaMode::Premultiplied));
}
#[test]
fn loss_bucket_classification() {
assert_eq!(LossBucket::from_model_loss(0), LossBucket::Lossless);
assert_eq!(LossBucket::from_model_loss(10), LossBucket::Lossless);
assert_eq!(LossBucket::from_model_loss(11), LossBucket::NearLossless);
assert_eq!(LossBucket::from_model_loss(50), LossBucket::NearLossless);
assert_eq!(LossBucket::from_model_loss(51), LossBucket::LowLoss);
assert_eq!(LossBucket::from_model_loss(150), LossBucket::LowLoss);
assert_eq!(LossBucket::from_model_loss(151), LossBucket::Moderate);
assert_eq!(LossBucket::from_model_loss(400), LossBucket::Moderate);
assert_eq!(LossBucket::from_model_loss(401), LossBucket::High);
}
#[test]
fn generate_jpeg_to_jpeg_matrix() {
let ops = [
OpCategory::Passthrough,
OpCategory::ResizeGentle,
OpCategory::ResizeSharp,
];
let matrix = generate_path_matrix(
&[®istry::JPEG],
&ops,
&[®istry::JPEG],
QualityThreshold::MaxBucket(LossBucket::Moderate),
);
assert!(!matrix.is_empty());
let stats = matrix_stats(&matrix);
assert!(stats.paths_found > 0, "should find at least some paths");
}
#[test]
fn full_matrix_produces_results() {
let all_ops = [
OpCategory::Passthrough,
OpCategory::ResizeGentle,
OpCategory::ResizeSharp,
];
let codecs: Vec<&CodecFormats> = registry::ALL_CODECS.to_vec();
let matrix = generate_path_matrix(
&codecs,
&all_ops,
&codecs,
QualityThreshold::MaxBucket(LossBucket::High),
);
let stats = matrix_stats(&matrix);
assert!(stats.total_triples > 100, "should have many triples");
assert!(stats.paths_found > 0, "should find paths");
assert!(
stats.paths_found as f64 / stats.total_triples as f64 > 0.5,
"most triples should have valid paths: {}/{}",
stats.paths_found,
stats.total_triples
);
}
#[test]
fn quality_threshold_filters_correctly() {
let src = PixelDescriptor::RGBF32_LINEAR;
let provenance = Provenance::with_origin_depth(ChannelType::F32);
let lossless_path = optimal_path(
src,
provenance,
OpCategory::Passthrough,
PixelDescriptor::RGB8_SRGB,
QualityThreshold::Lossless,
);
let relaxed_path = optimal_path(
src,
provenance,
OpCategory::Passthrough,
PixelDescriptor::RGB8_SRGB,
QualityThreshold::MaxBucket(LossBucket::Moderate),
);
assert!(lossless_path.is_none(), "f32→u8 should not be lossless");
assert!(
relaxed_path.is_some(),
"f32→u8 should work with relaxed threshold"
);
}
}