use jpegli_bench_utils::{
decode_jpeg_to_rgb, ChromaSubsampling, ColorMode, EncoderConfig, EncoderImpl, ImageData,
QualityMetrics,
};
use std::env;
use std::fs::File;
use std::io::Write;
use std::path::Path;
use std::time::Instant;
#[derive(Debug, Clone)]
#[allow(dead_code)] struct Config {
encoders: Vec<EncoderConfig>,
qualities: Vec<u8>,
metrics: Vec<Metric>,
output_csv: Option<String>,
pareto: bool,
same_size: bool,
image_path: String,
}
#[derive(Debug, Clone, Copy)]
enum Metric {
Dssim,
Ssim2,
Butteraugli,
}
impl Metric {
fn name(&self) -> &'static str {
match self {
Metric::Dssim => "DSSIM",
Metric::Ssim2 => "SSIM2",
Metric::Butteraugli => "Butteraugli",
}
}
}
#[derive(Debug)]
struct Result {
encoder: String,
quality: u8,
bytes: usize,
bpp: f64,
dssim: Option<f64>,
ssim2: Option<f64>,
butteraugli: Option<f64>,
encode_ms: f64,
}
fn parse_args() -> Config {
let args: Vec<String> = env::args().collect();
let mut encoders = Vec::new();
let mut qualities = Vec::new();
let mut metrics = Vec::new();
let mut output_csv = None;
let mut pareto = false;
let mut same_size = false;
let mut quick = false;
let mut image_path = None;
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"--encoder" => {
i += 1;
if i < args.len() {
encoders.push(parse_encoder(&args[i]));
}
}
"--quality" => {
i += 1;
if i < args.len() {
qualities.push(args[i].parse().expect("invalid quality"));
}
}
"--metric" => {
i += 1;
if i < args.len() {
match args[i].as_str() {
"dssim" => metrics.push(Metric::Dssim),
"ssim2" => metrics.push(Metric::Ssim2),
"butteraugli" => metrics.push(Metric::Butteraugli),
"all" => {
metrics.extend([Metric::Dssim, Metric::Ssim2, Metric::Butteraugli])
}
_ => eprintln!("Unknown metric: {}", args[i]),
}
}
}
"--output" => {
i += 1;
if i < args.len() {
output_csv = Some(args[i].clone());
}
}
"--pareto" => pareto = true,
"--same-size" => same_size = true,
"--quick" => quick = true,
arg if !arg.starts_with('-') => {
image_path = Some(arg.to_string());
}
_ => eprintln!("Unknown argument: {}", args[i]),
}
i += 1;
}
if encoders.is_empty() {
encoders.push(EncoderConfig::new(EncoderImpl::JpegliRs).color(ColorMode::YCbCr));
encoders.push(EncoderConfig::new(EncoderImpl::JpegliRs).color(ColorMode::Xyb));
encoders.push(EncoderConfig::new(EncoderImpl::CJpegli).color(ColorMode::YCbCr));
}
if qualities.is_empty() {
if quick {
qualities = vec![10, 30, 50, 70, 90];
} else if pareto {
qualities = (10..=95).step_by(5).collect();
} else {
qualities = vec![75];
}
}
if metrics.is_empty() {
metrics = vec![Metric::Dssim, Metric::Ssim2, Metric::Butteraugli];
}
let image_path = image_path.unwrap_or_else(|| {
eprintln!("Usage: quality_compare [OPTIONS] <image.png>");
eprintln!(" --encoder <name> jpegli-rs-ycbcr, jpegli-rs-xyb, cjpegli");
eprintln!(" --quality <n> Quality level 0-100");
eprintln!(" --metric <name> dssim, ssim2, butteraugli, all");
eprintln!(" --output <file> CSV output file");
eprintln!(" --pareto Sweep quality levels for Pareto curve");
eprintln!(" --quick Quick mode (5 quality levels)");
std::process::exit(1);
});
Config {
encoders,
qualities,
metrics,
output_csv,
pareto,
same_size,
image_path,
}
}
fn parse_encoder(name: &str) -> EncoderConfig {
match name {
"jpegli-rs-ycbcr" | "jpegli-ycbcr" | "jpegli" => {
EncoderConfig::new(EncoderImpl::JpegliRs).color(ColorMode::YCbCr)
}
"jpegli-rs-ycbcr-hybrid" | "jpegli-ycbcr-hybrid" | "jpegli-hybrid" | "hybrid" => {
EncoderConfig::new(EncoderImpl::JpegliRs)
.color(ColorMode::YCbCr)
.hybrid(true)
}
"jpegli-rs-xyb" | "jpegli-xyb" | "xyb" => {
EncoderConfig::new(EncoderImpl::JpegliRs).color(ColorMode::Xyb)
}
"cjpegli" => EncoderConfig::new(EncoderImpl::CJpegli).color(ColorMode::YCbCr),
"jpegli-rs-ycbcr-444" => EncoderConfig::new(EncoderImpl::JpegliRs)
.color(ColorMode::YCbCr)
.subsampling(ChromaSubsampling::S444),
"jpegli-rs-ycbcr-hybrid-444" => EncoderConfig::new(EncoderImpl::JpegliRs)
.color(ColorMode::YCbCr)
.subsampling(ChromaSubsampling::S444)
.hybrid(true),
"jpegli-rs-xyb-444" => EncoderConfig::new(EncoderImpl::JpegliRs)
.color(ColorMode::Xyb)
.subsampling(ChromaSubsampling::S444),
_ => {
eprintln!("Unknown encoder: {}. Using jpegli-rs-ycbcr", name);
EncoderConfig::new(EncoderImpl::JpegliRs).color(ColorMode::YCbCr)
}
}
}
fn run_comparison(config: &Config) -> Vec<Result> {
let img = ImageData::from_png(Path::new(&config.image_path)).expect("Failed to load PNG");
let orig_rgb = img.as_rgb_image();
let pixels = img.pixel_count();
println!(
"Image: {} ({}x{}, {:.2} MP)",
config.image_path,
img.width,
img.height,
pixels as f64 / 1_000_000.0
);
println!();
print!("{:<25} {:>5} {:>10} {:>8}", "Encoder", "Q", "Size", "BPP");
for metric in &config.metrics {
print!(" {:>12}", metric.name());
}
println!(" {:>8}", "Time");
println!("{}", "-".repeat(80));
let mut results = Vec::new();
for encoder in &config.encoders {
for &quality in &config.qualities {
let mut enc = encoder.clone();
enc.quality = quality;
let start = Instant::now();
let jpeg_data: Vec<u8> = match enc.encode(&img) {
Ok(data) => data,
Err(e) => {
eprintln!(" {} q{}: {}", enc.short_name(), quality, e);
continue;
}
};
let encode_ms = start.elapsed().as_secs_f64() * 1000.0;
let bpp = jpeg_data.len() as f64 * 8.0 / pixels as f64;
let decoded = decode_jpeg_to_rgb(&jpeg_data).expect("Failed to decode JPEG");
let mut dssim = None;
let mut ssim2 = None;
let mut butteraugli = None;
for metric in &config.metrics {
match metric {
Metric::Dssim => {
dssim = Some(QualityMetrics::dssim(orig_rgb.as_ref(), decoded.as_ref()));
}
Metric::Ssim2 => {
ssim2 = Some(QualityMetrics::ssimulacra2(
orig_rgb.as_ref(),
decoded.as_ref(),
));
}
Metric::Butteraugli => {
butteraugli = Some(QualityMetrics::butteraugli(
orig_rgb.as_ref(),
decoded.as_ref(),
));
}
}
}
print!(
"{:<25} {:>5} {:>10} {:>8.3}",
enc.short_name(),
quality,
format_size(jpeg_data.len()),
bpp
);
for metric in &config.metrics {
let value = match metric {
Metric::Dssim => dssim.map(|v| format!("{:.6}", v)),
Metric::Ssim2 => ssim2.map(|v| format!("{:.2}", v)),
Metric::Butteraugli => butteraugli.map(|v| format!("{:.4}", v)),
};
print!(" {:>12}", value.unwrap_or_default());
}
println!(" {:>7.1}ms", encode_ms);
results.push(Result {
encoder: enc.short_name(),
quality,
bytes: jpeg_data.len(),
bpp,
dssim,
ssim2,
butteraugli,
encode_ms,
});
}
}
results
}
fn write_csv(results: &[Result], path: &str, config: &Config) -> std::io::Result<()> {
let mut f = File::create(path)?;
write!(f, "encoder,quality,bytes,bpp")?;
for metric in &config.metrics {
write!(f, ",{}", metric.name().to_lowercase())?;
}
writeln!(f, ",encode_ms")?;
for r in results {
write!(f, "{},{},{},{:.4}", r.encoder, r.quality, r.bytes, r.bpp)?;
for metric in &config.metrics {
let value = match metric {
Metric::Dssim => r.dssim,
Metric::Ssim2 => r.ssim2,
Metric::Butteraugli => r.butteraugli,
};
if let Some(v) = value {
write!(f, ",{:.6}", v)?;
} else {
write!(f, ",")?;
}
}
writeln!(f, ",{:.2}", r.encode_ms)?;
}
Ok(())
}
fn format_size(bytes: usize) -> String {
if bytes >= 1_000_000 {
format!("{:.2} MB", bytes as f64 / 1_000_000.0)
} else if bytes >= 1_000 {
format!("{:.1} KB", bytes as f64 / 1_000.0)
} else {
format!("{} B", bytes)
}
}
fn main() {
let config = parse_args();
let results = run_comparison(&config);
if let Some(csv_path) = &config.output_csv {
if let Err(e) = write_csv(&results, csv_path, &config) {
eprintln!("Failed to write CSV: {}", e);
} else {
println!("\nResults written to {}", csv_path);
}
}
if results.len() > 1 {
println!("\n--- Summary ---");
let encoders: Vec<String> = config
.encoders
.iter()
.map(|e: &EncoderConfig| e.short_name())
.collect();
for encoder in &encoders {
let encoder_results: Vec<_> =
results.iter().filter(|r| &r.encoder == encoder).collect();
if encoder_results.is_empty() {
continue;
}
let avg_bpp: f64 =
encoder_results.iter().map(|r| r.bpp).sum::<f64>() / encoder_results.len() as f64;
let avg_ssim2 = if config.metrics.iter().any(|m| matches!(m, Metric::Ssim2)) {
let sum: f64 = encoder_results.iter().filter_map(|r| r.ssim2).sum();
let count = encoder_results.iter().filter(|r| r.ssim2.is_some()).count();
if count > 0 {
Some(sum / count as f64)
} else {
None
}
} else {
None
};
print!("{}: avg bpp={:.3}", encoder, avg_bpp);
if let Some(s) = avg_ssim2 {
print!(", avg SSIM2={:.2}", s);
}
println!();
}
}
}