You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
snekdown/src/utils/image_converting.rs

250 lines
7.1 KiB
Rust

/*
* Snekdown - Custom Markdown flavour and parser
* Copyright (C) 2021 Trivernis
* See LICENSE for more information.
*/
use crate::elements::Metadata;
use crate::utils::caching::CacheStorage;
use crate::utils::downloads::download_path;
use image::imageops::FilterType;
use image::io::Reader as ImageReader;
use image::{GenericImageView, ImageFormat, ImageResult};
use indicatif::{ProgressBar, ProgressStyle};
use mime::Mime;
use parking_lot::Mutex;
use rayon::prelude::*;
use std::io;
use std::io::Cursor;
use std::path::PathBuf;
use std::sync::Arc;
#[derive(Clone, Debug)]
pub struct ImageConverter {
images: Vec<Arc<Mutex<PendingImage>>>,
target_format: Option<ImageFormat>,
target_size: Option<(u32, u32)>,
}
impl ImageConverter {
pub fn new() -> Self {
Self {
images: Vec::new(),
target_format: None,
target_size: None,
}
}
pub fn set_target_size(&mut self, target_size: (u32, u32)) {
self.target_size = Some(target_size)
}
pub fn set_target_format(&mut self, target_format: ImageFormat) {
self.target_format = Some(target_format);
}
/// Adds an image to convert
pub fn add_image(&mut self, path: PathBuf) -> Arc<Mutex<PendingImage>> {
let image = Arc::new(Mutex::new(PendingImage::new(path)));
self.images.push(image.clone());
image
}
/// Converts all images
pub fn convert_all(&mut self) {
let pb = Arc::new(Mutex::new(ProgressBar::new(self.images.len() as u64)));
pb.lock().set_style(
ProgressStyle::default_bar()
.template("Processing images: [{bar:40.cyan/blue}]")
.progress_chars("=> "),
);
self.images.par_iter().for_each(|image| {
let mut image = image.lock();
if let Err(e) = image.convert(self.target_format.clone(), self.target_size.clone()) {
log::error!("Failed to embed image {:?}: {}", image.path, e)
}
pb.lock().tick();
});
pb.lock().finish_and_clear();
}
}
#[derive(Clone, Debug)]
pub struct PendingImage {
pub path: PathBuf,
pub data: Option<Vec<u8>>,
cache: CacheStorage,
pub mime: Mime,
brightness: Option<i32>,
contrast: Option<f32>,
huerotate: Option<i32>,
grayscale: bool,
invert: bool,
}
impl PendingImage {
pub fn new(path: PathBuf) -> Self {
let mime = get_mime(&path);
Self {
path,
data: None,
cache: CacheStorage::new(),
mime,
brightness: None,
contrast: None,
grayscale: false,
invert: false,
huerotate: None,
}
}
pub fn assign_from_meta<M: Metadata>(&mut self, meta: &M) {
if let Some(brightness) = meta.get_integer("brightness") {
self.brightness = Some(brightness as i32);
}
if let Some(contrast) = meta.get_float("contrast") {
self.contrast = Some(contrast as f32);
}
if let Some(huerotate) = meta.get_float("huerotate") {
self.huerotate = Some(huerotate as i32);
}
self.grayscale = meta.get_bool("grayscale");
self.invert = meta.get_bool("invert");
}
/// Converts the image to the specified target format (specified by target_extension)
pub fn convert(
&mut self,
target_format: Option<ImageFormat>,
target_size: Option<(u32, u32)>,
) -> ImageResult<()> {
let format = target_format
.or_else(|| {
self.path
.extension()
.and_then(|extension| ImageFormat::from_extension(extension))
})
.unwrap_or(ImageFormat::Png);
let output_path = self.get_output_path(format, target_size);
self.mime = get_mime(&output_path);
if self.cache.has_file(&output_path) {
self.data = Some(self.cache.read(&output_path)?)
} else {
self.convert_image(format, target_size)?;
if let Some(data) = &self.data {
self.cache.write(&output_path, data)?;
}
}
Ok(())
}
/// Converts the image
fn convert_image(
&mut self,
format: ImageFormat,
target_size: Option<(u32, u32)>,
) -> ImageResult<()> {
let mut image = ImageReader::open(self.get_path()?)?.decode()?;
if let Some((width, height)) = target_size {
let dimensions = image.dimensions();
if dimensions.0 > width || dimensions.1 > height {
image = image.resize(width, height, FilterType::Lanczos3);
}
}
if let Some(brightness) = self.brightness {
image = image.brighten(brightness);
}
if let Some(contrast) = self.contrast {
image = image.adjust_contrast(contrast);
}
if let Some(rotate) = self.huerotate {
image = image.huerotate(rotate);
}
if self.grayscale {
image = image.grayscale();
}
if self.invert {
image.invert();
}
let data = Vec::new();
let mut writer = Cursor::new(data);
image.write_to(&mut writer, format)?;
self.data = Some(writer.into_inner());
Ok(())
}
/// Returns the path of the file
fn get_path(&self) -> io::Result<PathBuf> {
if !self.path.exists() {
if self.cache.has_file(&self.path) {
return Ok(self.cache.get_file_path(&self.path));
}
if let Some(data) = download_path(self.path.to_string_lossy().to_string()) {
self.cache.write(&self.path, data)?;
return Ok(self.cache.get_file_path(&self.path));
}
}
Ok(self.path.clone())
}
/// Returns the output file name after converting the image
fn get_output_path(
&self,
target_format: ImageFormat,
target_size: Option<(u32, u32)>,
) -> PathBuf {
let mut path = self.path.clone();
let mut file_name = path.file_stem().unwrap().to_string_lossy().to_string();
let extension = target_format.extensions_str()[0];
let type_name = format!("{:?}", target_format);
if let Some(target_size) = target_size {
file_name += &*format!("-w{}-h{}", target_size.0, target_size.1);
}
if let Some(b) = self.brightness {
file_name += &*format!("-b{}", b);
}
if let Some(c) = self.contrast {
file_name += &*format!("-c{}", c);
}
if let Some(h) = self.huerotate {
file_name += &*format!("-h{}", h);
}
file_name += &*format!("{}-{}", self.invert, self.grayscale);
file_name += format!("-{}", type_name).as_str();
path.set_file_name(file_name);
path.set_extension(extension);
path
}
}
fn get_mime(path: &PathBuf) -> Mime {
let mime = mime_guess::from_ext(
path.clone()
.extension()
.and_then(|e| e.to_str())
.unwrap_or("png"),
)
.first()
.unwrap_or(mime::IMAGE_PNG);
mime
}