1 /** 2 In-memory images. 3 4 License: 5 This Source Code Form is subject to the terms of 6 the Mozilla Public License, v. 2.0. If a copy of 7 the MPL was not distributed with this file, You 8 can obtain one at http://mozilla.org/MPL/2.0/. 9 10 Copyright: Vladimir Panteleev <vladimir@thecybershadow.net> 11 Copyright: Guillaume Piolat <contact@auburnsounds.com> 12 */ 13 14 module dplug.graphics.image; 15 16 import dplug.core.math; 17 import dplug.core.vec; 18 import dplug.core.nogc; 19 import dplug.math.box; 20 public import dplug.graphics.color; 21 import gamut; 22 23 nothrow @nogc: 24 25 /// A view is any type which provides a width, height, 26 /// and can be indexed to get the color at a specific 27 /// coordinate. 28 enum isView(T) = 29 is(typeof(T.init.w) : size_t) && // width 30 is(typeof(T.init.h) : size_t) && // height 31 is(typeof(T.init[0, 0]) ); // color information 32 33 /// Returns the color type of the specified view. 34 /// By convention, colors are structs with numeric 35 /// fields named after the channel they indicate. 36 alias ViewColor(T) = typeof(T.init[0, 0]); 37 38 /// Views can be read-only or writable. 39 enum isWritableView(T) = 40 isView!T && 41 is(typeof(T.init[0, 0] = ViewColor!T.init)); 42 43 /// Optionally, a view can also provide direct pixel 44 /// access. We call these "direct views". 45 enum isDirectView(T) = 46 isView!T && 47 is(typeof(T.init.scanline(0)) : ViewColor!T[]); 48 49 /// Mixin which implements view primitives on top of 50 /// existing direct view primitives. 51 mixin template DirectView() 52 { 53 alias COLOR = typeof(scanline(0)[0]); 54 55 /// Implements the view[x, y] operator. 56 ref COLOR opIndex(int x, int y) 57 { 58 return scanline(y)[x]; 59 } 60 61 /// Implements the view[x, y] = c operator. 62 COLOR opIndexAssign(COLOR value, int x, int y) 63 { 64 return scanline(y)[x] = value; 65 } 66 } 67 68 /// Blits a view onto another. 69 /// The views must have the same size. 70 void blitTo(SRC, DST)(auto ref SRC src, auto ref DST dst) 71 if (isView!SRC && isWritableView!DST) 72 { 73 assert(src.w == dst.w && src.h == dst.h, "View size mismatch"); 74 foreach (y; 0..src.h) 75 { 76 static if (isDirectView!SRC && isDirectView!DST) 77 dst.scanline(y)[] = src.scanline(y)[]; 78 else 79 { 80 foreach (x; 0..src.w) 81 dst[x, y] = src[x, y]; 82 } 83 } 84 } 85 86 /// Helper function to blit an image onto another at a specified location. 87 void blitTo(SRC, DST)(auto ref SRC src, auto ref DST dst, int x, int y) 88 { 89 src.blitTo(dst.crop(x, y, x+src.w, y+src.h)); 90 } 91 92 /// Default implementation for the .size method. 93 /// Asserts that the view has the desired size. 94 void size(V)(auto ref V src, int w, int h) 95 if (isView!V) 96 { 97 assert(src.w == w && src.h == h, "Wrong size for " ~ V.stringof); 98 } 99 100 // *************************************************************************** 101 102 /// Mixin which implements view primitives on top of 103 /// another view, using a coordinate transform function. 104 mixin template Warp(V) 105 if (isView!V) 106 { 107 V src; 108 109 auto ref ViewColor!V opIndex(int x, int y) 110 { 111 warp(x, y); 112 return src[x, y]; 113 } 114 115 static if (isWritableView!V) 116 ViewColor!V opIndexAssign(ViewColor!V value, int x, int y) 117 { 118 warp(x, y); 119 return src[x, y] = value; 120 } 121 } 122 123 /// Represents a reference to COLOR data 124 /// already existing elsewhere in memory. 125 /// Assumes that pixels are stored row-by-row, 126 /// with a known distance between each row. 127 struct ImageRef(COLOR) 128 { 129 int w, h; 130 size_t pitch; /// In bytes, not COLORs 131 COLOR* pixels; 132 133 /// Returns an array for the pixels at row y. 134 COLOR[] scanline(int y) 135 { 136 assert(y >= 0 && y < h); 137 assert(pitch); 138 139 // perf: this cast help in 64-bit, since it avoids a MOVSXD to extend a signed 32-bit into a 64-bit pointer offset 140 uint unsignedY = cast(uint)y; 141 142 auto row = cast(COLOR*)(cast(ubyte*)pixels + unsignedY * pitch); 143 return row[0..w]; 144 } 145 146 mixin DirectView; 147 148 /// Returns a cropped view of the same `ImageRef`. 149 ImageRef!COLOR cropBorder(int borderPixels) 150 { 151 assert(w >= 2*borderPixels); 152 assert(h >= 2*borderPixels); 153 154 ImageRef cropped; 155 cropped.w = w - 2*borderPixels; 156 cropped.h = h - 2*borderPixels; 157 cropped.pitch = pitch; 158 cropped.pixels = cast(COLOR*)(cast(ubyte*)pixels + borderPixels*pitch + borderPixels*COLOR.sizeof); 159 return cropped; 160 } 161 } 162 163 unittest 164 { 165 static assert(isDirectView!(ImageRef!ubyte)); 166 } 167 168 /// Convert an `OwnedImage` to an `ImageRef`. 169 ImageRef!L8 toRef(OwnedImage!L8 src) pure 170 { 171 return ImageRef!L8(src.w, src.h, src.pitchInBytes(), src.scanlinePtr(0)); 172 } 173 174 ///ditto 175 ImageRef!L16 toRef(OwnedImage!L16 src) pure 176 { 177 return ImageRef!L16(src.w, src.h, src.pitchInBytes(), src.scanlinePtr(0)); 178 } 179 180 ///ditto 181 ImageRef!RGBA toRef(OwnedImage!RGBA src) pure 182 { 183 return ImageRef!RGBA(src.w, src.h, src.pitchInBytes(), src.scanlinePtr(0)); 184 } 185 186 ///ditto 187 ImageRef!RGBA16 toRef(OwnedImage!RGBA16 src) pure 188 { 189 return ImageRef!RGBA16(src.w, src.h, src.pitchInBytes(), src.scanlinePtr(0)); 190 } 191 192 /// Copy the indicated row of src to a COLOR buffer. 193 void copyScanline(SRC, COLOR)(auto ref SRC src, int y, COLOR[] dst) 194 if (isView!SRC && is(COLOR == ViewColor!SRC)) 195 { 196 static if (isDirectView!SRC) 197 dst[] = src.scanline(y)[]; 198 else 199 { 200 assert(src.w == dst.length); 201 foreach (x; 0..src.w) 202 dst[x] = src[x, y]; 203 } 204 } 205 206 /// Copy a view's pixels (top-to-bottom) to a COLOR buffer. 207 void copyPixels(SRC, COLOR)(auto ref SRC src, COLOR[] dst) 208 if (isView!SRC && is(COLOR == ViewColor!SRC)) 209 { 210 assert(dst.length == src.w * src.h); 211 foreach (y; 0..src.h) 212 src.copyScanline(y, dst[y*src.w..(y+1)*src.w]); 213 } 214 215 /// Crop an ImageRef and get an ImageRef instead of a Voldemort type. 216 /// This also avoid adding offset to coordinates. 217 ImageRef!COLOR cropImageRef(COLOR)(ImageRef!COLOR src, box2i rect) 218 { 219 assert(rect.max.x <= src.w); 220 assert(rect.max.y <= src.h); 221 ImageRef!COLOR result; 222 result.w = rect.width; 223 result.h = rect.height; 224 result.pitch = src.pitch; 225 COLOR[] scan = src.scanline(rect.min.y); 226 result.pixels = &scan[rect.min.x]; 227 return result; 228 } 229 ///ditto 230 ImageRef!COLOR cropImageRef(COLOR)(ImageRef!COLOR src, int xmin, int ymin, int xmax, int ymax) 231 { 232 return src.cropImageRef(box2i(xmin, ymin, xmax, ymax)); 233 } 234 235 /// Manually managed image. 236 final class OwnedImage(COLOR) 237 { 238 public: 239 nothrow: 240 @nogc: 241 242 /// Width of the meaningful area. Public in order to be an `Image`. 243 int w; 244 245 /// Height of the meaningful area. Public in order to be an `Image`. 246 int h; 247 248 /// Create empty and with no size. 249 this() nothrow @nogc 250 { 251 } 252 253 /// Create with given initial size. 254 /// 255 /// Params: 256 /// w Width of meaningful area in pixels. 257 /// h Height of meaningful area in pixels. 258 /// border Number of border pixels around the meaningful area. For the right border, this could actually be more depending on other constraints. 259 /// rowAlignment Alignment of the _first_ pixel of each row, in bytes (excluding border). 260 /// xMultiplicity Starting with the first meaningful pixel of a line, force the number of adressable 261 /// pixels to be a multiple of `xMultiplicity`. 262 /// All these "padding" samples added at the right of each line if needed will be considered 263 /// part of the border and replicated if need be. 264 /// This is eg. to process a whole line with only aligned loads. 265 /// Implemented with additional right border. 266 /// trailingSamples When reading from a meaningful position, you can read 1 + `trailingSamples` neightbours 267 /// Their value is NOT guaranteed in any way and is completely undefined. 268 /// They are guaranteed non-NaN if you've generated borders and did not write into borders. 269 /// Because of row alignment padding, this is not implemented with a few samples at the end 270 /// of the buffer, but is instead again a right border extension. 271 /// 272 /// Note: default arguments leads to a gapless representation. 273 this(int w, int h, int border = 0, int rowAlignment = 1, int xMultiplicity = 1, int trailingSamples = 0) nothrow @nogc 274 { 275 this(); 276 size(w, h, border, rowAlignment, xMultiplicity, trailingSamples); 277 } 278 279 /// Create from already loaded dense RGBA pixel data. 280 /// `buffer` should be allocated with `alignedMalloc`/`alignedRealloc` with an alignment of 1. 281 /// Ownership of `buffer` is given to the `OwnedImage`. 282 this(int w, int h, ubyte* buffer) 283 { 284 this.w = w; 285 this.h = h; 286 _pixels = cast(COLOR*) buffer; 287 _bytePitch = w * cast(int)(COLOR.sizeof); 288 _buffer = buffer; 289 _border = 0; 290 _borderRight = 0; 291 } 292 293 ~this() 294 { 295 if (_buffer !is null) 296 { 297 // FUTURE: use intel-intrinsics _mm_malloc/_mm_free 298 alignedFree(_buffer, 1); // Note: this allocation methods must be synced with the one in JPEG and PNL loading. 299 _buffer = null; 300 } 301 } 302 303 /// Returns: A pointer to the pixels at row y. Excluding border pixels. 304 COLOR* scanlinePtr(int y) pure 305 { 306 assert(y >= 0 && y < h); 307 308 // perf: this cast help in 64-bit, since it avoids a MOVSXD to extend a signed 32-bit into a 64-bit pointer offset 309 uint offsetBytes = _bytePitch * cast(uint)y; 310 311 return cast(COLOR*)( cast(ubyte*)_pixels + offsetBytes ); 312 } 313 314 /// Returns: A slice of the pixels at row y. Excluding border pixels. 315 COLOR[] scanline(int y) pure 316 { 317 return scanlinePtr(y)[0..w]; 318 } 319 320 /// Resize the image, the content is lost and the new content is undefined. 321 /// 322 /// Params: 323 /// w Width of meaningful area in pixels. 324 /// h Height of meaningful area in pixels. 325 /// border Number of border pixels around the meaningful area. For the right border, this could actually be more depending on other constraints. 326 /// rowAlignment Alignment of the _first_ pixel of each row, in bytes (excluding border). 327 /// xMultiplicity Starting with the first meaningful pixel of a line, force the number of adressable 328 /// pixels to be a multiple of `xMultiplicity`. 329 /// All these "padding" samples added at the right of each line if needed will be considered 330 /// part of the border and replicated if need be. 331 /// This is eg. to process a whole line with only aligned loads. 332 /// Implemented with additional right border. 333 /// trailingSamples When reading from a meaningful position, you can read 1 + `trailingSamples` neightbours 334 /// Their value is NOT guaranteed in any way and is completely undefined. 335 /// They are guaranteed non-NaN if you've generated borders and did not write into borders. 336 /// Because of row alignment padding, this is not implemented with a few samples at the end 337 /// of the buffer, but is instead again a right border extension. 338 /// 339 /// Note: Default arguments leads to a gapless representation. 340 void size(int width, 341 int height, 342 int border = 0, 343 int rowAlignment = 1, 344 int xMultiplicity = 1, 345 int trailingSamples = 0) 346 { 347 assert(width >= 0); // Note sure if 0 is supported 348 assert(height >= 0); // Note sure if 0 is supported 349 assert(border >= 0); 350 assert(rowAlignment >= 1); // Not yet implemented! 351 assert(xMultiplicity >= 1); // Not yet implemented! 352 353 // Compute size of right border. 354 // How many "padding samples" do we need to extend the right border with to respect `xMultiplicity`? 355 int rightPadding = computeRightPadding(w, border, xMultiplicity); 356 int borderRight = border + rightPadding; 357 if (borderRight < trailingSamples) 358 borderRight = trailingSamples; 359 360 int actualWidthInSamples = border + width + borderRight; 361 int actualHeightInSamples = border + height + border; 362 363 // Compute byte pitch and align it on `rowAlignment` 364 int bytePitch = cast(int)(COLOR.sizeof) * actualWidthInSamples; 365 bytePitch = cast(int) nextMultipleOf(bytePitch, rowAlignment); 366 367 this.w = width; 368 this.h = height; 369 this._border = border; 370 this._borderRight = borderRight; 371 this._bytePitch = bytePitch; 372 373 // How many bytes do we need for all samples? A bit more for aligning the first valid pixel. 374 size_t allocationSize = bytePitch * actualHeightInSamples; 375 allocationSize += (rowAlignment - 1); 376 377 // We don't need to preserve former data, nor to align the first border pixel 378 this._buffer = alignedReallocDiscard(this._buffer, allocationSize, 1); 379 380 // Set _pixels to the right place 381 size_t offsetToFirstMeaningfulPixel = bytePitch * borderTop() + COLOR.sizeof * borderLeft(); 382 this._pixels = cast(COLOR*) nextAlignedPointer(&_buffer[offsetToFirstMeaningfulPixel], rowAlignment); 383 384 // Test alignment of rows 385 assert( isPointerAligned(_pixels, rowAlignment) ); 386 if (height > 0) 387 assert( isPointerAligned(scanlinePtr(0), rowAlignment) ); 388 389 if (border == 0 && rowAlignment == 1 && xMultiplicity == 1 && trailingSamples == 0) 390 { 391 assert(isGapless()); 392 } 393 } 394 395 /// Returns: `true` if rows of pixels are immediately consecutive in memory. 396 /// Meaning that there is no border or lost pixels in the data. 397 bool isGapless() pure 398 { 399 return w * COLOR.sizeof == _bytePitch; 400 } 401 402 /// Returns: Number of bytes to add to a COLOR* pointer to get to the previous/next line. 403 /// This pitch is guaranteed to be positive (>= 0). 404 int pitchInBytes() pure 405 { 406 return _bytePitch; 407 } 408 409 /// Returns: Number of border pixels in the left direction (small X). 410 int borderLeft() pure 411 { 412 return _border; 413 } 414 415 /// Returns: Number of border pixels in the right direction (large X). 416 int borderRight() pure 417 { 418 return _borderRight; 419 } 420 421 /// Returns: Number of border pixels in the left direction (small Y). 422 int borderTop() pure 423 { 424 return _border; 425 } 426 427 /// Returns: Number of border pixels in the left direction (large Y). 428 int borderBottom() pure 429 { 430 return _border; 431 } 432 433 // It is a `DirectView`. 434 mixin DirectView; 435 436 /// Fills the whole image, border included, with a single color value. 437 void fillWith(COLOR fill) 438 { 439 for (int y = -borderTop(); y < h + borderBottom(); ++y) 440 { 441 int pixelsInRow = borderLeft() + w + borderRight(); 442 COLOR* scan = unsafeScanlinePtr(y) - borderLeft(); 443 scan[0..pixelsInRow] = fill; 444 } 445 } 446 447 /// Fill the borders by taking the nearest existing pixel in the meaningful area. 448 void replicateBorders() 449 { 450 replicateBordersTouching( box2i.rectangle(0, 0, this.w, this.h) ); 451 } 452 453 /// Fill the borders _touching updatedRect_ by taking the nearest existing pixel in the meaningful area. 454 void replicateBordersTouching(box2i updatedRect) 455 { 456 if (w < 1 || h < 1) 457 return; // can't replicate borders of an empty image 458 459 // BORDER REPLICATION. 460 // If an area is touching left border, then the border needs replication. 461 // If an area is touching left and top border, then the border needs replication and the corner pixel should be filled too. 462 // Same for all four corners. Border only applies to level 0. 463 // 464 // OOOOOOOxxxxxxxxxxxxxxx 465 // Ooooooo 466 // Oo o 467 // Oo o <------ if the update area is made of 'o', then the area to replicate is 'O' 468 // Ooooooo 469 // x 470 // x 471 472 int W = this.w; 473 int H = this.h; 474 475 int minx = updatedRect.min.x; 476 int maxx = updatedRect.max.x; 477 int miny = updatedRect.min.y; 478 int maxy = updatedRect.max.y; 479 480 bool touchLeft = (minx == 0); 481 bool touchTop = (miny == 0); 482 bool touchRight = (maxx == W); 483 bool touchBottom = (maxy == H); 484 485 int bTop = borderTop(); 486 int bBott = borderBottom(); 487 int bLeft = borderLeft(); 488 int bRight = borderRight(); 489 490 if (touchTop) 491 { 492 if (touchLeft) 493 { 494 COLOR topLeft = this[0, 0]; 495 for (int y = -borderTop; y < 0; ++y) 496 for (int x = -borderLeft; x < 0; ++x) 497 unsafeScanlinePtr(y)[x] = topLeft; 498 } 499 500 for (int y = -borderTop; y < 0; ++y) 501 { 502 unsafeScanlinePtr(y)[minx..maxx] = scanline(0)[minx..maxx]; 503 } 504 505 if (touchRight) 506 { 507 COLOR topRight = this[W-1, 0]; 508 for (int y = -borderTop; y < 0; ++y) 509 for (int x = 0; x < borderRight(); ++x) 510 unsafeScanlinePtr(y)[W + x] = topRight; 511 } 512 } 513 514 if (touchLeft) 515 { 516 for (int y = miny; y < maxy; ++y) 517 { 518 COLOR edge = this[0, y]; 519 for (int x = -borderLeft(); x < 0; ++x) 520 unsafeScanlinePtr(y)[x] = edge; 521 } 522 } 523 524 if (touchRight) 525 { 526 for (int y = miny; y < maxy; ++y) 527 { 528 COLOR edge = this[W-1, y]; 529 for (int x = 0; x < borderRight(); ++x) 530 unsafeScanlinePtr(y)[W + x] = edge; 531 } 532 } 533 534 if (touchBottom) 535 { 536 if (touchLeft) 537 { 538 COLOR bottomLeft = this[0, H-1]; 539 for (int y = H; y < H + borderTop(); ++y) 540 for (int x = -borderLeft; x < 0; ++x) 541 unsafeScanlinePtr(y)[x] = bottomLeft; 542 } 543 544 for (int y = H; y < H + borderBottom(); ++y) 545 { 546 unsafeScanlinePtr(y)[minx..maxx] = scanline(H-1)[minx..maxx]; 547 } 548 549 if (touchRight) 550 { 551 COLOR bottomRight = this[W-1, H-1]; 552 for (int y = H; y < H + borderTop(); ++y) 553 for (int x = 0; x < borderRight(); ++x) 554 unsafeScanlinePtr(y)[W + x] = bottomRight; 555 } 556 } 557 } 558 559 private: 560 561 /// Adress of the first meaningful pixel 562 COLOR* _pixels; 563 564 /// Samples difference between rows of pixels. 565 int _bytePitch; 566 567 /// Address of the allocation itself 568 void* _buffer = null; 569 570 /// Size of left, top and bottom borders, around the meaningful area. 571 int _border; 572 573 /// Size of border at thr right of the meaningful area (most positive X) 574 int _borderRight; 575 576 // Internal use: allows to get the scanlines of top and bottom borders too 577 COLOR* unsafeScanlinePtr(int y) pure 578 { 579 assert(y >= -borderTop()); // negative Y allows to get top border scanlines 580 assert(y < h + borderBottom()); // Y overflow allows to get bottom border scanlines 581 int byteOffset = _bytePitch * y; // unlike the normal `scanlinePtr`, there will be a MOVSXD here 582 return cast(COLOR*)(cast(ubyte*)(_pixels) + byteOffset); 583 } 584 585 static int computeRightPadding(int width, int border, int xMultiplicity) pure 586 { 587 int nextMultiple = cast(int)(nextMultipleOf(width + border, xMultiplicity)); 588 return nextMultiple - (width + border); 589 } 590 591 static size_t nextMultipleOf(size_t base, size_t multiple) pure 592 { 593 assert(multiple > 0); 594 size_t n = (base + multiple - 1) / multiple; 595 return multiple * n; 596 } 597 598 /// Returns: next pointer aligned with alignment bytes. 599 static void* nextAlignedPointer(void* start, size_t alignment) pure 600 { 601 return cast(void*)nextMultipleOf(cast(size_t)(start), alignment); 602 } 603 } 604 605 unittest 606 { 607 static assert(isDirectView!(OwnedImage!ubyte)); 608 assert(OwnedImage!RGBA.computeRightPadding(4, 0, 4) == 0); 609 assert(OwnedImage!RGBA.computeRightPadding(1, 3, 5) == 1); 610 assert(OwnedImage!L16.computeRightPadding(2, 0, 4) == 2); 611 assert(OwnedImage!RGBA.computeRightPadding(2, 1, 4) == 1); 612 assert(OwnedImage!RGBf.nextMultipleOf(0, 7) == 0); 613 assert(OwnedImage!RGBAf.nextMultipleOf(1, 7) == 7); 614 assert(OwnedImage!RGBA.nextMultipleOf(6, 7) == 7); 615 assert(OwnedImage!RGBA.nextMultipleOf(7, 7) == 7); 616 assert(OwnedImage!RGBA.nextMultipleOf(8, 7) == 14); 617 618 { 619 OwnedImage!RGBA img = mallocNew!(OwnedImage!RGBA); 620 scope(exit) destroyFree(img); 621 img.size(0, 0); // should be supported 622 623 int border = 10; 624 img.size(0, 0, border); // should also be supported (border with no data!) 625 img.replicateBorders(); // should not crash 626 627 border = 0; 628 img.size(1, 1, border); 629 img.replicateBorders(); // should not crash 630 } 631 } 632 633 // Test border replication 634 unittest 635 { 636 OwnedImage!L8 img = mallocNew!(OwnedImage!L8); 637 scope(exit) destroyFree(img); 638 int width = 2; 639 int height = 2; 640 int border = 2; 641 int xMultiplicity = 1; 642 int trailing = 3; 643 img.size(width, height, border, xMultiplicity, trailing); 644 assert(img.w == 2 && img.h == 2); 645 assert(img.borderLeft() == 2 && img.borderRight() == 3); 646 assert(img._bytePitch == 7); 647 648 img.fillWith(L8(5)); 649 img.scanline(0)[0].l = 1; 650 img.scanlinePtr(0)[1].l = 2; 651 img[0, 1].l = 3; 652 img[1, 1].l = 4; 653 654 img.replicateBorders(); 655 ubyte[7][6] correct = 656 [ 657 [1, 1, 1, 2, 2, 2, 2], 658 [1, 1, 1, 2, 2, 2, 2], 659 [1, 1, 1, 2, 2, 2, 2], 660 [3, 3, 3, 4, 4, 4, 4], 661 [3, 3, 3, 4, 4, 4, 4], 662 [3, 3, 3, 4, 4, 4, 4], 663 ]; 664 665 for (int y = -2; y < 4; ++y) 666 { 667 for (int x = -2; x < 5; ++x) 668 { 669 L8 read = img.unsafeScanlinePtr(y)[x]; 670 ubyte good = correct[y+2][x+2]; 671 assert(read.l == good); 672 } 673 } 674 } 675 676 // Test toRef 677 unittest 678 { 679 OwnedImage!L8 img = mallocNew!(OwnedImage!L8); 680 scope(exit) destroyFree(img); 681 682 int border = 0; 683 int rowAlignment = 1; 684 int xMultiplicity = 1; 685 int trailingSamples = 4; 686 img.size(10, 10, border, rowAlignment, xMultiplicity, trailingSamples); // Ask for 4 trailing samples 687 ImageRef!L8 r = img.toRef(); 688 assert(r.pitch >= 10 + 4); // per the API, OwnedImage could add more space than required 689 } 690 691 /// Loads an image from compressed data. 692 /// The returned `OwnedImage!RGBA` should be destroyed with `destroyFree`. 693 /// Throws: $(D ImageIOException) on error. 694 OwnedImage!RGBA loadOwnedImage(in void[] imageData) 695 { 696 Image image; 697 image.loadFromMemory(cast(const(ubyte[])) imageData, 698 LOAD_RGB | LOAD_8BIT | LOAD_ALPHA | LAYOUT_VERT_STRAIGHT | LAYOUT_GAPLESS); 699 if (image.isError) 700 { 701 assert(false, "Decoding failed"); // FUTURE: could do something more sensible maybe 702 } 703 704 return convertImageToOwnedImage_rgba8(image); 705 } 706 707 /// Loads two different images: 708 /// - the 1st is the RGB channels 709 /// - the 2nd is interpreted as greyscale and fetch in the alpha channel of the result. 710 /// The returned `OwnedImage!RGBA` should be destroyed with `destroyFree`. 711 OwnedImage!RGBA loadImageSeparateAlpha(in void[] imageDataRGB, in void[] imageDataAlpha) 712 { 713 Image alpha; 714 alpha.loadFromMemory(cast(const(ubyte[])) imageDataAlpha, 715 LOAD_GREYSCALE | LOAD_8BIT | LOAD_NO_ALPHA | LAYOUT_VERT_STRAIGHT | LAYOUT_GAPLESS); 716 if (alpha.isError) 717 { 718 assert(false, "Decoding failed"); // same remark as above 719 } 720 721 Image rgb; 722 rgb.loadFromMemory(cast(const(ubyte[])) imageDataRGB, 723 LOAD_RGB | LOAD_8BIT | LOAD_ALPHA | LAYOUT_VERT_STRAIGHT | LAYOUT_GAPLESS); 724 if (rgb.isError) 725 { 726 assert(false, "Decoding failed"); // same remark as above 727 } 728 729 if ( (rgb.width != alpha.width) || (rgb.height != alpha.height) ) 730 { 731 // If you fail here, typically size of your Diffuse map doesn't match the Emissive map. 732 assert(false, "Image size mismatch"); // same remark as above 733 } 734 735 int W = rgb.width; 736 int H = rgb.height; 737 738 for (int y = 0; y < H; ++y) 739 { 740 ubyte* scanA = cast(ubyte*) alpha.scanline(y); 741 ubyte* scanRGBA = cast(ubyte*) rgb.scanline(y); 742 for (int x = 0; x < W; ++x) 743 { 744 scanRGBA[4*x + 3] = scanA[x]; 745 } 746 } 747 748 return convertImageToOwnedImage_rgba8(rgb); 749 } 750 751 /// Loads two different images: 752 /// - the 1st is the RGB channels 753 /// - the 2nd is interpreted as greyscale and fetch in the alpha channel of the result. 754 /// The returned `OwnedImage!RGBA` should be destroyed with `destroyFree`. 755 OwnedImage!RGBA loadImageWithFilledAlpha(in void[] imageDataRGB, ubyte alphaValue) 756 { 757 Image rgb; 758 rgb.loadFromMemory(cast(const(ubyte[])) imageDataRGB, 759 LOAD_RGB | LOAD_8BIT | LOAD_ALPHA | LAYOUT_VERT_STRAIGHT | LAYOUT_GAPLESS); 760 if (rgb.isError) 761 { 762 assert(false, "Decoding failed"); // same remark as above 763 } 764 int W = rgb.width; 765 int H = rgb.height; 766 for (int y = 0; y < H; ++y) 767 { 768 ubyte* scanRGBA = cast(ubyte*) rgb.scanline(y); 769 for (int x = 0; x < W; ++x) 770 { 771 scanRGBA[4*x + 3] = alphaValue; 772 } 773 } 774 return convertImageToOwnedImage_rgba8(rgb); 775 } 776 777 778 /// Loads an image to be a 16-bit one channel image (`OwnedImage!L16`). 779 /// The returned `OwnedImage!L16` should be destroyed with `destroyFree`. 780 /// Throws: $(D ImageIOException) on error. 781 OwnedImage!L16 loadOwnedImageDepth(in void[] imageData) 782 { 783 Image image; 784 image.loadFromMemory(cast(const(ubyte[])) imageData, LOAD_NO_ALPHA); 785 if (image.isError) 786 { 787 assert(false, "Decoding failed"); 788 } 789 790 if (image.type() == PixelType.rgb8) 791 { 792 // Legacy 8-bit to 16-bit depth conversion 793 // This is the original legacy way to load depth. 794 // Load as 8-bit RGBA, then mix the channels so as to support 795 // legacy depth maps. 796 Image image16; 797 image16.setSize(image.width, image.height, PixelType.l16, LAYOUT_VERT_STRAIGHT | LAYOUT_GAPLESS); 798 int width = image.width; 799 int height = image.height; 800 801 OwnedImage!L16 result = mallocNew!(OwnedImage!L16)(width, height); 802 803 for (int j = 0; j < height; ++j) 804 { 805 ubyte* inDepth = cast(ubyte*) image.scanline(j); 806 ushort* outDepth = cast(ushort*) image16.scanline(j); 807 808 for (int i = 0; i < width; ++i) 809 { 810 // Using 257 to span the full range of depth: 257 * (255+255+255)/3 = 65535 811 // If we do that inside stb_image, it will first reduce channels _then_ increase bitdepth. 812 // Instead, this is used to gain 1.5 bit of accuracy for 8-bit depth maps. :) 813 float d = 0.5f + 257 * (inDepth[3*i+0] + inDepth[3*i+1] + inDepth[3*i+2]) / 3; 814 outDepth[i] = cast(ushort)(d); 815 } 816 } 817 return convertImageToOwnedImage_l16(image16); 818 } 819 else 820 { 821 // Ensure the format pixel is l16, and the layout is compatible with OwnedImage 822 image.convertTo(PixelType.l16, LAYOUT_VERT_STRAIGHT | LAYOUT_GAPLESS); 823 return convertImageToOwnedImage_l16(image); 824 } 825 } 826 827 /// Make a gamut `Image` view from a dplug:graphics `ImageRef`. 828 /// The original `OwnedImage` is still the owner of the pixel data. 829 void createViewFromImageRef(COLOR)(ref Image view, ImageRef!COLOR source) 830 { 831 static if (is(COLOR == RGBA)) 832 { 833 view.createViewFromData(source.pixels, source.w, source.h, PixelType.rgba8, cast(int)source.pitch); 834 } 835 else static if (is(COLOR == RGB)) 836 { 837 view.createViewFromData(source.pixels, source.w, source.h, PixelType.rgb8, cast(int)source.pitch); 838 } 839 else static if (is(COLOR == L8)) 840 { 841 view.createViewFromData(source.pixels, source.w, source.h, PixelType.l8, cast(int)source.pitch); 842 } 843 else static if (is(COLOR == L16)) 844 { 845 view.createViewFromData(source.pixels, source.w, source.h, PixelType.l16, cast(int)source.pitch); 846 } 847 else 848 static assert(false); 849 } 850 851 /// On the contrary, get an ImageRef!XXX from a Gamut `Image` without disowning it. 852 ImageRef!COLOR getImageRef(COLOR)(ref Image image) 853 { 854 // Must check dynamic type though. 855 static if (is(COLOR == L8)) 856 assert(image.type() == PixelType.l8); 857 else static if (is(COLOR == L16)) 858 assert(image.type() == PixelType.l16); 859 else static if (is(COLOR == RGB)) 860 assert(image.type() == PixelType.rgb8); 861 else static if (is(COLOR == RGBA)) 862 assert(image.type() == PixelType.rgba8); 863 else static if (is(COLOR == RGBA16)) 864 assert(image.type() == PixelType.rgba16); 865 else 866 static assert(false); 867 868 ImageRef!COLOR r; 869 r.w = image.width(); 870 r.h = image.height(); 871 assert(image.pitchInBytes() >= 0); // Doesn't work for negative pitches. 872 r.pitch = image.pitchInBytes; 873 r.pixels = cast(COLOR*) image.scanptr(0); 874 return r; 875 } 876 877 878 /// Convert and disown gamut Image to OwnedImage. 879 /// Result: ìmage` is disowned, result is owning the image data. 880 OwnedImage!RGBA convertImageToOwnedImage_rgba8(ref Image image) 881 { 882 assert(image.type() == PixelType.rgba8); 883 OwnedImage!RGBA r = mallocNew!(OwnedImage!RGBA); 884 r.w = image.width; 885 r.h = image.height; 886 r._pixels = cast(RGBA*) image.scanline(0); 887 r._bytePitch = image.pitchInBytes(); 888 r._buffer = image.disownData(); 889 r._border = 0; // TODO should keep it from input 890 r._borderRight = 0; // TODO should keep it from input 891 return r; 892 } 893 894 ///ditto 895 OwnedImage!RGB convertImageToOwnedImage_rgb8(ref Image image) 896 { 897 assert(image.type() == PixelType.rgb8); 898 OwnedImage!RGB r = mallocNew!(OwnedImage!RGB); 899 r.w = image.width; 900 r.h = image.height; 901 r._pixels = cast(RGB*) image.scanline(0); 902 r._bytePitch = image.pitchInBytes(); 903 r._buffer = image.disownData(); 904 r._border = 0; // TODO should keep it from input 905 r._borderRight = 0; // TODO should keep it from input 906 return r; 907 } 908 909 ///ditto 910 OwnedImage!L16 convertImageToOwnedImage_l16(ref Image image) 911 { 912 assert(image.type() == PixelType.l16); 913 OwnedImage!L16 r = mallocNew!(OwnedImage!L16); 914 r.w = image.width; 915 r.h = image.height; 916 r._pixels = cast(L16*) image.scanline(0); 917 r._bytePitch = image.pitchInBytes(); 918 r._buffer = image.disownData(); 919 r._border = 0; // TODO should keep it from input 920 r._borderRight = 0; // TODO should keep it from input 921 return r; 922 } 923 924 ///ditto 925 OwnedImage!L8 convertImageToOwnedImage_l8(ref Image image) 926 { 927 assert(image.type() == PixelType.l8); 928 OwnedImage!L8 r = mallocNew!(OwnedImage!L8); 929 r.w = image.width; 930 r.h = image.height; 931 r._pixels = cast(L8*) image.scanline(0); 932 r._bytePitch = image.pitchInBytes(); 933 r._buffer = image.disownData(); 934 r._border = 0; // TODO should keep it from input 935 r._borderRight = 0; // TODO should keep it from input 936 return r; 937 }