All source listed below is under MIT license if no LICENSE file stating different is available.

termimg

Picture-perfect image rendering in the native terminal. The library takes raw RGBA pixel data and reproduces it using the highest-fidelity protocol the terminal supports, falling back through progressively coarser Unicode block characters when graphics protocols are unavailable.

Rendering protocols

The terminal is inspected at runtime and the best available protocol is selected automatically.

Protocol Fidelity Mechanism
Kitty Exact pixels RGBA transmitted in base64 chunks via the kitty graphics protocol
iTerm2 Exact Raw file bytes via OSC 1337 inline image
Sixel 256-color palette DCS sixel bands with RLE compression and Floyd-Steinberg dithering
Quarter-block Four sub-cells per character Unicode quadrant blocks (16 characters) with 24-bit fg/bg
Half-block Two vertical samples per cell Unicode U+2580 with 24-bit foreground/background

Protocol detection

Priority order: kitty > iterm2 > sixel > quarterblock > halfblock.

Detection uses environment variables as heuristics (live escape-sequence queries are not used to avoid terminal lock-up edge cases):

Variable / Heuristic Detected protocol
KITTY_WINDOW_ID is set kitty
TERM_PROGRAM is kitty/ghostty/WezTerm kitty
WEZTERM_EXECUTABLE is set kitty + sixel
TERM contains kitty kitty
TERM_PROGRAM is iTerm.app/vscode iterm2
COLORTERM contains sixel sixel
TERM contains sixel sixel
TERM is foot/xterm-foot/mlterm sixel
TERM_PROGRAM is tmux none (passthrough required)

Half-block is always available and serves as the universal fallback.

Terminal compatibility

Terminal kitty iterm2 sixel quarterblock halfblock
kitty Y - - Y Y
iTerm2 - Y - Y Y
WezTerm Y - Y Y Y
Ghostty Y - - Y Y
Foot - - Y Y Y
xterm (compiled) - - Y Y Y
mlterm - - Y Y Y
VS Code terminal - Y - Y Y
GNOME Terminal - - - Y Y
Konsole - - - Y Y
tmux - - - Y Y
Windows Terminal - - - Y Y

Any truecolor-capable terminal supports the block renderers.

Block densities

The density field in RenderOptions controls how many pixel samples map to each terminal cell. Higher densities produce finer detail at the cost of larger output.

Density Enum Sub-cells per cell Block characters used Best for
Full bdFull 1 One color per cell Minimal output
Half bdHalf 2 vertical U+2580 half block General use (default)
Quarter bdQuarter 2x2 quadrant All 16 Unicode quadrants Thumbnails, fine detail

Presets

Preset Protocol Fit Width Density Dither Best use
defaultOptions() Auto Contain 90% Half No Full-size display
thumbnailOptions() Auto Contain 40 cols Quarter Yes Thumbnails, previews

Scaling

All renderers use bilinear interpolation for high-quality upscaling and downscaling. Edge-clamped sampling ensures no black borders appear at edges. Floyd-Steinberg error diffusion dithering is available for the block-character and sixel renderers (enable with opts.dither = true), reducing banding in gradients when downscaling.

Requirements

Nim 2.0 or newer. A truecolor-capable terminal is required for block-character output; kitty, iTerm2, or sixel support enables exact pixel rendering.

Installation

Add to your project's .nimble file:

requires "termimg"

Then install locally or from a path:

nimble install          # from the project root
nimble install /path/to/termimg

Or use directly without nimble:

nim c --path:src your_app.nim

Quick start

import termimg

# Detect terminal capabilities once at startup
let caps = detectCapabilities()

# Default rendering - half-block, contain fit, 90% width
let output = renderImageRgba(pixelBytes, 640, 480, caps, defaultOptions())
stdout.write(output)

# Thumbnail - quarter-block, 40 cols max, dithered
let thumb = renderImageRgba(pixelBytes, 640, 480, caps, thumbnailOptions())
stdout.write(thumb)

# Raw file bytes (iTerm2 protocol requires raw image bytes)
let raw = readFile("image.png")
let output2 = renderImageRaw(raw, 800, 600, caps, defaultOptions())
stdout.write(output2)

