use crate::encode::tables::robidoux::{ROBIDOUX_LUMINANCE, quality_to_scale_factor, scale_table};
use crate::foundation::consts::DCT_BLOCK_SIZE;
use super::fingerprint::{EncoderFamily, generate_ijg_table};
use super::scanner::ScanResult;
#[derive(Debug, Clone)]
pub struct QualityEstimate {
pub value: f32,
pub scale: QualityScale,
pub confidence: Confidence,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum QualityScale {
IjgQuality,
MozjpegQuality,
ButteraugliDistance,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Confidence {
Exact,
Approximate,
}
pub(crate) fn estimate_quality(scan: &ScanResult, encoder: &EncoderFamily) -> QualityEstimate {
match encoder {
EncoderFamily::LibjpegTurbo | EncoderFamily::ImageMagick | EncoderFamily::IjgFamily => {
estimate_ijg_quality(scan)
}
EncoderFamily::Mozjpeg => estimate_mozjpeg_quality(scan),
EncoderFamily::CjpegliYcbcr | EncoderFamily::CjpegliXyb => estimate_jpegli_quality(scan),
EncoderFamily::Photoshop | EncoderFamily::Unknown => estimate_ijg_quality_approximate(scan),
}
}
fn estimate_ijg_quality(scan: &ScanResult) -> QualityEstimate {
let luma = match &scan.dqt_tables[0] {
Some(t) => &t.values,
None => {
return QualityEstimate {
value: 75.0,
scale: QualityScale::IjgQuality,
confidence: Confidence::Approximate,
};
}
};
let chroma = scan.dqt_tables[1].as_ref().map(|t| &t.values);
for q in 1..=100u8 {
let ref_luma = generate_ijg_table(q, false);
if *luma == ref_luma {
if let Some(chroma_vals) = chroma {
let ref_chroma = generate_ijg_table(q, true);
if *chroma_vals == ref_chroma {
return QualityEstimate {
value: q as f32,
scale: QualityScale::IjgQuality,
confidence: Confidence::Exact,
};
}
} else {
return QualityEstimate {
value: q as f32,
scale: QualityScale::IjgQuality,
confidence: Confidence::Exact,
};
}
}
}
find_closest_ijg_quality(luma)
}
fn find_closest_ijg_quality(luma: &[u16; 64]) -> QualityEstimate {
let mut best_q = 75u8;
let mut best_sse = u64::MAX;
for q in 1..=100u8 {
let ref_table = generate_ijg_table(q, false);
let sse = compute_sse(luma, &ref_table);
if sse < best_sse {
best_sse = sse;
best_q = q;
}
}
QualityEstimate {
value: best_q as f32,
scale: QualityScale::IjgQuality,
confidence: Confidence::Approximate,
}
}
fn estimate_ijg_quality_approximate(scan: &ScanResult) -> QualityEstimate {
let luma = match &scan.dqt_tables[0] {
Some(t) => &t.values,
None => {
return QualityEstimate {
value: 75.0,
scale: QualityScale::IjgQuality,
confidence: Confidence::Approximate,
};
}
};
find_closest_ijg_quality(luma)
}
fn estimate_mozjpeg_quality(scan: &ScanResult) -> QualityEstimate {
let table = match &scan.dqt_tables[0] {
Some(t) => &t.values,
None => {
return QualityEstimate {
value: 75.0,
scale: QualityScale::MozjpegQuality,
confidence: Confidence::Approximate,
};
}
};
for q in 1..=100u8 {
let scale = quality_to_scale_factor(q);
let ref_table = scale_table(&ROBIDOUX_LUMINANCE, scale, true);
if *table == ref_table {
return QualityEstimate {
value: q as f32,
scale: QualityScale::MozjpegQuality,
confidence: Confidence::Exact,
};
}
}
let mut best_q = 75u8;
let mut best_sse = u64::MAX;
for q in 1..=100u8 {
let scale = quality_to_scale_factor(q);
let ref_table = scale_table(&ROBIDOUX_LUMINANCE, scale, true);
let sse = compute_sse(table, &ref_table);
if sse < best_sse {
best_sse = sse;
best_q = q;
}
}
QualityEstimate {
value: best_q as f32,
scale: QualityScale::MozjpegQuality,
confidence: Confidence::Approximate,
}
}
fn estimate_jpegli_quality(scan: &ScanResult) -> QualityEstimate {
let y = match &scan.dqt_tables[0] {
Some(t) => t,
None => {
return QualityEstimate {
value: 1.0,
scale: QualityScale::ButteraugliDistance,
confidence: Confidence::Approximate,
};
}
};
let cb = match &scan.dqt_tables[1] {
Some(t) => t,
None => {
return QualityEstimate {
value: 1.0,
scale: QualityScale::ButteraugliDistance,
confidence: Confidence::Approximate,
};
}
};
let cr = match &scan.dqt_tables[2] {
Some(t) => t,
None => {
return QualityEstimate {
value: 1.0,
scale: QualityScale::ButteraugliDistance,
confidence: Confidence::Approximate,
};
}
};
let y_qt = natural_to_quant_table(&y.values, y.precision);
let cb_qt = natural_to_quant_table(&cb.values, cb.precision);
let cr_qt = natural_to_quant_table(&cr.values, cr.precision);
let distance = crate::quant::quant_vals_to_distance(&y_qt, &cb_qt, &cr_qt);
QualityEstimate {
value: distance,
scale: QualityScale::ButteraugliDistance,
confidence: Confidence::Exact,
}
}
fn natural_to_quant_table(natural: &[u16; 64], precision: u8) -> crate::types::QuantTable {
use crate::foundation::consts::JPEG_ZIGZAG_ORDER;
let mut zigzag = [0u16; DCT_BLOCK_SIZE];
for natural_idx in 0..64 {
let zigzag_idx = JPEG_ZIGZAG_ORDER[natural_idx] as usize;
zigzag[zigzag_idx] = natural[natural_idx];
}
crate::types::QuantTable {
values: zigzag,
precision,
}
}
fn compute_sse(a: &[u16; 64], b: &[u16; 64]) -> u64 {
let mut sse = 0u64;
for i in 0..64 {
let diff = a[i] as i64 - b[i] as i64;
sse += (diff * diff) as u64;
}
sse
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ijg_quality_roundtrip() {
for q in [10, 25, 50, 75, 85, 90, 95, 100] {
let luma = generate_ijg_table(q, false);
let chroma = generate_ijg_table(q, true);
let scan = mock_scan_two_tables(&luma, &chroma);
let estimate = estimate_ijg_quality(&scan);
assert_eq!(
estimate.value, q as f32,
"IJG Q{q} roundtrip failed: got {}",
estimate.value
);
assert_eq!(estimate.confidence, Confidence::Exact);
}
}
#[test]
fn test_mozjpeg_quality_roundtrip() {
for q in [10, 25, 50, 75, 85, 90, 95, 100] {
let scale = quality_to_scale_factor(q);
let table = scale_table(&ROBIDOUX_LUMINANCE, scale, true);
let scan = mock_scan_identical_tables(&table);
let estimate = estimate_mozjpeg_quality(&scan);
assert_eq!(
estimate.value, q as f32,
"Mozjpeg Q{q} roundtrip failed: got {}",
estimate.value
);
assert_eq!(estimate.confidence, Confidence::Exact);
}
}
#[test]
fn test_approximate_for_non_ijg_table() {
let mut table = generate_ijg_table(75, false);
table[0] += 1;
let scan = mock_scan_two_tables(&table, &generate_ijg_table(75, true));
let estimate = estimate_ijg_quality(&scan);
assert_eq!(estimate.confidence, Confidence::Approximate);
assert!((estimate.value - 75.0).abs() < 5.0);
}
fn mock_scan_two_tables(luma: &[u16; 64], chroma: &[u16; 64]) -> ScanResult {
use super::super::scanner::{DqtEntry, ScanResult};
ScanResult {
dqt_tables: [
Some(DqtEntry {
values: *luma,
precision: 0,
}),
Some(DqtEntry {
values: *chroma,
precision: 0,
}),
None,
None,
],
sof: None,
total_ac_symbols: 0,
dht_count: 0,
has_jfif: false,
has_icc_profile: false,
has_adobe: false,
has_photoshop_iptc: false,
sos_count: 0,
}
}
fn mock_scan_identical_tables(table: &[u16; 64]) -> ScanResult {
mock_scan_two_tables(table, table)
}
}