The OpenD Programming Language

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;