1 /**
2 QOIX support.
3 This is "living standard" format living in Gamut that tries to improve upon QOI.
4 
5 Copyright: Copyright Guillaume Piolat 2022
6 License:   $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
7 */
8 module gamut.plugins.qoix;
9 
10 nothrow @nogc @safe:
11 
12 import core.stdc.stdlib: malloc, free, realloc;
13 import core.stdc.string: memcpy;
14 import gamut.types;
15 import gamut.io;
16 import gamut.image;
17 import gamut.plugin;
18 import gamut.internals.errors;
19 import gamut.internals.types;
20 
21 version(decodeQOIX)
22 {
23     import gamut.codecs.qoi2avg;
24     import gamut.codecs.qoiplane;
25     import gamut.codecs.qoi10b;
26     import gamut.codecs.lz4;
27 }
28 else version(encodeQOIX)
29 {
30     import gamut.codecs.qoi2avg;
31     import gamut.codecs.qoi2plane;
32     import gamut.codecs.qoi10b;
33     import gamut.codecs.lz4;
34 }
35 
36 ImageFormatPlugin makeQOIXPlugin()
37 {
38     ImageFormatPlugin p;
39     p.format = "QOIX";
40     p.extensionList = "qoix";
41 
42     p.mimeTypes = "image/qoix";
43 
44     version(decodeQOIX)
45         p.loadProc = &loadQOIX;
46     else
47         p.loadProc = null;
48     version(encodeQOIX)
49         p.saveProc = &saveQOIX;
50     else
51         p.saveProc = null;
52     p.detectProc = &detectQOIX;
53     return p;
54 }
55 
56 // IMPORTANT: QOIX uses 3 possible codecs internally
57 //   - QOI2AVG in qoi2avg.d for RGB8 and RGBA8
58 //   - QOI-Plane for L8/LA8
59 //   - QOI-10b for 16-bit (lossy)
60 
61 version(decodeQOIX)
62 void loadQOIX(ref Image image, IOStream *io, IOHandle handle, int page, int flags, void *data) @trusted
63 {
64     // Read all available bytes from input
65     // This is temporary.
66 
67     // Find length of input
68     if (io.seek(handle, 0, SEEK_END) != 0)
69     {
70         image.error(kStrImageDecodingIOFailure);
71         return;
72     }
73 
74     int len = cast(int) io.tell(handle); // works, see io.d for why
75 
76     if (!io.rewind(handle))
77     {
78         image.error(kStrImageDecodingIOFailure);
79         return;
80     }
81 
82     ubyte* buf = cast(ubyte*) malloc(len);
83     if (buf is null)
84     {
85         image.error(kStrImageDecodingMallocFailure);
86         return;
87     }
88     scope(exit) free(buf);
89 
90     int requestedComp = computeRequestedImageComponents(flags);
91     if (requestedComp == 0) // error
92     {
93         image.error(kStrInvalidFlags);
94         return;
95     }
96     if (requestedComp == -1)
97         requestedComp = 0; // auto
98 
99     ubyte* decoded;
100     qoi_desc desc;
101 
102     // read all input at once.
103     if (len != io.read(buf, 1, len, handle))
104     {
105         image.error(kStrImageDecodingIOFailure);
106         return;
107     }
108 
109     PixelType decodedToType;
110     decoded = cast(ubyte*) qoix_lz4_decode(buf, len, &desc, flags, decodedToType);
111 
112     // Note: do not use desc.channels or desc.bits here, it doesn't mean anything anymore.
113 
114     if (decoded is null)
115     {
116         image.error(kStrImageDecodingFailed);
117         return;
118     }    
119 
120     if (!imageIsValidSize(desc.width, desc.height))
121     {
122         image.error(kStrImageTooLarge);
123         free(decoded);
124         return;
125     }
126 
127     image._allocArea = decoded;
128     image._data = decoded;
129     image._width = desc.width;
130     image._height = desc.height;
131 
132     // PERF: allocate a QOIX decoding buffer with proper layout by passing layoutConstraints to qoix_lz4_decode
133     image._layoutConstraints = 0; // No particular constraint followed in QOIX decoder, for now.
134 
135     image._type = decodedToType;
136     image._pitch = desc.pitchBytes;
137     image._pixelAspectRatio = desc.pixelAspectRatio;
138     image._resolutionY = desc.resolutionY;
139 
140     // Convert to target type and constraints.
141     image.convertTo(applyLoadFlags(image._type, flags), cast(LayoutConstraints) flags);
142 }
143 
144 
145 bool detectQOIX(IOStream *io, IOHandle handle) @trusted
146 {
147     static immutable ubyte[4] qoixSignature = [0x71, 0x6f, 0x69, 0x78]; // "qoix"
148     return fileIsStartingWithSignature(io, handle, qoixSignature);
149 }
150 
151 version(encodeQOIX)
152 bool saveQOIX(ref const(Image) image, IOStream *io, IOHandle handle, int page, int flags, void *data) @trusted
153 {
154     if (page != 0)
155         return false;
156 
157     qoi_desc desc;
158     desc.width = image._width;
159     desc.height = image._height;
160     desc.pitchBytes = image._pitch;
161     desc.colorspace = QOI_SRGB;
162     desc.compression = QOIX_COMPRESSION_NONE; // whatever, this will get overwritten. QOIX is valid with 0 or 1.
163     desc.pixelAspectRatio = image._pixelAspectRatio;
164     desc.resolutionY = image._resolutionY;
165 
166     switch (image._type)
167     {
168         case PixelType.l8: 
169             desc.bitdepth = 8;
170             desc.channels = 1; 
171             break;
172         case PixelType.la8:  
173             desc.bitdepth = 8;
174             desc.channels = 2; 
175             break;
176         case PixelType.rgb8: 
177             desc.bitdepth = 8;
178             desc.channels = 3; 
179             break;
180         case PixelType.rgba8:
181             desc.bitdepth = 8;
182             desc.channels = 4; 
183             break;
184         case PixelType.l16: 
185             desc.channels = 1; 
186             desc.bitdepth = 10;
187             break;
188         case PixelType.la16:   
189             desc.channels = 2; 
190             desc.bitdepth = 10;
191             break;
192         case PixelType.rgb16:  
193             desc.channels = 3; 
194             desc.bitdepth = 10;
195             break;
196         case PixelType.rgba16: 
197             desc.channels = 4; 
198             desc.bitdepth = 10;
199             break;
200         default: 
201             return false; // not supported
202     }
203         
204     int qoilen;
205 
206     // Note: this can, or not, encode to LZ4 the payload.
207     ubyte* encoded = cast(ubyte*) qoix_lz4_encode(image._data, &desc, &qoilen);
208 
209     if (encoded == null)
210         return false;
211     scope(exit) free(encoded);
212 
213     // Write all output at once.
214     if (qoilen != io.write(encoded, 1, qoilen, handle))
215         return false;
216 
217     return true;
218 }
219 
220 /// Encode in QOIX + LZ4. Result should be freed with `free()`.
221 /// File format of final QOIX:
222 ///   QOIX header (QOIX_HEADER_SIZE bytes with compression = QOIX_COMPRESSION_LZ4)
223 ///   Original data size (4 bytes)
224 ///   LZ4 encoded opcodes
225 /// Note: desc.compression is ignored. This function chooses the compression.
226 version(encodeQOIX)
227 ubyte* qoix_lz4_encode(const(ubyte)* data, const(qoi_desc)* desc, int *out_len) @trusted
228 {
229     // Encode to QOIX
230     int qoilen;
231     ubyte* qoix;
232 
233     // Choose a codec based upon input data.
234     // 10-bit is always QOI-10b.
235     // 8-bit with 1 or 2 channels is QOI-Plane.
236     // 8-bit with 3 or 4 channels is QOI2AVG.
237     // All these sub-codecs have the same header format, and can be LZ4-encoded further.
238     if (desc.bitdepth == 10)
239     {
240         qoix = qoi10b_encode(data, desc, &qoilen);
241     }
242     else
243     {
244         assert(desc.bitdepth == 8);
245         if (desc.channels == 1 || desc.channels == 2)
246         {
247             qoix = qoiplane_encode(data, desc, &qoilen);
248         }
249         else
250         {
251             qoix = qoix_encode(data, desc, &qoilen);
252         }
253     }
254 
255     if (qoix is null)
256         return null;
257 
258     ubyte[] qoixHeader = qoix[0..QOIX_HEADER_SIZE];
259     ubyte[] qoixData = qoix[QOIX_HEADER_SIZE..qoilen];
260     int datalen = cast(int) qoixData.length;
261 
262     int originalDataSize = cast(int) qoixData.length;
263 
264 
265     // Encode QOI in LZ4, except the header. Is it smaller?
266     int maxsize = LZ4_compressBound(datalen);
267     ubyte* lz4Data = cast(ubyte*) malloc(QOIX_HEADER_SIZE + 4 + maxsize);
268     lz4Data[0..QOIX_HEADER_SIZE] = qoix[0..QOIX_HEADER_SIZE];
269     int p = QOIX_HEADER_SIZE;
270     qoi_write_32(lz4Data, &p, datalen);
271     int lz4Size = LZ4_compress(cast(const(char)*)&qoixData[0], 
272                                cast(char*)&lz4Data[QOIX_HEADER_SIZE + 4], 
273                                datalen);
274     if (lz4Size < 0)
275     {
276         free(qoix);
277         return null; // compression attempt failed, this is an error
278     }
279 
280     // Only use LZ4 compression in the end if it was actually smaller.
281     bool useCompressed = lz4Size + 4 < originalDataSize;
282     if (useCompressed)
283     {
284         free(qoix); // free original uncompressed QOIX
285         *out_len = QOIX_HEADER_SIZE + 4 + lz4Size;
286         lz4Data = cast(ubyte*) realloc(lz4Data, *out_len); // realloc this to fit memory to actually used
287         lz4Data[QOIX_HEADER_OFFSET_COMPRESSION] = QOIX_COMPRESSION_LZ4;
288         return lz4Data;
289     }
290     else
291     {
292         free(lz4Data);
293         *out_len = qoilen;
294         assert(qoix[QOIX_HEADER_OFFSET_COMPRESSION] == QOIX_COMPRESSION_NONE);
295         return qoix; // return original QOIX
296     }
297 }
298 
299 /// Decodes a QOIX + LZ4
300 /// File format:
301 ///   QOIX header (15 bytes)
302 ///   Original data size (4 bytes)
303 ///   LZ4 encoded opcodes
304 /// Warning: qoi_desc.channels is the encoded channel count.
305 /// requestedType may or may not be followed as a wish.
306 /// The actual type, after flags applied, is in decodedType.
307 version(decodeQOIX)
308 ubyte* qoix_lz4_decode(const(ubyte)* data, 
309                        int size, 
310                        qoi_desc *desc, 
311                        int flags,
312                        out PixelType decodedType) @trusted
313 {
314     if (size < QOIX_HEADER_SIZE)
315         return null;
316 
317     if (!validLoadFlags(flags))
318         return null;
319 
320     int compression    = data[QOIX_HEADER_OFFSET_COMPRESSION];
321     int streamChannels = data[QOIX_HEADER_OFFSET_CHANNELS];
322     int streamBitdepth = data[QOIX_HEADER_OFFSET_BITDEPTH];
323 
324     // What type should it be once decompressed?
325     PixelType streamType;
326     if (!identifyTypeFromStream(streamChannels, streamBitdepth, streamType))
327     {
328         // Corrupted stream, unknown type.
329         return null;
330     }
331 
332     int uncompressedQOIXSize;
333     const(ubyte)* uncompressedQOIX = null;
334     ubyte* decQOIX = null;
335 
336     if (compression == QOIX_COMPRESSION_LZ4)
337     {
338         if (size < QOIX_HEADER_SIZE + 4)
339             return null;
340 
341         // Read original size of data.
342         int p = QOIX_HEADER_SIZE;
343         int orig = qoi_read_32(data, &p);
344 
345         if (orig < 0)
346             return null; // too large, corrupted.
347 
348         // Allocate decoding buffer for uncompressed QOIX.
349         decQOIX = cast(ubyte*) malloc(QOIX_HEADER_SIZE + orig);
350 
351         decQOIX[0..QOIX_HEADER_SIZE] = data[0..QOIX_HEADER_SIZE];
352         decQOIX[QOIX_HEADER_OFFSET_COMPRESSION] = QOIX_COMPRESSION_NONE; // remove "compressed" label in header
353 
354         const(ubyte)[] lz4Data = data[QOIX_HEADER_SIZE + 4 ..size];
355 
356         int qoilen = LZ4_decompress_fast(cast(char*)&lz4Data[0], cast(char*)&decQOIX[QOIX_HEADER_SIZE], orig);
357 
358         if (qoilen < 0)
359         {
360             free(decQOIX);
361             return null;
362         }
363 
364         uncompressedQOIXSize = QOIX_HEADER_SIZE + orig;
365         uncompressedQOIX = decQOIX;
366     }
367     else if (compression == QOIX_COMPRESSION_NONE)
368     {
369         uncompressedQOIXSize = size;
370         uncompressedQOIX = data;
371     }
372     else
373         return null;
374 
375  
376     ubyte* image;
377     if (streamBitdepth == 10)
378     {
379         // Using qoi10b.d codec
380         decodedType = applyLoadFlags_QOI10b(streamType, flags);
381         decodedType = streamType;
382         int channels = pixelTypeNumChannels(decodedType);
383 
384         // This codec can convert 1/2/3/4 to 1/2/3/4 channels on decode, per scanline.
385         image = qoi10b_decode(uncompressedQOIX, uncompressedQOIXSize, desc, channels);
386     }
387     else if (streamBitdepth == 8)
388     {
389         if (streamChannels == 1 || streamChannels == 2)
390         {
391             // Using qoiplane.d codec
392             decodedType = applyLoadFlags_QOIPlane(streamType, flags);
393             decodedType = streamType;
394             int channels = pixelTypeNumChannels(decodedType);
395             image = qoiplane_decode(uncompressedQOIX, uncompressedQOIXSize, desc, channels);
396         }
397         else if (streamChannels == 3 || streamChannels == 4)
398         {
399             // Using qoi2avg.d codec
400             decodedType = applyLoadFlags_QOI2AVG(streamType, flags);
401             decodedType = streamType;
402             int channels = pixelTypeNumChannels(decodedType);
403             image = qoix_decode(uncompressedQOIX, uncompressedQOIXSize, desc, channels);
404         }
405     }
406     else
407     {
408         free(decQOIX);
409         return null;
410     }
411 
412     scope(exit) free(decQOIX);
413 
414     return image;
415 }
416 
417 // Construct output type from channel count and bitness.
418 bool identifyTypeFromStream(int channels, int bitdepth, out PixelType type)
419 {
420     if (bitdepth == 8)
421     {
422         if (channels == 1)
423             type = PixelType.l8;
424         else if (channels == 2)
425             type = PixelType.la8;
426         else if (channels == 3)
427             type = PixelType.rgb8;
428         else if (channels == 4)
429             type = PixelType.rgba8;
430         else
431             return false;
432     }
433     else if (bitdepth == 10)
434     {
435         if (channels == 1)
436             type = PixelType.l16;
437         else if (channels == 2)
438             type = PixelType.la16;
439         else if (channels == 3)
440             type = PixelType.rgb16;
441         else if (channels == 4)
442             type = PixelType.rgba16;
443         else
444             return false;
445     }
446     else
447         return false;
448     return true;
449 }
450 
451 // Given those load flags, what is the best effort the decoder can do?
452 PixelType applyLoadFlags_QOI2AVG(PixelType type, LoadFlags flags)
453 {
454     if (pixelTypeIs8Bit(type))
455     {
456         // QOI2AVG can only convert rgb8 <=> rgba8 at decode-time
457         if (flags & LOAD_ALPHA)
458             type = convertPixelTypeToAddAlphaChannel(type);
459 
460         if (flags & LOAD_NO_ALPHA)
461             type = convertPixelTypeToDropAlphaChannel(type);
462     }
463     return type;
464 }
465 
466 // Given those load flags, what is the best effort the decoder can do?
467 PixelType applyLoadFlags_QOIPlane(PixelType type, LoadFlags flags)
468 {
469     if (pixelTypeIs8Bit(type))
470     {
471         // QOIPlane can convert ubyte8 <=> la8
472         if (flags & LOAD_ALPHA)
473             type = convertPixelTypeToAddAlphaChannel(type);
474 
475         if (flags & LOAD_NO_ALPHA)
476             type = convertPixelTypeToDropAlphaChannel(type);
477     }
478     return type;
479 }
480 
481 // Given those load flags, what is the best effort the decoder can do?
482 PixelType applyLoadFlags_QOI10b(PixelType type, LoadFlags flags)
483 {
484     // QOI-10b can convert to 1/2/3/4 channels at decode-time
485     if (pixelTypeIs16Bit(type))
486     {
487         if (flags & LOAD_GREYSCALE)
488             type = convertPixelTypeToGreyscale(type);
489 
490         if (flags & LOAD_RGB)
491             type = convertPixelTypeToRGB(type);
492 
493         if (flags & LOAD_ALPHA)
494             type = convertPixelTypeToAddAlphaChannel(type);
495 
496         if (flags & LOAD_NO_ALPHA)
497             type = convertPixelTypeToDropAlphaChannel(type);
498     }
499     return type;
500 }