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.:autoresolves at render time using the transport's capabilities (see the Images guide for the resolution table). Explicit protocols are honored except overCellSession-style transports where:halfblocksis 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 fullExRatatui.Style.color/0shape: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:
:startmetadata —%{format: atom, bytes: non_neg_integer}.:formatis one of:png,:jpeg,:gif,:webp,:bmp, or:unknown(sniffed from the magic bytes).:stopmetadata — adds:widthand:heighton 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
@type background() :: nil | ExRatatui.Style.color()
@type new_opts() :: [protocol: protocol(), resize: resize(), background: background()]
@type probe_result() :: %{ protocol: protocol(), font_size: {pos_integer(), pos_integer()} }
@type protocol() :: :auto | :halfblocks | :kitty | :sixel | :iterm2
@type resize() :: :fit | :crop | :scale
Functions
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.
@spec dimensions(ExRatatui.Widgets.Image.t() | reference()) :: {non_neg_integer(), non_neg_integer()}
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.
@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.
@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.
@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 asdimensions/1returns).cell_area— the render area in cells, as{cols, rows}.font_size— the terminal's cell pixel size, as{width, height}. Get this fromprobe_terminal/0or default to{10, 20}(which matchesPicker::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}