use crate::{error::IoError, limits::check_image_dimensions};
use jpeg_encoder::{ColorType, Encoder};
use kornia_image::{
color_spaces::{Gray8, Rgb8},
Image, ImageLayout, ImageSize, PixelFormat,
};
use std::{fs, io::Cursor, path::Path};
pub fn write_image_jpeg_rgb8(
file_path: impl AsRef<Path>,
image: &Image<u8, 3>,
quality: u8,
) -> Result<(), IoError> {
write_image_jpeg_imp(file_path, image, ColorType::Rgb, quality)
}
pub fn write_image_jpeg_gray8(
file_path: impl AsRef<Path>,
image: &Image<u8, 1>,
quality: u8,
) -> Result<(), IoError> {
write_image_jpeg_imp(file_path, image, ColorType::Luma, quality)
}
pub fn encode_image_jpeg_rgb8(
image: &Image<u8, 3>,
quality: u8,
buffer: &mut Vec<u8>,
) -> Result<(), IoError> {
let (width, height) = jpeg_dimensions(image.width(), image.height())?;
let encoder = Encoder::new(buffer, quality);
encoder.encode(image.as_slice(), width, height, ColorType::Rgb)?;
Ok(())
}
pub fn encode_image_jpeg_bgra8(
image: &Image<u8, 4>,
quality: u8,
buffer: &mut Vec<u8>,
) -> Result<(), IoError> {
let (width, height) = jpeg_dimensions(image.width(), image.height())?;
let encoder = Encoder::new(buffer, quality);
encoder.encode(image.as_slice(), width, height, ColorType::Bgra)?;
Ok(())
}
pub fn encode_image_jpeg_gray8(
image: &Image<u8, 1>,
quality: u8,
buffer: &mut Vec<u8>,
) -> Result<(), IoError> {
let (width, height) = jpeg_dimensions(image.width(), image.height())?;
let encoder = Encoder::new(buffer, quality);
encoder.encode(image.as_slice(), width, height, ColorType::Luma)?;
Ok(())
}
fn jpeg_dimensions(width: usize, height: usize) -> Result<(u16, u16), IoError> {
match (u16::try_from(width), u16::try_from(height)) {
(Ok(w), Ok(h)) => Ok((w, h)),
_ => Err(IoError::DimensionTooLarge {
width,
height,
max_side: u16::MAX as usize,
}),
}
}
fn write_image_jpeg_imp<const N: usize>(
file_path: impl AsRef<Path>,
image: &Image<u8, N>,
color_type: ColorType,
quality: u8,
) -> Result<(), IoError> {
let (width, height) = jpeg_dimensions(image.width(), image.height())?;
let encoder = Encoder::new_file(file_path, quality)?;
encoder.encode(image.as_slice(), width, height, color_type)?;
Ok(())
}
pub fn read_image_jpeg_rgb8(file_path: impl AsRef<Path>) -> Result<Rgb8, IoError> {
let img = read_image_jpeg_impl::<3>(file_path)?;
Ok(Rgb8::from_size_vec(img.size(), img.into_vec())?)
}
pub fn read_image_jpeg_mono8(file_path: impl AsRef<Path>) -> Result<Gray8, IoError> {
let img = read_image_jpeg_impl::<1>(file_path)?;
Ok(Gray8::from_size_vec(img.size(), img.into_vec())?)
}
pub fn decode_image_jpeg_rgb8(src: &[u8], dst: &mut Image<u8, 3>) -> Result<(), IoError> {
decode_jpeg_impl(src, dst)
}
pub fn decode_image_jpeg_mono8(src: &[u8], dst: &mut Image<u8, 1>) -> Result<(), IoError> {
decode_jpeg_impl(src, dst)
}
fn read_image_jpeg_impl<const N: usize>(
file_path: impl AsRef<Path>,
) -> Result<Image<u8, N>, IoError> {
use zune_jpeg::zune_core::colorspace::ColorSpace;
use zune_jpeg::zune_core::options::DecoderOptions;
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 = fs::read(file_path)?;
let mut decoder = zune_jpeg::JpegDecoder::new(Cursor::new(&jpeg_data));
decoder.decode_headers()?;
let image_info = decoder.info().ok_or_else(|| {
IoError::JpegDecodingError(zune_jpeg::errors::DecodeErrors::Format(String::from(
"Failed to find image info from its metadata",
)))
})?;
check_image_dimensions(image_info.width as usize, image_info.height as usize)?;
let colorspace = match image_info.components {
1 => ColorSpace::Luma,
3 => ColorSpace::RGB,
n => {
return Err(IoError::JpegDecodingError(
zune_jpeg::errors::DecodeErrors::Format(format!(
"Unsupported JPEG component count: {}. Expected 1 (grayscale) or 3 (RGB)",
n
)),
))
}
};
if image_info.components != N as u8 {
return Err(IoError::JpegDecodingError(
zune_jpeg::errors::DecodeErrors::Format(format!(
"Channel mismatch: JPEG has {} components but requested {}",
image_info.components, N
)),
));
}
let image_size = ImageSize {
width: image_info.width as usize,
height: image_info.height as usize,
};
let options = DecoderOptions::default().jpeg_set_out_colorspace(colorspace);
let mut decoder = zune_jpeg::JpegDecoder::new_with_options(Cursor::new(&jpeg_data), options);
let img_data = decoder.decode()?;
Ok(Image::new(image_size, img_data)?)
}
fn decode_jpeg_impl<const C: usize>(src: &[u8], dst: &mut Image<u8, C>) -> Result<(), IoError> {
use zune_jpeg::zune_core::colorspace::ColorSpace;
use zune_jpeg::zune_core::options::DecoderOptions;
let mut decoder = zune_jpeg::JpegDecoder::new(Cursor::new(src));
decoder.decode_headers()?;
let image_info = decoder.info().ok_or_else(|| {
IoError::JpegDecodingError(zune_jpeg::errors::DecodeErrors::Format(String::from(
"Failed to find image info from its metadata",
)))
})?;
check_image_dimensions(image_info.width as usize, image_info.height as usize)?;
let colorspace = match image_info.components {
1 => ColorSpace::Luma,
3 => ColorSpace::RGB,
n => {
return Err(IoError::JpegDecodingError(
zune_jpeg::errors::DecodeErrors::Format(format!(
"Unsupported JPEG component count: {}. Expected 1 (grayscale) or 3 (RGB)",
n
)),
))
}
};
if image_info.components != C as u8 {
return Err(IoError::JpegDecodingError(
zune_jpeg::errors::DecodeErrors::Format(format!(
"Channel mismatch: JPEG has {} components but destination expects {}",
image_info.components, C
)),
));
}
if [image_info.height as usize, image_info.width as usize] != [dst.height(), dst.width()] {
return Err(IoError::DecodeMismatchResolution(
image_info.height as usize,
image_info.width as usize,
dst.height(),
dst.width(),
));
}
let options = DecoderOptions::default().jpeg_set_out_colorspace(colorspace);
let mut decoder = zune_jpeg::JpegDecoder::new_with_options(Cursor::new(src), options);
decoder.decode_into(dst.as_slice_mut())?;
Ok(())
}
pub fn decode_image_jpeg_layout(src: &[u8]) -> Result<ImageLayout, IoError> {
let mut decoder = zune_jpeg::JpegDecoder::new(Cursor::new(src));
decoder.decode_headers()?;
let image_info = decoder.info().ok_or_else(|| {
IoError::JpegDecodingError(zune_jpeg::errors::DecodeErrors::Format(String::from(
"Failed to find image info from its metadata",
)))
})?;
let size = ImageSize {
width: image_info.width as usize,
height: image_info.height as usize,
};
check_image_dimensions(size.width, size.height)?;
Ok(ImageLayout::new(
size,
image_info.components,
PixelFormat::U8,
))
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::{create_dir_all, read};
#[test]
fn test_read_jpeg() -> Result<(), IoError> {
let image = read_image_jpeg_rgb8("../../tests/data/dog.jpeg")?;
assert_eq!(image.cols(), 258);
assert_eq!(image.rows(), 195);
Ok(())
}
#[test]
fn test_read_write_jpeg() -> Result<(), IoError> {
let tmp_dir = tempfile::tempdir()?;
create_dir_all(tmp_dir.path())?;
let file_path = tmp_dir.path().join("dog.jpeg");
let image_data = read_image_jpeg_rgb8("../../tests/data/dog.jpeg")?;
write_image_jpeg_rgb8(&file_path, &image_data, 100)?;
let image_data_back = read_image_jpeg_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(())
}
#[test]
fn test_decode_jpeg() -> Result<(), IoError> {
let bytes = read("../../tests/data/dog.jpeg")?;
let mut image = Rgb8::from_size_val([258, 195].into(), 0)?;
decode_image_jpeg_rgb8(&bytes, &mut image)?;
assert_eq!(image.cols(), 258);
assert_eq!(image.rows(), 195);
assert_eq!(image.num_channels(), 3);
Ok(())
}
#[test]
fn test_decode_jpeg_size() -> Result<(), IoError> {
let bytes = read("../../tests/data/dog.jpeg")?;
let layout = decode_image_jpeg_layout(bytes.as_slice())?;
assert_eq!(layout.image_size.width, 258);
assert_eq!(layout.image_size.height, 195);
assert_eq!(layout.channels, 3);
Ok(())
}
#[test]
fn test_encode_jpeg_rgb8_with_buffer() -> Result<(), IoError> {
let image = read_image_jpeg_rgb8("../../tests/data/dog.jpeg")?;
let mut buffer = Vec::new();
encode_image_jpeg_rgb8(&image, 100, &mut buffer)?;
assert!(buffer.len() > 2, "JPEG output is too small");
assert_eq!(buffer[0], 0xFF, "Invalid JPEG magic byte 1");
assert_eq!(buffer[1], 0xD8, "Invalid JPEG magic byte 2");
let mut decoded: Image<u8, 3> = Image::from_size_val([258, 195].into(), 0)?;
decode_image_jpeg_rgb8(&buffer, &mut decoded)?;
assert_eq!(decoded.cols(), 258);
assert_eq!(decoded.rows(), 195);
Ok(())
}
#[test]
fn test_encode_jpeg_gray8_with_buffer() -> Result<(), IoError> {
let image = Image::<u8, 1>::from_size_val([258, 195].into(), 128)?;
let mut buffer = Vec::new();
encode_image_jpeg_gray8(&image, 100, &mut buffer)?;
assert!(buffer.len() > 2, "JPEG output is too small");
assert_eq!(buffer[0], 0xFF, "Invalid JPEG magic byte 1");
assert_eq!(buffer[1], 0xD8, "Invalid JPEG magic byte 2");
let mut decoded: Image<u8, 1> = Image::from_size_val([258, 195].into(), 0)?;
decode_image_jpeg_mono8(&buffer, &mut decoded)?;
assert_eq!(decoded.cols(), 258);
assert_eq!(decoded.rows(), 195);
Ok(())
}
#[test]
fn test_encode_jpeg_rejects_side_above_u16() -> Result<(), IoError> {
let image = Image::<u8, 1>::from_size_val([u16::MAX as usize + 1, 1].into(), 0)?;
let mut buffer = Vec::new();
assert!(matches!(
encode_image_jpeg_gray8(&image, 90, &mut buffer),
Err(IoError::DimensionTooLarge {
max_side: 65535,
..
})
));
Ok(())
}
#[test]
fn test_encode_jpeg_buffer_reuse() -> Result<(), IoError> {
let image1 = read_image_jpeg_rgb8("../../tests/data/dog.jpeg")?;
let image2 = Image::<u8, 3>::from_size_val([100, 100].into(), 255)?;
let mut buffer = Vec::new();
encode_image_jpeg_rgb8(&image1, 100, &mut buffer)?;
let size1 = buffer.len();
assert!(size1 > 0, "First encode should produce data");
encode_image_jpeg_rgb8(&image2, 100, &mut buffer)?;
let size2 = buffer.len();
assert!(size2 > 0, "Second encode should produce data");
assert_eq!(buffer[0], 0xFF, "Invalid JPEG magic byte 1");
assert_eq!(buffer[1], 0xD8, "Invalid JPEG magic byte 2");
encode_image_jpeg_rgb8(&image1, 90, &mut buffer)?;
let size3 = buffer.len();
assert!(size3 > 0, "Third encode should produce data");
Ok(())
}
}