use crate::turbojpeg_ffi::*;
use crate::{Image, ImageError};
use memmap2::Mmap;
use std::ffi::CStr;
use std::fs::File;
use std::io::Write;
use std::path::Path;
use std::ptr;
struct TjHandle(tjhandle);
impl Drop for TjHandle {
fn drop(&mut self) {
if !self.0.is_null() {
unsafe { tj3Destroy(self.0) }
}
}
}
unsafe fn get_tj_error(handle: tjhandle) -> String {
let err_ptr = tj3GetErrorStr(handle);
if err_ptr.is_null() {
"Unknown TurboJPEG error".to_string()
} else {
CStr::from_ptr(err_ptr).to_string_lossy().into_owned()
}
}
pub fn load_jpeg<P: AsRef<Path>>(path: P) -> Result<Image, ImageError> {
let file = File::open(path.as_ref())?;
let file_data = unsafe { Mmap::map(&file)? };
decode_jpeg(&file_data)
}
pub fn decode_jpeg(file_data: &[u8]) -> Result<Image, ImageError> {
unsafe {
let handle = tj3Init(TJINIT_TJINIT_DECOMPRESS as i32);
if handle.is_null() {
return Err(ImageError::Jpeg(
"Failed to create TurboJPEG decompressor".into(),
));
}
let _guard = TjHandle(handle);
if tj3DecompressHeader(handle, file_data.as_ptr(), file_data.len()) != 0 {
return Err(ImageError::Jpeg(get_tj_error(handle)));
}
let width = tj3Get(handle, TJPARAM_TJPARAM_JPEGWIDTH as i32) as u32;
let height = tj3Get(handle, TJPARAM_TJPARAM_JPEGHEIGHT as i32) as u32;
if width == 0 || height == 0 {
return Err(ImageError::Jpeg("Invalid JPEG dimensions".into()));
}
let stride = (width * 4) as i32;
let buf_size = (stride as u32 * height) as usize;
let mut data: Vec<u8> = Vec::with_capacity(buf_size);
data.set_len(buf_size);
if tj3Decompress8(
handle,
file_data.as_ptr(),
file_data.len(),
data.as_mut_ptr(),
stride,
TJPF_TJPF_RGBA,
) != 0
{
return Err(ImageError::Jpeg(get_tj_error(handle)));
}
Ok(Image {
data,
width,
height,
})
}
}
pub fn load_jpegs<P1: AsRef<Path> + Sync, P2: AsRef<Path> + Sync>(
path1: P1,
path2: P2,
) -> Result<(Image, Image), ImageError> {
let (r1, r2) = rayon::join(|| load_jpeg(path1.as_ref()), || load_jpeg(path2.as_ref()));
Ok((r1?, r2?))
}
pub fn save_jpeg<P: AsRef<Path>>(image: &Image, path: P, quality: u8) -> Result<(), ImageError> {
unsafe {
let handle = tj3Init(TJINIT_TJINIT_COMPRESS as i32);
if handle.is_null() {
return Err(ImageError::Jpeg(
"Failed to create TurboJPEG compressor".into(),
));
}
let _guard = TjHandle(handle);
let quality = quality.clamp(1, 100) as i32;
if tj3Set(handle, TJPARAM_TJPARAM_QUALITY as i32, quality) != 0 {
return Err(ImageError::Jpeg(get_tj_error(handle)));
}
if tj3Set(handle, TJPARAM_TJPARAM_SUBSAMP as i32, TJSAMP_TJSAMP_420) != 0 {
return Err(ImageError::Jpeg(get_tj_error(handle)));
}
let mut jpeg_buf: *mut u8 = ptr::null_mut();
let mut jpeg_size: usize = 0;
let stride = (image.width * 4) as i32;
if tj3Compress8(
handle,
image.data.as_ptr(),
image.width as i32,
stride,
image.height as i32,
TJPF_TJPF_RGBA,
&mut jpeg_buf,
&mut jpeg_size,
) != 0
{
return Err(ImageError::Jpeg(get_tj_error(handle)));
}
let jpeg_slice = std::slice::from_raw_parts(jpeg_buf, jpeg_size);
let mut file = File::create(path.as_ref())?;
file.write_all(jpeg_slice)?;
tj3Free(jpeg_buf as *mut _);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_jpeg_roundtrip() {
let mut img = Image::new(100, 100);
for y in 0..100 {
for x in 0..100 {
let idx = (y * 100 + x) * 4;
img.data[idx] = (x * 255 / 100) as u8; img.data[idx + 1] = (y * 255 / 100) as u8; img.data[idx + 2] = 128; img.data[idx + 3] = 255; }
}
let temp_path = "/tmp/blazediff_test.jpg";
save_jpeg(&img, temp_path, 95).expect("Failed to save JPEG");
let loaded = load_jpeg(temp_path).expect("Failed to load JPEG");
assert_eq!(loaded.width, 100);
assert_eq!(loaded.height, 100);
std::fs::remove_file(temp_path).ok();
}
}