The OpenD Programming Language

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 version(encodeQOIX)
109 ubyte* qoiplane_encode(const(ubyte)* data, const(qoi_desc)* desc, int *out_len) 
110 {
111     if ( (desc.channels != 1 && desc.channels != 2) ||
112         desc.height >= QOIX_PIXELS_MAX / desc.width ||
113           desc.compression != QOIX_COMPRESSION_NONE
114     ) {
115         return null;
116     }
117 
118     if (desc.bitdepth != 8)
119         return null;
120 
121     int channels = desc.channels;
122 
123     // At worst, each pixel take 12 bit to be encoded.
124     int num_pixels = desc.width * desc.height;
125     int worst_case_nibbles_for_one_pixel = (channels == 1 ? 3 : 6);
126     int max_size = (num_pixels * worst_case_nibbles_for_one_pixel + 1) / 2
127                  + QOIX_HEADER_SIZE + cast(int)(qoiplane_padding.sizeof);
128 
129     ubyte* stream;
130 
131     int p = 0; // write index into output stream
132     ubyte* bytes = cast(ubyte*) QOI_MALLOC(max_size);
133     if (!bytes) 
134     {
135         return null;
136     }
137 
138     version(benchmark)
139     {
140         int numQOIPLANE_DIFF1 = 0;
141         int numQOIPLANE_DIFF2 = 0;
142         int numQOIPLANE_DIRECT = 0;
143         int numQOIPLANE_REPEAT1 = 0;
144         int numQOIPLANE_REPEAT2 = 0;
145         int numQOIPLANE_LA    = 0;
146         
147         int encodedQOIPLANE_REPEAT1 = 0;
148         int encodedQOIPLANE_REPEAT2 = 0;
149     }
150 
151     qoi_write_32(bytes, &p, QOIX_MAGIC);
152     qoi_write_32(bytes, &p, desc.width);
153     qoi_write_32(bytes, &p, desc.height);
154     bytes[p++] = 1; // Put a version number :)
155     bytes[p++] = desc.channels; // 1, or 2
156     bytes[p++] = desc.bitdepth; // 8, or 10
157     bytes[p++] = desc.colorspace;
158     bytes[p++] = QOIX_COMPRESSION_NONE;
159     qoi_write_32f(bytes, &p, desc.pixelAspectRatio);
160     qoi_write_32f(bytes, &p, desc.resolutionY);
161 
162     bool writeHiNibble = true; // nibble index into output stream.
163 
164     void outputNibble(ubyte nibble) nothrow @nogc
165     {
166         assert(nibble < 16);
167         if (writeHiNibble)
168         {
169             bytes[p] = cast(ubyte)(nibble << 4);
170         }
171         else
172         {
173             bytes[p++] |= nibble;
174         }
175         writeHiNibble = !writeHiNibble;
176     }
177 
178     void outputByte(ubyte b)
179     {
180         if (writeHiNibble)
181         {
182             bytes[p++] = b;
183         }
184         else
185         {
186             bytes[p++] |= (b >>> 4);
187             bytes[p] = cast(ubyte)(b << 4);
188         }
189     }
190 
191     void encodeRun(ref int run) nothrow @nogc
192     {
193         assert(run > 0 && run <= 258);
194         if (run <= 3)
195         {
196             ubyte nibble =  0xc | cast(ubyte)(run - 1);
197             outputNibble(nibble); // QOIPLANE_REPEAT1
198             version(benchmark) 
199             {
200                 numQOIPLANE_REPEAT1++;
201                 encodedQOIPLANE_REPEAT1 += run;
202             }
203         }
204         else
205         {
206             run -= 4;
207             outputNibble(0xf); // QOIPLANE_REPEAT2
208             outputByte(cast(ubyte)run);
209             version(benchmark)
210             {
211                 numQOIPLANE_REPEAT2++;
212                 encodedQOIPLANE_REPEAT2 += run;
213             }
214         }
215         run = 0;
216     }
217 
218     qoi_la_t px = initialPredictor;
219     qoi_la_t px_ref = initialPredictor;
220 
221     int stride = desc.width * channels;
222     int run = 0;
223     int pixels_encoded = 0;
224 
225     for (int posy = 0; posy < desc.height; ++posy)
226     {
227         const(ubyte)* line = data + desc.pitchBytes * posy;
228         const(ubyte)* lineAbove = (posy > 0) ? (data + desc.pitchBytes * (posy - 1)) : null;
229 
230         for (int posx = 0; posx < desc.width; ++posx)
231         {
232             // last pixel is the new predictor
233             px_ref = px;
234 
235             // take next pixel to encode
236             if (channels == 1)
237             {
238                 px.l = line[posx * channels];
239             }
240             else
241             {
242                 px.l = line[posx * channels + 0];
243                 px.a = line[posx * channels + 1];
244             }
245 
246             if (px == px_ref)
247             {
248                 run++;
249                 if (run == 258 || (pixels_encoded + 1 == num_pixels))
250                     encodeRun(run);
251             }
252             else
253             {
254                 if (run > 0) 
255                     encodeRun(run);
256 
257                 byte va = cast(byte)(px.a - px_ref.a);
258 
259                 if (va) 
260                 {
261                     assert(channels == 2);
262 
263                     if (va >= -7 && va <= 7)
264                     {
265                         outputNibble( 0xb);
266                         outputNibble( cast(ubyte)(va + 8) ); // QOIPLANE_ADIFF
267                         goto encode_color;
268                     } 
269                     else
270                     { 
271                         outputNibble(0xb); // QOIPLANE_LA
272                         outputNibble(0x0);
273                         outputByte(px.l);
274                         outputByte(px.a);
275                         version(benchmark) numQOIPLANE_LA++;
276                     }
277                 }
278                 else
279                 {
280                 encode_color:
281 
282                     // take top pixel (if it exist), else it's the same predictor
283                     ubyte px_top = (posy > 0) ? lineAbove[posx * channels] : px_ref.l;
284                     ubyte px_avg = (px_top + px_ref.l + 1) / 2;
285 
286                     byte diff_avg = cast(byte)(px.l - px_avg);
287 
288                     if (diff_avg >= -4 && diff_avg <= 3)
289                     {
290                         ubyte nibble = 0x0 | cast(ubyte)(diff_avg + 4);
291                         outputNibble(nibble); // QOIPLANE_DIFF1
292                         version(benchmark) numQOIPLANE_DIFF1++;
293                     } 
294                     else if (diff_avg >= -16 && diff_avg <= 15)
295                     {
296                         ubyte diff2b =  0x80 | cast(ubyte)(diff_avg + 16);
297                         outputByte(diff2b); // QOIPLANE_DIFF2
298                         version(benchmark) numQOIPLANE_DIFF2++;
299                     } 
300                     else
301                     {
302                         outputNibble(0xa); // QOIPLANE_DIRECT
303                         outputByte(px.l);
304                         version(benchmark) numQOIPLANE_DIRECT++;
305                     }
306                 }
307             }
308 
309             pixels_encoded++;
310         }
311     }
312 
313     // Put 3x QOIPLANE_REPEAT2 with full bits in order to have 4 0xff bytes
314     foreach(i; 0..9) outputNibble(0xf);
315 
316     // Last nibble to fit
317     if (!writeHiNibble) outputNibble(0xf);
318 
319 
320     version(benchmark)
321     {
322         double totalOps = numQOIPLANE_DIFF1 + numQOIPLANE_DIFF2 + numQOIPLANE_DIRECT + numQOIPLANE_REPEAT1 + numQOIPLANE_REPEAT2;
323 
324         double pixelsQOIPLANE_DIFF1 = numQOIPLANE_DIFF1 / cast(double)pixels_encoded;
325         double pixelsQOIPLANE_DIFF2 = numQOIPLANE_DIFF2 / cast(double)pixels_encoded;
326         double pixelsQOIPLANE_DIRECT = numQOIPLANE_DIRECT / cast(double)pixels_encoded;
327         double pixelsQOIPLANE_REPEAT1 = encodedQOIPLANE_REPEAT1 / cast(double)pixels_encoded;
328         double pixelsQOIPLANE_REPEAT2 = encodedQOIPLANE_REPEAT2 / cast(double)pixels_encoded;
329         double pixelsQOIPLANE_LA = numQOIPLANE_LA / cast(double)pixels_encoded;
330 
331         double sizeQOIPLANE_DIFF1   = 4 * numQOIPLANE_DIFF1 / (8.0 * p);
332         double sizeQOIPLANE_DIFF2   = 8 * numQOIPLANE_DIFF2 / (8.0 * p);
333         double sizeQOIPLANE_DIRECT  = 12 * numQOIPLANE_DIRECT / (8.0 * p);
334         double sizeQOIPLANE_REPEAT1 = 4 * numQOIPLANE_REPEAT1 / (8.0 * p);
335         double sizeQOIPLANE_REPEAT2 = 12 * numQOIPLANE_REPEAT2 / (8.0 * p);
336         double sizeQOIPLANE_LA      = 20 * numQOIPLANE_LA / (8.0 * p);
337 
338         printf("Num QOIPLANE_DIFF1 = %d\n", numQOIPLANE_DIFF1);
339         printf(" * pixels  = %.2f\n", pixelsQOIPLANE_DIFF1 * 100);
340         printf(" * size    = %.2f\n\n", sizeQOIPLANE_DIFF1 * 100);
341 
342         printf("Num QOIPLANE_DIFF2 = %d\n", numQOIPLANE_DIFF2);
343         printf(" * pixels  = %.2f\n", pixelsQOIPLANE_DIFF2 * 100);
344         printf(" * size    = %.2f\n\n", sizeQOIPLANE_DIFF2 * 100);
345 
346         printf("Num QOIPLANE_DIRECT = %d\n", numQOIPLANE_DIRECT);
347         printf(" * pixels  = %.2f\n", pixelsQOIPLANE_DIRECT * 100);
348         printf(" * size    = %.2f\n\n", sizeQOIPLANE_DIRECT * 100);
349 
350         printf("Num QOIPLANE_REPEAT1 = %d\n", encodedQOIPLANE_REPEAT1);
351         printf(" * pixels  = %.2f\n", pixelsQOIPLANE_REPEAT1 * 100);
352         printf(" * size    = %.2f\n\n", sizeQOIPLANE_REPEAT1 * 100);
353 
354         printf("Num QOIPLANE_REPEAT2 = %d\n", encodedQOIPLANE_REPEAT2);
355         printf(" * pixels  = %.2f\n", pixelsQOIPLANE_REPEAT2 * 100);
356         printf(" * size    = %.2f\n\n", sizeQOIPLANE_REPEAT2 * 100);
357 
358         printf("Num QOIPLANE_LA = %d\n", numQOIPLANE_LA);
359         printf(" * pixels  = %.2f\n", pixelsQOIPLANE_LA * 100);
360         printf(" * size    = %.2f\n\n", sizeQOIPLANE_LA * 100);
361     }
362 
363     *out_len = p;
364     return bytes;
365 }
366 
367 
368 
369 /* Decode a QOI-plane image from memory.
370 
371 The function either returns null on failure (invalid parameters or malloc 
372 failed) or a pointer to the decoded pixels. On success, the qoi_desc struct 
373 is filled with the description from the file header.
374 
375 The returned pixel data should be free()d after use. */
376 version(decodeQOIX)
377 ubyte* qoiplane_decode(const(ubyte)* data, int size, qoi_desc *desc, int channels) 
378 { 
379     if ((channels < 0 && channels > 2) ||
380             size < QOIX_HEADER_SIZE + cast(int)(qoiplane_padding.sizeof)) 
381     {
382         return null;
383     }
384 
385     const(ubyte)* bytes = data;
386 
387     int p = 0;
388 
389     uint header_magic = qoi_read_32(bytes, &p);
390     desc.width = qoi_read_32(bytes, &p);
391     desc.height = qoi_read_32(bytes, &p);
392     int qoix_version = bytes[p++];
393     desc.channels = bytes[p++];
394     desc.bitdepth = bytes[p++];
395     desc.colorspace = bytes[p++];
396     desc.compression = bytes[p++];
397     desc.pixelAspectRatio = qoi_read_32f(bytes, &p);
398     desc.resolutionY = qoi_read_32f(bytes, &p);
399 
400     if (desc.width == 0 || desc.height == 0 || 
401         desc.channels < 1 || desc.channels > 2 ||
402         desc.colorspace > 1 ||
403         desc.bitdepth != 8 ||
404         qoix_version > 1 ||
405         desc.compression != QOIX_COMPRESSION_NONE ||
406         header_magic != QOIX_MAGIC ||
407         desc.height >= QOIX_PIXELS_MAX / desc.width
408         ) 
409     {
410         return null;
411     }
412 
413     if (channels == 0) 
414     {
415         channels = desc.channels;
416     }
417 
418     int stride = desc.width * channels;
419     desc.pitchBytes = stride; // FUTURE: force to decode with a given layout / image
420 
421     int num_pixels = desc.width * desc.height;
422     int output_bytes = num_pixels * channels;
423 
424     ubyte* pixels = cast(ubyte*) QOI_MALLOC(output_bytes);
425     if (!pixels) 
426         return null;
427 
428     bool readHiNibble = true; // nibble index into output stream.
429 
430     ubyte readNibble() nothrow @nogc
431     {
432         ubyte r;
433         if (readHiNibble)
434             r = (bytes[p] >>> 4);
435         else
436             r = (bytes[p++] & 0xf);
437         readHiNibble = !readHiNibble;
438         assert(r < 16);
439         return r;
440     }
441 
442     ubyte readUbyte()
443     {
444         ubyte hi = cast(ubyte)(readNibble() << 4);
445         ubyte lo = readNibble();
446         return hi | lo;
447     }
448 
449     qoi_la_t px = initialPredictor;
450     qoi_la_t px_ref = initialPredictor;
451 
452     int decoded_pixels = 0;
453     int run = 0;
454 
455     for (int posy = 0; posy < desc.height; ++posy)
456     {
457         ubyte* line = pixels + desc.pitchBytes * posy;
458 
459         // Note: don't read alpha in line above, since it may not exist if decoding 2 channels to 1
460         const(ubyte)* lineAbove = (posy > 0) ? (pixels + desc.pitchBytes * (posy - 1)) : null;
461 
462         for (int posx = 0; posx < desc.width; ++posx)
463         {
464             px_ref = px;
465 
466             if (run > 0) 
467             {
468                 run--;
469             }
470             else if (decoded_pixels < num_pixels)
471             {
472                 decode_op:
473                 ubyte op = readNibble();
474 
475                 if ((op & 0xf) == 0xf) // QOIPLANE_REPEAT2
476                 {
477                     run = readUbyte() + 3;
478                     if (run == 258) 
479                         run = 0x7fffffff; // fill with last pixel until end of decode
480                 }
481                 else if ((op & 0xc) == 0xc) // QOIPLANE_REPEAT1
482                 {
483                     run = (op & 0x3);
484                 }
485                 else
486                 {
487                     // Compute predictors.
488                     ubyte px_top = (posy > 0) ? lineAbove[posx * channels] : px_ref.l;
489                     ubyte px_avg = (px_top + px_ref.l + 1) / 2;
490 
491                     if ((op & 0x8) == 0) // QOIPLANE_DIFF1
492                     {
493                         assert(op < 8);
494                         px.l = cast(ubyte)(px_avg + op - 4);
495                     }
496                     else if ((op & 0xe) == 0x8) // QOIPLANE_DIFF2
497                     {
498                         int vg_l = ((op & 1) << 4) + readNibble();
499                         assert(vg_l >= 0 && vg_l <= 31);
500                         vg_l -= 16;
501                         px.l = cast(ubyte)(px_avg + vg_l);
502                     } 
503                     else if ((op & 0xf) == 0xa) // QOIPLANE_DIRECT
504                     {
505                         px.l = readUbyte();
506                     }
507                     else if ((op & 0xf) == 0xb)
508                     {
509                         int diff = readNibble();
510                         if (diff == 0) // QOIPLANE_LA
511                         {
512                             px.l = readUbyte();
513                             px.a = readUbyte();
514                         }
515                         else
516                         {
517                             // QOIPLANE_ADIFF
518                             px.a = cast(ubyte)(px_ref.a + diff - 8); // -7 to 7
519                             goto decode_op;
520                         }
521                     }
522                     else
523                         assert(false);
524                 }
525                 decoded_pixels++;
526             }
527 
528             if (channels == 1)
529             {
530                 line[posx * 1] = px.l;
531             }
532             else
533             {
534                 line[posx * 2 + 0] = px.l;
535                 line[posx * 2 + 1] = px.a;
536             }
537         }
538     }
539 
540     return pixels;
541 }