sugarcraft / candy-codeshot
PHP port of charmbracelet/freeze — render code or terminal output to SVG (or PNG with ext-gd) screenshots with macOS-style window controls, drop shadow, line numbers, and inline ANSI SGR colour parsing.
Requires
- php: ^8.3
- ext-mbstring: *
- sugarcraft/sugar-ansi-parser: dev-master
- sugarcraft/sugar-core: dev-master
Requires (Dev)
- phpunit/phpunit: ^10.5
Suggests
- ext-gd: Required for PNG output in PngRenderer
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-28 16:45:41 UTC
README
CandyCodeshot
Formerly published as
sugarcraft/candy-freeze(renamed 2026-10). Namespace changed:SugarCraft\Freeze\→SugarCraft\CodeShot\.
candy-codeshot — turn code or terminal output into an SVG (or PNG) screenshot,
for PHP 8.3+. SVG output needs no ext-gd / Imagick; the SVG is plain text
suitable for git diffs and CI artifacts. PNG output is also available and
requires ext-gd.
composer require sugarcraft/candy-codeshot
CLI
echo "function hello() { return 'world'; }" \ | candy-codeshot --theme dracula --line-numbers > out.svg candy-codeshot input.php \ --theme tokyo-night --no-window --output screenshot.svg candy-codeshot notes.md --format png -o notes.png
Flags
All sixteen options. Flag values are separate arguments —
--theme dracula, not --theme=dracula (the = form is rejected as an
unrecognised flag).
--theme <name>— colour palette:dark(default),light,dracula,tokyo-night,nord.--padding <n>— content padding inside the frame (default 24).--no-window— drop the window chrome entirely (traffic lights, title bar).--no-shadow— drop the drop-shadow.--no-border— drop the frame outline.--line-numbers— render a left-gutter line counter.--border-radius <n>— corner radius of the frame (default 8).--window-style <style>— chrome style:macos(default),windows-terminal,iterm,hyper,none.nonecollapses the title-bar band (no empty 36 px strip).--ligatures— enable ligatures (font-variant-ligatures: normal). SVG only.--font <path>— embed a TTF/OTF/WOFF/WOFF2 font in the SVG. SVG only.--unsafe-paths— allow input /--font/--outputpaths outside the CWD (see Path confinement).--highlight <start:end[:color]>— highlight linesstart:endwith an optional colour (default#fffbe6). Ranges are validated:start >= 1,end >= start, else a friendly refusal, exit 2. SVG only.--format <svg|png>— output format (defaultsvg;pngrequiresext-gd).-t,--type <lang>— force the syntax-highlighting language (default: filename, then shebang, then content detection).-o,--output <path>— write the screenshot to a file instead of stdout.-h,--help— show the help message.
Exit codes
0— success.1— missing composerautoload.php(installation problem; the binary names itself and says to runcomposer install).2— usage or input error (bad flag, value missing, path outside the CWD, refused highlight, missingext-gdfor PNG, …).
Path confinement
For security, the input path, --font path and --output path must resolve
inside the current working directory; anything outside (including ..
traversal) is refused with exit 2. This keeps an untrusted command line — or a
scripted caller feeding paths from outside data — from reading arbitrary files
(embedding a font or reading input) or writing outside the project. Pass
--unsafe-paths to lift the restriction deliberately.
PNG output
--format png renders the same screenshot through PngRenderer using the GD
bitmap fonts:
candy-codeshot script.php --theme dracula --format png -o shot.png
PNG mode is more limited than SVG (see Known limitations):
theme fontSize/lineHeight are not honoured (GD's built-in bitmap font is
fixed-size), fonts cannot be embedded, and the SVG-only options --ligatures,
--font and --highlight are ignored. When you pass any of them together with
--format png, the CLI announces each dropped option on stderr instead of
failing silently. If ext-gd is missing, PNG requests exit 2 with a clear
message. Writing --format svg (or omitting it) to a *.png filename — or
--format png to a *.svg filename — still writes the bytes of the requested
format but warns on stderr that the extension and format disagree.
Library
use SugarCraft\CodeShot\SvgRenderer; $svg = SvgRenderer::dracula() ->withLineNumbers(true) ->withWindow(true) ->withPadding(24) ->withLigatures(true) ->render($code); file_put_contents('out.svg', $svg);
ANSI input is honoured — SGR foreground and background colours (16 / 256 /
24-bit truecolor) plus bold / italic / underline become <tspan> segments
and per-segment <rect> fills in the output. Foreground colours are read in
both spellings xterm allows — the flat 38;2;R;G;B and the grouped
38:2::R:G:B (ECMA-48 sub-parameters, where the slot after the mode is a
colour-space id and not the red component) — and both resolve to the same
#rrggbb. SGR codes the renderers deliberately do not map: reverse (7),
hidden (8), strikethrough (9) and dim (2) — they are parsed and ignored.
$svg = SvgRenderer::dark()->render("\x1b[31merror:\x1b[0m something broke"); // With background colour $svg = SvgRenderer::dark()->render("\x1b[44m\x1b[37malert:\x1b[0m background highlight");
PngRenderer is the GD-backed sibling with the same fluent surface:
use SugarCraft\CodeShot\PngRenderer; $png = PngRenderer::dracula() ->withLineNumbers(true) ->render($code); // requires ext-gd
API map
| Class | Role |
|---|---|
SvgRenderer |
TEA-style renderer producing SVG; factories dark()/light()/dracula()/tokyoNight()/nord(), fluent with*(). |
PngRenderer |
GD-backed renderer producing PNG bytes; same theme, fixed bitmap fonts. |
Theme |
Immutable palette + font config shared by both renderers (window-chrome style lives on the renderers). |
WindowStyle |
Enum of chrome styles: Macos, WindowsTerminal, ITerm2, Hyper, None. |
WindowChromeGeometry |
Per-style traffic-light / title-bar geometry (used by the SVG chrome). |
CodeHighlighter |
Keyword/string/comment highlighting for a detected language under a theme. |
LanguageDetector |
Heuristic language detection from filename, shebang and content. |
LayoutCalculator |
Canvas maths: content box, header band, shadow margin, totals. |
Segment |
One styled run of text (text + fg/bg/style flags) produced by the parser/highlighter. |
Theme\VsCodeThemeLoader |
Import a VS Code themes/*.json colour scheme into a Theme. |
Theme\ChromaThemeLoader |
Import a Chroma style map into a Theme. |
OutputWriter (+ StringOutputWriter, FileOutputWriter) |
Chunked-output helpers for callers that stream screenshots themselves; the renderers return complete bytes and do not call them. |
Lang |
i18n lookup wrapper (16 locales shipped under lang/). |
Ligatures
Code editors render ligatures (→, >=, !==, etc.) when font-variant-ligatures: normal is set. Enable it explicitly (SVG only):
$svg = SvgRenderer::dracula() ->withLigatures(true) ->render($code);
Language Detection
LanguageDetector provides heuristic detection from filename, shebang or content:
use SugarCraft\CodeShot\LanguageDetector; // From filename extension (first choice for file input) $lang = LanguageDetector::detectFromFilename('script.py'); // "python" $lang = LanguageDetector::detectFromFilename('foo.php'); // "php" // From content (shebang, then content signatures) $lang = LanguageDetector::detect($code); // "php", "bash", "python", ...
The CLI applies these sources in this order for a file argument: filename
extension first (so a Python file whose body has no distinctive markers still
highlights as Python), then shebang, then content signatures for stdin or when
the extension is unknown. Force a language explicitly with -t/--type.
Source details:
- Filename extension — the richest source (
detectFromFilename):.php,.py/.pyw,.js/.mjs/.cjs,.ts/.tsx,.rb,.sh,.go,.rs,.c,.cpp,.java,.cs,.swift,.kt,.scala,.sql,.html,.css/.scss/.sass,.json,.yaml/.yml,.md,.xml, and more. - Shebang —
#!/bin/bash,#!/usr/bin/env node,#!/usr/bin/env php, etc. - Content signatures — a deliberately small list matched by
detect(): php, javascript, python, ruby, bash, sql, html, css, json, yaml, markdown (TypeScript/Go/Rust are recognised by extension only, not by content).
Returns "text" when no match is found.
Themes
SvgRenderer::dark(); // charm-ish #0d1117 SvgRenderer::light(); // #f6f8fa SvgRenderer::dracula(); // #282a36 SvgRenderer::tokyoNight(); // #1a1b26 SvgRenderer::nord(); // #2e3440
Build a custom theme via the Theme constructor — set background / foreground
/ border / shadow / line-number colour / window-control colours / font family
/ size / line height. VS Code and Chroma palettes can be imported with
Theme\VsCodeThemeLoader::load() and Theme\ChromaThemeLoader::load().
Known limitations
- PNG / GD bitmap fonts —
PngRendereruses GD's built-in bitmap font: no Unicode beyond Latin-1, and the theme'sfontSize/lineHeightare not honoured. Font embedding, ligatures and--highlightare SVG-only (the CLI announces what PNG drops). - Shadow technique differs by format — SVG uses an
feDropShadowfilter; PNG draws a flat offset rectangle. Expect slightly different shadows. windows-terminalchrome is deliberately dark — its title bar uses the hard-coded#1e1e1e/#303030/#444444of the real Windows Terminal default theme and is not re-tinted by--theme(future seam: theme-derived chrome colours).- No SVG
<title>/<desc>— screenshots carry no accessible name yet. - Tabs and East-Asian-width glyphs count as one cell — layout width uses
mb_strlenper code point, so tab stops and wide CJK glyphs can shift the frame width relative to a browser's rendering. - Theme JSON truncation — theme loaders read at most 1 MB; larger files
fail with a
JsonException.
Demos
Code screenshot
ANSI input
Testing
cd candy-codeshot && composer install && vendor/bin/phpunit
Accessor deprecation (2026-10-24)
Per the house bare-accessor convention (AGENTS.md: "Bare accessors (no get"), ruling 3b), StringOutputWriter::getResult() was renamed to its bare twin result() — unclaimed on the class, and deliberately not lifted onto the OutputWriter interface, because FileOutputWriter has no accumulated string to hand back; result() is this implementation's surface. The legacy spelling stays a byte-identical one-line BC forwarder marked @deprecated with REMOVED-IN: 2.0.0; it will be removed in candy-codeshot v2.0.0.
| Namespace | Class | Deprecated | Use instead |
|---|---|---|---|
CodeShot |
StringOutputWriter |
getResult() |
result() |
tests/AccessorDeprecationTest.php pins BC identity on a live writer (empty buffer, after writes, after flush), the forwarder shape (single return line + markers), a discovery-first src/ census with an inverse leg that reddens any unmarked public get*(), and the README table agreement. The bundled renderers never call the writer, so there were no internal call sites to switch; the existing StringOutputWriterTest needles stay green through the forwarder.
Credits & inspiration
Originally inspired by the Go Charm ecosystem; SugarCraft is developed as a native PHP project.

