From 04a269f4f850a2f1179fed3a5ccc16923d9f396a Mon Sep 17 00:00:00 2001 From: Ray Date: Wed, 1 Jul 2026 11:55:27 +0200 Subject: [PATCH] ADDED: Support for .pep image fileformat, useful for pixl-art --- src/config.h | 3 + src/external/pep.h | 1328 ++++++++++++++++++++++++++++++++++++++++++++ src/rtextures.c | 53 ++ 3 files changed, 1384 insertions(+) create mode 100644 src/external/pep.h diff --git a/src/config.h b/src/config.h index cdb79a676..e77089b68 100644 --- a/src/config.h +++ b/src/config.h @@ -244,6 +244,9 @@ #ifndef SUPPORT_FILEFORMAT_QOI #define SUPPORT_FILEFORMAT_QOI 1 #endif +#ifndef SUPPORT_FILEFORMAT_PEP + #define SUPPORT_FILEFORMAT_PEP 0 +#endif #ifndef SUPPORT_FILEFORMAT_PSD #define SUPPORT_FILEFORMAT_PSD 0 // Disabled by default #endif diff --git a/src/external/pep.h b/src/external/pep.h new file mode 100644 index 000000000..bc8a793b1 --- /dev/null +++ b/src/external/pep.h @@ -0,0 +1,1328 @@ +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Prediction-Encoded Pixels (pep) +// +// author(s): +// ENDESGA - https://x.com/ENDESGA | https://bsky.app/profile/endesga.bsky.social +// +// contributer(s): +// Mariusz Dzikowski - https://github.com/demurzasty +// Akreson - https://github.com/Akreson +// Alepacho - https://github.com/Alepacho +// +// https://github.com/ENDESGA/pep +// 2026 - CC0 - FOSS forever +// + +// The .pep type is a pixel art format made to compress as small as possible. +// It uses a custom "Prediction by Partial Matching" compression method +// designed from the ground up for palettized pixel art. + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma region - DEPENDENCIES +// + +#pragma once + +#ifndef _GNU_SOURCE + #define _GNU_SOURCE +#endif + +#include // uint*_t +#include // mem-alloc +#include // FILE +#include // memset + +#pragma endregion dependencies + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma region - MACROS +// + +#define __PEP_STRINGIFY( VALUE ) #VALUE +#define _PEP_STRINGIFY( VALUE ) __PEP_STRINGIFY( VALUE ) + +//////////////////////////////////////////////////////////////// +#pragma region - memory + +// This defines a set of macros that serve as wrappers for the standard +// C library memory management functions: `malloc`, `realloc`, and `free`. +// These macros can be used to easily replace the underlying memory allocation +// implementation in a project, for example, with a custom allocator or a +// debug-enabled version, without modifying all call sites. +#ifndef PEP_MALLOC + #define PEP_MALLOC( size ) malloc( size ) + #define PEP_REALLOC( ptr, size ) realloc( ptr, size ) + #define PEP_FREE( ptr ) free( ptr ) +#endif + +#pragma endregion memory + +//////////////////////////////////////////////////////////////// +#pragma region - bits + +// Provides a cross-platform macro to count leading zeros in a 32-bit integer. +#ifndef PEP_COUNT_LEADING_ZEROS + #ifdef _MSC_VER + // Microsoft Visual C++ compiler. + #define PEP_COUNT_LEADING_ZEROS( x ) __lzcnt( x ) + #else + // GCC/Clang compilers. + #define PEP_COUNT_LEADING_ZEROS( x ) __builtin_clz( x ) + #endif +#endif + +// How many bits do we need to fit N values? +#define PEP_BITS_TO_FIT( N ) ( ( ( N ) <= 1 ) ? 1 : ( 32 - PEP_COUNT_LEADING_ZEROS( ( N ) - 1 ) ) ) + +#pragma endregion bits + +//////////////////////////////////////////////////////////////// +#pragma region - hash + +// Standard hash multipliers, there might be better ones: +#define _PEP_HASH_N 1540483477u // 0x5BD1E995 MurmurHash2 +#define _PEP_HASH_W 3332679571u // 0xC6A4A793 MurmurHash64A +#define _PEP_HASH_NW 3432918353u // 0xCC9E2D51 MurmurHash3 +#define _PEP_HASH_NE 2246822507u // 0x85EBCA6B MurmurHash3 +// Hash-prospector, avalanche-optimized, by Chris Wellons: +#define _PEP_HASH_NN 2146121005u // 0x7FEB352D lowbias32 +#define _PEP_HASH_WW 2890668881u // 0xAC4C1B51 triple32 round-2 +#define _PEP_HASH_NNW 830770091u // 0x31848BAB triple32 round-3 +#define _PEP_HASH_NNE 1935289751u // 0x735A2D97 prospector +#define _PEP_HASH_NWW 2943497623u // 0xAF723597 prospector + +#pragma endregion hash + +#pragma endregion macros + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma region - CONSTANTS +// + +#define PEP_NAME "pep" + +//////////////////////////////////////////////////////////////// +#pragma region - version + +#define PEP_VERSION_MAJOR 0 +#define PEP_VERSION_MINOR 6 +#define PEP_VERSION_PATCH 0 +#define PEP_VERSION _PEP_STRINGIFY( PEP_VERSION_MAJOR ) "." _PEP_STRINGIFY( PEP_VERSION_MINOR ) "." _PEP_STRINGIFY( PEP_VERSION_PATCH ) + +#pragma endregion version + +//////////////////////////////////////////////////////////////// +#pragma region - frequency + +// This is the amount of frequencies per context, and the amount of contexts, +// with [256] being the order0 context. +// Originally there were 256*256 contexts, but I found the image didn't get +// much bigger with the same amount of frequencies. Theoretically the more +// contexts you have the smaller the image is... +#define PEP_FREQ_N 257 +#define PEP_FREQ_END ( PEP_FREQ_N - 1 ) +#define PEP_CONTEXTS_MAX PEP_FREQ_END + +// PEP_FREQ_MAX is the maximum accumulative frequency. +// This is the starting maximum, and is scaled as the image compresses, via +// the palette-delta; which seems to roughly correlate with the complexity. +// This value works better for low-res images. +#define PEP_FREQ_MAX ( PEP_FREQ_END >> 1 ) + +#pragma endregion frequency + +//////////////////////////////////////////////////////////////// +#pragma region - arithmetic -coding + +// These constants are for the 63bit arithmetic-coding, specifically not 64bit +// because of overflow. +#define PEP_CODE_BITS 24lu +#define PEP_CODE_BITS_INV ( 32lu - PEP_CODE_BITS ) +#define PEP_FREQ_MAX_BITS 14lu +#define PEP_PROB_MAX_VALUE ( 1 << PEP_FREQ_MAX_BITS ) +#define PEP_CODE_MAX_VALUE ( ( 1 << PEP_CODE_BITS ) - 1 ) + +#pragma endregion arithmetic -coding + +#pragma endregion defaults + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma region - DECLARATIONS +// + +//////////////////////////////////////////////////////////////// +#pragma region - formats + +// .pep can automatically convert from RGBA to BGRA (little/big endian), +// which is often for low-level/backend rendering pipelines. +// Windows only supports ARGB for example, and so sometimes it's easier to +// make the whole application use that format. +typedef enum +{ + pep_rgba, + pep_bgra, + + pep_abgr, + pep_argb +} +pep_format; + +// Palette colors can be restricted in the serialization phase to a maximum +// amount of bits per channel. +// The default is 8 bits per channel (standard 32 bit colors). +// This is NOT bits-per-pixel and does NOT cap palette_size. +typedef enum +{ + pep_1bit, + pep_2bit, + pep_4bit, + pep_8bit +} +pep_channel_bits; + +#pragma endregion formats + +//////////////////////////////////////////////////////////////// +#pragma region - arithmetic -coding + +// During the compression process the context per frequency-group needs to be +// tracked, with the sum of all frequencies being stored. +typedef struct +{ + uint16_t freq[ PEP_FREQ_N ]; + uint32_t sum; +} +_pep_context; + +// Arithmetic coding structures: +typedef struct +{ + uint8_t* data_ref; + uint32_t low; + uint32_t range; +} +_pep_ac_encode; + +typedef struct +{ + uint8_t* data_ref; + uint8_t* end_of_data; + uint32_t low; + uint32_t range; + uint32_t code; +} +_pep_ac_decode; + +typedef struct +{ + uint32_t high; + uint32_t low; + uint32_t scale; +} +_pep_prob; + +typedef struct +{ + _pep_prob prob; + uint32_t symbol; +} +_pep_sym_decode; + +#pragma endregion arithmetic -coding + +//////////////////////////////////////////////////////////////// +#pragma region - pep + +// This is the main struct-type that contains values for using this format. +// +// `channel_bits` controls how many bits each R/G/B/A component of every +// palette entry is quantized to during serialization. It does NOT limit +// the number of palette entries (that's `palette_size`, up to 256). +// For example, pep_2bit means each channel has 4 possible values (0/85/170/255), +// so the palette can only draw from 4*4*4 = 64 distinct RGB colors. +typedef struct +{ + uint8_t* bytes; + uint64_t bytes_size; + uint16_t width; + uint16_t height; + pep_format format; + uint32_t palette[ 256 ]; + uint8_t palette_size; + pep_channel_bits channel_bits; + uint8_t model; +} +pep; + +#pragma endregion pep + +//////////////////////////////////////////////////////////////// +#pragma region - functions + +pep pep_compress( const uint32_t* in_pixels, const uint16_t width, const uint16_t height, const pep_format in_format, const pep_channel_bits in_channel_bits ); +uint32_t* pep_decompress( const pep* const in_pep, const pep_format out_format, const uint8_t first_color_transparent, uint8_t const pre_multiply ); +void pep_free( pep* in_pep ); +uint8_t* pep_serialize( const pep* in_pep, uint32_t* const out_size ); +pep pep_deserialize( const uint8_t* const in_bytes, const uint32_t in_bytes_size ); +uint8_t pep_save( const pep* const in_pep, const char* const file_path ); +pep pep_load( const char* const file_path ); + +#pragma endregion functions + +#pragma endregion declarations + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma region - DEFINITIONS +// + +#ifdef PEP_IMPLEMENTATION + +#pragma region - private / functions + +static _pep_prob _pep_get_prob_from_context( const _pep_context* const context, const uint32_t symbol ); +static void _pep_arith_encode( _pep_ac_encode* const ac, const _pep_prob prob ); +static void _pep_arith_encode_normalize( _pep_ac_encode* const ac ); +static uint32_t _pep_arith_decode_curr_freq( _pep_ac_decode* const ac, const uint32_t scale ); +static void _pep_arith_decode_update( _pep_ac_decode* const ac, const _pep_prob prob ); +static _pep_sym_decode _pep_get_sym_from_freq( const _pep_context* const context, const uint32_t target_freq ); +static uint32_t _pep_pre_multiply( const uint32_t pixel, const pep_format format ); +static uint32_t _pep_reformat( const uint32_t in_color, const pep_format in_format, const pep_format out_format ); + +#pragma endregion private / functions + +#ifdef _MSC_VER + // Intrin header is only needed for implementation. + #include // __lzcnt + + // Disable unsafe fopen usage warning on MSVC compiler. + #pragma warning( push ) + #pragma warning( disable : 4996 ) +#endif + +//////////////////////////////////////////////////////////////// +#pragma region - arithmetic + +//////////////////////////////// +#pragma region | encode + +// Getting cumulative frequnce of symbol +static _pep_prob _pep_get_prob_from_context( const _pep_context* const context, const uint32_t symbol ) +{ + _pep_prob prob = { 0 }; + prob.scale = context->sum; + + for( uint32_t index = 0; index < symbol; ++index ) + { + prob.low += context->freq[ index ]; + } + + prob.high = prob.low + context->freq[ symbol ]; + return prob; +} + +// This encodes a symbol into the arithmetic-coding range. It scales the +// current range based on the symbol's frequency and total frequency count. +static void _pep_arith_encode( _pep_ac_encode* const ac, const _pep_prob prob ) +{ + ac->range /= prob.scale; + ac->low += prob.low * ac->range; + ac->range *= prob.high - prob.low; +} + +// Adjusts the arithmetic-coding range by removing a boundary value +// Main goal of this process is to keep our range from getting +// too small. +static void _pep_arith_encode_normalize( _pep_ac_encode* const ac ) +{ + while( 1 ) + { + if( ( ac->low ^ ( ac->low + ac->range ) ) >= PEP_CODE_MAX_VALUE ) + { + if( ac->range >= PEP_PROB_MAX_VALUE ) break; + + ac->range = PEP_PROB_MAX_VALUE - ( ac->low & ( PEP_PROB_MAX_VALUE - 1 ) ); + } + + uint8_t byte = ac->low >> PEP_CODE_BITS; + ac->low <<= PEP_CODE_BITS_INV; + ac->range <<= PEP_CODE_BITS_INV; + *ac->data_ref++= byte; + } +} + +#pragma endregion encode + +//////////////////////////////// +#pragma region | decode + +// Getting current frequency by doing reverse trasformation +static uint32_t _pep_arith_decode_curr_freq( _pep_ac_decode* const ac, const uint32_t scale ) +{ + ac->range /= scale; + uint32_t result = ( ac->code - ac->low ) / ( ac->range ); + return result; +} + +// Same as with the encode_normalize, only on decode we reading in value +static void _pep_arith_decode_update( _pep_ac_decode* const ac, const _pep_prob prob ) +{ + ac->low += ac->range * prob.low; + ac->range *= prob.high - prob.low; + + while( 1 ) + { + if( ( ac->low ^ ( ac->low + ac->range ) ) >= PEP_CODE_MAX_VALUE ) + { + if( ac->range < PEP_PROB_MAX_VALUE ) + { + ac->range = PEP_PROB_MAX_VALUE - ( ac->low & ( PEP_PROB_MAX_VALUE - 1 ) ); + } + else break; + } + + uint8_t in_byte = 0; + if( ac->data_ref != ac->end_of_data ) + { + in_byte = *ac->data_ref++; + } + + ac->code = ( ac->code << 8 ) | in_byte; + ac->range <<= 8; + ac->low <<= 8; + } +} + +static _pep_sym_decode _pep_get_sym_from_freq( const _pep_context* const context, const uint32_t target_freq ) +{ + _pep_sym_decode result = { 0 }; + + uint32_t s = 0; + uint32_t freq = 0; + for( ; s <= PEP_FREQ_END; ++s ) + { + freq += context->freq[ s ]; + if( freq > target_freq ) break; + } + if( s > PEP_FREQ_END ) s = PEP_FREQ_END; + + result.prob.high = freq; + result.prob.low = freq - context->freq[ s ]; + result.prob.scale = context->sum; + result.symbol = s; + + return result; +} + +#pragma endregion decode + +#pragma endregion arithmetic + +//////////////////////////////////////////////////////////////// +#pragma region - compression + +//////////////////////////////// +#pragma region | compression / hidden + +// pep supports pre-multiplying the RGB channels with the A channel. +static uint32_t _pep_pre_multiply( const uint32_t pixel, const pep_format format ) +{ + uint8_t* bytes = ( uint8_t* )( &pixel ); + + if( format <= pep_bgra ) + { + uint32_t scaled_a = ( uint32_t )( bytes[ 3 ] ) * 257; + bytes[ 1 ] = ( uint8_t )( ( ( uint32_t )( bytes[ 1 ] ) * scaled_a + 32896 ) >> 16 ); + bytes[ 2 ] = ( uint8_t )( ( ( uint32_t )( bytes[ 2 ] ) * scaled_a + 32896 ) >> 16 ); + bytes[ 3 ] = ( uint8_t )( ( ( uint32_t )( bytes[ 3 ] ) * scaled_a + 32896 ) >> 16 ); + } + else + { + uint32_t scaled_a = ( uint32_t )( bytes[ 0 ] ) * 257; + bytes[ 0 ] = ( uint8_t )( ( ( uint32_t )( bytes[ 0 ] ) * scaled_a + 32896 ) >> 16 ); + bytes[ 1 ] = ( uint8_t )( ( ( uint32_t )( bytes[ 1 ] ) * scaled_a + 32896 ) >> 16 ); + bytes[ 2 ] = ( uint8_t )( ( ( uint32_t )( bytes[ 2 ] ) * scaled_a + 32896 ) >> 16 ); + } + + return pixel; +} + +// pep supports "dynamic formats", where you can specify what the in-bytes are, +// and reformat to a different channel-order. +// This means two "identical" pep files can have different formats, but you +// can choose how to reformat it when it decompresses! +static uint32_t _pep_reformat( const uint32_t in_color, const pep_format in_format, const pep_format out_format ) +{ + if( in_format == out_format ) return in_color; + + if( in_format <= pep_bgra && out_format <= pep_bgra ) + { // RGBA <-> BGRA: swap R and B + return( in_color & 0xff00ff00 ) | ( ( in_color & 0x000000ff ) << 16 ) | ( ( in_color & 0x00ff0000 ) >> 16 ); + } + else if( in_format >= pep_abgr && out_format >= pep_abgr ) + { // ABGR <-> ARGB: swap R and B + return( in_color & 0x00ff00ff ) | ( ( in_color & 0x0000ff00 ) << 16 ) | ( ( in_color & 0xff000000 ) >> 16 ); + } + else if( ( in_format ^ out_format ) == 2 ) + { // Alpha flip: RGBA <-> ARGB or BGRA <-> ABGR + return( ( in_color & 0x000000ff ) << 24 ) | ( ( in_color & 0x0000ff00 ) << 8 ) | ( ( in_color & 0x00ff0000 ) >> 8 ) | ( ( in_color & 0xff000000 ) >> 24 ); + } + else if( in_format < out_format ) + { // Flip: RGBA/BGRA -> ABGR/ARGB + return( ( in_color & 0xff000000 ) >> 24 ) | ( ( in_color & 0x00ffffff ) << 8 ); + } + else + { // Flip: ABGR/ARGB -> RGBA/BGRA + return( ( in_color & 0x000000ff ) << 24 ) | ( ( in_color & 0xffffff00 ) >> 8 ); + } +} + +// Update the frequency table after encoding/decoding a symbol. +// This increments the symbol's frequency and the total sum. +// When we hit freq_max, we increase the freq_max via a complexity +// approximation via the palette-size, then we scale everything down +// to a quarter to keep the frequencies manageable. +// This dynamic part helps the compression adapt to the image's patterns. +#define _PEP_UPDATE( CONTEXT, SYMBOL, FREQ_MAX, PALETTE_SIZE )\ + do\ + {\ + CONTEXT->freq[ SYMBOL ] += 2;\ + CONTEXT->sum += 2;\ + if( CONTEXT->freq[ SYMBOL ] >= FREQ_MAX || CONTEXT->sum >= PEP_PROB_MAX_VALUE )\ + {\ + FREQ_MAX += ( PEP_FREQ_END - PALETTE_SIZE ) >> 1;\ + CONTEXT->sum = 0;\ + for( uint64_t f = 0; f < PEP_FREQ_N; f++ )\ + {\ + const uint16_t _f = CONTEXT->freq[ f ];\ + if( _f == 0 ) continue;\ + const uint16_t scaled = ( _f + 1 ) >> 1;\ + CONTEXT->freq[ f ] = scaled;\ + CONTEXT->sum += scaled;\ + }\ + }\ + }\ + while( 0 ) + +#pragma endregion hidden + +//////////////////////////////// +#pragma region | compression / visible + +#define PEP_HASH_BIT_CONTEXT ( 1u << 16 ) + +enum +{ + _PEP_TAP_HASH, + _PEP_TAP_NW, + _PEP_TAP_W +}; + +// Different context models perform differently depending on the pixel data. +// From lots of testing it seems these 3 structures give the best results +// across the 3 major types of pixel art. +// Pep will sacrifice some time to ensure the file is as small as possible +// by testing all 3 of these without assumptions. +static const int8_t _pep_models[] = + { + _PEP_TAP_HASH, // typically for ~2 colors + _PEP_TAP_NW, // typically for ~4-8 colors + _PEP_TAP_W // typically for >=64 colors + }; +#define PEP_NUM_MODELS ( ( uint8_t )( sizeof( _pep_models ) / sizeof( _pep_models[ 0 ] ) ) ) + +typedef struct +{ + _pep_context* contexts; + uint32_t index_end; + uint32_t index_escape; + int8_t kind; +} +_pep_model_context; + +// Allocate only the context regions a given model uses, then seed order-0. +static uint8_t _pep_model_init( _pep_model_context* m, int8_t kind, uint32_t pcount ) +{ + memset( m, 0, sizeof( *m ) ); + m->kind = kind; + m->index_escape = pcount; + m->index_end = pcount + 1; + + // order0 starts at offset 0, followed by the specific context level + uint32_t off = 1; + switch( kind ) + { + case _PEP_TAP_HASH: + { + off += PEP_HASH_BIT_CONTEXT; + break; + } + case _PEP_TAP_NW: + { + off += m->index_end * m->index_end; + break; + } + case _PEP_TAP_W: + { + off += m->index_end; + break; + } + } + + m->contexts = ( _pep_context* )calloc( off, sizeof( _pep_context ) ); + if( !m->contexts ) return 0; + + _pep_context* order0 = &m->contexts[ 0 ]; + for( uint32_t index = 0; index < pcount; index++ ) order0->freq[ index ] = 1; + order0->sum = pcount; + return 1; +} + +static void _pep_model_free( _pep_model_context* m ) +{ + if( m->contexts ) + { + free( m->contexts ); + m->contexts = NULL; + } +} + +// Resolve the context pointer for the specific level of this pixel. +static _pep_context* _pep_build_level( const _pep_model_context* m, const uint8_t* idx, uint32_t pos, uint32_t x, uint32_t y, uint16_t width ) +{ + // It's only possible to tap previous values: + /* + - NNW NN NNE + NWW NW N NE + WW W [x] + */ + // The 10th NNWW tap has dimishing returns + + const uint32_t SENT = m->index_escape; + const uint32_t index_end = m->index_end; + const uint32_t Wv = ( x > 0 ) ? idx[ pos - 1 ] : SENT; + const uint32_t Nv = ( y > 0 ) ? idx[ pos - width ] : SENT; + + switch( m->kind ) + { + case _PEP_TAP_HASH: + { + const uint32_t NWv = ( x > 0 && y > 0 ) ? idx[ pos - width - 1 ] : SENT; + const uint32_t NNv = ( y > 1 ) ? idx[ pos - 2 * width ] : SENT; + const uint32_t WWv = ( x > 1 ) ? idx[ pos - 2 ] : SENT; + const uint32_t NEv = ( ( x + 1 ) < width && y > 0 ) ? idx[ pos - width + 1 ] : SENT; + const uint32_t NNWv = ( y > 1 && x > 0 ) ? idx[ pos - 2 * width - 1 ] : SENT; + const uint32_t NNEv = ( y > 1 && ( x + 1 ) < width ) ? idx[ pos - 2 * width + 1 ] : SENT; + const uint32_t NWWv = ( x > 1 && y > 0 ) ? idx[ pos - width - 2 ] : SENT; + + uint32_t h = Nv * _PEP_HASH_N + Wv * _PEP_HASH_W + NWv * _PEP_HASH_NW + NEv * _PEP_HASH_NE + NNv * _PEP_HASH_NN + WWv * _PEP_HASH_WW + NNWv * _PEP_HASH_NNW + NNEv * _PEP_HASH_NNE + NWWv * _PEP_HASH_NWW; + h ^= h >> 15; + return &m->contexts[ 1 + ( h & ( PEP_HASH_BIT_CONTEXT - 1 ) ) ]; + } + case _PEP_TAP_NW: + { + return &m->contexts[ 1 + Nv * index_end + Wv ]; + } + case _PEP_TAP_W: + { + return &m->contexts[ 1 + Wv ]; + } + } + return NULL; +} + +// Encode all pixels with one model and return the size of the byte stream. +static uint64_t _pep_encode_model( const uint8_t* idx, uint16_t width, uint16_t height, uint32_t pcount, uint8_t model, uint8_t* out ) +{ + _pep_model_context m; + if( !_pep_model_init( &m, _pep_models[ model ], pcount ) ) return 0; + const uint32_t index_escape = m.index_escape; + + _pep_ac_encode ac = { 0 }; + ac.range = ( uint32_t )( ( 1llu << 32 ) - 1 ); + ac.data_ref = out; + uint16_t freq_max = PEP_FREQ_MAX; + _pep_context* order0 = &m.contexts[ 0 ]; + + uint32_t pos = 0; + for( uint32_t y = 0; y < height; y++ ) + { + for( uint32_t x = 0; x < width; x++, pos++ ) + { + const uint32_t symbol = idx[ pos ]; + _pep_context* context = _pep_build_level( &m, idx, pos, x, y, width ); + uint8_t encoded_in_level = 0; + + if( context->sum != 0 ) + { + if( context->freq[ symbol ] != 0 ) + { + _pep_prob prob = _pep_get_prob_from_context( context, symbol ); + _pep_arith_encode( &ac, prob ); + _pep_arith_encode_normalize( &ac ); + _PEP_UPDATE( context, symbol, freq_max, pcount ); + encoded_in_level = 1; + } + else + { + _pep_prob prob = _pep_get_prob_from_context( context, index_escape ); + _pep_arith_encode( &ac, prob ); + _pep_arith_encode_normalize( &ac ); + context->freq[ index_escape ] += 1; + context->sum += 1; + } + } + + if( !encoded_in_level ) + { + _pep_prob prob = _pep_get_prob_from_context( order0, symbol ); + _pep_arith_encode( &ac, prob ); + _pep_arith_encode_normalize( &ac ); + _PEP_UPDATE( order0, symbol, freq_max, pcount ); + + if( context->sum == 0 ) + { + context->freq[ index_escape ] = 1; + context->sum = 1; + } + context->freq[ symbol ] = 1; + context->sum += 1; + } + } + } + + for( uint8_t index = 0; index < 4; index++ ) + { + uint8_t byte = ac.low >> PEP_CODE_BITS; + ac.low <<= PEP_CODE_BITS_INV; + *ac.data_ref++= byte; + } + + uint64_t size = ( uint64_t ) ( ac.data_ref - out ); + _pep_model_free( &m ); + return size; +} + +// The format of the in_pixels has to be the same as in_format. +// out_format is the one applied to the newly compressed pep + +// Compresses raw pixels into a pep, trying each context model and keeping the +// smallest result (the winning model index is stored in out.model). +pep pep_compress( const uint32_t* in_pixels, const uint16_t width, const uint16_t height, const pep_format in_format, const pep_channel_bits in_channel_bits ) +{ + pep out_pep = { 0 }; + uint32_t area = ( uint32_t )width * height; + if( in_pixels == NULL || area == 0 ) return out_pep; + + out_pep.width = width; + out_pep.height = height; + out_pep.format = in_format; + out_pep.channel_bits = in_channel_bits; + + uint8_t* idx = ( uint8_t* )PEP_MALLOC( area ); + + uint32_t palette[ 256 ]; + uint16_t count = 0; // 1-256 distinct colours + const uint32_t* p = in_pixels; + const uint32_t* p_end = p + area; + uint32_t last_p = 0, + this_p = 0; + while( p < p_end ) + { + this_p = *p; + if( p > in_pixels && this_p == last_p ) + { + p++; + continue; + } + uint16_t n = 0; + while( n < count && this_p != palette[ n ] ) n++; + if( n >= count && count < 256 ) palette[ count++ ] = this_p; + last_p = this_p; + p++; + } + memcpy( out_pep.palette, palette, ( size_t ) count * sizeof( uint32_t ) ); + out_pep.palette_size = ( uint8_t )count; + for( uint32_t index = 0; index < area; index++ ) + { + uint32_t c = in_pixels[ index ]; + uint16_t k = 0; + while( k < count && palette[ k ] != c ) k++; + idx[ index ] = ( uint8_t )( k < count ? k : 0 ); + } + const uint32_t pcount = out_pep.palette_size ? out_pep.palette_size : 256; + + uint8_t* best = ( uint8_t* )PEP_MALLOC( ( size_t ) area * sizeof( uint32_t ) + 64 ); + uint8_t* tmp = ( uint8_t* )PEP_MALLOC( ( size_t ) area * sizeof( uint32_t ) + 64 ); + uint64_t best_size = 0; + uint8_t best_model = 0; + + for( uint8_t model = 0; model < PEP_NUM_MODELS; model++ ) + { + uint64_t size = _pep_encode_model( idx, width, height, pcount, model, tmp ); + if( size == 0 ) continue; + + if( best_size == 0 || size < best_size ) + { + best_size = size; + best_model = model; + memcpy( best, tmp, size ); + } + } + + out_pep.bytes = ( uint8_t* )PEP_REALLOC( best, best_size ); + out_pep.bytes_size = best_size; + out_pep.model = ( uint8_t )best_model; + + PEP_FREE( tmp ); + PEP_FREE( idx ); + return out_pep; +} + +// Decode all pixels with one model (mirror of _pep_encode_model). +static void _pep_decode_model( const uint8_t* in_bytes, uint64_t in_size, uint16_t width, uint16_t height, uint32_t pcount, uint8_t model, const uint32_t* palette, pep_format in_format, pep_format out_format, uint8_t pre_multiply, uint8_t* idx, uint32_t* out_pixels ) +{ + _pep_model_context m; + if( !_pep_model_init( &m, _pep_models[ model ], pcount ) ) return; + const uint32_t index_escape = m.index_escape; + + _pep_ac_decode ac = { 0 }; + ac.range = ( uint32_t )( ( 1llu << 32 ) - 1 ); + ac.data_ref = ( uint8_t* )in_bytes; + ac.end_of_data = ( uint8_t* )in_bytes + in_size; + for( uint8_t index = 0; index < 4; ++index ) + { + uint8_t b = 0; + if( ac.data_ref != ac.end_of_data ) b = *ac.data_ref++; + ac.code = ( ac.code << 8 ) | b; + } + + uint16_t freq_max = PEP_FREQ_MAX; + _pep_context* order0 = &m.contexts[ 0 ]; + uint32_t pos = 0; + + for( uint32_t y = 0; y < height; y++ ) + { + for( uint32_t x = 0; x < width; x++, pos++ ) + { + _pep_context* context = _pep_build_level( &m, idx, pos, x, y, width ); + uint32_t symbol = 0; + uint8_t decoded_in_level = 0; + + if( context->sum != 0 ) + { + uint32_t f = _pep_arith_decode_curr_freq( &ac, context->sum ); + _pep_sym_decode r = _pep_get_sym_from_freq( context, f ); + _pep_arith_decode_update( &ac, r.prob ); + + if( r.symbol != index_escape ) + { + symbol = r.symbol; + _PEP_UPDATE( context, symbol, freq_max, pcount ); + decoded_in_level = 1; + } + else + { + context->freq[ index_escape ] += 1; + context->sum += 1; + } + } + + if( !decoded_in_level ) + { + uint32_t f = _pep_arith_decode_curr_freq( &ac, order0->sum ); + _pep_sym_decode r = _pep_get_sym_from_freq( order0, f ); + _pep_arith_decode_update( &ac, r.prob ); + symbol = r.symbol; + _PEP_UPDATE( order0, symbol, freq_max, pcount ); + + if( context->sum == 0 ) + { + context->freq[ index_escape ] = 1; + context->sum = 1; + } + context->freq[ symbol ] = 1; + context->sum += 1; + } + + idx[ pos ] = ( uint8_t )symbol; + uint32_t pixel = ( symbol < pcount ) ? _pep_reformat( palette[ symbol ], in_format, out_format ) : 0; + if( pre_multiply != 0 ) pixel = _pep_pre_multiply( pixel, out_format ); + out_pixels[ pos ] = pixel; + } + } + _pep_model_free( &m ); +} + +uint32_t* pep_decompress( const pep* const in_pep, const pep_format out_format, const uint8_t transparent_first_color, uint8_t const pre_multiply ) +{ + if( in_pep == NULL ) return NULL; + if( in_pep->bytes == NULL || in_pep->bytes_size == 0 || in_pep->width == 0 || in_pep->height == 0 ) return NULL; + + const uint16_t width = in_pep->width, + height = in_pep->height; + const uint32_t area = ( uint32_t )width * height; + uint32_t* out_pixels = ( uint32_t* )PEP_MALLOC( area * sizeof( uint32_t ) ); + uint8_t* idx = ( uint8_t* )PEP_MALLOC( area ); + + const uint32_t pcount = in_pep->palette_size ? in_pep->palette_size : 256; + static uint32_t palette[ 256 ]; + memcpy( palette, in_pep->palette, pcount * sizeof( uint32_t ) ); + if( transparent_first_color != 0 ) + { + if( in_pep->format <= pep_bgra ) palette[ 0 ] &= 0x00ffffff; + else palette[ 0 ] &= 0xffffff00; + } + + uint8_t model = in_pep->model; + if( model >= PEP_NUM_MODELS ) model = 0; + + _pep_decode_model( in_pep->bytes, in_pep->bytes_size, width, height, pcount, model, palette, in_pep->format, out_format, pre_multiply, idx, out_pixels ); + + PEP_FREE( idx ); + return out_pixels; +} + +void pep_free( pep* in_pep ) +{ + if( in_pep && in_pep->bytes ) + { + free( in_pep->bytes ); + in_pep->bytes = NULL; + in_pep->bytes_size = 0; + } +} + +#pragma endregion visible + +#pragma endregion compression + +//////////////////////////////////////////////////////////////// +#pragma region - serialization + +uint8_t* pep_serialize( const pep* in_pep, uint32_t* const out_size ) +{ + if( !in_pep || !in_pep->width || !in_pep->height || !in_pep->bytes_size || !in_pep->bytes ) + { + *out_size = 0; + return NULL; + } + + uint16_t palette_count = in_pep->palette_size ? in_pep->palette_size : 256; + + const uint16_t w = in_pep->width - 1; + const uint16_t h = in_pep->height - 1; + const uint8_t is_small = ( w <= 255 && h <= 255 ) ? 1 : 0; + const uint8_t dim_bytes = is_small ? 2 : 3; + + // calculate variable-length size bytes + uint8_t size_bytes = 0; + uint32_t temp_size = in_pep->bytes_size; + while( temp_size >= 0x80 ) + { + size_bytes++; + temp_size >>= 7; + } + ++size_bytes; + + // check if bitmap (black and white) + uint8_t is_bitmap = 0; + if( palette_count == 2 ) + { + const uint8_t* const color0 = ( uint8_t* )( &in_pep->palette[ 0 ] ); + const uint8_t* const color1 = ( uint8_t* )( &in_pep->palette[ 1 ] ); + + const uint8_t is_white0 = ( color0[ 0 ] == 255 && color0[ 1 ] == 255 && color0[ 2 ] == 255 && color0[ 3 ] == 255 ); + const uint8_t is_white1 = ( color1[ 0 ] == 255 && color1[ 1 ] == 255 && color1[ 2 ] == 255 && color1[ 3 ] == 255 ); + + if( in_pep->format <= pep_bgra ) + { + is_bitmap = ( ( is_white0 && color1[ 0 ] == 0 && color1[ 1 ] == 0 && color1[ 2 ] == 0 && color1[ 3 ] == 255 ) || ( color0[ 0 ] == 0 && color0[ 1 ] == 0 && color0[ 2 ] == 0 && color0[ 3 ] == 255 && is_white1 ) ); + } + else + { + is_bitmap = ( ( is_white0 && color1[ 0 ] == 255 && color1[ 1 ] == 0 && color1[ 2 ] == 0 && color1[ 3 ] == 0 ) || ( color0[ 0 ] == 255 && color0[ 1 ] == 0 && color0[ 2 ] == 0 && color0[ 3 ] == 0 && is_white1 ) ); + } + } + + // check if all palette alphas are 255 + uint8_t only_rgb = 1; + if( !is_bitmap ) + { + for( uint16_t index = 0; index < palette_count; index++ ) + { + if( ( ( ( uint8_t* )( &in_pep->palette[ index ] ) )[ 3 ] ) != 0xff ) + { + only_rgb = 0; + break; + } + } + } + + // calculate palette bytes + uint16_t palette_bytes = 0; + if( !is_bitmap ) + { + const uint16_t channel_bits = 1 << in_pep->channel_bits; + if( channel_bits == 8 ) + { + palette_bytes = palette_count * ( only_rgb ? 3 : 4 ); + } + else + { + const uint16_t channels = only_rgb ? 3 : 4; + palette_bytes = ( channel_bits * channels * palette_count + 7 ) >> 3; + } + } + + // allocate the exact size (subtract 1 for palette_size byte if bitmap) + const uint64_t total_size = dim_bytes + size_bytes + ( is_bitmap ? 0 : 1 ) + palette_bytes + in_pep->bytes_size + 4 + 1; + uint8_t* out_bytes = ( uint8_t* )PEP_MALLOC( total_size ); + uint8_t* out_bytes_ref = out_bytes; + + // flags: format (2), channel_bits (2), is_small (1), only_rgb (1), is_bitmap (1) + *out_bytes_ref++= ( in_pep->format & 0x3 ) | ( ( in_pep->channel_bits & 0x3 ) << 2 ) | ( ( is_small & 0x1 ) << 4 ) | ( ( only_rgb & 0x1 ) << 5 ) | ( ( is_bitmap & 0x1 ) << 6 ); + *out_bytes_ref++= in_pep->model; // takes the last extra byte + + // width/height + if( is_small ) + { + *out_bytes_ref++= w & 0xff; + *out_bytes_ref++= h & 0xff; + } + else + { + const uint32_t packed_dims = ( ( w & 0xfff ) << 12 ) | ( h & 0xfff ); + *out_bytes_ref++= ( packed_dims >> 16 ) & 0xff; + *out_bytes_ref++= ( packed_dims >> 8 ) & 0xff; + *out_bytes_ref++= packed_dims & 0xff; + } + + // variable-length size + uint32_t size = in_pep->bytes_size; + while( size >= 0x80 ) + { + *out_bytes_ref++= ( size | 0x80 ) & 0xff; + size >>= 7; + } + *out_bytes_ref++= size; + + if( !is_bitmap ) + { + // palette size + *out_bytes_ref++= in_pep->palette_size; + + // palette + const uint16_t channel_bits = 1 << in_pep->channel_bits; + const uint8_t shift = 8 - channel_bits; + const uint8_t mask = ( 1 << channel_bits ) - 1; + + if( channel_bits == 8 ) + { + for( uint16_t index = 0; index < palette_count; index++ ) + { + uint8_t* color = ( uint8_t* )( &( in_pep->palette[ index ] ) ); + *out_bytes_ref++= color[ 0 ]; + *out_bytes_ref++= color[ 1 ]; + *out_bytes_ref++= color[ 2 ]; + if( !only_rgb ) *out_bytes_ref++= color[ 3 ]; + } + } + else + { + uint32_t bit_buffer = 0; + uint8_t bit_count = 0; + + for( uint16_t index = 0; index < palette_count; index++ ) + { + uint8_t* color = ( uint8_t* )( &( in_pep->palette[ index ] ) ); + + bit_buffer = ( bit_buffer << channel_bits ) | ( ( color[ 0 ] >> shift ) &mask ); + bit_buffer = ( bit_buffer << channel_bits ) | ( ( color[ 1 ] >> shift ) &mask ); + bit_buffer = ( bit_buffer << channel_bits ) | ( ( color[ 2 ] >> shift ) &mask ); + bit_count += channel_bits * 3; + + if( !only_rgb ) + { + bit_buffer = ( bit_buffer << channel_bits ) | ( ( color[ 3 ] >> shift ) &mask ); + bit_count += channel_bits; + } + + while( bit_count >= 8 ) + { + bit_count -= 8; + *out_bytes_ref++= ( bit_buffer >> bit_count ) & 0xff; + } + } + + if( bit_count > 0 ) + { + *out_bytes_ref++= ( bit_buffer << ( 8 - bit_count ) ) & 0xff; + } + } + } + + memcpy( out_bytes_ref, in_pep->bytes, in_pep->bytes_size ); + out_bytes_ref += in_pep->bytes_size; + *out_bytes_ref = '\0'; + + *out_size = out_bytes_ref - out_bytes; + return out_bytes; +} + +pep pep_deserialize( const uint8_t* const in_bytes, const uint32_t in_bytes_size ) +{ + pep out_pep = { 0 }; + + if( !in_bytes || in_bytes_size == 0 ) return out_pep; + + const uint8_t* bytes_ref = in_bytes; + const uint8_t* const bytes_end = in_bytes + in_bytes_size; + + // packed flags + if( bytes_ref + 1 > bytes_end ) return out_pep; + uint8_t packed_flags = *bytes_ref++; + if( bytes_ref + 1 > bytes_end ) return out_pep; + out_pep.model = *bytes_ref++; + out_pep.format = ( pep_format ) ( packed_flags & 0x3 ); + out_pep.channel_bits = ( pep_channel_bits ) ( ( packed_flags >> 2 ) & 0x3 ); + uint8_t is_small = ( packed_flags >> 4 ) & 0x1; + uint8_t only_rgb = ( packed_flags >> 5 ) & 0x1; + uint8_t is_bitmap = ( packed_flags >> 6 ) & 0x1; + + // width/height + uint8_t dim_bytes = is_small ? 2 : 3; + if( bytes_ref + dim_bytes > bytes_end ) return out_pep; + uint16_t w, + h; + if( is_small ) + { + w = *bytes_ref++; + h = *bytes_ref++; + } + else + { + uint32_t packed_dims = ( *bytes_ref++ << 16 ); + packed_dims |= ( *bytes_ref++ << 8 ); + packed_dims |= *bytes_ref++; + w = ( packed_dims >> 12 ) & 0xfff; + h = packed_dims & 0xfff; + } + out_pep.width = w + 1; + out_pep.height = h + 1; + + // variable-length size + uint32_t bytes_size = 0; + uint8_t shift = 0; + uint8_t byte_val; + do + { + if( bytes_ref >= bytes_end ) return out_pep; + byte_val = *bytes_ref++; + if( shift < 32 ) bytes_size |= ( ( uint32_t )( byte_val &0x7f ) ) << shift; + shift += 7; + } + while( ( byte_val & 0x80 ) && shift < 35 ); + out_pep.bytes_size = bytes_size; + + // handle bitmap or read palette + uint32_t remaining = ( uint32_t )( bytes_end - bytes_ref ); + + if( is_bitmap ) + { + if( remaining < bytes_size ) return out_pep; + + out_pep.palette_size = 2; + out_pep.palette[ 0 ] = out_pep.format <= pep_bgra ? 0xff000000 : 0x000000ff; + out_pep.palette[ 1 ] = 0xffffffff; + } + else + { + // palette size + if( remaining < 1 ) return out_pep; + out_pep.palette_size = *bytes_ref++; + remaining--; + + uint16_t palette_count = out_pep.palette_size ? out_pep.palette_size : 256; + + // palette + const uint8_t channel_bits = 1 << out_pep.channel_bits; + const uint8_t mask = ( 1 << channel_bits ) - 1; + uint32_t palette_bytes; + if( channel_bits == 8 ) + { + palette_bytes = palette_count * ( only_rgb ? 3 : 4 ); + } + else + { + uint16_t channels = only_rgb ? 3 : 4; + palette_bytes = ( channel_bits * channels * palette_count + 7 ) >> 3; + } + + if( remaining < palette_bytes || remaining - palette_bytes < bytes_size ) return out_pep; + + if( channel_bits == 8 ) + { + for( uint16_t index = 0; index < palette_count; index++ ) + { + uint8_t* color = ( uint8_t* )( &( out_pep.palette[ index ] ) ); + color[ 0 ] = *bytes_ref++; + color[ 1 ] = *bytes_ref++; + color[ 2 ] = *bytes_ref++; + color[ 3 ] = only_rgb ? 0xff : *bytes_ref++; + } + } + else + { + uint32_t bit_buffer = 0; + uint8_t bit_count = 0; + + for( uint16_t index = 0; index < palette_count; index++ ) + { + uint8_t channels = only_rgb ? 3 : 4; + uint8_t channel_values[ 4 ]; + + for( uint8_t c = 0; c < channels; c++ ) + { + while( bit_count < channel_bits ) + { + bit_buffer = ( bit_buffer << 8 ) | *bytes_ref++; + bit_count += 8; + } + bit_count -= channel_bits; + channel_values[ c ] = ( bit_buffer >> bit_count ) & mask; + + uint8_t scaled = channel_values[ c ] << ( 8 - channel_bits ); + if( channel_bits < 8 ) + { + scaled |= ( scaled >> channel_bits ); + if( channel_bits < 4 ) + { + scaled |= ( scaled >> ( 2 * channel_bits ) ); + } + } + channel_values[ c ] = scaled; + } + + uint8_t* color = ( uint8_t* )( &( out_pep.palette[ index ] ) ); + color[ 0 ] = channel_values[ 0 ]; + color[ 1 ] = channel_values[ 1 ]; + color[ 2 ] = channel_values[ 2 ]; + color[ 3 ] = only_rgb ? 0xff : channel_values[ 3 ]; + } + } + } + + // copy image data + out_pep.bytes = ( uint8_t* )PEP_MALLOC( bytes_size ); + if( out_pep.bytes ) + { + memcpy( out_pep.bytes, bytes_ref, bytes_size ); + } + + return out_pep; +} + +#pragma endregion serialization + +//////////////////////////////////////////////////////////////// +#pragma region - file + +// For both save/load, file_path should end in ".pep": +// e.g. "texture.pep", "assets/image.pep" + +// Saves pep into a file. +// Returns 0 on failure, 1 on success +uint8_t pep_save( const pep* const in_pep, const char* const file_path ) +{ + if( !in_pep || !file_path ) + { + return 0; + } + + uint32_t bytes_size = 0; + uint8_t* bytes = pep_serialize( in_pep, &bytes_size ); + + if( !bytes || bytes_size == 0 ) + { + return 0; + } + + FILE * file = fopen( file_path, "wb" ); + if( !file ) + { + PEP_FREE( bytes ); + return 0; + } + + size_t written = fwrite( bytes, 1, bytes_size, file ); + + fclose( file ); + PEP_FREE( bytes ); + + #ifdef PEP_DEBUG + printf( "pep: %lld\nfile: %zu\nmodel: %d\n", in_pep->bytes_size, written, in_pep->model ); + #endif + + return written == bytes_size; +} + +// Loads .pep file into returned pep struct +pep pep_load( const char* const file_path ) +{ + pep out_pep = { 0 }; + + if( !file_path ) + { + return out_pep; + } + + FILE * file = fopen( file_path, "rb" ); + if( !file ) + { + return out_pep; + } + + fseek( file, 0, SEEK_END ); + long file_size = ftell( file ); + fseek( file, 0, SEEK_SET ); + + if( file_size <= 0 ) + { + fclose( file ); + return out_pep; + } + + uint8_t* bytes = ( uint8_t* )PEP_MALLOC( file_size ); + + size_t read = fread( bytes, 1, file_size, file ); + fclose( file ); + + if( read != ( size_t ) file_size ) + { + PEP_FREE( bytes ); + return out_pep; + } + + out_pep = pep_deserialize( bytes, ( uint32_t )read ); + PEP_FREE( bytes ); + + #ifdef PEP_DEBUG + printf( "\npep: %lld\nfile: %ld\n", out_pep.bytes_size, file_size ); + #endif + + return out_pep; +} + +#pragma endregion file + +#ifdef _MSC_VER + #pragma warning( pop ) +#endif + +#endif // PEP_IMPLEMENTATION + + +#pragma endregion definitions + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/rtextures.c b/src/rtextures.c index 724fe73cd..35edd2098 100644 --- a/src/rtextures.c +++ b/src/rtextures.c @@ -12,6 +12,7 @@ * #define SUPPORT_FILEFORMAT_JPG 0 * #define SUPPORT_FILEFORMAT_GIF 1 * #define SUPPORT_FILEFORMAT_QOI 1 +* #define SUPPORT_FILEFORMAT_PEP 1 * #define SUPPORT_FILEFORMAT_PSD 0 * #define SUPPORT_FILEFORMAT_HDR 0 * #define SUPPORT_FILEFORMAT_PIC 0 @@ -188,7 +189,15 @@ #if defined(_MSC_VER) #pragma warning(pop) // Disable MSVC warning suppression #endif +#endif +#if SUPPORT_FILEFORMAT_PEP + #define PEP_MALLOC RL_MALLOC + #define PEP_REALLOC RL_REALLOC + #define PEP_FREE RL_FREE + + #define PEP_IMPLEMENTATION + #include "external/pep.h" #endif #if SUPPORT_IMAGE_EXPORT @@ -510,6 +519,31 @@ Image LoadImageFromMemory(const char *fileType, const unsigned char *fileData, i } } #endif +#if SUPPORT_FILEFORMAT_PEP + else if ((strcmp(fileType, ".pep") == 0) || (strcmp(fileType, ".PEP") == 0)) + { + if (fileData != NULL) + { + // Deserialize file data into pep structure + pep p = pep_deserialize(fileData, dataSize); + + // Decompress pep data for image struct + // Decompression options: + // - First color transparent, 3rd parameter: 1 + // - Premultiply alpha on loading, 4th parameter: 1 + unsigned int *pepData = pep_decompress(&p, pep_rgba, 1, 1); + + image.data = pepData; + image.width = p.width; + image.height = p.height; + image.format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8; + image.mipmaps = 1; + + PEP_FREE(pepData); + pep_free(&p); + } + } +#endif #if SUPPORT_FILEFORMAT_DDS else if ((strcmp(fileType, ".dds") == 0) || (strcmp(fileType, ".DDS") == 0)) { @@ -681,6 +715,25 @@ bool ExportImage(Image image, const char *fileName) } } #endif +#if SUPPORT_FILEFORMAT_PEP + else if (IsFileExtension(fileName, ".pep")) + { + if (image.format == PIXELFORMAT_UNCOMPRESSED_R8G8B8A8) + { + // NOTE: Only RGBA 32bit (8bit per channel) image format supported + pep p = pep_compress(image.data, image.width, image.height, pep_rgba, pep_8bit); + + unsigned int pepDataSize = 0; + unsigned char *pepData = pep_serialize(&p, &pepDataSize); + + result = SaveFileData(fileName, pepData, pepDataSize); + + PEP_FREE(pepData); + pep_free(&p); + } + else TRACELOG(LOG_WARNING, "IMAGE: Image can not be exported, .pep requires RGBA 32bit input"); + } +#endif #if SUPPORT_FILEFORMAT_KTX else if (IsFileExtension(fileName, ".ktx")) {