use std::io::Cursor;
use std::path::Path;
use image::GenericImageView;
use ratatui::{
style::{Color, Style},
text::{Line, Span},
};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ImageProtocol {
Kitty,
Iterm2,
Halfblock,
}
pub fn detect_protocol() -> ImageProtocol {
if std::env::var("KITTY_WINDOW_ID").is_ok() {
return ImageProtocol::Kitty;
}
if let Ok(term) = std::env::var("TERM_PROGRAM") {
match term.as_str() {
"ghostty" => return ImageProtocol::Kitty,
"iTerm.app" | "WezTerm" => return ImageProtocol::Iterm2,
_ => {}
}
}
ImageProtocol::Halfblock
}
pub fn encode_native_png(path: &Path, cell_width: u32, cell_height: u32) -> Option<String> {
let img = image::open(path).ok()?;
let (orig_w, orig_h) = img.dimensions();
if orig_w == 0 || orig_h == 0 {
return None;
}
let target_w = cell_width * 8;
let target_h = cell_height * 16;
let scale = f64::min(
target_w as f64 / orig_w as f64,
target_h as f64 / orig_h as f64,
)
.min(1.0);
let new_w = ((orig_w as f64 * scale).round() as u32).max(1);
let new_h = ((orig_h as f64 * scale).round() as u32).max(1);
let resized = img.resize_exact(new_w, new_h, image::imageops::FilterType::Triangle);
let mut buf = Cursor::new(Vec::new());
resized
.write_to(&mut buf, image::ImageFormat::Png)
.ok()?;
use base64::Engine;
Some(base64::engine::general_purpose::STANDARD.encode(buf.into_inner()))
}
pub fn render_image(path: &Path, max_width: u32) -> Option<Vec<Line<'static>>> {
let img = image::open(path).ok()?;
let cap_width = max_width;
let cap_height: u32 = 60;
let (orig_w, orig_h) = img.dimensions();
if orig_w == 0 || orig_h == 0 {
return None;
}
let scale = f64::min(
cap_width as f64 / orig_w as f64,
cap_height as f64 / orig_h as f64,
)
.min(1.0);
let new_w = ((orig_w as f64 * scale).round() as u32).max(1);
let new_h = ((orig_h as f64 * scale).round() as u32).max(1);
let resized = img.resize_exact(new_w, new_h, image::imageops::FilterType::Triangle);
let rgba = resized.to_rgba8();
let (w, h) = rgba.dimensions();
let row_pairs = h.div_ceil(2);
let mut lines: Vec<Line<'static>> = Vec::with_capacity(row_pairs as usize);
for row in 0..row_pairs {
let y_top = row * 2;
let y_bot = y_top + 1;
let mut spans: Vec<Span<'static>> = Vec::with_capacity(w as usize + 1);
spans.push(Span::raw(" "));
for x in 0..w {
let top_pixel = rgba.get_pixel(x, y_top);
let fg = if top_pixel[3] < 128 {
Color::Reset
} else {
Color::Rgb(top_pixel[0], top_pixel[1], top_pixel[2])
};
let bg = if y_bot < h {
let bot_pixel = rgba.get_pixel(x, y_bot);
if bot_pixel[3] < 128 {
Color::Reset
} else {
Color::Rgb(bot_pixel[0], bot_pixel[1], bot_pixel[2])
}
} else {
Color::Reset
};
spans.push(Span::styled(
"▀",
Style::default().fg(fg).bg(bg),
));
}
lines.push(Line::from(spans));
}
Some(lines)
}