#sixel #terminal-graphics #kitty #iterm #terminal

rasteroid

turn images / videos into inline content for you terminal (iterm / kitty / sixel)

2 releases

Uses new Rust 2024

new 0.1.1 May 3, 2025
0.1.0 May 3, 2025

#9 in Rendering

Download history 54/week @ 2025-04-28

54 downloads per month
Used in mcat

MIT license

44KB
686 lines

rasteroid

A Rust library for displaying images and videos inline in terminal emulators, part of the mcat project.

Crates.io Documentation MIT License

Overview

rasteroid is a Rust library that enables displaying images and videos directly within terminal emulators. It provides support for multiple terminal graphics protocols, making it easy to integrate rich visual content into terminal applications.

Auto Detection

Protocol Terminal Emulators Description
Kitty Kitty, Ghostty High-performance terminal graphics protocol
iTerm2 iTerm2, WezTerm, Mintty, Rio, Warp, Konsole Widely supported protocol for inline images
Sixel Foot, Windows Terminal, sixel-tmux Legacy but widely supported pixel graphics format

Installation

Add to your Cargo.toml:

[dependencies]
rasteroid = "0.1.0"

Usage

Basic Usage

use std::fs::File;
use std::io::{self, Read};
use rasteroid::{InlineEncoder, inline_an_image};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Load an image file
    let mut file = File::open("image.png")?;
    let mut buffer = Vec::new();
    file.read_to_end(&mut buffer)?;
    
    // Auto-detect terminal and display image
    let encoder = InlineEncoder::auto_detect(false, false, false);
    inline_an_image(&buffer, io::stdout(), None, &encoder)?;
    
    Ok(())
}

Specifying Encoder Type

use std::io;
use rasteroid::{InlineEncoder, inline_an_image};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Load image data
    let mut file = File::open("image.png")?;
    let mut buffer = Vec::new();
    file.read_to_end(&mut buffer)?;
    
    // Explicitly choose Kitty protocol
    let encoder = InlineEncoder::Kitty;
    
    // Center the image and display it
    let center_offset = Some(10); // 10 columns from left
    inline_an_image(&buffer, io::stdout(), center_offset, &encoder)?;
    
    Ok(())
}

Working with Image Transformations

use image::io::Reader as ImageReader;
use rasteroid::image_extended::InlineImage;
use std::io;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Load an image
    let img = ImageReader::open("photo.jpg")?.decode()?;
    
    // Resize to 50% of terminal width, auto height
    let (resized_data, center_offset) = img.resize_plus(Some("50%"), None)?;
    
    // Display with auto-detected protocol
    let encoder = mcat_rasteroid::InlineEncoder::auto_detect(false, false, false);
    mcat_rasteroid::inline_an_image(&resized_data, io::stdout(), Some(center_offset), &encoder)?;
    
    Ok(())
}

Zoom and Pan

use image::io::Reader as ImageReader;
use rasteroid::image_extended::InlineImage;
use std::io;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Load image
    let img = ImageReader::open("large_map.png")?.decode()?;
    
    // Zoom in (level 3) and pan right (+2) and down (+1)
    let zoomed = img.zoom_pan(Some(3), Some(2), Some(1));
    
    // Resize to fit terminal width
    let (resized_data, center_offset) = zoomed.resize_plus(Some("80%"), None)?;
    
    // Display
    let encoder = mcat_rasteroid::InlineEncoder::auto_detect(false, false, false);
    mcat_rasteroid::inline_an_image(&resized_data, io::stdout(), Some(center_offset), &encoder)?;
    
    Ok(())
}

Dimension Specification

When resizing images, you can specify dimensions in various formats:

  • "800px" - Absolute pixel size
  • "50%" - Percentage of terminal size
  • "40c" - Terminal cell count
  • "800" - Raw number (interpreted as pixels)

Video Support

rasteroid supports displaying video frames using the Kitty / Iterm protocols:

the following is how mcat uses it:

For Iterm

