snatch is a plugin-driven bitmap processing pipeline for retro/pixel workflows.
It is still great for bitmap fonts, but the architecture is now more generic:
- extract bitmap data from a source
- transform that data (optional)
- export in a target format
snatch runs in three stages:
- Extractor plugin
- Reads the input source (
ttf,image, etc.) - Produces a
snatch_fontbitmap representation
- Transformer plugin (optional)
- Reads
snatch_font - Can annotate or replace data through
font->user_data
- Exporter plugin
- Reads
snatch_fontand optional transformed data - Writes the final artifact (
.png,.s,.bin,.c, ...)
Pipeline examples:
TTF -> ttf_extractor -> tiny_font_transformer -> sdcc_asm_tiny_font_exporter -> .s
image -> image_extractor -> (no transform) -> raw_bin_exporter -> .bin
TTF -> ttf_extractor -> tiny_font_transformer -> raw_bin_exporter -> tiny_font_bin_extractor -> tiny_bmp_transformer -> png_exporter
image -> image_passthrough_extractor -> dither_1bpp_transformer -> gem_img_transformer -> gem_img_exporter -> .img
image/ttf -> ... -> gem_font_transformer -> gem_fnt_exporter -> .fnt
image -> ... -> gem_icn_transformer -> gem_icn_exporter -> .icn
This is why the new model is more flexible: with appropriate plugins, you can run non-font flows too (for example: extract one glyph/region from color image -> dither transform -> 1bpp image export).
- Extract bitmap font glyphs from image sheets (
image_extractor) - Rasterize TTF fonts to 1bpp glyph bitmaps (
ttf_extractor) - Read GEM/GDOS bitmap fonts, GEM raster images, and GEM icon sets (
gem_fnt_extractor,gem_img_extractor,gem_icn_extractor) - Run optional transformers before export
- Export to PNG grid, Partner SDCC ASM (tiny or bitmap), raw binary, raw C array, and GEM
.FNT/.IMG/.ICN - Control ASCII range, colors, margins/padding, font size, fixed/proportional mode
Practical GEM status today:
- GEM
.FNTexport and extract work on repo-generated files and the sample fonts undertest/data/gem - GEM
.IMGexport and extract support round-tripping monochrome images - GEM
.ICNexport and extract support the standard binary icon-set container used by the fixtures undertest/data/icn
Detailed format references:
- Tiny binary/font layout:
docs/tiny-bin.md - Tiny replay/raster semantics:
docs/tiny-raster.md - Bitmap assembly/font layout:
docs/bitmap-asm.md - GEM/GDOS bitmap font layout:
docs/gem-fnt.md - GEM raster image layout:
docs/gem-img.md - GEM icon-set layout:
docs/gem-icn.md
Quick header reference (font_t, both tiny and bitmap streams):
byte 0: flags
byte 1: first_ascii
byte 2: last_ascii
byte 3: empty_width
byte 4: max_glyph_width
byte 5: glyph_height
byte 6: advance
byte 7: descent
Quick tiny move-byte reference:
bit: 7 6 5 4 3 2 1 0
c0 |dx| |dy| sy sx c1
color = c0 | (c1 << 1)
dx = sx ? -|dx| : |dx| (|dx| in 0..3)
dy = sy ? -|dy| : |dy| (|dy| in 0..3)
git clone https://github.com/retro-vault/snatch.git
cd snatch
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build -j$(nproc)ctest --test-dir build --output-on-failure./bin/snatch \
--plugin-dir ./bin/plugins \
--extractor-parameters "input=fonts/Retro.ttf,first_ascii=32,last_ascii=126,font_size=16" \
--exporter png_exporter \
--exporter-parameters "output=out/font-grid.png,columns=16,rows=6"./bin/snatch \
--plugin-dir ./bin/plugins \
--extractor-parameters "input=assets/fontsheet.png,margins_left=2,margins_top=2,margins_right=2,margins_bottom=2,padding_left=1,padding_top=1,padding_right=1,padding_bottom=1,columns=16,rows=6,first_ascii=32,last_ascii=126,fore_color=#000000,back_color=#FFFFFF" \
--exporter raw_bin_exporter \
--exporter-parameters "output=out/font.bin"./bin/snatch \
--plugin-dir ./bin/plugins \
--extractor-parameters "input=fonts/Retro.ttf,first_ascii=32,last_ascii=127,font_size=16" \
--transformer tiny_font_transformer \
--exporter sdcc_asm_tiny_font_exporter \
--exporter-parameters "output=out/my_font.s,module=my_font,symbol=my_font,proportional=true,empty_width=3,advance=2"./bin/snatch \
--plugin-dir ./bin/plugins \
--extractor-parameters "input=fonts/Retro.ttf,first_ascii=32,last_ascii=127,font_size=16" \
--transformer bitmap_font_transformer \
--transformer-parameters "font_mode=proportional,empty_width=3,advance=2" \
--exporter raw_c_exporter \
--exporter-parameters "output=out/font_raw.c,bytes_per_line=8,symbol=font_data"./bin/snatch \
--plugin-dir ./bin/plugins \
--extractor-parameters "input=test/data/ttfs/pixChicago.ttf,first_ascii=32,last_ascii=127,font_size=16,font_mode=proportional" \
--transformer gem_font_transformer \
--transformer-parameters "face_id=2,face_name=pixChicago Prop,point_size=16,empty_width=4" \
--exporter gem_fnt_exporter \
--exporter-parameters "output=out/pixChicago_32_127_proportional.fnt"./bin/snatch \
--plugin-dir ./bin/plugins \
--extractor gem_fnt_extractor \
--extractor-parameters "input=test/data/gem/fntgr08.fnt" \
--exporter png_exporter \
--exporter-parameters "output=out/fntgr08.png,columns=16,rows=16,padding=0,grid_thickness=0"./bin/snatch \
--plugin-dir ./bin/plugins \
--extractor image_passthrough_extractor \
--extractor-parameters "input=test/data/images/tut2.png" \
--transformer dither_1bpp_transformer \
--transformer-parameters "threshold=128" \
--exporter png_exporter \
--exporter-parameters "output=out/tut2_dithered.png,columns=1,rows=1,padding=0,grid_thickness=0"
./bin/snatch \
--plugin-dir ./bin/plugins \
--extractor image_extractor \
--extractor-parameters "input=out/tut2_dithered.png,columns=1,rows=1,first_ascii=0,last_ascii=0" \
--transformer gem_img_transformer \
--exporter gem_img_exporter \
--exporter-parameters "output=out/tut2_dithered_gem.img"./bin/snatch \
--plugin-dir ./bin/plugins \
--extractor gem_img_extractor \
--extractor-parameters "input=out/tut2_dithered_gem.img" \
--exporter png_exporter \
--exporter-parameters "output=out/tut2_roundtrip_from_gem.png,columns=1,rows=1,padding=0,grid_thickness=0"
./bin/snatch \
--plugin-dir ./bin/plugins \
--extractor gem_icn_extractor \
--extractor-parameters "input=test/data/icn/ALERT1.ICN,plane=composite" \
--exporter png_exporter \
--exporter-parameters "output=out/ALERT1.png,columns=9,rows=8,padding=1,grid_thickness=1"| Name | Input | Purpose |
|---|---|---|
ttf_extractor |
ttf |
Rasterize TTF glyphs into 1bpp bitmap glyphs |
image_extractor |
image |
Extract glyph bitmaps from grid image sheets |
image_passthrough_extractor |
image |
Load full image as grayscale passthrough payload in user_data |
tiny_font_bin_extractor |
bin |
Load Partner Tiny binary stream into user_data for raster decoding |
gem_img_extractor |
img |
Load GEM .IMG raster image into a bitmap glyph |
gem_icn_extractor |
icn |
Load GEM .ICN icon set into bitmap glyphs |
gem_fnt_extractor |
fnt |
Load GEM .FNT bitmap font into bitmap glyphs |
| Name | Purpose | Notes |
|---|---|---|
tiny_font_transformer |
Vectorize bitmap glyphs into Partner Tiny move streams | Segment-coverage optimizer (optimize=true) + optional mask pass (include_mask=true) |
tiny_bmp_transformer |
Interpret Partner Tiny moves and rebuild bitmap glyphs | Intended for tiny_font_bin_extractor + png_exporter |
bitmap_font_transformer |
Serialize bitmap font to Partner bitmap byte stream | Intended for sdcc_asm_bitmap_font_exporter, raw_bin_exporter, raw_c_exporter |
fzx_transformer |
Compute ZX Spectrum FZX-style glyph metadata | Stores metadata in font->user_data |
gem_font_transformer |
Serialize bitmap glyphs into a GEM/GDOS .FNT byte stream |
Intended for gem_fnt_exporter or gem_fnt_c_exporter |
gem_img_transformer |
Serialize the first bitmap glyph into a GEM .IMG byte stream |
Intended for gem_img_exporter or gem_img_c_exporter |
gem_icn_transformer |
Serialize the first bitmap glyph into a GEM .ICN icon-set byte stream |
Intended for gem_icn_exporter or gem_icn_c_exporter |
dither_1bpp_transformer |
Dither grayscale passthrough image to 1bpp bitmap glyph | Intended for image_passthrough_extractor + png_exporter |
| Name | Format | Standard | Purpose |
|---|---|---|---|
png_exporter |
png |
snatch-grid |
Render bitmap font as PNG grid |
sdcc_asm_tiny_font_exporter |
asm |
sdcc-asm-tiny-font |
SDCC assembly export for Partner tiny format |
sdcc_asm_bitmap_font_exporter |
asm |
sdcc-asm-bitmap-font |
SDCC assembly export for Partner bitmap format |
gem_fnt_exporter |
fnt |
gem-fnt |
Binary GEM/GDOS bitmap font export |
gem_fnt_c_exporter |
c |
gem-fnt |
GEM/GDOS bitmap font stream as const uint8_t[] |
gem_img_exporter |
img |
gem-img |
Binary GEM raster image export |
gem_img_c_exporter |
c |
gem-img |
GEM raster image stream as const uint8_t[] |
gem_icn_exporter |
icn |
gem-icn |
Binary GEM icon-set export |
gem_icn_c_exporter |
c |
gem-icn |
GEM icon-set stream as const uint8_t[] |
raw_bin_exporter |
bin |
raw-1bpp |
Raw continuous byte stream (or Partner Tiny stream when input is tiny_font_transformer) |
raw_c_exporter |
c |
raw-1bpp |
Raw byte stream as const uint8_t[] |
dummy_exporter |
txt |
debug-dump |
Diagnostic exporter |
| Option | Alias | Description |
|---|---|---|
--extractor |
-q |
Optional extractor plugin override |
--extractor-parameters |
-v |
Extractor params (k=v,...) |
--plugin-dir |
-d |
Plugin search directory override |
--transformer |
-w |
Optional transformer plugin name |
--transformer-parameters |
-y |
Transformer params (k=v,...) |
--exporter |
-e |
Exporter plugin name |
--exporter-parameters |
-x |
Exporter params (k=v,...) |
Stage-specific tuning should be passed to the owning plugin:
- extractor options via
--extractor-parameters - transformer options via
--transformer-parameters - exporter options via
--exporter-parameters
If --extractor is omitted, snatch infers it from input extension:
.ttf,.otf->ttf_extractor- common image extensions (
.png,.jpg,.jpeg, ...) ->image_extractor
Required ownership split:
- extractor owns input path: set
input=...in--extractor-parameters - exporter owns output path: set
output=...in--exporter-parameters - there is no positional input argument and no root
--outputoption anymore
Use concrete exporter names directly (no separate format parameter):
sdcc_asm_tiny_font_exportersdcc_asm_bitmap_font_exporterraw_c_exporterraw_bin_exporterpng_exporterdummy_exporter
Backward-compatible aliases are still accepted (e.g. raw_bin, raw_c, png, dummy, sdcc_asm_tiny_font, sdcc_asm_bitmap_font).
GEM notes:
gem_fnt_extractorauto-detects the big-endian Atari/GEM.FNTsamples undertest/data/gemgem_icn_extractorexpects the binary GEM icon-set container; normalized binary fixtures live undertest/data/icngem_img_transformercurrently emits monochrome GEM images
Concept example (full image passthrough -> dither -> PNG):
./bin/snatch \
--plugin-dir ./bin/plugins \
--extractor image_passthrough_extractor \
--extractor-parameters "input=test/data/font-sheets/tut.png" \
--transformer dither_1bpp_transformer \
--transformer-parameters "threshold=128" \
--exporter png_exporter \
--exporter-parameters "output=out/tut_dither_1bpp.png,columns=1,rows=1,padding=0,grid_thickness=0"Partner Tiny roundtrip (binary -> rasterized grid):
# 1) Create Partner Tiny binary stream.
./bin/snatch \
--plugin-dir ./bin/plugins \
--extractor-parameters "input=fonts/Retro.ttf,first_ascii=65,last_ascii=70,font_size=16" \
--transformer tiny_font_transformer \
--exporter raw_bin_exporter \
--exporter-parameters "output=out/retro_tiny.bin,font_mode=proportional,empty_width=3,advance=1"
# 2) Load tiny stream and rasterize it back to bitmap grid.
./bin/snatch \
--plugin-dir ./bin/plugins \
--extractor tiny_font_bin_extractor \
--extractor-parameters "input=out/retro_tiny.bin" \
--transformer tiny_bmp_transformer \
--exporter png_exporter \
--exporter-parameters "output=out/retro_tiny_roundtrip.png,columns=3,rows=2,padding=1,grid_thickness=1"snatch searches plugin directories in this order and stops at the first one that provides the requested plugins:
--plugin-dir <dir>SNATCH_PLUGIN_DIR${CMAKE_INSTALL_FULL_LIBDIR}/snatch/plugins~/.local/lib/snatch/plugins
Plugins export:
int snatch_plugin_get(const snatch_plugin_info** out);Plugin ABI: include/snatch/plugin.h
snatch_plugin_info.kind:
SNATCH_PLUGIN_KIND_EXTRACTOR->extract_fontSNATCH_PLUGIN_KIND_TRANSFORMER->transform_fontSNATCH_PLUGIN_KIND_EXPORTER->export_font
- Create a plugin folder and CMake file under
plugins/<name>/. - Add it to
plugins/CMakeLists.txtwithadd_subdirectory(<name>). - Implement
snatch_plugin_get(...)and a staticsnatch_plugin_info. - Build and run with
--plugin-dir ./bin/plugins.
Extractor skeleton:
int my_extract(
const char* input_path,
const snatch_kv* options,
unsigned options_count,
snatch_font* out_font,
char* errbuf,
unsigned errbuf_len
);Transformer skeleton:
int my_transform(
snatch_font* font,
const snatch_kv* options,
unsigned options_count,
char* errbuf,
unsigned errbuf_len
);Exporter skeleton:
int my_export(
const snatch_font* font,
const char* output_path,
const snatch_kv* options,
unsigned options_count,
char* errbuf,
unsigned errbuf_len
);Minimal plugin descriptor shape:
const snatch_plugin_info k_info = {
"my_plugin",
"Short description",
"author",
"format-or-input",
"standard-or-profile",
SNATCH_PLUGIN_ABI_VERSION,
SNATCH_PLUGIN_KIND_EXPORTER, // or EXTRACTOR/TRANSFORMER
nullptr, // transform callback if transformer
nullptr, // export callback if exporter
nullptr // extract callback if extractor
};Notes:
- For transformers, use
font->user_datafor stage-to-stage contracts. - For exporters,
format/standardshould be non-empty. - Keep plugin-owned buffers alive for as long as
snatchmay read them.
- FreeType (
freetype): FreeType License (FTL) or GPLv2 - stb (
stb_image,stb_image_write): public domain or MIT - argparse (
cofyc/argparse): MIT - GoogleTest (
googletest): BSD 3-Clause
TTF fonts under test/data/ttfs are freeware and remain copyright of their authors.
GEM sample note:
test/data/gemcontains real GEM.FNTsamples used to verify extractor compatibilitytest/data/icncontains binary GEM.ICNfixtures used by the icon extractor path