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 }