use crate::config::Config;
use crate::error::{ViuError, ViuResult};
use crate::utils::terminal_size;
use crossterm::cursor::{MoveRight, MoveTo, MoveToPreviousLine};
use crossterm::execute;
use image::{DynamicImage, GenericImageView};
use std::io::Write;
#[cfg(feature = "print-file")]
use std::path::Path;
mod block;
pub use block::BlockPrinter;
mod kitty;
pub use kitty::{get_kitty_support, KittyPrinter, KittySupport};
#[cfg(all(feature = "sixel", not(windows)))]
mod sixel;
#[cfg(all(feature = "sixel", not(windows)))]
pub use self::sixel::SixelPrinter;
#[cfg(any(feature = "icy_sixel", all(feature = "sixel", windows)))]
mod icy_sixel;
#[cfg(any(feature = "icy_sixel", all(feature = "sixel", windows)))]
pub use self::icy_sixel::IcySixelPrinter;
#[cfg(any(feature = "sixel", feature = "icy_sixel"))]
mod sixel_util;
#[cfg(any(feature = "sixel", feature = "icy_sixel"))]
pub use self::sixel_util::is_sixel_supported;
mod iterm;
pub(crate) mod read_key;
pub use iterm::iTermPrinter;
pub use iterm::is_iterm_supported;
#[cfg(test)]
use read_key::test_utils::TestKeys;
use read_key::ReadKey;
pub trait Printer {
fn print(
&self,
stdin: &impl ReadKey,
stdout: &mut impl Write,
img: &DynamicImage,
config: &Config,
) -> ViuResult<(u32, u32)>;
#[cfg(feature = "print-file")]
fn print_from_file<P: AsRef<Path>>(
&self,
stdin: &impl ReadKey,
stdout: &mut impl Write,
filename: P,
config: &Config,
) -> ViuResult<(u32, u32)> {
let img = image::ImageReader::open(filename)?
.with_guessed_format()?
.decode()?;
self.print(stdin, stdout, &img, config)
}
}
#[allow(non_camel_case_types)]
#[derive(Debug, Clone, Copy)]
pub enum PrinterType {
Block,
Kitty,
iTerm,
#[cfg(all(feature = "sixel", not(windows)))]
Sixel,
#[cfg(any(feature = "icy_sixel", all(feature = "sixel", windows)))]
IcySixel,
}
impl Printer for PrinterType {
fn print(
&self,
stdin: &impl ReadKey,
stdout: &mut impl Write,
img: &DynamicImage,
config: &Config,
) -> ViuResult<(u32, u32)> {
match self {
PrinterType::Block => BlockPrinter.print(stdin, stdout, img, config),
PrinterType::Kitty => KittyPrinter.print(stdin, stdout, img, config),
PrinterType::iTerm => iTermPrinter.print(stdin, stdout, img, config),
#[cfg(all(feature = "sixel", not(windows)))]
PrinterType::Sixel => SixelPrinter.print(stdin, stdout, img, config),
#[cfg(any(feature = "icy_sixel", all(feature = "sixel", windows)))]
PrinterType::IcySixel => IcySixelPrinter.print(stdin, stdout, img, config),
}
}
#[cfg(feature = "print-file")]
fn print_from_file<P: AsRef<Path>>(
&self,
stdin: &impl ReadKey,
stdout: &mut impl Write,
filename: P,
config: &Config,
) -> ViuResult<(u32, u32)> {
match self {
PrinterType::Block => BlockPrinter.print_from_file(stdin, stdout, filename, config),
PrinterType::Kitty => KittyPrinter.print_from_file(stdin, stdout, filename, config),
PrinterType::iTerm => iTermPrinter.print_from_file(stdin, stdout, filename, config),
#[cfg(all(feature = "sixel", not(windows)))]
PrinterType::Sixel => SixelPrinter.print_from_file(stdin, stdout, filename, config),
#[cfg(any(feature = "icy_sixel", all(feature = "sixel", windows)))]
PrinterType::IcySixel => {
IcySixelPrinter.print_from_file(stdin, stdout, filename, config)
}
}
}
}
pub fn resize(img: &DynamicImage, width: Option<u32>, height: Option<u32>) -> DynamicImage {
let (w, h) = find_best_fit(img, width, height);
img.resize_exact(
w,
2 * h - img.height() % 2,
image::imageops::FilterType::CatmullRom,
)
}
fn find_best_fit(img: &DynamicImage, width: Option<u32>, height: Option<u32>) -> (u32, u32) {
let (img_width, img_height) = img.dimensions();
match (width, height) {
(None, None) => {
let (term_w, term_h) = terminal_size();
let (w, h) = fit_dimensions(img_width, img_height, term_w as u32, term_h as u32);
let h = if h == term_h as u32 { h - 1 } else { h };
(w, h)
}
(Some(w), None) => fit_dimensions(img_width, img_height, w, img_height),
(None, Some(h)) => fit_dimensions(img_width, img_height, img_width, h),
(Some(w), Some(h)) => (w, h),
}
}
fn fit_dimensions(width: u32, height: u32, bound_width: u32, bound_height: u32) -> (u32, u32) {
let bound_height = 2 * bound_height;
if width <= bound_width && height <= bound_height {
return (width, std::cmp::max(1, height / 2 + height % 2));
}
let ratio = width * bound_height;
let nratio = bound_width * height;
let use_width = nratio <= ratio;
let intermediate = if use_width {
height * bound_width / width
} else {
width * bound_height / height
};
if use_width {
(bound_width, std::cmp::max(1, intermediate / 2))
} else {
(intermediate, std::cmp::max(1, bound_height / 2))
}
}
fn adjust_offset(stdout: &mut impl Write, config: &Config) -> ViuResult {
if config.absolute_offset {
if config.y >= 0 {
execute!(stdout, MoveTo(config.x, config.y as u16))?;
} else {
return Err(ViuError::InvalidConfiguration(
"absolute_offset is true but y offset is negative".to_owned(),
));
}
} else {
if config.y < 0 {
execute!(stdout, MoveToPreviousLine(-config.y as u16))?;
} else {
for _ in 0..config.y {
writeln!(stdout)?;
}
}
if config.x > 0 {
execute!(stdout, MoveRight(config.x))?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn test_adjust_offset_output(config: &Config, str: &str) {
let mut vec = Vec::new();
adjust_offset(&mut vec, config).unwrap();
assert_eq!(std::str::from_utf8(&vec).unwrap(), str);
}
fn best_fit_large_test_image() -> DynamicImage {
DynamicImage::ImageRgba8(image::RgbaImage::new(600, 499))
}
fn best_fit_small_test_image() -> DynamicImage {
DynamicImage::ImageRgba8(image::RgbaImage::new(40, 25))
}
fn resize_get_large_test_image() -> DynamicImage {
DynamicImage::ImageRgba8(image::RgbaImage::new(1000, 799))
}
fn resize_get_small_test_image() -> DynamicImage {
DynamicImage::ImageRgba8(image::RgbaImage::new(20, 10))
}
#[test]
fn test_resize_none() {
let width = None;
let height = None;
let img = resize_get_large_test_image();
let new_img = resize(&img, width, height);
assert_eq!(new_img.width(), 60);
assert_eq!(new_img.height(), 45);
let img = resize_get_small_test_image();
let new_img = resize(&img, width, height);
assert_eq!(new_img.width(), 20);
assert_eq!(new_img.height(), 10);
}
#[test]
fn test_resize_some_none() {
let width = Some(100);
let height = None;
let img = resize_get_large_test_image();
let new_img = resize(&img, width, height);
assert_eq!(new_img.width(), 100);
assert_eq!(new_img.height(), 77);
let img = resize_get_small_test_image();
let new_img = resize(&img, width, height);
assert_eq!(new_img.width(), 20);
assert_eq!(new_img.height(), 10);
}
#[test]
fn test_resize_none_some() {
let width = None;
let mut height = Some(90);
let img = resize_get_large_test_image();
let new_img = resize(&img, width, height);
assert_eq!(new_img.width(), 225);
assert_eq!(new_img.height(), 179);
height = Some(4);
let img = resize_get_small_test_image();
let new_img = resize(&img, width, height);
assert_eq!(new_img.width(), 16);
assert_eq!(new_img.height(), 8);
}
#[test]
fn test_resize_some_some() {
let width = Some(15);
let height = Some(9);
let img = resize_get_large_test_image();
let new_img = resize(&img, width, height);
assert_eq!(new_img.width(), 15);
assert_eq!(new_img.height(), 17);
let img = resize_get_small_test_image();
let new_img = resize(&img, width, height);
assert_eq!(new_img.width(), 15);
assert_eq!(new_img.height(), 18);
}
#[test]
fn find_best_fit_none() {
let width = None;
let height = None;
let img = best_fit_large_test_image();
let (w, h) = find_best_fit(&img, width, height);
assert_eq!(w, 57);
assert_eq!(h, 23);
let img = best_fit_small_test_image();
let (w, h) = find_best_fit(&img, width, height);
assert_eq!(w, 40);
assert_eq!(h, 13);
let img = DynamicImage::ImageRgba8(image::RgbaImage::new(160, 80));
let (w, h) = find_best_fit(&img, width, height);
assert_eq!(w, 80);
assert_eq!(h, 20);
}
#[test]
fn find_best_fit_some_none() {
let width = Some(100);
let height = None;
let img = best_fit_large_test_image();
let (w, h) = find_best_fit(&img, width, height);
assert_eq!(w, 100);
assert_eq!(h, 41);
let img = best_fit_small_test_image();
let (w, h) = find_best_fit(&img, width, height);
assert_eq!(w, 40);
assert_eq!(h, 13);
let width = Some(6);
let (w, h) = find_best_fit(&img, width, height);
assert_eq!(w, 6);
assert_eq!(h, 1);
let width = Some(3);
let (w, h) = find_best_fit(&img, width, height);
assert_eq!(w, 3);
assert_eq!(h, 1);
}
#[test]
fn find_best_fit_none_some() {
let width = None;
let height = Some(90);
let img = best_fit_large_test_image();
let (w, h) = find_best_fit(&img, width, height);
assert_eq!(w, 216);
assert_eq!(h, 90);
let height = Some(4);
let img = best_fit_small_test_image();
let (w, h) = find_best_fit(&img, width, height);
assert_eq!(w, 12);
assert_eq!(h, 4);
}
#[test]
fn find_best_fit_some_some() {
let width = Some(15);
let height = Some(9);
let img = best_fit_large_test_image();
let (w, h) = find_best_fit(&img, width, height);
assert_eq!(w, 15);
assert_eq!(h, 9);
let img = best_fit_small_test_image();
let (w, h) = find_best_fit(&img, width, height);
assert_eq!(w, 15);
assert_eq!(h, 9);
}
#[test]
fn test_fit_dimensions() {
assert_eq!((40, 20), fit_dimensions(100, 100, 40, 50));
assert_eq!((20, 10), fit_dimensions(100, 100, 40, 10));
assert_eq!((30, 10), fit_dimensions(240, 160, 30, 100));
assert_eq!((200, 140), fit_dimensions(300, 420, 320, 140));
}
#[test]
fn test_fit_smaller_than_bounds() {
assert_eq!((4, 2), fit_dimensions(4, 3, 80, 24));
assert_eq!((4, 1), fit_dimensions(4, 1, 80, 24));
}
#[test]
fn test_fit_equal_to_bounds() {
assert_eq!((80, 12), fit_dimensions(80, 24, 80, 24));
}
#[test]
fn test_zero_offset() {
let config = Config {
absolute_offset: false,
x: 0,
y: 0,
..Default::default()
};
test_adjust_offset_output(&config, "");
}
#[test]
fn test_adjust_offset_absolute() {
let mut config = Config {
absolute_offset: true,
x: 3,
y: 4,
..Default::default()
};
config.x = 3;
config.y = 0;
test_adjust_offset_output(&config, "\x1b[1;4H");
config.x = 7;
config.y = 4;
test_adjust_offset_output(&config, "\x1b[5;8H");
}
#[test]
fn test_adjust_offset_not_absolute() {
let mut config = Config {
absolute_offset: false,
x: 3,
y: 4,
..Default::default()
};
test_adjust_offset_output(&config, "\n\n\n\n\x1b[3C");
config.x = 1;
config.y = -2;
test_adjust_offset_output(&config, "\x1b[2F\x1b[1C");
}
#[test]
fn test_invalid_adjust_offset() {
let config = Config {
absolute_offset: true,
y: -1,
..Default::default()
};
let mut vec = Vec::new();
let err = adjust_offset(&mut vec, &config).unwrap_err();
assert!(matches!(err, ViuError::InvalidConfiguration { .. }));
}
}