wazum / stipple
Render small SVG icons to monochrome ANSI for terminal UIs (Laravel/Prompts, Symfony Console, plain CLI).
Requires
- php: ^8.2
- ext-dom: *
- ext-gd: *
- ext-mbstring: *
- ext-simplexml: *
- meyfa/php-svg: ^0.16
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.50
- phpstan/phpstan: ^1.11
- phpunit/phpunit: ^11
README
Render small SVG icons as monochrome ANSI in the terminal — pure PHP, zero system dependencies.
Drop it into any PHP CLI tool that wants real icons next to its menu items. The output is a plain string ending in \n per row — works equally well with echo, Symfony Console, Laravel/Prompts, or whatever else writes to a TTY. Two pluggable samplers ship: Braille (default) for highest density and half-block as a more universal fallback.
Preview
The actions-brand-github icon from TYPO3.Icons (MIT-licensed) — source SVG and Braille rendering at heights 4, 6, 8:
Same icon at height(8) rendered with the half-block sampler — coarser, but works in any terminal/font:
▄▄███████▄▄
███▀█▀▀██▀███
████ ████
████ ████
▀███▄ ▄████
▀███▀ ████▀
▀▀█ ██▀
Install
composer require wazum/stipple
Requires PHP 8.2+ with ext-gd, ext-mbstring, ext-dom, ext-simplexml. No system binaries needed — rasterization is handled in pure PHP via meyfa/php-svg.
Usage
use Wazum\Stipple\Stipple; // One-shot, defaults: height 8 cells, terminal default fg, Braille sampler. echo Stipple::render('/path/to/icon.svg'); // Fluent echo Stipple::make('/path/to/icon.svg') ->height(4) // cells; valid 1..256 ->color('#00ffff') // optional, 6-digit hex; null → terminal default fg ->accent('#ff8700') // overrides the fallback in any var(--icon-color-accent, …) call in the SVG ->threshold(0.5) // alpha-weighted luminance cutoff in [0.0, 1.0] ->toString(); // Inline SVG instead of a path Stipple::makeFromString('<svg ...>')->toString();
The output is a plain string ending in \n per row, safe to echo or pass to Laravel/Prompts' note()/info().
Samplers
use Wazum\Stipple\Sampler\BrailleSampler; use Wazum\Stipple\Sampler\HalfBlockSampler; Stipple::make($path)->sampler(new BrailleSampler())->toString(); // default — 2x4 px/cell Stipple::make($path)->sampler(new HalfBlockSampler())->toString(); // 1x2 px/cell, more universal
| Sampler | Density | Glyphs | Best for |
|---|---|---|---|
BrailleSampler (default) |
2×4 px/cell | U+2800–U+28FF |
Highest fidelity for line-art icons. Needs a Braille-capable monospace font (JetBrains Mono, Cascadia, DejaVu, Iosevka all work). |
HalfBlockSampler |
1×2 px/cell | ▀ ▄ █ |
Universal — works in any terminal/font including legacy cmd.exe. |
For a 16×16 SVG at height(4) the Braille sampler maps 1:1 with the source pixel grid; at height(8) it super-samples 2×.
Demo
Two scripts are bundled. Run them after composer install:
php bin/demo.php # height 4/6/8 comparison across two samplers php bin/icon-row.php # ten icons rendered side-by-side in a single 4-line row
Pluggable rasterizer
The default rasterizer wraps meyfa/php-svg. You can swap in a different backend later by implementing RasterizerInterface:
use Wazum\Stipple\Rasterizer\RasterizerInterface; final class RsvgConvertRasterizer implements RasterizerInterface { public function rasterize(string $svg, int $widthPx, int $heightPx): \GdImage { /* … */ } } Stipple::make($path)->rasterizer(new RsvgConvertRasterizer())->toString();
Security
The preprocessor hardens SVG input before rasterization:
- DOCTYPE / ENTITY declarations are rejected pre-parse (XXE attack surface).
<script>,<foreignObject>, and all<image>elements are rejected after parse — embedded raster is out of scope, and allowing<image href="file://..."/>would let the rasterizer dependencyfile_get_contents()arbitrary local files.- libxml is invoked with
LIBXML_NONET(no network). currentColoris substituted with a configurable foreground hex;var(--icon-color-accent, ...)is resolved DOM-side so the rasterizer never has to deal with CSS custom properties.
Supported SVG features
The preprocessor handles common patterns found in icon SVGs from any source:
fill="currentColor"andstroke="currentColor"— substituted with#ffffffso the rasterizer always renders at full luminance, regardless of the terminal foreground colour.style="fill: currentColor; …"— same substitution inside inline CSS, with other declarations preserved.var(--icon-color-accent, <fallback-hex>)— resolved DOM-side using either the configuredaccent()value or the embedded fallback hex (the rasterizer doesn't resolve CSS custom properties on its own).viewBox(space- or comma-separated) and rootwidth/heightnumeric attributes for aspect-ratio resolution.
Anything not in the above list is passed through to the rasterizer untouched.
Development
composer install vendor/bin/phpunit vendor/bin/phpstan analyse src --level=8
License
MIT — see LICENSE.