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