fn video_to_gif(input: impl AsRef<str>) -> Result<Vec<u8>, Box<dyn error::Error>> {
    let input = input.as_ref();
    if input.ends_with(".gif") {
        let path = Path::new(input);
        let bytes = fs::read(path)?;
        return Ok(bytes);
    }

    let mut command =
        match fetch_manager::get_ffmpeg() {
            Some(c) => c,
            None => return Err(
                "ffmpeg isn't installed. either install it manually, or call `mcat --fetch-ffmpeg`"
                    .into(),
            ),
        };
    command
        .hwaccel("auto")
        .input(input)
        .format("gif")
        .output("-");

    let mut child = command.spawn()?;
    let mut stdout = child
        .take_stdout()
        .ok_or("failed to get stdout for ffmpeg")?;

    let mut output_bytes = Vec::new();
    stdout.read_to_end(&mut output_bytes)?;

    child.wait()?; // ensure process finishes cleanly

    Ok(output_bytes)
}

For Kitty:

fn video_to_frames(
    input: impl AsRef<str>,
) -> Result<Box<dyn Iterator<Item = OutputVideoFrame>>, Box<dyn error::Error>> {
    let input = input.as_ref();
    if !ffmpeg_sidecar::command::ffmpeg_is_installed() {
        eprintln!("ffmpeg isn't installed, installing.. it may take a little");
        ffmpeg_sidecar::download::auto_download()?;
    }

    let mut command =
        match fetch_manager::get_ffmpeg() {
            Some(c) => c,
            None => return Err(
                "ffmpeg isn't installed. either install it manually, or call `mcat --fetch-ffmpeg`"
                    .into(),
            ),
        };
    command.hwaccel("auto").input(input).rawvideo();

    let mut child = command.spawn()?;
    let frames = child.iter()?.filter_frames();

    Ok(Box::new(frames))
}

Finally:

// OutputVideoFrame is from ffmpeg-sidecar (you can use whatever suits you, just needs to impl frame)
pub struct KittyFrames(pub OutputVideoFrame);
impl Frame for KittyFrames {
    fn width(&self) -> u16 {
        self.0.width as u16
    }
    fn height(&self) -> u16 {
        self.0.height as u16
    }
    fn timestamp(&self) -> f32 {
        self.0.timestamp
    }
    fn data(&self) -> &[u8] {
        &self.0.data
    }
}

pub fn inline_a_video(
    input: impl AsRef<str>,
    out: &mut impl Write,
    inline_encoder: &rasteroid::InlineEncoder,
    center: bool,
) -> Result<(), Box<dyn error::Error>> {
    match inline_encoder {
        rasteroid::InlineEncoder::Kitty => {
            let frames = video_to_frames(input)?;
            let mut kitty_frames = frames.map(KittyFrames);
            let id = rand::random::<u32>();
            rasteroid::kitty_encoder::encode_frames(&mut kitty_frames, out, id, center)?;
            Ok(())
        }
        rasteroid::InlineEncoder::Iterm => {
            let gif = video_to_gif(input)?;
            let dyn_img = image::load_from_memory_with_format(&gif, image::ImageFormat::Gif)?;
            let offset = match center {
                true => Some(rasteroid::term_misc::center_image(dyn_img.width() as u16)),
                false => None,
            };
            rasteroid::iterm_encoder::encode_image(&gif, out, offset)?;
            Ok(())
        }
        rasteroid::InlineEncoder::Sixel => Err("Cannot view videos in sixel".into()),
    }
}

Terminal Size Utilities

rasteroid provides utilities for working with terminal dimensions:

use rasteroid::term_misc::{init_winsize, break_size_string, get_winsize, SizeDirection, dim_to_px};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Initialize with fallback values
    let spx = break_size_string("1920x1080")?;
    let sc = break_size_string("100x30")?;
    init_winsize(&spx, &sc, None)?;
    
    // Get window size
    let winsize = get_winsize();
    println!("Terminal is {} columns by {} rows", winsize.sc_width, winsize.sc_height);
    println!("Terminal is {} pixels by {} pixels", winsize.spx_width, winsize.spx_height);
    
    // Convert dimensions
    let width_px = dim_to_px("50%", SizeDirection::Width)?;
    println!("50% of terminal width is {} pixels", width_px);
    
    Ok(())
}

License

This project is licensed under the MIT License - see the LICENSE under mcat for details.

Dependencies

~131MB
~2M SLoC