use crate::color::Color;
use crate::console::{Console, ConsoleOptions, Renderable};
use crate::segment::Segment;
use crate::style::Style;
#[derive(Debug, Clone)]
pub struct Image {
pub(crate) rgba: Vec<u8>,
pub(crate) width_px: u32,
pub(crate) height_px: u32,
pub(crate) target_cols: Option<usize>,
pub(crate) target_rows: Option<usize>,
}
impl Image {
pub fn from_rgba(width_px: u32, height_px: u32, rgba: Vec<u8>) -> Self {
let expected = width_px as usize * height_px as usize * 4;
assert_eq!(
rgba.len(),
expected,
"rgba length {} does not match {}×{}×4={}",
rgba.len(),
width_px,
height_px,
expected
);
Image {
rgba,
width_px,
height_px,
target_cols: None,
target_rows: None,
}
}
#[cfg(feature = "inline-images")]
pub fn from_path<P: AsRef<std::path::Path>>(path: P) -> Result<Self, ImageError> {
let dyn_img = ::image::open(path).map_err(ImageError::Decode)?;
Ok(Self::from_dyn_image(dyn_img))
}
#[cfg(feature = "inline-images")]
pub fn from_bytes(bytes: &[u8]) -> Result<Self, ImageError> {
let dyn_img = ::image::load_from_memory(bytes).map_err(ImageError::Decode)?;
Ok(Self::from_dyn_image(dyn_img))
}
#[cfg(feature = "inline-images")]
fn from_dyn_image(img: ::image::DynamicImage) -> Self {
let rgba_img = img.into_rgba8();
let width_px = rgba_img.width();
let height_px = rgba_img.height();
let rgba = rgba_img.into_raw();
Image {
rgba,
width_px,
height_px,
target_cols: None,
target_rows: None,
}
}
pub fn pixel_width(&self) -> u32 {
self.width_px
}
pub fn pixel_height(&self) -> u32 {
self.height_px
}
pub fn width(mut self, cols: usize) -> Self {
self.target_cols = Some(cols);
self
}
pub fn height(mut self, rows: usize) -> Self {
self.target_rows = Some(rows);
self
}
fn resolve_cell_size(&self, console_width: usize) -> (usize, usize) {
let px_w = self.width_px as f64;
let px_h = self.height_px as f64;
match (self.target_cols, self.target_rows) {
(Some(c), Some(r)) => (c.max(1), r.max(1)),
(Some(c), None) => {
let c = c.max(1).min(console_width);
let r = if px_w > 0.0 {
((c as f64 * px_h / (px_w * 2.0)).round() as usize).max(1)
} else {
1
};
(c, r)
}
(None, Some(r)) => {
let r = r.max(1);
let c = if px_h > 0.0 {
((r as f64 * px_w * 2.0 / px_h).round() as usize)
.max(1)
.min(console_width)
} else {
1
};
(c, r)
}
(None, None) => {
let c = 40_usize.min(console_width).max(1);
let r = if px_w > 0.0 {
((c as f64 * px_h / (px_w * 2.0)).round() as usize).max(1)
} else {
1
};
(c, r)
}
}
}
fn resize_nearest(&self, dst_w: u32, dst_h: u32) -> Vec<u8> {
let src_w = self.width_px as f64;
let src_h = self.height_px as f64;
let mut out = vec![0u8; (dst_w * dst_h * 4) as usize];
for dy in 0..dst_h {
for dx in 0..dst_w {
let sx = ((dx as f64 + 0.5) * src_w / dst_w as f64) as u32;
let sy = ((dy as f64 + 0.5) * src_h / dst_h as f64) as u32;
let sx = sx.min(self.width_px - 1);
let sy = sy.min(self.height_px - 1);
let src_off = ((sy * self.width_px + sx) * 4) as usize;
let dst_off = ((dy * dst_w + dx) * 4) as usize;
out[dst_off..dst_off + 4].copy_from_slice(&self.rgba[src_off..src_off + 4]);
}
}
out
}
fn render_halfblock(&self, console: &Console, opts: &ConsoleOptions) -> Vec<Segment> {
let (cols, rows) = self.resolve_cell_size(opts.max_width);
let dst_w = cols as u32;
let dst_h = (rows * 2) as u32;
let pixels = if self.width_px == dst_w && self.height_px == dst_h {
std::borrow::Cow::Borrowed(self.rgba.as_slice())
} else {
std::borrow::Cow::Owned(self.resize_nearest(dst_w, dst_h))
};
let mut segments: Vec<Segment> = Vec::with_capacity(rows * (cols + 1));
for row in 0..rows {
for col in 0..cols {
let top_off = ((row * 2) as u32 * dst_w + col as u32) as usize * 4;
let bot_off = ((row * 2 + 1) as u32 * dst_w + col as u32) as usize * 4;
let tr = pixels[top_off];
let tg = pixels[top_off + 1];
let tb = pixels[top_off + 2];
let br = pixels[bot_off];
let bg_g = pixels[bot_off + 1];
let bb = pixels[bot_off + 2];
let fg_color = Color::from_rgb(tr, tg, tb);
let bg_color = Color::from_rgb(br, bg_g, bb);
let style = Style::null().fg(fg_color).bg(bg_color);
let cell_text = "▀";
segments.push(Segment::styled(cell_text, style));
}
segments.push(Segment::line());
}
let _ = console; segments
}
fn render_kitty(&self, opts: &ConsoleOptions) -> Vec<Segment> {
let (cols, rows) = self.resolve_cell_size(opts.max_width);
let dst_w = cols as u32;
let dst_h = (rows * 2) as u32;
let pixels: Vec<u8> = if self.width_px == dst_w && self.height_px == dst_h {
self.rgba.clone()
} else {
self.resize_nearest(dst_w, dst_h)
};
let b64 = crate::utils::control::base64_encode(&pixels);
let apc = format!("\x1b_Gf=32,s={},v={},a=T;{}\x1b\\", dst_w, dst_h, b64);
vec![Segment::text(&apc), Segment::line()]
}
}
impl Renderable for Image {
fn gilt_console(&self, console: &Console, opts: &ConsoleOptions) -> Vec<Segment> {
let caps = console.capabilities();
if console.is_recording() {
return self.render_halfblock(console, opts);
}
if caps.kitty {
return self.render_kitty(opts);
}
self.render_halfblock(console, opts)
}
}
#[cfg(feature = "inline-images")]
#[derive(Debug)]
pub enum ImageError {
Decode(::image::ImageError),
}
#[cfg(feature = "inline-images")]
impl std::fmt::Display for ImageError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ImageError::Decode(e) => write!(f, "image decode error: {}", e),
}
}
}
#[cfg(feature = "inline-images")]
impl std::error::Error for ImageError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ImageError::Decode(e) => Some(e),
}
}
}