The OpenD Programming Language

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(1, 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     image._layerCount = 1;
140     image._layerOffset = 0;
141 
142     // Convert to target type and constraints.
143     image.convertTo(applyLoadFlags(image._type, flags), cast(LayoutConstraints) flags);
144 }
145 
146 
147 bool detectQOIX(IOStream *io, IOHandle handle) @trusted
148 {
149     static immutable ubyte[4] qoixSignature = [0x71, 0x6f, 0x69, 0x78]; // "qoix"
150     return fileIsStartingWithSignature(io, handle, qoixSignature);
151 }
152 
153 version(encodeQOIX)
154 bool saveQOIX(ref const(Image) image, IOStream *io, IOHandle handle, int page, int flags, void *data) @trusted
155 {
156     if (page != 0)
157         return false;
158 
159     qoi_desc desc;
160     desc.width = image._width;
161     desc.height = image._height;
162     desc.pitchBytes = image._pitch;
163     desc.colorspace = QOI_SRGB;
164     desc.compression = QOIX_COMPRESSION_NONE; // whatever, this will get overwritten. QOIX is valid with 0 or 1.
165     desc.pixelAspectRatio = image._pixelAspectRatio;
166     desc.resolutionY = image._resolutionY;
167 
168     switch (image._type)
169     {
170         case PixelType.l8: 
171             desc.bitdepth = 8;
172             desc.channels = 1; 
173             break;
174         case PixelType.la8:  
175             desc.bitdepth = 8;
176             desc.channels = 2; 
177             break;
178         case PixelType.rgb8: 
179             desc.bitdepth = 8;
180             desc.channels = 3; 
181             break;
182         case PixelType.rgba8:
183             desc.bitdepth = 8;
184             desc.channels = 4; 
185             break;
186         case PixelType.l16: 
187             desc.channels = 1; 
188             desc.bitdepth = 10;
189             break;
190         case PixelType.la16:   
191             desc.channels = 2; 
192             desc.bitdepth = 10;
193             break;
194         case PixelType.rgb16:  
195             desc.channels = 3; 
196             desc.bitdepth = 10;
197             break;
198         case PixelType.rgba16: 
199             desc.channels = 4; 
200             desc.bitdepth = 10;
201             break;
202         default: 
203             return false; // not supported
204     }
205         
206     int qoilen;
207 
208     // Note: this can, or not, encode to LZ4 the payload.
209     ubyte* encoded = cast(ubyte*) qoix_lz4_encode(image._data, &desc, &qoilen);
210 
211     if (encoded == null)
212         return false;
213     scope(exit) free(encoded);
214 
215     // Write all output at once.
216     if (qoilen != io.write(encoded, 1, qoilen, handle))
217         return false;
218 
219     return true;
220 }
221 
222 /// Encode in QOIX + LZ4. Result should be freed with `free()`.
223 /// File format of final QOIX:
224 ///   QOIX header (QOIX_HEADER_SIZE bytes with compression = QOIX_COMPRESSION_LZ4)
225 ///   Original data size (4 bytes)
226 ///   LZ4 encoded opcodes
227 /// Note: desc.compression is ignored. This function chooses the compression.
228 version(encodeQOIX)
229 ubyte* qoix_lz4_encode(const(ubyte)* data, const(qoi_desc)* desc, int *out_len) @trusted
230 {
231     // Encode to QOIX
232     int qoilen;
233     ubyte* qoix;
234 
235     // Choose a codec based upon input data.
236     // 10-bit is always QOI-10b.
237     // 8-bit with 1 or 2 channels is QOI-Plane.
238     // 8-bit with 3 or 4 channels is QOI2AVG.
239     // All these sub-codecs have the same header format, and can be LZ4-encoded further.
240     if (desc.bitdepth == 10)
241     {
242         qoix = qoi10b_encode(data, desc, &qoilen);
243     }
244     else
245     {
246         assert(desc.bitdepth == 8);
247         if (desc.channels == 1 || desc.channels == 2)
248         {
249             qoix = qoiplane_encode(data, desc, &qoilen);
250         }
251         else
252         {
253             qoix = qoix_encode(data, desc, &qoilen);
254         }
255     }
256 
257     if (qoix is null)
258         return null;
259 
260     ubyte[] qoixHeader = qoix[0..QOIX_HEADER_SIZE];
261     ubyte[] qoixData = qoix[QOIX_HEADER_SIZE..qoilen];
262     int datalen = cast(int) qoixData.length;
263 
264     int originalDataSize = cast(int) qoixData.length;
265 
266 
267     // Encode QOI in LZ4, except the header. Is it smaller?
268     int maxsize = LZ4_compressBound(datalen);
269     ubyte* lz4Data = cast(ubyte*) malloc(QOIX_HEADER_SIZE + 4 + maxsize);
270     lz4Data[0..QOIX_HEADER_SIZE] = qoix[0..QOIX_HEADER_SIZE];
271     int p = QOIX_HEADER_SIZE;
272     qoi_write_32(lz4Data, &p, datalen);
273     int lz4Size = LZ4_compress(cast(const(char)*)&qoixData[0], 
274                                cast(char*)&lz4Data[QOIX_HEADER_SIZE + 4], 
275                                datalen);
276     if (lz4Size < 0)
277     {
278         free(qoix);
279         return null; // compression attempt failed, this is an error
280     }
281 
282     // Only use LZ4 compression in the end if it was actually smaller.
283     bool useCompressed = lz4Size + 4 < originalDataSize;
284     if (useCompressed)
285     {
286         free(qoix); // free original uncompressed QOIX
287         *out_len = QOIX_HEADER_SIZE + 4 + lz4Size;
288         lz4Data = cast(ubyte*) realloc(lz4Data, *out_len); // realloc this to fit memory to actually used
289         lz4Data[QOIX_HEADER_OFFSET_COMPRESSION] = QOIX_COMPRESSION_LZ4;
290         return lz4Data;
291     }
292     else
293     {
294         free(lz4Data);
295         *out_len = qoilen;
296         assert(qoix[QOIX_HEADER_OFFSET_COMPRESSION] == QOIX_COMPRESSION_NONE);
297 
298         // tighten the QOIX allocation in order to save bytes
299         qoix = cast(ubyte*) realloc(qoix, qoilen);
300 
301         return qoix; // return original QOIX
302     }
303 }
304 
305 /// Decodes a QOIX + LZ4
306 /// File format:
307 ///   QOIX header (15 bytes)
308 ///   Original data size (4 bytes)
309 ///   LZ4 encoded opcodes
310 /// Warning: qoi_desc.channels is the encoded channel count.
311 /// requestedType may or may not be followed as a wish.
312 /// The actual type, after flags applied, is in decodedType.
313 version(decodeQOIX)
314 ubyte* qoix_lz4_decode(const(ubyte)* data, 
315                        int size, 
316                        qoi_desc *desc, 
317                        int flags,
318                        out PixelType decodedType) @trusted
319 {
320     if (size < QOIX_HEADER_SIZE)
321         return null;
322 
323     if (!validLoadFlags(flags))
324         return null;
325 
326     int compression    = data[QOIX_HEADER_OFFSET_COMPRESSION];
327     int streamChannels = data[QOIX_HEADER_OFFSET_CHANNELS];
328     int streamBitdepth = data[QOIX_HEADER_OFFSET_BITDEPTH];
329 
330     // What type should it be once decompressed?
331     PixelType streamType;
332     if (!identifyTypeFromStream(streamChannels, streamBitdepth, streamType))
333     {
334         // Corrupted stream, unknown type.
335         return null;
336     }
337 
338     int uncompressedQOIXSize;
339     const(ubyte)* uncompressedQOIX = null;
340     ubyte* decQOIX = null;
341 
342     if (compression == QOIX_COMPRESSION_LZ4)
343     {
344         if (size < QOIX_HEADER_SIZE + 4)
345             return null;
346 
347         // Read original size of data.
348         int p = QOIX_HEADER_SIZE;
349         int orig = qoi_read_32(data, &p);
350 
351         if (orig < 0)
352             return null; // too large, corrupted.
353 
354         // Allocate decoding buffer for uncompressed QOIX.
355         decQOIX = cast(ubyte*) malloc(QOIX_HEADER_SIZE + orig);
356 
357         decQOIX[0..QOIX_HEADER_SIZE] = data[0..QOIX_HEADER_SIZE];
358         decQOIX[QOIX_HEADER_OFFSET_COMPRESSION] = QOIX_COMPRESSION_NONE; // remove "compressed" label in header
359 
360         const(ubyte)[] lz4Data = data[QOIX_HEADER_SIZE + 4 ..size];
361 
362         int qoilen = LZ4_decompress_fast(cast(char*)&lz4Data[0], cast(char*)&decQOIX[QOIX_HEADER_SIZE], orig);
363 
364         if (qoilen < 0)
365         {
366             free(decQOIX);
367             return null;
368         }
369 
370         uncompressedQOIXSize = QOIX_HEADER_SIZE + orig;
371         uncompressedQOIX = decQOIX;
372     }
373     else if (compression == QOIX_COMPRESSION_NONE)
374     {
375         uncompressedQOIXSize = size;
376         uncompressedQOIX = data;
377     }
378     else
379         return null;
380 
381  
382     ubyte* image;
383     if (streamBitdepth == 10)
384     {
385         // Using qoi10b.d codec
386         decodedType = applyLoadFlags_QOI10b(streamType, flags);
387         decodedType = streamType;
388         int channels = pixelTypeNumChannels(decodedType);
389 
390         // This codec can convert 1/2/3/4 to 1/2/3/4 channels on decode, per scanline.
391         image = qoi10b_decode(uncompressedQOIX, uncompressedQOIXSize, desc, channels);
392     }
393     else if (streamBitdepth == 8)
394     {
395         if (streamChannels == 1 || streamChannels == 2)
396         {
397             // Using qoiplane.d codec
398             decodedType = applyLoadFlags_QOIPlane(streamType, flags);
399             decodedType = streamType;
400             int channels = pixelTypeNumChannels(decodedType);
401             image = qoiplane_decode(uncompressedQOIX, uncompressedQOIXSize, desc, channels);
402         }
403         else if (streamChannels == 3 || streamChannels == 4)
404         {
405             // Using qoi2avg.d codec
406             decodedType = applyLoadFlags_QOI2AVG(streamType, flags);
407             decodedType = streamType;
408             int channels = pixelTypeNumChannels(decodedType);
409             image = qoix_decode(uncompressedQOIX, uncompressedQOIXSize, desc, channels);
410         }
411     }
412     else
413     {
414         free(decQOIX);
415         return null;
416     }
417 
418     scope(exit) free(decQOIX);
419 
420     return image;
421 }
422 
423 // Construct output type from channel count and bitness.
424 bool identifyTypeFromStream(int channels, int bitdepth, out PixelType type)
425 {
426     if (bitdepth == 8)
427     {
428         if (channels == 1)
429             type = PixelType.l8;
430         else if (channels == 2)
431             type = PixelType.la8;
432         else if (channels == 3)
433             type = PixelType.rgb8;
434         else if (channels == 4)
435             type = PixelType.rgba8;
436         else
437             return false;
438     }
439     else if (bitdepth == 10)
440     {
441         if (channels == 1)
442             type = PixelType.l16;
443         else if (channels == 2)
444             type = PixelType.la16;
445         else if (channels == 3)
446             type = PixelType.rgb16;
447         else if (channels == 4)
448             type = PixelType.rgba16;
449         else
450             return false;
451     }
452     else
453         return false;
454     return true;
455 }
456 
457 // Given those load flags, what is the best effort the decoder can do?
458 PixelType applyLoadFlags_QOI2AVG(PixelType type, LoadFlags flags)
459 {
460     if (pixelTypeIs8Bit(type))
461     {
462         // QOI2AVG can only convert rgb8 <=> rgba8 at decode-time
463         if (flags & LOAD_ALPHA)
464             type = convertPixelTypeToAddAlphaChannel(type);
465 
466         if (flags & LOAD_NO_ALPHA)
467             type = convertPixelTypeToDropAlphaChannel(type);
468     }
469     return type;
470 }
471 
472 // Given those load flags, what is the best effort the decoder can do?
473 PixelType applyLoadFlags_QOIPlane(PixelType type, LoadFlags flags)
474 {
475     if (pixelTypeIs8Bit(type))
476     {
477         // QOIPlane can convert ubyte8 <=> la8
478         if (flags & LOAD_ALPHA)
479             type = convertPixelTypeToAddAlphaChannel(type);
480 
481         if (flags & LOAD_NO_ALPHA)
482             type = convertPixelTypeToDropAlphaChannel(type);
483     }
484     return type;
485 }
486 
487 // Given those load flags, what is the best effort the decoder can do?
488 PixelType applyLoadFlags_QOI10b(PixelType type, LoadFlags flags)
489 {
490     // QOI-10b can convert to 1/2/3/4 channels at decode-time
491     if (pixelTypeIs16Bit(type))
492     {
493         if (flags & LOAD_GREYSCALE)
494             type = convertPixelTypeToGreyscale(type);
495 
496         if (flags & LOAD_RGB)
497             type = convertPixelTypeToRGB(type);
498 
499         if (flags & LOAD_ALPHA)
500             type = convertPixelTypeToAddAlphaChannel(type);
501 
502         if (flags & LOAD_NO_ALPHA)
503             type = convertPixelTypeToDropAlphaChannel(type);
504     }
505     return type;
506 }