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
renderImageRgbapipeline thumbnailOptionspreset 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)