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 }