ENDESGA/pep

Prediction-Encoded Pixels - a tiny yet powerful single-header pixel art compression method that focuses on size

C

370

53 commits

updated Jun 10, 2026

See the code

README

Prediction-Encoded Pixels

This format is specifically designed to be for low-color pixel art (<=16 colors works best, up to 256 colors is supported).

It uses "Prediction by Partial Matching" compression, which is able to compress packed-palette-indices smaller than GIF, PNG, and QOI, while sacrificing a bit of time. It's 2-10x slower than GIF/PNG (fast)/QOI (depending on the image), but often compresses the image 20-50% smaller than GIF/PNG (and multiple-times smaller than QOI).

If you care about compressed image size, this is for you. It's somewhere between GIF and WEBP (.webp can compress better at times, but it's painfully slow).


( this is currently in the EXPERIMENTAL phase! )


How to use it:

Use the C header like:

#define PEP_IMPLEMENTATION
#include "pep.h"

Results:

"PNG max" is the maximum compression via pngslim, and is a lot slower (10s of seconds) than the usual "PNG fast" that comes from exporting via Aseprite/magick/etc.

tree1

tree1

112x96 : 4 colors (channel_bits:4)

FormatSize (bytes)Compression RatioSpeed
PEP5390.098xFast
JPEG XL6520.119xFast
WebP8740.159xSlow
PNG (max)9060.165xVery Slow
GIF1,0470.191xFaster
PNG (fast)1,0810.197xFaster
QOI2,4250.441xVery Fast
BMP5,4941.00xVery Fast

font

font

192x144 : 2 colors (channel_bits:1)

FormatSize (bytes)Compression RatioSpeed
PEP7960.222xFast
JPEG XL9130.255xFast
WebP1,2520.349xSlow
PNG (max)1,2560.350xVery Slow
PNG (fast)1,8350.512xFaster
GIF1,9190.535xFaster
BMP3,5861.00xVery Fast
QOI6,6691.860xVery Fast

nz_scene

nz_scene

640x200 : 251 colors

FormatSize (bytes)Compression RatioSpeed
PEP70,8290.549xFast
WebP71,7220.556xSlow
PNG (max)81,0380.628xVery Slow
JPEG XL81,2760.630xFast
PNG (fast)85,3750.661xFaster
GIF96,9970.751xFaster
BMP129,0781.00xVery Fast
QOI180,5331.399xVery Fast

baboon

baboon

512x512 : 256 colors

FormatSize (bytes)Compression RatioSpeed
PEP154,7780.588xFast
PNG (max)184,2190.700xVery Slow
PNG (fast)195,1300.741xFaster
GIF221,1650.840xFaster
BMP263,2221.00xVery Fast
QOI457,9781.739xVery Fast

Mini-documentation:

pep is designed for games too, so the compression outputs a structure that has useful elements. The pep.data pointer ONLY contains the bytes for the pixels. This library doesn't have a BMP loader, so this is specifically designed for setups where the color bytes already exist. You just feed it into pep_compress() with the correct in_format of the bytes, and then you're able to use it however you like! Often you just use pep_save() after compressing, and then pep_load() + pep_decompress() to access the image data.

/*
pep_compress() parameters:
	uint32_t*        PIXEL_BYTES = raw RGBA/BGRA pixels
	uint16_t         WIDTH       = width of the image
	uint16_t         HEIGHT      = height of the image
	pep_format       IN_FORMAT   = channel-byte-order of PIXEL_BYTES: pep_rgba, pep_bgra, pep_argb, or pep_abgr
	pep_channel_bits BITS  = bits-per-channel: pep_1bit, pep_2bit, pep_4bit, pep_8bit (default)
returns:
	a pep struct
*/
pep p = pep_compress( PIXEL_BYTES, WIDTH, HEIGHT, IN_FORMAT, BITS );

/*
pep_decompress() parameters:
	pep*       IN_PEP                  = pep struct-pointer to decompress
	pep_format OUT_FORMAT              = channel-byte-order of the new pixels: pep_rgba, pep_bgra, pep_argb, or pep_abgr
	uint8_t    FIRST_COLOR_TRANSPARENT = 0 or 1 to make the first color have 0 Alpha
	uint8_t    PRE_MULTIPLY            = 0 or 1 to make the RGB channels pre-multiplied with A
returns:
	a uint32_t* with the uncompressed pixel data
*/
uint32_t* pixels = pep_decompress( IN_PEP, OUT_FORMAT, FIRST_COLOR_TRANSPARENT, PRE_MULTIPLY );

