go-pixelart

August 5, 2026 ยท View on GitHub

Test & Heroku Deployment Test & Docker Build+Push

Transform picture into pixel art

:medal_military: Awesome tiny project getting fourth place :four: at GoHack Hackathon 2022 (1011 participants)

The picture is cut into square blocks - the slices are counted along the short or the long edge - every block is replaced by the average color of the pixels it covers, and that color is mapped to a retro palette (CGA, EGA, VGA...) or to a grayscale/contrast filter.

Two ways to use it: an HTTP API (with a small web page) and a CLI.


Quick start

Run the image from the GitHub Container Registry

docker run -it --rm -p 8080:8080 ghcr.io/fairhive-labs/pixelart

Build and run it locally

docker build -t fairhivelabs/pixelart . && docker run -it --rm -p 8080:8080 fairhivelabs/pixelart
# or
docker compose up --build
# or, without docker
make run

Then visit http://localhost:8080 :rocket: and pixelize your pictures ;)


HTTP API

MethodPathDescription
GET/web page to upload and pixelize a picture
POST/pixelizetransform a picture, returns HTML or JSON
GET/healthliveness probe, returns ok
GET/favicon.icofavicon, cached and revalidated with an ETag

POST /pixelize

multipart/form-data, every field is required:

FieldTypeValuesDescription
filefilePNG or JPEGthe picture to transform
slicesinteger1 to 1000, and not more than the smallest side of the picturenumber of blocks cut along the selected edge
edgestringshort, longedge the slices are counted on
filterstringcga2, cga4, cga16, ega, vga, identity, dark-contrast, dark-gray, gray, invert, light-gray, xraycolor palette applied to each block
QueryValuesDescription
mimehtml (default), jsonresponse format

The pixel art is returned in the same format as the picture that was uploaded (a PNG in, a PNG out).

HTML response

Returns 201 Created and a page displaying the pixel art, which can be rotated and resized:

curl -X POST "http://localhost:8080/pixelize" \
  -F "file=@pictures/wfvr.png" \
  -F "edge=short" \
  -F "slices=100" \
  -F "filter=ega" \
  --output pixelart.html

JSON response

Returns 201 Created and the picture, base64 encoded:

curl -s -X POST "http://localhost:8080/pixelize?mime=json" \
  -F "file=@pictures/wfvr.png" \
  -F "edge=short" \
  -F "slices=100" \
  -F "filter=ega" | jq -r .data | base64 -d > pixelart.png
{
  "data": "iVBORw0KGgoAAAANSUhEUgAAB+QAAAp4CAIAAADg...",
  "encoding": "base64",
  "filter": "ega",
  "length": 36192
}

Status codes

CodeWhen
201 Createdthe pixel art was produced
400 Bad Requesta field is missing or invalid, or slices is larger than the picture
413 Content Too Largethe upload is over MAX_UPLOAD_BYTES, or the picture declares more than MAX_PIXELS
415 Unsupported Media Typethe file is not a PNG or a JPEG, or cannot be decoded
503 Service UnavailableMAX_CONCURRENT_JOBS transformations are already running, retry later (Retry-After)

The API is public and CORS is open to any origin (POST, OPTIONS), without credentials.


Filters

Applied to the average color of each block. pictures/wfvr.png is the source of every sample below.

FilterDescriptionSample
cga22 colors CGA palette, black and whitewfvr_cga2.png
cga44 colors CGA palettewfvr_cga4.png
cga1616 colors CGA palettewfvr_cga16.png
ega64 colors EGA palettewfvr_cga64.png
vga18 bits VGA color, 6 bits per channel-
identitytrue color, no palettewfvr_pixel80_shortedge_truecolor.png
graygrayscale, average of the channels-
dark-graygrayscale using the darkest channelwfvr_dark_gray.png
light-graygrayscale using the brightest channel-
dark-contrastdarkens the two brightest channelswfvr_dark_contrast.png
invertinverted colors-
xrayinverted, then brightest channel-

Transparent pixels stay transparent with the palette filters.


CLI

make cli          # builds ./bin/pixelart
./bin/pixelart -slices 80 -edge long -filter cga16 pictures/wfvr.png
# or without building
go run ./cmd -slices 80 -edge long -filter cga16 pictures/wfvr.png
FlagDefaultDescription
-slices100number of slices cut along the selected edge
-edgeshortshort or long
-filtercga4any filter of the table above
-out<picture>_<timestamp>.<ext>output file
-quality75JPEG output quality, from 1 to 100
๐Ÿ‘‰ Source file "pictures/wfvr.png" opened
๐Ÿค– Image DECODED - Format is "png"
๐Ÿ–ผ  Original Dimension = [ 2010 x 2679 ]
๐Ÿ‘พ Processing Transformation...
โœ… Transformation is over
๐Ÿ’พ Pixel Art saved in file "pictures/wfvr_20260804-233956.png"

Exit codes: 0 on success, 2 on a usage error, 1 when the transformation fails.


Configuration

Every setting is an environment variable read at startup:

VariableDefaultDescription
PORT8080port the server listens on
MAX_UPLOAD_BYTES10485760 (10 MiB)largest accepted request body
MAX_PIXELS40000000 (40 MP)largest accepted picture, checked before decoding
MAX_CONCURRENT_JOBSnumber of CPUstransformations running at the same time, further requests get a 503
GIN_MODEreleaseset to debug for gin's verbose mode

The dimensions are read from the picture header before it is decoded, so an oversized picture is rejected without allocating memory for it.


Development

make build   # builds the API into ./bin/api
make run     # builds and runs it on :8080
make cli     # builds the CLI into ./bin/pixelart
make test    # go test ./...
make race    # go test -race ./...
make bench   # filter benchmarks
make vet     # go vet ./...

Go 1.25 or later is required (see go.mod).

Layout:

PathContent
api/HTTP server, embedded templates and assets
cmd/command line tool
internal/filter/pixel, basic and convolution filters, CGA/EGA/VGA palettes
internal/colorutils/color helpers
pictures/sample pictures

The filters are parallelized over the available CPUs; internal/filter/equivalence_test.go keeps the original implementations as an oracle and checks the output pixel per pixel, so an optimization that changes the result fails the build.


Deployment

Docker

The image builds a static binary and runs it as an unprivileged user on a pinned alpine, with a health check on /health:

docker build -t fairhivelabs/pixelart .
docker run -it --rm -p 8080:8080 -e MAX_UPLOAD_BYTES=20971520 fairhivelabs/pixelart

GitHub Container Registry

Every push on main publishes ghcr.io/fairhive-labs/pixelart (see .github/workflows/docker.yml).

Heroku

.github/workflows/heroku.yml deploys the Docker image on every push on main. The deployment is skipped when the HEROKU_API_KEY secret is missing. Set the HEROKU_APP_NAME and HEROKU_EMAIL repository variables to target another app - the health check URL follows the app name.


License

MIT