Getting started

Shell
dotnet add package Nullean.TerminalImages

Requires .NET 10.

C#
using Nullean.TerminalImages;
using Nullean.TerminalImages.Detection;

// 1. Ask what the terminal speaks. This reads the environment and, on macOS and Linux, asks the terminal once.
TerminalImageCapabilities capabilities = await TerminalImageDetector.DetectAsync();

// 2. Draw. The renderer scales the image to fit, picks the encoder, and leaves the cursor below the image.
var renderer = new TerminalImageRenderer(capabilities);
bool drawn = renderer.RenderPng(File.ReadAllBytes("photo.png"), Console.Out);

Render returns false when the terminal cannot show images at all (output redirected, TERM=dumb, or NULLEAN_TERMINAL_IMAGES=none).

Write to a Stream with a UTF-8 writer if you draw half blocks: they contain ▀.

C#
using var stdout = Console.OpenStandardOutput();
using var writer = new StreamWriter(stdout, new UTF8Encoding(false));
renderer.Render(image, writer);
writer.Flush();

The core works on RgbaImage: width, height and 8-bit RGBA pixels (non-premultiplied, rows top to bottom).

C#
var image = new RgbaImage(width, height, pixels);
var decoded = PngDecoder.Decode(File.ReadAllBytes(path));
byte[] png = PngEncoder.Encode(image);
  1. your own pixels
  2. Nullean.TerminalImages.Png: every PNG colour type, depth and Adam7 interlacing

There is no JPEG, GIF or WebP decoder: decode those with a library of your choice and pass the pixels in.

C#
// Console.WindowWidth and WindowHeight are 0 when stdout is not a terminal, so check before using them.
var columns = Console.WindowWidth > 0 ? Console.WindowWidth : 80;
var rows = Console.WindowHeight > 2 ? Console.WindowHeight - 2 : 24;

var renderer = new TerminalImageRenderer(capabilities, new TerminalImageRenderOptions
{
    MaxColumns = columns,
    MaxRows = rows,
    AllowUpscale = false,
    Resample = ResampleMode.Auto,
    FallbackCellSize = new CellPixelSize(10, 20),
});

TerminalImageLayout layout = renderer.Layout(image);
  1. small images keep their size
  2. Nearest for pixel art
  3. used when the terminal's cell size is unknown
  4. the pixel size and cells, without drawing

The cell size matters: a Sixel image is drawn at its pixel size, so the renderer needs to know how many pixels a cell is to cover the right number of rows. A probe finds it (CSI 16 t); otherwise set NULLEAN_TERMINAL_CELL_SIZE=9x18 or FallbackCellSize.

C#
var capabilities = new TerminalImageCapabilities { Supported = [TerminalImageProtocol.Kitty] };

or use an encoder directly:

C#
new SixelEncoder { MaxColors = 64, Dither = true }.Encode(image, default, Console.Out);
new KittyEncoder { Compress = true }.Encode(image, new CellSize(40, 10), Console.Out);
Variable Effect
NULLEAN_TERMINAL_IMAGES kitty, iterm2, sixel, blocks, none force a protocol; auto detects
NULLEAN_TERMINAL_CELL_SIZE WIDTHxHEIGHT in pixels, for example 9x18