use std::{
error, fmt,
fs::{self, File, OpenOptions},
io::{BufWriter, Write},
path::{Path, PathBuf},
};
use image::{
Frame, GenericImageView, ImageError, ImageFormat,
codecs::{gif::GifEncoder, jpeg::JpegEncoder},
};
use webp_animation::prelude::*;
use crate::util::Image;
type SaveResult<T> = Result<T, SaveError>;
#[derive(Debug)]
pub enum SaveError {
Image(ImageError),
Io(std::io::Error),
#[allow(unused)]
WebpAnimation(webp_animation::Error),
LibWebp(libwebp::error::WebPSimpleError),
#[cfg(feature = "jxl")]
JxlEncode(jpegxl_rs::EncodeError),
}
impl fmt::Display for SaveError {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
SaveError::Image(ref e) => e.fmt(f),
SaveError::Io(ref e) => e.fmt(f),
SaveError::WebpAnimation(_) => write!(f, "error encoding webp"),
SaveError::LibWebp(ref e) => e.fmt(f),
#[cfg(feature = "jxl")]
SaveError::JxlEncode(ref e) => e.fmt(f),
}
}
}
impl error::Error for SaveError {
#[inline]
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match *self {
SaveError::Image(ref e) => Some(e),
SaveError::Io(ref e) => Some(e),
SaveError::WebpAnimation(_) => None,
SaveError::LibWebp(ref e) => Some(e),
#[cfg(feature = "jxl")]
SaveError::JxlEncode(ref e) => Some(e),
}
}
}
impl From<ImageError> for SaveError {
#[inline]
fn from(err: ImageError) -> SaveError {
SaveError::Image(err)
}
}
impl From<std::io::Error> for SaveError {
#[inline]
fn from(err: std::io::Error) -> SaveError {
SaveError::Io(err)
}
}
impl From<webp_animation::Error> for SaveError {
#[inline]
fn from(err: webp_animation::Error) -> SaveError {
SaveError::WebpAnimation(err)
}
}
impl From<libwebp::error::WebPSimpleError> for SaveError {
#[inline]
fn from(err: libwebp::error::WebPSimpleError) -> SaveError {
SaveError::LibWebp(err)
}
}
#[cfg(feature = "jxl")]
impl From<jpegxl_rs::EncodeError> for SaveError {
#[inline]
fn from(err: jpegxl_rs::EncodeError) -> SaveError {
SaveError::JxlEncode(err)
}
}
fn open_file(path: impl AsRef<Path>) -> Result<File, std::io::Error> {
OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(path)
}
fn get_temp_path(path: impl AsRef<Path>) -> PathBuf {
use rand::{distr::Alphanumeric, prelude::*};
let mut id = String::from('.');
id.extend(
rand::rng()
.sample_iter(&Alphanumeric)
.take(8)
.map(char::from),
);
let mut buf = path.as_ref().to_path_buf();
buf.set_file_name(id);
buf
}
#[inline]
pub fn save_with_format(
path: impl AsRef<Path>,
image: &Image,
format: ImageFormat,
) -> SaveResult<()> {
let temp_path = get_temp_path(path.as_ref());
let file = open_file(&temp_path)?;
if let Err(err) = image.buffer().write_to(&mut BufWriter::new(file), format) {
let _ = fs::remove_file(&temp_path);
Err(err)?;
}
if let Err(err) = fs::rename(&temp_path, path) {
let _ = fs::remove_file(&temp_path);
Err(err)?;
}
Ok(())
}
pub fn jpeg(path: impl AsRef<Path>, image: &Image, quality: u8) -> SaveResult<()> {
let temp_path = get_temp_path(path.as_ref());
let file = open_file(&temp_path)?;
let mut encoder = JpegEncoder::new_with_quality(BufWriter::new(file), quality);
let buffer = image.buffer();
encoder.encode(
buffer.as_bytes(),
buffer.width(),
buffer.height(),
buffer.color().into(),
)?;
Ok(fs::rename(temp_path, path)?)
}
#[cfg(feature = "jxl")]
pub fn jpeg_xl(path: impl AsRef<Path>, image: &Image, quality: f32, lossy: bool) -> SaveResult<()> {
use std::io;
use image::DynamicImage;
use jpegxl_rs::encode::{self, EncoderFrame, EncoderResult, EncoderSpeed};
use crate::util::HasAlpha;
let temp_path = get_temp_path(path.as_ref());
let mut file = open_file(&temp_path)?;
let mut encoder = encode::encoder_builder()
.quality(quality)
.lossless(!lossy)
.speed(EncoderSpeed::Kitten)
.use_container(true)
.has_alpha(image.buffer().has_alpha())
.build()?;
let data = match image.buffer() {
DynamicImage::ImageRgb8(image_buffer) => {
let frame = EncoderFrame::new(image_buffer.as_raw()).num_channels(3);
let result: EncoderResult<u8> =
encoder.encode_frame(&frame, image_buffer.width(), image_buffer.height())?;
result.data
}
DynamicImage::ImageRgba8(image_buffer) => {
let frame = EncoderFrame::new(image_buffer.as_raw()).num_channels(4);
let result: EncoderResult<u8> =
encoder.encode_frame(&frame, image_buffer.width(), image_buffer.height())?;
result.data
}
DynamicImage::ImageRgb16(image_buffer) => {
let frame = EncoderFrame::new(image_buffer.as_raw()).num_channels(3);
let result: EncoderResult<u16> =
encoder.encode_frame(&frame, image_buffer.width(), image_buffer.height())?;
result.data
}
DynamicImage::ImageRgba16(image_buffer) => {
let frame = EncoderFrame::new(image_buffer.as_raw()).num_channels(4);
let result: EncoderResult<u16> =
encoder.encode_frame(&frame, image_buffer.width(), image_buffer.height())?;
result.data
}
DynamicImage::ImageRgb32F(image_buffer) => {
let frame = EncoderFrame::new(image_buffer.as_raw()).num_channels(3);
let result: EncoderResult<f32> =
encoder.encode_frame(&frame, image_buffer.width(), image_buffer.height())?;
result.data
}
DynamicImage::ImageRgba32F(image_buffer) => {
let frame = EncoderFrame::new(image_buffer.as_raw()).num_channels(4);
let result: EncoderResult<f32> =
encoder.encode_frame(&frame, image_buffer.width(), image_buffer.height())?;
result.data
}
_ => {
return Err(
io::Error::new(io::ErrorKind::InvalidInput, "Unsupported color format").into(),
);
}
};
file.write_all(&data)?;
Ok(fs::rename(temp_path, path)?)
}
pub fn gif(path: impl AsRef<Path>, images: Vec<Image>) -> SaveResult<()> {
let temp_path = get_temp_path(path.as_ref());
let file = open_file(&temp_path)?;
let frames: Vec<Frame> = images.into_iter().map(|image| image.into()).collect();
let mut encoder = GifEncoder::new(BufWriter::new(file));
encoder.encode_frames(frames)?;
Ok(fs::rename(temp_path, path)?)
}
pub fn webp_animation(
path: impl AsRef<Path>,
images: Vec<Image>,
quality: f32,
lossy: bool,
) -> SaveResult<()> {
let encoding_type = if lossy {
EncodingType::Lossy(LossyEncodingConfig::default())
} else {
EncodingType::Lossless
};
let config = EncodingConfig {
encoding_type,
quality,
method: 6,
};
let dimensions = images[0].buffer().dimensions();
let options = EncoderOptions {
encoding_config: Some(config),
..Default::default()
};
let mut encoder = Encoder::new_with_options(dimensions, options)?;
let mut timestamp: i32 = 0;
for image in images {
encoder.add_frame(&image.buffer().to_rgba8().into_raw(), timestamp)?;
timestamp += image.delay.as_millis() as i32;
}
let webp_data = encoder.finalize(timestamp)?;
let temp_path = get_temp_path(path.as_ref());
let mut file = open_file(&temp_path)?;
file.write_all(&webp_data)?;
Ok(fs::rename(temp_path, path)?)
}
pub fn webp(path: impl AsRef<Path>, image: &Image, quality: f32, lossy: bool) -> SaveResult<()> {
let (width, height) = image.buffer().dimensions();
let webp_data = if lossy {
libwebp::WebPEncodeLosslessRGBA(
&image.buffer().to_rgba8().into_raw(),
width,
height,
width * 4,
)?
} else {
libwebp::WebPEncodeRGBA(
&image.buffer().to_rgba8().into_raw(),
width,
height,
width * 4,
quality,
)?
};
let temp_path = get_temp_path(path.as_ref());
let mut file = open_file(&temp_path)?;
file.write_all(&webp_data)?;
Ok(fs::rename(temp_path, path)?)
}