use crate::color::Color;
use crate::console::{Console, ConsoleOptions, Renderable};
use crate::segment::Segment;
use crate::style::Style;
fn sixel_emit_run(out: &mut String, val: u8, len: usize) {
if len == 0 {
return;
}
let ch = (0x3F + val) as char;
if len >= 4 {
out.push('!');
out.push_str(&len.to_string());
out.push(ch);
} else {
for _ in 0..len {
out.push(ch);
}
}
}
#[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 max_w = (cols as u32).saturating_mul(20).max(1);
let max_h = (rows as u32).saturating_mul(40).max(1);
let (tw, th) = if self.width_px <= max_w && self.height_px <= max_h {
(self.width_px.max(1), self.height_px.max(1))
} else {
let scale = (max_w as f64 / self.width_px.max(1) as f64)
.min(max_h as f64 / self.height_px.max(1) as f64);
(
((self.width_px as f64 * scale).round() as u32).max(1),
((self.height_px as f64 * scale).round() as u32).max(1),
)
};
let pixels: Vec<u8> = if self.width_px == tw && self.height_px == th {
self.rgba.clone()
} else {
self.resize_nearest(tw, th)
};
let b64 = crate::utils::control::base64_encode(&pixels);
let apc = format!(
"\x1b_Gf=32,s={},v={},c={},r={},a=T;{}\x1b\\",
tw, th, cols, rows, b64
);
vec![Segment::new(&apc, None, Some(vec![])), Segment::line()]
}
fn render_sixel(&self, opts: &ConsoleOptions) -> Vec<Segment> {
let (cols, rows) = self.resolve_cell_size(opts.max_width);
let w = (cols as u32 * 10).max(1) as usize;
let h = (rows as u32 * 20).max(1) as usize;
let pixels = if self.width_px as usize == w && self.height_px as usize == h {
std::borrow::Cow::Borrowed(self.rgba.as_slice())
} else {
std::borrow::Cow::Owned(self.resize_nearest(w as u32, h as u32))
};
let q = |c: u8| ((c as u32 * 5 + 127) / 255) as usize; let mut idx = vec![0u16; w * h];
let mut used = [false; 216];
for (i, px) in pixels.chunks_exact(4).enumerate() {
let p = q(px[0]) * 36 + q(px[1]) * 6 + q(px[2]);
idx[i] = p as u16;
used[p] = true;
}
let mut out = String::with_capacity(w * h / 4 + 256);
out.push_str("\x1bP0;1;0q");
out.push_str(&format!("\"1;1;{w};{h}"));
let lvl = |l: usize| l * 20; for (p, &u) in used.iter().enumerate() {
if u {
out.push_str(&format!(
"#{};2;{};{};{}",
p,
lvl(p / 36),
lvl((p / 6) % 6),
lvl(p % 6)
));
}
}
let mut band = 0usize;
while band < h {
let band_h = (h - band).min(6);
let mut present = [false; 216];
for y in band..band + band_h {
for x in 0..w {
present[idx[y * w + x] as usize] = true;
}
}
let mut first = true;
for (p, &is_present) in present.iter().enumerate() {
if !is_present {
continue;
}
if !first {
out.push('$'); }
first = false;
out.push_str(&format!("#{p}"));
let mut run_val = 0u8;
let mut run_len = 0usize;
for x in 0..w {
let mut bits = 0u8;
for (dy, row) in (band..band + band_h).enumerate() {
if idx[row * w + x] as usize == p {
bits |= 1 << dy;
}
}
if run_len > 0 && bits == run_val {
run_len += 1;
} else {
sixel_emit_run(&mut out, run_val, run_len);
run_val = bits;
run_len = 1;
}
}
sixel_emit_run(&mut out, run_val, run_len);
}
out.push('-'); band += 6;
}
out.push_str("\x1b\\"); vec![Segment::new(&out, None, Some(vec![])), Segment::line()]
}
#[cfg(feature = "inline-images")]
fn render_iterm(&self, opts: &ConsoleOptions) -> Vec<Segment> {
let (cols, rows) = self.resolve_cell_size(opts.max_width);
let png = self.to_png();
let b64 = crate::utils::control::base64_encode(&png);
let osc = format!(
"\x1b]1337;File=inline=1;size={};width={};height={};preserveAspectRatio=1:{}\x07",
png.len(),
cols,
rows,
b64,
);
vec![Segment::new(&osc, None, Some(vec![])), Segment::line()]
}
#[cfg(feature = "inline-images")]
fn to_png(&self) -> Vec<u8> {
use std::io::Cursor;
let img = ::image::RgbaImage::from_raw(self.width_px, self.height_px, self.rgba.clone())
.expect("rgba buffer length matches width_px × height_px × 4");
let mut buf = Cursor::new(Vec::new());
::image::DynamicImage::ImageRgba8(img)
.write_to(&mut buf, ::image::ImageFormat::Png)
.expect("PNG encoding to an in-memory buffer cannot fail");
buf.into_inner()
}
}
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);
}
#[cfg(feature = "inline-images")]
if caps.iterm {
return self.render_iterm(opts);
}
if caps.sixel {
return self.render_sixel(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),
}
}
}