use jpegli::encoder::ChromaSubsampling;
use jpegli::encoder::{EncoderConfig, PixelLayout};
use std::io::Cursor;
use std::process::Command;
fn encode_rgb(
width: u32,
height: u32,
data: &[u8],
config: &EncoderConfig,
) -> jpegli::encoder::Result<Vec<u8>> {
let mut enc = config.encode_from_bytes(width, height, PixelLayout::Rgb8Srgb)?;
enc.push_packed(data, enough::Unstoppable)?;
enc.finish()
}
fn encode_gray(
width: u32,
height: u32,
data: &[u8],
config: &EncoderConfig,
) -> jpegli::encoder::Result<Vec<u8>> {
let mut enc = config.encode_from_bytes(width, height, PixelLayout::Gray8Srgb)?;
enc.push_packed(data, enough::Unstoppable)?;
enc.finish()
}
fn decode_with_zune(jpeg_data: &[u8]) -> Result<Vec<u8>, String> {
let cursor = Cursor::new(jpeg_data);
zune_jpeg::JpegDecoder::new(cursor)
.decode()
.map_err(|e| format!("{:?}", e))
}
#[test]
fn test_progressive_grayscale_gradient() {
let width = 16u32;
let height = 16u32;
let mut data = Vec::with_capacity((width * height) as usize);
for _y in 0..height {
for x in 0..width {
data.push((x * 16) as u8);
}
}
let config = EncoderConfig::grayscale(90.0).progressive(true);
let jpeg_data =
encode_gray(width, height, &data, &config).expect("Progressive encoding should succeed");
assert!(jpeg_data.len() > 100, "JPEG should be at least 100 bytes");
assert_eq!(jpeg_data[0], 0xFF, "Should start with FF");
assert_eq!(jpeg_data[1], 0xD8, "Should have SOI marker");
let mut found_sof2 = false;
for i in 0..jpeg_data.len() - 1 {
if jpeg_data[i] == 0xFF && jpeg_data[i + 1] == 0xC2 {
found_sof2 = true;
break;
}
}
assert!(found_sof2, "Progressive JPEG should have SOF2 marker");
assert_eq!(jpeg_data[jpeg_data.len() - 2], 0xFF);
assert_eq!(jpeg_data[jpeg_data.len() - 1], 0xD9);
if let Ok(output) = Command::new("djpeg")
.args(["-outfile", "/dev/null"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.spawn()
{
let _ = output.wait_with_output();
}
}
#[test]
fn test_progressive_solid_gray() {
let width = 16u32;
let height = 16u32;
let data = vec![128u8; (width * height) as usize];
let config = EncoderConfig::grayscale(90.0).progressive(true);
let jpeg_data =
encode_gray(width, height, &data, &config).expect("Progressive encoding should succeed");
assert!(jpeg_data.len() > 50);
assert_eq!(&jpeg_data[0..2], &[0xFF, 0xD8]); assert_eq!(&jpeg_data[jpeg_data.len() - 2..], &[0xFF, 0xD9]); }
#[test]
fn test_progressive_rgb() {
let width = 16u32;
let height = 16u32;
let mut data = Vec::with_capacity((width * height * 3) as usize);
for y in 0..height {
for x in 0..width {
data.push((x * 16) as u8); data.push((y * 16) as u8); data.push(128); }
}
let config = EncoderConfig::ycbcr(90.0, ChromaSubsampling::Quarter).progressive(true);
let jpeg_data =
encode_rgb(width, height, &data, &config).expect("Progressive RGB encoding should succeed");
let mut found_sof2 = false;
for i in 0..jpeg_data.len() - 1 {
if jpeg_data[i] == 0xFF && jpeg_data[i + 1] == 0xC2 {
found_sof2 = true;
break;
}
}
assert!(found_sof2, "Progressive JPEG should have SOF2 marker");
}
#[test]
fn test_progressive_has_multiple_scans() {
let width = 32u32;
let height = 32u32;
let mut data = Vec::with_capacity((width * height) as usize);
for y in 0..height {
for x in 0..width {
data.push(((x + y) * 4) as u8);
}
}
let config = EncoderConfig::grayscale(85.0).progressive(true);
let jpeg_data = encode_gray(width, height, &data, &config).expect("Encoding should succeed");
let mut sos_count = 0;
for i in 0..jpeg_data.len() - 1 {
if jpeg_data[i] == 0xFF && jpeg_data[i + 1] == 0xDA {
sos_count += 1;
}
}
assert!(
sos_count >= 2,
"Progressive JPEG should have at least 2 scans, found {}",
sos_count
);
}
#[test]
fn test_progressive_optimized_smaller() {
let width = 64u32;
let height = 64u32;
let mut data = Vec::with_capacity((width * height * 3) as usize);
for y in 0..height {
for x in 0..width {
let val = ((x * 13 + y * 17) % 256) as u8;
data.push(val); data.push(255 - val); data.push((val / 2) + 64); }
}
let config_no_opt = EncoderConfig::ycbcr(90.0, ChromaSubsampling::Quarter)
.quality(85.0)
.progressive(true)
.optimize_huffman(false);
let result = encode_rgb(width, height, &data, &config_no_opt);
assert!(result.is_err(), "Progressive + fixed Huffman should fail");
let config_opt = EncoderConfig::ycbcr(90.0, ChromaSubsampling::Quarter)
.quality(85.0)
.progressive(true)
.optimize_huffman(true);
let opt_data = encode_rgb(width, height, &data, &config_opt)
.expect("Progressive with optimized Huffman should succeed");
assert_eq!(&opt_data[0..2], &[0xFF, 0xD8]);
let config_baseline = EncoderConfig::ycbcr(90.0, ChromaSubsampling::Quarter)
.quality(85.0)
.progressive(false)
.optimize_huffman(true);
let baseline_data =
encode_rgb(width, height, &data, &config_baseline).expect("Baseline should succeed");
let savings = baseline_data.len() as f64 - opt_data.len() as f64;
let savings_pct = savings / baseline_data.len() as f64 * 100.0;
println!(
"Progressive: {} bytes, Baseline: {} bytes, Savings: {:.1}%",
opt_data.len(),
baseline_data.len(),
savings_pct
);
}
#[test]
fn test_progressive_optimized_external_decode() {
let width = 32u32;
let height = 32u32;
let mut data = Vec::with_capacity((width * height) as usize);
for y in 0..height {
for x in 0..width {
data.push(((x * 8 + y * 8) % 256) as u8);
}
}
let config = EncoderConfig::grayscale(90.0).progressive(true);
let jpeg_data = encode_gray(width, height, &data, &config).expect("Encoding should succeed");
assert_eq!(&jpeg_data[0..2], &[0xFF, 0xD8]); assert_eq!(&jpeg_data[jpeg_data.len() - 2..], &[0xFF, 0xD9]);
let mut found_dht = false;
let mut found_sof2 = false;
for i in 0..jpeg_data.len() - 1 {
if jpeg_data[i] == 0xFF {
match jpeg_data[i + 1] {
0xC4 => found_dht = true,
0xC2 => found_sof2 = true,
_ => {}
}
}
}
assert!(found_dht, "Should have DHT marker");
assert!(found_sof2, "Should have SOF2 marker for progressive");
let decoded = decode_with_zune(&jpeg_data).expect("zune-jpeg should decode");
assert_eq!(decoded.len(), (width * height * 3) as usize);
}
#[test]
fn test_progressive_optimized_larger_image() {
let width = 256u32;
let height = 256u32;
let mut data = Vec::with_capacity((width * height * 3) as usize);
for y in 0..height {
for x in 0..width {
let base = ((x as f32 / width as f32) * 255.0) as u8;
let noise = ((x * 7 + y * 13) % 64) as u8;
data.push(base.wrapping_add(noise)); data.push(255u8.wrapping_sub(base)); data.push(((y * 255) / height) as u8); }
}
let config_prog = EncoderConfig::ycbcr(90.0, ChromaSubsampling::Quarter)
.quality(85.0)
.progressive(true)
.optimize_huffman(true);
let prog_data = encode_rgb(width, height, &data, &config_prog)
.expect("Progressive encoding should succeed");
let config_baseline = EncoderConfig::ycbcr(90.0, ChromaSubsampling::Quarter)
.quality(85.0)
.progressive(false)
.optimize_huffman(true);
let baseline_data = encode_rgb(width, height, &data, &config_baseline)
.expect("Baseline encoding should succeed");
let size_ratio = prog_data.len() as f64 / baseline_data.len() as f64;
println!(
"256x256 RGB: progressive={} bytes, baseline={} bytes, ratio={:.2}",
prog_data.len(),
baseline_data.len(),
size_ratio
);
assert!(
size_ratio < 1.15,
"Progressive should not be much larger than baseline: ratio={}",
size_ratio
);
let decoded_prog = decode_with_zune(&prog_data).expect("Progressive should decode");
let decoded_baseline = decode_with_zune(&baseline_data).expect("Baseline should decode");
assert_eq!(decoded_prog.len(), decoded_baseline.len());
}
#[test]
fn test_progressive_optimized_solid_color() {
let width = 64u32;
let height = 64u32;
let data: Vec<u8> = (0..(width * height)).flat_map(|_| [255u8, 0, 0]).collect();
let config = EncoderConfig::ycbcr(90.0, ChromaSubsampling::Quarter)
.quality(90.0)
.progressive(true)
.optimize_huffman(true);
let jpeg_data = encode_rgb(width, height, &data, &config).expect("Encoding should succeed");
assert!(jpeg_data.len() < 2000, "Solid color should compress well");
let decoded = decode_with_zune(&jpeg_data).expect("Should decode");
assert_eq!(decoded.len(), (width * height * 3) as usize);
}
#[test]
fn test_progressive_optimized_high_frequency() {
let width = 64u32;
let height = 64u32;
let mut data = Vec::with_capacity((width * height * 3) as usize);
for y in 0..height {
for x in 0..width {
let val = if (x + y) % 2 == 0 { 255u8 } else { 0u8 };
data.push(val);
data.push(val);
data.push(val);
}
}
let config = EncoderConfig::ycbcr(90.0, ChromaSubsampling::Quarter)
.quality(85.0)
.progressive(true)
.optimize_huffman(true);
let jpeg_data = encode_rgb(width, height, &data, &config).expect("Encoding should succeed");
let decoded = decode_with_zune(&jpeg_data).expect("Should decode");
assert_eq!(decoded.len(), (width * height * 3) as usize);
}
#[test]
fn test_progressive_optimized_quality_levels() {
let width = 64u32;
let height = 64u32;
let mut data = Vec::with_capacity((width * height * 3) as usize);
for y in 0..height {
for x in 0..width {
let noise = ((x * 7 + y * 13) % 64) as u8;
data.push(((x * 4) as u8).wrapping_add(noise));
data.push(((y * 4) as u8).wrapping_add(noise / 2));
data.push(128u8.wrapping_add(noise));
}
}
let mut prev_size = 0usize;
for quality in [70.0, 85.0, 95.0] {
let config = EncoderConfig::ycbcr(90.0, ChromaSubsampling::Quarter)
.quality(quality)
.progressive(true)
.optimize_huffman(true);
let jpeg_data = encode_rgb(width, height, &data, &config)
.unwrap_or_else(|_| panic!("Q{} encoding should succeed", quality));
if prev_size > 0 {
assert!(
jpeg_data.len() >= prev_size - 100,
"Q{} ({} bytes) should not be much smaller than lower quality ({} bytes)",
quality,
jpeg_data.len(),
prev_size
);
}
prev_size = jpeg_data.len();
decode_with_zune(&jpeg_data).unwrap_or_else(|_| panic!("Q{} should decode", quality));
}
}
#[test]
fn test_progressive_optimized_single_block() {
let width = 8u32;
let height = 8u32;
let data: Vec<u8> = (0..64).flat_map(|i| [i as u8 * 4, 128, 64]).collect();
let config = EncoderConfig::ycbcr(90.0, ChromaSubsampling::Quarter)
.quality(90.0)
.progressive(true)
.optimize_huffman(true);
let jpeg_data = encode_rgb(width, height, &data, &config).expect("Single block should encode");
assert_eq!(&jpeg_data[0..2], &[0xFF, 0xD8]);
assert_eq!(&jpeg_data[jpeg_data.len() - 2..], &[0xFF, 0xD9]);
decode_with_zune(&jpeg_data).expect("Single block should decode");
}
#[test]
fn test_progressive_optimized_grayscale_sizes() {
for size in [16u32, 32, 64, 128] {
let mut data = Vec::with_capacity((size * size) as usize);
for y in 0..size {
for x in 0..size {
data.push(((x + y) * 255 / (2 * size - 2)) as u8);
}
}
let config = EncoderConfig::grayscale(85.0)
.progressive(true)
.optimize_huffman(true);
let jpeg_data = encode_gray(size, size, &data, &config)
.unwrap_or_else(|_| panic!("{}x{} gray should encode", size, size));
let decoded = decode_with_zune(&jpeg_data)
.unwrap_or_else(|_| panic!("{}x{} gray should decode", size, size));
assert_eq!(decoded.len(), (size * size * 3) as usize);
}
}
#[test]
fn test_progressive_optimized_scan_structure() {
let width = 32u32;
let height = 32u32;
let data: Vec<u8> = (0..(width * height * 3)).map(|i| (i % 256) as u8).collect();
let config = EncoderConfig::ycbcr(90.0, ChromaSubsampling::Quarter)
.quality(85.0)
.progressive(true)
.optimize_huffman(true);
let jpeg_data = encode_rgb(width, height, &data, &config).expect("Encoding should succeed");
let mut sos_count = 0;
let mut dht_count = 0;
let mut dqt_count = 0;
let mut sof2_count = 0;
let mut i = 0;
while i < jpeg_data.len() - 1 {
if jpeg_data[i] == 0xFF {
match jpeg_data[i + 1] {
0xDA => sos_count += 1,
0xC4 => dht_count += 1,
0xDB => dqt_count += 1,
0xC2 => sof2_count += 1,
_ => {}
}
}
i += 1;
}
assert_eq!(sof2_count, 1, "Should have exactly 1 SOF2 marker");
assert!(dht_count >= 1, "Should have at least 1 DHT marker");
assert!(dqt_count >= 1, "Should have at least 1 DQT marker");
assert!(
sos_count >= 2,
"Progressive should have at least 2 SOS markers"
);
}
#[test]
fn test_progressive_optimized_non_square() {
let width = 128u32;
let height = 32u32;
let mut data = Vec::with_capacity((width * height * 3) as usize);
for y in 0..height {
for x in 0..width {
data.push((x * 2) as u8);
data.push((y * 8) as u8);
data.push(128);
}
}
let config = EncoderConfig::ycbcr(90.0, ChromaSubsampling::Quarter)
.quality(85.0)
.progressive(true)
.optimize_huffman(true);
let jpeg_data = encode_rgb(width, height, &data, &config).expect("Wide image should encode");
let decoded = decode_with_zune(&jpeg_data).expect("Wide image should decode");
assert_eq!(decoded.len(), (width * height * 3) as usize);
let width = 32u32;
let height = 128u32;
let mut data = Vec::with_capacity((width * height * 3) as usize);
for y in 0..height {
for x in 0..width {
data.push((x * 8) as u8);
data.push((y * 2) as u8);
data.push(128);
}
}
let jpeg_data = encode_rgb(width, height, &data, &config).expect("Tall image should encode");
let decoded = decode_with_zune(&jpeg_data).expect("Tall image should decode");
assert_eq!(decoded.len(), (width * height * 3) as usize);
}
#[test]
fn test_progressive_optimized_odd_dimensions() {
let config = EncoderConfig::ycbcr(90.0, ChromaSubsampling::Quarter)
.quality(85.0)
.progressive(true)
.optimize_huffman(true);
for (width, height) in [(17u32, 23u32), (33, 41), (65, 70), (100, 99)] {
let mut data = Vec::with_capacity((width * height * 3) as usize);
for y in 0..height {
for x in 0..width {
data.push(((x.wrapping_mul(7).wrapping_add(y.wrapping_mul(3))) % 256) as u8);
data.push(((x.wrapping_mul(3).wrapping_add(y.wrapping_mul(11))) % 256) as u8);
data.push(((x.wrapping_add(y.wrapping_mul(5))) % 256) as u8);
}
}
let jpeg_data = encode_rgb(width, height, &data, &config)
.unwrap_or_else(|_| panic!("{}x{} should encode", width, height));
let decoded = decode_with_zune(&jpeg_data)
.unwrap_or_else(|_| panic!("{}x{} should decode", width, height));
assert_eq!(
decoded.len(),
(width * height * 3) as usize,
"Decoded size mismatch for {}x{}",
width,
height
);
}
}
#[test]
fn test_baseline_still_works() {
let width = 16u32;
let height = 16u32;
let mut data = Vec::with_capacity((width * height) as usize);
for _y in 0..height {
for x in 0..width {
data.push((x * 16) as u8);
}
}
let config = EncoderConfig::grayscale(90.0).progressive(false);
let jpeg_data =
encode_gray(width, height, &data, &config).expect("Baseline encoding should succeed");
let mut found_sof0 = false;
for i in 0..jpeg_data.len() - 1 {
if jpeg_data[i] == 0xFF && jpeg_data[i + 1] == 0xC0 {
found_sof0 = true;
break;
}
}
assert!(found_sof0, "Baseline JPEG should have SOF0 marker");
let sos_count = (0..jpeg_data.len() - 1)
.filter(|&i| jpeg_data[i] == 0xFF && jpeg_data[i + 1] == 0xDA)
.count();
assert_eq!(sos_count, 1, "Baseline JPEG should have exactly 1 scan");
}
#[test]
fn test_progressive_all_quality_levels() {
let width = 64u32;
let height = 64u32;
let mut data = Vec::with_capacity((width * height * 3) as usize);
for y in 0..height {
for x in 0..width {
let noise = ((x * 7 + y * 13) % 64) as u8;
data.push(((x * 4) as u8).wrapping_add(noise));
data.push(((y * 4) as u8).wrapping_add(noise / 2));
data.push(128u8.wrapping_add(noise));
}
}
let mut prev_size = 0usize;
for q in (10..=100).step_by(5) {
let config = EncoderConfig::ycbcr(90.0, ChromaSubsampling::Quarter)
.quality(q as f32)
.progressive(true)
.optimize_huffman(true);
let jpeg_data = encode_rgb(width, height, &data, &config)
.unwrap_or_else(|_| panic!("Q{} encoding should succeed", q));
let size = jpeg_data.len();
decode_with_zune(&jpeg_data).unwrap_or_else(|_| panic!("Q{} should decode", q));
if prev_size > 0 {
assert!(
size >= prev_size.saturating_sub(500),
"Q{} ({} bytes) should not be much smaller than Q{} ({} bytes)",
q,
size,
q - 5,
prev_size
);
}
prev_size = size;
println!("Q{}: {} bytes - OK", q, size);
}
}
#[test]
fn test_progressive_quality_various_content() {
struct TestCase {
name: &'static str,
width: u32,
height: u32,
generator: fn(u32, u32) -> Vec<u8>,
}
fn solid_red(w: u32, h: u32) -> Vec<u8> {
(0..w * h).flat_map(|_| [255, 0, 0]).collect()
}
fn gradient(w: u32, h: u32) -> Vec<u8> {
(0..h)
.flat_map(|y| (0..w).flat_map(move |x| [(x * 255 / w) as u8, (y * 255 / h) as u8, 128]))
.collect()
}
fn checkerboard(w: u32, h: u32) -> Vec<u8> {
(0..h)
.flat_map(|y| {
(0..w).flat_map(move |x| {
if (x / 8 + y / 8) % 2 == 0 {
[0, 0, 0]
} else {
[255, 255, 255]
}
})
})
.collect()
}
fn photo_like(w: u32, h: u32) -> Vec<u8> {
(0..h)
.flat_map(|y| {
(0..w).flat_map(move |x| {
let r = ((x.wrapping_mul(17) ^ y.wrapping_mul(31)) % 256) as u8;
let g = ((x.wrapping_mul(13) ^ y.wrapping_mul(23)) % 256) as u8;
let b = ((x.wrapping_mul(11) ^ y.wrapping_mul(19)) % 256) as u8;
[r, g, b]
})
})
.collect()
}
let test_cases = [
TestCase {
name: "solid_red",
width: 32,
height: 32,
generator: solid_red,
},
TestCase {
name: "gradient",
width: 64,
height: 64,
generator: gradient,
},
TestCase {
name: "checkerboard",
width: 48,
height: 48,
generator: checkerboard,
},
TestCase {
name: "photo_like",
width: 128,
height: 96,
generator: photo_like,
},
TestCase {
name: "photo_like_odd",
width: 127,
height: 95,
generator: photo_like,
},
];
let quality_levels = [30.0, 50.0, 75.0, 85.0, 95.0];
for tc in &test_cases {
let data = (tc.generator)(tc.width, tc.height);
for &q in &quality_levels {
let config = EncoderConfig::ycbcr(90.0, ChromaSubsampling::Quarter)
.quality(q)
.progressive(true)
.optimize_huffman(true);
let jpeg_data = encode_rgb(tc.width, tc.height, &data, &config)
.unwrap_or_else(|_| panic!("{} Q{} encoding should succeed", tc.name, q));
decode_with_zune(&jpeg_data)
.unwrap_or_else(|_| panic!("{} Q{} should decode", tc.name, q));
println!(
"{} {}x{} Q{}: {} bytes",
tc.name,
tc.width,
tc.height,
q,
jpeg_data.len()
);
}
}
}
#[test]
fn test_progressive_extreme_low_quality() {
let width = 64u32;
let height = 64u32;
let data: Vec<u8> = (0..height)
.flat_map(|y| {
(0..width).flat_map(move |x| {
let r = ((x.wrapping_mul(17) ^ y.wrapping_mul(31)) % 256) as u8;
let g = ((x.wrapping_mul(13) ^ y.wrapping_mul(23)) % 256) as u8;
let b = ((x.wrapping_mul(11) ^ y.wrapping_mul(19)) % 256) as u8;
[r, g, b]
})
})
.collect();
for q in [1.0, 2.0, 3.0, 5.0, 7.0, 10.0] {
let config = EncoderConfig::ycbcr(90.0, ChromaSubsampling::Quarter)
.quality(q)
.progressive(true)
.optimize_huffman(true);
let jpeg_data = encode_rgb(width, height, &data, &config)
.unwrap_or_else(|_| panic!("Q{} encoding should succeed", q));
decode_with_zune(&jpeg_data).unwrap_or_else(|_| panic!("Q{} should decode", q));
println!("Q{}: {} bytes", q, jpeg_data.len());
}
}
#[test]
#[ignore] fn test_libjpeg_compatibility_noise() {
let width = 64u32;
let height = 64u32;
let data: Vec<u8> = (0..height)
.flat_map(|y| {
(0..width).flat_map(move |x| {
let r = ((x.wrapping_mul(17) ^ y.wrapping_mul(31)) % 256) as u8;
let g = ((x.wrapping_mul(13) ^ y.wrapping_mul(23)) % 256) as u8;
let b = ((x.wrapping_mul(11) ^ y.wrapping_mul(19)) % 256) as u8;
[r, g, b]
})
})
.collect();
let config = EncoderConfig::ycbcr(90.0, ChromaSubsampling::Quarter)
.quality(50.0)
.progressive(true)
.optimize_huffman(true);
let jpeg_data = encode_rgb(width, height, &data, &config).expect("Encoding should succeed");
let zune_decoded = decode_with_zune(&jpeg_data).expect("zune-jpeg should decode");
assert_eq!(
zune_decoded.len(),
(width * height * 3) as usize,
"Decoded size should match"
);
let temp_path = std::env::temp_dir().join("test_libjpeg_compat.jpg");
std::fs::write(&temp_path, &jpeg_data).expect("Failed to write temp file");
let output = Command::new("djpeg")
.arg(&temp_path)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.output();
match output {
Ok(result) => {
let stderr = String::from_utf8_lossy(&result.stderr);
if !stderr.is_empty() {
eprintln!("djpeg stderr: {}", stderr);
}
assert!(
result.status.success(),
"djpeg should decode without errors. stderr: {}",
stderr
);
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
eprintln!("djpeg not found, skipping libjpeg compatibility check");
}
Err(e) => {
panic!("Failed to run djpeg: {}", e);
}
}
let _ = std::fs::remove_file(&temp_path);
}
#[test]
#[ignore] fn test_cpp_pixel_parity() {
let width = 64u32;
let height = 64u32;
let data: Vec<u8> = (0..height)
.flat_map(|y| {
(0..width).flat_map(move |x| {
let r = ((x.wrapping_mul(17) ^ y.wrapping_mul(31)) % 256) as u8;
let g = ((x.wrapping_mul(13) ^ y.wrapping_mul(23)) % 256) as u8;
let b = ((x.wrapping_mul(11) ^ y.wrapping_mul(19)) % 256) as u8;
[r, g, b]
})
})
.collect();
let config = EncoderConfig::ycbcr(90.0, ChromaSubsampling::Quarter)
.quality(50.0)
.progressive(true)
.optimize_huffman(true);
let rust_jpeg = encode_rgb(width, height, &data, &config).expect("Encoding should succeed");
let rust_decoded = decode_with_zune(&rust_jpeg).expect("Should decode");
assert_eq!(
rust_decoded.len(),
(width * height * 3) as usize,
"Decoded size should match input dimensions"
);
println!(
"Rust JPEG: {} bytes, decoded to {} bytes",
rust_jpeg.len(),
rust_decoded.len()
);
}