The OpenD Programming Language

1 //ketmar: Adam didn't wrote this, don't blame him!
2 module arsd.targa;
3 
4 import arsd.color;
5 import std.stdio : File; // sorry
6 
7 static if (__traits(compiles, { import iv.vfs; })) enum ArsdTargaHasIVVFS = true; else enum ArsdTargaHasIVVFS = false;
8 static if (ArsdTargaHasIVVFS) import iv.vfs;
9 
10 
11 // ////////////////////////////////////////////////////////////////////////// //
12 public MemoryImage loadTgaMem (const(void)[] buf, const(char)[] filename=null) {
13   static struct MemRO {
14     const(ubyte)[] data;
15     long pos;
16 
17     this (const(void)[] abuf) { data = cast(const(ubyte)[])abuf; }
18 
19     @property long tell () { return pos; }
20     @property long size () { return data.length; }
21 
22     void seek (long offset, int whence=Seek.Set) {
23       switch (whence) {
24         case Seek.Set:
25           if (offset < 0 || offset > data.length) throw new Exception("invalid offset");
26           pos = offset;
27           break;
28         case Seek.Cur:
29           if (offset < -pos || offset > data.length-pos) throw new Exception("invalid offset");
30           pos += offset;
31           break;
32         case Seek.End:
33           pos = data.length+offset;
34           if (pos < 0 || pos > data.length) throw new Exception("invalid offset");
35           break;
36         default:
37           throw new Exception("invalid offset origin");
38       }
39     }
40 
41     ptrdiff_t read (void* buf, size_t count) @system {
42       if (pos >= data.length) return 0;
43       if (count > 0) {
44         import core.stdc.string : memcpy;
45         long rlen = data.length-pos;
46         if (rlen >= count) rlen = count;
47         assert(rlen != 0);
48         memcpy(buf, data.ptr+pos, cast(size_t)rlen);
49         pos += rlen;
50         return cast(ptrdiff_t)rlen;
51       } else {
52         return 0;
53       }
54     }
55   }
56 
57   auto rd = MemRO(buf);
58   return loadTga(rd, filename);
59 }
60 
61 static if (ArsdTargaHasIVVFS) public MemoryImage loadTga (VFile fl) { return loadTgaImpl(fl, fl.name); }
62 public MemoryImage loadTga (File fl) { return loadTgaImpl(fl, fl.name); }
63 public MemoryImage loadTga(T:const(char)[]) (T fname) {
64   static if (is(T == typeof(null))) {
65     throw new Exception("cannot load nameless tga");
66   } else {
67     static if (ArsdTargaHasIVVFS) {
68       return loadTga(VFile(fname));
69     } else static if (is(T == string)) {
70       return loadTga(File(fname), fname);
71     } else {
72       return loadTga(File(fname.idup), fname);
73     }
74   }
75 }
76 
77 // pass filename to ease detection
78 // hack around "has scoped destruction, cannot build closure"
79 public MemoryImage loadTga(ST) (auto ref ST fl, const(char)[] filename=null) if (isReadableStream!ST && isSeekableStream!ST) { return loadTgaImpl(fl, filename); }
80 
81 private MemoryImage loadTgaImpl(ST) (auto ref ST fl, const(char)[] filename) {
82   enum TGAFILESIGNATURE = "TRUEVISION-XFILE.\x00";
83 
84   static immutable ubyte[32] cmap16 = [0,8,16,25,33,41,49,58,66,74,82,90,99,107,115,123,132,140,148,156,165,173,181,189,197,206,214,222,230,239,247,255];
85 
86   static struct Header {
87     ubyte idsize;
88     ubyte cmapType;
89     ubyte imgType;
90     ushort cmapFirstIdx;
91     ushort cmapSize;
92     ubyte cmapElementSize;
93     ushort originX;
94     ushort originY;
95     ushort width;
96     ushort height;
97     ubyte bpp;
98     ubyte imgdsc;
99 
100     @property bool zeroBits () const pure nothrow @safe @nogc { return ((imgdsc&0xc0) == 0); }
101     @property bool xflip () const pure nothrow @safe @nogc { return ((imgdsc&0b010000) != 0); }
102     @property bool yflip () const pure nothrow @safe @nogc { return ((imgdsc&0b100000) == 0); }
103   }
104 
105   static struct ExtFooter {
106     uint extofs;
107     uint devdirofs;
108     char[18] sign=0;
109   }
110 
111   static struct Extension {
112     ushort size;
113     char[41] author=0;
114     char[324] comments=0;
115     ushort month, day, year;
116     ushort hour, minute, second;
117     char[41] jid=0;
118     ushort jhours, jmins, jsecs;
119     char[41] producer=0;
120     ushort prodVer;
121     ubyte prodSubVer;
122     ubyte keyR, keyG, keyB, keyZero;
123     ushort pixratioN, pixratioD;
124     ushort gammaN, gammaD;
125     uint ccofs;
126     uint wtfofs;
127     uint scanlineofs;
128     ubyte attrType;
129   }
130 
131   ExtFooter extfooter;
132   uint rleBC, rleDC;
133   ubyte[4] rleLast;
134   Color[256] cmap;
135 
136   void readPixel(bool asRLE, uint bytesPerPixel) (ubyte[] pixel, scope ubyte delegate () readByte) {
137     static if (asRLE) {
138       if (rleDC > 0) {
139         // still counting
140         static if (bytesPerPixel == 1) pixel.ptr[0] = rleLast.ptr[0];
141         else pixel.ptr[0..bytesPerPixel] = rleLast.ptr[0..bytesPerPixel];
142         --rleDC;
143         return;
144       }
145       if (rleBC > 0) {
146         --rleBC;
147       } else {
148         ubyte b = readByte();
149         if (b&0x80) rleDC = (b&0x7f); else rleBC = (b&0x7f);
150       }
151       foreach (immutable idx; 0..bytesPerPixel) rleLast.ptr[idx] = pixel.ptr[idx] = readByte();
152     } else {
153       foreach (immutable idx; 0..bytesPerPixel) pixel.ptr[idx] = readByte();
154     }
155   }
156 
157   // 8 bit color-mapped row
158   Color readColorCM8(bool asRLE) (scope ubyte delegate () readByte) {
159     ubyte[1] pixel = void;
160     readPixel!(asRLE, 1)(pixel[], readByte);
161     auto cmp = cast(const(ubyte)*)(cmap.ptr+pixel.ptr[0]);
162     return Color(cmp[0], cmp[1], cmp[2]);
163   }
164 
165   // 8 bit greyscale
166   Color readColorBM8(bool asRLE) (scope ubyte delegate () readByte) {
167     ubyte[1] pixel = void;
168     readPixel!(asRLE, 1)(pixel[], readByte);
169     return Color(pixel.ptr[0], pixel.ptr[0], pixel.ptr[0]);
170   }
171 
172   // 16 bit greyscale
173   Color readColorBM16(bool asRLE) (scope ubyte delegate () readByte) {
174     ubyte[2] pixel = void;
175     readPixel!(asRLE, 2)(pixel[], readByte);
176     immutable ubyte v = cast(ubyte)((pixel.ptr[0]|(pixel.ptr[1]<<8))>>8);
177     return Color(v, v, v);
178   }
179 
180   // 16 bit
181   Color readColor16(bool asRLE) (scope ubyte delegate () readByte) {
182     ubyte[2] pixel = void;
183     readPixel!(asRLE, 2)(pixel[], readByte);
184     immutable v = pixel.ptr[0]+(pixel.ptr[1]<<8);
185     return Color(cmap16.ptr[(v>>10)&0x1f], cmap16.ptr[(v>>5)&0x1f], cmap16.ptr[v&0x1f]);
186   }
187 
188   // 24 bit or 32 bit
189   Color readColorTrue(bool asRLE, uint bytesPerPixel) (scope ubyte delegate () readByte) {
190     ubyte[bytesPerPixel] pixel = void;
191     readPixel!(asRLE, bytesPerPixel)(pixel[], readByte);
192     static if (bytesPerPixel == 4) {
193       return Color(pixel.ptr[2], pixel.ptr[1], pixel.ptr[0], pixel.ptr[3]);
194     } else {
195       return Color(pixel.ptr[2], pixel.ptr[1], pixel.ptr[0]);
196     }
197   }
198 
199   bool isGoodExtension (const(char)[] filename) {
200     if (filename.length >= 4) {
201       // try extension
202       auto ext = filename[$-4..$];
203       if (ext[0] == '.' && (ext[1] == 'T' || ext[1] == 't') && (ext[2] == 'G' || ext[2] == 'g') && (ext[3] == 'A' || ext[3] == 'a')) return true;
204     }
205     // try signature
206     return false;
207   }
208 
209   bool detect(ST) (auto ref ST fl, const(char)[] filename) if (isReadableStream!ST && isSeekableStream!ST) {
210     bool goodext = false;
211     if (fl.size < 45) return false; // minimal 1x1 tga
212     if (filename.length) { goodext = isGoodExtension(filename); if (!goodext) return false; }
213     // try footer
214     fl.seek(-(4*2+18), Seek.End);
215     extfooter.extofs = fl.readNum!uint;
216     extfooter.devdirofs = fl.readNum!uint;
217     fl.rawReadExact(extfooter.sign[]);
218     if (extfooter.sign != TGAFILESIGNATURE) {
219       //if (!goodext) return false;
220       extfooter = extfooter.init;
221       return true; // alas, footer is optional
222     }
223     return true;
224   }
225 
226   if (!detect(fl, filename)) throw new Exception("not a TGA");
227   fl.seek(0);
228   Header hdr;
229   fl.readStruct(hdr);
230   // parse header
231   // arbitrary size limits
232   if (hdr.width  == 0 || hdr.width > 32000) throw new Exception("invalid tga width");
233   if (hdr.height == 0 || hdr.height > 32000) throw new Exception("invalid tga height");
234   switch (hdr.bpp) {
235     case 1: case 2: case 4: case 8: case 15: case 16: case 24: case 32: break;
236     default: throw new Exception("invalid tga bpp");
237   }
238   uint bytesPerPixel = ((hdr.bpp)>>3);
239   if (bytesPerPixel == 0 || bytesPerPixel > 4) throw new Exception("invalid tga pixel size");
240   bool loadCM = false;
241   // get the row reading function
242   ubyte readByte () { ubyte b; fl.rawReadExact((&b)[0..1]); return b; }
243   scope Color delegate (scope ubyte delegate () readByte) readColor;
244   switch (hdr.imgType) {
245     case 2: // true color, no rle
246       switch (bytesPerPixel) {
247         case 2: readColor = &readColor16!false; break;
248         case 3: readColor = &readColorTrue!(false, 3); break;
249         case 4: readColor = &readColorTrue!(false, 4); break;
250         default: throw new Exception("invalid tga pixel size");
251       }
252       break;
253     case 10: // true color, rle
254       switch (bytesPerPixel) {
255         case 2: readColor = &readColor16!true; break;
256         case 3: readColor = &readColorTrue!(true, 3); break;
257         case 4: readColor = &readColorTrue!(true, 4); break;
258         default: throw new Exception("invalid tga pixel size");
259       }
260       break;
261     case 3: // black&white, no rle
262       switch (bytesPerPixel) {
263         case 1: readColor = &readColorBM8!false; break;
264         case 2: readColor = &readColorBM16!false; break;
265         default: throw new Exception("invalid tga pixel size");
266       }
267       break;
268     case 11: // black&white, rle
269       switch (bytesPerPixel) {
270         case 1: readColor = &readColorBM8!true; break;
271         case 2: readColor = &readColorBM16!true; break;
272         default: throw new Exception("invalid tga pixel size");
273       }
274       break;
275     case 1: // colormap, no rle
276       if (bytesPerPixel != 1) throw new Exception("invalid tga pixel size");
277       loadCM = true;
278       readColor = &readColorCM8!false;
279       break;
280     case 9: // colormap, rle
281       if (bytesPerPixel != 1) throw new Exception("invalid tga pixel size");
282       loadCM = true;
283       readColor = &readColorCM8!true;
284       break;
285     default: throw new Exception("invalid tga format");
286   }
287   // check for valid colormap
288   switch (hdr.cmapType) {
289     case 0:
290       if (hdr.cmapFirstIdx != 0 || hdr.cmapSize != 0) throw new Exception("invalid tga colormap type");
291       break;
292     case 1:
293       if (hdr.cmapElementSize != 15 && hdr.cmapElementSize != 16 && hdr.cmapElementSize != 24 && hdr.cmapElementSize != 32) throw new Exception("invalid tga colormap type");
294       if (hdr.cmapSize == 0) throw new Exception("invalid tga colormap type");
295       break;
296     default: throw new Exception("invalid tga colormap type");
297   }
298   if (!hdr.zeroBits) throw new Exception("invalid tga header");
299   void loadColormap () {
300     if (hdr.cmapType != 1) throw new Exception("invalid tga colormap type");
301     // calculate color map size
302     uint colorEntryBytes = 0;
303     switch (hdr.cmapElementSize) {
304       case 15:
305       case 16: colorEntryBytes = 2; break;
306       case 24: colorEntryBytes = 3; break;
307       case 32: colorEntryBytes = 4; break;
308       default: throw new Exception("invalid tga colormap type");
309     }
310     uint colorMapBytes = colorEntryBytes*hdr.cmapSize;
311     if (colorMapBytes == 0) throw new Exception("invalid tga colormap type");
312     // if we're going to use the color map, read it in.
313     if (loadCM) {
314       if (hdr.cmapFirstIdx+hdr.cmapSize > 256) throw new Exception("invalid tga colormap type");
315       ubyte readCMB () {
316         if (colorMapBytes == 0) return 0;
317         --colorMapBytes;
318         return readByte;
319       }
320       cmap[] = Color.black;
321       auto cmp = cmap.ptr;
322       switch (colorEntryBytes) {
323         case 2:
324           foreach (immutable n; 0..hdr.cmapSize) {
325             uint v = readCMB();
326             v |= readCMB()<<8;
327             cmp.b = cmap16.ptr[v&0x1f];
328             cmp.g = cmap16.ptr[(v>>5)&0x1f];
329             cmp.r = cmap16.ptr[(v>>10)&0x1f];
330             ++cmp;
331           }
332           break;
333         case 3:
334           foreach (immutable n; 0..hdr.cmapSize) {
335             cmp.b = readCMB();
336             cmp.g = readCMB();
337             cmp.r = readCMB();
338             ++cmp;
339           }
340           break;
341         case 4:
342           foreach (immutable n; 0..hdr.cmapSize) {
343             cmp.b = readCMB();
344             cmp.g = readCMB();
345             cmp.r = readCMB();
346             cmp.a = readCMB();
347             ++cmp;
348           }
349           break;
350         default: throw new Exception("invalid tga colormap type");
351       }
352     } else {
353       // skip colormap
354       fl.seek(colorMapBytes, Seek.Cur);
355     }
356   }
357 
358   // now load the data
359   fl.seek(hdr.idsize, Seek.Cur);
360   if (hdr.cmapType != 0) loadColormap();
361 
362   // we don't know if alpha is premultiplied yet
363   bool hasAlpha = (bytesPerPixel == 4);
364   bool validAlpha = hasAlpha;
365   bool premult = false;
366 
367   auto tcimg = new TrueColorImage(hdr.width, hdr.height);
368   scope(failure) .destroy(tcimg);
369 
370   {
371     // read image data
372     immutable bool xflip = hdr.xflip, yflip = hdr.yflip;
373     Color* pixdata = tcimg.imageData.colors.ptr;
374     if (yflip) pixdata += (hdr.height-1)*hdr.width;
375     foreach (immutable y; 0..hdr.height) {
376       auto d = pixdata;
377       if (xflip) d += hdr.width-1;
378       foreach (immutable x; 0..hdr.width) {
379         *d = readColor(&readByte);
380         if (xflip) --d; else ++d;
381       }
382       if (yflip) pixdata -= hdr.width; else pixdata += hdr.width;
383     }
384   }
385 
386   if (hasAlpha) {
387     if (extfooter.extofs != 0) {
388       Extension ext;
389       fl.seek(extfooter.extofs);
390       fl.readStruct(ext);
391       // some idiotic writers set 494 instead 495, tolerate that
392       if (ext.size < 494) throw new Exception("invalid tga extension record");
393       if (ext.attrType == 4) {
394         // premultiplied alpha
395         foreach (ref Color clr; tcimg.imageData.colors) {
396           if (clr.a != 0) {
397             clr.r = Color.clampToByte(clr.r*255/clr.a);
398             clr.g = Color.clampToByte(clr.g*255/clr.a);
399             clr.b = Color.clampToByte(clr.b*255/clr.a);
400           }
401         }
402       } else if (ext.attrType != 3) {
403         validAlpha = false;
404       }
405     } else {
406       // some writers sets all alphas to zero, check for that
407       validAlpha = false;
408       foreach (ref Color clr; tcimg.imageData.colors) if (clr.a != 0) { validAlpha = true; break; }
409     }
410     if (!validAlpha) foreach (ref Color clr; tcimg.imageData.colors) clr.a = 255;
411   }
412   return tcimg;
413 }
414 
415 
416 // ////////////////////////////////////////////////////////////////////////// //
417 private:
418 static if (!ArsdTargaHasIVVFS) {
419 import core.stdc.stdio : SEEK_SET, SEEK_CUR, SEEK_END;
420 
421 enum Seek : int {
422   Set = SEEK_SET,
423   Cur = SEEK_CUR,
424   End = SEEK_END,
425 }
426 
427 
428 // ////////////////////////////////////////////////////////////////////////// //
429 // augmentation checks
430 // is this "low-level" stream that can be read?
431 enum isLowLevelStreamR(T) = is(typeof((inout int=0) {
432   auto t = T.init;
433   ubyte[1] b;
434   ptrdiff_t r = t.read(b.ptr, 1);
435 }));
436 
437 // is this "low-level" stream that can be written?
438 enum isLowLevelStreamW(T) = is(typeof((inout int=0) {
439   auto t = T.init;
440   ubyte[1] b;
441   ptrdiff_t w = t.write(b.ptr, 1);
442 }));
443 
444 
445 // is this "low-level" stream that can be seeked?
446 enum isLowLevelStreamS(T) = is(typeof((inout int=0) {
447   auto t = T.init;
448   long p = t.lseek(0, 0);
449 }));
450 
451 
452 // ////////////////////////////////////////////////////////////////////////// //
453 // augment low-level streams with `rawRead`
454 T[] rawRead(ST, T) (auto ref ST st, T[] buf) if (isLowLevelStreamR!ST && !is(T == const) && !is(T == immutable)) {
455   if (buf.length > 0) {
456     auto res = st.read(buf.ptr, buf.length*T.sizeof);
457     if (res == -1 || res%T.sizeof != 0) throw new Exception("read error");
458     return buf[0..res/T.sizeof];
459   } else {
460     return buf[0..0];
461   }
462 }
463 
464 // augment low-level streams with `rawWrite`
465 void rawWrite(ST, T) (auto ref ST st, in T[] buf) if (isLowLevelStreamW!ST) {
466   if (buf.length > 0) {
467     auto res = st.write(buf.ptr, buf.length*T.sizeof);
468     if (res == -1 || res%T.sizeof != 0) throw new Exception("write error");
469   }
470 }
471 
472 // read exact size or throw error
473 package(arsd) T[] rawReadExact(ST, T) (auto ref ST st, T[] buf) if (isReadableStream!ST && !is(T == const) && !is(T == immutable)) {
474   if (buf.length == 0) return buf;
475   auto left = buf.length*T.sizeof;
476   auto dp = cast(ubyte*)buf.ptr;
477   while (left > 0) {
478     auto res = st.rawRead(cast(void[])(dp[0..left]));
479     if (res.length == 0) throw new Exception("read error");
480     dp += res.length;
481     left -= res.length;
482   }
483   return buf;
484 }
485 
486 // write exact size or throw error (just for convenience)
487 void rawWriteExact(ST, T) (auto ref ST st, in T[] buf) if (isWriteableStream!ST) { st.rawWrite(buf); }
488 
489 // if stream doesn't have `.size`, but can be seeked, emulate it
490 long size(ST) (auto ref ST st) if (isSeekableStream!ST && !streamHasSize!ST) {
491   auto opos = st.tell;
492   st.seek(0, Seek.End);
493   auto res = st.tell;
494   st.seek(opos);
495   return res;
496 }
497 
498 
499 // ////////////////////////////////////////////////////////////////////////// //
500 // check if a given stream supports `eof`
501 enum streamHasEof(T) = is(typeof((inout int=0) {
502   auto t = T.init;
503   bool n = t.eof;
504 }));
505 
506 // check if a given stream supports `seek`
507 enum streamHasSeek(T) = is(typeof((inout int=0) {
508   import core.stdc.stdio : SEEK_END;
509   auto t = T.init;
510   t.seek(0);
511   t.seek(0, SEEK_END);
512 }));
513 
514 // check if a given stream supports `tell`
515 enum streamHasTell(T) = is(typeof((inout int=0) {
516   auto t = T.init;
517   long pos = t.tell;
518 }));
519 
520 // check if a given stream supports `size`
521 enum streamHasSize(T) = is(typeof((inout int=0) {
522   auto t = T.init;
523   long pos = t.size;
524 }));
525 
526 // check if a given stream supports `rawRead()`.
527 // it's enough to support `void[] rawRead (void[] buf)`
528 enum isReadableStream(T) = is(typeof((inout int=0) {
529   auto t = T.init;
530   ubyte[1] b;
531   auto v = cast(void[])b;
532   t.rawRead(v);
533 }));
534 
535 // check if a given stream supports `rawWrite()`.
536 // it's enough to support `inout(void)[] rawWrite (inout(void)[] buf)`
537 enum isWriteableStream(T) = is(typeof((inout int=0) {
538   auto t = T.init;
539   ubyte[1] b;
540   t.rawWrite(cast(void[])b);
541 }));
542 
543 // check if a given stream supports `.seek(ofs, [whence])`, and `.tell`
544 enum isSeekableStream(T) = (streamHasSeek!T && streamHasTell!T);
545 
546 // check if we can get size of a given stream.
547 // this can be done either with `.size`, or with `.seek` and `.tell`
548 enum isSizedStream(T) = (streamHasSize!T || isSeekableStream!T);
549 
550 // ////////////////////////////////////////////////////////////////////////// //
551 private enum isGoodEndianness(string s) = (s == "LE" || s == "le" || s == "BE" || s == "be");
552 
553 private template isLittleEndianness(string s) if (isGoodEndianness!s) {
554   enum isLittleEndianness = (s == "LE" || s == "le");
555 }
556 
557 private template isBigEndianness(string s) if (isGoodEndianness!s) {
558   enum isLittleEndianness = (s == "BE" || s == "be");
559 }
560 
561 private template isSystemEndianness(string s) if (isGoodEndianness!s) {
562   version(LittleEndian) {
563     enum isSystemEndianness = isLittleEndianness!s;
564   } else {
565     enum isSystemEndianness = isBigEndianness!s;
566   }
567 }
568 
569 
570 // ////////////////////////////////////////////////////////////////////////// //
571 // write integer value of the given type, with the given endianness (default: little-endian)
572 // usage: st.writeNum!ubyte(10)
573 void writeNum(T, string es="LE", ST) (auto ref ST st, T n) if (isGoodEndianness!es && isWriteableStream!ST && __traits(isIntegral, T)) {
574   static assert(T.sizeof <= 8); // just in case
575   static if (isSystemEndianness!es) {
576     st.rawWriteExact((&n)[0..1]);
577   } else {
578     ubyte[T.sizeof] b = void;
579     version(LittleEndian) {
580       // convert to big-endian
581       foreach_reverse (ref x; b) { x = n&0xff; n >>= 8; }
582     } else {
583       // convert to little-endian
584       foreach (ref x; b) { x = n&0xff; n >>= 8; }
585     }
586     st.rawWriteExact(b[]);
587   }
588 }
589 
590 
591 // read integer value of the given type, with the given endianness (default: little-endian)
592 // usage: auto v = st.readNum!ubyte
593 T readNum(T, string es="LE", ST) (auto ref ST st) if (isGoodEndianness!es && isReadableStream!ST && __traits(isIntegral, T)) {
594   static assert(T.sizeof <= 8); // just in case
595   static if (isSystemEndianness!es) {
596     T v = void;
597     st.rawReadExact((&v)[0..1]);
598     return v;
599   } else {
600     ubyte[T.sizeof] b = void;
601     st.rawReadExact(b[]);
602     T v = 0;
603     version(LittleEndian) {
604       // convert from big-endian
605       foreach (ubyte x; b) { v <<= 8; v |= x; }
606     } else {
607       // conver from little-endian
608       foreach_reverse (ubyte x; b) { v <<= 8; v |= x; }
609     }
610     return v;
611   }
612 }
613 
614 
615 private enum reverseBytesMixin = "
616   foreach (idx; 0..b.length/2) {
617     ubyte t = b[idx];
618     b[idx] = b[$-idx-1];
619     b[$-idx-1] = t;
620   }
621 ";
622 
623 
624 // write floating value of the given type, with the given endianness (default: little-endian)
625 // usage: st.writeNum!float(10)
626 void writeNum(T, string es="LE", ST) (auto ref ST st, T n) if (isGoodEndianness!es && isWriteableStream!ST && __traits(isFloating, T)) {
627   static assert(T.sizeof <= 8);
628   static if (isSystemEndianness!es) {
629     st.rawWriteExact((&n)[0..1]);
630   } else {
631     import core.stdc.string : memcpy;
632     ubyte[T.sizeof] b = void;
633     memcpy(b.ptr, &v, T.sizeof);
634     mixin(reverseBytesMixin);
635     st.rawWriteExact(b[]);
636   }
637 }
638 
639 
640 // read floating value of the given type, with the given endianness (default: little-endian)
641 // usage: auto v = st.readNum!float
642 T readNum(T, string es="LE", ST) (auto ref ST st) if (isGoodEndianness!es && isReadableStream!ST && __traits(isFloating, T)) {
643   static assert(T.sizeof <= 8);
644   T v = void;
645   static if (isSystemEndianness!es) {
646     st.rawReadExact((&v)[0..1]);
647   } else {
648     import core.stdc.string : memcpy;
649     ubyte[T.sizeof] b = void;
650     st.rawReadExact(b[]);
651     mixin(reverseBytesMixin);
652     memcpy(&v, b.ptr, T.sizeof);
653   }
654   return v;
655 }
656 
657 
658 // ////////////////////////////////////////////////////////////////////////// //
659 void readStruct(string es="LE", SS, ST) (auto ref ST fl, ref SS st)
660 if (is(SS == struct) && isGoodEndianness!es && isReadableStream!ST)
661 {
662   void unserData(T) (ref T v) {
663     import std.traits : Unqual;
664     alias UT = Unqual!T;
665     static if (is(T : V[], V)) {
666       // array
667       static if (__traits(isStaticArray, T)) {
668         foreach (ref it; v) unserData(it);
669       } else static if (is(UT == char)) {
670         // special case: dynamic `char[]` array will be loaded as asciiz string
671         char c;
672         for (;;) {
673           if (fl.rawRead((&c)[0..1]).length == 0) break; // don't require trailing zero on eof
674           if (c == 0) break;
675           v ~= c;
676         }
677       } else {
678         assert(0, "cannot load dynamic arrays yet");
679       }
680     } else static if (is(T : V[K], K, V)) {
681       assert(0, "cannot load associative arrays yet");
682     } else static if (__traits(isIntegral, UT) || __traits(isFloating, UT)) {
683       // this takes care of `*char` and `bool` too
684       v = cast(UT)fl.readNum!(UT, es);
685     } else static if (is(T == struct)) {
686       // struct
687       import std.traits : FieldNameTuple, hasUDA;
688       foreach (string fldname; FieldNameTuple!T) {
689         unserData(__traits(getMember, v, fldname));
690       }
691     }
692   }
693 
694   unserData(st);
695 }
696 }