SVG, PNG, GIF, and MP4 export

Use save_svg for dependency-free snapshots. Install the optional export extra for PNG, GIF, and MP4; the base animation installation remains unchanged.

pip install "bruhanimate[export]"
# In a source checkout:
uv sync --locked --extra export

MP4 uses the FFmpeg executable supplied by imageio-ffmpeg, without a separate FFmpeg installation on supported Windows, Linux, and macOS wheels. Files play in ordinary image/video viewers. These exports do not require a .cast player.

Snapshots

Capture supplies immutable Frame objects. Export them without opening a terminal, consuming input, or advancing the animation again:

from pathlib import Path
from tempfile import TemporaryDirectory
from bruhanimate import ExportStyle, capture, save_svg

frames = []
capture("plasma", on_frame=frames.append, frames=1, width=20, height=6)
with TemporaryDirectory() as directory:
    save_svg(frames[0], Path(directory) / "plasma.svg",
             style=ExportStyle(cell_size=(10, 20), background=(8, 8, 16)))

save_png(frame, path, style=...) has the same interface. ExportStyle is a frozen dataclass with discoverable cell_size, font_size, font, foreground, and background fields. Dimensions and font size are pixels; colors are RGB integer tuples. Raster output defaults to Pillow’s bundled font. Pass a local TrueType/OpenType font path for your preferred monospace font. SVG uses the viewer’s monospace font and does not embed that file.

Animation

export_animation accepts named effects, owned Animation instances, or an effect_factory just like capture. ExportFormat completes "gif" and "mp4"; omitting format infers it from the filename suffix.

from pathlib import Path
from tempfile import TemporaryDirectory
from importlib.util import find_spec
from bruhanimate import export_animation
from bruhanimate.demos.routing_demo import build_scene

if find_spec("PIL") and find_spec("imageio_ffmpeg"):
    with TemporaryDirectory() as directory:
        export_animation(Path(directory) / "route.mp4",
                         effect_factory=build_scene, width=50, height=15,
                         duration=1, fps=20, updates_per_second=30)

For a persistent clip, pass your own path (for example "route.mp4"). The parent directory must exist. Existing files raise FileExistsError unless overwrite=True. Files are published only after rendering and encoding finish; failures remove temporary output and preserve any previous destination.

Timing, ownership, and limits

  • Supply a finite frames or duration bound. Time is virtual; rendering does not sleep. Seeded effects follow Seeded capture and recording reproducibility rules. on_complete="exit" stops when content finishes; "hold" captures until the bound. Zero bounds write no file and start no content or encoder.

  • N frames play for N/FPS seconds. GIF delays round to 10 ms, so FPS must not exceed 100. GIF loops forever. MP4 is silent H.264 video with streamed frames; odd pixel dimensions gain one black padding row/column for codec compatibility.

  • GIF retains RGB frames up to gif_max_pixels (default 20 million pixels across frames). Encoding can use additional memory. Choose MP4 for longer clips, or reduce dimensions/duration. MP4 retains only current rendering state.

  • Dimensions remain fixed. No keyboard or resize events are generated. Resourceful animations still start their own devices/processes. Export owns content and closes it on completion or failure, and closes/reaps its encoder. As in Player, KeyboardInterrupt ends capture normally and can save the frames captured so far.

  • Single-cell glyphs and ANSI colors (16/256/RGB), bold, dim, underline, and inverse are supported. Other SGR attributes, wide/combining glyphs, and cursor escapes raise errors. Terminal theme colors are not read: exports use the xterm palette and explicit defaults. Raster glyphs are clipped to their cells.

API

SVG snapshots and optional raster/video export through headless capture.

class bruhanimate.export.ExportStyle(cell_size: tuple[int, int] = (10, 20), font_size: int = 16, font: str | PathLike[str] | None = None, foreground: tuple[int, int, int] = (229, 229, 229), background: tuple[int, int, int] = (12, 12, 12))[source]

Pixel cell size, font and default RGB colors for exported terminal cells.

