Wezen
Dependency-free Ruby encoders for deterministic APNG, GIF, PNG, and asciinema demo media.
Features · Installation · Quick start · Output formats · Development
Wezen turns RGBA frames and terminal events into portable, reproducible demo assets. It is built for documentation, fixtures, and recording pipelines where the same input should produce the same bytes.
The name comes from δ Canis Majoris: its IAU name derives from the Arabic al-wazn, meaning “the weight.”
Features
- Lossless APNG with optional changed-frame rectangles, plus PNG stills
- GIF89a encoding with deterministic median-cut palettes and dithering
- Frame coalescing, scaling, cropping, sampling, blending, and change bounds
- Asciinema v2 JSON Lines output for terminal recordings
- Byte-stable output for identical inputs
- No runtime dependencies beyond Ruby's
zlibandjsonstandard libraries
Installation
Add Wezen to your Gemfile:
gem "wezen"Then install dependencies:
bundle installOr install the gem directly:
gem install wezenWezen requires Ruby 3.1 or newer.
Quick start
Create a two-frame animation and write lossless APNG and compatible GIF versions:
require "wezen"
animation = Wezen::Animation.new(width: 2, height: 1)
animation.add([255, 0, 0, 255, 0, 0, 255, 255].pack("C*"), delay_ms: 100)
animation.add([255, 0, 0, 255, 0, 255, 0, 255].pack("C*"), delay_ms: 100)
Wezen::APNG.write("demo.apng", animation)
Wezen::GIF.write("demo.gif", animation, dither: :bayer)Frames are packed RGBA strings in row-major order. Animation#add merges
identical consecutive frames and adds their delays automatically.
Output formats
| Format | Use case | Behavior |
|---|---|---|
| APNG | Lossless animation | Preserves RGBA pixels and stores changed rectangles when possible |
| GIF89a | Broad compatibility | Uses a deterministic global palette with optional dithering |
| PNG | Still frames | Writes a single lossless RGBA image |
| Asciinema v2 | Terminal sessions | Writes chronological output, input, and resize events as JSON Lines |
APNG
APNG is the preferred animation format when color and alpha fidelity matter:
bytes = Wezen::APNG.encode(animation, compression: 9, diff: true)
File.binwrite("demo.apng", bytes)GIF
Control palette size, palette strategy, dithering, and frame differences:
Wezen::GIF.write(
"demo.gif",
animation,
colors: 128,
palette: :global,
dither: :floyd_steinberg,
diff: true
)Provide a Wezen::Palette when the recording needs a fixed application palette.
Supported dithering modes are :floyd_steinberg, :bayer, and :none.
PNG
Write a single packed RGBA frame:
frame = animation.each.first
Wezen::PNG.write("frame.png", animation.width, animation.height, frame.rgba)Asciinema casts
Create an asciinema v2 recording from chronological terminal events:
events = [
Wezen::Cast::Event.new(0.0, :output, "$ bundle exec rake\r\n"),
Wezen::Cast::Event.new(0.4, :output, "12 examples, 0 failures\r\n")
]
Wezen::Cast.write(
"demo.cast",
width: 80,
height: 24,
title: "Test run",
events: events
)Event kinds are :output, :input, and :resize.
Transform animations
Every transformation returns a new Wezen::Animation:
animation = animation.coalesce(tolerance: 2)
animation = animation.scale(to_width: 640, to_height: 360)
animation = animation.crop([0, 0, 320, 180])
animation = animation.drop_to(fps: 12)Use Wezen::Image directly for lower-level scaling, cropping, alpha blending,
and changed-pixel bounds.
Design decisions
Wezen deliberately ships encoders without runtime decoders. This keeps the gem small, portable, and focused while round-trip tests validate generated media.
The architecture records explain the durable format choices:
Development
bundle install
bundle exec rake
bundle exec rake bench
bundle exec rbs -I sig validate
gem build --strict wezen.gemspecContributing
Bug reports and pull requests are welcome in the GitHub repository.
License
Wezen is available under the MIT License.