use std::io::stdout;
use std::path::PathBuf;
use std::time::Instant;
use std::{error, fmt};
use crossterm::cursor::MoveUp;
use crossterm::execute;
use image::imageops::FilterType;
use imageproc::contrast::adaptive_threshold;
#[cfg(feature = "rayon")]
use indicatif::ParallelProgressIterator;
#[cfg(not(feature = "rayon"))]
use indicatif::ProgressIterator;
use indicatif::ProgressStyle;
#[cfg(feature = "rayon")]
use rayon::prelude::*;
use crate::background_string::BackgroundStringArtConverter;
use crate::braille::BrailleArtConverter;
use crate::{
AsciiArt, AsciiArtConverter, AsciiArtConverterError, AsciiArtConverterOptions,
AsciiStringError, CustomRatioResize, SizeError, DEFAULT_ASCII_STRING, DEFAULT_FONT_RATIO,
};
pub fn calculate_frame_time(framerate: Option<f64>) -> u64 {
framerate.map_or(0, |framerate| (1000f64 / framerate) as u64)
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
pub struct AsciiPlayer {}
impl AsciiPlayer {
#[deprecated(
since = "3.1.0",
note = "Use `Iterator::rev` instead"
)]
pub fn reverse_ascii_string(ascii_string: String) -> String {
ascii_string.chars().rev().collect()
}
pub fn render_frame(
path: &PathBuf,
options: &AsciiPlayerOptions,
converter_options: &AsciiArtConverterOptions,
) -> Result<AsciiArt, AsciiPlayerError> {
let img = image::open(path)?;
let processed_img = match options.threshold {
Some(threshold) => {
image::DynamicImage::from(adaptive_threshold(&img.to_luma8(), threshold))
}
None => img,
};
let prepared_img = processed_img.resize_custom_ratio(
options.width,
options.height,
options.font_ratio,
options.filter,
);
let ascii_art = match (options.clone().background_string, options.braille) {
(Some(background_string), _) => {
prepared_img.background_string_art(&background_string, options.colored)?
}
(None, true) => prepared_img.braille_art(options.colored)?,
(None, false) => prepared_img.ascii_art(converter_options)?,
};
Ok(ascii_art)
}
pub fn play_frames(
paths: &[PathBuf],
options: &AsciiPlayerOptions,
) -> Result<(), AsciiPlayerError> {
let mut first_frame = true;
let converter_options = options.to_owned().into();
loop {
for path in paths.iter() {
let start = Instant::now();
let ascii_art = AsciiPlayer::render_frame(path, options, &converter_options)?;
if !first_frame {
execute!(stdout(), MoveUp(ascii_art.height.try_into().unwrap()))
.unwrap_or_default();
} else {
first_frame = false;
}
println!("{}", ascii_art);
while options.frame_time > start.elapsed().as_millis().try_into().unwrap() {}
}
if !options.looped {
break;
}
}
Ok(())
}
fn pre_render(
paths: &[PathBuf],
options: &AsciiPlayerOptions,
) -> Result<Vec<AsciiArt>, AsciiPlayerError> {
let converter_options = options.to_owned().into();
#[cfg(feature = "rayon")]
let iter = paths.into_par_iter();
#[cfg(not(feature = "rayon"))]
let iter = paths.iter();
let progress_bar_style = ProgressStyle::with_template(
"{elapsed_precise} | {wide_bar} {percent}% | ETA: {eta} | FPS: {per_sec} | {pos}/{len}",
)
.unwrap_or_else(|_| ProgressStyle::default_bar());
let frames = iter
.progress_with_style(progress_bar_style)
.map(|path| AsciiPlayer::render_frame(path, options, &converter_options))
.collect::<Result<Vec<AsciiArt>, AsciiPlayerError>>()?;
Ok(frames)
}
pub fn play_pre_rendered_frames(
paths: &[PathBuf],
options: &AsciiPlayerOptions,
) -> Result<(), AsciiPlayerError> {
let mut first_frame = true;
let frames = AsciiPlayer::pre_render(paths, options)?;
loop {
frames.iter().for_each(|ascii_art| {
let start = Instant::now();
if !first_frame {
execute!(stdout(), MoveUp(ascii_art.height.try_into().unwrap()))
.unwrap_or_default();
} else {
first_frame = false;
}
println!("{}", ascii_art);
while options.frame_time > start.elapsed().as_millis().try_into().unwrap() {}
});
if !options.looped {
break;
}
}
Ok(())
}
pub fn play(paths: &[PathBuf], options: &AsciiPlayerOptions) -> Result<(), AsciiPlayerError> {
if options.pre_render {
return AsciiPlayer::play_pre_rendered_frames(paths, options);
}
AsciiPlayer::play_frames(paths, options)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct AsciiPlayerOptions {
pub width: Option<u32>,
pub height: Option<u32>,
pub ascii_string: String,
pub colored: bool,
pub frame_time: u64,
pub pre_render: bool,
pub font_ratio: f64,
pub looped: bool,
pub filter: FilterType,
pub threshold: Option<u32>,
pub braille: bool,
pub background_string: Option<String>,
}
impl Default for AsciiPlayerOptions {
fn default() -> AsciiPlayerOptions {
AsciiPlayerOptions {
width: None,
height: None,
ascii_string: DEFAULT_ASCII_STRING.to_owned(),
colored: false,
frame_time: 0,
pre_render: false,
font_ratio: DEFAULT_FONT_RATIO,
looped: false,
filter: FilterType::Triangle,
threshold: None,
braille: false,
background_string: None,
}
}
}
impl From<AsciiPlayerOptions> for AsciiArtConverterOptions {
fn from(o: AsciiPlayerOptions) -> AsciiArtConverterOptions {
AsciiArtConverterOptions {
ascii_string: o.ascii_string,
colored: o.colored,
}
}
}
#[derive(Debug)]
pub enum AsciiPlayerError {
Image(image::ImageError),
AsciiConverter(AsciiArtConverterError),
}
impl error::Error for AsciiPlayerError {}
impl fmt::Display for AsciiPlayerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AsciiPlayerError::Image(err) => {
write!(f, "Image error: {}", err)
}
AsciiPlayerError::AsciiConverter(err) => {
write!(f, "ASCII art converter error: {}", err)
}
}
}
}
impl From<image::ImageError> for AsciiPlayerError {
fn from(err: image::ImageError) -> AsciiPlayerError {
AsciiPlayerError::Image(err)
}
}
impl From<AsciiArtConverterError> for AsciiPlayerError {
fn from(err: AsciiArtConverterError) -> AsciiPlayerError {
AsciiPlayerError::AsciiConverter(err)
}
}
impl From<AsciiStringError> for AsciiPlayerError {
fn from(err: AsciiStringError) -> AsciiPlayerError {
AsciiPlayerError::AsciiConverter(AsciiArtConverterError::AsciiStringError(err))
}
}
impl From<SizeError> for AsciiPlayerError {
fn from(err: SizeError) -> AsciiPlayerError {
AsciiPlayerError::AsciiConverter(AsciiArtConverterError::SizeError(err))
}
}