Skip to content

Commit

Permalink
Add WebP_Lossy2 to full benchmarks
Browse files Browse the repository at this point in the history
Also add note about MarcioPais/SIF

Updates #2
  • Loading branch information
nigeltao committed Dec 5, 2022
1 parent 5146b2f commit 5671f58
Show file tree
Hide file tree
Showing 8 changed files with 145 additions and 64 deletions.
34 changes: 29 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# QOIR: a Simple, Lossless Image File Format

QOIR (pronounced like "choir") is simple, lossless image file format that is
QOIR (pronounced like "choir") is a simple, lossless image file format that is
very fast to encode and decode while achieving compression ratios roughly
comparable to PNG.

**WARNING: THIS FORMAT IS EXPERIMENTAL AND SUBJECT TO INCOMPATIBLE CHANGES.**

It was inspired by the [QOI image file format](https://qoiformat.org/),
building on it in a number of ways:

Expand Down Expand Up @@ -124,6 +126,14 @@ on the official zstd (or lz4) library.
The "compression ratio" numbers simply take the benchmark suite PNG images "as
is" without re-encoding.

Here's a chart of (single-threaded) relative decode speed versus relative
compression ratio, from the table above. Upwards and rightwards is better. It's
not that encoding speed, code complexity, additional dependencies, multi-thread
friendliness and compatibility with other software aren't important. They're
just not in this chart.

![RelDecSpeed vs RelCmpRatio](./doc/reldecspeed_vs_relcmpratio.png)


### Lossy Benchmarks

Expand All @@ -136,12 +146,17 @@ again, there are trade-offs.
QOIR_Lossy 0.641 RelCmpRatio 0.903 RelEncSpeed 0.731 RelDecSpeed (1)
JXL_Lossy/l3 0.440 RelCmpRatio 0.051 RelEncSpeed 0.091 RelDecSpeed
JXL_Lossy/l7 0.305 RelCmpRatio 0.013 RelEncSpeed 0.070 RelDecSpeed
LZ4PNG_Lossy 1.095 RelCmpRatio 0.945 RelEncSpeed 1.251 RelDecSpeed (3)
LZ4PNG_Lossy2 1.095 RelCmpRatio 0.945 RelEncSpeed 1.251 RelDecSpeed (5), (3)
WebP_Lossy 0.084 RelCmpRatio 0.065 RelEncSpeed 0.453 RelDecSpeed
ZPNG_Lossy 0.645 RelCmpRatio 0.674 RelEncSpeed 0.898 RelDecSpeed (3)
WebP_Lossy2 0.443 RelCmpRatio 0.015 RelEncSpeed 0.435 RelDecSpeed (5)
ZPNG_Lossy2 0.645 RelCmpRatio 0.674 RelEncSpeed 0.898 RelDecSpeed (5), (3)
```

Lossy encoders (other than QOIR and ZPNG) use the respective libraries' default
(5), the Lossy2 suffix, means that the images are encoded losslessly (even
though e.g. WebP does have its own lossy format) but after applying QOIR's
lossiness=2 quantization, reducing each pixel from 8 to 6 bits per channel.

Other lossy encoders (other than QOIR) use the respective libraries' default
options. Different size / speed / quality trade-offs may be achievable with
other options.

Expand Down Expand Up @@ -223,6 +238,9 @@ channel, which ImageZero cannot represent.

JPEG wasn't measured, for the same "cannot represent alpha" reason.

[MarcioPais/SIF](https://github.com/MarcioPais/SIF), "Simple Image Format",
wasn't measured, for the same "cannot represent alpha" reason.

PNG/libspng and PNG/lodepng weren't measured. They are presumably roughly
comparable, [within a factor of
2](https://nigeltao.github.io/blog/2021/fastest-safest-png-decoder.html#appendix-benchmark-numbers),
Expand All @@ -234,6 +252,12 @@ to PNG/libpng and PNG/stb.
The LZ4 block compression implementation in this repository is available as a
stand-alone [Single File LZ4 C Library](https://github.com/nigeltao/sflz4).

You can instead subset just the `lz4.c` and `lz4.h` files from the [official
lz4 library](https://github.com/lz4/lz4/tree/dev/lib), if you don't mind having
two files.

[smalllz4](https://create.stephan-brumme.com/smallz4/) is another alternative.


## License

Expand All @@ -242,4 +266,4 @@ Apache 2. See the [LICENSE](LICENSE) file for details.

---

Updated on November 2022.
Updated on December 2022.
10 changes: 8 additions & 2 deletions adapter/all_adapters.h
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ my_encode_lz4png_lossless( //
qoir_pixel_buffer* src_pixbuf);

qoir_encode_result //
my_encode_lz4png_lossy( //
my_encode_lz4png_lossy2( //
const uint8_t* png_ptr, //
const size_t png_len, //
qoir_pixel_buffer* src_pixbuf);
Expand Down Expand Up @@ -169,14 +169,20 @@ my_encode_webp_lossy( //
const size_t png_len, //
qoir_pixel_buffer* src_pixbuf);

qoir_encode_result //
my_encode_webp_lossy2( //
const uint8_t* png_ptr, //
const size_t png_len, //
qoir_pixel_buffer* src_pixbuf);

qoir_encode_result //
my_encode_zpng_lossless( //
const uint8_t* png_ptr, //
const size_t png_len, //
qoir_pixel_buffer* src_pixbuf);

qoir_encode_result //
my_encode_zpng_lossy( //
my_encode_zpng_lossy2( //
const uint8_t* png_ptr, //
const size_t png_len, //
qoir_pixel_buffer* src_pixbuf);
Expand Down
6 changes: 3 additions & 3 deletions adapter/lz4png_adapter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ my_encode_lz4png( //
const uint8_t* png_ptr, //
const size_t png_len, //
qoir_pixel_buffer* src_pixbuf, //
bool lossy, //
bool lossy2, //
bool no_filter) {
unsigned int num_channels;
switch (src_pixbuf->pixcfg.pixfmt) {
Expand Down Expand Up @@ -162,7 +162,7 @@ my_encode_lz4png( //
const size_t n = static_cast<size_t>(num_pixels);

uint8_t* lossy_data = nullptr;
if (lossy) {
if (lossy2) {
lossy_data = static_cast<uint8_t*>(malloc(n));
if (!lossy_data) {
qoir_encode_result fail_result = {0};
Expand Down Expand Up @@ -219,7 +219,7 @@ my_encode_lz4png_lossless( //
}

qoir_encode_result //
my_encode_lz4png_lossy( //
my_encode_lz4png_lossy2( //
const uint8_t* png_ptr, //
const size_t png_len, //
qoir_pixel_buffer* src_pixbuf) {
Expand Down
73 changes: 54 additions & 19 deletions adapter/webp_adapter.c
Original file line number Diff line number Diff line change
Expand Up @@ -89,34 +89,57 @@ typedef size_t //
int stride, //
uint8_t** output);

static qoir_encode_result //
my_encode_webp( //
const uint8_t* png_ptr, //
const size_t png_len, //
qoir_pixel_buffer* src_pixbuf, //
webp_encode_func encode_rgb_func, //
webp_encode_func encode_rgba_func) {
static qoir_encode_result //
my_encode_webp( //
const uint8_t* png_ptr, //
const size_t png_len, //
qoir_pixel_buffer* src_pixbuf, //
webp_encode_func encode_rgb_func, //
webp_encode_func encode_rgba_func, //
bool lossy2) {
// The WebP file format cannot represent an image dimension greater than ((1
// << 14) - 1) = 16383, or 16384 for lossless. Use PNG/libpng instead.
if ((src_pixbuf->pixcfg.width_in_pixels > 16383) ||
(src_pixbuf->pixcfg.height_in_pixels > 16383)) {
const uint32_t w = src_pixbuf->pixcfg.width_in_pixels;
const uint32_t h = src_pixbuf->pixcfg.height_in_pixels;
if ((w > 16383) || (h > 16383)) {
return my_encode_png_libpng(png_ptr, png_len, src_pixbuf);
}

void* to_free = NULL;
const uint8_t* src_ptr = src_pixbuf->data;
size_t src_stride = src_pixbuf->stride_in_bytes;
if (lossy2) {
uint32_t bytes_per_pixel =
qoir_pixel_format__bytes_per_pixel(src_pixbuf->pixcfg.pixfmt);
uint64_t len = w * h * (uint64_t)bytes_per_pixel;
uint8_t* ptr = (len <= SIZE_MAX) ? (uint8_t*)malloc(len) : NULL;
if (!ptr) {
qoir_encode_result fail_result = {0};
fail_result.status_message = "#my_encode_webp: out of memory";
return fail_result;
}
size_t stride = w * bytes_per_pixel;
for (uint32_t y = 0; y < h; y++) {
const uint8_t* s = src_ptr + (y * src_stride);
uint8_t* d = ptr + (y * stride);
for (uint32_t i = 0; i < (w * bytes_per_pixel); i++) {
uint8_t value = (*s++) >> 2;
*d++ = (value << 2) | (value >> 4);
}
}
to_free = ptr;
src_ptr = ptr;
src_stride = w * bytes_per_pixel;
}

uint8_t* tmp_ptr = NULL;
size_t dst_len = 0;
switch (src_pixbuf->pixcfg.pixfmt) {
case QOIR_PIXEL_FORMAT__RGB:
dst_len = (*encode_rgb_func)(src_pixbuf->data,
src_pixbuf->pixcfg.width_in_pixels,
src_pixbuf->pixcfg.height_in_pixels,
src_pixbuf->stride_in_bytes, &tmp_ptr);
dst_len = (*encode_rgb_func)(src_ptr, w, h, src_stride, &tmp_ptr);
break;
case QOIR_PIXEL_FORMAT__RGBA_NONPREMUL:
dst_len = (*encode_rgba_func)(src_pixbuf->data,
src_pixbuf->pixcfg.width_in_pixels,
src_pixbuf->pixcfg.height_in_pixels,
src_pixbuf->stride_in_bytes, &tmp_ptr);
dst_len = (*encode_rgba_func)(src_ptr, w, h, src_stride, &tmp_ptr);
break;
default: {
qoir_encode_result fail_result = {0};
Expand All @@ -125,6 +148,7 @@ my_encode_webp( //
}
}
if (!dst_len) {
free(to_free);
qoir_encode_result fail_result = {0};
fail_result.status_message =
"#my_encode_webp: WebPEncodeLosslessEtc failed";
Expand All @@ -134,12 +158,14 @@ my_encode_webp( //
uint8_t* dst_ptr = (uint8_t*)malloc(dst_len);
if (!dst_ptr) {
WebPFree(tmp_ptr);
free(to_free);
qoir_encode_result fail_result = {0};
fail_result.status_message = "#my_encode_webp: out of memory";
return fail_result;
}
memcpy(dst_ptr, tmp_ptr, dst_len);
WebPFree(tmp_ptr);
free(to_free);

qoir_encode_result result = {0};
result.owned_memory = dst_ptr;
Expand Down Expand Up @@ -178,7 +204,7 @@ my_encode_webp_lossless( //
const size_t png_len, //
qoir_pixel_buffer* src_pixbuf) {
return my_encode_webp(png_ptr, png_len, src_pixbuf, WebPEncodeLosslessRGB,
WebPEncodeLosslessRGBA);
WebPEncodeLosslessRGBA, false);
}

qoir_encode_result //
Expand All @@ -187,7 +213,16 @@ my_encode_webp_lossy( //
const size_t png_len, //
qoir_pixel_buffer* src_pixbuf) {
return my_encode_webp(png_ptr, png_len, src_pixbuf, my_WebPEncodeRGB,
my_WebPEncodeRGBA);
my_WebPEncodeRGBA, false);
}

qoir_encode_result //
my_encode_webp_lossy2( //
const uint8_t* png_ptr, //
const size_t png_len, //
qoir_pixel_buffer* src_pixbuf) {
return my_encode_webp(png_ptr, png_len, src_pixbuf, WebPEncodeLosslessRGB,
WebPEncodeLosslessRGBA, true);
}

#ifdef __cplusplus
Expand Down
6 changes: 3 additions & 3 deletions adapter/zpng_adapter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ my_encode_zpng( //
const uint8_t* png_ptr, //
const size_t png_len, //
qoir_pixel_buffer* src_pixbuf, //
bool lossy, //
bool lossy2, //
bool no_filter) {
unsigned int num_channels;
switch (src_pixbuf->pixcfg.pixfmt) {
Expand Down Expand Up @@ -90,7 +90,7 @@ my_encode_zpng( //
const size_t n = static_cast<size_t>(num_pixels);

uint8_t* lossy_data = nullptr;
if (lossy) {
if (lossy2) {
lossy_data = static_cast<uint8_t*>(malloc(n));
if (!lossy_data) {
qoir_encode_result fail_result = {0};
Expand Down Expand Up @@ -147,7 +147,7 @@ my_encode_zpng_lossless( //
}

qoir_encode_result //
my_encode_zpng_lossy( //
my_encode_zpng_lossy2( //
const uint8_t* png_ptr, //
const size_t png_len, //
qoir_pixel_buffer* src_pixbuf) {
Expand Down
Loading

0 comments on commit 5671f58

Please sign in to comment.