|
# 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)
|