# 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:
```nim
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
```nim
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)