commit 3678301916cce063f95c3f525d82e21c2a382cd6 Author: Green Sky Date: Wed Mar 6 01:04:41 2024 +0100 Squashed 'external/qoi/qoi/' content from commit 30d15d79b7 git-subtree-dir: external/qoi/qoi git-subtree-split: 30d15d79b7726b977cd889151cc5cd6b17742f8f diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9234e61 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +images/ +stb_image.h +stb_image_write.h +qoibench +qoiconv diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..aaaa5f7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4dc4f9a --- /dev/null +++ b/Makefile @@ -0,0 +1,22 @@ +CC ?= gcc +CFLAGS_BENCH ?= -std=gnu99 -O3 +LFLAGS_BENCH ?= -lpng +CFLAGS_CONV ?= -std=c99 -O3 + +TARGET_BENCH ?= qoibench +TARGET_CONV ?= qoiconv + +all: $(TARGET_BENCH) $(TARGET_CONV) + +bench: $(TARGET_BENCH) + +$(TARGET_BENCH):$(TARGET_BENCH).c + $(CC) $(CFLAGS_BENCH) $(CFLAGS) $(TARGET_BENCH).c -o $(TARGET_BENCH) $(LFLAGS_BENCH) + +conv: $(TARGET_CONV) +$(TARGET_CONV):$(TARGET_CONV).c + $(CC) $(CFLAGS_CONV) $(CFLAGS) $(TARGET_CONV).c -o $(TARGET_CONV) + +.PHONY: clean +clean: + $(RM) $(TARGET_BENCH) $(TARGET_CONV) diff --git a/README.md b/README.md new file mode 100644 index 0000000..3bb9dcb --- /dev/null +++ b/README.md @@ -0,0 +1,179 @@ +![QOI Logo](https://qoiformat.org/qoi-logo.svg) + +# 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 and format specification. + +More info at https://qoiformat.org + + +## 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 + + +## MIME Type, File Extension + +The recommended MIME type for QOI images is `image/qoi`. While QOI is not yet +officially registered with IANA, I believe QOI has found enough adoption to +prevent any future image format from choosing the same name, thus making a +MIME type collision highly unlikely ([see #167](https://github.com/phoboslab/qoi/issues/167)). + +The recommended file extension for QOI images is `.qoi` + + +## Limitations + +The QOI file format allows for huge images with up to 18 exa-pixels. A streaming +en-/decoder can handle these with minimal RAM requirements, assuming there is +enough storage space. + +This particular implementation of QOI however is limited to images with a +maximum size of 400 million pixels. It will safely refuse to en-/decode anything +larger than that. This is not a streaming en-/decoder. It loads the whole image +file into RAM before doing any work and is not extensively optimized for +performance (but it's still very fast). + +If this is a limitation for your use case, please look into any of the other +implementations listed below. + + +## Improvements, New Versions and Contributing + +The QOI format has been finalized. It was a conscious decision to **not** have a +version number in the file header. If you have a working QOI implementation today, +you can rest assured that it will be compatible with all QOI files tomorrow. + +There are a lot of interesting ideas for a successor of QOI, but none of these will +be implemented here. That doesn't mean you shouldn't experiment with QOI, but please +be aware that pull requests that change the format will not be accepted. + +Likewise, pull requests for performance improvements will probably not be accepted +either, as this "reference implementation" tries to be as easy to read as possible. + + +## Tools + +- [floooh/qoiview](https://github.com/floooh/qoiview) - native QOI viewer +- [pfusik/qoi-fu](https://github.com/pfusik/qoi-fu/releases) - QOI Plugin installer for Windows Explorer, Finder, GNOME, GIMP, Paint.NET and XnView +- [iOrange/QoiFileTypeNet](https://github.com/iOrange/QoiFileTypeNet/releases) - QOI Plugin for Paint.NET +- [iOrange/QOIThumbnailProvider](https://github.com/iOrange/QOIThumbnailProvider) - Add thumbnails for QOI images in Windows Explorer +- [Tom94/tev](https://github.com/Tom94/tev) - another native QOI viewer (allows pixel peeping and comparison with other image formats) +- [qoiconverterx](https://apps.apple.com/br/app/qoiconverterx/id1602159820) QOI <=> PNG converter available on the Mac App Store +- [kaetemi/qoi-ma](https://github.com/kaetemi/qoi-max) - QOI Bitmap I/O Plugin for 3ds Max +- [rtexviewer](https://raylibtech.itch.io/rtexviewer) - texture viewer, supports QOI +- [rtexpacker](https://raylibtech.itch.io/rtexpacker) - texture packer, supports QOI +- [DmitriySalnikov/godot_qoi](https://github.com/DmitriySalnikov/godot_qoi) - QOI GDNative Addon for Godot Engine +- [dan9er/farbfeld-convert-qoi](https://gitlab.com/dan9er/farbfeld-convert-qoi) - QOI <=> farbfeld converter +- [LTMX/Unity.QOI](https://github.com/LTMX/Unity.QOI) - QOI Importer and Exporter for the Unity3D Game Engine +- [Ben1138/unity-qoi](https://github.com/Ben1138/unity-qoi) - QOI Importer(only) support for the Unity3D Game Engine +- [xiaozhuai/jetbrains-qo](https://github.com/xiaozhuai/jetbrains-qoi) - [QOI Support](https://plugins.jetbrains.com/plugin/19352-qoi-support) for Jetbrains' IDE. +- [serge-ivamov/QOIql](https://github.com/serge-ivamov/QOIql) - MacOS QuickLook plugin for QOI +- [tobozo/kde-thumbnailer-qoi](https://github.com/tobozo/kde-thumbnailer-qoi) - QOI Thumbnailer for KDE +- [walksanatora/qoi-thumbnailer-nemo](https://github.com/walksanatora/qoi-thumbnailer-nemo) - QOI Thumbnailer for Nemo +- [hzeller/timg](https://github.com/hzeller/timg) - a terminal image viewer with QOI support +- [LuisAlfredo92/Super-QOI-converter](https://github.com/LuisAlfredo92/Super-QOI-converter "LuisAlfredo92/Super-QOI-converter") - A program to convert JPG, JPEG, BMP, and PNG to QOI + - [Console version](https://github.com/LuisAlfredo92/Super-QOI-converter-Console- "Console version"): Available for Linux, OSX and Windows + - [GUI version](https://github.com/LuisAlfredo92/Super-QOI-converter-GUI- "GUI version"): Available only for windows +- [tacent view](https://github.com/bluescan/tacentview) - Image and texture viewer, supports QOI +- [colemanrgb/qoi2spr](https://github.com/colemanrgb/qoi2spr) - A variety of applications for decoding and encoding of QOI images on [RISC OS](https://www.riscosopen.org/) + +## Implementations & Bindings of QOI + +- [pfusik/qoi-fu](https://github.com/pfusik/qoi-fu) - Fusion, transpiling to C, C++, C#, D, Java, JavaScript, Python, Swift and TypeScript +- [kodonnell/qoi](https://github.com/kodonnell/qoi) - Python +- [JaffaKetchup/dqoi](https://github.com/JaffaKetchup/dqoi) - Dart, with Flutter support +- [Cr4xy/lua-qoi](https://github.com/Cr4xy/lua-qoi) - Lua +- [superzazu/SDL_QOI](https://github.com/superzazu/SDL_QOI) - C, SDL2 bindings +- [saharNooby/qoi-java](https://github.com/saharNooby/qoi-java) - Java +- [MasterQ32/zig-qoi](https://github.com/MasterQ32/zig-qoi) - Zig +- [rbino/qoix](https://github.com/rbino/qoix) - Elixir +- [NUlliiON/QoiSharp](https://github.com/NUlliiON/QoiSharp) - C# +- [aldanor/qoi-rust](https://github.com/aldanor/qoi-rust) - Rust +- [zakarumych/rapid-qoi](https://github.com/zakarumych/rapid-qoi) - Rust +- [takeyourhatoff/qoi](https://github.com/takeyourhatoff/qoi) - Go +- [DosWorld/pasqoi](https://github.com/DosWorld/pasqoi) - Pascal +- [elihwyma/Swift-QOI](https://github.com/elihwyma/Swift-QOI) - Swift +- [xfmoulet/qoi](https://github.com/xfmoulet/qoi) - Go +- [erratique.ch/qoic](https://erratique.ch/software/qoic) - OCaml +- [arian/go-qoi](https://github.com/arian/go-qoi) - Go +- [kchapelier/qoijs](https://github.com/kchapelier/qoijs) - JavaScript +- [KristofferC/QOI.jl](https://github.com/KristofferC/QOI.jl) - Julia +- [shadowMitia/libqoi](https://github.com/shadowMitia/libqoi) - C++ +- [MKCG/php-qoi](https://github.com/MKCG/php-qoi) - PHP +- [LightHouseSoftware/qoiformats](https://github.com/LightHouseSoftware/qoiformats) - D +- [mhoward540/qoi-nim](https://github.com/mhoward540/qoi-nim) - Nim +- [wx257osn2/qoixx](https://github.com/wx257osn2/qoixx) - C++ +- [Tiefseetauchner/lr-paint](https://github.com/Tiefseetauchner/lr-paint) - Processing +- [amstan/qoi-fpga](https://github.com/amstan/qoi-fpga) - FPGA: verilog +- [musabkilic/qoi-decoder](https://github.com/musabkilic/qoi-decoder) - Python +- [mathpn/py-qoi](https://github.com/mathpn/py-qoi) - Python +- [JohannesFriedrich/qoi4R](https://github.com/JohannesFriedrich/qoi4R) - R +- [shraiwi/mini-qoi](https://github.com/shraiwi/mini-qoi) - C, streaming decoder +- [10maurycy10/libqoi/](https://github.com/10maurycy10/libqoi/) - Rust +- [0xd34df00d/hsqoi](https://github.com/0xd34df00d/hsqoi) - Haskell +- [418Coffee/qoi-v](https://github.com/418Coffee/qoi-v) - V +- [Imagine-Programming/QoiImagePlugin](https://github.com/Imagine-Programming/QoiImagePlugin) - PureBasic +- [Fabien-Chouteau/qoi-spark](https://github.com/Fabien-Chouteau/qoi-spark) - Ada/SPARK formally proven +- [mzgreen/qoi-kotlin](https://github.com/mzgreen/qoi-kotlin) - Kotlin Multiplatform +- [Aftersol/Simplified-QOI-Codec](https://github.com/Aftersol/Simplified-QOI-Codec) - C99, encoder and decoder, freestanding +- [AuburnSounds/gamut](https://github.com/AuburnSounds/gamut) - D +- [AngusJohnson/TQoiImage](https://github.com/AngusJohnson/TQoiImage) - Delphi +- [MarkJeronimus/qoi-java-spi](https://github.com/MarkJeronimus/qoi-java-spi) - Java SPI +- [aumouvantsillage/qoi-racket](https://github.com/aumouvantsillage/qoi-racket) - Racket +- [rubikscraft/qoi-stream](https://github.com/rubikscraft/qoi-stream) - C99, one byte at a time streaming encoder and decoder +- [rubikscraft/qoi-img](https://github.com/rubikscraft/qoi-img) - NodeJS typescript, bindings to both [QOIxx](https://github.com/wx257osn2/qoixx) and [qoi-stream](https://github.com/rubikscraft/qoi-stream) +- [grego/hare-qoi](https://git.sr.ht/~grego/hare-qoi) - Hare +- [MrNocole/ZTQOI](https://github.com/MrNocole/ZTQOI) - Objective-C +- [bpanthi977/qoi](https://github.com/bpanthi977/qoi) - Common Lisp +- [Floessie/pam2qoi](https://github.com/Floessie/pam2qoi) - C++ +- [SpeckyYT/spwn-qoi](https://github.com/SpeckyYT/spwn-qoi) - SPWN +- [n00bmind/qoi](https://github.com/n00bmind/qoi) - Jai +- [SixLabors/ImageSharp](https://github.com/SixLabors/ImageSharp) - C# image proccesing library +- [zertovitch/gid](https://github.com/zertovitch/gid) - Ada +- [nazrin/lil](https://codeberg.org/nazrin/lil) - Lua image library + +## QOI Support in Other Software + +- [Amiga OS QOI datatype](https://github.com/dgaw/qoi-datatype) - adds support for decoding QOI images to the Amiga operating system. +- [SerenityOS](https://github.com/SerenityOS/serenity) - supports decoding QOI system wide through a custom [cpp implementation in LibGfx](https://github.com/SerenityOS/serenity/blob/master/Userland/Libraries/LibGfx/QOILoader.h) +- [Raylib](https://github.com/raysan5/raylib) - supports decoding and encoding QOI textures through its [rtextures module](https://github.com/raysan5/raylib/blob/master/src/rtextures.c) +- [Rebol3](https://github.com/Oldes/Rebol3/issues/39) - supports decoding and encoding QOI using a native codec +- [c-ray](https://github.com/vkoskiv/c-ray) - supports QOI natively +- [SAIL](https://sail.software) - image decoding library, supports decoding and encoding QOI images +- [Orx](https://github.com/orx/orx) - 2D game engine, supports QOI natively +- [IrfanView](https://www.irfanview.com) - supports decoding and encoding QOI through its Formats plugin +- [ImageMagick](https://github.com/ImageMagick/ImageMagick) - supports decoding and encoding QOI, since 7.1.0-20 +- [barebox](https://barebox.org) - bootloader, supports decoding QOI images for splash logo, since v2022.03.0 +- [KorGE](https://korge.org) - & KorIM Kotlin 2D game engine and imaging library, supports decoding and encoding QOI natively since 2.7.0 +- [DOjS](https://github.com/SuperIlu/DOjS) - DOS JavaScript Canvas implementation supports loading QOI files +- [XnView MP](https://www.xnview.com/en/xnviewmp/) - supports decoding QOI since 1.00 +- [ffmpeg](https://ffmpeg.org/) - supports decoding and encoding QOI since 5.1 +- [JPEGView](https://github.com/sylikc/jpegview) - lightweight Windows image viewer, supports decoding and encoding of QOI natively, since 1.1.44 +- [darktable](https://github.com/darktable-org/darktable) - photography workflow application and raw developer, supports decoding since 4.4.0 +- [KDE](https://kde.org) - supports decoding and encoding QOI images. Implemented in [KImageFormats](https://invent.kde.org/frameworks/kimageformats) +- [EFL](https://www.enlightenment.org) - supports decoding and encoding QOI images since 1.27. +- [Swingland](https://git.sr.ht/~phlash/swingland) - supports QOI decoding/loading via the `ImageIO` API of this Java Swing reimplemenation for Wayland +- [Imagine](https://www.nyam.pe.kr/dev/imagine/) - supports decoding and encoding QOI images since 1.3.9 +- [Uiua](https://uiua.org) - supports decoding and encoding QOI images since 0.8.0 + +## Packages + +- [AUR](https://aur.archlinux.org/pkgbase/qoi-git/) - system-wide qoi.h, qoiconv and qoibench install as split packages. +- [Debian](https://packages.debian.org/bookworm/source/qoi) - packages for binaries and qoi.h +- [Ubuntu](https://launchpad.net/ubuntu/+source/qoi) - packages for binaries and qoi.h + +Packages for other systems [tracked at Repology](https://repology.org/project/qoi/versions). diff --git a/qoi.h b/qoi.h new file mode 100644 index 0000000..f2800b0 --- /dev/null +++ b/qoi.h @@ -0,0 +1,649 @@ +/* + +Copyright (c) 2021, Dominic Szablewski - https://phoboslab.org +SPDX-License-Identifier: MIT + + +QOI - The "Quite OK Image" format for fast, lossless image compression + +-- About + +QOI encodes and decodes images in a lossless format. Compared to stb_image and +stb_image_write QOI offers 20x-50x faster encoding, 3x-4x faster decoding and +20% better compression. + + +-- Synopsis + +// Define `QOI_IMPLEMENTATION` in *one* C/C++ file before including this +// library to create the implementation. + +#define QOI_IMPLEMENTATION +#include "qoi.h" + +// Encode and store an RGBA buffer to the file system. The qoi_desc describes +// the input pixel data. +qoi_write("image_new.qoi", rgba_pixels, &(qoi_desc){ + .width = 1920, + .height = 1080, + .channels = 4, + .colorspace = QOI_SRGB +}); + +// Load and decode a QOI image from the file system into a 32bbp RGBA buffer. +// The qoi_desc struct will be filled with the width, height, number of channels +// and colorspace read from the file header. +qoi_desc desc; +void *rgba_pixels = qoi_read("image.qoi", &desc, 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. + +This library uses memset() to zero-initialize the index. To supply your own +implementation you can define QOI_ZEROARR before including this library. + + +-- Data Format + +A QOI file has a 14 byte header, followed by any number of data "chunks" and an +8-byte end marker. + +struct qoi_header_t { + char magic[4]; // magic bytes "qoif" + uint32_t width; // image width in pixels (BE) + uint32_t height; // image height in pixels (BE) + uint8_t channels; // 3 = RGB, 4 = RGBA + uint8_t colorspace; // 0 = sRGB with linear alpha, 1 = all channels linear +}; + +Images are encoded row by row, left to right, top to bottom. The decoder and +encoder start with {r: 0, g: 0, b: 0, a: 255} as the previous pixel value. An +image is complete when all pixels specified by width * height have been covered. + +Pixels are encoded as + - a run of the previous pixel + - an index into an array of previously seen pixels + - a difference to the previous pixel value in r,g,b + - full r,g,b or r,g,b,a values + +The color channels are assumed to not be premultiplied with the alpha channel +("un-premultiplied alpha"). + +A running array[64] (zero-initialized) 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 formed by a hash function of +the color value. In the encoder, if the pixel value at the index matches the +current pixel, this index position is written to the stream as QOI_OP_INDEX. +The hash function for the index is: + + index_position = (r * 3 + g * 5 + b * 7 + a * 11) % 64 + +Each chunk starts with a 2- or 8-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. All +values encoded in these data bits have the most significant bit on the left. + +The 8-bit tags have precedence over the 2-bit tags. A decoder must check for the +presence of an 8-bit tag first. + +The byte stream's end is marked with 7 0x00 bytes followed a single 0x01 byte. + + +The possible chunks are: + + +.- QOI_OP_INDEX ----------. +| Byte[0] | +| 7 6 5 4 3 2 1 0 | +|-------+-----------------| +| 0 0 | index | +`-------------------------` +2-bit tag b00 +6-bit index into the color index array: 0..63 + +A valid encoder must not issue 2 or more consecutive QOI_OP_INDEX chunks to the +same index. QOI_OP_RUN should be used instead. + + +.- QOI_OP_DIFF -----------. +| Byte[0] | +| 7 6 5 4 3 2 1 0 | +|-------+-----+-----+-----| +| 0 1 | dr | dg | db | +`-------------------------` +2-bit tag b01 +2-bit red channel difference from the previous pixel between -2..1 +2-bit green channel difference from the previous pixel between -2..1 +2-bit blue channel difference from the previous pixel between -2..1 + +The difference to the current channel values are using a wraparound operation, +so "1 - 2" will result in 255, while "255 + 1" will result in 0. + +Values are stored as unsigned integers with a bias of 2. E.g. -2 is stored as +0 (b00). 1 is stored as 3 (b11). + +The alpha value remains unchanged from the previous pixel. + + +.- QOI_OP_LUMA -------------------------------------. +| Byte[0] | Byte[1] | +| 7 6 5 4 3 2 1 0 | 7 6 5 4 3 2 1 0 | +|-------+-----------------+-------------+-----------| +| 1 0 | green diff | dr - dg | db - dg | +`---------------------------------------------------` +2-bit tag b10 +6-bit green channel difference from the previous pixel -32..31 +4-bit red channel difference minus green channel difference -8..7 +4-bit blue channel difference minus green channel difference -8..7 + +The green channel is used to indicate the general direction of change and is +encoded in 6 bits. The red and blue channels (dr and db) base their diffs off +of the green channel difference and are encoded in 4 bits. I.e.: + dr_dg = (cur_px.r - prev_px.r) - (cur_px.g - prev_px.g) + db_dg = (cur_px.b - prev_px.b) - (cur_px.g - prev_px.g) + +The difference to the current channel values are using a wraparound operation, +so "10 - 13" will result in 253, while "250 + 7" will result in 1. + +Values are stored as unsigned integers with a bias of 32 for the green channel +and a bias of 8 for the red and blue channel. + +The alpha value remains unchanged from the previous pixel. + + +.- QOI_OP_RUN ------------. +| Byte[0] | +| 7 6 5 4 3 2 1 0 | +|-------+-----------------| +| 1 1 | run | +`-------------------------` +2-bit tag b11 +6-bit run-length repeating the previous pixel: 1..62 + +The run-length is stored with a bias of -1. Note that the run-lengths 63 and 64 +(b111110 and b111111) are illegal as they are occupied by the QOI_OP_RGB and +QOI_OP_RGBA tags. + + +.- QOI_OP_RGB ------------------------------------------. +| Byte[0] | Byte[1] | Byte[2] | Byte[3] | +| 7 6 5 4 3 2 1 0 | 7 .. 0 | 7 .. 0 | 7 .. 0 | +|-------------------------+---------+---------+---------| +| 1 1 1 1 1 1 1 0 | red | green | blue | +`-------------------------------------------------------` +8-bit tag b11111110 +8-bit red channel value +8-bit green channel value +8-bit blue channel value + +The alpha value remains unchanged from the previous pixel. + + +.- QOI_OP_RGBA ---------------------------------------------------. +| Byte[0] | Byte[1] | Byte[2] | Byte[3] | Byte[4] | +| 7 6 5 4 3 2 1 0 | 7 .. 0 | 7 .. 0 | 7 .. 0 | 7 .. 0 | +|-------------------------+---------+---------+---------+---------| +| 1 1 1 1 1 1 1 1 | red | green | blue | alpha | +`-----------------------------------------------------------------` +8-bit tag b11111111 +8-bit red channel value +8-bit green channel value +8-bit blue channel value +8-bit alpha channel value + +*/ + + +/* ----------------------------------------------------------------------------- +Header - Public functions */ + +#ifndef QOI_H +#define QOI_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* A pointer to a qoi_desc struct has to be supplied to all of qoi's functions. +It describes either the input format (for qoi_write and qoi_encode), or is +filled with the description read from the file header (for qoi_read and +qoi_decode). + +The colorspace in this qoi_desc is an enum where + 0 = sRGB, i.e. gamma scaled RGB channels and a linear alpha channel + 1 = all channels are linear +You may use the constants QOI_SRGB or QOI_LINEAR. The colorspace is purely +informative. It will be saved to the file header, but does not affect +how chunks are en-/decoded. */ + +#define QOI_SRGB 0 +#define QOI_LINEAR 1 + +typedef struct { + unsigned int width; + unsigned int height; + unsigned char channels; + unsigned char colorspace; +} qoi_desc; + +#ifndef QOI_NO_STDIO + +/* Encode raw RGB or RGBA pixels into a QOI image and write it to the file +system. The qoi_desc struct must be filled with the image width, height, +number of channels (3 = RGB, 4 = RGBA) and the colorspace. + +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, const qoi_desc *desc); + + +/* Read and decode a QOI image from the file system. If channels is 0, the +number of channels from the file header is used. If channels is 3 or 4 the +output format will be forced into this number of channels. + +The function either returns NULL on failure (invalid data, or malloc or fopen +failed) or a pointer to the decoded pixels. On success, the qoi_desc struct +will be filled with the description from the file header. + +The returned pixel data should be free()d after use. */ + +void *qoi_read(const char *filename, qoi_desc *desc, int channels); + +#endif /* QOI_NO_STDIO */ + + +/* Encode raw RGB or RGBA pixels into a QOI image in memory. + +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 use. */ + +void *qoi_encode(const void *data, const qoi_desc *desc, int *out_len); + + +/* Decode a QOI image from memory. + +The function either returns NULL on failure (invalid parameters or malloc +failed) or a pointer to the decoded pixels. On success, the qoi_desc struct +is filled with the description from the file header. + +The returned pixel data should be free()d after use. */ + +void *qoi_decode(const void *data, int size, qoi_desc *desc, int channels); + + +#ifdef __cplusplus +} +#endif +#endif /* QOI_H */ + + +/* ----------------------------------------------------------------------------- +Implementation */ + +#ifdef QOI_IMPLEMENTATION +#include +#include + +#ifndef QOI_MALLOC + #define QOI_MALLOC(sz) malloc(sz) + #define QOI_FREE(p) free(p) +#endif +#ifndef QOI_ZEROARR + #define QOI_ZEROARR(a) memset((a),0,sizeof(a)) +#endif + +#define QOI_OP_INDEX 0x00 /* 00xxxxxx */ +#define QOI_OP_DIFF 0x40 /* 01xxxxxx */ +#define QOI_OP_LUMA 0x80 /* 10xxxxxx */ +#define QOI_OP_RUN 0xc0 /* 11xxxxxx */ +#define QOI_OP_RGB 0xfe /* 11111110 */ +#define QOI_OP_RGBA 0xff /* 11111111 */ + +#define QOI_MASK_2 0xc0 /* 11000000 */ + +#define QOI_COLOR_HASH(C) (C.rgba.r*3 + C.rgba.g*5 + C.rgba.b*7 + C.rgba.a*11) +#define QOI_MAGIC \ + (((unsigned int)'q') << 24 | ((unsigned int)'o') << 16 | \ + ((unsigned int)'i') << 8 | ((unsigned int)'f')) +#define QOI_HEADER_SIZE 14 + +/* 2GB is the max file size that this implementation can safely handle. We guard +against anything larger than that, assuming the worst case with 5 bytes per +pixel, rounded down to a nice clean value. 400 million pixels ought to be +enough for anybody. */ +#define QOI_PIXELS_MAX ((unsigned int)400000000) + +typedef union { + struct { unsigned char r, g, b, a; } rgba; + unsigned int v; +} qoi_rgba_t; + +static const unsigned char qoi_padding[8] = {0,0,0,0,0,0,0,1}; + +static void qoi_write_32(unsigned char *bytes, int *p, unsigned int v) { + bytes[(*p)++] = (0xff000000 & v) >> 24; + bytes[(*p)++] = (0x00ff0000 & v) >> 16; + bytes[(*p)++] = (0x0000ff00 & v) >> 8; + bytes[(*p)++] = (0x000000ff & v); +} + +static unsigned int qoi_read_32(const unsigned char *bytes, int *p) { + unsigned int a = bytes[(*p)++]; + unsigned int b = bytes[(*p)++]; + unsigned int c = bytes[(*p)++]; + unsigned int d = bytes[(*p)++]; + return a << 24 | b << 16 | c << 8 | d; +} + +void *qoi_encode(const void *data, const qoi_desc *desc, int *out_len) { + int i, max_size, p, run; + int px_len, px_end, px_pos, channels; + unsigned char *bytes; + const unsigned char *pixels; + qoi_rgba_t index[64]; + qoi_rgba_t px, px_prev; + + if ( + data == NULL || out_len == NULL || desc == NULL || + desc->width == 0 || desc->height == 0 || + desc->channels < 3 || desc->channels > 4 || + desc->colorspace > 1 || + desc->height >= QOI_PIXELS_MAX / desc->width + ) { + return NULL; + } + + max_size = + desc->width * desc->height * (desc->channels + 1) + + QOI_HEADER_SIZE + sizeof(qoi_padding); + + p = 0; + bytes = (unsigned char *) QOI_MALLOC(max_size); + if (!bytes) { + return NULL; + } + + qoi_write_32(bytes, &p, QOI_MAGIC); + qoi_write_32(bytes, &p, desc->width); + qoi_write_32(bytes, &p, desc->height); + bytes[p++] = desc->channels; + bytes[p++] = desc->colorspace; + + + pixels = (const unsigned char *)data; + + QOI_ZEROARR(index); + + run = 0; + px_prev.rgba.r = 0; + px_prev.rgba.g = 0; + px_prev.rgba.b = 0; + px_prev.rgba.a = 255; + px = px_prev; + + px_len = desc->width * desc->height * desc->channels; + px_end = px_len - desc->channels; + channels = desc->channels; + + for (px_pos = 0; px_pos < px_len; px_pos += channels) { + px.rgba.r = pixels[px_pos + 0]; + px.rgba.g = pixels[px_pos + 1]; + px.rgba.b = pixels[px_pos + 2]; + + if (channels == 4) { + px.rgba.a = pixels[px_pos + 3]; + } + + if (px.v == px_prev.v) { + run++; + if (run == 62 || px_pos == px_end) { + bytes[p++] = QOI_OP_RUN | (run - 1); + run = 0; + } + } + else { + int index_pos; + + if (run > 0) { + bytes[p++] = QOI_OP_RUN | (run - 1); + run = 0; + } + + index_pos = QOI_COLOR_HASH(px) % 64; + + if (index[index_pos].v == px.v) { + bytes[p++] = QOI_OP_INDEX | index_pos; + } + else { + index[index_pos] = px; + + if (px.rgba.a == px_prev.rgba.a) { + signed char vr = px.rgba.r - px_prev.rgba.r; + signed char vg = px.rgba.g - px_prev.rgba.g; + signed char vb = px.rgba.b - px_prev.rgba.b; + + signed char vg_r = vr - vg; + signed char vg_b = vb - vg; + + if ( + vr > -3 && vr < 2 && + vg > -3 && vg < 2 && + vb > -3 && vb < 2 + ) { + bytes[p++] = QOI_OP_DIFF | (vr + 2) << 4 | (vg + 2) << 2 | (vb + 2); + } + else if ( + vg_r > -9 && vg_r < 8 && + vg > -33 && vg < 32 && + vg_b > -9 && vg_b < 8 + ) { + bytes[p++] = QOI_OP_LUMA | (vg + 32); + bytes[p++] = (vg_r + 8) << 4 | (vg_b + 8); + } + else { + bytes[p++] = QOI_OP_RGB; + bytes[p++] = px.rgba.r; + bytes[p++] = px.rgba.g; + bytes[p++] = px.rgba.b; + } + } + else { + bytes[p++] = QOI_OP_RGBA; + bytes[p++] = px.rgba.r; + bytes[p++] = px.rgba.g; + bytes[p++] = px.rgba.b; + bytes[p++] = px.rgba.a; + } + } + } + px_prev = px; + } + + for (i = 0; i < (int)sizeof(qoi_padding); i++) { + bytes[p++] = qoi_padding[i]; + } + + *out_len = p; + return bytes; +} + +void *qoi_decode(const void *data, int size, qoi_desc *desc, int channels) { + const unsigned char *bytes; + unsigned int header_magic; + unsigned char *pixels; + qoi_rgba_t index[64]; + qoi_rgba_t px; + int px_len, chunks_len, px_pos; + int p = 0, run = 0; + + if ( + data == NULL || desc == NULL || + (channels != 0 && channels != 3 && channels != 4) || + size < QOI_HEADER_SIZE + (int)sizeof(qoi_padding) + ) { + return NULL; + } + + bytes = (const unsigned char *)data; + + header_magic = qoi_read_32(bytes, &p); + desc->width = qoi_read_32(bytes, &p); + desc->height = qoi_read_32(bytes, &p); + desc->channels = bytes[p++]; + desc->colorspace = bytes[p++]; + + if ( + desc->width == 0 || desc->height == 0 || + desc->channels < 3 || desc->channels > 4 || + desc->colorspace > 1 || + header_magic != QOI_MAGIC || + desc->height >= QOI_PIXELS_MAX / desc->width + ) { + return NULL; + } + + if (channels == 0) { + channels = desc->channels; + } + + px_len = desc->width * desc->height * channels; + pixels = (unsigned char *) QOI_MALLOC(px_len); + if (!pixels) { + return NULL; + } + + QOI_ZEROARR(index); + px.rgba.r = 0; + px.rgba.g = 0; + px.rgba.b = 0; + px.rgba.a = 255; + + chunks_len = size - (int)sizeof(qoi_padding); + for (px_pos = 0; px_pos < px_len; px_pos += channels) { + if (run > 0) { + run--; + } + else if (p < chunks_len) { + int b1 = bytes[p++]; + + if (b1 == QOI_OP_RGB) { + px.rgba.r = bytes[p++]; + px.rgba.g = bytes[p++]; + px.rgba.b = bytes[p++]; + } + else if (b1 == QOI_OP_RGBA) { + px.rgba.r = bytes[p++]; + px.rgba.g = bytes[p++]; + px.rgba.b = bytes[p++]; + px.rgba.a = bytes[p++]; + } + else if ((b1 & QOI_MASK_2) == QOI_OP_INDEX) { + px = index[b1]; + } + else if ((b1 & QOI_MASK_2) == QOI_OP_DIFF) { + px.rgba.r += ((b1 >> 4) & 0x03) - 2; + px.rgba.g += ((b1 >> 2) & 0x03) - 2; + px.rgba.b += ( b1 & 0x03) - 2; + } + else if ((b1 & QOI_MASK_2) == QOI_OP_LUMA) { + int b2 = bytes[p++]; + int vg = (b1 & 0x3f) - 32; + px.rgba.r += vg - 8 + ((b2 >> 4) & 0x0f); + px.rgba.g += vg; + px.rgba.b += vg - 8 + (b2 & 0x0f); + } + else if ((b1 & QOI_MASK_2) == QOI_OP_RUN) { + run = (b1 & 0x3f); + } + + index[QOI_COLOR_HASH(px) % 64] = px; + } + + pixels[px_pos + 0] = px.rgba.r; + pixels[px_pos + 1] = px.rgba.g; + pixels[px_pos + 2] = px.rgba.b; + + if (channels == 4) { + pixels[px_pos + 3] = px.rgba.a; + } + } + + return pixels; +} + +#ifndef QOI_NO_STDIO +#include + +int qoi_write(const char *filename, const void *data, const qoi_desc *desc) { + FILE *f = fopen(filename, "wb"); + int size, err; + void *encoded; + + if (!f) { + return 0; + } + + encoded = qoi_encode(data, desc, &size); + if (!encoded) { + fclose(f); + return 0; + } + + fwrite(encoded, 1, size, f); + fflush(f); + err = ferror(f); + fclose(f); + + QOI_FREE(encoded); + return err ? 0 : size; +} + +void *qoi_read(const char *filename, qoi_desc *desc, int channels) { + FILE *f = fopen(filename, "rb"); + int size, bytes_read; + void *pixels, *data; + + if (!f) { + return NULL; + } + + fseek(f, 0, SEEK_END); + size = ftell(f); + if (size <= 0 || fseek(f, 0, SEEK_SET) != 0) { + fclose(f); + return NULL; + } + + data = QOI_MALLOC(size); + if (!data) { + fclose(f); + return NULL; + } + + bytes_read = fread(data, 1, size, f); + fclose(f); + pixels = (bytes_read != size) ? NULL : qoi_decode(data, bytes_read, desc, channels); + QOI_FREE(data); + return pixels; +} + +#endif /* QOI_NO_STDIO */ +#endif /* QOI_IMPLEMENTATION */ diff --git a/qoibench.c b/qoibench.c new file mode 100644 index 0000000..e447c5a --- /dev/null +++ b/qoibench.c @@ -0,0 +1,610 @@ +/* + +Copyright (c) 2021, Dominic Szablewski - https://phoboslab.org +SPDX-License-Identifier: MIT + + +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 -lpng -O3 -o qoibench + +*/ + +#include +#include +#include + +#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 +#if defined(__linux) + #define HAVE_POSIX_TIMER + #include + #ifdef CLOCK_MONOTONIC + #define CLOCKID CLOCK_MONOTONIC + #else + #define CLOCKID CLOCK_REALTIME + #endif +#elif defined(__APPLE__) + #define HAVE_MACH_TIMER + #include +#elif defined(_WIN32) + #define WIN32_LEAN_AND_MEAN + #include +#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 channels, 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, + channels == 3 ? PNG_COLOR_TYPE_RGB : 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 * channels); + } + + libpng_write_t write_data = { + .size = 0, + .capacity = w * h * channels, + .data = malloc(w * h * channels) + }; + + 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 %ld 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 png_warning_callback(png_structp png_ptr, png_const_charp warning_msg) { + // Ignore warnings about sRGB profiles and such. +} + +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, png_warning_callback); + 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 + + +int opt_runs = 1; +int opt_nopng = 0; +int opt_nowarmup = 0; +int opt_noverify = 0; +int opt_nodecode = 0; +int opt_noencode = 0; +int opt_norecurse = 0; +int opt_onlytotals = 0; + +enum { + LIBPNG, + STBI, + QOI, + BENCH_COUNT /* must be the last element */ +}; +static const char *const lib_names[BENCH_COUNT] = { + // NOTE: pad with spaces so everything lines up properly + [LIBPNG] = "libpng: ", + [STBI] = "stbi: ", + [QOI] = "qoi: ", +}; + +typedef struct { + uint64_t size; + uint64_t encode_time; + uint64_t decode_time; +} benchmark_lib_result_t; + +typedef struct { + int count; + uint64_t raw_size; + uint64_t px; + int w; + int h; + benchmark_lib_result_t libs[BENCH_COUNT]; +} benchmark_result_t; + + +void benchmark_print_result(benchmark_result_t res) { + res.px /= res.count; + res.raw_size /= res.count; + + double px = res.px; + printf(" decode ms encode ms decode mpps encode mpps size kb rate\n"); + for (int i = 0; i < BENCH_COUNT; ++i) { + if (opt_nopng && (i == LIBPNG || i == STBI)) { + continue; + } + res.libs[i].encode_time /= res.count; + res.libs[i].decode_time /= res.count; + res.libs[i].size /= res.count; + printf( + "%s %8.1f %8.1f %8.2f %8.2f %8ld %4.1f%%\n", + lib_names[i], + (double)res.libs[i].decode_time/1000000.0, + (double)res.libs[i].encode_time/1000000.0, + (res.libs[i].decode_time > 0 ? px / ((double)res.libs[i].decode_time/1000.0) : 0), + (res.libs[i].encode_time > 0 ? px / ((double)res.libs[i].encode_time/1000.0) : 0), + res.libs[i].size/1024, + ((double)res.libs[i].size/(double)res.raw_size) * 100.0 + ); + } + printf("\n"); +} + +// Run __VA_ARGS__ a number of times and measure the time taken. The first +// run is ignored. +#define BENCHMARK_FN(NOWARMUP, RUNS, AVG_TIME, ...) \ + do { \ + uint64_t time = 0; \ + for (int i = NOWARMUP; 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 encoded_png_size; + int encoded_qoi_size; + int w; + int h; + int channels; + + // Load the encoded PNG, encoded QOI and raw pixels into memory + if(!stbi_info(path, &w, &h, &channels)) { + ERROR("Error decoding header %s", path); + } + + if (channels != 3) { + channels = 4; + } + + void *pixels = (void *)stbi_load(path, &w, &h, NULL, channels); + void *encoded_png = fload(path, &encoded_png_size); + void *encoded_qoi = qoi_encode(pixels, &(qoi_desc){ + .width = w, + .height = h, + .channels = channels, + .colorspace = QOI_SRGB + }, &encoded_qoi_size); + + if (!pixels || !encoded_qoi || !encoded_png) { + ERROR("Error encoding %s", path); + } + + // Verify QOI Output + + if (!opt_noverify) { + qoi_desc dc; + void *pixels_qoi = qoi_decode(encoded_qoi, encoded_qoi_size, &dc, channels); + if (memcmp(pixels, pixels_qoi, w * h * channels) != 0) { + ERROR("QOI roundtrip pixel mismatch for %s", path); + } + free(pixels_qoi); + } + + + + benchmark_result_t res = {0}; + res.count = 1; + res.raw_size = w * h * channels; + res.px = w * h; + res.w = w; + res.h = h; + + + // Decoding + + if (!opt_nodecode) { + if (!opt_nopng) { + BENCHMARK_FN(opt_nowarmup, opt_runs, res.libs[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(opt_nowarmup, opt_runs, res.libs[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(opt_nowarmup, opt_runs, res.libs[QOI].decode_time, { + qoi_desc desc; + void *dec_p = qoi_decode(encoded_qoi, encoded_qoi_size, &desc, 4); + free(dec_p); + }); + } + + + // Encoding + if (!opt_noencode) { + if (!opt_nopng) { + BENCHMARK_FN(opt_nowarmup, opt_runs, res.libs[LIBPNG].encode_time, { + int enc_size; + void *enc_p = libpng_encode(pixels, w, h, channels, &enc_size); + res.libs[LIBPNG].size = enc_size; + free(enc_p); + }); + + BENCHMARK_FN(opt_nowarmup, opt_runs, res.libs[STBI].encode_time, { + int enc_size = 0; + stbi_write_png_to_func(stbi_write_callback, &enc_size, w, h, channels, pixels, 0); + res.libs[STBI].size = enc_size; + }); + } + + BENCHMARK_FN(opt_nowarmup, opt_runs, res.libs[QOI].encode_time, { + int enc_size; + void *enc_p = qoi_encode(pixels, &(qoi_desc){ + .width = w, + .height = h, + .channels = channels, + .colorspace = QOI_SRGB + }, &enc_size); + res.libs[QOI].size = enc_size; + free(enc_p); + }); + } + + free(pixels); + free(encoded_png); + free(encoded_qoi); + + return res; +} + +void benchmark_directory(const char *path, benchmark_result_t *grand_total) { + DIR *dir = opendir(path); + if (!dir) { + ERROR("Couldn't open directory %s", path); + } + + struct dirent *file; + + if (!opt_norecurse) { + for (int i = 0; (file = readdir(dir)) != NULL; i++) { + if ( + file->d_type & DT_DIR && + strcmp(file->d_name, ".") != 0 && + strcmp(file->d_name, "..") != 0 + ) { + char subpath[1024]; + snprintf(subpath, 1024, "%s/%s", path, file->d_name); + benchmark_directory(subpath, grand_total); + } + } + rewinddir(dir); + } + + benchmark_result_t dir_total = {0}; + + int has_shown_head = 0; + for (int i = 0; (file = readdir(dir)) != NULL; i++) { + if (strcmp(file->d_name + strlen(file->d_name) - 4, ".png") != 0) { + continue; + } + + if (!has_shown_head) { + has_shown_head = 1; + printf("## Benchmarking %s/*.png -- %d runs\n\n", path, opt_runs); + } + + char *file_path = malloc(strlen(file->d_name) + strlen(path)+8); + sprintf(file_path, "%s/%s", path, file->d_name); + + benchmark_result_t res = benchmark_image(file_path); + + if (!opt_onlytotals) { + printf("## %s size: %dx%d\n", file_path, res.w, res.h); + benchmark_print_result(res); + } + + free(file_path); + + dir_total.count++; + dir_total.raw_size += res.raw_size; + dir_total.px += res.px; + for (int i = 0; i < BENCH_COUNT; ++i) { + dir_total.libs[i].encode_time += res.libs[i].encode_time; + dir_total.libs[i].decode_time += res.libs[i].decode_time; + dir_total.libs[i].size += res.libs[i].size; + } + + grand_total->count++; + grand_total->raw_size += res.raw_size; + grand_total->px += res.px; + for (int i = 0; i < BENCH_COUNT; ++i) { + grand_total->libs[i].encode_time += res.libs[i].encode_time; + grand_total->libs[i].decode_time += res.libs[i].decode_time; + grand_total->libs[i].size += res.libs[i].size; + } + } + closedir(dir); + + if (dir_total.count > 0) { + printf("## Total for %s\n", path); + benchmark_print_result(dir_total); + } +} + +int main(int argc, char **argv) { + if (argc < 3) { + printf("Usage: qoibench [options]\n"); + printf("Options:\n"); + printf(" --nowarmup ... don't perform a warmup run\n"); + printf(" --nopng ...... don't run png encode/decode\n"); + printf(" --noverify ... don't verify qoi roundtrip\n"); + printf(" --noencode ... don't run encoders\n"); + printf(" --nodecode ... don't run decoders\n"); + printf(" --norecurse .. don't descend into directories\n"); + printf(" --onlytotals . don't print individual image results\n"); + printf("Examples\n"); + printf(" qoibench 10 images/textures/\n"); + printf(" qoibench 1 images/textures/ --nopng --nowarmup\n"); + exit(1); + } + + for (int i = 3; i < argc; i++) { + if (strcmp(argv[i], "--nowarmup") == 0) { opt_nowarmup = 1; } + else if (strcmp(argv[i], "--nopng") == 0) { opt_nopng = 1; } + else if (strcmp(argv[i], "--noverify") == 0) { opt_noverify = 1; } + else if (strcmp(argv[i], "--noencode") == 0) { opt_noencode = 1; } + else if (strcmp(argv[i], "--nodecode") == 0) { opt_nodecode = 1; } + else if (strcmp(argv[i], "--norecurse") == 0) { opt_norecurse = 1; } + else if (strcmp(argv[i], "--onlytotals") == 0) { opt_onlytotals = 1; } + else { ERROR("Unknown option %s", argv[i]); } + } + + opt_runs = atoi(argv[1]); + if (opt_runs <=0) { + ERROR("Invalid number of runs %d", opt_runs); + } + + benchmark_result_t grand_total = {0}; + benchmark_directory(argv[2], &grand_total); + + if (grand_total.count > 0) { + printf("# Grand total for %s\n", argv[2]); + benchmark_print_result(grand_total); + } + else { + printf("No images found in %s\n", argv[2]); + } + + return 0; +} diff --git a/qoiconv.c b/qoiconv.c new file mode 100644 index 0000000..caef2ee --- /dev/null +++ b/qoiconv.c @@ -0,0 +1,91 @@ +/* + +Copyright (c) 2021, Dominic Szablewski - https://phoboslab.org +SPDX-License-Identifier: MIT + + +Command line tool to convert between png <> qoi format + +Requires: + -"stb_image.h" (https://github.com/nothings/stb/blob/master/stb_image.h) + -"stb_image_write.h" (https://github.com/nothings/stb/blob/master/stb_image_write.h) + -"qoi.h" (https://github.com/phoboslab/qoi/blob/master/qoi.h) + +Compile with: + gcc qoiconv.c -std=c99 -O3 -o qoiconv + +*/ + + +#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) { + puts("Usage: qoiconv "); + puts("Examples:"); + puts(" qoiconv input.png output.qoi"); + puts(" qoiconv input.qoi output.png"); + exit(1); + } + + void *pixels = NULL; + int w, h, channels; + if (STR_ENDS_WITH(argv[1], ".png")) { + if(!stbi_info(argv[1], &w, &h, &channels)) { + printf("Couldn't read header %s\n", argv[1]); + exit(1); + } + + // Force all odd encodings to be RGBA + if(channels != 3) { + channels = 4; + } + + pixels = (void *)stbi_load(argv[1], &w, &h, NULL, channels); + } + else if (STR_ENDS_WITH(argv[1], ".qoi")) { + qoi_desc desc; + pixels = qoi_read(argv[1], &desc, 0); + channels = desc.channels; + w = desc.width; + h = desc.height; + } + + 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, channels, pixels, 0); + } + else if (STR_ENDS_WITH(argv[2], ".qoi")) { + encoded = qoi_write(argv[2], pixels, &(qoi_desc){ + .width = w, + .height = h, + .channels = channels, + .colorspace = QOI_SRGB + }); + } + + if (!encoded) { + printf("Couldn't write/encode %s\n", argv[2]); + exit(1); + } + + free(pixels); + return 0; +} diff --git a/qoifuzz.c b/qoifuzz.c new file mode 100644 index 0000000..69954d1 --- /dev/null +++ b/qoifuzz.c @@ -0,0 +1,32 @@ +/* + +Copyright (c) 2021, Dominic Szablewski - https://phoboslab.org +SPDX-License-Identifier: MIT + + +clang fuzzing harness for qoi_decode + +Compile and run with: + clang -fsanitize=address,fuzzer -g -O0 qoifuzz.c && ./a.out + +*/ + + +#define QOI_IMPLEMENTATION +#include "qoi.h" +#include +#include + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + int w, h; + if (size < 4) { + return 0; + } + + qoi_desc desc; + void* decoded = qoi_decode((void*)(data + 4), (int)(size - 4), &desc, *((int *)data)); + if (decoded != NULL) { + free(decoded); + } + return 0; +}