use crate::{
error::IoError,
limits::{alloc_image, check_image_dimensions},
};
use kornia_image::{Image, ImageError, ImageSize};
use std::{path::Path, sync::Mutex};
use turbojpeg;
pub use turbojpeg::Subsamp;
#[derive(thiserror::Error, Debug)]
pub enum JpegTurboError {
#[error("Something went wrong with the JPEG compressor")]
TurboJpegError(#[from] turbojpeg::Error),
#[error("Image data is not contiguous")]
ImageDataNotContiguous,
#[error("Failed to create image")]
ImageCreationError(#[from] ImageError),
#[error("Mutex is poisoned")]
MutexPoisoned,
#[error("pixel buffer length {got} != expected {expected}")]
InvalidBufferLength {
got: usize,
expected: usize,
},
#[error("JPEG of {width}x{height} exceeds the maximum of {max_pixels} pixels")]
ImageTooLarge {
width: usize,
height: usize,
max_pixels: usize,
},
#[error("Failed to allocate {0} bytes for the decoded image")]
AllocationFailed(usize),
#[error(transparent)]
IoError(#[from] std::io::Error),
}
impl JpegTurboError {
fn from_limits(err: IoError) -> Self {
match err {
IoError::ImageTooLarge {
width,
height,
max_pixels,
} => Self::ImageTooLarge {
width,
height,
max_pixels,
},
IoError::AllocationFailed(bytes) => Self::AllocationFailed(bytes),
IoError::ImageCreationError(e) => Self::ImageCreationError(e),
IoError::JpegTurboError(e) => e,
other => Self::IoError(std::io::Error::other(other)),
}
}
}
pub struct JpegTurboDecoder(Mutex<turbojpeg::Decompressor>);
pub struct JpegTurboEncoder(Mutex<turbojpeg::Compressor>);
impl JpegTurboEncoder {
pub fn new() -> Result<Self, JpegTurboError> {
let compressor = turbojpeg::Compressor::new()?;
Ok(Self(Mutex::new(compressor)))
}
pub fn encode_rgb8(&self, image: &Image<u8, 3>) -> Result<Vec<u8>, JpegTurboError> {
self.encode_rgb8_slice(image.as_slice(), image.size())
}
pub fn encode_rgb8_slice(
&self,
pixels: &[u8],
size: ImageSize,
) -> Result<Vec<u8>, JpegTurboError> {
let expected = size.checked_len(3)?;
if pixels.len() != expected {
return Err(JpegTurboError::InvalidBufferLength {
got: pixels.len(),
expected,
});
}
let buf = turbojpeg::Image {
pixels,
width: size.width,
pitch: 3 * size.width,
height: size.height,
format: turbojpeg::PixelFormat::RGB,
};
Ok(self
.0
.lock()
.map_err(|_| JpegTurboError::MutexPoisoned)?
.compress_to_vec(buf)?)
}
pub fn set_quality(&self, quality: i32) -> Result<(), JpegTurboError> {
Ok(self
.0
.lock()
.map_err(|_| JpegTurboError::MutexPoisoned)?
.set_quality(quality)?)
}
pub fn set_subsamp(&self, subsamp: turbojpeg::Subsamp) -> Result<(), JpegTurboError> {
self.0
.lock()
.map_err(|_| JpegTurboError::MutexPoisoned)?
.set_subsamp(subsamp)?;
Ok(())
}
}
impl JpegTurboDecoder {
pub fn new() -> Result<Self, JpegTurboError> {
let decompressor = turbojpeg::Decompressor::new()?;
Ok(JpegTurboDecoder(Mutex::new(decompressor)))
}
pub fn read_header(&self, jpeg_data: &[u8]) -> Result<ImageSize, JpegTurboError> {
let header = self
.0
.lock()
.map_err(|_| JpegTurboError::MutexPoisoned)?
.read_header(jpeg_data)?;
check_image_dimensions(header.width, header.height).map_err(JpegTurboError::from_limits)?;
Ok(ImageSize {
width: header.width,
height: header.height,
})
}
pub fn decode_rgb8(&self, jpeg_data: &[u8]) -> Result<Image<u8, 3>, JpegTurboError> {
let mut dst =
alloc_image(self.read_header(jpeg_data)?).map_err(JpegTurboError::from_limits)?;
self.decode_rgb8_into(jpeg_data, &mut dst)?;
Ok(dst)
}
pub fn decode_gray8(&self, jpeg_data: &[u8]) -> Result<Image<u8, 1>, JpegTurboError> {
let mut dst =
alloc_image(self.read_header(jpeg_data)?).map_err(JpegTurboError::from_limits)?;
self.decode_gray8_into(jpeg_data, &mut dst)?;
Ok(dst)
}
pub fn decode_rgb8_into(
&self,
jpeg_data: &[u8],
dst: &mut Image<u8, 3>,
) -> Result<(), JpegTurboError> {
let size = dst.size();
self.decode_into(
jpeg_data,
dst.as_slice_mut(),
size,
turbojpeg::PixelFormat::RGB,
)
}
pub fn decode_gray8_into(
&self,
jpeg_data: &[u8],
dst: &mut Image<u8, 1>,
) -> Result<(), JpegTurboError> {
let size = dst.size();
self.decode_into(
jpeg_data,
dst.as_slice_mut(),
size,
turbojpeg::PixelFormat::GRAY,
)
}
fn decode_into(
&self,
jpeg_data: &[u8],
pixels: &mut [u8],
image_size: ImageSize,
format: turbojpeg::PixelFormat,
) -> Result<(), JpegTurboError> {
let header_size = self.read_header(jpeg_data)?;
if header_size != image_size {
return Err(JpegTurboError::ImageCreationError(
ImageError::InvalidImageSize(
header_size.width,
header_size.height,
image_size.width,
image_size.height,
),
));
}
let pitch = format.size() * image_size.width;
let buf = turbojpeg::Image {
pixels,
width: image_size.width,
pitch,
height: image_size.height,
format,
};
self.0
.lock()
.map_err(|_| JpegTurboError::MutexPoisoned)?
.decompress(jpeg_data, buf)?;
Ok(())
}
}
pub fn read_image_jpegturbo_rgb8(file_path: impl AsRef<Path>) -> Result<Image<u8, 3>, IoError> {
let file_path = file_path.as_ref().to_owned();
if !file_path.exists() {
return Err(IoError::FileDoesNotExist(file_path.to_path_buf()));
}
if file_path
.extension()
.is_none_or(|ext| !ext.eq_ignore_ascii_case("jpg") && !ext.eq_ignore_ascii_case("jpeg"))
{
return Err(IoError::InvalidFileExtension(file_path.to_path_buf()));
}
let jpeg_data = std::fs::read(file_path)?;
let image = {
let decoder = JpegTurboDecoder::new()?;
decoder.decode_rgb8(&jpeg_data)?
};
Ok(image)
}
pub fn write_image_jpegturbo_rgb8(
file_path: impl AsRef<Path>,
image: &Image<u8, 3>,
quality: u8,
) -> Result<(), IoError> {
let file_path = file_path.as_ref().to_owned();
let encoder = JpegTurboEncoder::new()?;
encoder.set_quality(quality as i32)?;
let jpeg_data = encoder.encode_rgb8(image)?;
std::fs::write(file_path, jpeg_data)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::IoError;
#[test]
fn encode_rgb8_slice_matches_owning_and_validates_len() -> Result<(), JpegTurboError> {
let size = ImageSize {
width: 64,
height: 48,
};
let pixels: Vec<u8> = (0..size.width * size.height * 3)
.map(|i| (i % 251) as u8)
.collect();
let enc = JpegTurboEncoder::new()?;
let img = Image::<u8, 3>::from_size_slice(size, &pixels)?;
assert_eq!(
enc.encode_rgb8_slice(&pixels, size)?,
enc.encode_rgb8(&img)?
);
assert!(matches!(
enc.encode_rgb8_slice(&pixels[..pixels.len() - 1], size),
Err(JpegTurboError::InvalidBufferLength { .. })
));
Ok(())
}
#[test]
fn image_decoder() -> Result<(), JpegTurboError> {
let jpeg_data = std::fs::read("../../tests/data/dog.jpeg")?;
let image_size = JpegTurboDecoder::new()?.read_header(&jpeg_data)?;
assert_eq!(image_size.width, 258);
assert_eq!(image_size.height, 195);
let image = JpegTurboDecoder::new()?.decode_rgb8(&jpeg_data)?;
assert_eq!(image.cols(), 258);
assert_eq!(image.rows(), 195);
assert_eq!(image.num_channels(), 3);
Ok(())
}
#[test]
fn image_decoder_gray8() -> Result<(), JpegTurboError> {
let jpeg_data = std::fs::read("../../tests/data/dog.jpeg")?;
let image = JpegTurboDecoder::new()?.decode_gray8(&jpeg_data)?;
assert_eq!(image.cols(), 258);
assert_eq!(image.rows(), 195);
assert_eq!(image.num_channels(), 1);
assert_eq!(image.as_slice().len(), 258 * 195);
Ok(())
}
#[test]
fn image_encoder() -> Result<(), Box<dyn std::error::Error>> {
let jpeg_data_fs = std::fs::read("../../tests/data/dog.jpeg")?;
let image = JpegTurboDecoder::new()?.decode_rgb8(&jpeg_data_fs)?;
let jpeg_data = JpegTurboEncoder::new()?.encode_rgb8(&image)?;
let image_back = JpegTurboDecoder::new()?.decode_rgb8(&jpeg_data)?;
assert_eq!(image_back.cols(), 258);
assert_eq!(image_back.rows(), 195);
assert_eq!(image_back.num_channels(), 3);
Ok(())
}
#[test]
fn read_jpeg() -> Result<(), IoError> {
let image = read_image_jpegturbo_rgb8("../../tests/data/dog.jpeg")?;
assert_eq!(image.cols(), 258);
assert_eq!(image.rows(), 195);
Ok(())
}
#[test]
fn read_write_jpeg() -> Result<(), IoError> {
let tmp_dir = tempfile::tempdir()?;
std::fs::create_dir_all(tmp_dir.path())?;
let file_path = tmp_dir.path().join("dog.jpeg");
let image_data = read_image_jpegturbo_rgb8("../../tests/data/dog.jpeg")?;
write_image_jpegturbo_rgb8(&file_path, &image_data, 100)?;
let image_data_back = read_image_jpegturbo_rgb8(&file_path)?;
assert!(file_path.exists(), "File does not exist: {file_path:?}");
assert_eq!(image_data_back.cols(), 258);
assert_eq!(image_data_back.rows(), 195);
assert_eq!(image_data_back.num_channels(), 3);
Ok(())
}
}