arsd/pcx.d

545 lines
17 KiB
D

//ketmar: Adam didn't wrote this, don't blame him!
//TODO: other bpp formats besides 8 and 24
module arsd.pcx;
import arsd.color;
import std.stdio : File; // sorry
static if (__traits(compiles, { import iv.vfs; })) enum ArsdPcxHasIVVFS = true; else enum ArsdPcxHasIVVFS = false;
static if (ArsdPcxHasIVVFS) import iv.vfs;
// ////////////////////////////////////////////////////////////////////////// //
public MemoryImage loadPcxMem (const(void)[] buf, const(char)[] filename=null) {
static struct MemRO {
const(ubyte)[] data;
long pos;
this (const(void)[] abuf) { data = cast(const(ubyte)[])abuf; }
@property long tell () { return pos; }
@property long size () { return data.length; }
void seek (long offset, int whence=Seek.Set) {
switch (whence) {
case Seek.Set:
if (offset < 0 || offset > data.length) throw new Exception("invalid offset");
pos = offset;
break;
case Seek.Cur:
if (offset < -pos || offset > data.length-pos) throw new Exception("invalid offset");
pos += offset;
break;
case Seek.End:
pos = data.length+offset;
if (pos < 0 || pos > data.length) throw new Exception("invalid offset");
break;
default:
throw new Exception("invalid offset origin");
}
}
ptrdiff_t read (void* buf, size_t count) @system {
if (pos >= data.length) return 0;
if (count > 0) {
import core.stdc.string : memcpy;
long rlen = data.length-pos;
if (rlen >= count) rlen = count;
assert(rlen != 0);
memcpy(buf, data.ptr+pos, cast(size_t)rlen);
pos += rlen;
return cast(ptrdiff_t)rlen;
} else {
return 0;
}
}
}
auto rd = MemRO(buf);
return loadPcx(rd, filename);
}
static if (ArsdPcxHasIVVFS) public MemoryImage loadPcx (VFile fl) { return loadPcxImpl(fl, fl.name); }
public MemoryImage loadPcx (File fl) { return loadPcxImpl(fl, fl.name); }
public MemoryImage loadPcx(T:const(char)[]) (T fname) {
static if (is(T == typeof(null))) {
throw new Exception("cannot load nameless tga");
} else {
static if (ArsdPcxHasIVVFS) {
return loadPcx(VFile(fname));
} else static if (is(T == string)) {
return loadPcx(File(fname), fname);
} else {
return loadPcx(File(fname.idup), fname);
}
}
}
// ////////////////////////////////////////////////////////////////////////// //
// pass filename to ease detection
// hack around "has scoped destruction, cannot build closure"
public MemoryImage loadPcx(ST) (auto ref ST fl, const(char)[] filename=null) if (isReadableStream!ST && isSeekableStream!ST) { return loadPcxImpl(fl, filename); }
private MemoryImage loadPcxImpl(ST) (auto ref ST fl, const(char)[] filename) {
import core.stdc.stdlib : malloc, free;
// PCX file header
static struct PCXHeader {
ubyte manufacturer; // 0x0a --signifies a PCX file
ubyte ver; // version 5 is what we look for
ubyte encoding; // when 1, it's RLE encoding (only type as of yet)
ubyte bitsperpixel; // how many bits to represent 1 pixel
ushort xmin, ymin, xmax, ymax; // dimensions of window (really insigned?)
ushort hdpi, vdpi; // device resolution (horizontal, vertical)
ubyte[16*3] colormap; // 16-color palette
ubyte reserved;
ubyte colorplanes; // number of color planes
ushort bytesperline; // number of bytes per line (per color plane)
ushort palettetype; // 1 = color,2 = grayscale (unused in v.5+)
ubyte[58] filler; // used to fill-out 128 byte header (useless)
}
bool isGoodExtension (const(char)[] filename) {
if (filename.length >= 4) {
auto ext = filename[$-4..$];
if (ext[0] == '.' && (ext[1] == 'P' || ext[1] == 'p') && (ext[2] == 'C' || ext[2] == 'c') && (ext[3] == 'X' || ext[3] == 'x')) return true;
}
return false;
}
// check file extension, if any
if (filename.length && !isGoodExtension(filename)) return null;
// we should have at least header
if (fl.size < 129) throw new Exception("invalid pcx file size");
fl.seek(0);
PCXHeader hdr;
fl.readStruct(hdr);
// check some header fields
if (hdr.manufacturer != 0x0a) throw new Exception("invalid pcx manufacturer");
if (/*header.ver != 0 && header.ver != 2 && header.ver != 3 &&*/ hdr.ver != 5) throw new Exception("invalid pcx version");
if (hdr.encoding != 0 && hdr.encoding != 1) throw new Exception("invalid pcx compresstion");
int wdt = hdr.xmax-hdr.xmin+1;
int hgt = hdr.ymax-hdr.ymin+1;
// arbitrary size limits
if (wdt < 1 || wdt > 32000) throw new Exception("invalid pcx width");
if (hgt < 1 || hgt > 32000) throw new Exception("invalid pcx height");
if (hdr.bytesperline < wdt) throw new Exception("invalid pcx hdr");
// if it's not a 256-color PCX file, and not 24-bit PCX file, gtfo
bool bpp24 = false;
bool hasAlpha = false;
if (hdr.colorplanes == 1) {
if (hdr.bitsperpixel != 8 && hdr.bitsperpixel != 24 && hdr.bitsperpixel != 32) throw new Exception("invalid pcx bpp");
bpp24 = (hdr.bitsperpixel == 24);
hasAlpha = (hdr.bitsperpixel == 32);
} else if (hdr.colorplanes == 3 || hdr.colorplanes == 4) {
if (hdr.bitsperpixel != 8) throw new Exception("invalid pcx bpp");
bpp24 = true;
hasAlpha = (hdr.colorplanes == 4);
}
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); }
// additional checks
if (hdr.reserved != 0) throw new Exception("invalid pcx hdr");
// 8bpp files MUST have palette
if (!bpp24 && fl.size < 129+769) throw new Exception("invalid pcx file size");
void readLine (ubyte* line) {
foreach (immutable p; 0..hdr.colorplanes) {
int count = 0;
ubyte b;
foreach (immutable n; 0..hdr.bytesperline) {
if (count == 0) {
// read next byte, do RLE decompression by the way
fl.rawReadExact((&b)[0..1]);
if (hdr.encoding) {
if ((b&0xc0) == 0xc0) {
count = b&0x3f;
if (count == 0) throw new Exception("invalid pcx RLE data");
fl.rawReadExact((&b)[0..1]);
} else {
count = 1;
}
} else {
count = 1;
}
}
assert(count > 0);
line[n] = b;
--count;
}
// allow excessive counts, why not?
line += hdr.bytesperline;
}
}
int lsize = hdr.bytesperline*hdr.colorplanes;
if (!bpp24 && lsize < 768) lsize = 768; // so we can use it as palette buffer
auto line = cast(ubyte*)malloc(lsize);
if (line is null) throw new Exception("out of memory");
scope(exit) free(line);
IndexedImage iimg;
TrueColorImage timg;
scope(failure) { .destroy(timg); .destroy(iimg); }
if (!bpp24) {
iimg = new IndexedImage(wdt, hgt);
} else {
timg = new TrueColorImage(wdt, hgt);
}
foreach (immutable y; 0..hgt) {
readLine(line);
if (!bpp24) {
import core.stdc.string : memcpy;
// 8bpp, with palette
memcpy(iimg.data.ptr+wdt*y, line, wdt);
} else {
// 24bpp
auto src = line;
auto dest = timg.imageData.bytes.ptr+(wdt*4)*y; //RGBA
if (hdr.colorplanes != 1) {
// planar
foreach (immutable x; 0..wdt) {
*dest++ = src[0]; // red
*dest++ = src[hdr.bytesperline]; // green
*dest++ = src[hdr.bytesperline*2]; // blue
if (hasAlpha) {
*dest++ = src[hdr.bytesperline*3]; // blue
} else {
*dest++ = 255; // alpha (opaque)
}
++src;
}
} else {
// flat
foreach (immutable x; 0..wdt) {
*dest++ = *src++; // red
*dest++ = *src++; // green
*dest++ = *src++; // blue
if (hasAlpha) {
*dest++ = *src++; // alpha
} else {
*dest++ = 255; // alpha (opaque)
}
}
}
}
}
// read palette
if (!bpp24) {
fl.seek(-769, Seek.End);
if (fl.readNum!ubyte != 12) throw new Exception("invalid pcx palette");
// it is guaranteed to have at least 768 bytes in `line`
fl.rawReadExact(line[0..768]);
if (iimg.palette.length < 256) iimg.palette.length = 256;
foreach (immutable cidx; 0..256) {
/* nope, it is not in VGA format
// transform [0..63] palette to [0..255]
int r = line[cidx*3+0]*255/63;
int g = line[cidx*3+1]*255/63;
int b = line[cidx*3+2]*255/63;
iimg.palette[cidx] = Color(r, g, b, 255);
*/
iimg.palette[cidx] = Color(line[cidx*3+0], line[cidx*3+1], line[cidx*3+2], 255);
}
return iimg;
} else {
return timg;
}
}
// ////////////////////////////////////////////////////////////////////////// //
private:
static if (!ArsdPcxHasIVVFS) {
import core.stdc.stdio : SEEK_SET, SEEK_CUR, SEEK_END;
enum Seek : int {
Set = SEEK_SET,
Cur = SEEK_CUR,
End = SEEK_END,
}
// ////////////////////////////////////////////////////////////////////////// //
// augmentation checks
// is this "low-level" stream that can be read?
enum isLowLevelStreamR(T) = is(typeof((inout int=0) {
auto t = T.init;
ubyte[1] b;
ptrdiff_t r = t.read(b.ptr, 1);
}));
// is this "low-level" stream that can be written?
enum isLowLevelStreamW(T) = is(typeof((inout int=0) {
auto t = T.init;
ubyte[1] b;
ptrdiff_t w = t.write(b.ptr, 1);
}));
// is this "low-level" stream that can be seeked?
enum isLowLevelStreamS(T) = is(typeof((inout int=0) {
auto t = T.init;
long p = t.lseek(0, 0);
}));
// ////////////////////////////////////////////////////////////////////////// //
// augment low-level streams with `rawRead`
T[] rawRead(ST, T) (auto ref ST st, T[] buf) if (isLowLevelStreamR!ST && !is(T == const) && !is(T == immutable)) {
if (buf.length > 0) {
auto res = st.read(buf.ptr, buf.length*T.sizeof);
if (res == -1 || res%T.sizeof != 0) throw new Exception("read error");
return buf[0..res/T.sizeof];
} else {
return buf[0..0];
}
}
// augment low-level streams with `rawWrite`
void rawWrite(ST, T) (auto ref ST st, in T[] buf) if (isLowLevelStreamW!ST) {
if (buf.length > 0) {
auto res = st.write(buf.ptr, buf.length*T.sizeof);
if (res == -1 || res%T.sizeof != 0) throw new Exception("write error");
}
}
// read exact size or throw error
T[] rawReadExact(ST, T) (auto ref ST st, T[] buf) if (isReadableStream!ST && !is(T == const) && !is(T == immutable)) {
if (buf.length == 0) return buf;
auto left = buf.length*T.sizeof;
auto dp = cast(ubyte*)buf.ptr;
while (left > 0) {
auto res = st.rawRead(cast(void[])(dp[0..left]));
if (res.length == 0) throw new Exception("read error");
dp += res.length;
left -= res.length;
}
return buf;
}
// write exact size or throw error (just for convenience)
void rawWriteExact(ST, T) (auto ref ST st, in T[] buf) if (isWriteableStream!ST) { st.rawWrite(buf); }
// if stream doesn't have `.size`, but can be seeked, emulate it
long size(ST) (auto ref ST st) if (isSeekableStream!ST && !streamHasSize!ST) {
auto opos = st.tell;
st.seek(0, Seek.End);
auto res = st.tell;
st.seek(opos);
return res;
}
// ////////////////////////////////////////////////////////////////////////// //
// check if a given stream supports `eof`
enum streamHasEof(T) = is(typeof((inout int=0) {
auto t = T.init;
bool n = t.eof;
}));
// check if a given stream supports `seek`
enum streamHasSeek(T) = is(typeof((inout int=0) {
import core.stdc.stdio : SEEK_END;
auto t = T.init;
t.seek(0);
t.seek(0, SEEK_END);
}));
// check if a given stream supports `tell`
enum streamHasTell(T) = is(typeof((inout int=0) {
auto t = T.init;
long pos = t.tell;
}));
// check if a given stream supports `size`
enum streamHasSize(T) = is(typeof((inout int=0) {
auto t = T.init;
long pos = t.size;
}));
// check if a given stream supports `rawRead()`.
// it's enough to support `void[] rawRead (void[] buf)`
enum isReadableStream(T) = is(typeof((inout int=0) {
auto t = T.init;
ubyte[1] b;
auto v = cast(void[])b;
t.rawRead(v);
}));
// check if a given stream supports `rawWrite()`.
// it's enough to support `inout(void)[] rawWrite (inout(void)[] buf)`
enum isWriteableStream(T) = is(typeof((inout int=0) {
auto t = T.init;
ubyte[1] b;
t.rawWrite(cast(void[])b);
}));
// check if a given stream supports `.seek(ofs, [whence])`, and `.tell`
enum isSeekableStream(T) = (streamHasSeek!T && streamHasTell!T);
// check if we can get size of a given stream.
// this can be done either with `.size`, or with `.seek` and `.tell`
enum isSizedStream(T) = (streamHasSize!T || isSeekableStream!T);
// ////////////////////////////////////////////////////////////////////////// //
private enum isGoodEndianness(string s) = (s == "LE" || s == "le" || s == "BE" || s == "be");
private template isLittleEndianness(string s) if (isGoodEndianness!s) {
enum isLittleEndianness = (s == "LE" || s == "le");
}
private template isBigEndianness(string s) if (isGoodEndianness!s) {
enum isLittleEndianness = (s == "BE" || s == "be");
}
private template isSystemEndianness(string s) if (isGoodEndianness!s) {
version(LittleEndian) {
enum isSystemEndianness = isLittleEndianness!s;
} else {
enum isSystemEndianness = isBigEndianness!s;
}
}
// ////////////////////////////////////////////////////////////////////////// //
// write integer value of the given type, with the given endianness (default: little-endian)
// usage: st.writeNum!ubyte(10)
void writeNum(T, string es="LE", ST) (auto ref ST st, T n) if (isGoodEndianness!es && isWriteableStream!ST && __traits(isIntegral, T)) {
static assert(T.sizeof <= 8); // just in case
static if (isSystemEndianness!es) {
st.rawWriteExact((&n)[0..1]);
} else {
ubyte[T.sizeof] b = void;
version(LittleEndian) {
// convert to big-endian
foreach_reverse (ref x; b) { x = n&0xff; n >>= 8; }
} else {
// convert to little-endian
foreach (ref x; b) { x = n&0xff; n >>= 8; }
}
st.rawWriteExact(b[]);
}
}
// read integer value of the given type, with the given endianness (default: little-endian)
// usage: auto v = st.readNum!ubyte
T readNum(T, string es="LE", ST) (auto ref ST st) if (isGoodEndianness!es && isReadableStream!ST && __traits(isIntegral, T)) {
static assert(T.sizeof <= 8); // just in case
static if (isSystemEndianness!es) {
T v = void;
st.rawReadExact((&v)[0..1]);
return v;
} else {
ubyte[T.sizeof] b = void;
st.rawReadExact(b[]);
T v = 0;
version(LittleEndian) {
// convert from big-endian
foreach (ubyte x; b) { v <<= 8; v |= x; }
} else {
// conver from little-endian
foreach_reverse (ubyte x; b) { v <<= 8; v |= x; }
}
return v;
}
}
private enum reverseBytesMixin = "
foreach (idx; 0..b.length/2) {
ubyte t = b[idx];
b[idx] = b[$-idx-1];
b[$-idx-1] = t;
}
";
// write floating value of the given type, with the given endianness (default: little-endian)
// usage: st.writeNum!float(10)
void writeNum(T, string es="LE", ST) (auto ref ST st, T n) if (isGoodEndianness!es && isWriteableStream!ST && __traits(isFloating, T)) {
static assert(T.sizeof <= 8);
static if (isSystemEndianness!es) {
st.rawWriteExact((&n)[0..1]);
} else {
import core.stdc.string : memcpy;
ubyte[T.sizeof] b = void;
memcpy(b.ptr, &v, T.sizeof);
mixin(reverseBytesMixin);
st.rawWriteExact(b[]);
}
}
// read floating value of the given type, with the given endianness (default: little-endian)
// usage: auto v = st.readNum!float
T readNum(T, string es="LE", ST) (auto ref ST st) if (isGoodEndianness!es && isReadableStream!ST && __traits(isFloating, T)) {
static assert(T.sizeof <= 8);
T v = void;
static if (isSystemEndianness!es) {
st.rawReadExact((&v)[0..1]);
} else {
import core.stdc.string : memcpy;
ubyte[T.sizeof] b = void;
st.rawReadExact(b[]);
mixin(reverseBytesMixin);
memcpy(&v, b.ptr, T.sizeof);
}
return v;
}
// ////////////////////////////////////////////////////////////////////////// //
void readStruct(string es="LE", SS, ST) (auto ref ST fl, ref SS st)
if (is(SS == struct) && isGoodEndianness!es && isReadableStream!ST)
{
void unserData(T) (ref T v) {
import std.traits : Unqual;
alias UT = Unqual!T;
static if (is(T : V[], V)) {
// array
static if (__traits(isStaticArray, T)) {
foreach (ref it; v) unserData(it);
} else static if (is(UT == char)) {
// special case: dynamic `char[]` array will be loaded as asciiz string
char c;
for (;;) {
if (fl.rawRead((&c)[0..1]).length == 0) break; // don't require trailing zero on eof
if (c == 0) break;
v ~= c;
}
} else {
assert(0, "cannot load dynamic arrays yet");
}
} else static if (is(T : V[K], K, V)) {
assert(0, "cannot load associative arrays yet");
} else static if (__traits(isIntegral, UT) || __traits(isFloating, UT)) {
// this takes care of `*char` and `bool` too
v = cast(UT)fl.readNum!(UT, es);
} else static if (is(T == struct)) {
// struct
import std.traits : FieldNameTuple, hasUDA;
foreach (string fldname; FieldNameTuple!T) {
unserData(__traits(getMember, v, fldname));
}
}
}
unserData(st);
}
}