1 //ketmar: Adam didn't wrote this, don't blame him!
2 //TODO: other bpp formats besides 8 and 24
3 module arsd.pcx;
4 
5 import arsd.color;
6 import std.stdio : File; // sorry
7 
8 static if (__traits(compiles, { import iv.vfs; })) enum ArsdPcxHasIVVFS = true; else enum ArsdPcxHasIVVFS = false;
9 static if (ArsdPcxHasIVVFS) import iv.vfs;
10 
11 
12 // ////////////////////////////////////////////////////////////////////////// //
13 public MemoryImage loadPcxMem (const(void)[] buf, const(char)[] filename=null) {
14   static struct MemRO {
15     const(ubyte)[] data;
16     long pos;
17 
18     this (const(void)[] abuf) { data = cast(const(ubyte)[])abuf; }
19 
20     @property long tell () { return pos; }
21     @property long size () { return data.length; }
22 
23     void seek (long offset, int whence=Seek.Set) {
24       switch (whence) {
25         case Seek.Set:
26           if (offset < 0 || offset > data.length) throw new Exception("invalid offset");
27           pos = offset;
28           break;
29         case Seek.Cur:
30           if (offset < -pos || offset > data.length-pos) throw new Exception("invalid offset");
31           pos += offset;
32           break;
33         case Seek.End:
34           pos = data.length+offset;
35           if (pos < 0 || pos > data.length) throw new Exception("invalid offset");
36           break;
37         default:
38           throw new Exception("invalid offset origin");
39       }
40     }
41 
42     ptrdiff_t read (void* buf, size_t count) {
43       if (pos >= data.length) return 0;
44       if (count > 0) {
45         import core.stdc.string : memcpy;
46         long rlen = data.length-pos;
47         if (rlen >= count) rlen = count;
48         assert(rlen != 0);
49         memcpy(buf, data.ptr+pos, cast(size_t)rlen);
50         pos += rlen;
51         return cast(ptrdiff_t)rlen;
52       } else {
53         return 0;
54       }
55     }
56   }
57 
58   auto rd = MemRO(buf);
59   return loadPcx(rd, filename);
60 }
61 
62 static if (ArsdPcxHasIVVFS) public MemoryImage loadPcx (VFile fl) { return loadPcxImpl(fl, fl.name); }
63 public MemoryImage loadPcx (File fl) { return loadPcxImpl(fl, fl.name); }
64 public MemoryImage loadPcx(T:const(char)[]) (T fname) {
65   static if (is(T == typeof(null))) {
66     throw new Exception("cannot load nameless tga");
67   } else {
68     static if (ArsdPcxHasIVVFS) {
69       return loadPcx(VFile(fname));
70     } else static if (is(T == string)) {
71       return loadPcx(File(fname), fname);
72     } else {
73       return loadPcx(File(fname.idup), fname);
74     }
75   }
76 }
77 
78 
79 // ////////////////////////////////////////////////////////////////////////// //
80 // pass filename to ease detection
81 // hack around "has scoped destruction, cannot build closure"
82 public MemoryImage loadPcx(ST) (auto ref ST fl, const(char)[] filename=null) if (isReadableStream!ST && isSeekableStream!ST) { return loadPcxImpl(fl, filename); }
83 
84 private MemoryImage loadPcxImpl(ST) (auto ref ST fl, const(char)[] filename) {
85   import core.stdc.stdlib : malloc, free;
86 
87   // PCX file header
88   static struct PCXHeader {
89     ubyte manufacturer; // 0x0a --signifies a PCX file
90     ubyte ver; // version 5 is what we look for
91     ubyte encoding; // when 1, it's RLE encoding (only type as of yet)
92     ubyte bitsperpixel; // how many bits to represent 1 pixel
93     ushort xmin, ymin, xmax, ymax; // dimensions of window (really insigned?)
94     ushort hdpi, vdpi; // device resolution (horizontal, vertical)
95     ubyte[16*3] colormap; // 16-color palette
96     ubyte reserved;
97     ubyte colorplanes; // number of color planes
98     ushort bytesperline; // number of bytes per line (per color plane)
99     ushort palettetype; // 1 = color,2 = grayscale (unused in v.5+)
100     ubyte[58] filler; // used to fill-out 128 byte header (useless)
101   }
102 
103   bool isGoodExtension (const(char)[] filename) {
104     if (filename.length >= 4) {
105       auto ext = filename[$-4..$];
106       if (ext[0] == '.' && (ext[1] == 'P' || ext[1] == 'p') && (ext[2] == 'C' || ext[2] == 'c') && (ext[3] == 'X' || ext[3] == 'x')) return true;
107     }
108     return false;
109   }
110 
111   // check file extension, if any
112   if (filename.length && !isGoodExtension(filename)) return null;
113 
114   // we should have at least header
115   if (fl.size < 129) throw new Exception("invalid pcx file size");
116 
117   fl.seek(0);
118   PCXHeader hdr;
119   fl.readStruct(hdr);
120 
121   // check some header fields
122   if (hdr.manufacturer != 0x0a) throw new Exception("invalid pcx manufacturer");
123   if (/*header.ver != 0 && header.ver != 2 && header.ver != 3 &&*/ hdr.ver != 5) throw new Exception("invalid pcx version");
124   if (hdr.encoding != 0 && hdr.encoding != 1) throw new Exception("invalid pcx compresstion");
125 
126   int wdt = hdr.xmax-hdr.xmin+1;
127   int hgt = hdr.ymax-hdr.ymin+1;
128 
129   // arbitrary size limits
130   if (wdt < 1 || wdt > 32000) throw new Exception("invalid pcx width");
131   if (hgt < 1 || hgt > 32000) throw new Exception("invalid pcx height");
132 
133   if (hdr.bytesperline < wdt) throw new Exception("invalid pcx hdr");
134 
135   // if it's not a 256-color PCX file, and not 24-bit PCX file, gtfo
136   bool bpp24 = false;
137   bool hasAlpha = false;
138   if (hdr.colorplanes == 1) {
139     if (hdr.bitsperpixel != 8 && hdr.bitsperpixel != 24 && hdr.bitsperpixel != 32) throw new Exception("invalid pcx bpp");
140     bpp24 = (hdr.bitsperpixel == 24);
141     hasAlpha = (hdr.bitsperpixel == 32);
142   } else if (hdr.colorplanes == 3 || hdr.colorplanes == 4) {
143     if (hdr.bitsperpixel != 8) throw new Exception("invalid pcx bpp");
144     bpp24 = true;
145     hasAlpha = (hdr.colorplanes == 4);
146   }
147 
148   version(arsd_debug_pcx) { import core.stdc.stdio; printf("colorplanes=%u; bitsperpixel=%u; bytesperline=%u\n", cast(uint)hdr.colorplanes, cast(uint)hdr.bitsperpixel, cast(uint)hdr.bytesperline); }
149 
150   // additional checks
151   if (hdr.reserved != 0) throw new Exception("invalid pcx hdr");
152 
153   // 8bpp files MUST have palette
154   if (!bpp24 && fl.size < 129+769) throw new Exception("invalid pcx file size");
155 
156   void readLine (ubyte* line) {
157     foreach (immutable p; 0..hdr.colorplanes) {
158       int count = 0;
159       ubyte b;
160       foreach (immutable n; 0..hdr.bytesperline) {
161         if (count == 0) {
162           // read next byte, do RLE decompression by the way
163           fl.rawReadExact((&b)[0..1]);
164           if (hdr.encoding) {
165             if ((b&0xc0) == 0xc0) {
166               count = b&0x3f;
167               if (count == 0) throw new Exception("invalid pcx RLE data");
168               fl.rawReadExact((&b)[0..1]);
169             } else {
170               count = 1;
171             }
172           } else {
173             count = 1;
174           }
175         }
176         assert(count > 0);
177         line[n] = b;
178         --count;
179       }
180       // allow excessive counts, why not?
181       line += hdr.bytesperline;
182     }
183   }
184 
185   int lsize = hdr.bytesperline*hdr.colorplanes;
186   if (!bpp24 && lsize < 768) lsize = 768; // so we can use it as palette buffer
187   auto line = cast(ubyte*)malloc(lsize);
188   if (line is null) throw new Exception("out of memory");
189   scope(exit) free(line);
190 
191   IndexedImage iimg;
192   TrueColorImage timg;
193   scope(failure) { .destroy(timg); .destroy(iimg); }
194 
195   if (!bpp24) {
196     iimg = new IndexedImage(wdt, hgt);
197   } else {
198     timg = new TrueColorImage(wdt, hgt);
199   }
200 
201   foreach (immutable y; 0..hgt) {
202     readLine(line);
203     if (!bpp24) {
204       import core.stdc.string : memcpy;
205       // 8bpp, with palette
206       memcpy(iimg.data.ptr+wdt*y, line, wdt);
207     } else {
208       // 24bpp
209       auto src = line;
210       auto dest = timg.imageData.bytes.ptr+(wdt*4)*y; //RGBA
211       if (hdr.colorplanes != 1) {
212         // planar
213         foreach (immutable x; 0..wdt) {
214           *dest++ = src[0]; // red
215           *dest++ = src[hdr.bytesperline]; // green
216           *dest++ = src[hdr.bytesperline*2]; // blue
217           if (hasAlpha) {
218             *dest++ = src[hdr.bytesperline*3]; // blue
219           } else {
220             *dest++ = 255; // alpha (opaque)
221           }
222           ++src;
223         }
224       } else {
225         // flat
226         foreach (immutable x; 0..wdt) {
227           *dest++ = *src++; // red
228           *dest++ = *src++; // green
229           *dest++ = *src++; // blue
230           if (hasAlpha) {
231             *dest++ = *src++; // alpha
232           } else {
233             *dest++ = 255; // alpha (opaque)
234           }
235         }
236       }
237     }
238   }
239 
240   // read palette
241   if (!bpp24) {
242     fl.seek(-769, Seek.End);
243     if (fl.readNum!ubyte != 12) throw new Exception("invalid pcx palette");
244     // it is guaranteed to have at least 768 bytes in `line`
245     fl.rawReadExact(line[0..768]);
246     if (iimg.palette.length < 256) iimg.palette.length = 256;
247     foreach (immutable cidx; 0..256) {
248       /* nope, it is not in VGA format
249       // transform [0..63] palette to [0..255]
250       int r = line[cidx*3+0]*255/63;
251       int g = line[cidx*3+1]*255/63;
252       int b = line[cidx*3+2]*255/63;
253       iimg.palette[cidx] = Color(r, g, b, 255);
254       */
255       iimg.palette[cidx] = Color(line[cidx*3+0], line[cidx*3+1], line[cidx*3+2], 255);
256     }
257     return iimg;
258   } else {
259     return timg;
260   }
261 }
262 
263 
264 // ////////////////////////////////////////////////////////////////////////// //
265 private:
266 static if (!ArsdPcxHasIVVFS) {
267 import core.stdc.stdio : SEEK_SET, SEEK_CUR, SEEK_END;
268 
269 enum Seek : int {
270   Set = SEEK_SET,
271   Cur = SEEK_CUR,
272   End = SEEK_END,
273 }
274 
275 
276 // ////////////////////////////////////////////////////////////////////////// //
277 // augmentation checks
278 // is this "low-level" stream that can be read?
279 enum isLowLevelStreamR(T) = is(typeof((inout int=0) {
280   auto t = T.init;
281   ubyte[1] b;
282   ptrdiff_t r = t.read(b.ptr, 1);
283 }));
284 
285 // is this "low-level" stream that can be written?
286 enum isLowLevelStreamW(T) = is(typeof((inout int=0) {
287   auto t = T.init;
288   ubyte[1] b;
289   ptrdiff_t w = t.write(b.ptr, 1);
290 }));
291 
292 
293 // is this "low-level" stream that can be seeked?
294 enum isLowLevelStreamS(T) = is(typeof((inout int=0) {
295   auto t = T.init;
296   long p = t.lseek(0, 0);
297 }));
298 
299 
300 // ////////////////////////////////////////////////////////////////////////// //
301 // augment low-level streams with `rawRead`
302 T[] rawRead(ST, T) (auto ref ST st, T[] buf) if (isLowLevelStreamR!ST && !is(T == const) && !is(T == immutable)) {
303   if (buf.length > 0) {
304     auto res = st.read(buf.ptr, buf.length*T.sizeof);
305     if (res == -1 || res%T.sizeof != 0) throw new Exception("read error");
306     return buf[0..res/T.sizeof];
307   } else {
308     return buf[0..0];
309   }
310 }
311 
312 // augment low-level streams with `rawWrite`
313 void rawWrite(ST, T) (auto ref ST st, in T[] buf) if (isLowLevelStreamW!ST) {
314   if (buf.length > 0) {
315     auto res = st.write(buf.ptr, buf.length*T.sizeof);
316     if (res == -1 || res%T.sizeof != 0) throw new Exception("write error");
317   }
318 }
319 
320 // read exact size or throw error
321 T[] rawReadExact(ST, T) (auto ref ST st, T[] buf) if (isReadableStream!ST && !is(T == const) && !is(T == immutable)) {
322   if (buf.length == 0) return buf;
323   auto left = buf.length*T.sizeof;
324   auto dp = cast(ubyte*)buf.ptr;
325   while (left > 0) {
326     auto res = st.rawRead(cast(void[])(dp[0..left]));
327     if (res.length == 0) throw new Exception("read error");
328     dp += res.length;
329     left -= res.length;
330   }
331   return buf;
332 }
333 
334 // write exact size or throw error (just for convenience)
335 void rawWriteExact(ST, T) (auto ref ST st, in T[] buf) if (isWriteableStream!ST) { st.rawWrite(buf); }
336 
337 // if stream doesn't have `.size`, but can be seeked, emulate it
338 long size(ST) (auto ref ST st) if (isSeekableStream!ST && !streamHasSize!ST) {
339   auto opos = st.tell;
340   st.seek(0, Seek.End);
341   auto res = st.tell;
342   st.seek(opos);
343   return res;
344 }
345 
346 
347 // ////////////////////////////////////////////////////////////////////////// //
348 // check if a given stream supports `eof`
349 enum streamHasEof(T) = is(typeof((inout int=0) {
350   auto t = T.init;
351   bool n = t.eof;
352 }));
353 
354 // check if a given stream supports `seek`
355 enum streamHasSeek(T) = is(typeof((inout int=0) {
356   import core.stdc.stdio : SEEK_END;
357   auto t = T.init;
358   t.seek(0);
359   t.seek(0, SEEK_END);
360 }));
361 
362 // check if a given stream supports `tell`
363 enum streamHasTell(T) = is(typeof((inout int=0) {
364   auto t = T.init;
365   long pos = t.tell;
366 }));
367 
368 // check if a given stream supports `size`
369 enum streamHasSize(T) = is(typeof((inout int=0) {
370   auto t = T.init;
371   long pos = t.size;
372 }));
373 
374 // check if a given stream supports `rawRead()`.
375 // it's enough to support `void[] rawRead (void[] buf)`
376 enum isReadableStream(T) = is(typeof((inout int=0) {
377   auto t = T.init;
378   ubyte[1] b;
379   auto v = cast(void[])b;
380   t.rawRead(v);
381 }));
382 
383 // check if a given stream supports `rawWrite()`.
384 // it's enough to support `inout(void)[] rawWrite (inout(void)[] buf)`
385 enum isWriteableStream(T) = is(typeof((inout int=0) {
386   auto t = T.init;
387   ubyte[1] b;
388   t.rawWrite(cast(void[])b);
389 }));
390 
391 // check if a given stream supports `.seek(ofs, [whence])`, and `.tell`
392 enum isSeekableStream(T) = (streamHasSeek!T && streamHasTell!T);
393 
394 // check if we can get size of a given stream.
395 // this can be done either with `.size`, or with `.seek` and `.tell`
396 enum isSizedStream(T) = (streamHasSize!T || isSeekableStream!T);
397 
398 // ////////////////////////////////////////////////////////////////////////// //
399 private enum isGoodEndianness(string s) = (s == "LE" || s == "le" || s == "BE" || s == "be");
400 
401 private template isLittleEndianness(string s) if (isGoodEndianness!s) {
402   enum isLittleEndianness = (s == "LE" || s == "le");
403 }
404 
405 private template isBigEndianness(string s) if (isGoodEndianness!s) {
406   enum isLittleEndianness = (s == "BE" || s == "be");
407 }
408 
409 private template isSystemEndianness(string s) if (isGoodEndianness!s) {
410   version(LittleEndian) {
411     enum isSystemEndianness = isLittleEndianness!s;
412   } else {
413     enum isSystemEndianness = isBigEndianness!s;
414   }
415 }
416 
417 
418 // ////////////////////////////////////////////////////////////////////////// //
419 // write integer value of the given type, with the given endianness (default: little-endian)
420 // usage: st.writeNum!ubyte(10)
421 void writeNum(T, string es="LE", ST) (auto ref ST st, T n) if (isGoodEndianness!es && isWriteableStream!ST && __traits(isIntegral, T)) {
422   static assert(T.sizeof <= 8); // just in case
423   static if (isSystemEndianness!es) {
424     st.rawWriteExact((&n)[0..1]);
425   } else {
426     ubyte[T.sizeof] b = void;
427     version(LittleEndian) {
428       // convert to big-endian
429       foreach_reverse (ref x; b) { x = n&0xff; n >>= 8; }
430     } else {
431       // convert to little-endian
432       foreach (ref x; b) { x = n&0xff; n >>= 8; }
433     }
434     st.rawWriteExact(b[]);
435   }
436 }
437 
438 
439 // read integer value of the given type, with the given endianness (default: little-endian)
440 // usage: auto v = st.readNum!ubyte
441 T readNum(T, string es="LE", ST) (auto ref ST st) if (isGoodEndianness!es && isReadableStream!ST && __traits(isIntegral, T)) {
442   static assert(T.sizeof <= 8); // just in case
443   static if (isSystemEndianness!es) {
444     T v = void;
445     st.rawReadExact((&v)[0..1]);
446     return v;
447   } else {
448     ubyte[T.sizeof] b = void;
449     st.rawReadExact(b[]);
450     T v = 0;
451     version(LittleEndian) {
452       // convert from big-endian
453       foreach (ubyte x; b) { v <<= 8; v |= x; }
454     } else {
455       // conver from little-endian
456       foreach_reverse (ubyte x; b) { v <<= 8; v |= x; }
457     }
458     return v;
459   }
460 }
461 
462 
463 private enum reverseBytesMixin = "
464   foreach (idx; 0..b.length/2) {
465     ubyte t = b[idx];
466     b[idx] = b[$-idx-1];
467     b[$-idx-1] = t;
468   }
469 ";
470 
471 
472 // write floating value of the given type, with the given endianness (default: little-endian)
473 // usage: st.writeNum!float(10)
474 void writeNum(T, string es="LE", ST) (auto ref ST st, T n) if (isGoodEndianness!es && isWriteableStream!ST && __traits(isFloating, T)) {
475   static assert(T.sizeof <= 8);
476   static if (isSystemEndianness!es) {
477     st.rawWriteExact((&n)[0..1]);
478   } else {
479     import core.stdc.string : memcpy;
480     ubyte[T.sizeof] b = void;
481     memcpy(b.ptr, &v, T.sizeof);
482     mixin(reverseBytesMixin);
483     st.rawWriteExact(b[]);
484   }
485 }
486 
487 
488 // read floating value of the given type, with the given endianness (default: little-endian)
489 // usage: auto v = st.readNum!float
490 T readNum(T, string es="LE", ST) (auto ref ST st) if (isGoodEndianness!es && isReadableStream!ST && __traits(isFloating, T)) {
491   static assert(T.sizeof <= 8);
492   T v = void;
493   static if (isSystemEndianness!es) {
494     st.rawReadExact((&v)[0..1]);
495   } else {
496     import core.stdc.string : memcpy;
497     ubyte[T.sizeof] b = void;
498     st.rawReadExact(b[]);
499     mixin(reverseBytesMixin);
500     memcpy(&v, b.ptr, T.sizeof);
501   }
502   return v;
503 }
504 
505 
506 // ////////////////////////////////////////////////////////////////////////// //
507 void readStruct(string es="LE", SS, ST) (auto ref ST fl, ref SS st)
508 if (is(SS == struct) && isGoodEndianness!es && isReadableStream!ST)
509 {
510   void unserData(T) (ref T v) {
511     import std.traits : Unqual;
512     alias UT = Unqual!T;
513     static if (is(T : V[], V)) {
514       // array
515       static if (__traits(isStaticArray, T)) {
516         foreach (ref it; v) unserData(it);
517       } else static if (is(UT == char)) {
518         // special case: dynamic `char[]` array will be loaded as asciiz string
519         char c;
520         for (;;) {
521           if (fl.rawRead((&c)[0..1]).length == 0) break; // don't require trailing zero on eof
522           if (c == 0) break;
523           v ~= c;
524         }
525       } else {
526         assert(0, "cannot load dynamic arrays yet");
527       }
528     } else static if (is(T : V[K], K, V)) {
529       assert(0, "cannot load associative arrays yet");
530     } else static if (__traits(isIntegral, UT) || __traits(isFloating, UT)) {
531       // this takes care of `*char` and `bool` too
532       v = cast(UT)fl.readNum!(UT, es);
533     } else static if (is(T == struct)) {
534       // struct
535       import std.traits : FieldNameTuple, hasUDA;
536       foreach (string fldname; FieldNameTuple!T) {
537         unserData(__traits(getMember, v, fldname));
538       }
539     }
540   }
541 
542   unserData(st);
543 }
544 }