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 version(encodeQOIX) 109 ubyte* qoiplane_encode(const(ubyte)* data, const(qoi_desc)* desc, int *out_len) 110 { 111 if ( (desc.channels != 1 && desc.channels != 2) || 112 desc.height >= QOIX_PIXELS_MAX / desc.width || 113 desc.compression != QOIX_COMPRESSION_NONE 114 ) { 115 return null; 116 } 117 118 if (desc.bitdepth != 8) 119 return null; 120 121 int channels = desc.channels; 122 123 // At worst, each pixel take 12 bit to be encoded. 124 int num_pixels = desc.width * desc.height; 125 int worst_case_nibbles_for_one_pixel = (channels == 1 ? 3 : 6); 126 int max_size = (num_pixels * worst_case_nibbles_for_one_pixel + 1) / 2 127 + QOIX_HEADER_SIZE + cast(int)(qoiplane_padding.sizeof); 128 129 ubyte* stream; 130 131 int p = 0; // write index into output stream 132 ubyte* bytes = cast(ubyte*) QOI_MALLOC(max_size); 133 if (!bytes) 134 { 135 return null; 136 } 137 138 version(benchmark) 139 { 140 int numQOIPLANE_DIFF1 = 0; 141 int numQOIPLANE_DIFF2 = 0; 142 int numQOIPLANE_DIRECT = 0; 143 int numQOIPLANE_REPEAT1 = 0; 144 int numQOIPLANE_REPEAT2 = 0; 145 int numQOIPLANE_LA = 0; 146 147 int encodedQOIPLANE_REPEAT1 = 0; 148 int encodedQOIPLANE_REPEAT2 = 0; 149 } 150 151 qoi_write_32(bytes, &p, QOIX_MAGIC); 152 qoi_write_32(bytes, &p, desc.width); 153 qoi_write_32(bytes, &p, desc.height); 154 bytes[p++] = 1; // Put a version number :) 155 bytes[p++] = desc.channels; // 1, or 2 156 bytes[p++] = desc.bitdepth; // 8, or 10 157 bytes[p++] = desc.colorspace; 158 bytes[p++] = QOIX_COMPRESSION_NONE; 159 qoi_write_32f(bytes, &p, desc.pixelAspectRatio); 160 qoi_write_32f(bytes, &p, desc.resolutionY); 161 162 bool writeHiNibble = true; // nibble index into output stream. 163 164 void outputNibble(ubyte nibble) nothrow @nogc 165 { 166 assert(nibble < 16); 167 if (writeHiNibble) 168 { 169 bytes[p] = cast(ubyte)(nibble << 4); 170 } 171 else 172 { 173 bytes[p++] |= nibble; 174 } 175 writeHiNibble = !writeHiNibble; 176 } 177 178 void outputByte(ubyte b) 179 { 180 if (writeHiNibble) 181 { 182 bytes[p++] = b; 183 } 184 else 185 { 186 bytes[p++] |= (b >>> 4); 187 bytes[p] = cast(ubyte)(b << 4); 188 } 189 } 190 191 void encodeRun(ref int run) nothrow @nogc 192 { 193 assert(run > 0 && run <= 258); 194 if (run <= 3) 195 { 196 ubyte nibble = 0xc | cast(ubyte)(run - 1); 197 outputNibble(nibble); // QOIPLANE_REPEAT1 198 version(benchmark) 199 { 200 numQOIPLANE_REPEAT1++; 201 encodedQOIPLANE_REPEAT1 += run; 202 } 203 } 204 else 205 { 206 run -= 4; 207 outputNibble(0xf); // QOIPLANE_REPEAT2 208 outputByte(cast(ubyte)run); 209 version(benchmark) 210 { 211 numQOIPLANE_REPEAT2++; 212 encodedQOIPLANE_REPEAT2 += run; 213 } 214 } 215 run = 0; 216 } 217 218 qoi_la_t px = initialPredictor; 219 qoi_la_t px_ref = initialPredictor; 220 221 int stride = desc.width * channels; 222 int run = 0; 223 int pixels_encoded = 0; 224 225 for (int posy = 0; posy < desc.height; ++posy) 226 { 227 const(ubyte)* line = data + desc.pitchBytes * posy; 228 const(ubyte)* lineAbove = (posy > 0) ? (data + desc.pitchBytes * (posy - 1)) : null; 229 230 for (int posx = 0; posx < desc.width; ++posx) 231 { 232 // last pixel is the new predictor 233 px_ref = px; 234 235 // take next pixel to encode 236 if (channels == 1) 237 { 238 px.l = line[posx * channels]; 239 } 240 else 241 { 242 px.l = line[posx * channels + 0]; 243 px.a = line[posx * channels + 1]; 244 } 245 246 if (px == px_ref) 247 { 248 run++; 249 if (run == 258 || (pixels_encoded + 1 == num_pixels)) 250 encodeRun(run); 251 } 252 else 253 { 254 if (run > 0) 255 encodeRun(run); 256 257 byte va = cast(byte)(px.a - px_ref.a); 258 259 if (va) 260 { 261 assert(channels == 2); 262 263 if (va >= -7 && va <= 7) 264 { 265 outputNibble( 0xb); 266 outputNibble( cast(ubyte)(va + 8) ); // QOIPLANE_ADIFF 267 goto encode_color; 268 } 269 else 270 { 271 outputNibble(0xb); // QOIPLANE_LA 272 outputNibble(0x0); 273 outputByte(px.l); 274 outputByte(px.a); 275 version(benchmark) numQOIPLANE_LA++; 276 } 277 } 278 else 279 { 280 encode_color: 281 282 // take top pixel (if it exist), else it's the same predictor 283 ubyte px_top = (posy > 0) ? lineAbove[posx * channels] : px_ref.l; 284 ubyte px_avg = (px_top + px_ref.l + 1) / 2; 285 286 byte diff_avg = cast(byte)(px.l - px_avg); 287 288 if (diff_avg >= -4 && diff_avg <= 3) 289 { 290 ubyte nibble = 0x0 | cast(ubyte)(diff_avg + 4); 291 outputNibble(nibble); // QOIPLANE_DIFF1 292 version(benchmark) numQOIPLANE_DIFF1++; 293 } 294 else if (diff_avg >= -16 && diff_avg <= 15) 295 { 296 ubyte diff2b = 0x80 | cast(ubyte)(diff_avg + 16); 297 outputByte(diff2b); // QOIPLANE_DIFF2 298 version(benchmark) numQOIPLANE_DIFF2++; 299 } 300 else 301 { 302 outputNibble(0xa); // QOIPLANE_DIRECT 303 outputByte(px.l); 304 version(benchmark) numQOIPLANE_DIRECT++; 305 } 306 } 307 } 308 309 pixels_encoded++; 310 } 311 } 312 313 // Put 3x QOIPLANE_REPEAT2 with full bits in order to have 4 0xff bytes 314 foreach(i; 0..9) outputNibble(0xf); 315 316 // Last nibble to fit 317 if (!writeHiNibble) outputNibble(0xf); 318 319 320 version(benchmark) 321 { 322 double totalOps = numQOIPLANE_DIFF1 + numQOIPLANE_DIFF2 + numQOIPLANE_DIRECT + numQOIPLANE_REPEAT1 + numQOIPLANE_REPEAT2; 323 324 double pixelsQOIPLANE_DIFF1 = numQOIPLANE_DIFF1 / cast(double)pixels_encoded; 325 double pixelsQOIPLANE_DIFF2 = numQOIPLANE_DIFF2 / cast(double)pixels_encoded; 326 double pixelsQOIPLANE_DIRECT = numQOIPLANE_DIRECT / cast(double)pixels_encoded; 327 double pixelsQOIPLANE_REPEAT1 = encodedQOIPLANE_REPEAT1 / cast(double)pixels_encoded; 328 double pixelsQOIPLANE_REPEAT2 = encodedQOIPLANE_REPEAT2 / cast(double)pixels_encoded; 329 double pixelsQOIPLANE_LA = numQOIPLANE_LA / cast(double)pixels_encoded; 330 331 double sizeQOIPLANE_DIFF1 = 4 * numQOIPLANE_DIFF1 / (8.0 * p); 332 double sizeQOIPLANE_DIFF2 = 8 * numQOIPLANE_DIFF2 / (8.0 * p); 333 double sizeQOIPLANE_DIRECT = 12 * numQOIPLANE_DIRECT / (8.0 * p); 334 double sizeQOIPLANE_REPEAT1 = 4 * numQOIPLANE_REPEAT1 / (8.0 * p); 335 double sizeQOIPLANE_REPEAT2 = 12 * numQOIPLANE_REPEAT2 / (8.0 * p); 336 double sizeQOIPLANE_LA = 20 * numQOIPLANE_LA / (8.0 * p); 337 338 printf("Num QOIPLANE_DIFF1 = %d\n", numQOIPLANE_DIFF1); 339 printf(" * pixels = %.2f\n", pixelsQOIPLANE_DIFF1 * 100); 340 printf(" * size = %.2f\n\n", sizeQOIPLANE_DIFF1 * 100); 341 342 printf("Num QOIPLANE_DIFF2 = %d\n", numQOIPLANE_DIFF2); 343 printf(" * pixels = %.2f\n", pixelsQOIPLANE_DIFF2 * 100); 344 printf(" * size = %.2f\n\n", sizeQOIPLANE_DIFF2 * 100); 345 346 printf("Num QOIPLANE_DIRECT = %d\n", numQOIPLANE_DIRECT); 347 printf(" * pixels = %.2f\n", pixelsQOIPLANE_DIRECT * 100); 348 printf(" * size = %.2f\n\n", sizeQOIPLANE_DIRECT * 100); 349 350 printf("Num QOIPLANE_REPEAT1 = %d\n", encodedQOIPLANE_REPEAT1); 351 printf(" * pixels = %.2f\n", pixelsQOIPLANE_REPEAT1 * 100); 352 printf(" * size = %.2f\n\n", sizeQOIPLANE_REPEAT1 * 100); 353 354 printf("Num QOIPLANE_REPEAT2 = %d\n", encodedQOIPLANE_REPEAT2); 355 printf(" * pixels = %.2f\n", pixelsQOIPLANE_REPEAT2 * 100); 356 printf(" * size = %.2f\n\n", sizeQOIPLANE_REPEAT2 * 100); 357 358 printf("Num QOIPLANE_LA = %d\n", numQOIPLANE_LA); 359 printf(" * pixels = %.2f\n", pixelsQOIPLANE_LA * 100); 360 printf(" * size = %.2f\n\n", sizeQOIPLANE_LA * 100); 361 } 362 363 *out_len = p; 364 return bytes; 365 } 366 367 368 369 /* Decode a QOI-plane image from memory. 370 371 The function either returns null on failure (invalid parameters or malloc 372 failed) or a pointer to the decoded pixels. On success, the qoi_desc struct 373 is filled with the description from the file header. 374 375 The returned pixel data should be free()d after use. */ 376 version(decodeQOIX) 377 ubyte* qoiplane_decode(const(ubyte)* data, int size, qoi_desc *desc, int channels) 378 { 379 if ((channels < 0 && channels > 2) || 380 size < QOIX_HEADER_SIZE + cast(int)(qoiplane_padding.sizeof)) 381 { 382 return null; 383 } 384 385 const(ubyte)* bytes = data; 386 387 int p = 0; 388 389 uint header_magic = qoi_read_32(bytes, &p); 390 desc.width = qoi_read_32(bytes, &p); 391 desc.height = qoi_read_32(bytes, &p); 392 int qoix_version = bytes[p++]; 393 desc.channels = bytes[p++]; 394 desc.bitdepth = bytes[p++]; 395 desc.colorspace = bytes[p++]; 396 desc.compression = bytes[p++]; 397 desc.pixelAspectRatio = qoi_read_32f(bytes, &p); 398 desc.resolutionY = qoi_read_32f(bytes, &p); 399 400 if (desc.width == 0 || desc.height == 0 || 401 desc.channels < 1 || desc.channels > 2 || 402 desc.colorspace > 1 || 403 desc.bitdepth != 8 || 404 qoix_version > 1 || 405 desc.compression != QOIX_COMPRESSION_NONE || 406 header_magic != QOIX_MAGIC || 407 desc.height >= QOIX_PIXELS_MAX / desc.width 408 ) 409 { 410 return null; 411 } 412 413 if (channels == 0) 414 { 415 channels = desc.channels; 416 } 417 418 int stride = desc.width * channels; 419 desc.pitchBytes = stride; // FUTURE: force to decode with a given layout / image 420 421 int num_pixels = desc.width * desc.height; 422 int output_bytes = num_pixels * channels; 423 424 ubyte* pixels = cast(ubyte*) QOI_MALLOC(output_bytes); 425 if (!pixels) 426 return null; 427 428 bool readHiNibble = true; // nibble index into output stream. 429 430 ubyte readNibble() nothrow @nogc 431 { 432 ubyte r; 433 if (readHiNibble) 434 r = (bytes[p] >>> 4); 435 else 436 r = (bytes[p++] & 0xf); 437 readHiNibble = !readHiNibble; 438 assert(r < 16); 439 return r; 440 } 441 442 ubyte readUbyte() 443 { 444 ubyte hi = cast(ubyte)(readNibble() << 4); 445 ubyte lo = readNibble(); 446 return hi | lo; 447 } 448 449 qoi_la_t px = initialPredictor; 450 qoi_la_t px_ref = initialPredictor; 451 452 int decoded_pixels = 0; 453 int run = 0; 454 455 for (int posy = 0; posy < desc.height; ++posy) 456 { 457 ubyte* line = pixels + desc.pitchBytes * posy; 458 459 // Note: don't read alpha in line above, since it may not exist if decoding 2 channels to 1 460 const(ubyte)* lineAbove = (posy > 0) ? (pixels + desc.pitchBytes * (posy - 1)) : null; 461 462 for (int posx = 0; posx < desc.width; ++posx) 463 { 464 px_ref = px; 465 466 if (run > 0) 467 { 468 run--; 469 } 470 else if (decoded_pixels < num_pixels) 471 { 472 decode_op: 473 ubyte op = readNibble(); 474 475 if ((op & 0xf) == 0xf) // QOIPLANE_REPEAT2 476 { 477 run = readUbyte() + 3; 478 if (run == 258) 479 run = 0x7fffffff; // fill with last pixel until end of decode 480 } 481 else if ((op & 0xc) == 0xc) // QOIPLANE_REPEAT1 482 { 483 run = (op & 0x3); 484 } 485 else 486 { 487 // Compute predictors. 488 ubyte px_top = (posy > 0) ? lineAbove[posx * channels] : px_ref.l; 489 ubyte px_avg = (px_top + px_ref.l + 1) / 2; 490 491 if ((op & 0x8) == 0) // QOIPLANE_DIFF1 492 { 493 assert(op < 8); 494 px.l = cast(ubyte)(px_avg + op - 4); 495 } 496 else if ((op & 0xe) == 0x8) // QOIPLANE_DIFF2 497 { 498 int vg_l = ((op & 1) << 4) + readNibble(); 499 assert(vg_l >= 0 && vg_l <= 31); 500 vg_l -= 16; 501 px.l = cast(ubyte)(px_avg + vg_l); 502 } 503 else if ((op & 0xf) == 0xa) // QOIPLANE_DIRECT 504 { 505 px.l = readUbyte(); 506 } 507 else if ((op & 0xf) == 0xb) 508 { 509 int diff = readNibble(); 510 if (diff == 0) // QOIPLANE_LA 511 { 512 px.l = readUbyte(); 513 px.a = readUbyte(); 514 } 515 else 516 { 517 // QOIPLANE_ADIFF 518 px.a = cast(ubyte)(px_ref.a + diff - 8); // -7 to 7 519 goto decode_op; 520 } 521 } 522 else 523 assert(false); 524 } 525 decoded_pixels++; 526 } 527 528 if (channels == 1) 529 { 530 line[posx * 1] = px.l; 531 } 532 else 533 { 534 line[posx * 2 + 0] = px.l; 535 line[posx * 2 + 1] = px.a; 536 } 537 } 538 } 539 540 return pixels; 541 }