ExRatatui.Image (ExRatatui v0.10.0)

Copy Markdown View Source

Construct image widgets from raw image bytes.

Decodes PNG/JPEG/GIF/WebP/BMP binaries into a stateful widget handle backed by ratatui-image. The same widget renders across every ExRatatui transport: in a Kitty-graphics capable local terminal it uses the Kitty protocol; over CellSession (Livebook / Kino) it falls back to Unicode halfblocks automatically.

{:ok, picture} = ExRatatui.Image.new(File.read!("priv/slides/cover.png"))

# Or pick explicit options
{:ok, picture} =
  ExRatatui.Image.new(bytes, resize: :crop, protocol: :kitty)

Options

  • :protocol - which terminal image protocol to render with. One of :auto (default), :halfblocks, :kitty, :sixel, :iterm2. :auto resolves at render time using the transport's capabilities (see the Images guide for the resolution table). Explicit protocols are honored except over CellSession-style transports where :halfblocks is forced.
  • :resize - resize strategy. :fit (default, preserve aspect ratio inside the rect), :crop (preserve aspect, fill the rect, crop the overflow), or :scale (stretch to fill).
  • :background - background color used to fill transparency / unused area. Accepts the full ExRatatui.Style.color/0 shape: nil (default, transparent), a named color atom (:red, :dark_gray, …), an {:rgb, r, g, b} tuple, an {:indexed, n} xterm 256-color code, or a raw {r, g, b} tuple. Named and indexed values are converted to RGB at the Elixir boundary using the standard ANSI palette.

Errors

new/2 returns {:ok, widget} on success, or {:error, {:decode_failed, message}} when the bytes can't be decoded as a supported image format.

Telemetry

Each new/2 call emits a [:ex_ratatui, :image, :decode] span:

  • :start metadata — %{format: atom, bytes: non_neg_integer}. :format is one of :png, :jpeg, :gif, :webp, :bmp, or :unknown (sniffed from the magic bytes).
  • :stop metadata — adds :width and :height on success, or :error (reason) on failure.

Per-render encode timing (Kitty / Sixel / iTerm2 payload generation) isn't emitted as its own event — it happens inside the Rust render NIF; the existing [:ex_ratatui, :render, :frame] span covers total frame time, which includes image encode.

Summary

Functions

Probes the local terminal and caches the result on terminal_ref.

Return the {width, height} of the decoded source image in pixels.

Decode image bytes into a stateful widget.

Queries the local terminal for image-protocol capabilities and font size.

Predicts the rendered output pixel dimensions for an image, given the cell area it'll be drawn into.

Types

background()

@type background() :: nil | ExRatatui.Style.color()

new_opts()

@type new_opts() :: [protocol: protocol(), resize: resize(), background: background()]

probe_result()

@type probe_result() :: %{
  protocol: protocol(),
  font_size: {pos_integer(), pos_integer()}
}

protocol()

@type protocol() :: :auto | :halfblocks | :kitty | :sixel | :iterm2

resize()

@type resize() :: :fit | :crop | :scale

Functions

auto_local_protocol(terminal_ref)

@spec auto_local_protocol(reference()) :: :ok | {:error, term()}

Probes the local terminal and caches the result on terminal_ref.

When the probe succeeds, the cached protocol and font size are used for every protocol: :auto image rendered through terminal_ref — Kitty on a Kitty terminal, halfblocks on a basic terminal, etc. When it fails (no TTY, no response), the cache stays empty and :auto images fall back to halfblocks. Either way this is a one-shot opt-in: call it once at app start, typically right after acquiring the terminal reference.

ExRatatui.run(fn terminal ->
  ExRatatui.Image.auto_local_protocol(terminal)
  # ...
end)

Returns :ok on success, {:error, reason} if the probe failed (the cache is untouched in that case). Per-image explicit protocol choices at ExRatatui.Image.new/2 are always honored regardless of the probe.

dimensions(ref)

Return the {width, height} of the decoded source image in pixels.

This is the original image's pixel size, not its rendered cell size. Useful for laying out around an image of known aspect ratio.

new(bytes, opts \\ [])

@spec new(binary(), new_opts()) ::
  {:ok, ExRatatui.Widgets.Image.t()} | {:error, {:decode_failed, String.t()}}

Decode image bytes into a stateful widget.

Returns {:ok, %ExRatatui.Widgets.Image{}} on success, or {:error, {:decode_failed, message}} if bytes is not a valid PNG/JPEG/GIF/WebP/BMP payload. The format is auto-detected from the bytes — no extension or content-type hint is required.

probe_terminal()

@spec probe_terminal() :: {:ok, probe_result()} | {:error, term()}

Queries the local terminal for image-protocol capabilities and font size.

Sends a small escape-sequence probe to stdout and waits for the terminal's reply on stdin (ratatui-image's Picker::from_query_stdio). Runs on a dirty IO scheduler so it doesn't block the BEAM main run queue. Returns the detected protocol and cell pixel size on success, or {:error, reason} if the terminal didn't respond, isn't a TTY, or the probe timed out.

Use this when you want to decide your own fallback policy. Most apps should call auto_local_protocol/1 instead, which caches the result on a terminal reference so protocol: :auto images render with the detected protocol automatically.

render_size(arg1, arg2, arg3, resize)

@spec render_size(
  {pos_integer(), pos_integer()},
  {pos_integer(), pos_integer()},
  {pos_integer(), pos_integer()},
  resize()
) :: {pos_integer(), pos_integer()}

Predicts the rendered output pixel dimensions for an image, given the cell area it'll be drawn into.

Mirrors ratatui-image's Resize::needs_resize_pixels + the fit_area_proportionally helper byte-for-byte (no drift). Useful for status panels in demos, layout decisions where you want to size sibling widgets relative to where the image will actually render, or understanding why :fit and :crop produce identical output when the source image is smaller than the target area on both axes.

Inputs

  • source — the source image's pixel dimensions as {width, height} (same as dimensions/1 returns).
  • cell_area — the render area in cells, as {cols, rows}.
  • font_size — the terminal's cell pixel size, as {width, height}. Get this from probe_terminal/0 or default to {10, 20} (which matches Picker::halfblocks).
  • resize — one of :fit / :crop / :scale.

Returns {width_px, height_px} for the rendered pixel size.

The Fit/Crop no-upscale clamp

Both :fit and :crop clamp output to the source image's natural pixel size — they never upscale. This means a 400×300 source rendered into an 800×500 target stays at 400×300 anchored at the corner. Only :scale upscales aspect-preservingly to fill the area. See the Images guide for the full rationale.

iex> ExRatatui.Image.render_size({400, 300}, {80, 24}, {10, 20}, :fit)
{400, 300}
iex> ExRatatui.Image.render_size({400, 300}, {80, 24}, {10, 20}, :scale)
{640, 480}
iex> ExRatatui.Image.render_size({2000, 1000}, {80, 24}, {10, 20}, :fit)
{800, 400}