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