This commit is contained in:
Dominic Szablewski 2021-11-24 11:07:17 +01:00
commit 19dc63cf17
5 changed files with 1130 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
images/

22
README.md Normal file
View File

@ -0,0 +1,22 @@
# QOI - The “Quite OK Image” format for fast, lossless image compression
Single-file MIT licensed library for C/C++
See [qoi.h](https://github.com/phoboslab/qoi/blob/master/qoi.h) for
the documentation.
More info at https://phoboslab.org/log/2021/11/qoi-fast-lossless-image-compression
## Why?
Compared to stb_image and stb_image_write QOI offers 20x-50x faster encoding,
3x-4x faster decoding and 20% better compression. It's also stupidly simple and
fits in about 300 lines of C.
## Example Usage
- [qoiconv.c](https://github.com/phoboslab/qoi/blob/master/qoiconv.c)
converts between png <> qoi
- [qoibench.c](https://github.com/phoboslab/qoi/blob/master/qoibench.c)
a simple wrapper to benchmark stbi, libpng and qoi

509
qoi.h Normal file
View File

@ -0,0 +1,509 @@
/*
QOI - The Quite OK Image format for fast, lossless image compression
Dominic Szablewski - https://phoboslab.org
-- LICENSE: The MIT License(MIT)
Copyright(c) 2021 Dominic Szablewski
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files(the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and / or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions :
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-- About
QOI encodes and decodes images in a lossless format. An encoded QOI image is
usually around 10--30% larger than a decently optimized PNG image.
QOI outperforms simpler PNG encoders in compression ratio and performance. QOI
images are typically 20% smaller than PNGs written with stbi_image but 10%
larger than with libpng. Encoding is 25-50x faster and decoding is 3-4x faster
than stbi_image or libpng.
-- Synopsis
// Define `QOI_IMPLEMENTATION` in *one* C/C++ file before including this
// library to create the implementation.
#define QOI_IMPLEMENTATION
#include "qoi.h"
// Load and decode a QOI image from the file system into a 32bbp RGBA buffer
int width, height;
void *rgba_pixels = qoi_read("image.qoi", &width, &height, 4);
// Encode and store an RGBA buffer to the file system
qoi_write("image_new.qoi", rgba_pixels, width, height, 4);
-- Documentation
This library provides the following functions;
- qoi_read -- read and decode a QOI file
- qoi_decode -- decode the raw bytes of a QOI image from memory
- qoi_write -- encode and write a QOI file
- qoi_encode -- encode an rgba buffer into a QOI image in memory
See the function declaration below for the signature and more information.
If you don't want/need the qoi_read and qoi_write functions, you can define
QOI_NO_STDIO before including this library.
This library uses malloc() and free(). To supply your own malloc implementation
you can define QOI_MALLOC and QOI_FREE before including this library.
-- Data Format
A QOI file has the following header, followed by any number of data "chunks".
struct qoi_header_t {
char [4]; // magic bytes "qoif"
unsigned short width; // image width in pixels
unsigned short height; // image height in pixels
unsigned int size; // number of data bytes following this header
};
The decoder and encoder start with {r: 0, g: 0, b: 0, a: 255} as the previous
pixel value. Pixels are either encoded as
- a run of the previous pixel
- an index into a previously seen pixel
- a difference to the previous pixel value in r,g,b,a
- full r,g,b,a values
A running array[64] of previously seen pixel values is maintained by the encoder
and decoder. Each pixel that is seen by the encoder and decoder is put into this
array at the position (r^g^b^a) % 64. In the encoder, if the pixel value at this
index matches the current pixel, this index position is written to the stream.
Each chunk starts with a 2, 3 or 4 bit tag, followed by a number of data bits.
The bit length of chunks is divisible by 8 - i.e. all chunks are byte aligned.
QOI_INDEX {
u8 tag : 2; // b00
u8 idx : 6; // 6-bit index into the color index array: 0..63
}
QOI_RUN_8 {
u8 tag : 3; // b010
u8 run : 5; // 5-bit run-length repeating the previous pixel: 1..32
}
QOI_RUN16 {
u8 tag : 3; // b011
u16 run : 13; // 13-bit run-length repeating the previous pixel: 33..8224
}
QOI_DIFF8 {
u8 tag : 2; // b10
u8 dr : 2; // 2-bit red channel difference: -1..2
u8 dg : 2; // 2-bit green channel difference: -1..2
u8 db : 2; // 2-bit blue channel difference: -1..2
}
QOI_DIFF16 {
u8 tag : 3; // b110
u8 dr : 5; // 5-bit red channel difference: -15..16
u8 dg : 4; // 4-bit green channel difference: -7.. 8
u8 db : 4; // 4-bit blue channel difference: -7.. 8
}
QOI_DIFF24 {
u8 tag : 4; // b1110
u8 dr : 5; // 5-bit red channel difference: -15..16
u8 dg : 5; // 5-bit green channel difference: -15..16
u8 db : 5; // 5-bit blue channel difference: -15..16
u8 da : 5; // 5-bit alpha channel difference: -15..16
}
QOI_COLOR {
u8 tag : 4; // b1111
u8 has_r: 1; // red byte follows
u8 has_g: 1; // green byte follows
u8 has_b: 1; // blue byte follows
u8 has_a: 1; // alpha byte follows
u8 r; // red value if has_r == 1: 0..255
u8 g; // green value if has_g == 1: 0..255
u8 b; // blue value if has_b == 1: 0..255
u8 a; // alpha value if has_a == 1: 0..255
}
*/
// -----------------------------------------------------------------------------
// Header - Public functions
#ifndef QOI_H
#define QOI_H
#ifdef __cplusplus
extern "C" {
#endif
#ifndef QOI_NO_STDIO
// Encode raw RGB or RGBA pixels into a QOI image write it to the file system.
// w and h denote the the width and height of the pixel data. channels must be
// either 3 for RGB data or 4 for RGBA.
// The function returns 0 on failure (invalid parameters, or fopen or malloc
// failed) or the number of bytes written on success.
int qoi_write(const char *filename, const void *data, int w, int h, int channels);
// Read and decode a QOI image from the file system into either raw RGB
// (channels=3) or RGBA (channels=4) pixel data.
// The function either returns NULL on failure (invalid data, or malloc or fopen
// failed) or a pointer to the decoded pixels. On success out_w and out_h will
// be set to the width and height of the decoded image.
// The returned pixel data should be free()d after use.
void *qoi_read(const char *filename, int *out_w, int *out_h, int channels);
#endif // QOI_NO_STDIO
// Encode raw RGB or RGBA pixels into a QOI image in memory. w and h denote the
// width and height of the pixel data. channels must be either 3 for RGB data
// or 4 for RGBA.
// The function either returns NULL on failure (invalid parameters or malloc
// failed) or a pointer to the encoded data on success. On success the out_len
// is set to the size in bytes of the encoded data.
// The returned qoi data should be free()d after user.
void *qoi_encode(const void *data, int w, int h, int channels, int *out_len);
// Decode a QOI image from memory into either raw RGB (channels=3) or RGBA
// (channels=4) pixel data.
// The function either returns NULL on failure (invalid parameters or malloc
// failed) or a pointer to the decoded pixels. On success out_w and out_h will
// be set to the width and height of the decoded image.
// The returned pixel data should be free()d after use.
void *qoi_decode(const void *data, int size, int *out_w, int *out_h, int channels);
#ifdef __cplusplus
}
#endif
#endif // QOI_H
// -----------------------------------------------------------------------------
// Implementation
#ifdef QOI_IMPLEMENTATION
#include <stdlib.h>
#ifndef QOI_MALLOC
#define QOI_MALLOC(sz) malloc(sz)
#define QOI_FREE(p) free(p)
#endif
#define QOI_INDEX 0x00 // 00xxxxxx
#define QOI_RUN_8 0x40 // 010xxxxx
#define QOI_RUN_16 0x60 // 011xxxxx
#define QOI_DIFF_8 0x80 // 10xxxxxx
#define QOI_DIFF_16 0xc0 // 110xxxxx
#define QOI_DIFF_24 0xe0 // 1110xxxx
#define QOI_COLOR 0xf0 // 1111xxxx
#define QOI_MASK_2 0xc0 // 11000000
#define QOI_MASK_3 0xe0 // 11100000
#define QOI_MASK_4 0xf0 // 11110000
#define QOI_COLOR_HASH(C) (C.rgba.r ^ C.rgba.g ^ C.rgba.b ^ C.rgba.a)
typedef union {
struct { unsigned char r, g, b, a; } rgba;
unsigned int v;
} qoi_rgba_t;
typedef union {
char chars[4];
unsigned int v;
} qoi_magic_t;
typedef struct {
qoi_magic_t magic;
unsigned short width;
unsigned short height;
unsigned int size;
} qoi_header_t;
const static qoi_magic_t qoi_magic = {.chars = {'q','o','i','f'}};
void *qoi_encode(const void *data, int w, int h, int channels, int *out_len) {
if (
data == NULL || out_len == NULL ||
w <= 0 || w >= (1 << 16) ||
h <= 0 || h >= (1 << 16) ||
channels < 3 || channels > 4
) {
return NULL;
}
int max_size = w * h * (channels + 1) + sizeof(qoi_header_t) + 4;
int p = 0;
unsigned char *bytes = QOI_MALLOC(max_size);
if (!bytes) {
return NULL;
}
*(qoi_header_t *)bytes = (qoi_header_t) {
.magic = qoi_magic,
.width = w,
.height = h,
.size = 0 // will be set at the end
};
p += sizeof(qoi_header_t);
const unsigned char *pixels = (const unsigned char *)data;
qoi_rgba_t index[64] = {0};
int run = 0;
qoi_rgba_t px_prev = {.rgba = {.r = 0, .g = 0, .b = 0, .a = 255}};
qoi_rgba_t px = px_prev;
int px_len = w * h * channels;
int px_end = px_len - channels;
for (int px_pos = 0; px_pos < px_len; px_pos += channels) {
if (channels == 4) {
px = *(qoi_rgba_t *)(pixels + px_pos);
}
else {
px.rgba.r = pixels[px_pos];
px.rgba.g = pixels[px_pos+1];
px.rgba.b = pixels[px_pos+2];
}
if (px.v == px_prev.v) {
run++;
}
if (run > 0 && (run == 0x2020 || px.v != px_prev.v || px_pos == px_end)) {
if (run < 33) {
run -= 1;
bytes[p++] = QOI_RUN_8 | run;
}
else {
run -= 33;
bytes[p++] = QOI_RUN_16 | run >> 8;
bytes[p++] = run;
}
run = 0;
}
if (px.v != px_prev.v) {
int index_pos = QOI_COLOR_HASH(px) % 64;
if (index[index_pos].v == px.v) {
bytes[p++] = QOI_INDEX | index_pos;
}
else {
index[index_pos] = px;
int vr = px.rgba.r - px_prev.rgba.r;
int vg = px.rgba.g - px_prev.rgba.g;
int vb = px.rgba.b - px_prev.rgba.b;
int va = px.rgba.a - px_prev.rgba.a;
if (
vr > -16 && vr < 17 && vg > -16 && vg < 17 &&
vb > -16 && vb < 17 && va > -16 && va < 17
) {
if (
va == 0 && vr > -2 && vr < 3 &&
vg > -2 && vg < 3 && vb > -2 && vb < 3
) {
bytes[p++] = QOI_DIFF_8 | ((vr + 1) << 4) | (vg + 1) << 2 | (vb + 1);
}
else if (
va == 0 && vr > -16 && vr < 17 &&
vg > -8 && vg < 9 && vb > -8 && vb < 9
) {
bytes[p++] = QOI_DIFF_16 | (vr + 15);
bytes[p++] = ((vg + 7) << 4) | (vb + 7);
}
else {
bytes[p++] = QOI_DIFF_24 | ((vr + 15) >> 1);
bytes[p++] = ((vr + 15) << 7) | ((vg + 15) << 2) | ((vb + 15) >> 3);
bytes[p++] = ((vb + 15) << 5) | (va + 15);
}
}
else {
bytes[p++] = QOI_COLOR | (vr?8:0)|(vg?4:0)|(vb?2:0)|(va?1:0);
if (vr) { bytes[p++] = px.rgba.r; }
if (vg) { bytes[p++] = px.rgba.g; }
if (vb) { bytes[p++] = px.rgba.b; }
if (va) { bytes[p++] = px.rgba.a; }
}
}
}
px_prev = px;
}
for (int i = 0; i < 4; i++) {
bytes[p++] = 0;
}
((qoi_header_t *)bytes)->size = p - sizeof(qoi_header_t);
*out_len = p;
return bytes;
}
void *qoi_decode(const void *data, int size, int *out_w, int *out_h, int channels) {
if (size < sizeof(qoi_header_t)) {
return NULL;
}
qoi_header_t *header = (qoi_header_t *)data;
if (
channels < 3 || channels > 4 ||
!header->width || !header->height ||
header->size + sizeof(qoi_header_t) != size ||
header->magic.v != qoi_magic.v
) {
return NULL;
}
int px_len = header->width * header->height * channels;
unsigned char *pixels = QOI_MALLOC(px_len);
if (!pixels) {
return NULL;
}
const unsigned char *bytes = (const unsigned char *)data + sizeof(qoi_header_t);
int data_len = header->size;
qoi_rgba_t px = {.rgba = {.r = 0, .g = 0, .b = 0, .a = 255}};
qoi_rgba_t index[64] = {0};
int run = 0;
for (int px_pos = 0, p = 0; px_pos < px_len && p < data_len; px_pos += channels) {
if (run > 0) {
run--;
}
else {
int b1 = bytes[p++];
if ((b1 & QOI_MASK_2) == QOI_INDEX) {
px = index[b1 ^ QOI_INDEX];
}
else if ((b1 & QOI_MASK_3) == QOI_RUN_8) {
run = (b1 & 0x1f);
}
else if ((b1 & QOI_MASK_3) == QOI_RUN_16) {
int b2 = bytes[p++];
run = (((b1 & 0x1f) << 8) | (b2)) + 32;
}
else if ((b1 & QOI_MASK_2) == QOI_DIFF_8) {
px.rgba.r += ((b1 >> 4) & 0x03) - 1;
px.rgba.g += ((b1 >> 2) & 0x03) - 1;
px.rgba.b += ( b1 & 0x03) - 1;
}
else if ((b1 & QOI_MASK_3) == QOI_DIFF_16) {
int b2 = bytes[p++];
px.rgba.r += (b1 & 0x1f) - 15;
px.rgba.g += (b2 >> 4) - 7;
px.rgba.b += (b2 & 0x0f) - 7;
}
else if ((b1 & QOI_MASK_4) == QOI_DIFF_24) {
int b2 = bytes[p++];
int b3 = bytes[p++];
px.rgba.r += (((b1 & 0x0f) << 1) | (b2 >> 7)) - 15;
px.rgba.g += ((b2 & 0x7c) >> 2) - 15;
px.rgba.b += (((b2 & 0x03) << 3) | ((b3 & 0xe0) >> 5)) - 15;
px.rgba.a += (b3 & 0x1f) - 15;
}
else if ((b1 & QOI_MASK_4) == QOI_COLOR) {
if (b1 & 8) { px.rgba.r = bytes[p++]; }
if (b1 & 4) { px.rgba.g = bytes[p++]; }
if (b1 & 2) { px.rgba.b = bytes[p++]; }
if (b1 & 1) { px.rgba.a = bytes[p++]; }
}
index[QOI_COLOR_HASH(px) % 64] = px;
}
if (channels == 4) {
*(qoi_rgba_t*)(pixels + px_pos) = px;
}
else {
pixels[px_pos] = px.rgba.r;
pixels[px_pos+1] = px.rgba.g;
pixels[px_pos+2] = px.rgba.b;
}
}
*out_w = header->width;
*out_h = header->height;
return pixels;
}
#ifndef QOI_NO_STDIO
#include <stdio.h>
int qoi_write(const char *filename, const void *data, int w, int h, int channels) {
int size;
void *encoded = qoi_encode(data, w, h, channels, &size);
if (!encoded) {
return 0;
}
FILE *f = fopen(filename, "wb");
if (!f) {
QOI_FREE(encoded);
return 0;
}
fwrite(encoded, 1, size, f);
fclose(f);
QOI_FREE(encoded);
return size;
}
void *qoi_read(const char *filename, int *out_w, int *out_h, int channels) {
FILE *f = fopen(filename, "rb");
if (!f) {
return NULL;
}
fseek(f, 0, SEEK_END);
int size = ftell(f);
fseek(f, 0, SEEK_SET);
void *data = QOI_MALLOC(size);
if (!data) {
return NULL;
}
int bytes_read = fread(data, 1, size, f);
fclose(f);
void *pixels = qoi_decode(data, bytes_read, out_w, out_h, channels);
QOI_FREE(data);
return pixels;
}
#endif // QOI_NO_STDIO
#endif // QOI_IMPLEMENTATION

512
qoibench.c Normal file
View File

@ -0,0 +1,512 @@
/*
Simple benchmark suite for png, stbi and qoi
Requires libpng, "stb_image.h" and "stb_image_write.h"
Compile with:
gcc qoibench.c -std=gnu99 -O3 -o qoibench
Dominic Szablewski - https://phoboslab.org
-- LICENSE: The MIT License(MIT)
Copyright(c) 2021 Dominic Szablewski
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files(the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and / or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions :
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#include <stdio.h>
#include <dirent.h>
#include <png.h>
#define STB_IMAGE_IMPLEMENTATION
#define STBI_ONLY_PNG
#define STBI_NO_LINEAR
#include "stb_image.h"
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"
#define QOI_IMPLEMENTATION
#include "qoi.h"
// -----------------------------------------------------------------------------
// Cross platform high resolution timer
// From https://gist.github.com/ForeverZer0/0a4f80fc02b96e19380ebb7a3debbee5
#include <stdint.h>
#if defined(__linux)
#define HAVE_POSIX_TIMER
#include <time.h>
#ifdef CLOCK_MONOTONIC
#define CLOCKID CLOCK_MONOTONIC
#else
#define CLOCKID CLOCK_REALTIME
#endif
#elif defined(__APPLE__)
#define HAVE_MACH_TIMER
#include <mach/mach_time.h>
#elif defined(_WIN32)
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#endif
static uint64_t ns() {
static uint64_t is_init = 0;
#if defined(__APPLE__)
static mach_timebase_info_data_t info;
if (0 == is_init) {
mach_timebase_info(&info);
is_init = 1;
}
uint64_t now;
now = mach_absolute_time();
now *= info.numer;
now /= info.denom;
return now;
#elif defined(__linux)
static struct timespec linux_rate;
if (0 == is_init) {
clock_getres(CLOCKID, &linux_rate);
is_init = 1;
}
uint64_t now;
struct timespec spec;
clock_gettime(CLOCKID, &spec);
now = spec.tv_sec * 1.0e9 + spec.tv_nsec;
return now;
#elif defined(_WIN32)
static LARGE_INTEGER win_frequency;
if (0 == is_init) {
QueryPerformanceFrequency(&win_frequency);
is_init = 1;
}
LARGE_INTEGER now;
QueryPerformanceCounter(&now);
return (uint64_t) ((1e9 * now.QuadPart) / win_frequency.QuadPart);
#endif
}
#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)
#define ERROR(...) printf("abort at line " TOSTRING(__LINE__) ": " __VA_ARGS__); printf("\n"); exit(1)
// -----------------------------------------------------------------------------
// libpng encode/decode wrappers
// Seriously, who thought this was a good abstraction for an API to read/write
// images?
typedef struct {
int size;
int capacity;
unsigned char *data;
} libpng_write_t;
void libpng_encode_callback(png_structp png_ptr, png_bytep data, png_size_t length) {
libpng_write_t *write_data = (libpng_write_t*)png_get_io_ptr(png_ptr);
if (write_data->size + length >= write_data->capacity) {
ERROR("PNG write");
}
memcpy(write_data->data + write_data->size, data, length);
write_data->size += length;
}
void *libpng_encode(void *pixels, int w, int h, int *out_len) {
png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
if (!png) {
ERROR("png_create_write_struct");
}
png_infop info = png_create_info_struct(png);
if (!info) {
ERROR("png_create_info_struct");
}
if (setjmp(png_jmpbuf(png))) {
ERROR("png_jmpbuf");
}
// Output is 8bit depth, RGBA format.
png_set_IHDR(
png,
info,
w, h,
8,
PNG_COLOR_TYPE_RGBA,
PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_DEFAULT,
PNG_FILTER_TYPE_DEFAULT
);
png_bytep row_pointers[h];
for(int y = 0; y < h; y++){
row_pointers[y] = ((unsigned char *)pixels + y * w * 4);
}
libpng_write_t write_data = {
.size = 0,
.capacity = w * h * 4,
.data = malloc(w * h * 4)
};
png_set_rows(png, info, row_pointers);
png_set_write_fn(png, &write_data, libpng_encode_callback, NULL);
png_write_png(png, info, PNG_TRANSFORM_IDENTITY, NULL);
png_destroy_write_struct(&png, &info);
*out_len = write_data.size;
return write_data.data;
}
typedef struct {
int pos;
int size;
unsigned char *data;
} libpng_read_t;
void png_decode_callback(png_structp png, png_bytep data, png_size_t length) {
libpng_read_t *read_data = (libpng_read_t*)png_get_io_ptr(png);
if (read_data->pos + length > read_data->size) {
ERROR("PNG read %d bytes at pos %d (size: %d)", length, read_data->pos, read_data->size);
}
memcpy(data, read_data->data + read_data->pos, length);
read_data->pos += length;
}
void *libpng_decode(void *data, int size, int *out_w, int *out_h) {
png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
if (!png) {
ERROR("png_create_read_struct");
}
png_infop info = png_create_info_struct(png);
if (!info) {
ERROR("png_create_info_struct");
}
libpng_read_t read_data = {
.pos = 0,
.size = size,
.data = data
};
png_set_read_fn(png, &read_data, png_decode_callback);
png_set_sig_bytes(png, 0);
png_read_info(png, info);
png_uint_32 w, h;
int bitDepth, colorType, interlaceType;
png_get_IHDR(png, info, &w, &h, &bitDepth, &colorType, &interlaceType, NULL, NULL);
// 16 bit -> 8 bit
png_set_strip_16(png);
// 1, 2, 4 bit -> 8 bit
if (bitDepth < 8) {
png_set_packing(png);
}
if (colorType & PNG_COLOR_MASK_PALETTE) {
png_set_expand(png);
}
if (!(colorType & PNG_COLOR_MASK_COLOR)) {
png_set_gray_to_rgb(png);
}
// set paletted or RGB images with transparency to full alpha so we get RGBA
if (png_get_valid(png, info, PNG_INFO_tRNS)) {
png_set_tRNS_to_alpha(png);
}
// make sure every pixel has an alpha value
if (!(colorType & PNG_COLOR_MASK_ALPHA)) {
png_set_filler(png, 255, PNG_FILLER_AFTER);
}
png_read_update_info(png, info);
unsigned char* out = malloc(w * h * 4);
*out_w = w;
*out_h = h;
// png_uint_32 rowBytes = png_get_rowbytes(png, info);
png_bytep row_pointers[h];
for (png_uint_32 row = 0; row < h; row++ ) {
row_pointers[row] = (png_bytep)(out + (row * w * 4));
}
png_read_image(png, row_pointers);
png_read_end(png, info);
png_destroy_read_struct( &png, &info, NULL);
return out;
}
// -----------------------------------------------------------------------------
// stb_image encode callback
void stbi_write_callback(void *context, void *data, int size) {
int *encoded_size = (int *)context;
*encoded_size += size;
// In theory we'd need to do another malloc(), memcpy() and free() here to
// be fair to the other decode functions...
}
// -----------------------------------------------------------------------------
// function to load a whole file into memory
void *fload(const char *path, int *out_size) {
FILE *fh = fopen(path, "rb");
if (!fh) {
ERROR("Can't open file");
}
fseek(fh, 0, SEEK_END);
int size = ftell(fh);
fseek(fh, 0, SEEK_SET);
void *buffer = malloc(size);
if (!buffer) {
ERROR("Malloc for %d bytes failed", size);
}
if (!fread(buffer, size, 1, fh)) {
ERROR("Can't read file %s", path);
}
fclose(fh);
*out_size = size;
return buffer;
}
// -----------------------------------------------------------------------------
// benchmark runner
typedef struct {
uint64_t size;
uint64_t encode_time;
uint64_t decode_time;
} benchmark_lib_result_t;
typedef struct {
uint64_t px;
int w;
int h;
benchmark_lib_result_t libpng;
benchmark_lib_result_t stbi;
benchmark_lib_result_t qoi;
} benchmark_result_t;
// Run __VA_ARGS__ a number of times and meassure the time taken. The first
// run is ignored.
#define BENCHMARK_FN(RUNS, AVG_TIME, ...) \
do { \
uint64_t time = 0; \
for (int i = 0; i <= RUNS; i++) { \
uint64_t time_start = ns(); \
__VA_ARGS__ \
uint64_t time_end = ns(); \
if (i > 0) { \
time += time_end - time_start; \
} \
} \
AVG_TIME = time / RUNS; \
} while (0)
benchmark_result_t benchmark_image(const char *path, int runs) {
int encoded_png_size;
int encoded_qoi_size;
int w;
int h;
// Load the encoded PNG, encoded QOI and raw pixels into memory
void *pixels = (void *)stbi_load(path, &w, &h, NULL, 4);
void *encoded_png = fload(path, &encoded_png_size);
void *encoded_qoi = qoi_encode(pixels, w, h, 4, &encoded_qoi_size);
if (!pixels || !encoded_qoi || !encoded_png) {
ERROR("Error decoding %s\n", path);
}
benchmark_result_t res = {0};
res.px = w * h;
res.w = w;
res.h = h;
// Decoding
BENCHMARK_FN(runs, res.libpng.decode_time, {
int dec_w, dec_h;
void *dec_p = libpng_decode(encoded_png, encoded_png_size, &dec_w, &dec_h);
free(dec_p);
});
BENCHMARK_FN(runs, res.stbi.decode_time, {
int dec_w, dec_h, dec_channels;
void *dec_p = stbi_load_from_memory(encoded_png, encoded_png_size, &dec_w, &dec_h, &dec_channels, 4);
free(dec_p);
});
BENCHMARK_FN(runs, res.qoi.decode_time, {
int dec_w, dec_h;
void *dec_p = qoi_decode(encoded_qoi, encoded_qoi_size, &dec_w, &dec_h, 4);
free(dec_p);
});
// Encoding
BENCHMARK_FN(runs, res.libpng.encode_time, {
int enc_size;
void *enc_p = libpng_encode(pixels, w, h, &enc_size);
res.libpng.size = enc_size;
free(enc_p);
});
BENCHMARK_FN(runs, res.stbi.encode_time, {
int enc_size = 0;
stbi_write_png_to_func(stbi_write_callback, &enc_size, w, h, 4, pixels, 0);
res.stbi.size = enc_size;
});
BENCHMARK_FN(runs, res.qoi.encode_time, {
int enc_size;
void *enc_p = qoi_encode(pixels, w, h, 4, &enc_size);
res.qoi.size = enc_size;
free(enc_p);
});
free(pixels);
free(encoded_png);
free(encoded_qoi);
return res;
}
void benchmark_print_result(const char *head, benchmark_result_t res) {
double px = res.px;
printf("## %s size: %dx%d\n", head, res.w, res.h);
printf(" decode ms encode ms decode mpps encode mpps size kb\n");
printf(
"libpng: %8.1f %8.1f %8.2f %8.2f %8d\n",
(double)res.libpng.decode_time/1000000.0,
(double)res.libpng.encode_time/1000000.0,
(res.libpng.decode_time > 0 ? px / ((double)res.libpng.decode_time/1000.0) : 0),
(res.libpng.encode_time > 0 ? px / ((double)res.libpng.encode_time/1000.0) : 0),
res.libpng.size/1024
);
printf(
"stbi: %8.1f %8.1f %8.2f %8.2f %8d\n",
(double)res.stbi.decode_time/1000000.0,
(double)res.stbi.encode_time/1000000.0,
(res.stbi.decode_time > 0 ? px / ((double)res.stbi.decode_time/1000.0) : 0),
(res.stbi.encode_time > 0 ? px / ((double)res.stbi.encode_time/1000.0) : 0),
res.stbi.size/1024
);
printf(
"qoi: %8.1f %8.1f %8.2f %8.2f %8d\n",
(double)res.qoi.decode_time/1000000.0,
(double)res.qoi.encode_time/1000000.0,
(res.qoi.decode_time > 0 ? px / ((double)res.qoi.decode_time/1000.0) : 0),
(res.qoi.encode_time > 0 ? px / ((double)res.qoi.encode_time/1000.0) : 0),
res.qoi.size/1024
);
printf("\n");
}
int main(int argc, char **argv) {
if (argc < 3) {
printf("Usage: qoibench <iterations> <directory>\n");
printf("Example: qoibench 10 images/textures/\n");
exit(1);
}
float total_percentage = 0;
int total_size = 0;
benchmark_result_t totals = {0};
int runs = atoi(argv[1]);
DIR *dir = opendir(argv[2]);
if (runs <=0) {
runs = 1;
}
if (!dir) {
ERROR("Couldn't open directory %s", argv[2]);
}
printf("## Benchmarking %s/*.png -- %d runs\n\n", argv[2], runs);
struct dirent *file;
int i;
for (i = 0; dir && (file = readdir(dir)) != NULL; i++) {
if (strcmp(file->d_name + strlen(file->d_name) - 4, ".png") != 0) {
continue;
}
char *file_path = malloc(strlen(file->d_name) + strlen(argv[2])+8);
sprintf(file_path, "%s/%s", argv[2], file->d_name);
benchmark_result_t res = benchmark_image(file_path, runs);
benchmark_print_result(file_path, res);
free(file_path);
totals.px += res.px;
totals.libpng.encode_time += res.libpng.encode_time;
totals.libpng.decode_time += res.libpng.decode_time;
totals.libpng.size += res.libpng.size;
totals.stbi.encode_time += res.stbi.encode_time;
totals.stbi.decode_time += res.stbi.decode_time;
totals.stbi.size += res.stbi.size;
totals.qoi.encode_time += res.qoi.encode_time;
totals.qoi.decode_time += res.qoi.decode_time;
totals.qoi.size += res.qoi.size;
}
closedir(dir);
totals.px /= i;
totals.libpng.encode_time /= i;
totals.libpng.decode_time /= i;
totals.libpng.size /= i;
totals.stbi.encode_time /= i;
totals.stbi.decode_time /= i;
totals.stbi.size /= i;
totals.qoi.encode_time /= i;
totals.qoi.decode_time /= i;
totals.qoi.size /= i;
benchmark_print_result("Totals (AVG)", totals);
return 0;
}

86
qoiconv.c Normal file
View File

@ -0,0 +1,86 @@
/*
Command line tool to convert between png <> qoi format
Requires "stb_image.h" and "stb_image_write.h"
Compile with:
gcc qoiconv.c -std=c99 -O3 -o qoiconv
Dominic Szablewski - https://phoboslab.org
-- LICENSE: The MIT License(MIT)
Copyright(c) 2021 Dominic Szablewski
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files(the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and / or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions :
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#define STB_IMAGE_IMPLEMENTATION
#define STBI_ONLY_PNG
#define STBI_NO_LINEAR
#include "stb_image.h"
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"
#define QOI_IMPLEMENTATION
#include "qoi.h"
#define STR_ENDS_WITH(S, E) (strcmp(S + strlen(S) - (sizeof(E)-1), E) == 0)
int main(int argc, char **argv) {
if (argc < 3) {
printf("Usage: qoiconv infile outfile\n");
printf("Examples:\n");
printf(" qoiconv image.png image.qoi\n");
printf(" qoiconv image.qoi image.png\n");
exit(1);
}
void *pixels = NULL;
int w, h;
if (STR_ENDS_WITH(argv[1], ".png")) {
pixels = (void *)stbi_load(argv[1], &w, &h, NULL, 4);
}
else if (STR_ENDS_WITH(argv[1], ".qoi")) {
pixels = qoi_read(argv[1], &w, &h, 4);
}
if (pixels == NULL) {
printf("Couldn't load/decode %s\n", argv[1]);
exit(1);
}
int encoded = 0;
if (STR_ENDS_WITH(argv[2], ".png")) {
encoded = stbi_write_png(argv[2], w, h, 4, pixels, 0);
}
else if (STR_ENDS_WITH(argv[2], ".qoi")) {
encoded = qoi_write(argv[2], pixels, w, h, 4);
}
if (!encoded) {
printf("Couldn't write/encode %s\n", argv[2]);
exit(1);
}
return 0;
}