/*
pep_free() parameters:
	pep* IN_PEP = pep struct-pointer to free
note:
	frees the internal bytes buffer and resets bytes_size to 0
*/
pep_free( IN_PEP );

/*
pep_serialize() parameters:
	pep*      IN_PEP   = pep struct-pointer to serialize
	uint32_t* OUT_SIZE = pointer to store the resulting byte array size
returns:
	a uint8_t* byte array containing the serialized pep data
note:
	caller must free() the returned byte array when done
*/
uint8_t* bytes = pep_serialize( IN_PEP, OUT_SIZE );

/*
pep_deserialize() parameters:
	uint8_t* IN_BYTES = byte array containing serialized pep data
returns:
	a pep struct reconstructed from the byte array
*/
pep p = pep_deserialize( IN_BYTES );

/*
pep_save() parameters:
	pep*  IN_PEP    = pep struct-pointer to save
	char* FILE_PATH = path to save the .pep file (e.g. "image.pep")
returns:
	uint8_t - 1 on success, 0 on failure
*/
uint8_t success = pep_save( IN_PEP, FILE_PATH );

/*
pep_load() parameters:
	char* FILE_PATH = path to the .pep file to load (e.g. "image.pep")
returns:
	a pep struct loaded from the file
note:
	returns an empty pep struct on failure
*/
pep p = pep_load( FILE_PATH );

References

Though a lot of this was bashed together with love and tweaked with brute-force, many underlying structures were inspired/referenced from these sources:

  1. Wikipedia: Prediction by Partial Matching.
  2. Cleary, J. G., & Witten, I. H. (1984): Data compression using adaptive coding and partial string matching. IEEE Transactions on Communications, 32(4), 396-402.
  3. Moffat, A. (1990): Implementing the PPM data compression scheme. IEEE Transactions on Communications, 38(11), 1917-1921.
  4. Cleary, J. G., Teahan, W. J., & Witten, I. H. (1995): Unbounded length contexts for PPM. Proceedings DCC-95, IEEE Computer Society Press, 52-61.
  5. Shkarin, D. (2002): PPM: One step to practicality. Proceedings of Data Compression Conference 2002, 202-211.

Please contribute to make PEP the best pixel art format there is!

-End :::.

c
compression
image-compression
pixel-art
single-header

ENDESGA/pep

Prediction-Encoded Pixels - a tiny yet powerful single-header pixel art compression method that focuses on size

C

370

53 commits

updated Jun 10, 2026

See the code

README

Prediction-Encoded Pixels

This format is specifically designed to be for low-color pixel art (<=16 colors works best, up to 256 colors is supported).

It uses "Prediction by Partial Matching" compression, which is able to compress packed-palette-indices smaller than GIF, PNG, and QOI, while sacrificing a bit of time. It's 2-10x slower than GIF/PNG (fast)/QOI (depending on the image), but often compresses the image 20-50% smaller than GIF/PNG (and multiple-times smaller than QOI).

If you care about compressed image size, this is for you. It's somewhere between GIF and WEBP (.webp can compress better at times, but it's painfully slow).


( this is currently in the EXPERIMENTAL phase! )


How to use it:

Use the C header like:

#define PEP_IMPLEMENTATION
#include "pep.h"

Results:

"PNG max" is the maximum compression via pngslim, and is a lot slower (10s of seconds) than the usual "PNG fast" that comes from exporting via Aseprite/magick/etc.

tree1

tree1

112x96 : 4 colors (channel_bits:4)

FormatSize (bytes)Compression RatioSpeed
PEP5390.098xFast
JPEG XL6520.119xFast
WebP8740.159xSlow
PNG (max)9060.165xVery Slow
GIF1,0470.191xFaster
PNG (fast)1,0810.197xFaster
QOI2,4250.441xVery Fast
BMP5,4941.00xVery Fast

font

font

192x144 : 2 colors (channel_bits:1)

FormatSize (bytes)Compression RatioSpeed
PEP7960.222xFast
JPEG XL9130.255xFast
WebP1,2520.349xSlow
PNG (max)1,2560.350xVery Slow
PNG (fast)1,8350.512xFaster
GIF1,9190.535xFaster
BMP3,5861.00xVery Fast
QOI6,6691.860xVery Fast

nz_scene

nz_scene

640x200 : 251 colors

FormatSize (bytes)Compression RatioSpeed
PEP70,8290.549xFast
WebP71,7220.556xSlow
PNG (max)81,0380.628xVery Slow
JPEG XL81,2760.630xFast
PNG (fast)85,3750.661xFaster
GIF96,9970.751xFaster
BMP129,0781.00xVery Fast
QOI180,5331.399xVery Fast

baboon

baboon

512x512 : 256 colors