Raster exports use Pillow’s bundled font unless font names a local TrueType/ OpenType file. Each glyph is centered and clipped to its own cell; font_size is positive pixels. SVG uses the viewer’s monospace font (no font embedding). Terminal theme colors are not read; ANSI palette colors use the xterm palette.

cell_size: tuple[int, int] = (10, 20)
font_size: int = 16
font: str | PathLike[str] | None = None
foreground: tuple[int, int, int] = (229, 229, 229)
background: tuple[int, int, int] = (12, 12, 12)
__init__(cell_size: tuple[int, int] = (10, 20), font_size: int = 16, font: str | PathLike[str] | None = None, foreground: tuple[int, int, int] = (229, 229, 229), background: tuple[int, int, int] = (12, 12, 12)) → None
bruhanimate.export.save_svg(frame: Frame, path: str | PathLike[str], *, style: ExportStyle = ExportStyle(cell_size=(10, 20), font_size=16, font=None, foreground=(229, 229, 229), background=(12, 12, 12)), overwrite: bool = False) → Path[source]

Save an immutable Frame as SVG using only the standard library.

Colors, bold, dim, underline and inverse SGR are supported; other attributes raise ValueError. Single-cell glyph rules match GridViewport. A complete file is published atomically; existing paths require overwrite=True. The parent directory must exist. No animation, input, terminal or resources are acquired beyond the output file. Returns the destination Path.

bruhanimate.export.save_png(frame: Frame, path: str | PathLike[str], *, style: ExportStyle = ExportStyle(cell_size=(10, 20), font_size=16, font=None, foreground=(229, 229, 229), background=(12, 12, 12)), overwrite: bool = False) → Path[source]

Save one Frame as PNG; requires the optional export extra.

See save_svg for supported SGR, validation and atomic/no-overwrite behavior. Font loading happens only during this call, never on package import.

class bruhanimate.export._MP4(path: Path, size: tuple[int, int], fps: float)[source]

One owned encoder process with bounded cleanup and checked exit status.

__init__(path: Path, size: tuple[int, int], fps: float)[source]
write(data: bytes) → None[source]
close(*, abort: bool = False) → None[source]
bruhanimate.export.export_animation(path: str | PathLike[str], effect: Literal['static', 'offset', 'noise', 'stars', 'plasma', 'gol', 'rain', 'matrix', 'drawlines', 'snow', 'twinkle', 'audio', 'firework', 'fire', 'julia', 'boids', 'sand', 'diffusion', 'water', 'smoke', 'metaballs', 'cloth', 'terrain', 'automaton', 'voronoi', 'perlin'] | str | Animation | None = None, *, effect_factory: Callable[[Buffer], Animation] | None = None, format: Literal['gif', 'mp4'] | None = None, style: ExportStyle = ExportStyle(cell_size=(10, 20), font_size=16, font=None, foreground=(229, 229, 229), background=(12, 12, 12)), width: int = 80, height: int = 24, fps: float = 30, updates_per_second: float | None = None, frames: int | None = None, duration: float | None = None, on_complete: Literal['hold', 'exit'] = 'exit', background: str = ' ', settings: object | None = None, preset: str | None = None, seed: int | None = None, overwrite: bool = False, gif_max_pixels: int = 20000000) → int[source]

Render owned content to GIF or H.264 MP4 without opening a terminal.

Requires the optional export extra. format defaults to the .gif/.mp4 suffix. Timing, finite frames/duration limits and ownership follow capture. Output contains no audio/input; N frames play for N/fps seconds. GIF rounds frame durations to 10ms and loops forever (fps must be <=100); MP4 streams frames to bundled FFmpeg, padding odd pixel dimensions by one black pixel for codec compatibility.

GIF retains frames, bounded by gif_max_pixels across all RGB images; use MP4 for longer clips. Zero frames writes no file and starts no content/encoder. Complete output is atomically published; failures clean temporary output and close owned content/encoder. Existing paths require overwrite=True. Parent directories must exist. Returned value is the number of captured frames.