Skip to content

Commit

Permalink
refac, added .gif to ASCII support
Browse files Browse the repository at this point in the history
  • Loading branch information
splurf committed Jun 11, 2024
1 parent 92ec7cd commit 157f4e1
Show file tree
Hide file tree
Showing 8 changed files with 297 additions and 177 deletions.
78 changes: 78 additions & 0 deletions src/base/ascii.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
use image::{codecs::gif::GifDecoder, AnimationDecoder, DynamicImage, Frame};
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use std::{fs::File, io::BufReader, path::Path, time::Duration};

use super::donut;
use crate::{Config, GifError, Result};

#[derive(Clone, Debug)]
pub struct AsciiFrame {
buffer: Vec<u8>,
delay: Duration,
}

impl AsciiFrame {
pub const fn new(buffer: Vec<u8>, delay: Duration) -> Self {
Self { buffer, delay }
}

pub const fn delay(&self) -> Duration {
self.delay
}
}

impl AsRef<[u8]> for AsciiFrame {
fn as_ref(&self) -> &[u8] {
self.buffer.as_ref()
}
}

pub fn frame_to_ascii(f: Frame, delay: Option<Duration>) -> AsciiFrame {
// use specified delay or delay of the current frame
let delay = delay.unwrap_or(f.delay().into());

// represent image buffer as dynamic image
let img = DynamicImage::from(f.into_buffer());

// convert image into ASCII art
let s = artem::convert(img, &Default::default());

// prepend HOME ASCII escape sequence
let mut buffer = b"\x1b[H".to_vec();
buffer.extend(s.as_bytes());

// buffer and delay data only
AsciiFrame { buffer, delay }
}

fn get_frames_from_path(path: &Path, fps: Option<f32>) -> Result<Vec<AsciiFrame>> {
// file reader
let input = BufReader::new(File::open(path)?);

// Configure the decoder such that it will expand the image to RGBA.
let decoder = GifDecoder::new(input).unwrap();

// Read the file header
let frames = decoder.into_frames().collect_frames().unwrap();

let delay = fps.map(|value| Duration::from_secs_f32(1.0 / value));

// convert frame buffer to ASCII, extract buffer and delay only
let ascii = frames
.into_par_iter()
.map(|f| frame_to_ascii(f, delay))
.collect::<Vec<AsciiFrame>>();

if ascii.iter().all(|f| f.delay().is_zero()) {
return Err(GifError::Delay.into());
}
Ok(ascii)
}

pub fn get_frames(cfg: &Config) -> Result<Vec<AsciiFrame>> {
if let Some(path) = cfg.images() {
get_frames_from_path(path, cfg.fps())
} else {
Ok(donut::get_frames())
}
}
25 changes: 22 additions & 3 deletions src/base/cfg.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use clap::Parser;
use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4, SocketAddrV6};
use std::{
net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4, SocketAddrV6},
path::{Path, PathBuf},
};

use super::Result;

Expand All @@ -24,8 +27,14 @@ pub struct Config {
port: u16,

/// Location path
#[arg(long, default_value = String::from('/'), value_parser = parse_path)]
#[arg(long, default_value_t = String::from('/'), value_parser = parse_path)]
path: String,

#[arg(short, long)]
images: Option<PathBuf>,

#[arg(short, long)]
fps: Option<f32>,
}

impl Config {
Expand All @@ -41,8 +50,18 @@ impl Config {
}
}

/// Return the `path` of the address
/// Return the URI path
pub fn path(&self) -> &str {
&self.path
}

/// Return the path to the images.
pub fn images(&self) -> Option<&Path> {
self.images.as_deref()
}

/// Return the frames/second, if specified.
pub const fn fps(&self) -> Option<f32> {
self.fps
}
}
12 changes: 0 additions & 12 deletions src/base/consts.rs

This file was deleted.

128 changes: 128 additions & 0 deletions src/base/donut.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
use std::{f32::consts::TAU, time::Duration};

use crate::AsciiFrame;

/// The delay between each frame
/// - 20.833333ms => ~48 FPS
const DELAY: Duration = Duration::from_nanos(20833333);

/// The characters required to make each donut frame
const CHARACTERS: [u8; 12] = [46, 44, 45, 126, 58, 59, 61, 33, 42, 35, 36, 64];

