+28
LICENSE
+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.
TestImages/dice.qoi
TestImages/dice.qoi
This is a binary file and will not be displayed.
TestImages/wikipedia_008.qoi
TestImages/wikipedia_008.qoi
This is a binary file and will not be displayed.
+73
img-inspect.c
+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
+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
+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
+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
+
}