1 // Written in the D programming language. 2 3 /** 4 This module implements the packed RGB _color type. 5 6 It is common in computer graphics to perform compression of image data to save 7 runtime memory. There is a very wide variety of common compressed image formats. 8 This module aims to support all of them! 9 10 Authors: Manu Evans 11 Copyright: Copyright (c) 2015, Manu Evans. 12 License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0) 13 Source: $(PHOBOSSRC std/experimental/color/_packedrgb.d) 14 */ 15 module std.experimental.color.packedrgb; 16 17 import std.experimental.color; 18 import std.experimental.color.rgb; 19 import std.experimental.color.colorspace : RGBColorSpace; 20 import std.experimental.normint; 21 22 import std.traits : isNumeric, isFloatingPoint, isSigned, isUnsigned, Unsigned; 23 24 @safe pure nothrow: 25 26 27 /** 28 Detect whether $(D_INLINECODE T) is a packed RGB color. 29 */ 30 enum isPackedRGB(T) = isInstanceOf!(PackedRGB, T); 31 32 /// 33 unittest 34 { 35 static assert(isPackedRGB!(PackedRGB!("rgb_5_6_5", ubyte)) == true); 36 static assert(isPackedRGB!(PackedRGB!("rgba_s10_s10_s10_u2", short)) == true); 37 static assert(isPackedRGB!(PackedRGB!("rg_f16_f16", float)) == true); 38 static assert(isPackedRGB!(PackedRGB!("rgb_f11_f11_f10", float)) == true); 39 static assert(isPackedRGB!(PackedRGB!("rgb_9_9_9_e5", float)) == true); 40 static assert(isPackedRGB!(PackedRGB!("rgb_f10_s4_u2", float)) == true); 41 static assert(isPackedRGB!int == false); 42 } 43 44 45 /** Component info struct. */ 46 struct ComponentInfo 47 { 48 /** Type of the component. */ 49 enum ComponentType : ubyte 50 { 51 /** Component is unsigned normalized integer. */ 52 Unsigned, 53 /** Component is signed normalized integer. */ 54 Signed, 55 /** Component is floating point. Floats with less than 16 bits precision are unsigned. */ 56 Float, 57 /** Component is floating point mantissa only. */ 58 Mantissa, 59 /** Component is floating point exponent only. */ 60 Exponent, 61 } 62 63 /** First bit, starting from bit 0 (LSB). */ 64 ubyte offset; 65 /** Number of bits. */ 66 ubyte bits; 67 /** Component type. */ 68 ComponentType type; 69 } 70 71 /** Buffer used for bit-packing. */ 72 struct Buffer(size_t N) 73 { 74 @safe pure nothrow @nogc: 75 76 private 77 { 78 static if (N >= 8 && (N & 7) == 0) 79 ulong[N/8] data; 80 else static if (N >= 4 && (N & 3) == 0) 81 uint[N/4] data; 82 else static if (N >= 2 && (N & 1) == 0) 83 ushort[N/2] data; 84 else 85 ubyte[N] data; 86 } 87 88 /** Read bits from the buffer. */ 89 @property uint bits(size_t Offset, size_t Bits)() const 90 { 91 enum Index = Offset / ElementWidth; 92 enum ElementOffset = Offset % ElementWidth; 93 static assert(Offset+Bits <= data.sizeof*8, "Bits are outside of data range"); 94 static assert(Index == (Offset+Bits-1) / ElementWidth, "Bits may not straddle element boundaries"); 95 return (data[Index] >> ElementOffset) & ((1UL << Bits)-1); 96 } 97 98 /** Write bits to the buffer. */ 99 @property void bits(size_t Offset, size_t Bits)(uint value) 100 { 101 enum Index = Offset / ElementWidth; 102 enum ElementOffset = Offset % ElementWidth; 103 static assert(Offset+Bits <= data.sizeof*8, "Bits are outside of data range"); 104 static assert(Index == (Offset+Bits-1) / ElementWidth, "Bits may not straddle element boundaries"); 105 data[Index] |= (value & ((1UL << Bits)-1)) << ElementOffset; 106 } 107 108 /** Element width for multi-element buffers. */ 109 enum ElementWidth = data[0].sizeof*8; 110 } 111 112 /** 113 A packed RGB color, parameterised with format, unpacked component type, and color space specification. 114 115 Params: format_ = Format of the packed color.$(BR) 116 Format shall be arranged for instance: "rgba_10_10_10_2" for 10 bits each RGB, and 2 bits alpha, starting from the least significant bit.$(BR) 117 Formats may specify packed floats: "rgba_f16_f16_f16_f16" for an RGBA half-precision float color.$(BR) 118 Low-precision floats are supported: "rgb_f11_f11_f10" for an RGB partial-precision floating point format. Floats with less than 16 bits always have a 5 bit exponent, and no sign bit.$(BR) 119 Formats may specify a shared exponent: "rgb_9_9_9_e5" for 9 mantissa bits each RGB, and a 5 bit shared exponent.$(BR) 120 Formats may specify the signed-ness for integer components: "rgb_s5_s5_s5_u1" for 5 bit signed RGB, and a 1 bit unsigned alpha. The 'u' is optional, default is assumed to be unsigned.$(BR) 121 Formats may contain a combination of the color channels r, g, b, l, a, x, in any order. Color channels l, and r, g, b are mutually exclusive, and may not appear together in the same color. 122 ComponentType_ = Type for the unpacked color channels. May be a basic integer or floating point type. 123 colorSpace_ = Color will be within the specified color space. 124 */ 125 struct PackedRGB(string format_, ComponentType_, RGBColorSpace colorSpace_ = RGBColorSpace.sRGB) 126 if (isNumeric!ComponentType_) 127 { 128 @safe pure nothrow @nogc: 129 130 // RGB colors may only contain components 'rgb', or 'l' (luminance) 131 // They may also optionally contain an 'a' (alpha) component, and 'x' (unused) components 132 static assert(allIn!("rgblax", components), "Invalid Color component '"d ~ notIn!("rgblax", components) ~ "'. RGB colors may only contain components: r, g, b, l, a, x"d); 133 static assert(anyIn!("rgbal", components), "RGB colors must contain at least one component of r, g, b, l, a."); 134 static assert(!canFind!(components, 'l') || !anyIn!("rgb", components), "RGB colors may not contain rgb AND luminance components together."); 135 136 /** The unpacked color type. */ 137 alias UnpackedColor = RGB!(components, ComponentType_, false, colorSpace_); 138 139 /** The packed color format. */ 140 enum format = format_; 141 142 /** The color components that were specified. */ 143 enum string components = GetComponents!format_; 144 145 /** Bit assignments for each component. */ 146 enum ComponentInfo[components.length] componentInfo = GetComponentInfos!AllInfos; 147 /** Shared exponent bits. */ 148 enum ComponentInfo sharedExponent = GetSharedExponent!AllInfos; 149 /** If the format has a shared exponent. */ 150 enum bool hasSharedExponent = sharedExponent.bits > 0; 151 152 /** The colors color space. */ 153 enum RGBColorSpace colorSpace = colorSpace_; 154 /** The color space descriptor. */ 155 enum RGBColorSpaceDesc!F colorSpaceDesc(F = double) = rgbColorSpaceDef!F(colorSpace_); 156 157 /** Number of bits per element. */ 158 enum BitsPerElement = numBits(AllInfos); 159 /** The raw packed data. */ 160 Buffer!(BitsPerElement/8) data; 161 162 /** Test if a particular component is present. */ 163 enum bool hasComponent(char c) = canFind!(components, c); 164 /** If the color has alpha. */ 165 enum bool hasAlpha = hasComponent!'a'; 166 167 /** The unpacked color. */ 168 @property ParentColor unpacked() 169 { 170 return convertColorImpl!(ParentColor)(this); 171 } 172 173 /** Construct a color from RGB and optional alpha values. */ 174 this(UnpackedColor color) 175 { 176 this = cast(typeof(this))color; 177 } 178 179 /** 180 Cast to other color types. 181 182 This cast is a convenience which simply forwards the call to convertColor. 183 */ 184 Color opCast(Color)() const if (isColor!Color) 185 { 186 return convertColor!Color(this); 187 } 188 189 // comparison 190 bool opEquals(typeof(this) rh) const 191 { 192 // TODO: mask out 'x' component 193 return data.data[] == rh.data.data[]; 194 } 195 196 197 package: 198 199 alias ParentColor = UnpackedColor; 200 201 static To convertColorImpl(To, From)(From color) if (isPackedRGB!From && isPackedRGB!To) 202 { 203 static if (From.colorSpace == To.colorSpace) 204 { 205 auto t = convertColorImpl!(From.ParentColor)(color); 206 return convertColorImpl!To(t); 207 } 208 else 209 { 210 auto t = convertColorImpl!(From.ParentColor)(color); 211 return convertColorImpl!To(cast(To.ParentColor)t); 212 } 213 } 214 215 static To convertColorImpl(To, From)(From color) @trusted if (isPackedRGB!From && isRGB!To) 216 { 217 // target component type might be NormalizedInt 218 static if (!isNumeric!(To.ComponentType)) 219 alias ToType = To.ComponentType.IntType; 220 else 221 alias ToType = To.ComponentType; 222 223 // if the color has a shared exponent 224 static if (From.hasSharedExponent) 225 int exp = cast(int)color.data.bits!(cast(size_t)From.sharedExponent.offset, cast(size_t)From.sharedExponent.bits) - ExpBias!(cast(size_t)From.sharedExponent.bits); 226 227 To r; 228 foreach (i; Iota!(0, From.componentInfo.length)) 229 { 230 // 'x' components are padding, no need to do work for them! 231 static if (To.components[i] != 'x') 232 { 233 enum info = From.componentInfo[i]; 234 enum size_t NumBits = info.bits; 235 236 uint bits = color.data.bits!(cast(size_t)info.offset, NumBits); 237 238 static if (info.type == ComponentInfo.ComponentType.Unsigned || 239 info.type == ComponentInfo.ComponentType.Signed) 240 { 241 enum Signed = info.type == ComponentInfo.ComponentType.Signed; 242 static if (isFloatingPoint!ToType) 243 ToType c = normBitsToFloat!(NumBits, Signed, ToType)(bits); 244 else 245 ToType c = cast(ToType)convertNormBits!(NumBits, Signed, ToType.sizeof*8, isSigned!ToType, Unsigned!ToType)(bits); 246 } 247 else static if (info.type == ComponentInfo.ComponentType.Float) 248 { 249 static assert(NumBits >= 6, "Needs at least 6 bits for a float!"); 250 251 // TODO: investigate a better way to select signed-ness in the format spec, maybe 'sf10', or 's10e5'? 252 enum bool Signed = NumBits >= 16; 253 254 // TODO: investigate a way to specify exponent bits in the format spec, maybe 'f10e3'? 255 enum Exponent = 5; 256 enum ExpBias = ExpBias!Exponent; 257 enum Mantissa = NumBits - Exponent - (Signed ? 1 : 0); 258 259 uint exponent = ((bits >> Mantissa) & BitsUMax!Exponent) - ExpBias + 127; 260 uint mantissa = (bits & BitsUMax!Mantissa) << (23 - Mantissa); 261 262 uint u = (Signed && (bits & SignBit!NumBits) ? SignBit!32 : 0) | (exponent << 23) | mantissa; 263 static if (isFloatingPoint!ToType) 264 ToType c = *cast(float*)&u; 265 else 266 ToType c = floatToNormInt!ToType(*cast(float*)&u); 267 } 268 else static if (info.type == ComponentInfo.ComponentType.Mantissa) 269 { 270 uint scale = (0x7F + (exp - info.bits)) << 23; 271 static if (isFloatingPoint!ToType) 272 ToType c = bits * *cast(float*)&scale; 273 else 274 ToType c = floatToNormInt!ToType(bits * *cast(float*)&scale); 275 } 276 mixin("r." ~ components[i] ~ " = To.ComponentType(c);"); 277 } 278 } 279 return r; 280 } 281 282 static To convertColorImpl(To, From)(From color) @trusted if (isRGB!From && isPackedRGB!To) 283 { 284 // target component type might be NormalizedInt 285 static if (!isNumeric!(From.ComponentType)) 286 alias FromType = From.ComponentType.IntType; 287 else 288 alias FromType = From.ComponentType; 289 290 To res; 291 292 // if the color has a shared exponent 293 static if (To.hasSharedExponent) 294 { 295 import std.algorithm : min, max, clamp; 296 297 // prepare exponent... 298 template SmallestMantissa(ComponentInfo[] Components) 299 { 300 template Impl(size_t i) 301 { 302 static if (i == Components.length) 303 alias Impl = TypeTuple!(); 304 else 305 alias Impl = TypeTuple!(Components[i].bits, Impl!(i+1)); 306 } 307 enum SmallestMantissa = min(Impl!0); 308 } 309 enum MantBits = SmallestMantissa!(To.componentInfo); 310 enum ExpBits = To.sharedExponent.bits; 311 enum ExpBias = ExpBias!ExpBits; 312 enum MaxExp = BitsUMax!ExpBits; 313 314 // the maximum representable value is the one represented by the smallest mantissa 315 enum MaxVal = cast(float)(BitsUMax!MantBits * (1<<(MaxExp-ExpBias))) / (1<<MantBits); 316 317 float maxc = 0; 318 foreach (i; Iota!(0, To.componentInfo.length)) 319 { 320 static if (To.components[i] != 'x') 321 mixin("maxc = max(maxc, cast(float)color." ~ To.components[i] ~ ");"); 322 } 323 maxc = clamp(maxc, 0, MaxVal); 324 325 import std.stdio; 326 327 int maxc_exp = ((*cast(uint*)&maxc >> 23) & 0xFF) - 127; 328 int sexp = max(-ExpBias - 1, maxc_exp) + 1 + ExpBias; 329 assert(sexp >= 0 && sexp <= MaxExp); 330 331 res.data.bits!(cast(size_t)To.sharedExponent.offset, cast(size_t)To.sharedExponent.bits) = cast(uint)sexp; 332 } 333 334 foreach (i; Iota!(0, To.componentInfo.length)) 335 { 336 // 'x' components are padding, no need to do work for them! 337 static if (To.components[i] != 'x') 338 { 339 enum info = To.componentInfo[i]; 340 enum size_t NumBits = info.bits; 341 342 static if (info.type == ComponentInfo.ComponentType.Unsigned || 343 info.type == ComponentInfo.ComponentType.Signed) 344 { 345 static if (isFloatingPoint!FromType) 346 mixin("FromType c = color." ~ components[i] ~ ";"); 347 else 348 mixin("FromType c = color." ~ components[i] ~ ".value;"); 349 350 enum Signed = info.type == ComponentInfo.ComponentType.Signed; 351 static if (isFloatingPoint!FromType) 352 uint bits = floatToNormBits!(NumBits, Signed)(c); 353 else 354 uint bits = convertNormBits!(FromType.sizeof*8, isSigned!FromType, NumBits, Signed)(cast(Unsigned!FromType)c); 355 } 356 else static if (info.type == ComponentInfo.ComponentType.Float) 357 { 358 static assert(NumBits >= 6, "Needs at least 6 bits for a float!"); 359 360 // TODO: investigate a better way to select signed-ness in the format spec, maybe 'sf10', or 's10e5'? 361 enum bool Signed = NumBits >= 16; 362 363 // TODO: investigate a way to specify exponent bits in the format spec, maybe 'f10e3'? 364 enum Exponent = 5; 365 enum ExpBias = ExpBias!Exponent; 366 enum Mantissa = NumBits - Exponent - (Signed ? 1 : 0); 367 368 mixin("float f = cast(float)color." ~ components[i] ~ ";"); 369 uint u = *cast(uint*)&f; 370 371 int exponent = ((u >> 23) & 0xFF) - 127 + ExpBias; 372 uint mantissa = (u >> (23 - Mantissa)) & BitsUMax!Mantissa; 373 if (exponent < 0) 374 { 375 exponent = 0; 376 mantissa = 0; 377 } 378 uint bits = (Signed && (u & SignBit!32) ? SignBit!NumBits : 0) | (exponent << Mantissa) | mantissa; 379 } 380 else static if (info.type == ComponentInfo.ComponentType.Mantissa) 381 { 382 // TODO: we could easily support signed values here... 383 384 uint denom_u = cast(uint)(127 + sexp - ExpBias - NumBits) << 23; 385 float denom = *cast(float*)&denom_u; 386 387 mixin("float c = clamp(cast(float)color." ~ To.components[i] ~ ", 0.0f, MaxVal);"); 388 uint bits = cast(uint)cast(int)(c / denom + 0.5f); 389 assert(bits <= BitsUMax!NumBits); 390 } 391 392 res.data.bits!(cast(size_t)info.offset, NumBits) = bits; 393 } 394 } 395 return res; 396 } 397 398 private: 399 enum AllInfos = ParseFormat!format_; 400 } 401 402 403 private: 404 405 // lots of logic to parse the format string 406 template GetComponents(string format) 407 { 408 string get(string s) 409 { 410 foreach (i; 0..s.length) 411 { 412 if (s[i] == '_') 413 return s[0..i]; 414 } 415 assert(false); 416 } 417 enum string GetComponents = get(format); 418 } 419 420 template GetComponentInfos(ComponentInfo[] infos) 421 { 422 template Impl(ComponentInfo[] infos, size_t i) 423 { 424 static if (i == infos.length) 425 enum Impl = infos; 426 else static if (infos[i].type == ComponentInfo.ComponentType.Exponent) 427 enum Impl = infos[0..i] ~ infos[i+1..$]; 428 else 429 enum Impl = Impl!(infos, i+1); 430 } 431 enum GetComponentInfos = Impl!(infos, 0); 432 } 433 434 template GetSharedExponent(ComponentInfo[] infos) 435 { 436 template Impl(ComponentInfo[] infos, size_t i) 437 { 438 static if (i == infos.length) 439 enum Impl = ComponentInfo(0, 0, ComponentInfo.ComponentType.Unsigned); 440 else static if (infos[i].type == ComponentInfo.ComponentType.Exponent) 441 enum Impl = infos[i]; 442 else 443 enum Impl = Impl!(infos, i+1); 444 } 445 enum GetSharedExponent = Impl!(infos, 0); 446 } 447 448 template ParseFormat(string format) 449 { 450 // parse the format string into component infos 451 ComponentInfo[] impl(string s) pure nothrow @safe 452 { 453 static int parseInt(ref string str) pure nothrow @nogc @safe 454 { 455 int n = 0; 456 while (str.length && str[0] >= '0' && str[0] <= '9') 457 { 458 n = n*10 + (str[0] - '0'); 459 str = str[1..$]; 460 } 461 return n; 462 } 463 464 while (s.length && s[0] != '_') 465 s = s[1..$]; 466 467 ComponentInfo[] infos; 468 469 int offset = 0; 470 bool hasSharedExp = false; 471 while (s.length && s[0] == '_') 472 { 473 s = s[1..$]; 474 assert(s.length); 475 476 char c = 0; 477 if (!(s[0] >= '0' && s[0] <= '9')) 478 { 479 c = s[0]; 480 s = s[1..$]; 481 } 482 483 int i = parseInt(s); 484 assert(i > 0); 485 486 infos ~= ComponentInfo(cast(ubyte)offset, cast(ubyte)i, ComponentInfo.ComponentType.Unsigned); 487 488 if (c) 489 { 490 if (c == 'e' && !hasSharedExp) 491 { 492 infos[$-1].type = ComponentInfo.ComponentType.Exponent; 493 hasSharedExp = true; 494 } 495 else if (c == 'f') 496 infos[$-1].type = ComponentInfo.ComponentType.Float; 497 else if (c == 's') 498 infos[$-1].type = ComponentInfo.ComponentType.Signed; 499 else if (c == 'u') 500 infos[$-1].type = ComponentInfo.ComponentType.Unsigned; 501 else 502 assert(false); 503 } 504 505 offset += i; 506 } 507 assert(s.length == 0); 508 509 if (hasSharedExp) 510 { 511 foreach (ref c; infos) 512 { 513 assert(c.type != ComponentInfo.ComponentType.Float && c.type != ComponentInfo.ComponentType.Signed); 514 if (c.type != ComponentInfo.ComponentType.Exponent) 515 c.type = ComponentInfo.ComponentType.Mantissa; 516 } 517 } 518 519 return infos; 520 } 521 enum ParseFormat = impl(format); 522 } 523 524 template Iota(alias start, alias end) 525 { 526 static if (end == start) 527 alias Iota = TypeTuple!(); 528 else 529 alias Iota = TypeTuple!(Iota!(start, end-1), end-1); 530 } 531 532 int numBits(ComponentInfo[] infos) pure nothrow @nogc @safe 533 { 534 int bits; 535 foreach (i; infos) 536 bits += i.bits; 537 int slop = bits % 8; 538 if (slop) 539 bits += 8 - slop; 540 return bits; 541 } 542 543 enum ExpBias(size_t n) = (1 << (n-1)) - 1;