use super::arithmetic::{self, ops::CheckedSub, Cast};
use super::types::{self, sides::abs::Sides, Point, Rect, Size};
use super::{defaults, imageops};
use crate::{debug, debug::Instant};
pub use image::ImageFormat;
use std::fs;
use std::io::{BufReader, Seek};
use std::path::{Path, PathBuf};
#[derive(Clone)]
pub struct Image {
pub(crate) inner: image::RgbaImage,
pub(crate) path: Option<PathBuf>,
}
impl std::ops::Deref for Image {
type Target = image::RgbaImage;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl std::ops::DerefMut for Image {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
impl Image {
#[inline]
#[must_use]
pub fn size(&self) -> Size {
Size::from(&self.inner)
}
#[inline]
#[must_use]
pub fn new(width: u32, height: u32) -> Self {
let inner = image::RgbaImage::new(width, height);
Self { inner, path: None }
}
#[inline]
#[must_use]
pub fn with_size(size: impl Into<Size>) -> Self {
let size = size.into();
Self::new(size.width, size.height)
}
#[inline]
#[must_use]
pub fn from_image(image: &image::DynamicImage) -> Self {
let inner = image.to_rgba8();
Self { inner, path: None }
}
#[inline]
pub fn from_reader(reader: impl std::io::BufRead + std::io::Seek) -> Result<Self, ReadError> {
match (|| {
let reader = image::io::Reader::new(reader).with_guessed_format()?;
let inner = reader.decode()?.to_rgba8();
let image = Self { inner, path: None };
Ok::<Self, ReadErrorSource>(image)
})() {
Ok(image) => Ok(image),
Err(err) => Err(ReadError {
path: None,
source: err,
}),
}
}
#[inline]
pub fn open(path: impl Into<PathBuf>) -> Result<Self, ReadError> {
let path = path.into();
let file = fs::OpenOptions::new()
.read(true)
.open(&path)
.map_err(|err| ReadError {
path: Some(path.clone()),
source: err.into(),
})?;
let reader = BufReader::new(&file);
let image = Self::from_reader(reader).map_err(|err| ReadError {
path: Some(path.clone()),
source: err.source,
})?;
Ok(Self {
path: Some(path),
..image
})
}
#[inline]
#[must_use]
pub fn is_portrait(&self) -> bool {
self.size().is_portrait()
}
#[inline]
#[must_use]
pub fn orientation(&self) -> super::Orientation {
self.size().orientation()
}
#[inline]
pub fn sub_image(
&mut self,
rect: &Rect,
) -> Result<image::SubImage<&mut image::RgbaImage>, SubImageError> {
pub struct SubImageRect {
top: u32,
left: u32,
bottom: u32,
right: u32,
}
let image_rect = Rect::from(self.size());
match (|| {
if !image_rect.contains(&rect.top_left()) {
return Err(arithmetic::Error::from(OutOfBoundsError {
bounds: image_rect,
point: rect.top_left(),
}));
}
if !image_rect.contains(&rect.bottom_right()) {
return Err(arithmetic::Error::from(OutOfBoundsError {
bounds: image_rect,
point: rect.bottom_right(),
}));
}
let sub_image_rect = SubImageRect {
top: rect.top.cast::<u32>()?,
left: rect.left.cast::<u32>()?,
bottom: rect.bottom.cast::<u32>()?,
right: rect.right.cast::<u32>()?,
};
Ok::<_, arithmetic::Error>(sub_image_rect)
})() {
Ok(sub_image_rect) => {
use image::GenericImage;
let x = sub_image_rect.left;
let y = sub_image_rect.top;
let width = sub_image_rect.right - sub_image_rect.left;
let height = sub_image_rect.bottom - sub_image_rect.top;
Ok(self.inner.sub_image(x, y, width, height))
}
Err(err) => Err(SubImageError {
rect: *rect,
size: self.size(),
source: err,
}),
}
}
#[inline]
pub fn fill(
&mut self,
color: impl Into<image::Rgba<u8>>,
mode: imageops::FillMode,
) -> Result<(), FillError> {
let rect: Rect = self.size().into();
self.fill_rect(color.into(), &rect, mode)
}
#[inline]
pub fn clip_alpha(&mut self, rect: &Rect, min: u8, max: u8) -> Result<(), SubImageError> {
let sub_image = self.sub_image(rect)?;
imageops::clip_alpha(sub_image, min, max);
Ok(())
}
#[inline]
pub fn fill_rect(
&mut self,
color: impl Into<image::Rgba<u8>>,
rect: &Rect,
mode: imageops::FillMode,
) -> Result<(), FillError> {
let size = self.size();
let color = color.into();
let sub_image = self.sub_image(rect).map_err(|err| FillError {
size,
rect: *rect,
source: err,
})?;
imageops::fill_rect(sub_image, color, mode);
Ok(())
}
#[inline]
pub fn resize_and_crop(
&mut self,
size: impl Into<Size>,
resize_mode: super::ResizeMode,
crop_mode: super::CropMode,
) -> Result<(), ResizeAndCropError> {
let size = size.into();
self.resize(size, resize_mode)?;
self.crop_to_fit(size, crop_mode)?;
Ok(())
}
#[inline]
pub fn fade_out(
&mut self,
start: impl Into<Point>,
end: impl Into<Point>,
axis: super::types::Axis,
) -> Result<(), FadeError> {
use super::types::Axis;
let start = start.into();
let end = end.into();
let switch_direction = match axis {
Axis::X => start.x < end.x,
Axis::Y => start.y < end.y,
};
match (|| {
let sub_image_rect = Rect::from_points(start, end);
let sub_image = self.sub_image(&sub_image_rect)?;
imageops::fade_out(sub_image, axis, switch_direction)?;
Ok::<_, arithmetic::Error>(())
})() {
Ok(_) => Ok(()),
Err(err) => Err(FadeError {
size: self.size(),
start,
end,
axis,
source: err,
}),
}
}
#[inline]
pub fn resize(
&mut self,
size: impl Into<Size>,
mode: super::ResizeMode,
) -> Result<(), ResizeError> {
#[allow(unused_variables)]
let start = Instant::now();
let size = size.into();
let resized = self
.size()
.scale_to(size, mode)
.map_err(|err| ResizeError {
size: self.size(),
target: size,
mode,
source: err,
})?;
let filter = defaults::FILTER_TYPE;
self.inner = imageops::resize(&self.inner, resized.width, resized.height, filter);
debug!(
"fitting to",
resized,
"took",
start.elapsed_millis(),
"msec"
);
Ok(())
}
#[inline]
pub fn overlay(
&mut self,
overlay_image: &impl std::ops::Deref<Target = image::RgbaImage>,
offset: impl Into<Point>,
) {
let offset: Point = offset.into();
imageops::overlay(&mut self.inner, &**overlay_image, offset.x, offset.y);
}
#[inline]
pub fn crop_to_fit(
&mut self,
size: impl Into<Size>,
mode: super::CropMode,
) -> Result<(), CropToFitError> {
let target = size.into();
match (|| {
let rect = self.size().crop_to_fit(target, mode)?;
self.crop(&rect)?;
Ok::<_, arithmetic::Error>(())
})() {
Ok(_) => Ok(()),
Err(err) => Err(CropToFitError {
size: self.size(),
target,
mode,
source: err,
}),
}
}
#[inline]
pub fn crop(&mut self, rect: &Rect) -> Result<(), CropRectError> {
let size = self.size();
self.inner = self
.sub_image(rect)
.map_err(|err| CropRectError {
size,
rect: *rect,
source: err.into(),
})?
.to_image();
Ok(())
}
#[inline]
pub fn crop_sides(&mut self, sides: Sides) -> Result<(), CropSidesError> {
match (|| {
let rect = Rect::from(self.size()).checked_sub(sides)?;
self.crop(&rect)?;
Ok::<_, arithmetic::Error>(())
})() {
Ok(_) => Ok(()),
Err(err) => Err(CropSidesError {
sides,
size: self.size(),
source: err,
}),
}
}
#[inline]
pub fn rotate(&mut self, angle: &super::Rotation) {
use super::Rotation;
if let Some(rotated) = match angle {
Rotation::Rotate0 => None,
Rotation::Rotate90 => Some(imageops::rotate90(&self.inner)),
Rotation::Rotate180 => Some(imageops::rotate180(&self.inner)),
Rotation::Rotate270 => Some(imageops::rotate270(&self.inner)),
} {
self.inner = rotated;
}
}
#[inline]
pub fn rotate_to_orientation(&mut self, orientation: super::Orientation) {
if self.orientation() != orientation {
self.rotate(&super::Rotation::Rotate90);
}
}
#[inline]
pub fn save_with_filename(
&self,
path: impl AsRef<Path>,
quality: impl Into<Option<u8>>,
) -> Result<(), SaveError> {
let path = path.as_ref();
let quality = quality.into();
match (|| {
let format = ImageFormat::from_path(path)?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let mut file = fs::OpenOptions::new()
.read(false)
.write(true)
.create(true)
.truncate(true)
.open(path)?;
self.encode_to(&mut file, format, quality)?;
Ok::<_, SaveErrorSource>(())
})() {
Ok(_) => Ok(()),
Err(err) => Err(SaveError {
path: Some(path.to_path_buf()),
format: ImageFormat::from_path(path).ok(),
quality,
source: err,
}),
}
}
#[inline]
pub fn save(&self, quality: impl Into<Option<u8>>) -> Result<(), SaveError> {
let quality = quality.into();
let (default_output, _) = self.output_path(None);
let path = default_output.ok_or(SaveError {
path: None,
format: None,
quality,
source: SaveErrorSource::MissingOutputPath,
})?;
self.save_with_filename(path, quality)
}
#[inline]
pub fn encode_to(
&self,
w: &mut (impl std::io::Write + Seek),
format: ImageFormat,
quality: impl Into<Option<u8>>,
) -> Result<(), image::ImageError> {
use image::{codecs, ImageEncoder, ImageOutputFormat};
let data = self.inner.as_raw().as_ref();
let color = image::ColorType::Rgba8;
let quality = quality.into();
let width = self.inner.width();
let height = self.inner.height();
match format.into() {
ImageOutputFormat::Png => {
codecs::png::PngEncoder::new(w).write_image(data, width, height, color)
}
ImageOutputFormat::Jpeg(_) => {
let quality = quality.unwrap_or(defaults::JPEG_QUALITY);
codecs::jpeg::JpegEncoder::new_with_quality(w, quality)
.write_image(data, width, height, color)
}
ImageOutputFormat::Gif => {
codecs::gif::GifEncoder::new(w).encode(data, width, height, color)
}
ImageOutputFormat::Ico => {
codecs::ico::IcoEncoder::new(w).write_image(data, width, height, color)
}
ImageOutputFormat::Bmp => {
codecs::bmp::BmpEncoder::new(w).write_image(data, width, height, color)
}
ImageOutputFormat::Tiff => {
codecs::tiff::TiffEncoder::new(w).write_image(data, width, height, color)
}
ImageOutputFormat::Unsupported(msg) => Err(image::error::ImageError::Unsupported(
image::error::UnsupportedError::from_format_and_kind(
image::error::ImageFormatHint::Unknown,
image::error::UnsupportedErrorKind::Format(
image::error::ImageFormatHint::Name(msg),
),
),
)),
_ => Err(image::error::ImageError::Unsupported(
image::error::UnsupportedError::from_format_and_kind(
image::error::ImageFormatHint::Unknown,
image::error::UnsupportedErrorKind::Format(
image::error::ImageFormatHint::Name("missing format".to_string()),
),
),
)),
}?;
Ok(())
}
#[inline]
fn output_path(&self, format: Option<ImageFormat>) -> (Option<PathBuf>, Option<ImageFormat>) {
let source_format = self
.path
.as_ref()
.and_then(|p| ImageFormat::from_path(p).ok());
let format = format.or(source_format);
let ext = format
.unwrap_or(ImageFormat::Jpeg)
.extensions_str()
.iter()
.next()
.unwrap_or(&"jpg");
let path = self.path.as_ref().and_then(|p| {
p.file_stem()
.map(|stem| format!("{}_with_border.{}", &stem.to_string_lossy(), &ext))
.map(|filename| p.with_file_name(filename))
});
(path, format)
}
}
#[derive(thiserror::Error, Debug)]
pub enum ReadErrorSource {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Image(#[from] image::error::ImageError),
}
#[derive(thiserror::Error, Debug)]
#[error("failed to read image from path {path:?}")]
pub struct ReadError {
path: Option<PathBuf>,
source: ReadErrorSource,
}
#[derive(thiserror::Error, Debug)]
pub enum SaveErrorSource {
#[error("missing output file path")]
MissingOutputPath,
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Image(#[from] image::error::ImageError),
}
#[derive(thiserror::Error, Debug)]
#[error("failed to save image to {path:?} with format {format:?} and quality {quality:?}")]
pub struct SaveError {
path: Option<PathBuf>,
format: Option<ImageFormat>,
quality: Option<u8>,
source: SaveErrorSource,
}
#[derive(thiserror::Error, PartialEq, Clone, Debug)]
#[error("failed to fill {rect:#?} of image with size {size:#?}")]
pub struct FillError {
rect: Rect,
size: Size,
source: SubImageError,
}
#[derive(thiserror::Error, PartialEq, Clone, Debug)]
#[error("failed to fade out image with size {size:#?} from {start:#?} to {end:#?} along {axis}")]
pub struct FadeError {
size: Size,
start: Point,
end: Point,
axis: types::Axis,
source: arithmetic::Error,
}
impl arithmetic::error::Arithmetic for FadeError {}
#[derive(thiserror::Error, PartialEq, Clone, Debug)]
#[error("failed to resize image with size {size:#?} to {target:#?} with mode {mode:?}")]
pub struct ResizeError {
size: Size,
target: Size,
mode: super::ResizeMode,
source: types::size::ScaleToError,
}
impl arithmetic::error::Arithmetic for ResizeError {}
#[derive(thiserror::Error, PartialEq, Clone, Debug)]
#[error("failed to crop image with size {size:#?} to fit {target:#?} with mode {mode:?}")]
pub struct CropToFitError {
size: Size,
target: Size,
mode: super::CropMode,
source: arithmetic::Error,
}
impl arithmetic::error::Arithmetic for CropToFitError {}
#[derive(thiserror::Error, PartialEq, Clone, Debug)]
#[error("failed to crop image with size {size:#?} to {rect:#?}")]
pub struct CropRectError {
size: Size,
rect: Rect,
source: arithmetic::Error,
}
impl arithmetic::error::Arithmetic for CropRectError {}
#[derive(thiserror::Error, PartialEq, Clone, Debug)]
#[error("failed to crop image with size {size:#?} by {sides:#?}")]
pub struct CropSidesError {
size: Size,
sides: Sides,
source: arithmetic::Error,
}
impl arithmetic::error::Arithmetic for CropSidesError {}
#[derive(thiserror::Error, Clone, Debug)]
pub enum CropError {
#[error(transparent)]
CropSides(#[from] CropSidesError),
#[error(transparent)]
CropRect(#[from] CropRectError),
#[error(transparent)]
CropToFit(#[from] CropToFitError),
}
#[derive(thiserror::Error, Clone, Debug)]
pub enum ResizeAndCropError {
#[error(transparent)]
Resize(#[from] ResizeError),
#[error(transparent)]
Crop(#[from] CropToFitError),
}
#[derive(thiserror::Error, PartialEq, Clone, Debug)]
#[error("failed to get sub image {rect:#?} for image with size {size:#?}")]
pub struct SubImageError {
size: Size,
rect: Rect,
source: arithmetic::Error,
}
impl arithmetic::error::Arithmetic for SubImageError {}
#[derive(thiserror::Error, PartialEq, Eq, Clone, Debug)]
#[error("point {point:#?} exceeds image bounds {bounds:#?}")]
pub struct OutOfBoundsError {
point: Point,
bounds: Rect,
}
impl arithmetic::error::Arithmetic for OutOfBoundsError {}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("failed to resize image")]
Resize(
#[from]
#[source]
ResizeError,
),
#[error("failed to crop image")]
Crop(
#[from]
#[source]
CropError,
),
#[error("failed to resize and crop image")]
ResizeAndCrop(
#[from]
#[source]
ResizeAndCropError,
),
#[error("failed to get subview of image")]
SubImage(
#[from]
#[source]
SubImageError,
),
#[error("failed to fill image")]
Fill(
#[from]
#[source]
FillError,
),
#[error("failed to fade out image")]
Fade(
#[from]
#[source]
FadeError,
),
#[error("failed to read image")]
Read(
#[from]
#[source]
ReadError,
),
#[error("failed to save image")]
Save(
#[from]
#[source]
SaveError,
),
}
#[cfg(test)]
mod tests {
use super::{Image, ImageFormat};
use image::RgbaImage;
macro_rules! output_path_tests {
($($name:ident: $values:expr,)*) => {
$(
#[test]
fn $name() {
let (path, format, want_path, want_format): (
Option<&str>,
Option<ImageFormat>,
Option<&str>,
Option<ImageFormat>
) = $values;
let img = Image {
inner: RgbaImage::new(32, 32),
path: path.map(Into::into),
};
let (have_path, have_format) = img.output_path(format);
assert_eq!(have_path, want_path.map(Into::into));
assert_eq!(have_format, want_format);
if let Some(p) = have_path {
assert_eq!(
ImageFormat::from_path(p).ok(),
want_format
);
};
}
)*
}
}
output_path_tests! {
test_no_path_no_format: (None, None, None, None),
test_jpg_path_no_format: (
Some("samples/lowres.jpg"), None,
Some("samples/lowres_with_border.jpg"), Some(ImageFormat::Jpeg)
),
test_png_path_no_format: (
Some("samples/lowres.png"), None,
Some("samples/lowres_with_border.png"), Some(ImageFormat::Png)
),
test_no_path_jpg_format: (
None, Some(ImageFormat::Jpeg),
None, Some(ImageFormat::Jpeg)
),
test_no_path_png_format: (
None, Some(ImageFormat::Png),
None, Some(ImageFormat::Png)
),
test_jpg_path_jpg_format: (
Some("samples/lowres.jpg"), Some(ImageFormat::Jpeg),
Some("samples/lowres_with_border.jpg"), Some(ImageFormat::Jpeg)
),
test_jpg_path_png_format: (
Some("samples/lowres.jpg"), Some(ImageFormat::Png),
Some("samples/lowres_with_border.png"), Some(ImageFormat::Png)
),
test_png_path_jpg_format: (
Some("samples/lowres.png"), Some(ImageFormat::Jpeg),
Some("samples/lowres_with_border.jpg"), Some(ImageFormat::Jpeg)
),
test_png_path_png_format: (
Some("samples/lowres.png"), Some(ImageFormat::Png),
Some("samples/lowres_with_border.png"), Some(ImageFormat::Png)
),
}
}