A minimal, stateless meme generator HTTP API in pure Rust.
Every meme is described entirely by its URL - there is no database, no cache server, and nothing to log in to. Backgrounds and caption geometry come from a directory of template folders, and images are rendered on demand, with an optional local cache for repeated template renders. It is a deliberately small reimplementation of jacebrowning/memegen: three endpoint groups, embedded fonts, no SaaS plumbing - around 2,700 lines, tests included, across four source files.
- Background
- Install
- Usage
- Templates
- Architecture
- Scope
- API
- Maintainers
- Acknowledgements
- Contributing
- License
The original memegen by Jace Browning is an excellent, long-running meme API: stateless, URL-as-state, with a large template corpus. It is written in Python (Sanic + Pillow) and carries a fair amount of hosted-service machinery - authentication, remote tracking, search, error reporting.
memegen-rs keeps the parts that make memegen elegant (the statelessness, the URL scheme, the config.yml + default.* template format) and drops everything that only exists to run it as a public SaaS. The result is a single self-contained binary with a tiny dependency surface, intended for self-hosting. It reads the same template layout as upstream, so an existing corpus drops in unchanged.
Requires Rust 1.95+ (pinned via rust-toolchain.toml).
git clone https://github.com/tenequm/memegen-rs.git
cd memegen-rs
cargo build --releaseThe binary is then at target/release/memegen-rs. The template corpus ships in templates/ (see Templates), so it runs out of the box.
cargo run # serves on http://0.0.0.0:5005
# or the release binary:
./target/release/memegen-rsEnvironment variables:
| Variable | Default | Purpose |
|---|---|---|
PORT |
5005 |
Listen port |
MEMEGEN_TEMPLATES_DIR |
templates |
Path to the template corpus |
MEMEGEN_CACHE_DIR |
(unset) | Directory for the render cache; unset means no cache |
MEMEGEN_CACHE_MAX_BYTES |
10737418240 |
Most disk the cache directory may use (10 GiB); the oldest renders are evicted when it is full |
MEMEGEN_CACHE_MEMORY_BYTES |
16777216 |
Most memory the cache keeps hot renders in (16 MiB) |
MEMEGEN_WATERMARK |
(unset) | Brand label drawn bottom-left on rendered images; unset means no watermark |
MEMEGEN_HEAD_HTML |
(unset) | HTML appended verbatim to the <head> of every page (gallery, builder, /docs), e.g. an analytics tag. Trusted operator config, not escaped; unset or empty leaves pages unchanged |
Render a meme:
# captioned: lines are /-separated, space is _, blank line is _
curl 'http://localhost:5005/images/fry/not_sure_if/this_works.png' -o meme.png
# any image as a background
curl 'http://localhost:5005/images/custom/top/bottom.png?background=https://picsum.photos/600/400' -o custom.png
# sized (padded with a blurred letterbox) and recolored
curl 'http://localhost:5005/images/ds/push_button/cant_decide.jpg?width=800&height=600&color=yellow' -o ds.jpgRender cache: with MEMEGEN_CACHE_DIR set, a successful template render (/images/{id}.{ext}, /images/{id}/{lines}.{ext}) is stored under its exact path and query string and served from there afterwards; identical concurrent requests render once. Those responses carry x-memegen-cache: hit or miss. /images/custom/... and error responses are never cached. The cache starts empty on every process start, so it belongs on disposable storage that only the server can write to. Keep it on a real disk - on tmpfs every cached byte is memory - and keep MEMEGEN_CACHE_MAX_BYTES below what the volume holds: startup does not check free space.
A template is a folder named by its ID:
templates/<id>/
config.yml # name, source, keywords, text-box geometry, example
default.png # background (png/jpg/webp/gif; first frame used for gif)
config.yml uses the same schema as upstream memegen, so any memegen-compatible corpus works. Text-box coordinates are fractions of the image (0.0-1.0). Alternate background variants (extra image files beside default.*) become selectable via ?style=<name>.
This repository ships a corpus of ~700 templates under templates/, read once at startup into an immutable in-memory registry. Add or replace templates by dropping folders in, or point MEMEGEN_TEMPLATES_DIR at a different directory. See License for the licensing posture on the bundled images.
- axum for HTTP routing and utoipa for the OpenAPI spec, rendered as interactive docs by Scalar at
/docs. maud renders the web UI (a searchable template gallery and a meme builder) at compile time. - image + imageproc + ab_glyph for rendering. A caption is autosized to its box, word-wrapped, drawn with a white fill and a black outline, and composited onto the background.
- serde-saphyr (pure-Rust YAML) parses each
config.yml. - foyer backs the optional render cache: a small memory tier over a disk tier of fixed-size block files it only ever writes inside, so the cache never grows past
MEMEGEN_CACHE_MAX_BYTES. - Fonts (Anton for the Impact look, Pangolin for handwriting, Manrope for the watermark; all SIL OFL 1.1) are embedded in the binary via
include_bytes!- no font directory, and it works on a fonts-less container. Anton and Pangolin cover Latin + full Cyrillic/Ukrainian; the Anton build is the Cyrillic-extended v2.300 fork from Tural/AntonFont (pending upstream as google/fonts#7552).
Four source files: template.rs (model, registry, URL codec, styling), render.rs (the rendering pipeline), main.rs (router, handlers, OpenAPI, error mapping), cache.rs (the optional render cache, a middleware on the template image routes).
The server has no rate limiter, and its render cache is off unless MEMEGEN_CACHE_DIR is set: without it every /images/ request renders on demand, and with it every new URL and every /images/custom/... request still does. At most one render per CPU core runs at a time and the rest wait, none are refused, so cap its CPU and memory (or put your own limiter in front) before exposing it publicly. Image and asset responses carry Cache-Control: public, max-age=86400 and CDN-Cache-Control: public, max-age=31536000, immutable, so a CDN that honors the latter keeps a render for a year - purge it when templates or rendering change. The server also fetches any URL passed as ?background= with no filtering of its own, so restrict its outbound network to the public internet if it can reach anything private.
The server also ships as a container image, ghcr.io/tenequm/memegen-rs (linux/amd64, template corpus baked in, listening on 5005). .github/workflows/image.yml builds ops/docker/Containerfile on every push to main and pushes it as sha-<full commit SHA> and latest; a v* tag makes .github/workflows/release.yml push the versioned image. CI builds images only; it deploys nothing. The cache directory is the only path the server writes to, so nothing else in the container needs to be writable.
A public instance runs at memegen.rs.
Image output in png, jpg, webp, and gif - animated when the template's source is an animated GIF, otherwise a single frame. Per-template font selection (Anton plus a Pangolin handwriting face) and text rotation are honored.
Still out of scope, layerable without restructuring:
- Animated WebP / MP4 output
- Overlay-image compositing
- Color emoji and the full upstream font set
Templates whose only background is an undecodable video (default.mp4 with no static still) are listed in the API but return 422 on render.
| Method | Path | Description |
|---|---|---|
GET |
/templates |
List all templates (JSON) |
GET |
/templates/{id} |
One template (JSON) |
GET |
/images/{id}.{ext} |
Blank template background |
GET |
/images/{id}/{lines}.{ext} |
Captioned meme |
GET |
/images/custom/{lines}.{ext}?background=<url> |
Caption any image by URL |
GET |
/openapi.json |
OpenAPI 3.1 spec |
GET |
/ |
Web UI: searchable template gallery |
GET |
/edit/{id} |
Web UI: meme builder (live preview, copy link/image) |
GET |
/docs |
Interactive API docs (Scalar) |
GET |
/SKILL.md, /llms.txt |
Agent usage docs (case-insensitive) |
Path encoding for {lines}: lines are separated by /; a space is _; a literal underscore is __; a blank line is _.
Query parameters for image endpoints:
| Param | Effect |
|---|---|
style |
Alternate background variant |
layout=top |
Place all captions at the top |
width, height |
Pad to size with a blurred letterbox |
color |
Text fill color (name or hex) |
Limits, so that one request cannot ask for an unbounded amount of memory. Going over one of the first two is a 422 with a JSON {"error": "..."} body:
widthandheightare at most 2048 each. A larger size is refused, never rendered smaller than asked.- A custom
backgroundis a PNG, JPEG, GIF or WebP of at most 10 MiB and 4096x4096 pixels. Only the first frame of an animated one is used. - A custom
backgroundover 2048 pixels on a side is scaled down to fit 2048x2048 before it is captioned, so a 4032x3024 photo comes back as 2048x1536. layout=topdraws the first 32 caption lines and drops the rest, the same way lines beyond a template's text boxes are dropped.
The full machine-readable contract is served at /openapi.json.
This project is a reimplementation of memegen by Jace Browning (memegen.link). The URL scheme, the template format, and the overall API shape are his design; this repository simply rebuilds a minimal subset of it in Rust. All credit for the original idea and the template ecosystem goes to him and the memegen contributors.
Also thanks to the Anton (Cyrillic extension by Tural/AntonFont), Pangolin, and Manrope typefaces (SIL OFL 1.1) and the maintainers of axum, image, imageproc, and ab_glyph.
Issues and pull requests are welcome - open an issue for bugs or ideas. Before submitting a PR, please run cargo fmt, cargo clippy, and cargo test.
Code, build scripts, and template config.yml markup are MIT (c) 2026 Misha Kolesnik. The embedded Anton, Pangolin, and Manrope fonts are SIL OFL 1.1.
The MIT license does not extend to the template background images and clips shipped under templates/. These are well-known internet meme formats whose underlying photos, film stills, and artwork are owned by their respective copyright holders. They are included in good faith for the same nominative/transformative use that meme-generation tools rely on (the same legal posture as memegen.link and the upstream memegen image), and no claim of ownership is made over them.
If you are a rights holder and want a template removed, email misha@kolesnik.io with the template name (its folder under templates/) and proof of rights. Removal requests are honored promptly - typically within a few days.
For zero redistribution exposure, remove templates/ and rely on the ?background=<url> custom-background endpoint instead.
