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) @system { 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 }