#![allow(dead_code)]
use crate::quant::Quality;
use crate::types::Subsampling;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum QualityComparisonMetric {
#[default]
Dssim,
Ssimulacra2,
Butteraugli,
}
#[derive(Debug, Clone, Copy)]
pub struct QualityConversion {
pub source_quality: u8,
pub subsampling: Subsampling,
pub metric: QualityComparisonMetric,
pub is_interpolated: bool,
}
impl QualityConversion {
#[must_use]
pub fn try_mozjpeg_equivalent(
quality: u8,
subsampling: Subsampling,
metric: QualityComparisonMetric,
) -> Option<Self> {
if quality >= 100 {
return Some(Self {
source_quality: 100,
subsampling,
metric,
is_interpolated: false,
});
}
let table = get_mapping_table(subsampling, metric)?;
if table.iter().any(|&(moz_q, _)| moz_q == quality) {
Some(Self {
source_quality: quality,
subsampling,
metric,
is_interpolated: false,
})
} else {
None
}
}
#[must_use]
pub fn mozjpeg_equivalent(
quality: u8,
subsampling: Subsampling,
metric: QualityComparisonMetric,
) -> Self {
if quality >= 100 {
return Self {
source_quality: 100,
subsampling,
metric,
is_interpolated: false,
};
}
let mapped_subsampling = match subsampling {
Subsampling::S444 => Subsampling::S444,
Subsampling::S422 | Subsampling::S420 | Subsampling::S440 => Subsampling::S420,
};
let table = get_mapping_table(mapped_subsampling, metric);
let is_interpolated = match table {
Some(t) => !t.iter().any(|&(moz_q, _)| moz_q == quality),
None => true,
};
Self {
source_quality: quality,
subsampling: mapped_subsampling,
metric,
is_interpolated,
}
}
#[must_use]
pub fn to_jpegli_quality(self) -> Quality {
if self.source_quality >= 100 {
return Quality::ApproxJpegli(100.0);
}
let table = match get_mapping_table(self.subsampling, self.metric) {
Some(t) => t,
None => {
return Quality::ApproxJpegli(self.source_quality as f32);
}
};
for &(moz_q, jpegli_q) in table {
if moz_q == self.source_quality {
return Quality::ApproxJpegli(jpegli_q as f32);
}
}
interpolate_quality(self.source_quality, table)
}
}
fn interpolate_quality(moz_q: u8, table: &[(u8, u8)]) -> Quality {
let mut lower: Option<(u8, u8)> = None;
let mut upper: Option<(u8, u8)> = None;
for &(tbl_moz_q, tbl_jpegli_q) in table {
if tbl_moz_q <= moz_q {
match lower {
None => lower = Some((tbl_moz_q, tbl_jpegli_q)),
Some((prev_q, _)) if tbl_moz_q > prev_q => lower = Some((tbl_moz_q, tbl_jpegli_q)),
_ => {}
}
}
if tbl_moz_q >= moz_q {
match upper {
None => upper = Some((tbl_moz_q, tbl_jpegli_q)),
Some((prev_q, _)) if tbl_moz_q < prev_q => upper = Some((tbl_moz_q, tbl_jpegli_q)),
_ => {}
}
}
}
match (lower, upper) {
(Some((l_moz, l_jpegli)), Some((u_moz, u_jpegli))) if l_moz != u_moz => {
let t = (moz_q - l_moz) as f32 / (u_moz - l_moz) as f32;
let jpegli_q = l_jpegli as f32 + t * (u_jpegli as f32 - l_jpegli as f32);
Quality::ApproxJpegli(jpegli_q)
}
(Some((_, jpegli_q)), _) => Quality::ApproxJpegli(jpegli_q as f32),
(_, Some((_, jpegli_q))) => Quality::ApproxJpegli(jpegli_q as f32),
(None, None) => {
Quality::ApproxJpegli(moz_q as f32)
}
}
}
fn get_mapping_table(
subsampling: Subsampling,
metric: QualityComparisonMetric,
) -> Option<&'static [(u8, u8)]> {
match (subsampling, metric) {
(Subsampling::S444, QualityComparisonMetric::Dssim) => Some(&MOZJPEG_TO_JPEGLI_444_DSSIM),
(Subsampling::S420, QualityComparisonMetric::Dssim) => Some(&MOZJPEG_TO_JPEGLI_420_DSSIM),
(Subsampling::S444, QualityComparisonMetric::Ssimulacra2) => {
Some(&MOZJPEG_TO_JPEGLI_444_SSIMULACRA2)
}
(Subsampling::S420, QualityComparisonMetric::Ssimulacra2) => {
Some(&MOZJPEG_TO_JPEGLI_420_SSIMULACRA2)
}
(Subsampling::S444, QualityComparisonMetric::Butteraugli) => {
Some(&MOZJPEG_TO_JPEGLI_444_BUTTERAUGLI)
}
(Subsampling::S420, QualityComparisonMetric::Butteraugli) => {
Some(&MOZJPEG_TO_JPEGLI_420_BUTTERAUGLI)
}
_ => None,
}
}
static MOZJPEG_TO_JPEGLI_444_DSSIM: [(u8, u8); 10] = [
(30, 28),
(40, 37),
(50, 47),
(60, 55),
(70, 65),
(75, 71),
(80, 77),
(85, 83),
(90, 89),
(95, 94),
];
static MOZJPEG_TO_JPEGLI_420_DSSIM: [(u8, u8); 10] = [
(30, 27),
(40, 36),
(50, 45),
(60, 54),
(70, 64),
(75, 70),
(80, 76),
(85, 82),
(90, 88),
(95, 94),
];
static MOZJPEG_TO_JPEGLI_444_SSIMULACRA2: [(u8, u8); 10] = [
(30, 29),
(40, 38),
(50, 48),
(60, 56),
(70, 66),
(75, 72),
(80, 78),
(85, 84),
(90, 89),
(95, 94),
];
static MOZJPEG_TO_JPEGLI_420_SSIMULACRA2: [(u8, u8); 10] = [
(30, 28),
(40, 37),
(50, 46),
(60, 55),
(70, 65),
(75, 71),
(80, 77),
(85, 83),
(90, 89),
(95, 94),
];
static MOZJPEG_TO_JPEGLI_444_BUTTERAUGLI: [(u8, u8); 10] = [
(30, 30),
(40, 39),
(50, 49),
(60, 57),
(70, 67),
(75, 73),
(80, 79),
(85, 85),
(90, 90),
(95, 95),
];
static MOZJPEG_TO_JPEGLI_420_BUTTERAUGLI: [(u8, u8); 10] = [
(30, 29),
(40, 38),
(50, 48),
(60, 56),
(70, 66),
(75, 72),
(80, 78),
(85, 84),
(90, 90),
(95, 95),
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_q100_passthrough() {
let conv = QualityConversion::mozjpeg_equivalent(
100,
Subsampling::S444,
QualityComparisonMetric::Dssim,
);
let q = conv.to_jpegli_quality();
assert_eq!(q.to_internal(), 100.0);
}
#[test]
fn test_try_mozjpeg_equivalent_exact() {
let conv = QualityConversion::try_mozjpeg_equivalent(
90,
Subsampling::S444,
QualityComparisonMetric::Dssim,
);
assert!(conv.is_some());
let conv = conv.unwrap();
assert!(!conv.is_interpolated);
let q = conv.to_jpegli_quality();
assert_eq!(q.to_internal(), 89.0);
}
#[test]
fn test_try_mozjpeg_equivalent_missing() {
let conv = QualityConversion::try_mozjpeg_equivalent(
87,
Subsampling::S444,
QualityComparisonMetric::Dssim,
);
assert!(conv.is_none());
}
#[test]
fn test_mozjpeg_equivalent_interpolation() {
let conv = QualityConversion::mozjpeg_equivalent(
87,
Subsampling::S444,
QualityComparisonMetric::Dssim,
);
assert!(conv.is_interpolated);
let q = conv.to_jpegli_quality();
let expected = 85.4;
assert!(
(q.to_internal() - expected).abs() < 0.5,
"Expected ~{}, got {}",
expected,
q.to_internal()
);
}
#[test]
fn test_subsampling_fallback() {
let conv = QualityConversion::mozjpeg_equivalent(
90,
Subsampling::S422,
QualityComparisonMetric::Dssim,
);
assert_eq!(conv.subsampling, Subsampling::S420);
}
#[test]
fn test_all_metrics() {
for metric in [
QualityComparisonMetric::Dssim,
QualityComparisonMetric::Ssimulacra2,
QualityComparisonMetric::Butteraugli,
] {
let conv = QualityConversion::mozjpeg_equivalent(85, Subsampling::S444, metric);
let q = conv.to_jpegli_quality();
assert!(q.to_internal() >= 80.0 && q.to_internal() <= 90.0);
}
}
#[test]
fn test_low_quality() {
let conv = QualityConversion::mozjpeg_equivalent(
20,
Subsampling::S444,
QualityComparisonMetric::Dssim,
);
let q = conv.to_jpegli_quality();
assert!(q.to_internal() <= 30.0);
}
}