A very small library for reading image files

🎉 Initial: QOI

qeaml fd2b2f67

+3
.gitignore
··· 1 + img-inspect 2 + img2ppm 3 + TestImages/*.ppm
+28
LICENSE
··· 1 + BSD 3-Clause License 2 + 3 + Copyright (c) 2025, qeaml 4 + 5 + Redistribution and use in source and binary forms, with or without 6 + modification, are permitted provided that the following conditions are met: 7 + 8 + 1. Redistributions of source code must retain the above copyright notice, this 9 + list of conditions and the following disclaimer. 10 + 11 + 2. Redistributions in binary form must reproduce the above copyright notice, 12 + this list of conditions and the following disclaimer in the documentation 13 + and/or other materials provided with the distribution. 14 + 15 + 3. Neither the name of the copyright holder nor the names of its 16 + contributors may be used to endorse or promote products derived from 17 + this software without specific prior written permission. 18 + 19 + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+3
README.md
··· 1 + # img 2 + 3 + A very small library for reading images.
TestImages/dice.qoi

This is a binary file and will not be displayed.

TestImages/wikipedia_008.qoi

This is a binary file and will not be displayed.

+3
build-img-inspect.sh
··· 1 + #!/bin/sh 2 + 3 + cc img-inspect.c img.c -o img-inspect
+3
build-img2ppm.sh
··· 1 + #!/bin/sh 2 + 3 + cc img2ppm.c img.c -o img2ppm "$@"
+73
img-inspect.c
··· 1 + #include "img.h" 2 + #include <stdio.h> 3 + 4 + struct Ctx { 5 + FILE *file; 6 + }; 7 + 8 + static ImgStatus ctxRead(ImgAny user, ImgAny buf, ImgSz bufSz) 9 + { 10 + struct Ctx *ctx = user; 11 + if(fread(buf, bufSz, 1, ctx->file) != 1) { 12 + return ImgErrIO; 13 + } 14 + return ImgOK; 15 + } 16 + 17 + static ImgStatus ctxSeek(ImgAny user, ImgOff off) 18 + { 19 + struct Ctx *ctx = user; 20 + fseek(ctx->file, off, SEEK_CUR); 21 + return ImgOK; 22 + } 23 + 24 + static ImgStatus ctxClose(ImgAny user) 25 + { 26 + struct Ctx *ctx = user; 27 + fclose(ctx->file); 28 + return ImgOK; 29 + } 30 + 31 + int main(int argc, char **argv) 32 + { 33 + if(argc < 2) { 34 + printf("Usage: %s <file>\n", argv[0]); 35 + return 1; 36 + } 37 + 38 + FILE *file = fopen(argv[1], "rb"); 39 + if(file == NULL) { 40 + perror("Could not open file"); 41 + return 1; 42 + } 43 + 44 + struct Ctx ctx; 45 + ctx.file = file; 46 + ImgFuncs funcs; 47 + funcs.user = &ctx; 48 + funcs.read = &ctxRead; 49 + funcs.seek = &ctxSeek; 50 + funcs.close = &ctxClose; 51 + 52 + Img img; 53 + ImgStatus status = imgOpen(&img, &funcs); 54 + if(status != ImgOK) { 55 + printf("Could not open image: %s\n", imgStatusMessage(status)); 56 + return 1; 57 + } 58 + 59 + printf( 60 + "Detected %s\n" 61 + "Size: %ux%u\n" 62 + "Channel count: %u\n", 63 + imgFormatName(img.format), 64 + img.width, img.height, 65 + img.channelCount); 66 + status = imgClose(&img); 67 + if(status != ImgOK) { 68 + printf("Could not close image: %s\n", imgStatusMessage(status)); 69 + return 1; 70 + } 71 + 72 + return 0; 73 + }
+242
img.c
··· 1 + #include "img.h" 2 + 3 + static ImgStatus determineFormat(Img *img); 4 + static ImgStatus openQoi(Img *img); 5 + static ImgStatus readQoi(Img *img, ImgAny buf, ImgSz bufSz); 6 + 7 + ImgStatus imgOpen(Img *img, const ImgFuncs *funcs) 8 + { 9 + img->funcs = *funcs; 10 + ImgStatus status = determineFormat(img); 11 + if(status != ImgOK) { return status; } 12 + switch(img->format) { 13 + case ImgFormatUnknown: 14 + return ImgErrUnknownFormat; 15 + case ImgFormatQoi: 16 + return openQoi(img); 17 + } 18 + } 19 + 20 + ImgStatus imgRead(Img *img, ImgAny buf, ImgSz bufSz) 21 + { 22 + if(img->width == 0 || img->height == 0 || img->channelCount == 0) { 23 + return ImgErrUnopened; 24 + } 25 + switch(img->format) { 26 + case ImgFormatUnknown: 27 + return ImgErrUnknownFormat; 28 + case ImgFormatQoi: 29 + return readQoi(img, buf, bufSz); 30 + } 31 + } 32 + 33 + ImgStatus imgClose(Img *img) 34 + { 35 + img->format = ImgFormatUnknown; 36 + img->width = 0; 37 + img->height = 0; 38 + img->channelCount = 0; 39 + if(img->funcs.close) { 40 + return img->funcs.close(img->funcs.user); 41 + } 42 + return ImgOK; 43 + } 44 + 45 + const char *imgFormatName(ImgFormat format) { 46 + switch(format) { 47 + case ImgFormatUnknown: 48 + return "Unknown"; 49 + case ImgFormatQoi: 50 + return "QOI"; 51 + } 52 + } 53 + 54 + const char *imgStatusMessage(ImgStatus status) { 55 + switch(status) { 56 + case ImgOK: 57 + return "OK"; 58 + case ImgErrIO: 59 + return "I/O error"; 60 + case ImgErrUnknownFormat: 61 + return "Unknown image format"; 62 + case ImgErrUnopened: 63 + return "No image opened"; 64 + case ImgErrQoiInvalidChannelCount: 65 + return "QOI: Invalid channel count"; 66 + case ImgErrQoiInvalidColorSpace: 67 + return "QOI: Invalid color space"; 68 + case ImgErrQoiBufferTooSmall: 69 + return "QOI: Buffer to small to decode into"; 70 + } 71 + } 72 + 73 + static ImgStatus read(Img *img, ImgAny buf, ImgSz bufSz) 74 + { 75 + return img->funcs.read(img->funcs.user, buf, bufSz); 76 + } 77 + 78 + static ImgStatus readU32(Img *img, ImgU32 *out) 79 + { 80 + return img->funcs.read(img->funcs.user, out, 4); 81 + } 82 + 83 + static ImgStatus readU32BE(Img *img, ImgU32 *out) 84 + { 85 + ImgStatus status = img->funcs.read(img->funcs.user, out, 4); 86 + if(status != ImgOK) { return status; } 87 + #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ 88 + *out = __builtin_bswap32(*out); 89 + #endif 90 + return ImgOK; 91 + } 92 + 93 + static ImgStatus seek(Img *img, ImgOff off) 94 + { 95 + return img->funcs.seek(img->funcs.user, off); 96 + } 97 + 98 + static ImgStatus determineFormat(Img *img) 99 + { 100 + ImgStatus status; 101 + char magic[4]; 102 + status = read(img, magic, 4); 103 + if(status != ImgOK) { return status; } 104 + status = seek(img, -4); 105 + if(status != ImgOK) { return status; } 106 + if(magic[0] == 'q' && magic[1] == 'o' && magic[2] == 'i' && magic[3] == 'f') { 107 + img->format = ImgFormatQoi; 108 + return ImgOK; 109 + } 110 + return ImgErrUnknownFormat; 111 + } 112 + 113 + struct RGBAS { 114 + ImgU8 r; 115 + ImgU8 g; 116 + ImgU8 b; 117 + ImgU8 a; 118 + }; 119 + 120 + typedef struct RGBAS RGBA; 121 + 122 + ImgStatus openQoi(Img *img) 123 + { 124 + ImgStatus status = seek(img, 4); 125 + if(status != ImgOK) { return status; } 126 + status = readU32BE(img, &img->width); 127 + if(status != ImgOK) { return status; } 128 + status = readU32BE(img, &img->height); 129 + if(status != ImgOK) { return status; } 130 + status = read(img, &img->channelCount, 1); 131 + if(status != ImgOK) { return status; } 132 + ImgU8 colorSpace; 133 + status = read(img, &colorSpace, 1); 134 + if(status != ImgOK) { return status; } 135 + 136 + if(img->channelCount != 3 && img->channelCount != 4) { 137 + return ImgErrQoiInvalidChannelCount; 138 + } 139 + if(colorSpace != 0 && colorSpace != 1) { 140 + return ImgErrQoiInvalidColorSpace; 141 + } 142 + 143 + return ImgOK; 144 + } 145 + 146 + static ImgU32 qoiIndex(RGBA color) { 147 + return (color.r * 3 + color.g * 5 + color.b * 7 + color.a * 11) % 64; 148 + } 149 + 150 + /* FIXME: doesn't decode 4-chanel images correctly */ 151 + ImgStatus readQoi(Img *img, ImgAny buf, ImgSz bufSz) 152 + { 153 + ImgSz expectedSize = (ImgSz)img->width * img->height * img->channelCount; 154 + if(bufSz < expectedSize) { 155 + return ImgErrQoiBufferTooSmall; 156 + } 157 + 158 + RGBA memory[64]; 159 + for(ImgSz i = 0; i < 64; ++i) { 160 + memory[i].r = 0; 161 + memory[i].g = 0; 162 + memory[i].b = 0; 163 + memory[i].a = 0; 164 + } 165 + RGBA pixel; 166 + pixel.r = 0; 167 + pixel.g = 0; 168 + pixel.b = 0; 169 + pixel.a = 255; 170 + 171 + ImgU8 *curr = buf; 172 + ImgU8 *end = curr + bufSz; 173 + ImgSz run = 0; 174 + ImgU8 chunk; 175 + ImgStatus status; 176 + ImgS8 dr; 177 + ImgS8 dg; 178 + ImgS8 db; 179 + 180 + while(curr < end) { 181 + if(run > 0) { 182 + --run; 183 + *curr++ = pixel.r; 184 + *curr++ = pixel.g; 185 + *curr++ = pixel.b; 186 + if(img->channelCount == 4) { 187 + *curr++ = pixel.a; 188 + } 189 + } 190 + 191 + status = read(img, &chunk, 1); 192 + if(status != ImgOK) { return status; } 193 + 194 + if(chunk == 0xFF) { 195 + status = read(img, &pixel.r, 1); 196 + if(status != ImgOK) { return status; } 197 + status = read(img, &pixel.g, 1); 198 + if(status != ImgOK) { return status; } 199 + status = read(img, &pixel.b, 1); 200 + if(status != ImgOK) { return status; } 201 + status = read(img, &pixel.a, 1); 202 + if(status != ImgOK) { return status; } 203 + } else if(chunk == 0xFE) { 204 + status = read(img, &pixel.r, 1); 205 + if(status != ImgOK) { return status; } 206 + status = read(img, &pixel.g, 1); 207 + if(status != ImgOK) { return status; } 208 + status = read(img, &pixel.b, 1); 209 + if(status != ImgOK) { return status; } 210 + } else if((chunk & 0xC0) == 0x00) { 211 + pixel = memory[chunk]; 212 + } else if((chunk & 0xC0) == 0x40) { 213 + dr = (ImgS8)((chunk & 0x30) >> 4) - 2; 214 + dg = (ImgS8)((chunk & 0x0C) >> 2) - 2; 215 + db = (ImgS8)(chunk & 0x03) - 2; 216 + pixel.r += dr; 217 + pixel.g += dg; 218 + pixel.b += db; 219 + } else if((chunk & 0xC0) == 0x80) { 220 + dg = (ImgS8)(chunk & 0x3F) - 32; 221 + status = read(img, &chunk, 1); 222 + if(status != ImgOK) { return status; } 223 + dr = (ImgS8)(chunk >> 4) - 8 + dg; 224 + db = (ImgS8)(chunk & 0xF) - 8 + dg; 225 + pixel.r += dr; 226 + pixel.g += dg; 227 + pixel.b += db; 228 + } else if((chunk & 0xC0) == 0xC0) { 229 + run = chunk & 0x3F; 230 + } 231 + 232 + memory[qoiIndex(pixel)] = pixel; 233 + *curr++ = pixel.r; 234 + *curr++ = pixel.g; 235 + *curr++ = pixel.b; 236 + if(img->channelCount == 4) { 237 + *curr++ = pixel.a; 238 + } 239 + } 240 + 241 + return ImgOK; 242 + }
+57
img.h
··· 1 + #pragma once 2 + 3 + typedef unsigned long int ImgSz; 4 + typedef signed long int ImgOff; 5 + typedef unsigned int ImgU32; 6 + typedef signed char ImgS8; 7 + typedef unsigned char ImgU8; 8 + typedef void *ImgAny; 9 + 10 + enum ImgStatusE { 11 + ImgOK, 12 + ImgErrIO, 13 + ImgErrUnknownFormat, 14 + ImgErrUnopened, 15 + ImgErrQoiInvalidChannelCount, 16 + ImgErrQoiInvalidColorSpace, 17 + ImgErrQoiBufferTooSmall, 18 + }; 19 + 20 + typedef enum ImgStatusE ImgStatus; 21 + 22 + typedef ImgStatus(*ImgReadFunc)(ImgAny user, ImgAny buf, ImgSz size); 23 + typedef ImgStatus(*ImgSeekFunc)(ImgAny user, ImgOff off); 24 + typedef ImgStatus(*ImgCloseFunc)(ImgAny user); 25 + 26 + struct ImgFuncsS { 27 + ImgAny user; 28 + ImgReadFunc read; 29 + ImgSeekFunc seek; 30 + ImgCloseFunc close; 31 + }; 32 + 33 + typedef struct ImgFuncsS ImgFuncs; 34 + 35 + enum ImgFormatE { 36 + ImgFormatUnknown, 37 + ImgFormatQoi, 38 + }; 39 + 40 + typedef enum ImgFormatE ImgFormat; 41 + 42 + struct ImgS { 43 + ImgFormat format; 44 + ImgU32 width; 45 + ImgU32 height; 46 + ImgU8 channelCount; 47 + ImgFuncs funcs; 48 + }; 49 + 50 + typedef struct ImgS Img; 51 + 52 + ImgStatus imgOpen(Img *img, const ImgFuncs *funcs); 53 + ImgStatus imgRead(Img *img, ImgAny buf, ImgSz bufSz); 54 + ImgStatus imgClose(Img *img); 55 + 56 + const char *imgFormatName(ImgFormat format); 57 + const char *imgStatusMessage(ImgStatus status);
+119
img2ppm.c
··· 1 + #include "img.h" 2 + #include <stdio.h> 3 + #include <stdlib.h> 4 + 5 + struct Ctx { 6 + FILE *file; 7 + }; 8 + 9 + static ImgStatus ctxRead(ImgAny user, ImgAny buf, ImgSz bufSz) 10 + { 11 + struct Ctx *ctx = user; 12 + if(fread(buf, bufSz, 1, ctx->file) != 1) { 13 + return ImgErrIO; 14 + } 15 + return ImgOK; 16 + } 17 + 18 + static ImgStatus ctxSeek(ImgAny user, ImgOff off) 19 + { 20 + struct Ctx *ctx = user; 21 + fseek(ctx->file, off, SEEK_CUR); 22 + return ImgOK; 23 + } 24 + 25 + static ImgStatus ctxClose(ImgAny user) 26 + { 27 + struct Ctx *ctx = user; 28 + fclose(ctx->file); 29 + return ImgOK; 30 + } 31 + 32 + static void saveToPPM_RGB(const Img *img, const ImgU8 *pixels, const char *filename) 33 + { 34 + FILE *file = fopen(filename, "wb"); 35 + if(file == NULL) { 36 + perror("Could not open output file"); 37 + return; 38 + } 39 + 40 + fprintf(file, "P6\n%u %u\n255\n", img->width, img->height); 41 + fwrite(pixels, 3, (ImgSz)img->width * img->height, file); 42 + fclose(file); 43 + } 44 + 45 + static void saveToPPM_RGBA(const Img *img, const ImgU8 *pixels, const char *filename) 46 + { 47 + FILE *file = fopen(filename, "wb"); 48 + if(file == NULL) { 49 + perror("Could not open output file"); 50 + return; 51 + } 52 + 53 + fprintf(file, "P6\n%u %u\n255\n", img->width, img->height); 54 + for(ImgSz i = 0; i < (ImgSz)img->width * img->height; ++i) { 55 + fwrite(&pixels[i * 4], 1, 3, file); 56 + } 57 + fclose(file); 58 + } 59 + 60 + int main(int argc, char **argv) 61 + { 62 + if(argc < 3) { 63 + printf("Usage: %s <input file> <output PPM file>\n", argv[0]); 64 + return 1; 65 + } 66 + 67 + FILE *file = fopen(argv[1], "rb"); 68 + if(file == NULL) { 69 + perror("Could not open file"); 70 + return 1; 71 + } 72 + 73 + struct Ctx ctx; 74 + ctx.file = file; 75 + ImgFuncs funcs; 76 + funcs.user = &ctx; 77 + funcs.read = &ctxRead; 78 + funcs.seek = &ctxSeek; 79 + funcs.close = &ctxClose; 80 + 81 + Img img; 82 + ImgStatus status = imgOpen(&img, &funcs); 83 + if(status != ImgOK) { 84 + printf("Could not open image: %s\n", imgStatusMessage(status)); 85 + return 1; 86 + } 87 + 88 + printf( 89 + "Detected %s\n" 90 + "Size: %ux%u\n" 91 + "Channel count: %u\n", 92 + imgFormatName(img.format), 93 + img.width, img.height, 94 + img.channelCount); 95 + 96 + ImgSz size = (ImgSz)img.width * img.height * img.channelCount; 97 + ImgU8 *pixels = (ImgU8*)malloc(size); 98 + status = imgRead(&img, pixels, size); 99 + if(status != ImgOK) { 100 + printf("Could not decode image: %s\n", imgStatusMessage(status)); 101 + // return 1; 102 + } 103 + 104 + if(img.channelCount == 3) { 105 + saveToPPM_RGB(&img, pixels, argv[2]); 106 + } else if(img.channelCount == 4) { 107 + saveToPPM_RGBA(&img, pixels, argv[2]); 108 + } else { 109 + printf("Image must have 3 or 4 channels. Not converting.\n"); 110 + } 111 + 112 + status = imgClose(&img); 113 + if(status != ImgOK) { 114 + printf("Could not close image: %s\n", imgStatusMessage(status)); 115 + return 1; 116 + } 117 + 118 + return 0; 119 + }