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 }