pub fn try_corpus_dir() -> Option<std::path::PathBuf> {
let dir = std::path::PathBuf::from(
std::env::var("CODEC_CORPUS_DIR")
.unwrap_or_else(|_| "/home/lilith/work/codec-corpus".into()),
);
dir.is_dir().then_some(dir)
}
pub fn corpus_dir() -> std::path::PathBuf {
try_corpus_dir().unwrap_or_else(|| {
panic!(
"Codec corpus not found. Set CODEC_CORPUS_DIR env var or install codec-corpus crate."
)
})
}
#[macro_export]
macro_rules! skip_without_corpus {
() => {
if $crate::test_helpers::try_corpus_dir().is_none() {
eprintln!("SKIPPED: codec corpus not available");
return;
}
};
}
#[macro_export]
macro_rules! skip_without_binary {
($path:expr) => {
if !std::path::Path::new(&$path).exists() {
eprintln!("SKIPPED: {} not available", $path);
return;
}
};
}
pub fn djxl_path() -> String {
std::env::var("DJXL_PATH")
.unwrap_or_else(|_| "/home/lilith/work/jxl-efforts/libjxl/build/tools/djxl".into())
}
pub fn cjxl_path() -> String {
std::env::var("CJXL_PATH")
.unwrap_or_else(|_| "/home/lilith/work/jxl-efforts/libjxl/build/tools/cjxl".into())
}
pub fn jxl_cli_path() -> String {
std::env::var("JXL_CLI_PATH")
.unwrap_or_else(|_| "/home/lilith/work/jxl-rs/target/release/jxl_cli".into())
}
pub fn output_dir(subdir: &str) -> std::path::PathBuf {
let base = std::path::PathBuf::from(
std::env::var("JXL_ENCODER_OUTPUT_DIR")
.unwrap_or_else(|_| "/mnt/v/output/jxl-encoder-rs".into()),
);
let dir = base.join(subdir);
if std::fs::create_dir_all(&dir).is_ok() {
return dir;
}
let fallback = std::env::temp_dir().join(format!("jxl-encoder-rs/{subdir}"));
let _ = std::fs::create_dir_all(&fallback);
fallback
}
pub fn output_dir_for(project: &str, subdir: &str) -> std::path::PathBuf {
let base = match std::env::var("JXL_ENCODER_OUTPUT_DIR") {
Ok(dir) => {
let p = std::path::PathBuf::from(dir);
p.parent().unwrap_or(&p).to_path_buf()
}
Err(_) => std::path::PathBuf::from("/mnt/v/output"),
};
let dir = base.join(project).join(subdir);
if std::fs::create_dir_all(&dir).is_ok() {
return dir;
}
let fallback = std::env::temp_dir().join(format!("{project}/{subdir}"));
let _ = std::fs::create_dir_all(&fallback);
fallback
}
#[cfg(test)]
use crate::error::Result;
#[cfg(test)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EncodingMode {
VarDct, Modular, }
#[cfg(test)]
pub struct DecodedImage {
pub width: usize,
pub height: usize,
pub channels: usize,
pub pixels: Vec<f32>,
}
#[cfg(test)]
impl DecodedImage {
pub fn get(&self, x: usize, y: usize, c: usize) -> f32 {
let idx = (y * self.width + x) * self.channels + c;
self.pixels[idx]
}
pub fn get_rgb_u8(&self, x: usize, y: usize) -> (u8, u8, u8) {
let r = (self.get(x, y, 0) * 255.0).clamp(0.0, 255.0) as u8;
let g = (self.get(x, y, 1) * 255.0).clamp(0.0, 255.0) as u8;
let b = (self.get(x, y, 2) * 255.0).clamp(0.0, 255.0) as u8;
(r, g, b)
}
}
#[cfg(test)]
pub fn decode_with_jxl_rs(data: &[u8]) -> Result<DecodedImage> {
use jxl::api::states::Initialized;
use jxl::api::{
JxlDataFormat, JxlDecoder, JxlDecoderOptions, JxlOutputBuffer, JxlPixelFormat,
ProcessingResult,
};
use jxl::image::{Image, Rect};
let options = JxlDecoderOptions::default();
let mut decoder: JxlDecoder<Initialized> = JxlDecoder::new(options);
let mut input = data;
let mut decoder = loop {
match decoder
.process(&mut input)
.map_err(|e| crate::error::Error::InvalidInput(format!("jxl-rs init error: {:?}", e)))?
{
ProcessingResult::Complete { result } => break result,
ProcessingResult::NeedsMoreInput { fallback, .. } => {
if input.is_empty() {
return Err(crate::error::Error::InvalidInput(
"jxl-rs: unexpected end of input during header parsing".to_string(),
));
}
decoder = fallback;
}
}
};
let basic_info = decoder.basic_info().clone();
let (width, height) = basic_info.size;
let default_format = decoder.current_pixel_format();
let num_channels = default_format.color_type.samples_per_pixel();
let num_extra = default_format.extra_channel_format.len();
let requested_format = JxlPixelFormat {
color_type: default_format.color_type,
color_data_format: Some(JxlDataFormat::f32()),
extra_channel_format: default_format
.extra_channel_format
.iter()
.map(|_| Some(JxlDataFormat::f32()))
.collect(),
};
decoder.set_pixel_format(requested_format);
let mut decoder = loop {
match decoder.process(&mut input).map_err(|e| {
crate::error::Error::InvalidInput(format!("jxl-rs frame error: {:?}", e))
})? {
ProcessingResult::Complete { result } => break result,
ProcessingResult::NeedsMoreInput { fallback, .. } => {
if input.is_empty() {
return Err(crate::error::Error::InvalidInput(
"jxl-rs: unexpected end of input during frame parsing".to_string(),
));
}
decoder = fallback;
}
}
};
let mut color_buffer = Image::<f32>::new((width * num_channels, height)).map_err(|e| {
crate::error::Error::InvalidInput(format!("jxl-rs buffer alloc error: {:?}", e))
})?;
let mut extra_buffers: Vec<Image<f32>> = (0..num_extra)
.map(|_| {
Image::<f32>::new((width, height)).map_err(|e| {
crate::error::Error::InvalidInput(format!(
"jxl-rs extra buffer alloc error: {:?}",
e
))
})
})
.collect::<Result<Vec<_>>>()?;
let mut buffers: Vec<_> = vec![JxlOutputBuffer::from_image_rect_mut(
color_buffer
.get_rect_mut(Rect {
origin: (0, 0),
size: (width * num_channels, height),
})
.into_raw(),
)];
for eb in &mut extra_buffers {
buffers.push(JxlOutputBuffer::from_image_rect_mut(
eb.get_rect_mut(Rect {
origin: (0, 0),
size: (width, height),
})
.into_raw(),
));
}
loop {
match decoder.process(&mut input, &mut buffers).map_err(|e| {
crate::error::Error::InvalidInput(format!("jxl-rs decode error: {:?}", e))
})? {
ProcessingResult::Complete { .. } => break,
ProcessingResult::NeedsMoreInput { fallback, .. } => {
if input.is_empty() {
return Err(crate::error::Error::InvalidInput(
"jxl-rs: unexpected end of input during decode".to_string(),
));
}
decoder = fallback;
}
}
}
let total_channels = num_channels + num_extra;
let mut pixels = Vec::with_capacity(width * height * total_channels);
for y in 0..height {
let color_row = color_buffer.row(y);
if num_extra == 0 {
pixels.extend_from_slice(color_row);
} else {
let extra_rows: Vec<&[f32]> = extra_buffers.iter().map(|eb| eb.row(y)).collect();
for x in 0..width {
for c in 0..num_channels {
pixels.push(color_row[x * num_channels + c]);
}
for (ec, extra_row) in extra_rows.iter().enumerate() {
let _ = ec;
pixels.push(extra_row[x]);
}
}
}
}
Ok(DecodedImage {
width,
height,
channels: total_channels,
pixels,
})
}
#[cfg(test)]
pub fn decode_with_djxl(data: &[u8]) -> Result<DecodedImage> {
use std::process::Command;
use core::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let id = COUNTER.fetch_add(1, Ordering::Relaxed);
let temp_dir = std::env::temp_dir();
let temp_jxl = temp_dir
.join(format!("decode_test_djxl_{id}.jxl"))
.to_string_lossy()
.into_owned();
let temp_png = temp_dir
.join(format!("decode_test_djxl_{id}.png"))
.to_string_lossy()
.into_owned();
std::fs::write(&temp_jxl, data).map_err(|e| {
crate::error::Error::InvalidInput(format!("Failed to write temp file: {:?}", e))
})?;
let djxl = djxl_path();
let output = Command::new(&djxl)
.args([&temp_jxl, &temp_png])
.output()
.map_err(|e| crate::error::Error::InvalidInput(format!("Failed to run djxl: {:?}", e)))?;
if !output.status.success() {
let _ = std::fs::remove_file(&temp_jxl);
return Err(crate::error::Error::InvalidInput(format!(
"djxl failed: {}",
String::from_utf8_lossy(&output.stderr)
)));
}
let img = image::open(&temp_png).map_err(|e| {
let _ = std::fs::remove_file(&temp_jxl);
let _ = std::fs::remove_file(&temp_png);
crate::error::Error::InvalidInput(format!("Failed to load decoded PNG: {:?}", e))
})?;
let rgb = img.to_rgb8();
let width = rgb.width() as usize;
let height = rgb.height() as usize;
let pixels: Vec<f32> = rgb.as_raw().iter().map(|&v| v as f32 / 255.0).collect();
eprintln!(
"DEBUG decode_with_djxl: {}x{}, first 9 u8 raw: {:?}",
width,
height,
rgb.as_raw().iter().take(9).copied().collect::<Vec<_>>()
);
let _ = std::fs::remove_file(&temp_jxl);
let _ = std::fs::remove_file(&temp_png);
Ok(DecodedImage {
width,
height,
channels: 3,
pixels,
})
}
#[cfg(test)]
pub fn decode_with_jxl_oxide(data: &[u8]) -> Result<DecodedImage> {
let mut image = jxl_oxide::JxlImage::builder()
.read(std::io::Cursor::new(data))
.map_err(|e| {
crate::error::Error::InvalidInput(format!("jxl-oxide decode failed: {:?}", e))
})?;
image.request_color_encoding(jxl_oxide::EnumColourEncoding::srgb_linear(
jxl_oxide::RenderingIntent::Relative,
));
let width = image.width() as usize;
let height = image.height() as usize;
let channels = image.pixel_format().channels();
let render = image.render_frame(0).map_err(|e| {
crate::error::Error::InvalidInput(format!("jxl-oxide render failed: {:?}", e))
})?;
let framebuffer = render.image_all_channels();
let buf = framebuffer.buf();
let pixels = buf.to_vec();
Ok(DecodedImage {
width,
height,
channels,
pixels,
})
}
#[cfg(test)]
pub fn parse_encoding_mode(data: &[u8]) -> Option<EncodingMode> {
if data.len() < 10 {
return None;
}
fn read_bit(data: &[u8], bit_pos: usize) -> Option<u8> {
let byte_idx = bit_pos / 8;
let bit_idx = bit_pos % 8;
if byte_idx >= data.len() {
return None;
}
Some((data[byte_idx] >> bit_idx) & 1)
}
for start_byte in 4..25 {
let start_bit = start_byte * 8;
let all_default = read_bit(data, start_bit)?;
if all_default == 0 {
let frame_type_0 = read_bit(data, start_bit + 1)?;
let frame_type_1 = read_bit(data, start_bit + 2)?;
let encoding_bit = read_bit(data, start_bit + 3)?;
if frame_type_0 == 0 && frame_type_1 == 0 {
return Some(match encoding_bit {
0 => EncodingMode::VarDct,
1 => EncodingMode::Modular,
_ => unreachable!(),
});
}
}
}
None
}
#[cfg(test)]
pub fn assert_encoding_mode(data: &[u8], expected: EncodingMode, test_name: &str) {
let actual = parse_encoding_mode(data).unwrap_or_else(|| {
panic!(
"{}: Could not parse encoding mode from bitstream",
test_name
)
});
assert_eq!(
actual, expected,
"{}: Expected {:?} but got {:?}. This test is not testing what it claims!",
test_name, expected, actual
);
}
#[cfg(test)]
pub fn test_lossless_roundtrip(
data: &[u8],
width: usize,
height: usize,
test_name: &str,
) -> Result<()> {
let encoded = crate::LosslessConfig::new()
.encode(data, width as u32, height as u32, crate::PixelLayout::Rgb8)
.map_err(|e| crate::error::Error::InvalidInput(format!("{e}")))?;
assert_encoding_mode(&encoded, EncodingMode::Modular, test_name);
let decoded = decode_with_jxl_rs(&encoded)?;
assert_eq!(decoded.width, width, "{}: width mismatch", test_name);
assert_eq!(decoded.height, height, "{}: height mismatch", test_name);
Ok(())
}
#[cfg(test)]
pub fn test_lossy_roundtrip(
data: &[u8],
width: usize,
height: usize,
distance: f32,
test_name: &str,
) -> Result<()> {
let encoded = crate::LossyConfig::new(distance)
.encode(data, width as u32, height as u32, crate::PixelLayout::Rgb8)
.map_err(|e| crate::error::Error::InvalidInput(format!("{e}")))?;
let debug_path = std::env::temp_dir().join(format!("{}.jxl", test_name));
std::fs::write(&debug_path, &encoded).ok();
eprintln!(
"DEBUG: Saved {} bytes to {}",
encoded.len(),
debug_path.display()
);
assert_encoding_mode(&encoded, EncodingMode::VarDct, test_name);
eprintln!("DEBUG: Decoding with jxl-rs (primary)...");
let decoded = decode_with_jxl_rs(&encoded)?;
assert_eq!(decoded.width, width, "{}: width mismatch", test_name);
assert_eq!(decoded.height, height, "{}: height mismatch", test_name);
Ok(())
}
#[cfg(test)]
pub fn test_lossy_roundtrip_with_quality(
data: &[u8],
width: usize,
height: usize,
distance: f32,
test_name: &str,
) -> Result<f64> {
let encoded = crate::LossyConfig::new(distance)
.encode(data, width as u32, height as u32, crate::PixelLayout::Rgb8)
.map_err(|e| crate::error::Error::InvalidInput(format!("{e}")))?;
let debug_path = std::env::temp_dir().join(format!("{}.jxl", test_name));
std::fs::write(&debug_path, &encoded).ok();
assert_encoding_mode(&encoded, EncodingMode::VarDct, test_name);
let decoded = decode_with_jxl_rs(&encoded)?;
assert_eq!(decoded.width, width, "{}: width mismatch", test_name);
assert_eq!(decoded.height, height, "{}: height mismatch", test_name);
let ssim2 = calculate_ssim2(data, &decoded, width, height);
eprintln!(
"{}: encoded {} bytes, SSIM2={:.2}",
test_name,
encoded.len(),
ssim2
);
Ok(ssim2)
}
#[cfg(test)]
pub fn calculate_ssim2(
original: &[u8],
decoded: &DecodedImage,
width: usize,
height: usize,
) -> f64 {
use fast_ssim2::compute_ssimulacra2;
use imgref::ImgVec;
let original_rgb: Vec<[u8; 3]> = original
.chunks_exact(3)
.map(|rgb| [rgb[0], rgb[1], rgb[2]])
.collect();
let decoded_rgb: Vec<[u8; 3]> = (0..height)
.flat_map(|y| {
(0..width).map(move |x| {
let r = (decoded.get(x, y, 0) * 255.0).clamp(0.0, 255.0) as u8;
let g = (decoded.get(x, y, 1) * 255.0).clamp(0.0, 255.0) as u8;
let b = (decoded.get(x, y, 2) * 255.0).clamp(0.0, 255.0) as u8;
[r, g, b]
})
})
.collect();
let src = ImgVec::new(original_rgb, width, height);
let dst = ImgVec::new(decoded_rgb, width, height);
compute_ssimulacra2(src.as_ref(), dst.as_ref()).unwrap_or(0.0)
}
pub fn test_output_dir(subdir: &str) -> std::path::PathBuf {
output_dir(subdir)
}
pub fn save_test_output(subdir: &str, filename: &str, data: &[u8]) {
let dir = test_output_dir(subdir);
let path = dir.join(filename);
match std::fs::write(&path, data) {
Ok(()) => eprintln!("Saved {} bytes to {}", data.len(), path.display()),
Err(e) => eprintln!("Could not save to {} ({})", path.display(), e),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_encoding_mode() {
let _ = parse_encoding_mode(&[]);
let _ = parse_encoding_mode(&[0xFF, 0x0A]);
let _ = parse_encoding_mode(&[0; 100]);
}
}