1 /* 2 Based on reference QOI reference implementation: https://github.com/phoboslab/qoi/blob/master/qoi.h 3 (by Dominic Szablewski - https://phoboslab.org) 4 Ported by aquaratixc (Oleg Bakharev) and LightHouse Software (lhs-blog.info) 5 6 Written in D programming language 7 */ 8 module qoiformats; 9 10 private { 11 import core.stdc.stdio; 12 import core.stdc.stdlib : free, malloc; 13 import core.stdc.string : memset; 14 15 import std.algorithm : clamp; 16 import std.conv : to; 17 import std.string : format, toLower; 18 19 template addProperty(T, string propertyName, string defaultValue = T.init.to!string) 20 { 21 const char[] addProperty = format( 22 ` 23 private %2$s %1$s = %4$s; 24 25 void set%3$s(%2$s %1$s) 26 { 27 this.%1$s = %1$s; 28 } 29 30 %2$s get%3$s() 31 { 32 return %1$s; 33 } 34 `, 35 "_" ~ propertyName.toLower, 36 T.stringof, 37 propertyName, 38 defaultValue 39 ); 40 } 41 42 enum QOI_CHANNELS : ubyte 43 { 44 RGB = 3, 45 RGBA = 4 46 }; 47 48 enum QOI_COLORSPACE : ubyte 49 { 50 SRGB = 0, 51 LINEAR = 1 52 }; 53 54 class QoiImageInfo 55 { 56 mixin(addProperty!(uint, "Width")); 57 mixin(addProperty!(uint, "Height")); 58 mixin(addProperty!(QOI_CHANNELS, "Channels", "QOI_CHANNELS.RGB")); 59 mixin(addProperty!(QOI_COLORSPACE, "Colorspace", "QOI_COLORSPACE.SRGB")); 60 61 this( 62 uint width = 0, 63 uint height = 0, 64 QOI_CHANNELS channels = QOI_CHANNELS.RGB, 65 QOI_COLORSPACE colorspace = QOI_COLORSPACE.SRGB 66 ) 67 { 68 _width = width; 69 _height = height; 70 _channels = channels; 71 _colorspace = colorspace; 72 } 73 74 override string toString() 75 { 76 return format( 77 "QoiImageInfo(width = %d, height = %d, channels = %s, colorspace = %s)", 78 _width, 79 _height, 80 _channels.to!string, 81 _colorspace.to!string 82 ); 83 } 84 } 85 86 class QoiOperation 87 { 88 static void write32(ubyte* bytes, int* p, uint v) 89 { 90 bytes[(*p)++] = (0xff000000 & v) >> 24; 91 bytes[(*p)++] = (0x00ff0000 & v) >> 16; 92 bytes[(*p)++] = (0x0000ff00 & v) >> 8; 93 bytes[(*p)++] = (0x000000ff & v); 94 } 95 96 static uint read32(ubyte* bytes, int* p) 97 { 98 uint a = bytes[(*p)++]; 99 uint b = bytes[(*p)++]; 100 uint c = bytes[(*p)++]; 101 uint d = bytes[(*p)++]; 102 return ( 103 (a << 24) | (b << 16) | (c << 8) | d 104 ); 105 } 106 107 static uint hash32(RGBA rgba) 108 { 109 return rgba.r * 3 + rgba.g * 5 + rgba.b * 7 + rgba.a * 11; 110 } 111 } 112 113 enum QOI_OP : ubyte 114 { 115 INDEX = 0x00, 116 DIFF = 0x40, 117 LUMA = 0x80, 118 RUN = 0xc0, 119 RGB = 0xfe, 120 RGBA = 0xff 121 } 122 123 enum ubyte QOI_MASK_2 = 0xc0; 124 125 enum uint QOI_MAGIC = ( 126 (cast(uint) 'q') << 24 | 127 (cast(uint) 'o') << 16 | 128 (cast(uint) 'i') << 8 | 129 (cast(uint) 'f') 130 ); 131 132 enum ubyte QOI_HEADER_SIZE = 14; 133 134 enum uint QOI_PIXELS_MAX = cast(uint) 400_000_000; 135 136 static ubyte[8] QOI_PADDING = [0, 0, 0, 0, 0, 0, 0, 1]; 137 138 union RGBA { 139 struct {ubyte r, g, b, a; }; 140 uint v; 141 } 142 } 143 144 class QoiColor 145 { 146 private 147 { 148 RGBA _rgba; 149 } 150 151 this(ubyte R = 0, ubyte G = 0, ubyte B = 0, ubyte A = 0) 152 { 153 _rgba.r = R; 154 _rgba.g = G; 155 _rgba.b = B; 156 _rgba.a = A; 157 } 158 159 ubyte getR() 160 { 161 return _rgba.r; 162 } 163 164 ubyte getG() 165 { 166 return _rgba.g; 167 } 168 169 ubyte getB() 170 { 171 return _rgba.b; 172 } 173 174 ubyte getA() 175 { 176 return _rgba.a; 177 } 178 179 void setR(ubyte r) 180 { 181 _rgba.r = r; 182 } 183 184 void setG(ubyte g) 185 { 186 _rgba.g = g; 187 } 188 189 void setB(ubyte b) 190 { 191 _rgba.b = b; 192 } 193 194 void setA(ubyte a) 195 { 196 _rgba.a = a; 197 } 198 199 RGBA get() 200 { 201 return _rgba; 202 } 203 204 const float luminance709() 205 { 206 return (_rgba.r * 0.2126f + _rgba.g * 0.7152f + _rgba.b * 0.0722f); 207 } 208 209 const float luminance601() 210 { 211 return (_rgba.r * 0.3f + _rgba.g * 0.59f + _rgba.b * 0.11f); 212 } 213 214 const float luminanceAverage() 215 { 216 return (_rgba.r + _rgba.g + _rgba.b) / 3.0; 217 } 218 219 alias luminance = luminance709; 220 221 override string toString() 222 { 223 return format( 224 "QoiColor(%d, %d, %d, %d, I = %f)", 225 _rgba.r, _rgba.g, _rgba.b, _rgba.a, this.luminance 226 ); 227 } 228 229 QoiColor opBinary(string op, T)(auto ref T rhs) 230 { 231 return mixin( 232 format(`new QoiColor( 233 cast(ubyte) clamp((_rgba.r %1$s rhs), 0, 255), 234 cast(ubyte) clamp((_rgba.g %1$s rhs), 0, 255), 235 cast(ubyte) clamp((_rgba.b %1$s rhs), 0, 255), 236 cast(ubyte) clamp((_rgba.a %1$s rhs), 0, 255) 237 ) 238 `, 239 op 240 ) 241 ); 242 } 243 244 QoiColor opBinary(string op)(QoiColor rhs) 245 { 246 return mixin( 247 format(`new QoiColor( 248 cast(ubyte) clamp((_rgba.r %1$s rhs.getR), 0, 255), 249 cast(ubyte) clamp((_rgba.g %1$s rhs.getG), 0, 255), 250 cast(ubyte) clamp((_rgba.b %1$s rhs.getB), 0, 255), 251 cast(ubyte) clamp((_rgba.a %1$s rhs.getA), 0, 255) 252 ) 253 `, 254 op 255 ) 256 ); 257 } 258 259 alias get this; 260 } 261 262 class QoiImage 263 { 264 private 265 { 266 QoiColor[] _image; 267 QoiImageInfo _info; 268 } 269 270 private 271 { 272 auto actualIndex(uint i) 273 { 274 auto S = _info.getHeight; 275 276 return clamp(i, 0, S - 1); 277 } 278 279 auto actualIndex(uint i, uint j) 280 { 281 auto W = cast(size_t) clamp(i, 0, this.getWidth - 1); 282 auto H = cast(size_t) clamp(j, 0, this.getHeight - 1); 283 auto S = this.getArea; 284 285 return clamp(W + H * this.getWidth, 0, S); 286 } 287 288 void* encode(void* data, QoiImageInfo info, int* outputLength) 289 { 290 RGBA[64] index; 291 RGBA px, pxPrevious; 292 int i, maximalSize, p, run; 293 int pxLength, pxEnd, pxPosition, channels; 294 ubyte* bytes, pixels; 295 296 297 if ( 298 (data is null) || (outputLength is null) || (info is null) || 299 (info.getWidth == 0) || (info.getHeight == 0) || 300 (info.getChannels < 3) || (info.getChannels > 4 ) || 301 (info.getColorspace > 1) || 302 (info.getHeight >= QOI_PIXELS_MAX / info.getWidth) 303 ) { 304 return null; 305 } 306 307 maximalSize = cast(int) ( 308 info.getWidth * info.getHeight * (info.getChannels + 1) + 309 QOI_HEADER_SIZE + QOI_PADDING.length 310 ); 311 312 p = 0; 313 bytes = cast(ubyte*) malloc(maximalSize); 314 if (!bytes) 315 { 316 return null; 317 } 318 319 QoiOperation.write32(bytes, &p, QOI_MAGIC); 320 QoiOperation.write32(bytes, &p, info.getWidth); 321 QoiOperation.write32(bytes, &p, info.getHeight); 322 bytes[p++] = cast(byte) info.getChannels; 323 bytes[p++] = cast(byte) info.getColorspace; 324 325 pixels = cast(ubyte*) data; 326 327 memset(index.ptr, 0, index.length); 328 329 run = 0; 330 with (pxPrevious) 331 { 332 r = 0; 333 g = 0; 334 b = 0; 335 a = 255; 336 } 337 px = pxPrevious; 338 339 pxLength = info.getWidth * info.getHeight * info.getChannels; 340 pxEnd = pxLength - info.getChannels; 341 channels = info.getChannels; 342 343 for (pxPosition = 0; pxPosition < pxLength; pxPosition += channels) 344 { 345 if (channels == 4) 346 { 347 px = *(cast(RGBA*)(pixels + pxPosition)); 348 } 349 else 350 { 351 with (px) 352 { 353 r = pixels[pxPosition + 0]; 354 g = pixels[pxPosition + 1]; 355 b = pixels[pxPosition + 2]; 356 } 357 } 358 359 if (px.v == pxPrevious.v) { 360 run++; 361 if (run == 62 || pxPosition == pxEnd) { 362 bytes[p++] = cast(ubyte) (QOI_OP.RUN | (run - 1)); 363 run = 0; 364 } 365 } 366 else { 367 int index_pos; 368 369 if (run > 0) { 370 bytes[p++] = cast(ubyte) (QOI_OP.RUN | (run - 1)); 371 run = 0; 372 } 373 374 index_pos = QoiOperation.hash32(px) % 64; 375 376 if (index[index_pos].v == px.v) { 377 bytes[p++] = cast(byte) (QOI_OP.INDEX | index_pos); 378 } 379 else { 380 index[index_pos] = px; 381 382 if (px.a == pxPrevious.a) { 383 byte vr = cast(byte) (px.r - pxPrevious.r); 384 byte vg = cast(byte) (px.g - pxPrevious.g); 385 byte vb = cast(byte) (px.b - pxPrevious.b); 386 387 byte vg_r = cast(byte) (vr - vg); 388 byte vg_b = cast(byte) (vb - vg); 389 390 if ( 391 (vr > -3) && (vr < 2) && 392 (vg > -3) && (vg < 2) && 393 (vb > -3) && (vb < 2) 394 ) { 395 bytes[p++] = cast(byte) (QOI_OP.DIFF | (vr + 2) << 4 | (vg + 2) << 2 | (vb + 2)); 396 } 397 else if ( 398 (vg_r > -9) && (vg_r < 8) && 399 (vg > -33) && (vg < 32) && 400 (vg_b > -9) && (vg_b < 8) 401 ) { 402 bytes[p++] = cast(byte) (QOI_OP.LUMA | (vg + 32)); 403 bytes[p++] = cast(byte) ((vg_r + 8) << 4 | (vg_b + 8)); 404 } 405 else { 406 bytes[p++] = QOI_OP.RGB; 407 with (px) 408 { 409 bytes[p++] = r; 410 bytes[p++] = g; 411 bytes[p++] = b; 412 } 413 } 414 } 415 else { 416 bytes[p++] = QOI_OP.RGBA; 417 with (px) 418 { 419 bytes[p++] = r; 420 bytes[p++] = g; 421 bytes[p++] = b; 422 bytes[p++] = a; 423 } 424 } 425 } 426 } 427 pxPrevious = px; 428 } 429 430 for (i = 0; i < cast(int) QOI_PADDING.length; i++) 431 { 432 bytes[p++] = QOI_PADDING[i]; 433 } 434 435 *outputLength = p; 436 return bytes; 437 } 438 439 void* decode(void* data, int size, QoiImageInfo info, int channels) 440 { 441 RGBA[64] index; 442 RGBA px; 443 ubyte* bytes, pixels; 444 uint headerMagic; 445 int p, run, pxLength, chunksLength, pxPosition; 446 447 if ( 448 (data is null) || 449 (info is null) || 450 (channels != 0 && channels != 3 && channels != 4) || 451 (size < QOI_HEADER_SIZE + cast(int) QOI_PADDING.length) 452 ) 453 { 454 return null; 455 } 456 457 bytes = cast(ubyte*) data; 458 459 headerMagic = QoiOperation.read32(bytes, &p); 460 461 with (info) 462 { 463 setWidth = QoiOperation.read32(bytes, &p); 464 setHeight = QoiOperation.read32(bytes, &p); 465 setChannels = cast(QOI_CHANNELS) bytes[p++]; 466 setColorspace = cast(QOI_COLORSPACE) bytes[p++]; 467 } 468 469 if ( 470 (info.getWidth == 0) || 471 (info.getHeight == 0) || 472 (info.getChannels < 3) || 473 (info.getChannels > 4) || 474 (info.getColorspace > 1) || 475 (headerMagic != QOI_MAGIC) || 476 (info.getHeight >= QOI_PIXELS_MAX / info.getWidth) 477 ) 478 { 479 return null; 480 } 481 482 if (channels == 0) 483 { 484 channels = info.getChannels; 485 } 486 487 pxLength = info.getWidth * info.getHeight * channels; 488 pixels = cast(ubyte*) malloc(pxLength); 489 490 if (!pixels) 491 { 492 return null; 493 } 494 495 memset(index.ptr, 0, index.length); 496 497 with (px) 498 { 499 r = 0; 500 g = 0; 501 b = 0; 502 a = 255; 503 } 504 505 chunksLength = size - cast(int) QOI_PADDING.length; 506 507 for (pxPosition = 0; pxPosition < pxLength; pxPosition += channels) 508 { 509 if (run > 0) 510 { 511 run--; 512 } 513 else if (p < chunksLength) { 514 int b1 = bytes[p++]; 515 516 if (b1 == QOI_OP.RGB) { 517 with (px) 518 { 519 r = bytes[p++]; 520 g = bytes[p++]; 521 b = bytes[p++]; 522 } 523 } 524 else if (b1 == QOI_OP.RGBA) { 525 with (px) 526 { 527 r = bytes[p++]; 528 g = bytes[p++]; 529 b = bytes[p++]; 530 a = bytes[p++]; 531 } 532 } 533 else if ((b1 & QOI_MASK_2) == QOI_OP.INDEX) { 534 px = index[b1]; 535 } 536 else if ((b1 & QOI_MASK_2) == QOI_OP.DIFF) { 537 with (px) 538 { 539 r += ((b1 >> 4) & 0x03) - 2; 540 g += ((b1 >> 2) & 0x03) - 2; 541 b += ( b1 & 0x03) - 2; 542 } 543 } 544 else if ((b1 & QOI_MASK_2) == QOI_OP.LUMA) { 545 int b2 = bytes[p++]; 546 int vg = (b1 & 0x3f) - 32; 547 with (px) 548 { 549 r += vg - 8 + ((b2 >> 4) & 0x0f); 550 g += vg; 551 b += vg - 8 + (b2 & 0x0f); 552 } 553 } 554 else if ((b1 & QOI_MASK_2) == QOI_OP.RUN) { 555 run = (b1 & 0x3f); 556 } 557 558 index[QoiOperation.hash32(px) % 64] = px; 559 } 560 561 if (channels == 4) 562 { 563 *(cast(RGBA*)(pixels + pxPosition)) = px; 564 } 565 else 566 { 567 with (px) 568 { 569 pixels[pxPosition + 0] = r; 570 pixels[pxPosition + 1] = g; 571 pixels[pxPosition + 2] = b; 572 } 573 } 574 } 575 576 return pixels; 577 } 578 579 580 void* read(char* filename, QoiImageInfo info, int channels) 581 { 582 int size, bytesRead; 583 void* pixels, data; 584 585 FILE* f = fopen(filename, "rb"); 586 if (!f) 587 { 588 return null; 589 } 590 591 fseek(f, 0, SEEK_END); 592 size = cast(int) ftell(f); 593 594 if (size <= 0) 595 { 596 fclose(f); 597 return null; 598 } 599 600 fseek(f, 0, SEEK_SET); 601 602 data = malloc(size); 603 604 if (!data) 605 { 606 fclose(f); 607 return null; 608 } 609 610 bytesRead = cast(int) fread(data, 1, size, f); 611 fclose(f); 612 613 pixels = decode(data, bytesRead, info, channels); 614 free(data); 615 616 return pixels; 617 } 618 619 int write(const char *filename, void* data, QoiImageInfo info) 620 { 621 int size; 622 void* encoded; 623 624 FILE* f = fopen(filename, "wb"); 625 if (!f) 626 { 627 return 0; 628 } 629 630 encoded = encode(data, info, &size); 631 if (!encoded) 632 { 633 fclose(f); 634 return 0; 635 } 636 637 fwrite(encoded, 1, size, f); 638 fclose(f); 639 640 free(encoded); 641 return size; 642 } 643 } 644 645 this( 646 uint width = 0, 647 uint height = 0, 648 QoiColor color = new QoiColor(0, 0, 0), 649 QOI_CHANNELS channels = QOI_CHANNELS.RGB, 650 QOI_COLORSPACE colorspace = QOI_COLORSPACE.SRGB 651 ) 652 { 653 _info = new QoiImageInfo(width, height, channels, colorspace); 654 655 foreach (x; 0..width) 656 { 657 foreach (y; 0..height) 658 { 659 _image ~= color; 660 } 661 } 662 } 663 664 // image width 665 uint getWidth() 666 { 667 return _info.getWidth; 668 } 669 670 // image height 671 uint getHeight() 672 { 673 return _info.getHeight; 674 } 675 676 // image area 677 uint getArea() 678 { 679 return _info.getWidth * _info.getHeight; 680 } 681 682 // image as array 683 QoiColor[] getImage() 684 { 685 return _image; 686 } 687 688 // image info 689 QoiImageInfo getInfo() 690 { 691 return _info; 692 } 693 694 // img[x, y] = color 695 QoiColor opIndexAssign(QoiColor color, uint x, uint y) 696 { 697 _image[actualIndex(x, y)] = color; 698 return color; 699 } 700 701 // img[x] = color 702 QoiColor opIndexAssign(QoiColor color, uint x) 703 { 704 _image[actualIndex(x)] = color; 705 return color; 706 } 707 708 // img[x, y] 709 QoiColor opIndex(uint x, uint y) 710 { 711 return _image[actualIndex(x, y)]; 712 } 713 714 // img[x] 715 QoiColor opIndex(uint x) 716 { 717 return _image[actualIndex(x)]; 718 } 719 720 // image as string 721 override string toString() 722 { 723 string accumulator = "["; 724 725 foreach (x; 0..this.getWidth) 726 { 727 string tmp = "["; 728 foreach (y; 0..this.getHeight) 729 { 730 tmp ~= _image[actualIndex(x, y)].toString ~ ", "; 731 } 732 tmp = tmp[0..$-2] ~ "], "; 733 accumulator ~= tmp; 734 } 735 return accumulator[0..$-2] ~ "]"; 736 } 737 738 // load QOI file 739 void load(string filename) 740 { 741 char* name = cast(char*) filename.ptr; 742 void* data = read(name, _info, 0); 743 744 if (data !is null) 745 { 746 auto channels = _info.getChannels; 747 auto squared = this.area * channels; 748 749 for (uint i = 0; i < squared; i += channels) 750 { 751 QoiColor rgba; 752 753 final switch (channels) with (QOI_CHANNELS) 754 { 755 case RGB: 756 auto q = cast(ubyte[]) data[i..i+3]; 757 rgba = new QoiColor(q[0], q[1], q[2]); 758 break; 759 case RGBA: 760 auto q = cast(ubyte[]) data[i..i+4]; 761 rgba = new QoiColor(q[0], q[1], q[2], q[3]); 762 break; 763 } 764 765 _image ~= rgba; 766 } 767 } 768 } 769 770 // save QOI file 771 void save(string filename) 772 { 773 char* name = cast(char*) filename.ptr; 774 auto channels = _info.getChannels; 775 auto squared = this.area; 776 777 ubyte[] data; 778 779 for (uint i = 0; i < squared; i++) 780 { 781 auto q = _image[i]; 782 final switch (channels) with (QOI_CHANNELS) 783 { 784 case RGB: 785 data ~= [q.getR, q.getG, q.getB]; 786 break; 787 case RGBA: 788 data ~= [q.getR, q.getG, q.getB, q.getA]; 789 break; 790 } 791 } 792 793 write(name, cast(void*) data.ptr, _info); 794 } 795 796 797 // aliases 798 alias width = getWidth; 799 alias height = getHeight; 800 alias area = getArea; 801 alias image = getImage; 802 alias info = getInfo; 803 }