FormatSize (bytes)Compression RatioSpeed
PEP154,7780.588xFast
PNG (max)184,2190.700xVery Slow
PNG (fast)195,1300.741xFaster
GIF221,1650.840xFaster
BMP263,2221.00xVery Fast
QOI457,9781.739xVery Fast

Mini-documentation:

pep is designed for games too, so the compression outputs a structure that has useful elements. The pep.data pointer ONLY contains the bytes for the pixels. This library doesn't have a BMP loader, so this is specifically designed for setups where the color bytes already exist. You just feed it into pep_compress() with the correct in_format of the bytes, and then you're able to use it however you like! Often you just use pep_save() after compressing, and then pep_load() + pep_decompress() to access the image data.

/*
pep_compress() parameters:
	uint32_t*        PIXEL_BYTES = raw RGBA/BGRA pixels
	uint16_t         WIDTH       = width of the image
	uint16_t         HEIGHT      = height of the image
	pep_format       IN_FORMAT   = channel-byte-order of PIXEL_BYTES: pep_rgba, pep_bgra, pep_argb, or pep_abgr
	pep_channel_bits BITS  = bits-per-channel: pep_1bit, pep_2bit, pep_4bit, pep_8bit (default)
returns:
	a pep struct
*/
pep p = pep_compress( PIXEL_BYTES, WIDTH, HEIGHT, IN_FORMAT, BITS );

/*
pep_decompress() parameters:
	pep*       IN_PEP                  = pep struct-pointer to decompress
	pep_format OUT_FORMAT              = channel-byte-order of the new pixels: pep_rgba, pep_bgra, pep_argb, or pep_abgr
	uint8_t    FIRST_COLOR_TRANSPARENT = 0 or 1 to make the first color have 0 Alpha
	uint8_t    PRE_MULTIPLY            = 0 or 1 to make the RGB channels pre-multiplied with A
returns:
	a uint32_t* with the uncompressed pixel data
*/
uint32_t* pixels = pep_decompress( IN_PEP, OUT_FORMAT, FIRST_COLOR_TRANSPARENT, PRE_MULTIPLY );

/*
pep_free() parameters:
	pep* IN_PEP = pep struct-pointer to free
note:
	frees the internal bytes buffer and resets bytes_size to 0
*/
pep_free( IN_PEP );

/*
pep_serialize() parameters:
	pep*      IN_PEP   = pep struct-pointer to serialize
	uint32_t* OUT_SIZE = pointer to store the resulting byte array size
returns:
	a uint8_t* byte array containing the serialized pep data
note:
	caller must free() the returned byte array when done
*/
uint8_t* bytes = pep_serialize( IN_PEP, OUT_SIZE );

/*
pep_deserialize() parameters:
	uint8_t* IN_BYTES = byte array containing serialized pep data
returns:
	a pep struct reconstructed from the byte array
*/
pep p = pep_deserialize( IN_BYTES );

/*
pep_save() parameters:
	pep*  IN_PEP    = pep struct-pointer to save
	char* FILE_PATH = path to save the .pep file (e.g. "image.pep")
returns:
	uint8_t - 1 on success, 0 on failure
*/
uint8_t success = pep_save( IN_PEP, FILE_PATH );

/*
pep_load() parameters:
	char* FILE_PATH = path to the .pep file to load (e.g. "image.pep")
returns:
	a pep struct loaded from the file
note:
	returns an empty pep struct on failure
*/
pep p = pep_load( FILE_PATH );

References

Though a lot of this was bashed together with love and tweaked with brute-force, many underlying structures were inspired/referenced from these sources:

  1. Wikipedia: Prediction by Partial Matching.
  2. Cleary, J. G., & Witten, I. H. (1984): Data compression using adaptive coding and partial string matching. IEEE Transactions on Communications, 32(4), 396-402.
  3. Moffat, A. (1990): Implementing the PPM data compression scheme. IEEE Transactions on Communications, 38(11), 1917-1921.
  4. Cleary, J. G., Teahan, W. J., & Witten, I. H. (1995): Unbounded length contexts for PPM. Proceedings DCC-95, IEEE Computer Society Press, 52-61.
  5. Shkarin, D. (2002): PPM: One step to practicality. Proceedings of Data Compression Conference 2002, 202-211.

Please contribute to make PEP the best pixel art format there is!

-End :::.

c
compression
image-compression
pixel-art
single-header

Significant stargazers

ExE Boss

117 followers · starred Sep 2026

Ray

5,700 followers · starred Aug 2025

Jan T. Sott

248 followers · starred Aug 2025

Cody Bennett

733 followers · starred Aug 2025