#![allow(clippy::too_many_lines)]
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::unsafe_derive_deserialize)]
pub mod arithmetic;
pub mod border;
#[cfg(feature = "builtin")]
pub mod builtin;
pub mod debug;
pub mod defaults;
pub mod error;
pub mod imageops;
pub mod img;
pub mod options;
#[cfg(test)]
mod test;
pub mod types;
#[cfg(feature = "wasm")]
pub mod wasm;
pub use border::Border;
pub use image::ImageFormat;
pub use imageops::FillMode;
pub use img::Image;
pub use options::*;
pub use sides::{abs::Sides, percent::Sides as SidesPercent};
pub use types::*;
use arithmetic::{
ops::{CheckedAdd, CheckedDiv, CheckedMul, CheckedSub},
Cast, Round,
};
use serde::Serialize;
use std::path::PathBuf;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
#[derive(Serialize, PartialEq, Clone, Debug)]
pub struct ResultSize {
output_size: Size,
content_size: Size,
margins: Sides,
frame_width: Sides,
scale_factor: f32,
}
pub struct ImageBorders {
images: Vec<img::Image>,
}
impl ImageBorders {
#[inline]
pub fn new(images: impl IntoIterator<Item = img::Image>) -> Result<ImageBorders, Error> {
let images: Vec<img::Image> = images.into_iter().collect();
if images.is_empty() {
Err(Error::MissingImage)
} else {
Ok(ImageBorders { images })
}
}
#[inline]
#[must_use]
pub fn single(img: img::Image) -> ImageBorders {
ImageBorders { images: vec![img] }
}
#[inline]
pub fn from_reader(reader: impl std::io::BufRead + std::io::Seek) -> Result<Self, Error> {
let img = Image::from_reader(reader)?;
Ok(Self::single(img))
}
#[inline]
pub fn open(path: impl Into<PathBuf>) -> Result<Self, Error> {
let img = Image::open(path)?;
Ok(Self::single(img))
}
#[inline]
pub fn render(
&mut self,
border_kind: impl Into<Option<border::Kind>>,
options: &Options,
) -> Result<img::Image, RenderError> {
let border_kind = border_kind.into();
let mut images: Vec<img::Image> = self.images.clone();
let primary = images.get_mut(0).ok_or(RenderError::MissingImage)?;
prepare_primary(primary, options)?;
let mut border = border_for_primary(border_kind, primary, options)?;
let result_size = compute_result_size(&border, &*primary, options)?;
debug!(&result_size);
let mut result_image = img::Image {
path: primary.path.clone(),
..img::Image::with_size(result_size.output_size)
};
result_image
.fill(options.background_color(), FillMode::Set)
.map_err(img::Error::from)?;
let content_rect = result_size
.output_size
.center(result_size.content_size)
.map_err(|err| error::Arithmetic {
msg: "failed to center content size".to_string(),
source: err.into(),
})?;
debug!(&content_rect);
#[cfg(feature = "debug")]
debug_fill(
&mut result_image,
"content size",
Color::rgba(0, 0, 255, 100),
&content_rect,
)?;
let content_rect_sub_margins =
content_rect
.checked_sub(result_size.margins)
.map_err(|err| error::Arithmetic {
msg: "failed to compute content rect without margins".into(),
source: err.into(),
})?;
result_image
.fill_rect(
options.frame_color,
&content_rect_sub_margins,
FillMode::Set,
)
.map_err(img::Error::from)?;
let border_rect = content_rect_sub_margins
.checked_sub(result_size.frame_width)
.map_err(|err| error::Arithmetic {
msg: "failed to compute border rect".into(),
source: err.into(),
})?;
debug!(&border_rect);
let border_size = border_rect.size().map_err(|err| error::Arithmetic {
msg: "failed to compute border size".into(),
source: err.into(),
})?;
#[cfg(feature = "debug")]
debug_fill(
&mut result_image,
"border size",
Color::rgba(0, 255, 0, 100),
&border_rect,
)?;
let primary_component = Rect::from(border_size);
debug!("overlay content");
match options.mode {
FitMode::Image => {
let primary_component = vec![primary_component];
let components = match border {
Some(ref mut border) => {
border.resize_and_crop(border_size, ResizeMode::Contain)?;
let default_image = primary.clone();
let components = border.transparent_components();
images.resize(components.len(), default_image);
components.iter().zip(images.iter_mut())
}
None => primary_component.iter().zip(images.iter_mut()),
};
for (idx, (component_rect, component)) in components.enumerate() {
draw_component(&mut result_image, component, component_rect, &border_rect)
.map_err(|err| RenderComponentError {
idx,
rect: *component_rect,
size: component.size(),
source: err.into(),
})?;
}
if let Some(border) = border {
result_image.overlay(&*border, border_rect.top_left());
}
}
FitMode::Border => {
let primary_component_rect = match border {
Some(ref mut border) => {
border.resize_and_crop(border_size, ResizeMode::Contain)?;
border.content_rect().map_err(border::Error::from)?
}
None => &primary_component,
};
draw_component(
&mut result_image,
primary,
primary_component_rect,
&border_rect,
)
.map_err(|err| RenderComponentError {
idx: 0,
rect: *primary_component_rect,
size: primary.size(),
source: err.into(),
})?;
if let Some(border) = border {
result_image.overlay(&*border, border_rect.top_left());
}
}
};
if options.preview {
overlay_visible_area(&mut result_image)?;
}
Ok(result_image)
}
}
#[inline]
fn compute_pre_result_size(
border: &Option<border::Border>,
primary: &img::Image,
options: &Options,
) -> Result<ResultSize, ResultSizeError> {
let scale_factor = options.scale_factor.clamp(0.0, 1.0);
let margin_factor = f64::from(options.margin).max(0.0);
let original_content_size = match border {
Some(border) => match options.mode {
FitMode::Image => border.size_for(primary.size())?,
FitMode::Border => border.size(),
},
None => primary.size(),
};
debug!(&primary.size());
debug!(&original_content_size);
let base = original_content_size.min_dim();
let frame_width: Sides =
options
.frame_width
.checked_mul(base)
.map_err(|err| error::Arithmetic {
msg: "failed to compute original frame width".to_string(),
source: err.into(),
})?;
debug!(&frame_width);
let margin = (|| {
let margin = CheckedMul::checked_mul(margin_factor, f64::from(base))?;
let margin = margin.cast::<u32>()?;
Ok::<_, arithmetic::Error>(margin)
})();
let margin = margin.map_err(|err| error::Arithmetic {
msg: "failed to compute original margin width".to_string(),
source: err,
})?;
let margins = Sides::uniform(margin);
debug!(&margins);
let content_size = original_content_size
.checked_add(frame_width)
.and_then(|size| size.checked_add(margins))
.map_err(|err| error::Arithmetic {
msg: "failed to compute content size".to_string(),
source: err.into(),
})?;
debug!(&content_size);
let default_output_size = content_size
.scale_by::<_, Round>(1.0 / scale_factor)
.map_err(|err| error::Arithmetic {
msg: "failed to compute default output size".to_string(),
source: err.into(),
})?;
debug!(&default_output_size);
let output_size = match options.output_size {
BoundedSize {
width: Some(width),
height: Some(height),
} => Size { width, height },
_ => default_output_size
.scale_to_bounds(options.output_size, ResizeMode::Contain)
.map_err(|err| error::Arithmetic {
msg: "failed to compute output size".to_string(),
source: err.into(),
})?,
};
let output_size = output_size
.scale_to_bounds(options.output_size_bounds, ResizeMode::Contain)
.map_err(|err| error::Arithmetic {
msg: "failed to bound output size".to_string(),
source: err.into(),
})?;
debug!(&output_size);
Ok(ResultSize {
output_size,
content_size,
margins,
frame_width,
scale_factor,
})
}
#[inline]
fn compute_result_size(
border: &Option<border::Border>,
primary: &img::Image,
options: &Options,
) -> Result<ResultSize, ResultSizeError> {
let pre_result_size = compute_pre_result_size(border, primary, options)?;
let post_content_size_scale = pre_result_size
.output_size
.checked_mul(pre_result_size.scale_factor)
.map_err(|err| error::Arithmetic {
msg: "failed to compute scaled content size".into(),
source: err.into(),
})?;
let pre_content_size = pre_result_size.content_size;
let post_content_size = pre_content_size
.scale_to(post_content_size_scale, ResizeMode::Contain)
.map_err(|err| error::Arithmetic {
msg: "failed to compute scaled content size".into(),
source: err.into(),
})?;
debug!(&post_content_size);
let pre_base = f64::from(pre_content_size.min_dim());
let post_base = f64::from(post_content_size.min_dim());
let scale = CheckedDiv::checked_div(post_base, pre_base).map_err(|err| error::Arithmetic {
msg: "failed to compute post base scale".into(),
source: err.into(),
})?;
debug!(&scale);
let frame_width = pre_result_size
.frame_width
.checked_mul(scale)
.map_err(|err| error::Arithmetic {
msg: "failed to compute scaled frame width".into(),
source: err.into(),
})?;
debug!(&frame_width);
let margins = pre_result_size
.margins
.checked_mul(scale)
.map_err(|err| error::Arithmetic {
msg: "failed to compute scaled margins".into(),
source: err.into(),
})?;
debug!(&margins);
Ok(ResultSize {
content_size: post_content_size,
margins,
frame_width,
..pre_result_size
})
}
#[inline]
fn border_for_primary(
border_kind: Option<border::Kind>,
primary: &img::Image,
options: &Options,
) -> Result<Option<Border>, RenderError> {
let mut border = match border_kind {
Some(border_kind) => {
let mut border = border_kind.into_border()?;
border.rotate_to_orientation(primary.orientation())?;
border.rotate(&options.border_rotation)?;
Some(border)
}
None => None,
};
if let Some(ref mut border) = border {
if let FitMode::Border = options.mode {
*border = Border::custom(border.clone(), primary.size(), None)
.map_err(border::Error::from)?;
}
}
Ok(border)
}
#[inline]
fn prepare_primary(primary: &mut img::Image, options: &Options) -> Result<(), PreparePrimaryError> {
primary.rotate(&options.image_rotation);
if let Some(crop_percent) = options.crop {
let crop = crop_percent
.checked_mul(primary.size())
.map_err(|err| error::Arithmetic {
msg: "failed to compute crop from relative crop".to_string(),
source: err.into(),
})?;
primary
.crop_sides(crop)
.map_err(img::CropError::from)
.map_err(img::Error::from)?;
};
Ok(())
}
#[cfg(feature = "debug")]
fn draw_text_mut(
image: &mut img::Image,
text: &str,
color: impl Into<image::Rgba<u8>>,
top_left: Point,
) -> Result<(), RenderError> {
use rusttype::{Font, Scale};
lazy_static::lazy_static! {
pub static ref INTER: Font<'static> = {
let font_data = include_bytes!("../fonts/Inter-Regular.ttf");
Font::try_from_bytes(font_data).expect("read font bytes")
};
};
let top_left = (|| {
let top_left = top_left.checked_add(Point { x: 3, y: 3 })?;
let x = top_left.x.cast::<i32>()?;
let y = top_left.y.cast::<i32>()?;
Ok::<_, arithmetic::Error>((x, y))
})();
let (x, y) = top_left.map_err(|err| error::Arithmetic {
msg: format!("failed to compute top left point for text `{}`", text),
source: err,
})?;
let scale = image
.size()
.max_dim()
.cast::<f32>()
.map_err(arithmetic::Error::from)
.and_then(|max_dim| CheckedMul::checked_mul(max_dim, 0.03).map_err(arithmetic::Error::from))
.map_err(|err| error::Arithmetic {
msg: "failed to compute text scale".into(),
source: err,
})?;
let scale = Scale::uniform(scale);
imageproc::drawing::draw_text_mut(&mut **image, color.into(), x, y, scale, &INTER, text);
Ok(())
}
#[inline]
fn overlay_visible_area(image: &mut img::Image) -> Result<(), RenderError> {
let size = image.size();
let preview_size = Size {
width: size.min_dim(),
height: size.min_dim(),
};
let preview_rect = size.center(preview_size).map_err(|err| error::Arithmetic {
msg: "failed to compute centered preview rect".into(),
source: err.into(),
})?;
let transparent_red = Color::rgba(255, 0, 0, 50);
image
.fill_rect(transparent_red, &preview_rect, FillMode::Blend)
.map_err(img::Error::from)?;
Ok(())
}
#[cfg(feature = "debug")]
#[inline]
fn debug_fill(
image: &mut img::Image,
text: &str,
color: impl Into<image::Rgba<u8>>,
rect: &Rect,
) -> Result<(), RenderError> {
image
.fill_rect(color, rect, FillMode::Blend)
.map_err(img::Error::from)?;
let black = Color::black();
draw_text_mut(image, text, black, rect.top_left())?;
Ok(())
}
#[inline]
fn draw_component(
image: &mut img::Image,
component: &mut img::Image,
component_rect: &Rect,
border_rect: &Rect,
) -> Result<(), RenderError> {
debug!("drawing", &component_rect);
let component_rect = (|| {
let mut component_rect = component_rect.checked_add(border_rect.top_left())?;
component_rect = component_rect.padded(3)?;
component_rect = component_rect.clamp(border_rect);
Ok::<_, arithmetic::Error>(component_rect)
})();
let component_rect = component_rect.map_err(|err| error::Arithmetic {
msg: "failed to compute component rect".into(),
source: err,
})?;
let component_size = component_rect.size().map_err(|err| error::Arithmetic {
msg: "failed to compute component rect size".into(),
source: err.into(),
})?;
let center_offset = component_rect
.center_offset_to(border_rect)
.map_err(|err| error::Arithmetic {
msg: "failed to compute center offset of component".into(),
source: err.into(),
})?;
#[cfg(feature = "debug")]
debug_fill(
image,
"component",
Color::rgba(255, 255, 0, 100),
&component_rect,
)?;
#[cfg(feature = "debug")]
{
let mut component = component.clone();
component
.clip_alpha(&Rect::from(component.size()), 0, 60)
.map_err(img::Error::from)?;
component
.resize(component_size, ResizeMode::Cover)
.map_err(img::Error::from)?;
let uncropped_offset =
component_size
.center(component.size())
.map_err(|err| error::Arithmetic {
msg: "failed to compute center offset of uncropped component".into(),
source: err.into(),
})?;
debug!(&uncropped_offset);
let uncropped_component_top_left = component_rect
.top_left()
.checked_add(uncropped_offset.top_left())
.map_err(|err| error::Arithmetic {
msg: "failed to compute top left of uncropped component".into(),
source: err.into(),
})?;
image.overlay(&component, uncropped_component_top_left);
}
component
.resize_and_crop(
component_size,
ResizeMode::Cover,
CropMode::Custom {
x: center_offset.x,
y: center_offset.y,
},
)
.map_err(img::Error::from)?;
assert_eq!(component_size, component.size());
image.overlay(component, component_rect.top_left());
Ok(())
}
#[derive(thiserror::Error, Debug)]
pub enum PreparePrimaryError {
#[error(transparent)]
Arithmetic(#[from] error::Arithmetic),
#[error(transparent)]
Image(#[from] img::Error),
}
#[derive(thiserror::Error, Debug)]
pub enum ResultSizeError {
#[error(transparent)]
Arithmetic(#[from] error::Arithmetic),
#[error(transparent)]
Border(#[from] border::Error),
}
#[derive(thiserror::Error, Debug)]
#[error("failed to render component {idx} with size {size:#?} in {rect:#?}")]
pub struct RenderComponentError {
idx: usize,
rect: Rect,
size: Size,
source: Box<RenderError>,
}
#[derive(thiserror::Error, Debug)]
pub enum RenderError {
#[error("missing input image")]
MissingImage,
#[error(transparent)]
Image(#[from] img::Error),
#[error(transparent)]
RenderComponent(#[from] RenderComponentError),
#[error(transparent)]
Arithmetic(#[from] error::Arithmetic),
#[error("failed to compute result size")]
ResultSize(
#[from]
#[source]
ResultSizeError,
),
#[error("failed to prepare primary image")]
PreparePrimary(
#[from]
#[source]
PreparePrimaryError,
),
#[error(transparent)]
Border(#[from] border::Error),
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("missing input image")]
MissingImage,
#[error("failed to read image")]
Read(
#[from]
#[source]
img::ReadError,
),
#[error(transparent)]
Image(#[from] img::Error),
#[error("render error")]
Render(
#[from]
#[source]
RenderError,
),
}
#[cfg(test)]
mod tests {
use super::border::{self, Border};
#[cfg(feature = "builtin")]
use super::{builtin, ImageFormat};
use super::{types, ImageBorders, Options};
use anyhow::Result;
#[cfg(feature = "builtin")]
use std::io::Cursor;
use std::path::PathBuf;
lazy_static::lazy_static! {
pub static ref OPTIONS: Options = Options {
output_size: types::BoundedSize {
width: Some(2000),
height: Some(2000),
},
mode: types::FitMode::Border,
crop: Some(types::sides::percent::Sides::uniform(0.05)),
scale_factor: 0.90,
frame_width: types::sides::percent::Sides::uniform(0.1),
margin: 0.1,
..Default::default()
};
}
macro_rules! format_tests {
($($name:ident: $values:expr,)*) => {
$(
#[cfg(feature = "builtin")]
#[test]
fn $name() -> Result<()> {
let (infile, outfile, options) = $values;
let repo: PathBuf = env!("CARGO_MANIFEST_DIR").into();
let input = repo.join(&infile);
let output = repo.join(&outfile);
assert!(input.is_file());
let mut borders = ImageBorders::open(&input)?;
let border = border::Kind::Builtin(builtin::Builtin::Border120_1);
let result = borders.render(Some(border), options)?;
result.save_with_filename(&output, None)?;
assert!(output.is_file());
Ok(())
}
)*
}
}
format_tests! {
test_open_and_save_jpg_to_jpg: (
"samples/lowres.jpg", "testing/lowres_jpg.jpg", &OPTIONS),
test_open_and_save_jpg_to_png: (
"samples/lowres.jpg", "testing/lowres_jpg.png", &OPTIONS),
test_open_and_save_jpg_to_tiff: (
"samples/lowres.jpg", "testing/lowres_jpg.tiff", &OPTIONS),
test_open_and_save_png_to_jpg: (
"samples/lowres.png", "testing/lowres_png.jpg", &OPTIONS),
test_open_and_save_png_to_png: (
"samples/lowres.png", "testing/lowres_png.png", &OPTIONS),
test_open_and_save_png_to_tiff: (
"samples/lowres.png", "testing/lowres_png.tiff", &OPTIONS),
test_open_and_save_tiff_to_jpg: (
"samples/lowres.tiff", "testing/lowres_png.jpg", &OPTIONS),
test_open_and_save_tiff_to_png: (
"samples/lowres.tiff", "testing/lowres_png.png", &OPTIONS),
test_open_and_save_tiff_to_tiff: (
"samples/lowres.tiff", "testing/lowres_png.tiff", &OPTIONS),
test_default_options: (
"samples/lowres.jpg", "testing/lowres_default.jpg", &Options::default()),
}
#[cfg(feature = "builtin")]
#[test]
fn test_read_write_in_memory() -> Result<()> {
let bytes = include_bytes!("../samples/lowres.jpg");
let input = Cursor::new(&bytes);
let mut borders = ImageBorders::from_reader(input)?;
let border = border::Kind::Builtin(builtin::Builtin::Border120_1);
let result = borders.render(Some(border), &OPTIONS)?;
let mut output = Cursor::new(Vec::new());
result.encode_to(&mut output, ImageFormat::Png, None)?;
assert!(output.position() > 100);
Ok(())
}
#[test]
fn test_custom_border() -> Result<()> {
let repo: PathBuf = env!("CARGO_MANIFEST_DIR").into();
let input = repo.join("samples/lowres.jpg");
let border_file = repo.join("samples/borders/border1.png");
let output = repo.join("testing/lowres_custom_border.jpg");
assert!(input.is_file());
assert!(border_file.is_file());
let border = border::Kind::Custom(Border::open(&border_file, None)?);
let mut borders = ImageBorders::open(&input)?;
let result = borders.render(Some(border), &OPTIONS)?;
result.save_with_filename(&output, None)?;
assert!(output.is_file());
Ok(())
}
}