use std::convert::{TryFrom, TryInto};
use std::io::BufReader;
use std::path::Path;
use convert_case::{Case, Casing};
use image::{DynamicImage, RgbImage};
use itertools::Itertools;
use pyo3::prelude::*;
use pyo3::types::PyList;
use regex::Regex;
use serde::Deserialize;
use crate::config::{Element, Replacement};
use crate::errors::{RahmenError, RahmenResult};
use crate::exiv2::Metadata;
pub trait Provider<D> {
fn next_image(&mut self) -> RahmenResult<Option<D>>;
}
impl<D> Provider<D> for Box<dyn Provider<D>> {
fn next_image(&mut self) -> RahmenResult<Option<D>> {
(**self).next_image()
}
}
fn load_jpeg<P: AsRef<Path>>(path: P, max_size: Option<usize>) -> RahmenResult<DynamicImage> {
let mut d = mozjpeg::Decompress::with_markers(mozjpeg::ALL_MARKERS).from_path(&path)?;
if let Some(max_size) = max_size {
let mut scale = 8;
let ratio_to_max_size = max_size as f32 / (d.width() * d.height()) as f32;
if ratio_to_max_size < 1. {
scale = (ratio_to_max_size * 8.) as u8 + 1;
}
d.scale(scale);
}
let mut decompress_started = d.to_colorspace(mozjpeg::ColorSpace::JCS_EXT_RGB)?;
let width = decompress_started.width();
let height = decompress_started.height();
let pixels: Vec<u8> = decompress_started.read_scanlines::<u8>()?;
RgbImage::from_raw(width as _, height as _, pixels)
.map(DynamicImage::ImageRgb8)
.ok_or_else(|| {
error!("Failed to decode image: {:?}", path.as_ref());
RahmenError::Retry
})
}
pub fn load_image_from_path<P: AsRef<Path>>(
path: P,
max_size: Option<usize>,
) -> RahmenResult<DynamicImage> {
let _t = crate::Timer::new(|e| debug!("Loading {}ms", e.as_millis()));
info!("Loading {:?}", path.as_ref());
match image::ImageFormat::from_path(&path)? {
image::ImageFormat::Jpeg => load_jpeg(path, max_size),
format => {
image::ImageReader::with_format(BufReader::new(std::fs::File::open(&path)?), format)
.decode()
.map_err(Into::into)
}
}
}
#[derive(Debug, Deserialize, Clone)]
pub struct LineSettings {
pub separator: String,
pub uniquify: bool,
}
pub fn str_to_case(s: String) -> RahmenResult<Case<'static>> {
let case_str = s.to_case(Case::Flat);
for case in Case::all_cases() {
if case_str == format!("{:?}", case).to_case(Case::Flat) {
return Ok(*case);
}
}
Err(RahmenError::CaseUnknown(s))
}
#[derive(Debug)]
enum StatusLineTransformation {
RegexReplace(Box<(Regex, String)>),
Capitalize,
ChangeCase(Case<'static>, Case<'static>),
}
impl StatusLineTransformation {
fn transform<S: AsRef<str>>(&self, input: S) -> String {
match self {
Self::RegexReplace(re) => re.0.replace_all(input.as_ref(), re.1.as_str()).into_owned(),
Self::Capitalize => input.as_ref().from_case(Case::Upper).to_case(Case::Title),
Self::ChangeCase(f, t) => input.as_ref().from_case(*f).to_case(*t),
}
}
}
impl TryFrom<Replacement> for StatusLineTransformation {
type Error = RahmenError;
fn try_from(value: Replacement) -> Result<Self, Self::Error> {
Ok(StatusLineTransformation::RegexReplace(Box::new((
Regex::new(value.regex.as_ref())?,
value.replace,
))))
}
}
#[derive(Debug)]
struct StatusLineElement {
tags: Vec<String>,
transformations: Vec<StatusLineTransformation>,
}
impl TryFrom<Element> for StatusLineElement {
type Error = RahmenError;
fn try_from(value: Element) -> Result<Self, Self::Error> {
let mut transformations = vec![];
if let Some(case_conversion) = value.case_conversion {
transformations.push(StatusLineTransformation::ChangeCase(
str_to_case(case_conversion.from)?,
str_to_case(case_conversion.to)?,
));
}
if value.capitalize.unwrap_or(false) {
transformations.push(StatusLineTransformation::Capitalize);
}
for replace in value.replace.into_iter().flat_map(Vec::into_iter) {
transformations.push(StatusLineTransformation::RegexReplace(Box::new((
Regex::new(replace.regex.as_ref())?,
replace.replace,
))));
}
Ok(Self {
transformations,
tags: value.exif_tags,
})
}
}
impl StatusLineElement {
fn process(&self, metadata: &Metadata) -> Option<String> {
if let Some(mut value) = self
.tags
.iter()
.map(|f| metadata.get_tag_interpreted_string(f).ok())
.find(Option::is_some)
.flatten()
{
for transformation in &self.transformations {
value = transformation.transform(value);
}
Some(value)
} else {
None
}
}
}
#[derive(Debug)]
pub struct StatusLineFormatter {
elements: Vec<StatusLineElement>,
separator: String,
py_postprocess_fn: Option<Py<PyAny>>,
}
impl StatusLineFormatter {
pub fn new<I: Iterator<Item = Element>>(
statusline_elements_iter: I,
py_postprocess: Option<String>,
separator: String,
) -> RahmenResult<Self> {
let mut elements = vec![];
for element in statusline_elements_iter {
elements.push(element.try_into()?);
}
let py_postprocess_fn = if let Some(postprocess_path) = py_postprocess {
Some(Python::attach(|py| -> PyResult<Py<PyAny>> {
let module = py.import(postprocess_path.as_str())?;
Ok(module.getattr("export")?.call0()?.unbind())
})?)
} else {
None
};
Ok(Self {
elements,
py_postprocess_fn,
separator,
})
}
pub fn format<P: AsRef<std::ffi::OsStr>>(&self, path: P) -> RahmenResult<String> {
let metadata = Metadata::new_from_path(path)?;
let mut line_elements = self
.elements
.iter()
.flat_map(move |element| {
if let Some(v) = element.process(&metadata) {
Some(v)
} else {
Some("".to_string())
}
})
.collect();
line_elements = if let Some(code) = &self.py_postprocess_fn {
Python::attach(|py| -> PyResult<Vec<String>> {
let tags = PyList::new(py, &line_elements)?;
code.bind(py)
.call1((tags, self.separator.as_str()))?
.extract()
})
.unwrap()
} else {
line_elements
};
Ok(line_elements
.iter()
.filter(|x| !x.is_empty())
.unique()
.join(&self.separator))
}
}