lb 0.6.0

A TUI library with ASCII/Unicode graphics.
Documentation
use std::error;
use std::io;
use std::thread;
use std::time::Instant;

use image::imageops;
use image::EncodableLayout;

use lb::buf;
use lb::buf::Buffer;
use lb::img;
use lb::mat;
use lb::prelude::*;
use lb::typeset;
use lb::typesetters;

const FALLBACK_RATIO: f32 = 0.44;
const FALLBACK_SIZE: mat::Size = mat::Size {
    width: 96,
    height: 32,
};

struct Watch(Instant, bool);

impl Watch {
    fn new(print: bool) -> Watch {
        Watch(Instant::now(), print)
    }

    fn lap(&mut self, name: &str) {
        if self.1 {
            eprintln!("{} took: {} ms", name, self.0.elapsed().as_millis());
        }
        self.0 = Instant::now();
    }
}

fn main() -> Result<(), Box<dyn error::Error>> {
    let args: Vec<String> = std::env::args().collect();

    if args.len() < 3 {
        eprintln!("error: expected at least 2 CLI arguments");
        eprintln!("usage: cargo run --example image -- <typesetter> <image>");

        std::process::exit(1);
    }

    let path = args.get(2).unwrap();

    let bench = args
        .get(3)
        .filter(|a| *a == "-b" || *a == "--bench")
        .is_some();

    match args[1].as_str() {
        "block" => print(&typesetters::Block, &path, bench),
        "half" => print(&typesetters::Half, &path, bench),
        "quadrant" => print(&typesetters::Quadrant, &path, bench),
        "sextant" => print(&typesetters::Sextant, &path, bench),
        "smooth" => print(&typesetters::Smooth, &path, bench),
        "asymmetric" => print(&typesetters::Asymmetric, &path, bench),
        _ => {
            eprintln!("unknown typesetter");
            eprintln!("available typesetters are: block, half, quadrant, sextant, and asymmetric");
            eprintln!("usage: cargo run --example image -- <typesetter> <image>");
            Ok(())
        }
    }
}

fn print<T>(typesetter: &T, path: &str, bench: bool) -> Result<(), Box<dyn error::Error>>
where
    T: typeset::Typesetter + Sync,
{
    let mut watch = Watch::new(bench);

    let img = image::open(path)?;

    watch.lap("opening");

    let ratio = correction_ratio().unwrap_or(FALLBACK_RATIO);
    let img_size: lb::Size = (img.width() as usize, img.height() as usize).into();
    let buffer_size = img_size.to_buffer_size(typesetter);
    let buffer_size = typesetter.fix_aspect_ratio(buffer_size, ratio);

    let size = best_fit(buffer_size);

    let mut buffer = buf::SimpleBuffer::new(size);

    let re_size = size.to_image_size(typesetter);

    let img = {
        let img = img.resize_exact(
            re_size.width as u32,
            re_size.height as u32,
            imageops::FilterType::Triangle,
        );

        watch.lap("resizing");

        let size = (img.width() as usize, img.height() as usize).into();
        let img = img.into_rgb8();
        let raw = img.as_bytes();
        img::ImgVec::from_bytes(&raw, size)?
    };

    watch.lap("from_bytes");

    let num_of_jobs = thread::available_parallelism()?;
    thread::scope(|s| {
        for workload in typesetter.workloads(buffer.as_mut(), img.as_ref(), num_of_jobs.get()) {
            s.spawn(move || workload.run());
        }
    });

    watch.lap("glyphing");

    let string = buffer.render();

    watch.lap("rendering");

    print!("{}", string);

    watch.lap("printing");

    Ok(())
}

#[cfg(not(feature = "term"))]
fn correction_ratio() -> io::Result<f32> {
    let size = termion::terminal_size()?;
    let size_px = termion::terminal_size_pixels()?;

    Ok((size.1 as f32 / size.0 as f32) * (size_px.0 as f32 / size_px.1 as f32))
}

#[cfg(not(feature = "term"))]
fn term_size() -> io::Result<mat::Size> {
    let term_size = termion::terminal_size()?;
    Ok(mat::Size {
        width: term_size.0 as usize,
        height: term_size.1 as usize,
    })
}

#[cfg(feature = "term")]
fn correction_ratio() -> io::Result<f32> {
    lb::term::glyph_aspect_ratio()
}

#[cfg(feature = "term")]
fn term_size() -> io::Result<mat::Size> {
    lb::term::size()
}

fn best_fit(img_size: mat::Size) -> mat::Size {
    let term_size = term_size().unwrap_or(FALLBACK_SIZE);

    let ratio = {
        let rw = term_size.width as f32 / img_size.width as f32;
        let rh = term_size.height as f32 / img_size.height as f32;

        if rw < rh {
            rw
        } else {
            rh
        }
    };

    mat::Size {
        width: (img_size.width as f32 * ratio) as usize,
        height: (img_size.height as f32 * ratio) as usize,
    }
}