Demo program

A complete demo is at examples/demo.nim:

make example

It renders four synthetic patterns (checkerboard, rainbow bars, target circles, warm glow) using every available protocol and prints terminal capability information. Includes:

  • Gallery: 3 patterns (checkerboard, rainbow, target) side-by-side
  • All four patterns in a column via thumbnailOptions()
  • Column layout: Same rainbow pattern at all 3 block densities stacked
  • Fit-mode grid: Target circles rendered under each of 4 fit modes
  • Extreme aspect ratios: Wide and tall checkerboard renders
make example

API reference

Types

Type Fields Description
Protocol enum: ptKitty, ptSixel, ptIterm2, ptQuarterBlock, ptHalfBlock, ptAuto Image protocol
FitMode enum: fmContain, fmStretch, fmWidth, fmOriginal, fmCellExact Scaling strategy
BlockDensity enum: bdFull, bdHalf, bdQuarter Pixel samples per cell
ImageData width, height: int; data: seq[uint8] RGBA pixel buffer
RenderOptions protocol, fit, maxWidth, maxHeight, maxWidthRatio, maxHeightRatio, backgroundRgb, dither, density Rendering parameters
TerminalCapabilities columns, rows, cell, protocols Detected terminal state
Geometry pixelWidth, pixelHeight, columns, sampleHeight Computed render dimensions
CellSize width, height: int Terminal cell dimensions in pixels

Procs

Proc Signature Description
detectCapabilities (): TerminalCapabilities Resolve terminal size and protocol support
selectProtocol (caps, preferred): Protocol Pick the best available protocol
computeGeometry (imgW, imgH, caps, opts): Geometry Compute aspect-preserving render dimensions
renderImageRgba (data, w, h, caps, opts): string Render RGBA pixels to escape sequence string
renderImageRaw (rawBytes, w, h, caps, opts): string Render raw file bytes (iTerm2 path)
defaultOptions (): RenderOptions Half-block, contain fit, 90% width, no dither
thumbnailOptions (): RenderOptions Quarter-block, 40 cols, 50% area, dithered
bilinearResizeRgba (img, dstW, dstH): seq[uint8] Bilinear RGBA scaling
bilinearResizeRgb (rgb, srcW, srcH, dstW, dstH): seq[uint8] Bilinear RGB scaling
terminalIsInteractive (): bool Check if both stdout and stdin are TTYs
animated (src: ImageSource): bool Check if an image source has multiple frames

Protocol-specific renderers

Proc Description
renderKitty(img, targetW, targetH): string Kitty graphics protocol
renderKittyFile(filePath): string Kitty file-based PNG render
renderSixel(img, bg, targetW, targetH, dither): string Sixel with 256-color palette
renderIterm2(rawBytes, w, h): string iTerm2 OSC 1337 inline image
renderHalfBlock(img, bg, columns, sampleHeight, dither): string Unicode half-block
renderQuarterBlock(img, bg, columns, sampleHeight, dither): string Unicode quarter-block

ImageSource (multi-frame / animated images)

Type Description
Frame image: ImageData, duration: float (seconds)
ImageSource frames: seq[Frame], loop: int (0 = infinite)

The library imports the data types but does not decode animated formats. Use a separate library (e.g. stb_image bindings) to produce ImageSource.

TerminalCapabilities helpers

Proc Description
caps.pixelWidth Total pixel width: columns * cell.width
caps.pixelHeight Total pixel height: rows * cell.height
caps.supports(proto) Check if a protocol is in the detected set

Fit modes

Mode Behavior
fmContain Scale to fit within terminal bounds, preserving aspect ratio (default)
fmStretch Fill the terminal bounds, ignoring aspect ratio
fmWidth Scale to fit terminal width, height follows aspect ratio
fmOriginal Use native size if it fits, otherwise scale to contain
fmCellExact User specifies exact column count, height derives from aspect ratio

Makefile targets

