1 /** 2 Manipulation on image types and layout, that do not belong to public API. 3 4 Copyright: Copyright Guillaume Piolat 2022 5 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 6 7 */ 8 module gamut.internals.types; 9 10 //import core.stdc.stdlib: malloc, realloc, free; 11 import core.stdc.string: memset; 12 13 import gamut.types; 14 15 nothrow @nogc @safe: 16 17 enum LayoutConstraints 18 LAYOUT_MULTIPLICITY_MASK = 3, 19 LAYOUT_TRAILING_MASK = 12, 20 LAYOUT_BORDER_MASK = 384, 21 LAYOUT_SCANLINE_ALIGNED_MASK = 112; 22 23 24 /// Returns: `true` if this `PixelType` is "plain", meaning that it's 1/2/3/4 channel of L/LA/RGB/RGBA data. 25 /// Currently: all images are plain, or have no data. 26 bool pixelTypeIsPlain(PixelType t) pure 27 { 28 return true; 29 } 30 31 /// Returns: `true` if this `PixelType` is planar, completely unsupported for now. 32 bool pixelTypeIsPlanar(PixelType t) pure 33 { 34 return false; // No support yet in gamut. 35 } 36 37 /// Returns: `true` if this `PixelType` is compressed, meaning the data is inscrutable until decoded. 38 bool pixelTypeIsCompressed(PixelType t) pure 39 { 40 return false; // No support yet in gamut. 41 } 42 43 /// Size of one pixel for given pixel type `type`, in bytes. 44 int pixelTypeSize(PixelType type) pure 45 { 46 final switch(type) with (PixelType) 47 { 48 case l8: return 1; 49 case l16: return 2; 50 case lf32: return 4; 51 case la8: return 2; 52 case la16: return 4; 53 case laf32: return 8; 54 case rgb8: return 3; 55 case rgb16: return 6; 56 case rgba8: return 4; 57 case rgba16: return 8; 58 case rgbf32: return 12; 59 case rgbaf32: return 16; 60 case unknown: assert(false); 61 } 62 } 63 64 enum int GAMUT_MAX_PIXEL_SIZE = 16; // keep it in sync 65 66 /// Number of channels in this image type. 67 int pixelTypeNumChannels(PixelType type) pure 68 { 69 final switch(type) with (PixelType) 70 { 71 case l8: return 1; 72 case l16: return 1; 73 case lf32: return 1; 74 case la8: return 2; 75 case la16: return 2; 76 case laf32: return 2; 77 case rgb8: return 3; 78 case rgb16: return 3; 79 case rgbf32: return 3; 80 case rgba8: return 4; 81 case rgba16: return 4; 82 case rgbaf32: return 4; 83 case unknown: assert(false); 84 } 85 } 86 87 /// Is this type 8-bit? 88 bool pixelTypeIs8Bit(PixelType type) pure 89 { 90 switch(type) with (PixelType) 91 { 92 case l8: 93 case la8: 94 case rgb8: 95 case rgba8: 96 return true; 97 default: 98 return false; 99 } 100 } 101 102 /// Is this type 16-bit? 103 bool pixelTypeIs16Bit(PixelType type) pure 104 { 105 switch(type) with (PixelType) 106 { 107 case l16: 108 case la16: 109 case rgb8: 110 case rgba8: 111 return true; 112 default: 113 return false; 114 } 115 } 116 117 /// Is this type 32-bit floating-point? 118 int pixelTypeIsFP32(PixelType type) pure 119 { 120 switch(type) with (PixelType) 121 { 122 case lf32: 123 case laf32: 124 case rgbf32: 125 case rgbaf32: 126 return true; 127 default: 128 return false; 129 } 130 } 131 132 /// Can this pixel type be losslessly expressed in rgba8? 133 bool pixelTypeExpressibleInRGBA8(PixelType type) pure 134 { 135 return pixelTypeIs8Bit(type); 136 } 137 138 /// Check if these image dimensions are valid in Gamut. 139 bool imageIsValidSize(int layers, int width, int height) pure 140 { 141 if (layers < 0 || width < 0 || height < 0) 142 return false; 143 144 if (layers > GAMUT_MAX_IMAGE_LAYERS) 145 return false; 146 147 if (width > GAMUT_MAX_IMAGE_WIDTH || height > GAMUT_MAX_IMAGE_HEIGHT) 148 return false; 149 150 return true; 151 } 152 153 154 /// From a layout constraint, get requested pixel multiplicity. 155 int layoutMultiplicity(LayoutConstraints constraints) pure 156 { 157 return 1 << (constraints & 3); 158 } 159 unittest 160 { 161 assert(layoutMultiplicity(LAYOUT_MULTIPLICITY_1) == 1); 162 assert(layoutMultiplicity(LAYOUT_MULTIPLICITY_8) == 8); 163 } 164 165 /// From a layout constraint, get requested trailing pixels. 166 int layoutTrailingPixels(LayoutConstraints constraints) pure @trusted 167 { 168 return (1 << ((constraints & 0x0C) >> 2)) - 1; 169 } 170 unittest 171 { 172 assert(layoutTrailingPixels(LAYOUT_TRAILING_0) == 0); 173 assert(layoutTrailingPixels(LAYOUT_TRAILING_1) == 1); 174 assert(layoutTrailingPixels(LAYOUT_TRAILING_3) == 3); 175 assert(layoutTrailingPixels(LAYOUT_TRAILING_7 | LAYOUT_MULTIPLICITY_8) == 7); 176 } 177 178 /// From a layout constraint, get scanline alignment. 179 int layoutScanlineAlignment(LayoutConstraints constraints) pure 180 { 181 return 1 << ((constraints >> 4) & 0x0f); 182 } 183 unittest 184 { 185 assert(layoutScanlineAlignment(LAYOUT_SCANLINE_ALIGNED_1 | LAYOUT_TRAILING_7) == 1); 186 assert(layoutScanlineAlignment(LAYOUT_SCANLINE_ALIGNED_128) == 128); 187 } 188 189 190 /// What is the scanline alignement of such a pointer? 191 LayoutConstraints getPointerAlignment(size_t ptr) 192 { 193 if ( (ptr & 127) == 0) return LAYOUT_SCANLINE_ALIGNED_128; 194 if ( (ptr & 63) == 0) return LAYOUT_SCANLINE_ALIGNED_64; 195 if ( (ptr & 31) == 0) return LAYOUT_SCANLINE_ALIGNED_32; 196 if ( (ptr & 15) == 0) return LAYOUT_SCANLINE_ALIGNED_16; 197 if ( (ptr & 7) == 0) return LAYOUT_SCANLINE_ALIGNED_8; 198 if ( (ptr & 3) == 0) return LAYOUT_SCANLINE_ALIGNED_4; 199 if ( (ptr & 1) == 0) return LAYOUT_SCANLINE_ALIGNED_2; 200 return LAYOUT_SCANLINE_ALIGNED_1; 201 } 202 203 /// From a layout constraint, get surrounding border width. 204 int layoutBorderWidth(LayoutConstraints constraints) pure 205 { 206 return (constraints >> 7) & 3; 207 } 208 unittest 209 { 210 assert(layoutBorderWidth(LAYOUT_BORDER_0) == 0); 211 assert(layoutBorderWidth(LAYOUT_BORDER_1) == 1); 212 assert(layoutBorderWidth(LAYOUT_BORDER_2 | LAYOUT_TRAILING_7) == 2); 213 assert(layoutBorderWidth(LAYOUT_BORDER_3) == 3); 214 } 215 216 /// From a layout constraint, get if being gapless is guaranteed. 217 bool layoutGapless(LayoutConstraints constraints) pure 218 { 219 return (constraints & LAYOUT_GAPLESS) != 0; 220 } 221 unittest 222 { 223 assert(layoutGapless(LAYOUT_GAPLESS)); 224 assert(!layoutGapless(0)); 225 } 226 227 /// Assuming the same `PixelType`, can an allocation made with constraint `older` 228 /// be used with constraint `newer`? 229 /// Note: `older` doesn't need to be a valid LayoutConstraints, but newer must be. 230 bool layoutConstraintsCompatible(LayoutConstraints newer, LayoutConstraints older) pure 231 { 232 if ((newer & LAYOUT_GAPLESS) && !(older & LAYOUT_GAPLESS)) 233 return false; 234 235 if ((newer & LAYOUT_VERT_FLIPPED) && !(older & LAYOUT_VERT_FLIPPED)) 236 return false; 237 if ((newer & LAYOUT_VERT_STRAIGHT) && !(older & LAYOUT_VERT_STRAIGHT)) 238 return false; 239 240 if (layoutMultiplicity(newer) > layoutMultiplicity(older)) 241 return false; 242 243 if (layoutTrailingPixels(newer) > layoutTrailingPixels(older)) 244 return false; 245 246 if (layoutScanlineAlignment(newer) > layoutScanlineAlignment(older)) 247 return false; 248 249 if (layoutBorderWidth(newer) > layoutBorderWidth(older)) 250 return false; 251 252 return true; // is compatible 253 } 254 255 /// _Assuming the same `PixelType`, can an allocation made with constraint `older` 256 /// be used with constraint `newer`? 257 bool layoutConstraintsValid(LayoutConstraints constraints) pure 258 { 259 bool forceVFlipped = (constraints & LAYOUT_VERT_FLIPPED) != 0; 260 bool forceNonVFlipped = (constraints & LAYOUT_VERT_STRAIGHT) != 0; 261 262 if (forceVFlipped && forceNonVFlipped) 263 return false; // Can't be flipped and non-flipped at the same time. 264 265 // LAYOUT_GAPLESS is incompatible with almost anything 266 if (layoutGapless(constraints)) 267 { 268 if (layoutMultiplicity(constraints) > 1) 269 return false; 270 if (layoutTrailingPixels(constraints) > 0) 271 return false; 272 if (layoutScanlineAlignment(constraints) > 1) 273 return false; 274 if (layoutBorderWidth(constraints) > 0) 275 return false; 276 } 277 278 return true; // Those constraints are not exclusive. 279 } 280 281 282 // As input: first scanline pointer and pitch in byte. 283 // As output: same, but following the constraints (flipped optionally). 284 // This is a way to flip image vertically. 285 void flipScanlinePointers(int width, 286 int height, 287 ref ubyte* dataPointer, 288 ref int bytePitch) pure @system 289 { 290 if (height >= 2) // Nothing to do for 0 or 1 row 291 { 292 ptrdiff_t offset_to_Nm1_row = cast(ptrdiff_t)(bytePitch) * (height - 1); 293 dataPointer += offset_to_Nm1_row; 294 } 295 bytePitch = -bytePitch; 296 } 297 298 // As input: first scanline pointer and pitch in byte. 299 // As output: same, but following the constraints (flipped optionally). 300 void applyVFlipConstraintsToScanlinePointers(int width, 301 int height, 302 ref ubyte* dataPointer, 303 ref int bytePitch, 304 LayoutConstraints constraints) pure @system 305 { 306 assert(layoutConstraintsValid(constraints)); 307 308 bool forceVFlipStorage = (constraints & LAYOUT_VERT_FLIPPED) != 0; 309 bool forceNonVFlipStorage = (constraints & LAYOUT_VERT_STRAIGHT) != 0; 310 311 // Should we flip the first scanline pointer and pitch? 312 bool shouldFlip = ( forceVFlipStorage && bytePitch > 0) || (forceNonVFlipStorage && bytePitch < 0); 313 314 if (shouldFlip) 315 { 316 flipScanlinePointers(width, height, dataPointer, bytePitch); 317 } 318 } 319 320 /// Allocate pixel data. Discard ancient data if any, and reallocate with `realloc`. 321 /// 322 /// Returns true in `err` in case of success. If the function is successful 323 /// then `deallocatePixelStorage` MUST be called later on. 324 /// 325 /// Params: 326 /// existingData The existing `mallocArea` from a former call to `allocatePixelStorage`. 327 /// type Pixel data type. 328 /// layers How many such of image are allocated. Must be 1 or greater. 329 /// width Image width. 330 /// height Image height. 331 /// constraints The layout constraints to follow for the scanlines and allocation. MUST be valid. 332 /// bonusBytes If non-zero, the area mallocArea[0..bonusBytes] can be used for user storage. 333 /// Only the caller can use as temp storage, since Image won't preserve knowledge of these 334 /// bonusBytes once the allocation is done. 335 /// clearWithZeroes Should fill the whole allocated area with zeroes (so that includes borders and gap bytes). 336 /// dataPointer The pointer to the first scanline. 337 /// mallocArea The pointer to the allocation beginning. Will be different from dataPointer and 338 /// must be kept somewhere. 339 /// pitchBytes Byte offset between two adjacent scanlines. Scanlines cannot ever overlap. 340 /// layerOffset Byte offset between two adjacent layers. Return 0 if only one layer. 341 /// err True if successful. Only err indicates success, not mallocArea. 342 /// 343 /// Note: even if you can request zero bytes, `realloc` can give you a non-null pointer, 344 /// that you would have to keep. This is a success case given by `err` only. 345 void allocatePixelStorage(ubyte* existingData, 346 PixelType type, 347 int layers, 348 int width, 349 int height, 350 LayoutConstraints constraints, 351 int bonusBytes, 352 bool clearWithZeroes, 353 out ubyte* dataPointer, // first scanline 354 out ubyte* mallocArea, // the result of realloc-ed 355 out int pitchBytes, 356 out int layerOffset, 357 out bool err) pure @trusted 358 { 359 assert(layers >= 0); // layers == 0 must be supported! 360 assert(width >= 0); // width == 0 and height == 0 must be supported! 361 assert(height >= 0); 362 assert(layoutConstraintsValid(constraints)); 363 364 // Width and height must be within limits. 365 if (!imageIsValidSize(layers, width, height)) 366 { 367 err = true; 368 return; 369 } 370 371 int border = layoutBorderWidth(constraints); 372 int rowAlignment = layoutScanlineAlignment(constraints); 373 int trailingPixels = layoutTrailingPixels(constraints); 374 int xMultiplicity = layoutMultiplicity(constraints); 375 bool gapless = layoutGapless(constraints); 376 377 assert(border >= 0); 378 assert(rowAlignment >= 1); 379 assert(xMultiplicity >= 1); 380 assert(trailingPixels >= 0); 381 382 static size_t nextMultipleOf(size_t base, size_t multiple) pure 383 { 384 assert(multiple > 0); 385 size_t n = (base + multiple - 1) / multiple; 386 return multiple * n; 387 } 388 389 static int computeRightPadding(int width, int border, int xMultiplicity) pure 390 { 391 int nextMultiple = cast(int)(nextMultipleOf(width + border, xMultiplicity)); 392 return nextMultiple - (width + border); 393 } 394 395 /// Returns: next pointer aligned with alignment bytes. 396 static ubyte* nextAlignedPointer(ubyte* start, size_t alignment) pure 397 { 398 return cast(ubyte*)nextMultipleOf(cast(size_t)(start), alignment); 399 } 400 401 // Compute size of right border, in pixels. 402 // How many "padding pixels" do we need to extend the right border with to respect `xMultiplicity`? 403 int rightPadding = computeRightPadding(width, border, xMultiplicity); 404 int borderRight = border + rightPadding; 405 if (borderRight < trailingPixels) 406 borderRight = trailingPixels; 407 408 int actualWidthInPixels = border + width + borderRight; 409 410 // Support layers: border exists for each layer. 411 long actualHeightOfOneLayer = cast(long)(border + height + border); 412 long actualHeightInPixels = actualHeightOfOneLayer * layers; 413 414 // Compute byte pitch and align it on `rowAlignment` 415 int pixelSize = pixelTypeSize(type); 416 417 // No overflow, actualWidthInPixels is at a maximum (1<<24)+16 418 // and pixel size is at a maxium 16. 419 int bytePitch = pixelSize * actualWidthInPixels; 420 bytePitch = cast(int) nextMultipleOf(bytePitch, rowAlignment); 421 422 assert(bytePitch >= 0); 423 424 // Could overflow to 64-bit here. 425 long sizeNeededBytes = cast(long)bytePitch * cast(long)actualHeightInPixels; 426 sizeNeededBytes += (rowAlignment - 1) + bonusBytes; 427 428 // A gamut image can't take more than `GAMUT_MAX_IMAGE_BYTES` bytes 429 // in its allocation, including bonus bytes and constraint bytes. 430 if (sizeNeededBytes > GAMUT_MAX_IMAGE_BYTES) 431 { 432 err = true; 433 return; 434 } 435 436 // And the allocation length must fit in a size_t variable. 437 if (sizeNeededBytes >= size_t.max) 438 { 439 err = true; // too large image in 32-bit, typically 440 return; 441 } 442 443 // How many bytes do we need for all samples? A bit more for aligning the first valid pixel. 444 size_t allocationSize = cast(size_t) sizeNeededBytes; 445 446 // PERF: on Windows, reusing previous allocation is much faster for same alloc size 447 // 314x faster vs free+malloc for same size 448 // 10x faster vs free+malloc for decreasing size 1 by 1 449 // 424x slower (quadratic) vs free+malloc for increasing size 1 by 1 450 // 0.5x slower vs free+malloc for random size 451 // If we store the allocation as slice, we could use the right way to realloc everytime. 452 enum ENABLE_REALLOC = true; // TODO 453 454 ubyte* allocation; 455 static if (ENABLE_REALLOC) 456 { 457 // We don't need to preserve former data, nor to align the allocation. 458 // Note: allocationSize can legally be zero. 459 460 // TODO: manages C23 allocationSize == 0 here 461 allocation = cast(ubyte*) fakePureRealloc(existingData, allocationSize); 462 } 463 else 464 { 465 free(existingData); 466 existingData = null; 467 allocation = cast(ubyte*) fakePureMalloc(allocationSize); 468 } 469 470 // realloc is allowed to return null if zero bytes required. 471 if (allocationSize != 0 && allocation is null) 472 { 473 err = true; 474 return; 475 } 476 477 // Optional by zero clearing of allocation area. 478 // PERF: realloc made a useless copy in that case. 479 if (clearWithZeroes && (allocationSize > 0)) 480 { 481 memset(allocation, 0, allocationSize); 482 } 483 484 // Compute pointer to pixel data itself. 485 size_t offsetToFirstMeaningfulPixel = bonusBytes + bytePitch * border + pixelSize * border; 486 ubyte* pixels = nextAlignedPointer(allocation + offsetToFirstMeaningfulPixel, rowAlignment); 487 488 // Apply vertical constraints: will the image be stored upside-down? 489 ubyte* firstScanlinePtr = pixels; 490 int finalPitchInBytes = bytePitch; 491 applyVFlipConstraintsToScanlinePointers(width, height, firstScanlinePtr, finalPitchInBytes, constraints); 492 493 dataPointer = firstScanlinePtr; 494 pitchBytes = finalPitchInBytes; 495 if (layers == 0 || layers == 1) 496 { 497 layerOffset = 0; 498 } 499 else 500 { 501 long offsetBetweenLayers = bytePitch * actualHeightOfOneLayer; 502 if (offsetBetweenLayers > layerOffset.max) 503 { 504 // TODO: what should be the maximum possible layerOffset? Should 505 // probably be size_t? 506 err = true; 507 return; 508 } 509 layerOffset = cast(int)offsetBetweenLayers; 510 } 511 mallocArea = allocation; 512 err = false; 513 514 // Check validity of result 515 { 516 // check gapless 517 int scanWidth = pixelSize * width; 518 if (gapless) 519 assert(scanWidth == (bytePitch < 0 ? -bytePitch : bytePitch)); 520 521 // check row alignment 522 static bool isPointerAligned(void* p, size_t alignment) pure 523 { 524 assert(alignment != 0); 525 return ( cast(size_t)p & (alignment - 1) ) == 0; 526 } 527 assert(isPointerAligned(dataPointer, rowAlignment)); 528 assert(isPointerAligned(dataPointer + pitchBytes, rowAlignment)); 529 } 530 } 531 532 // Deallocate pixel data. Everything allocated with `allocatePixelStorage` eventually needs 533 // to be through that function. 534 void deallocatePixelStorage(void* mallocArea) pure @system 535 { 536 fakePureFree(mallocArea); 537 } 538 539 // Deallocate encoded image. 540 void deallocateEncodedImage(ubyte[] encodedImage) pure @system 541 { 542 fakePureFree(encodedImage.ptr); 543 } 544 545 // Pretend to be pure. 546 extern (C) private pure @system @nogc nothrow 547 { 548 pragma(mangle, "malloc") void* fakePureMalloc(size_t); 549 pragma(mangle, "realloc") void* fakePureRealloc(void* ptr, size_t size); 550 pragma(mangle, "free") void fakePureFree(void* ptr); 551 } 552 553 bool validLoadFlags(LoadFlags loadFlags) pure 554 { 555 if ((loadFlags & LOAD_GREYSCALE) && (loadFlags & LOAD_RGB)) return false; 556 if ((loadFlags & LOAD_ALPHA) && (loadFlags & LOAD_NO_ALPHA)) return false; 557 558 int bitnessFlags = 0; 559 if (loadFlags & LOAD_8BIT) ++bitnessFlags; 560 if (loadFlags & LOAD_16BIT) ++bitnessFlags; 561 if (loadFlags & LOAD_FP32) ++bitnessFlags; 562 if (bitnessFlags > 1) 563 return false; 564 565 return true; 566 } 567 568 569 // Load flags to components asked, intended for an image decoder. 570 // This is for STB-style loading who can convert scanline as they are decoded. 571 // 572 // Return: 573 // -1 => keep input number of components 574 // 0 => error 575 // 1/2/3/4 => forced number of components. 576 int computeRequestedImageComponents(LoadFlags loadFlags) pure nothrow @nogc @safe 577 { 578 int requestedComp = -1; // keep original 579 580 if (!validLoadFlags(loadFlags)) 581 return 0; 582 583 if (loadFlags & LOAD_GREYSCALE) 584 { 585 if (loadFlags & LOAD_ALPHA) 586 requestedComp = 2; 587 else if (loadFlags & LOAD_NO_ALPHA) 588 requestedComp = 1; 589 } 590 else if (loadFlags & LOAD_RGB) 591 { 592 if (loadFlags & LOAD_ALPHA) 593 requestedComp = 4; 594 else if (loadFlags & LOAD_NO_ALPHA) 595 requestedComp = 3; 596 } 597 return requestedComp; 598 } 599 unittest 600 { 601 assert(computeRequestedImageComponents(LOAD_GREYSCALE) == -1); // keep same, because it is alpha-preserving. 602 assert(computeRequestedImageComponents(LOAD_GREYSCALE | LOAD_NO_ALPHA) == 1); 603 assert(computeRequestedImageComponents(LOAD_GREYSCALE | LOAD_ALPHA) == 2); 604 assert(computeRequestedImageComponents(LOAD_GREYSCALE | LOAD_ALPHA | LOAD_NO_ALPHA) == 0); // invalid 605 assert(computeRequestedImageComponents(LOAD_RGB) == -1); 606 assert(computeRequestedImageComponents(LOAD_RGB | LOAD_NO_ALPHA) == 3); 607 assert(computeRequestedImageComponents(LOAD_RGB | LOAD_GREYSCALE) == 0); // invalid 608 assert(computeRequestedImageComponents(LOAD_RGB | LOAD_ALPHA) == 4); 609 } 610 611 612 // Type conversion. 613 614 /// From a type and LoadFlags, get the target type using the loading flags. 615 /// This is when the decoder doesn't support inside conversion and we need to use convertTo. 616 PixelType applyLoadFlags(PixelType type, LoadFlags flags) 617 { 618 // Check incompatible load flags. 619 if (!validLoadFlags(flags)) 620 return PixelType.unknown; 621 622 if (flags & LOAD_GREYSCALE) 623 type = convertPixelTypeToGreyscale(type); 624 625 if (flags & LOAD_RGB) 626 type = convertPixelTypeToRGB(type); 627 628 if (flags & LOAD_ALPHA) 629 type = convertPixelTypeToAddAlphaChannel(type); 630 631 if (flags & LOAD_NO_ALPHA) 632 type = convertPixelTypeToDropAlphaChannel(type); 633 634 if (flags & LOAD_8BIT) 635 type = convertPixelTypeTo8Bit(type); 636 637 if (flags & LOAD_16BIT) 638 type = convertPixelTypeTo16Bit(type); 639 640 if (flags & LOAD_FP32) 641 type = convertPixelTypeToFP32(type); 642 643 return type; 644 } 645