use crate::foundation::consts::MARKER_SOF0;
use crate::quant::{STD_CHROMINANCE_QUANT, STD_LUMINANCE_QUANT};
use super::scanner::ScanResult;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum EncoderFamily {
LibjpegTurbo,
ImageMagick,
IjgFamily,
Mozjpeg,
CjpegliYcbcr,
CjpegliXyb,
Photoshop,
Unknown,
}
const STANDARD_AC_SYMBOLS_TOTAL: u16 = 324;
pub(crate) fn identify_encoder(scan: &ScanResult) -> EncoderFamily {
let sof = match &scan.sof {
Some(s) => s,
None => return EncoderFamily::Unknown,
};
let dqt_count = count_dqt_tables(scan);
match dqt_count {
3 => identify_three_table(scan, sof),
2 => identify_two_table(scan, sof),
1 => identify_single_table(scan, sof),
_ => EncoderFamily::Unknown,
}
}
fn count_dqt_tables(scan: &ScanResult) -> u8 {
scan.dqt_tables.iter().filter(|t| t.is_some()).count() as u8
}
fn identify_three_table(scan: &ScanResult, sof: &super::scanner::SofInfo) -> EncoderFamily {
if sof.num_components == 3 {
let ids: Vec<u8> = sof.components.iter().map(|c| c.0).collect();
if ids == [82, 71, 66] && scan.has_icc_profile {
return EncoderFamily::CjpegliXyb;
}
if !scan.has_jfif {
return EncoderFamily::CjpegliYcbcr;
}
}
EncoderFamily::Unknown
}
fn identify_two_table(scan: &ScanResult, sof: &super::scanner::SofInfo) -> EncoderFamily {
let table0 = match &scan.dqt_tables[0] {
Some(t) => t,
None => return EncoderFamily::Unknown,
};
let table1 = match &scan.dqt_tables[1] {
Some(t) => t,
None => return EncoderFamily::Unknown,
};
if table0.values == table1.values {
let is_progressive = sof.marker != MARKER_SOF0;
if is_progressive && scan.sos_count >= 4 {
return EncoderFamily::Mozjpeg;
}
if is_progressive {
return EncoderFamily::Mozjpeg;
}
}
if matches_ijg_tables(&table0.values, &table1.values) {
return identify_ijg_variant(scan, sof);
}
if scan.has_adobe && scan.has_photoshop_iptc {
return EncoderFamily::Photoshop;
}
EncoderFamily::Unknown
}
fn identify_single_table(scan: &ScanResult, sof: &super::scanner::SofInfo) -> EncoderFamily {
if sof.num_components != 1 {
return EncoderFamily::Unknown;
}
let table0 = match &scan.dqt_tables[0] {
Some(t) => t,
None => return EncoderFamily::Unknown,
};
if matches_ijg_luma_table(&table0.values) {
return identify_ijg_variant(scan, sof);
}
EncoderFamily::Unknown
}
fn matches_ijg_tables(luma: &[u16; 64], chroma: &[u16; 64]) -> bool {
for q in 1..=100u8 {
let ref_luma = generate_ijg_table(q, false);
let ref_chroma = generate_ijg_table(q, true);
if *luma == ref_luma && *chroma == ref_chroma {
return true;
}
}
false
}
fn matches_ijg_luma_table(table: &[u16; 64]) -> bool {
for q in 1..=100u8 {
let ref_table = generate_ijg_table(q, false);
if *table == ref_table {
return true;
}
}
false
}
fn identify_ijg_variant(scan: &ScanResult, _sof: &super::scanner::SofInfo) -> EncoderFamily {
let uses_standard_huffman = scan.total_ac_symbols == STANDARD_AC_SYMBOLS_TOTAL;
if uses_standard_huffman {
return EncoderFamily::LibjpegTurbo;
}
if scan.dht_count > 0 {
return EncoderFamily::ImageMagick;
}
EncoderFamily::IjgFamily
}
pub(crate) fn generate_ijg_table(quality: u8, is_chrominance: bool) -> [u16; 64] {
let q = quality.clamp(1, 100) as u32;
let scale = if q < 50 { 5000 / q } else { 200 - q * 2 };
let base = if is_chrominance {
&STD_CHROMINANCE_QUANT
} else {
&STD_LUMINANCE_QUANT
};
let mut result = [0u16; 64];
for i in 0..64 {
let temp = (base[i] as u32 * scale + 50) / 100;
let clamped = temp.clamp(1, 255) as u16;
result[i] = clamped;
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_ijg_table_q50_identity() {
let table = generate_ijg_table(50, false);
assert_eq!(table, STD_LUMINANCE_QUANT);
}
#[test]
fn test_generate_ijg_table_q100_all_ones() {
let table = generate_ijg_table(100, false);
for &v in &table {
assert_eq!(v, 1);
}
}
#[test]
fn test_generate_ijg_table_q75() {
let table = generate_ijg_table(75, false);
assert_eq!(table[0], 8);
assert_eq!(table[1], 6);
}
#[test]
fn test_generate_ijg_uses_integer_arithmetic() {
let table = generate_ijg_table(75, false);
assert_eq!(
table[0], 8,
"Must use integer arithmetic, not float rounding"
);
}
#[test]
fn test_robidoux_luma_chroma_identical() {
use crate::encode::tables::robidoux::{ROBIDOUX_CHROMINANCE, ROBIDOUX_LUMINANCE};
assert_eq!(ROBIDOUX_LUMINANCE, ROBIDOUX_CHROMINANCE);
}
}