Target Description
all Run check, test, and example
check Type-check library, test suite, and examples
test Compile and run the test program
example Compile and run the demo program
clean Remove build artifacts
make all        # full pipeline
make test       # compile and run tests
make example    # run the demo
make clean      # remove nimcache and binaries

Architecture

.gitignore
Makefile
README.md
termimg.nimble

src/termimg.nim      top-level module, re-exports public API (`import termimg`)
src/termimg/
  types.nim            shared types, Geometry, RenderOptions, computeGeometry
  capabilities.nim     terminal size, cell dimensions, protocol detection
  renderer.nim         protocol selector, renderImageRgba, renderImageRaw
  scaling.nim          bilinear RGBA/RGB interpolation, Floyd-Steinberg dithering
  kitty.nim            kitty graphics protocol encoder
  sixel.nim            sixel protocol encoder with palette quantization
  iterm2.nim           iTerm2 inline image protocol encoder
  halfblock.nim        Unicode half-block renderer (universal fallback)
  quarterblock.nim     Unicode quadrant-block renderer (2x density)

tests/
  test_termimg.nim     integration test: 31 test cases across all features (gallery, columns, comparisons, corruption)

examples/
  demo.nim             standalone demo program with gallery, column layout, fit-grid comparisons

How the block renderers work

Half-block (U+2580)

Each terminal cell displays two pixels vertically. Upper pixel = foreground colour (CSI 38;2;R;G;Bm), lower pixel = background colour (CSI 48;2;R;G;Bm). Colour sequences are emitted only when the colour changes between adjacent columns, minimizing output size.

┌────────────────────┐
│  fg: upper pixel   │  ← CSI 38;2;R;G;Bm
│  bg: lower pixel   │  ← CSI 48;2;R;G;Bm
│       ▀            │  ← U+2580 UPPER HALF BLOCK
└────────────────────┘

Quarter-block (16 quadrant characters)

Each terminal cell covers a 2x2 pixel region. The four sub-pixels are clustered into two colour groups (foreground and background) by luminance thresholding, then mapped to one of 16 Unicode block-drawing characters:

TL TR    mask bits:  ▖▗▘▙▚▛▜▝▞▟▀▄▌▐█
BL BR    3:TL 2:TR 1:BL 0:BR     spaces

Dithering

Floyd-Steinberg error diffusion is available for halfblock, quarterblock, and sixel renderers. Enable via opts.dither = true. Error is distributed to four neighboring pixels (7/16 right, 3/16 bottom-left, 5/16 bottom, 1/16 bottom-right), smoothing gradient banding at the cost of slight grain.

Verification

The library compiles without errors or warnings under Nim 2+. The test suite runs 31 test cases covering:

  • Protocol detection and capability reporting
  • Geometry computation at multiple aspect ratios
  • All five fit modes (contain, stretch, width, original, cellexact)
  • All three block densities (full, half, quarter)
  • Bilinear scaling (up and down)
  • Floyd-Steinberg dithering
  • Full renderImageRgba pipeline
  • thumbnailOptions preset pipeline
  • Zero/negative/invalid dimensions (guard rails)
  • Short RGBA buffer (guard against underflow)
  • Empty raw bytes (guard against underflow)
  • Non-terminal / fake environment (no crash)
  • Gallery layout (3 patterns side-by-side)
  • Column layout (3 densities stacked for comparison)
  • Fit-mode comparison grid (4 fit modes stacked)
  • Extreme aspect ratios (wide vs tall)
  • Off-by-one buffer sizes (1 byte short, 1 byte extra)
  • Tiny images (1x1, 1x10, 10x1)
  • Fully transparent image (alpha=0 everywhere)
  • Monochrome image (all same colour)
  • Extreme opts (maxWidth=0, maxWidth=-1, maxHeight=0)
  • Empty protocol set (fallback to half-block)
  • Extreme pixel aspect ratios (200x1, 1x200)
  • Zero background + dithering on black
  • Random-stress (20 renders at random sizes/fit modes/densities)
  • Tiny buffer claimed as huge image (guard rail)
examples
src
tests
.gitignore
Makefile
nimimg.nimble
README.md