#![warn(clippy::all, clippy::pedantic)]
use anyhow::{Context, Result};
use image::{ImageBuffer, Rgba};
use jxl_oxide::{JxlImage, PixelFormat};
use log::info;
use std::path::{Path, PathBuf};
#[must_use]
pub fn is_jxl_file(path: &Path) -> bool {
path.extension()
.and_then(|ext| ext.to_str())
.is_some_and(|ext| ext.eq_ignore_ascii_case("jxl"))
}
pub async fn convert_jxl_to_png(input_path: &Path, output_path: &Path) -> Result<()> {
info!(
"Converting JXL to PNG: {} -> {}",
input_path.display(),
output_path.display()
);
let jxl_data = tokio::fs::read(input_path)
.await
.with_context(|| format!("Failed to read JXL file: {}", input_path.display()))?;
let image = JxlImage::read_with_defaults(&jxl_data[..]).map_err(|e| {
anyhow::anyhow!("Failed to decode JXL file {}: {}", input_path.display(), e)
})?;
let (width, height) = (image.width(), image.height());
let mut rgba: ImageBuffer<Rgba<u8>, Vec<u8>> = ImageBuffer::new(width, height);
let render = image.render_frame(0).map_err(|e| {
anyhow::anyhow!("Failed to render JXL frame {}: {}", input_path.display(), e)
})?;
let mut stream = render.stream();
let channels = stream.channels() as usize;
let mut pixel_data = vec![0.0f32; width as usize * height as usize * channels];
stream.write_to_buffer(&mut pixel_data);
for y in 0..height {
for x in 0..width {
let pixel_idx = ((y * width + x) as usize) * channels;
let pixel = match image.pixel_format() {
PixelFormat::Rgba => {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let rgba = [
(pixel_data[pixel_idx] * 255.0) as u8,
(pixel_data[pixel_idx + 1] * 255.0) as u8,
(pixel_data[pixel_idx + 2] * 255.0) as u8,
(pixel_data[pixel_idx + 3] * 255.0) as u8,
];
Rgba(rgba)
}
PixelFormat::Rgb => {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let rgb = [
(pixel_data[pixel_idx] * 255.0) as u8,
(pixel_data[pixel_idx + 1] * 255.0) as u8,
(pixel_data[pixel_idx + 2] * 255.0) as u8,
255,
];
Rgba(rgb)
}
_ => anyhow::bail!("Unsupported JXL pixel format: {:?}", image.pixel_format()),
};
rgba.put_pixel(x, y, pixel);
}
}
rgba.save(output_path)
.with_context(|| format!("Failed to save PNG file: {}", output_path.display()))?;
info!("Successfully converted JXL to PNG");
Ok(())
}
pub async fn process_jxl_file<F, Fut>(input_path: &Path, processor: Option<F>) -> Result<()>
where
F: FnOnce(PathBuf) -> Fut + Send,
Fut: std::future::Future<Output = Result<()>> + Send,
{
if !is_jxl_file(input_path) {
anyhow::bail!("Not a JXL file: {}", input_path.display());
}
let png_path = input_path.with_extension("png");
let conversion_result = convert_jxl_to_png(input_path, &png_path).await;
if let Some(processor) = processor {
processor(png_path.clone()).await?;
}
tokio::fs::remove_file(input_path).await.with_context(|| {
format!(
"Failed to remove original JXL file: {}",
input_path.display()
)
})?;
info!("Successfully processed JXL file: {}", input_path.display());
conversion_result
}