/// Generate a single frame of the donut based on the given variables
fn gen_frame(
a: &mut f32,
b: &mut f32,
i: &mut f32,
j: &mut f32,
z: &mut [f32; 1760],
p: &mut [u8; 1760],
) -> Vec<u8> {
while *j < TAU {
while *i < TAU {
let c = f32::sin(*i);
let d = f32::cos(*j);
let e = f32::sin(*a);
let f = f32::sin(*j);
let g = f32::cos(*a);
let h = d + 2.0;
let q = 1.0 / (c * h * e + f * g + 5.0);
let l = f32::cos(*i);
let m = f32::cos(*b);
let n = f32::sin(*b);
let t = c * h * g - f * e;

let x = (40.0 + 30.0 * q * (l * h * m - t * n)) as i32;
let y = (12.0 + 15.0 * q * (l * h * n + t * m)) as i32;
let o = x + 80 * y;
let n = (8.0 * ((f * e - c * d * g) * m - c * d * e - f * g - l * d * n)) as i32;

if 22 > y && y > 0 && x > 0 && 80 > x && q > z[o as usize] {
z[o as usize] = q;
p[o as usize] = CHARACTERS[if n > 0 { n } else { 0 } as usize]
}
*i += 0.02
}
*i = 0.0;
*j += 0.07
}
*a += 0.04;
*b += 0.02;

let frame = p
.chunks_exact(80)
.map(<[u8]>::to_vec)
.collect::<Vec<_>>()
.join(&10);

*p = [32; 1760];
*z = [0.0; 1760];
*j = 0.0;

frame
}

/// *donut.c* refactored into rust
pub fn get_frames() -> Vec<AsciiFrame> {
let mut a = 0.0;
let mut b = 0.0;

let mut i = 0.0;
let mut j = 0.0;

let mut z = [0.0; 1760];
let mut p = [32; 1760];

// Generate the original `donut` frames (559234 bytes)
let mut frames = [0; 314].map(|_| gen_frame(&mut a, &mut b, &mut i, &mut j, &mut z, &mut p));
trim_frames(&mut frames); // 395012 bytes (~29% smaller)
frames.map(|buffer| AsciiFrame::new(buffer, DELAY)).to_vec()
}

/// Trim the *majority* of the unnecessary whitespace at the end of each line of every frame
/// TODO - Trim all redundant whitespace, without altering how the frames look
fn trim_frames(frames: &mut [Vec<u8>; 314]) {
// Convenience method for returning the lines of each frame
fn split(f: &[u8]) -> impl Iterator<Item = Vec<u8>> + '_ {
f.split(|c| *c == 10).map(<[u8]>::to_vec)
}

// Determine the maximum length of non-ASCII of each line for every frame
// TODO - Improve this or come up with a better algorithm
let maxes = {
let mut out = [[0; 314]; 22];

for (j, chunk) in frames
.iter()
.flat_map(|f| {
split(f)
.map(|u| {
u.iter()
.rposition(|c| CHARACTERS.contains(c))
.unwrap_or_default()
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>()
.chunks(22)
.enumerate()
{
for (i, n) in chunk.iter().enumerate() {
out[i][j] = *n
}
}
out.into_iter()
.map(|u| u.into_iter().max().unwrap_or_default())
.collect::<Vec<_>>()
};

// Drain the ASCII of each line for every frame for their max length
frames.iter_mut().for_each(|f| {
*f = split(f.as_slice())
.enumerate()
.map(|(i, l)| l[0..maxes[i]].to_vec())
.collect::<Vec<Vec<u8>>>()
.join(&10);
f.splice(0..0, "\x1b[H".bytes());
});
}
28 changes: 28 additions & 0 deletions src/base/err.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,27 @@ impl std::fmt::Display for UriError {
}
}

pub enum GifError {
Delay,
}

impl std::fmt::Display for GifError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Delay => "GIF (missing frame rate)",
})
}
}

impl From<GifError> for Invalid {
fn from(value: GifError) -> Self {
Self::Gif(value)
}
}

pub enum Invalid {
Uri(UriError),
Gif(GifError),
Format,
}

Expand All @@ -41,6 +60,7 @@ impl std::fmt::Display for Invalid {
"Invalid {}",
match self {
Self::Uri(e) => e.to_string(),
Self::Gif(e) => e.to_string(),
Self::Format => "http format".to_string(),
}
))
Expand All @@ -62,6 +82,7 @@ impl From<UriError> for Invalid {
pub enum Error {
IO(std::io::Error),
Parse(Invalid),
Gif(image::ImageError),
Sync,
}

Expand All @@ -77,6 +98,12 @@ impl<T: Into<Invalid>> From<T> for Error {
}
}

impl From<image::ImageError> for Error {
fn from(value: image::ImageError) -> Self {
Self::Gif(value)
}
}

impl<T> From<std::sync::PoisonError<std::sync::RwLockReadGuard<'_, T>>> for Error {
fn from(_: std::sync::PoisonError<std::sync::RwLockReadGuard<'_, T>>) -> Self {
Self::Sync
Expand Down Expand Up @@ -112,6 +139,7 @@ impl std::fmt::Display for Error {
f.write_str(&match self {
Self::IO(e) => e.to_string(),
Self::Parse(e) => e.to_string(),
Self::Gif(e) => e.to_string(),
Self::Sync => "An unexpected poison error has occurred".to_string(),
})
}
Expand Down
Loading

0 comments on commit 157f4e1

Please sign in to comment.