
A Typst WASM plugin for slicing a spritesheet into its individual sprites — give it an image and a grid, get back ready-to-place sprites.
What it does
spryst takes the raw bytes of a spritesheet (PNG, JPEG, GIF, or WebP) and cuts it into a
grid of sprites, each returned as a PNG. You describe the grid in one of two ways and the
plugin works out the rest, honouring any border (margin) and inter-tile gap (spacing).
- Grid mode — give
rowsandcols; the tile size is derived and must divide the usable area evenly. - Size mode — give
tile-widthandtile-height; the row/column counts are derived as the number of whole tiles that fit.
Usage
The easiest entry point is the spryst.typ wrapper:
#import "spryst.typ"
#let data = read("spritesheet.png", encoding: none)
// Inspect the sheet without decoding any sprites.
#let nfo = spryst.sheet-info(data, rows: 4, cols: 4)
// => (sheet_width, sheet_height, rows, cols, tile_width, tile_height, count)
// Pull out a single sprite, by index (row-major) or by (row, col).
#spryst.sprite-image(spryst.sprite(data, index: 5, rows: 4, cols: 4), width: 32pt)
#spryst.sprite-image(spryst.sprite(data, row: 1, col: 1, rows: 4, cols: 4))
// Or slice the whole sheet and lay every sprite out.
#let sheet = spryst.spritesheet(data, rows: 4, cols: 4)
#grid(
columns: sheet.cols,
..sheet.sprites.map(spr => spryst.sprite-image(spr, width: 24pt)),
)
// Slice once, then pull sprites by index or (row, col) on demand.
#let get-sprite = spryst.make-getter(sheet)
#get-sprite(5, width: 32pt)
#get-sprite(1, 2, width: 32pt)
Margin and spacing
Both are optional and default to 0. Pass a single number to apply it to both axes, or a
(x, y) array for per-axis control. A margin is the border between the sheet edge and
the outermost tiles; spacing is the gap between adjacent tiles.
#spryst.spritesheet(data, rows: 4, cols: 4, margin: 1, spacing: (2, 2))
Size mode
#spryst.spritesheet(data, tile-width: 16, tile-height: 16)
Plugin functions (low level)
Each function takes the sheet bytes plus CBOR-encoded arguments and returns a CBOR-encoded
response. Errors are returned as Err and surfaced by Typst as diagnostics. The
high-level spritesheet wrapper above is a thin convenience layer over split.
| Function | Arguments | Returns |
|---|---|---|
split(sheet, spec) |
sheet bytes, CBOR SliceSpec |
{ rows, cols, tile_width, tile_height, sprites: [...] } |
sprite(sheet, spec, selector) |
sheet bytes, CBOR SliceSpec, CBOR Selector |
one sprite dict |
info(sheet, spec) |
sheet bytes, CBOR SliceSpec |
{ sheet_width, sheet_height, rows, cols, tile_width, tile_height, count } |
A sprite dict is { row, col, x, y, width, height, png }, where png is a CBOR byte
string (decoded directly to Typst bytes).
SliceSpec fields: rows, cols, tile_width, tile_height (provide one pair), plus
margin_x, margin_y, spacing_x, spacing_y (default 0). Selector fields:
index, or both row and col.
Building
just install # one-time: wasm targets + wasi-stub
just build # builds typst/wasm/spryst.wasm
The Rust logic is unit-tested on the host:
cargo test
The plugin is also tested end-to-end through Typst with
tytanic. Each test renders an alphanumeric
spritesheet, slices it back apart with spryst, re-lays the sprites into a grid, and
checks the result is pixel-identical to the original — so a slicing or coordinate-maths
regression fails the build.
just test-typst # run every case (regenerates fixtures if missing)
just test-typst reassemble/c8-plain # one test
just test-typst -e 'glob:"*sep*"' # a test-set expression
The PNG fixtures under typst/tests/fixtures/ are committed. Regenerate them with just gen-fixtures after changing the cases, the glyph set, or the PPI (PPI in
typst/tests/lib.typ, mirrored by default.ppi in typst.toml). Both require the
Buenard font.
Acknowledgement of AI usage
The first revision of this project was developed by me, without any use of AI. When I was happy with the preliminary results, Claude Opus 4.8 was used to aid in several tasks.
First, it improved some Rust and Typst code (I am not particularly familiar with WASM). This was the lesser use of the tool.
Second, Claude largely contributed to writing the tests. I wrote the initial code to create the test assets, and then Claude was used to generate the Tytanic suite of tests.
Lastly, documentation (e.g., Typst docstrings), including parts of this README, were written by Claude. Generally, I find Claude capable at this task, so I allowed it to do so.
One main issue I had was Claude’s use of naming and convention. At times, some things were oddly named (especially regarding tests). I have manually revised and corrected anything out of place, but please feel free to open an issue if you find anything that I missed.