1 module gamut.codecs.qoiplane;
2
3 nothrow @nogc:
4
5 import core.stdc.stdlib: realloc, malloc, free;
6 import core.stdc.string: memset;
7
8 import gamut.codecs.qoi2avg;
9
10 //version = benchmark;
11
12 version(benchmark)
13 {
14 import core.stdc.stdio;
15 }
16
17 /// A QOI-inspired codec for 8-bit greyscale images.
18 ///
19 /// Because the input is 8-bit, we are forced to split bytes in nibbles.
20 ///
21 /// Incompatible adaptation of QOI format - https://phoboslab.org
22 ///
23 /// -- LICENSE: The MIT License(MIT)
24 /// Copyright(c) 2021 Dominic Szablewski (original QOI format)
25 /// Copyright(c) 2022 Guillaume Piolat (QOI-plane variant for 8-bit greyscale and greyscale + alpha images).
26 /// Permission is hereby granted, free of charge, to any person obtaining a copy of
27 /// this software and associated documentation files(the "Software"), to deal in
28 /// the Software without restriction, including without limitation the rights to
29 /// use, copy, modify, merge, publish, distribute, sublicense, and / or sell copies
30 /// of the Software, and to permit persons to whom the Software is furnished to do
31 /// so, subject to the following conditions :
32 /// The above copyright notice and this permission notice shall be included in all
33 /// copies or substantial portions of the Software.
34 /// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
35 /// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
36 /// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
37 /// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
38 /// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
39 /// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
40 /// SOFTWARE.
41
42 /// -- Documentation
43
44 /// This library provides the following functions;
45 /// - qoiplane_decode -- decode the raw bytes of a QOI-plane image from memory
46 /// - qoiplane_encode -- encode an rgba buffer into a QOI-plane image in memory
47 ///
48 ///
49 /// A QOI-Plane file has a 25 byte header, compatible with Gamut QOIX.
50 ///
51 /// struct qoix_header_t {
52 /// char magic[4]; // magic bytes "qoix"
53 /// uint32_t width; // image width in pixels (BE)
54 /// uint32_t height; // image height in pixels (BE)
55 /// uint8_t version_; // Major version of QOIX format.
56 /// uint8_t channels; // 1 = 8-bit luminance 2 = luminance + alpha (3 and 4 indicate QOI2AVG codec, see qoi2avg.d)
57 /// uint8_t bitdepth; // 8 = this qoiplane codec is always 8-bit (10 indicates QOI-10 codec, see qoi10b.d)
58 /// uint8_t colorspace; // 0 = sRGB with linear alpha, 1 = all channels linear
59 /// uint8_t compression; // 0 = none, 1 = LZ4
60 /// float pixelAspectRatio; // -1 = unknown, else Pixel Aspect Ratio
61 /// float resolutionX; // -1 = unknown, else physical resolution in DPI
62 /// };
63 ///
64 /// The decoder and encoder start with {l: 0} as the previous
65 /// pixel value. Pixels are either encoded as
66 /// - a run of the previous pixel
67 /// - a difference to the previous pixel value
68 /// - full luminance value
69 ///
70 /// Each chunk starts with a tag, followed by a number of data bits. The bit length
71 /// of chunks is divisible by 4 - i.e. all chunks are nibble aligned. All values
72 /// encoded in these data bits have the most significant bit on the left.
73 /// The last nibble needs to be 0xf.
74 ///
75 /// The byte stream's end is marked with 4 0xff bytes.
76 ///
77 ///
78 ///
79 /// Encoding:
80 ///
81 /// QOIPLANE_DIFF1 0xxx => diff -4..+3 vs average of rounded up left pixel and top pixel
82 /// QOIPLANE_DIFF2 100x xxxx => diff -16..15 vs average of rounded up left pixel and top pixel
83 /// QOIPLANE_ADIFF 1011 xxxx => diff -7..+7 in alpha channel
84 /// QOIPLANE_LA 1011 0000 xxxx xxxx aaaa aaaa => encode direct full values
85 /// QOIPLANE_DIRECT 1010 xxxx xxxx => direct value
86 /// If channels == 2 and the last opcode is not a QOIPLANE_ADIFF
87 /// then QOIPLANE_DIRECT encodes an alpha value.
88 /// QOIPLANE_REPEAT1 11xx => repeat 1 to 3 times the last pixel
89 /// QOIPLANE_REPEAT2 1111 xxxx xxxx => repeat 4 to 258 times a pixel.
90 /// (1111 1111 1111 disallowed, indicates end of stream)
91
92
93 static immutable ubyte[4] qoiplane_padding = [255,255,255,255]; // this is 4x a full QOIPLANE_REPEAT2
94
95 enum qoi_la_t initialPredictor = { l:0, a:255 };
96
97 struct qoi_la_t
98 {
99 ubyte l;
100 ubyte a;
101 }
102
103 /* Encode raw L8 pixels into a QOIPlane image in memory.
104 The function either returns null on failure (invalid parameters or malloc
105 failed) or a pointer to the encoded data on success. On success the out_len
106 is set to the size in bytes of the encoded data.
107 The returned qoi data should be free()d after use. */
108 ubyte* qoiplane_encode(const(ubyte)* data, const(qoi_desc)* desc, int *out_len)
109 {
110 if ( (desc.channels != 1 && desc.channels != 2) ||
111 desc.height >= QOIX_PIXELS_MAX / desc.width ||
112 desc.compression != QOIX_COMPRESSION_NONE
113 ) {
114 return null;
115 }
116
117 if (desc.bitdepth != 8)
118 return null;
119
120 int channels = desc.channels;
121
122 // At worst, each pixel take 12 bit to be encoded.
123 int num_pixels = desc.width * desc.height;
124 int worst_case_nibbles_for_one_pixel = (channels == 1 ? 3 : 6);
125 int max_size = (num_pixels * worst_case_nibbles_for_one_pixel + 1) / 2
126 + QOIX_HEADER_SIZE + cast(int)(qoiplane_padding.sizeof);
127
128 ubyte* stream;
129
130 int p = 0; // write index into output stream
131 ubyte* bytes = cast(ubyte*) QOI_MALLOC(max_size);
132 if (!bytes)
133 {
134 return null;
135 }
136
137 version(benchmark)
138 {
139 int numQOIPLANE_DIFF1 = 0;
140 int numQOIPLANE_DIFF2 = 0;
141 int numQOIPLANE_DIRECT = 0;
142 int numQOIPLANE_REPEAT1 = 0;
143 int numQOIPLANE_REPEAT2 = 0;
144 int numQOIPLANE_LA = 0;
145
146 int encodedQOIPLANE_REPEAT1 = 0;
147 int encodedQOIPLANE_REPEAT2 = 0;
148 }
149
150 qoi_write_32(bytes, &p, QOIX_MAGIC);
151 qoi_write_32(bytes, &p, desc.width);
152 qoi_write_32(bytes, &p, desc.height);
153 bytes[p++] = 1; // Put a version number :)
154 bytes[p++] = desc.channels; // 1, or 2
155 bytes[p++] = desc.bitdepth; // 8, or 10
156 bytes[p++] = desc.colorspace;
157 bytes[p++] = QOIX_COMPRESSION_NONE;
158 qoi_write_32f(bytes, &p, desc.pixelAspectRatio);
159 qoi_write_32f(bytes, &p, desc.resolutionY);
160
161 bool writeHiNibble = true; // nibble index into output stream.
162
163 void outputNibble(ubyte nibble) nothrow @nogc
164 {
165 assert(nibble < 16);
166 if (writeHiNibble)
167 {
168 bytes[p] = cast(ubyte)(nibble << 4);
169 }
170 else
171 {
172 bytes[p++] |= nibble;
173 }
174 writeHiNibble = !writeHiNibble;
175 }
176
177 void outputByte(ubyte b)
178 {
179 if (writeHiNibble)
180 {
181 bytes[p++] = b;
182 }
183 else
184 {
185 bytes[p++] |= (b >>> 4);
186 bytes[p] = cast(ubyte)(b << 4);
187 }
188 }
189
190 void encodeRun(ref int run) nothrow @nogc
191 {
192 assert(run > 0 && run <= 258);
193 if (run <= 3)
194 {
195 ubyte nibble = 0xc | cast(ubyte)(run - 1);
196 outputNibble(nibble); // QOIPLANE_REPEAT1
197 version(benchmark)
198 {
199 numQOIPLANE_REPEAT1++;
200 encodedQOIPLANE_REPEAT1 += run;
201 }
202 }
203 else
204 {
205 run -= 4;
206 outputNibble(0xf); // QOIPLANE_REPEAT2
207 outputByte(cast(ubyte)run);
208 version(benchmark)
209 {
210 numQOIPLANE_REPEAT2++;
211 encodedQOIPLANE_REPEAT2 += run;
212 }
213 }
214 run = 0;
215 }
216
217 qoi_la_t px = initialPredictor;
218 qoi_la_t px_ref = initialPredictor;
219
220 int stride = desc.width * channels;
221 int run = 0;
222 int pixels_encoded = 0;
223
224 for (int posy = 0; posy < desc.height; ++posy)
225 {
226 const(ubyte)* line = data + desc.pitchBytes * posy;
227 const(ubyte)* lineAbove = (posy > 0) ? (data + desc.pitchBytes * (posy - 1)) : null;
228
229 for (int posx = 0; posx < desc.width; ++posx)
230 {
231 // last pixel is the new predictor
232 px_ref = px;
233
234 // take next pixel to encode
235 if (channels == 1)
236 {
237 px.l = line[posx * channels];
238 }
239 else
240 {
241 px.l = line[posx * channels + 0];
242 px.a = line[posx * channels + 1];
243 }
244
245 if (px == px_ref)
246 {
247 run++;
248 if (run == 258 || (pixels_encoded + 1 == num_pixels))
249 encodeRun(run);
250 }
251 else
252 {
253 if (run > 0)
254 encodeRun(run);
255
256 byte va = cast(byte)(px.a - px_ref.a);
257
258 if (va)
259 {
260 assert(channels == 2);
261
262 if (va >= -7 && va <= 7)
263 {
264 outputNibble( 0xb);
265 outputNibble( cast(ubyte)(va + 8) ); // QOIPLANE_ADIFF
266 goto encode_color;
267 }
268 else
269 {
270 outputNibble(0xb); // QOIPLANE_LA
271 outputNibble(0x0);
272 outputByte(px.l);
273 outputByte(px.a);
274 version(benchmark) numQOIPLANE_LA++;
275 }
276 }
277 else
278 {
279 encode_color:
280
281 // take top pixel (if it exist), else it's the same predictor
282 ubyte px_top = (posy > 0) ? lineAbove[posx * channels] : px_ref.l;
283 ubyte px_avg = (px_top + px_ref.l + 1) / 2;
284
285 byte diff_avg = cast(byte)(px.l - px_avg);
286
287 if (diff_avg >= -4 && diff_avg <= 3)
288 {
289 ubyte nibble = 0x0 | cast(ubyte)(diff_avg + 4);
290 outputNibble(nibble); // QOIPLANE_DIFF1
291 version(benchmark) numQOIPLANE_DIFF1++;
292 }
293 else if (diff_avg >= -16 && diff_avg <= 15)
294 {
295 ubyte diff2b = 0x80 | cast(ubyte)(diff_avg + 16);
296 outputByte(diff2b); // QOIPLANE_DIFF2
297 version(benchmark) numQOIPLANE_DIFF2++;
298 }
299 else
300 {
301 outputNibble(0xa); // QOIPLANE_DIRECT
302 outputByte(px.l);
303 version(benchmark) numQOIPLANE_DIRECT++;
304 }
305 }
306 }
307
308 pixels_encoded++;
309 }
310 }
311
312 // Put 3x QOIPLANE_REPEAT2 with full bits in order to have 4 0xff bytes
313 foreach(i; 0..9) outputNibble(0xf);
314
315 // Last nibble to fit
316 if (!writeHiNibble) outputNibble(0xf);
317
318
319 version(benchmark)
320 {
321 double totalOps = numQOIPLANE_DIFF1 + numQOIPLANE_DIFF2 + numQOIPLANE_DIRECT + numQOIPLANE_REPEAT1 + numQOIPLANE_REPEAT2;
322
323 double pixelsQOIPLANE_DIFF1 = numQOIPLANE_DIFF1 / cast(double)pixels_encoded;
324 double pixelsQOIPLANE_DIFF2 = numQOIPLANE_DIFF2 / cast(double)pixels_encoded;
325 double pixelsQOIPLANE_DIRECT = numQOIPLANE_DIRECT / cast(double)pixels_encoded;
326 double pixelsQOIPLANE_REPEAT1 = encodedQOIPLANE_REPEAT1 / cast(double)pixels_encoded;
327 double pixelsQOIPLANE_REPEAT2 = encodedQOIPLANE_REPEAT2 / cast(double)pixels_encoded;
328 double pixelsQOIPLANE_LA = numQOIPLANE_LA / cast(double)pixels_encoded;
329
330 double sizeQOIPLANE_DIFF1 = 4 * numQOIPLANE_DIFF1 / (8.0 * p);
331 double sizeQOIPLANE_DIFF2 = 8 * numQOIPLANE_DIFF2 / (8.0 * p);
332 double sizeQOIPLANE_DIRECT = 12 * numQOIPLANE_DIRECT / (8.0 * p);
333 double sizeQOIPLANE_REPEAT1 = 4 * numQOIPLANE_REPEAT1 / (8.0 * p);
334 double sizeQOIPLANE_REPEAT2 = 12 * numQOIPLANE_REPEAT2 / (8.0 * p);
335 double sizeQOIPLANE_LA = 20 * numQOIPLANE_LA / (8.0 * p);
336
337 printf("Num QOIPLANE_DIFF1 = %d\n", numQOIPLANE_DIFF1);
338 printf(" * pixels = %.2f\n", pixelsQOIPLANE_DIFF1 * 100);
339 printf(" * size = %.2f\n\n", sizeQOIPLANE_DIFF1 * 100);
340
341 printf("Num QOIPLANE_DIFF2 = %d\n", numQOIPLANE_DIFF2);
342 printf(" * pixels = %.2f\n", pixelsQOIPLANE_DIFF2 * 100);
343 printf(" * size = %.2f\n\n", sizeQOIPLANE_DIFF2 * 100);
344
345 printf("Num QOIPLANE_DIRECT = %d\n", numQOIPLANE_DIRECT);
346 printf(" * pixels = %.2f\n", pixelsQOIPLANE_DIRECT * 100);
347 printf(" * size = %.2f\n\n", sizeQOIPLANE_DIRECT * 100);
348
349 printf("Num QOIPLANE_REPEAT1 = %d\n", encodedQOIPLANE_REPEAT1);
350 printf(" * pixels = %.2f\n", pixelsQOIPLANE_REPEAT1 * 100);
351 printf(" * size = %.2f\n\n", sizeQOIPLANE_REPEAT1 * 100);
352
353 printf("Num QOIPLANE_REPEAT2 = %d\n", encodedQOIPLANE_REPEAT2);
354 printf(" * pixels = %.2f\n", pixelsQOIPLANE_REPEAT2 * 100);
355 printf(" * size = %.2f\n\n", sizeQOIPLANE_REPEAT2 * 100);
356
357 printf("Num QOIPLANE_LA = %d\n", numQOIPLANE_LA);
358 printf(" * pixels = %.2f\n", pixelsQOIPLANE_LA * 100);
359 printf(" * size = %.2f\n\n", sizeQOIPLANE_LA * 100);
360 }
361
362 *out_len = p;
363 return bytes;
364 }
365
366
367
368 /* Decode a QOI-plane image from memory.
369
370 The function either returns null on failure (invalid parameters or malloc
371 failed) or a pointer to the decoded pixels. On success, the qoi_desc struct
372 is filled with the description from the file header.
373
374 The returned pixel data should be free()d after use. */
375 ubyte* qoiplane_decode(const(ubyte)* data, int size, qoi_desc *desc, int channels)
376 {
377 if ((channels < 0 && channels > 2) ||
378 size < QOIX_HEADER_SIZE + cast(int)(qoiplane_padding.sizeof))
379 {
380 return null;
381 }
382
383 const(ubyte)* bytes = data;
384
385 int p = 0;
386
387 uint header_magic = qoi_read_32(bytes, &p);
388 desc.width = qoi_read_32(bytes, &p);
389 desc.height = qoi_read_32(bytes, &p);
390 int qoix_version = bytes[p++];
391 desc.channels = bytes[p++];
392 desc.bitdepth = bytes[p++];
393 desc.colorspace = bytes[p++];
394 desc.compression = bytes[p++];
395 desc.pixelAspectRatio = qoi_read_32f(bytes, &p);
396 desc.resolutionY = qoi_read_32f(bytes, &p);
397
398 if (desc.width == 0 || desc.height == 0 ||
399 desc.channels < 1 || desc.channels > 2 ||
400 desc.colorspace > 1 ||
401 desc.bitdepth != 8 ||
402 qoix_version > 1 ||
403 desc.compression != QOIX_COMPRESSION_NONE ||
404 header_magic != QOIX_MAGIC ||
405 desc.height >= QOIX_PIXELS_MAX / desc.width
406 )
407 {
408 return null;
409 }
410
411 if (channels == 0)
412 {
413 channels = desc.channels;
414 }
415
416 int stride = desc.width * channels;
417 desc.pitchBytes = stride; // FUTURE: force to decode with a given layout / image
418
419 int num_pixels = desc.width * desc.height;
420 int output_bytes = num_pixels * channels;
421
422 ubyte* pixels = cast(ubyte*) QOI_MALLOC(output_bytes);
423 if (!pixels)
424 return null;
425
426 bool readHiNibble = true; // nibble index into output stream.
427
428 ubyte readNibble() nothrow @nogc
429 {
430 ubyte r;
431 if (readHiNibble)
432 r = (bytes[p] >>> 4);
433 else
434 r = (bytes[p++] & 0xf);
435 readHiNibble = !readHiNibble;
436 assert(r < 16);
437 return r;
438 }
439
440 ubyte readUbyte()
441 {
442 ubyte hi = cast(ubyte)(readNibble() << 4);
443 ubyte lo = readNibble();
444 return hi | lo;
445 }
446
447 qoi_la_t px = initialPredictor;
448 qoi_la_t px_ref = initialPredictor;
449
450 int decoded_pixels = 0;
451 int run = 0;
452
453 for (int posy = 0; posy < desc.height; ++posy)
454 {
455 ubyte* line = pixels + desc.pitchBytes * posy;
456
457 // Note: don't read alpha in line above, since it may not exist if decoding 2 channels to 1
458 const(ubyte)* lineAbove = (posy > 0) ? (pixels + desc.pitchBytes * (posy - 1)) : null;
459
460 for (int posx = 0; posx < desc.width; ++posx)
461 {
462 px_ref = px;
463
464 if (run > 0)
465 {
466 run--;
467 }
468 else if (decoded_pixels < num_pixels)
469 {
470 decode_op:
471 ubyte op = readNibble();
472
473 if ((op & 0xf) == 0xf) // QOIPLANE_REPEAT2
474 {
475 run = readUbyte() + 3;
476 if (run == 258)
477 run = 0x7fffffff; // fill with last pixel until end of decode
478 }
479 else if ((op & 0xc) == 0xc) // QOIPLANE_REPEAT1
480 {
481 run = (op & 0x3);
482 }
483 else
484 {
485 // Compute predictors.
486 ubyte px_top = (posy > 0) ? lineAbove[posx * channels] : px_ref.l;
487 ubyte px_avg = (px_top + px_ref.l + 1) / 2;
488
489 if ((op & 0x8) == 0) // QOIPLANE_DIFF1
490 {
491 assert(op < 8);
492 px.l = cast(ubyte)(px_avg + op - 4);
493 }
494 else if ((op & 0xe) == 0x8) // QOIPLANE_DIFF2
495 {
496 int vg_l = ((op & 1) << 4) + readNibble();
497 assert(vg_l >= 0 && vg_l <= 31);
498 vg_l -= 16;
499 px.l = cast(ubyte)(px_avg + vg_l);
500 }
501 else if ((op & 0xf) == 0xa) // QOIPLANE_DIRECT
502 {
503 px.l = readUbyte();
504 }
505 else if ((op & 0xf) == 0xb)
506 {
507 int diff = readNibble();
508 if (diff == 0) // QOIPLANE_LA
509 {
510 px.l = readUbyte();
511 px.a = readUbyte();
512 }
513 else
514 {
515 // QOIPLANE_ADIFF
516 px.a = cast(ubyte)(px_ref.a + diff - 8); // -7 to 7
517 goto decode_op;
518 }
519 }
520 else
521 assert(false);
522 }
523 decoded_pixels++;
524 }
525
526 if (channels == 1)
527 {
528 line[posx * 1] = px.l;
529 }
530 else
531 {
532 line[posx * 2 + 0] = px.l;
533 line[posx * 2 + 1] = px.a;
534 }
535 }
536 }
537
538 return pixels;
539 }