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