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 /// Premultiply by alpha 448 static if (is(COLOR == RGBA)) 449 { 450 void premultiply() 451 { 452 453 static RGBA premultiplyColor(RGBA c) pure nothrow @nogc 454 { 455 c.r = (c.r * c.a + 128) / 255; 456 c.g = (c.g * c.a + 128) / 255; 457 c.b = (c.b * c.a + 128) / 255; 458 return c; 459 } 460 461 for (int y = 0; y < h; ++y) 462 { 463 COLOR* scan = unsafeScanlinePtr(y); 464 for (int x = 0; x < w; ++x) 465 { 466 scan[x] = premultiplyColor(scan[x]); 467 } 468 } 469 } 470 } 471 472 /// Fill the borders by taking the nearest existing pixel in the meaningful area. 473 void replicateBorders() 474 { 475 replicateBordersTouching( box2i.rectangle(0, 0, this.w, this.h) ); 476 } 477 478 /// Fill the borders _touching updatedRect_ by taking the nearest existing pixel in the meaningful area. 479 void replicateBordersTouching(box2i updatedRect) 480 { 481 if (w < 1 || h < 1) 482 return; // can't replicate borders of an empty image 483 484 // BORDER REPLICATION. 485 // If an area is touching left border, then the border needs replication. 486 // If an area is touching left and top border, then the border needs replication and the corner pixel should be filled too. 487 // Same for all four corners. Border only applies to level 0. 488 // 489 // OOOOOOOxxxxxxxxxxxxxxx 490 // Ooooooo 491 // Oo o 492 // Oo o <------ if the update area is made of 'o', then the area to replicate is 'O' 493 // Ooooooo 494 // x 495 // x 496 497 int W = this.w; 498 int H = this.h; 499 500 int minx = updatedRect.min.x; 501 int maxx = updatedRect.max.x; 502 int miny = updatedRect.min.y; 503 int maxy = updatedRect.max.y; 504 505 bool touchLeft = (minx == 0); 506 bool touchTop = (miny == 0); 507 bool touchRight = (maxx == W); 508 bool touchBottom = (maxy == H); 509 510 int bTop = borderTop(); 511 int bBott = borderBottom(); 512 int bLeft = borderLeft(); 513 int bRight = borderRight(); 514 515 if (touchTop) 516 { 517 if (touchLeft) 518 { 519 COLOR topLeft = this[0, 0]; 520 for (int y = -borderTop; y < 0; ++y) 521 for (int x = -borderLeft; x < 0; ++x) 522 unsafeScanlinePtr(y)[x] = topLeft; 523 } 524 525 for (int y = -borderTop; y < 0; ++y) 526 { 527 unsafeScanlinePtr(y)[minx..maxx] = scanline(0)[minx..maxx]; 528 } 529 530 if (touchRight) 531 { 532 COLOR topRight = this[W-1, 0]; 533 for (int y = -borderTop; y < 0; ++y) 534 for (int x = 0; x < borderRight(); ++x) 535 unsafeScanlinePtr(y)[W + x] = topRight; 536 } 537 } 538 539 if (touchLeft) 540 { 541 for (int y = miny; y < maxy; ++y) 542 { 543 COLOR edge = this[0, y]; 544 for (int x = -borderLeft(); x < 0; ++x) 545 unsafeScanlinePtr(y)[x] = edge; 546 } 547 } 548 549 if (touchRight) 550 { 551 for (int y = miny; y < maxy; ++y) 552 { 553 COLOR edge = this[W-1, y]; 554 for (int x = 0; x < borderRight(); ++x) 555 unsafeScanlinePtr(y)[W + x] = edge; 556 } 557 } 558 559 if (touchBottom) 560 { 561 if (touchLeft) 562 { 563 COLOR bottomLeft = this[0, H-1]; 564 for (int y = H; y < H + borderTop(); ++y) 565 for (int x = -borderLeft; x < 0; ++x) 566 unsafeScanlinePtr(y)[x] = bottomLeft; 567 } 568 569 for (int y = H; y < H + borderBottom(); ++y) 570 { 571 unsafeScanlinePtr(y)[minx..maxx] = scanline(H-1)[minx..maxx]; 572 } 573 574 if (touchRight) 575 { 576 COLOR bottomRight = this[W-1, H-1]; 577 for (int y = H; y < H + borderTop(); ++y) 578 for (int x = 0; x < borderRight(); ++x) 579 unsafeScanlinePtr(y)[W + x] = bottomRight; 580 } 581 } 582 } 583 584 private: 585 586 /// Adress of the first meaningful pixel 587 COLOR* _pixels; 588 589 /// Samples difference between rows of pixels. 590 int _bytePitch; 591 592 /// Address of the allocation itself 593 void* _buffer = null; 594 595 /// Size of left, top and bottom borders, around the meaningful area. 596 int _border; 597 598 /// Size of border at thr right of the meaningful area (most positive X) 599 int _borderRight; 600 601 // Internal use: allows to get the scanlines of top and bottom borders too 602 COLOR* unsafeScanlinePtr(int y) pure 603 { 604 assert(y >= -borderTop()); // negative Y allows to get top border scanlines 605 assert(y < h + borderBottom()); // Y overflow allows to get bottom border scanlines 606 int byteOffset = _bytePitch * y; // unlike the normal `scanlinePtr`, there will be a MOVSXD here 607 return cast(COLOR*)(cast(ubyte*)(_pixels) + byteOffset); 608 } 609 610 static int computeRightPadding(int width, int border, int xMultiplicity) pure 611 { 612 int nextMultiple = cast(int)(nextMultipleOf(width + border, xMultiplicity)); 613 return nextMultiple - (width + border); 614 } 615 616 static size_t nextMultipleOf(size_t base, size_t multiple) pure 617 { 618 assert(multiple > 0); 619 size_t n = (base + multiple - 1) / multiple; 620 return multiple * n; 621 } 622 623 /// Returns: next pointer aligned with alignment bytes. 624 static void* nextAlignedPointer(void* start, size_t alignment) pure 625 { 626 return cast(void*)nextMultipleOf(cast(size_t)(start), alignment); 627 } 628 } 629 630 unittest 631 { 632 static assert(isDirectView!(OwnedImage!ubyte)); 633 assert(OwnedImage!RGBA.computeRightPadding(4, 0, 4) == 0); 634 assert(OwnedImage!RGBA.computeRightPadding(1, 3, 5) == 1); 635 assert(OwnedImage!L16.computeRightPadding(2, 0, 4) == 2); 636 assert(OwnedImage!RGBA.computeRightPadding(2, 1, 4) == 1); 637 assert(OwnedImage!RGBf.nextMultipleOf(0, 7) == 0); 638 assert(OwnedImage!RGBAf.nextMultipleOf(1, 7) == 7); 639 assert(OwnedImage!RGBA.nextMultipleOf(6, 7) == 7); 640 assert(OwnedImage!RGBA.nextMultipleOf(7, 7) == 7); 641 assert(OwnedImage!RGBA.nextMultipleOf(8, 7) == 14); 642 643 { 644 OwnedImage!RGBA img = mallocNew!(OwnedImage!RGBA); 645 scope(exit) destroyFree(img); 646 img.size(0, 0); // should be supported 647 648 int border = 10; 649 img.size(0, 0, border); // should also be supported (border with no data!) 650 img.replicateBorders(); // should not crash 651 652 border = 0; 653 img.size(1, 1, border); 654 img.replicateBorders(); // should not crash 655 } 656 } 657 658 // Test border replication 659 unittest 660 { 661 OwnedImage!L8 img = mallocNew!(OwnedImage!L8); 662 scope(exit) destroyFree(img); 663 int width = 2; 664 int height = 2; 665 int border = 2; 666 int xMultiplicity = 1; 667 int trailing = 3; 668 img.size(width, height, border, xMultiplicity, trailing); 669 assert(img.w == 2 && img.h == 2); 670 assert(img.borderLeft() == 2 && img.borderRight() == 3); 671 assert(img._bytePitch == 7); 672 673 img.fillWith(L8(5)); 674 img.scanline(0)[0].l = 1; 675 img.scanlinePtr(0)[1].l = 2; 676 img[0, 1].l = 3; 677 img[1, 1].l = 4; 678 679 img.replicateBorders(); 680 ubyte[7][6] correct = 681 [ 682 [1, 1, 1, 2, 2, 2, 2], 683 [1, 1, 1, 2, 2, 2, 2], 684 [1, 1, 1, 2, 2, 2, 2], 685 [3, 3, 3, 4, 4, 4, 4], 686 [3, 3, 3, 4, 4, 4, 4], 687 [3, 3, 3, 4, 4, 4, 4], 688 ]; 689 690 for (int y = -2; y < 4; ++y) 691 { 692 for (int x = -2; x < 5; ++x) 693 { 694 L8 read = img.unsafeScanlinePtr(y)[x]; 695 ubyte good = correct[y+2][x+2]; 696 assert(read.l == good); 697 } 698 } 699 } 700 701 // Test toRef 702 unittest 703 { 704 OwnedImage!L8 img = mallocNew!(OwnedImage!L8); 705 scope(exit) destroyFree(img); 706 707 int border = 0; 708 int rowAlignment = 1; 709 int xMultiplicity = 1; 710 int trailingSamples = 4; 711 img.size(10, 10, border, rowAlignment, xMultiplicity, trailingSamples); // Ask for 4 trailing samples 712 ImageRef!L8 r = img.toRef(); 713 assert(r.pitch >= 10 + 4); // per the API, OwnedImage could add more space than required 714 } 715 716 /// Loads an image from compressed data. 717 /// The returned `OwnedImage!RGBA` should be destroyed with `destroyFree`. 718 /// Throws: $(D ImageIOException) on error. 719 OwnedImage!RGBA loadOwnedImage(in void[] imageData) 720 { 721 Image image; 722 image.loadFromMemory(cast(const(ubyte[])) imageData, 723 LOAD_RGB | LOAD_8BIT | LOAD_ALPHA | LOAD_NO_PREMUL | LAYOUT_VERT_STRAIGHT | LAYOUT_GAPLESS); 724 if (image.isError) 725 { 726 assert(false, "Decoding failed"); // FUTURE: could do something more sensible maybe 727 } 728 729 return convertImageToOwnedImage_rgba8(image); 730 } 731 732 /// Loads an image from compressed data and ensure the alpha is premultiplied. 733 /// The returned `OwnedImage!RGBA` should be destroyed with `destroyFree`. 734 /// Throws: $(D ImageIOException) on error. 735 OwnedImage!RGBA loadOwnedImagePremul(in void[] imageData) 736 { 737 Image image; 738 image.loadFromMemory(cast(const(ubyte[])) imageData, 739 LOAD_RGB | LOAD_8BIT | LOAD_ALPHA | LOAD_PREMUL | LAYOUT_VERT_STRAIGHT | LAYOUT_GAPLESS); 740 if (image.isError) 741 { 742 assert(false, "Decoding failed"); // FUTURE: could do something more sensible maybe 743 } 744 745 return convertImageToOwnedImage_rgba8(image); 746 } 747 748 /// Loads two different images: 749 /// - the 1st is the RGB channels 750 /// - the 2nd is interpreted as greyscale and fetch in the alpha channel of the result. 751 /// The returned `OwnedImage!RGBA` should be destroyed with `destroyFree`. 752 OwnedImage!RGBA loadImageSeparateAlpha(in void[] imageDataRGB, in void[] imageDataAlpha) 753 { 754 Image alpha; 755 alpha.loadFromMemory(cast(const(ubyte[])) imageDataAlpha, 756 LOAD_GREYSCALE | LOAD_8BIT | LOAD_NO_ALPHA | LOAD_NO_PREMUL | LAYOUT_VERT_STRAIGHT | LAYOUT_GAPLESS); 757 if (alpha.isError) 758 { 759 assert(false, "Decoding failed"); // same remark as above 760 } 761 762 Image rgb; 763 rgb.loadFromMemory(cast(const(ubyte[])) imageDataRGB, 764 LOAD_RGB | LOAD_8BIT | LOAD_ALPHA | LOAD_NO_PREMUL | LAYOUT_VERT_STRAIGHT | LAYOUT_GAPLESS); 765 if (rgb.isError) 766 { 767 assert(false, "Decoding failed"); // same remark as above 768 } 769 770 if ( (rgb.width != alpha.width) || (rgb.height != alpha.height) ) 771 { 772 // If you fail here, typically size of your Diffuse map doesn't match the Emissive map. 773 assert(false, "Image size mismatch"); // same remark as above 774 } 775 776 int W = rgb.width; 777 int H = rgb.height; 778 779 for (int y = 0; y < H; ++y) 780 { 781 ubyte* scanA = cast(ubyte*) alpha.scanline(y); 782 ubyte* scanRGBA = cast(ubyte*) rgb.scanline(y); 783 for (int x = 0; x < W; ++x) 784 { 785 scanRGBA[4*x + 3] = scanA[x]; 786 } 787 } 788 789 return convertImageToOwnedImage_rgba8(rgb); 790 } 791 792 /// Loads two different images: 793 /// - the 1st is the RGB channels 794 /// - the 2nd is interpreted as greyscale and fetch in the alpha channel of the result. 795 /// The returned `OwnedImage!RGBA` should be destroyed with `destroyFree`. 796 OwnedImage!RGBA loadImageWithFilledAlpha(in void[] imageDataRGB, ubyte alphaValue) 797 { 798 Image rgb; 799 rgb.loadFromMemory(cast(const(ubyte[])) imageDataRGB, 800 LOAD_RGB | LOAD_8BIT | LOAD_ALPHA | LOAD_NO_PREMUL | LAYOUT_VERT_STRAIGHT | LAYOUT_GAPLESS); 801 if (rgb.isError) 802 { 803 assert(false, "Decoding failed"); // same remark as above 804 } 805 int W = rgb.width; 806 int H = rgb.height; 807 for (int y = 0; y < H; ++y) 808 { 809 ubyte* scanRGBA = cast(ubyte*) rgb.scanline(y); 810 for (int x = 0; x < W; ++x) 811 { 812 scanRGBA[4*x + 3] = alphaValue; 813 } 814 } 815 return convertImageToOwnedImage_rgba8(rgb); 816 } 817 818 819 /// Loads an image to be a 16-bit one channel image (`OwnedImage!L16`). 820 /// The returned `OwnedImage!L16` should be destroyed with `destroyFree`. 821 /// Throws: $(D ImageIOException) on error. 822 OwnedImage!L16 loadOwnedImageDepth(in void[] imageData) 823 { 824 Image image; 825 image.loadFromMemory(cast(const(ubyte[])) imageData, LOAD_NO_ALPHA); 826 if (image.isError) 827 { 828 assert(false, "Decoding failed"); 829 } 830 831 if (image.type() == PixelType.rgb8) 832 { 833 // Legacy 8-bit to 16-bit depth conversion 834 // This is the original legacy way to load depth. 835 // Load as 8-bit RGBA, then mix the channels so as to support 836 // legacy depth maps. 837 Image image16; 838 image16.setSize(image.width, image.height, PixelType.l16, LAYOUT_VERT_STRAIGHT | LAYOUT_GAPLESS); 839 int width = image.width; 840 int height = image.height; 841 842 OwnedImage!L16 result = mallocNew!(OwnedImage!L16)(width, height); 843 844 for (int j = 0; j < height; ++j) 845 { 846 ubyte* inDepth = cast(ubyte*) image.scanline(j); 847 ushort* outDepth = cast(ushort*) image16.scanline(j); 848 849 for (int i = 0; i < width; ++i) 850 { 851 // Using 257 to span the full range of depth: 257 * (255+255+255)/3 = 65535 852 // If we do that inside stb_image, it will first reduce channels _then_ increase bitdepth. 853 // Instead, this is used to gain 1.5 bit of accuracy for 8-bit depth maps. :) 854 float d = 0.5f + 257 * (inDepth[3*i+0] + inDepth[3*i+1] + inDepth[3*i+2]) / 3; 855 outDepth[i] = cast(ushort)(d); 856 } 857 } 858 return convertImageToOwnedImage_l16(image16); 859 } 860 else 861 { 862 // Ensure the format pixel is l16, and the layout is compatible with OwnedImage 863 image.convertTo(PixelType.l16, LAYOUT_VERT_STRAIGHT | LAYOUT_GAPLESS); 864 return convertImageToOwnedImage_l16(image); 865 } 866 } 867 868 /// Make a gamut `Image` view from a dplug:graphics `ImageRef`. 869 /// The original `OwnedImage` is still the owner of the pixel data. 870 void createViewFromImageRef(COLOR)(ref Image view, ImageRef!COLOR source) 871 { 872 static if (is(COLOR == RGBA)) 873 { 874 view.createViewFromData(source.pixels, source.w, source.h, PixelType.rgba8, cast(int)source.pitch); 875 } 876 else static if (is(COLOR == RGB)) 877 { 878 view.createViewFromData(source.pixels, source.w, source.h, PixelType.rgb8, cast(int)source.pitch); 879 } 880 else static if (is(COLOR == L8)) 881 { 882 view.createViewFromData(source.pixels, source.w, source.h, PixelType.l8, cast(int)source.pitch); 883 } 884 else static if (is(COLOR == L16)) 885 { 886 view.createViewFromData(source.pixels, source.w, source.h, PixelType.l16, cast(int)source.pitch); 887 } 888 else 889 static assert(false); 890 } 891 892 /// On the contrary, get an ImageRef!XXX from a Gamut `Image` without disowning it. 893 ImageRef!COLOR getImageRef(COLOR)(ref Image image) 894 { 895 // Must check dynamic type though. 896 static if (is(COLOR == L8)) 897 assert(image.type() == PixelType.l8); 898 else static if (is(COLOR == L16)) 899 assert(image.type() == PixelType.l16); 900 else static if (is(COLOR == RGB)) 901 assert(image.type() == PixelType.rgb8); 902 else static if (is(COLOR == RGBA)) 903 assert(image.type() == PixelType.rgba8); 904 else static if (is(COLOR == RGBA16)) 905 assert(image.type() == PixelType.rgba16); 906 else 907 static assert(false); 908 909 ImageRef!COLOR r; 910 r.w = image.width(); 911 r.h = image.height(); 912 assert(image.pitchInBytes() >= 0); // Doesn't work for negative pitches. 913 r.pitch = image.pitchInBytes; 914 r.pixels = cast(COLOR*) image.scanptr(0); 915 return r; 916 } 917 918 919 /// Convert and disown gamut Image to OwnedImage. 920 /// Result: ìmage` is disowned, result is owning the image data. 921 OwnedImage!RGBA convertImageToOwnedImage_rgba8(ref Image image) 922 { 923 assert(image.type() == PixelType.rgba8); 924 OwnedImage!RGBA r = mallocNew!(OwnedImage!RGBA); 925 r.w = image.width; 926 r.h = image.height; 927 r._pixels = cast(RGBA*) image.scanline(0); 928 r._bytePitch = image.pitchInBytes(); 929 r._buffer = image.disownData(); 930 r._border = 0; // TODO should keep it from input 931 r._borderRight = 0; // TODO should keep it from input 932 return r; 933 } 934 935 ///ditto 936 OwnedImage!RGB convertImageToOwnedImage_rgb8(ref Image image) 937 { 938 assert(image.type() == PixelType.rgb8); 939 OwnedImage!RGB r = mallocNew!(OwnedImage!RGB); 940 r.w = image.width; 941 r.h = image.height; 942 r._pixels = cast(RGB*) image.scanline(0); 943 r._bytePitch = image.pitchInBytes(); 944 r._buffer = image.disownData(); 945 r._border = 0; // TODO should keep it from input 946 r._borderRight = 0; // TODO should keep it from input 947 return r; 948 } 949 950 ///ditto 951 OwnedImage!L16 convertImageToOwnedImage_l16(ref Image image) 952 { 953 assert(image.type() == PixelType.l16); 954 OwnedImage!L16 r = mallocNew!(OwnedImage!L16); 955 r.w = image.width; 956 r.h = image.height; 957 r._pixels = cast(L16*) image.scanline(0); 958 r._bytePitch = image.pitchInBytes(); 959 r._buffer = image.disownData(); 960 r._border = 0; // TODO should keep it from input 961 r._borderRight = 0; // TODO should keep it from input 962 return r; 963 } 964 965 ///ditto 966 OwnedImage!L8 convertImageToOwnedImage_l8(ref Image image) 967 { 968 assert(image.type() == PixelType.l8); 969 OwnedImage!L8 r = mallocNew!(OwnedImage!L8); 970 r.w = image.width; 971 r.h = image.height; 972 r._pixels = cast(L8*) image.scanline(0); 973 r._bytePitch = image.pitchInBytes(); 974 r._buffer = image.disownData(); 975 r._border = 0; // TODO should keep it from input 976 r._borderRight = 0; // TODO should keep it from input 977 return r; 978 }