Crate ratatui_image

source
Expand description

§Image widgets with multiple graphics protocol backends for ratatui

ratatui is an immediate-mode TUI library. ratatui-image tackles 3 general problems when rendering images with an immediate-mode TUI:

Query the terminal for available graphics protocols

Some terminals may implement one or more graphics protocols, such as Sixels, or the iTerm2 or Kitty graphics protocols. Guess by env vars. If that fails, query the terminal with some control sequences. Fallback to “halfblocks” which uses some unicode half-block characters with fore- and background colors.

Query the terminal for the font-size in pixels.

If there is an actual graphics protocol available, it is necessary to know the font-size to be able to map the image pixels to character cell area. The image can be resized, fit, or cropped to an area. Query the terminal for the window and columns/rows sizes, and derive the font-size.

Render the image by the means of the guessed protocol.

Some protocols, like Sixels, are essentially “immediate-mode”, but we still need to avoid the TUI from overwriting the image area, even with blank characters. Other protocols, like Kitty, are essentially stateful, but at least provide a way to re-render an image that has been loaded, at a different or same position.

§Quick start

use ratatui::{backend::TestBackend, Terminal, Frame};
use ratatui_image::{picker::Picker, StatefulImage, protocol::StatefulProtocol};

struct App {
    // We need to hold the render state.
    image: Box<dyn StatefulProtocol>,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let backend = TestBackend::new(80, 30);
    let mut terminal = Terminal::new(backend)?;

    // Should use Picker::from_termios(), to get the font size,
    // but we can't put that here because that would break doctests!
    let mut picker = Picker::new((8, 12));
    // Guess the protocol.
    picker.guess_protocol();

    // Load an image with the image crate.
    let dyn_img = image::io::Reader::open("./assets/Ada.png")?.decode()?;

    // Create the Protocol which will be used by the widget.
    let image = picker.new_resize_protocol(dyn_img);

    let mut app = App { image };

    // This would be your typical `loop {` in a real app:
    terminal.draw(|f| ui(f, &mut app))?;

    Ok(())
}

fn ui(f: &mut Frame<'_>, app: &mut App) {
    // The image widget.
    let image = StatefulImage::new(None);
    // Render with the protocol state.
    f.render_stateful_widget(image, f.size(), &mut app.image);
}

The picker::Picker helper is there to do all this font-size and graphics-protocol guessing, and also to map character-cell-size to pixel size so that we can e.g. “fit” an image inside a desired columns+rows bound, and so on.

§Widget choice

  • The Image widget does not adapt to rendering area (except not drawing at all if space is insufficient), may be a bit more bug prone (overdrawing or artifacts), and is not friendly with some of the protocols (e.g. the Kitty graphics protocol, which is stateful). Its big upside is that it is stateless (in terms of ratatui, i.e. immediate-mode), and thus can never block the rendering thread/task. A lot of ratatui apps only use stateless widgets.
  • The StatefulImage widget adapts to its render area, is more robust against overdraw bugs and artifacts, and plays nicer with some of the graphics protocols. The resizing and encoding is blocking by default, but it is possible to offload this to another thread or async task (see examples/async.rs). It must be rendered with render_stateful_widget (i.e. with some mutable state).

§Examples

  • examples/demo.rs is a fully fledged demo.
  • examples/async.rs shows how to offload resize and encoding to another thread, to avoid blocking the UI thread.

The lib also includes a binary that renders an image file, but it is focused on testing.

§Features

  • rustix (default) enables much better guessing of graphics protocols with rustix::termios::tcgetattr.
  • crossterm or termion should match your ratatui backend. termwiz is available, but not working correctly with ratatu-image.
  • serde for #[derive]s on picker::ProtocolType for convenience, because it might be useful to save it in some user configuration.
  • image-defaults (default) just enables image/defaults (image has default-features = false). To only support a selection of image formats and cut down dependencies, disable this feature, add image to your crate, and enable its features/formats as desired. See https://doc.rust-lang.org/cargo/reference/features.html#feature-unification.

Modules§

  • Helper module to build a protocol, and swap protocols at runtime
  • Protocol backends for the widgets
  • Widget that separates resize+encode from rendering. This allows for rendering to be non-blocking, offloading resize+encode into another thread. See examples/async.rs for how to setup the threads and channels. At least one worker thread for resize+encode is required, the example shows how to combine the needs-resize-polling with other terminal events into one event loop.

Structs§

Enums§

Type Aliases§

  • The terminal’s font size in (width, height)