1 /// DirectDraw Surfaces (DDS files) 2 module nwn.dds; 3 4 import std.stdint; 5 import std.exception; 6 import std.algorithm; 7 import std.conv; 8 import std.traits; 9 import std.string: format; 10 import std.range: chunks; 11 debug import std.stdio; 12 13 import nwnlibd.parseutils; 14 import nwnlibd.bitmap; 15 16 /// DDS file parsing 17 struct Dds { 18 19 /// 20 this(in ubyte[] data){ 21 auto cr = ChunkReader(data); 22 23 enforce(cr.read!(char[4]) == "DDS ", "Data is not a DDS image"); 24 header = cr.read!Header; 25 26 enum requiredFlags = Header.Flags.DDSD_WIDTH | Header.Flags.DDSD_HEIGHT | Header.Flags.DDSD_PIXELFORMAT; 27 enforce((header.flags & requiredFlags) == requiredFlags, 28 "Unsupported DDS flags: " ~ header.flags.flagsToString!(Header.Flags)); 29 30 if(header.caps2 & Header.Caps2Flags.DDSCAPS2_CUBEMAP) 31 enforce(false, "Unsupported DDS cubemap"); 32 if (header.caps2 & Header.Caps2Flags.DDSCAPS2_VOLUME && header.depth > 0) 33 enforce(false, "Unsupported DDS volume map"); 34 35 36 if (header.ddpf.flags & (Header.DDPF.Flags.DDPF_RGB | Header.DDPF.Flags.DDPF_LUMINANCE)){ 37 enforce(header.ddpf.rgb_bit_count % 8 == 0, "header.ddpf.rgb_bit_count must a multiple of 8"); 38 39 blockSize = 1; 40 bpp = header.ddpf.rgb_bit_count > 0 ? header.ddpf.rgb_bit_count : 24; 41 } 42 else if (header.ddpf.flags & Header.DDPF.Flags.DDPF_FOURCC){ 43 // FourCC compression: http://www.buckarooshangar.com/flightgear/tut_dds.html 44 assert(header.ddpf.rgb_bit_count == 0, "Bad header.ddpf.rgb_bit_count value"); 45 switch(header.ddpf.four_cc) with(header.ddpf.FourCC){ 46 case DXT1: 47 blockSize = 8; 48 break; 49 case DXT3: 50 blockSize = 16; 51 break; 52 case DXT5: 53 blockSize = 16; 54 break; 55 case NONE, NONE2: 56 break; 57 default: assert(0, "FourCC '"~(cast(ubyte[])header.ddpf.four_cc).to!string~" not supported"); 58 } 59 bpp = 32; 60 } 61 else{ 62 enforce(false, "Unsupported DDS pixel format flags"); 63 } 64 65 auto w = header.width; 66 auto h = header.height; 67 68 mipmaps.length = header.mip_map_count == 0 ? 1 : header.mip_map_count; 69 foreach(ref image ; mipmaps){ 70 71 const len = header.ddpf.rgb_bit_count > 0 ? 72 (w * h * header.ddpf.rgb_bit_count / 8) 73 : (max(4, w) / 4 * max(4, h) / 4 * blockSize); 74 75 image = cr.readArray(len).dup; 76 77 w /= 2; 78 h /= 2; 79 } 80 81 assert(cr.bytesLeft == 0, "Remaining "~cr.bytesLeft.to!string~" bytes at the end of DDS data"); 82 } 83 /// 84 Header header; 85 86 private uint bpp, blockSize; 87 /// 88 ubyte[][] mipmaps; 89 90 /// Get a specifix pixel casted into a struct T. Size of T must match bytes per pixel. 91 ref T getPixel(T = ubyte[4])(in size_t x, in size_t y, uint mipmap = 0){ 92 enforce((header.ddpf.flags & header.ddpf.Flags.DDPF_FOURCC) == 0, "Not implemented for compressed DDS"); 93 94 assert(T.sizeof == bpp / 8, 95 format!"Pixel destination structure (%s, size=%d bits) does not match bit per pixel (%d)"(T.stringof, T.sizeof * 8, bpp) 96 ); 97 assert(mipmap < mipmaps.length, "Mip map out of bounds"); 98 99 //immutable w = header.mip_map_count / (2 ^^ mipmap); 100 //immutable h = header.mip_map_count / (2 ^^ mipmap); 101 immutable rowLength = ((header.width * (bpp / 8) + blockSize - 1) / blockSize) * blockSize; 102 103 return *cast(T*)&mipmaps[mipmap][rowLength * y + x * bpp / 8]; 104 } 105 106 /// 107 auto getPixelGrid(T = ubyte[4])(uint mipmap = 0){ 108 import std.range: chunks; 109 assert((header.ddpf.flags & header.ddpf.Flags.DDPF_FOURCC) == 0, "Not implemented for compressed DDS"); 110 111 assert(T.sizeof == bpp / 8, 112 format!"Pixel destination structure (%s, size=%d bits) does not match bit per pixel (%d)"(T.stringof, T.sizeof * 8, bpp) 113 ); 114 assert(mipmap < mipmaps.length, "Mip map out of bounds"); 115 116 immutable rowLength = ((header.width * (bpp / 8) + blockSize - 1) / blockSize) * blockSize; 117 118 return (cast(T[])(mipmaps[mipmap])).chunks(rowLength); 119 } 120 121 /// Behavior: 122 /// 1-byte color => grayscale 123 Bitmap!Pixel toBitmap(Pixel = ubyte[4])(uint mipmap = 0){ 124 import std.range: chunks; 125 assert((header.ddpf.flags & header.ddpf.Flags.DDPF_FOURCC) == 0, "Not implemented for compressed DDS"); 126 assert(Pixel.sizeof >= bpp / 8, "Pixel destination structure cannot b"); 127 assert(mipmap < mipmaps.length, "Mip map out of bounds"); 128 129 immutable rowLength = ((header.width * (bpp / 8) + blockSize - 1) / blockSize) * blockSize; 130 131 auto ret = Bitmap!Pixel(header.width, header.height); 132 133 //auto data = (cast(Pixel[])(mipmaps[mipmap])).chunks(rowLength); 134 foreach(y ; 0 .. header.height){ 135 const rowStart = rowLength * y; 136 foreach(x ; 0 .. header.width){ 137 uint sourceSize = bpp / 8; 138 auto pixData = mipmaps[mipmap][rowStart + sourceSize * x .. rowStart + sourceSize * (x + 1)]; 139 switch(sourceSize){ 140 case 1: 141 // Grayscale 142 static if (Pixel.sizeof == 1) ret[x, y] = *cast(Pixel*)[pixData[0]].ptr; 143 else static if(Pixel.sizeof == 2) ret[x, y] = *cast(Pixel*)[pixData[0], 0xff].ptr; 144 else static if(Pixel.sizeof == 3) ret[x, y] = *cast(Pixel*)[pixData[0], pixData[0], pixData[0]].ptr; 145 else static if(Pixel.sizeof == 4) ret[x, y] = *cast(Pixel*)[pixData[0], pixData[0], pixData[0], 0xff].ptr; 146 else enforce(0, format!"Unsupported %d-bytes color to %d-bytes bitmap"(bpp, Pixel.sizeof)); 147 break; 148 case 2: 149 // Grayscale + alpha 150 static if (Pixel.sizeof == 1) ret[x, y] = *cast(Pixel*)[pixData[0]].ptr; 151 else static if(Pixel.sizeof == 2) ret[x, y] = *cast(Pixel*)[pixData[0], pixData[1]].ptr; 152 else static if(Pixel.sizeof == 3) ret[x, y] = *cast(Pixel*)[pixData[0], pixData[0], pixData[0]].ptr; 153 else static if(Pixel.sizeof == 4) ret[x, y] = *cast(Pixel*)[pixData[0], pixData[0], pixData[0], pixData[1]].ptr; 154 else enforce(0, format!"Unsupported %d-bytes color to %d-bytes bitmap"(bpp, Pixel.sizeof)); 155 break; 156 case 3: 157 // BGR 158 static if (Pixel.sizeof == 1) ret[x, y] = *cast(Pixel*)[((pixData[0] + pixData[1] + pixData[2]) / 3).to!ubyte].ptr; 159 else static if(Pixel.sizeof == 2) ret[x, y] = *cast(Pixel*)[((pixData[0] + pixData[1] + pixData[2]) / 3).to!ubyte, 0xff].ptr; 160 else static if(Pixel.sizeof == 3) ret[x, y] = *cast(Pixel*)[pixData[2], pixData[1], pixData[0]].ptr; 161 else static if(Pixel.sizeof == 4) ret[x, y] = *cast(Pixel*)[pixData[2], pixData[1], pixData[0], 0xff].ptr; 162 else enforce(0, format!"Unsupported %d-bytes color to %d-bytes bitmap"(bpp, Pixel.sizeof)); 163 break; 164 case 4: 165 // BGRA 166 static if (Pixel.sizeof == 1) ret[x, y] = *cast(Pixel*)[(pixData[0] + pixData[1] + pixData[2]) / 3].ptr; 167 else static if(Pixel.sizeof == 2) ret[x, y] = *cast(Pixel*)[(pixData[0] + pixData[1] + pixData[2]) / 3, pixData[3]].ptr; 168 else static if(Pixel.sizeof == 3) ret[x, y] = *cast(Pixel*)[pixData[2], pixData[1], pixData[0]].ptr; 169 else static if(Pixel.sizeof == 4) ret[x, y] = *cast(Pixel*)[pixData[2], pixData[1], pixData[0], pixData[3]].ptr; 170 else enforce(0, format!"Unsupported %d-bytes color to %d-bytes bitmap"(bpp, Pixel.sizeof)); 171 break; 172 default: enforce(0, format!"Unsupported DDS %d-bytes color"(bpp)); 173 } 174 } 175 } 176 177 return ret; 178 } 179 180 /// 181 static align(1) struct Header{ 182 static assert(this.sizeof == 124); 183 align(1): 184 185 /// Size of the header struct. Always 124 186 uint32_t size; 187 /// 188 enum Flags{ 189 DDSD_CAPS = 0x1,/// Required in every .dds file. 190 DDSD_HEIGHT = 0x2,/// Required in every .dds file. 191 DDSD_WIDTH = 0x4,/// Required in every .dds file. 192 DDSD_PITCH = 0x8,/// Required when pitch is provided for an uncompressed texture. 193 DDSD_PIXELFORMAT = 0x1000,/// Required in every .dds file. 194 DDSD_MIPMAPCOUNT = 0x20000,/// Required in a mipmapped texture. 195 DDSD_LINEARSIZE = 0x80000,/// Required when pitch is provided for a compressed texture. 196 DDSD_DEPTH = 0x800000,/// Required in a depth texture. 197 } 198 uint32_t flags;/// See `Flags` 199 uint32_t height;/// Height in pixels 200 uint32_t width;/// Width in pixels 201 uint32_t linear_size;/// The pitch or number of bytes per scan line in an uncompressed texture; the total number of bytes in the top level texture for a compressed texture. 202 uint32_t depth;/// Depth of a volume texture (in pixels), otherwise unused. 203 uint32_t mip_map_count;/// Number of mipmap levels, otherwise unused. 204 uint32_t[11] _reserved1;/// Unused 205 /// DirectDraw Pixel Format 206 static struct DDPF { 207 static assert(this.sizeof == 32); 208 align(1): 209 uint32_t size;/// Structure size; set to 32 (bytes). 210 /// 211 enum Flags{ 212 DDPF_ALPHAPIXELS = 0x1,/// Texture contains alpha data; dwRGBAlphaBitMask contains valid data. 213 DDPF_ALPHA = 0x2,/// Used in some older DDS files for alpha channel only uncompressed data (dwRGBBitCount contains the alpha channel bitcount; dwABitMask contains valid data) 214 DDPF_FOURCC = 0x4,/// Texture contains compressed RGB data; dwFourCC contains valid data. 215 DDPF_RGB = 0x40,/// Texture contains uncompressed RGB data; dwRGBBitCount and the RGB masks (dwRBitMask, dwGBitMask, dwBBitMask) contain valid data. 216 DDPF_YUV = 0x200,/// Used in some older DDS files for YUV uncompressed data (dwRGBBitCount contains the YUV bit count; dwRBitMask contains the Y mask, dwGBitMask contains the U mask, dwBBitMask contains the V mask) 217 DDPF_LUMINANCE = 0x20000,/// Used in some older DDS files for single channel color uncompressed data (dwRGBBitCount contains the luminance channel bit count; dwRBitMask contains the channel mask). Can be combined with DDPF_ALPHAPIXELS for a two channel DDS file. 218 } 219 uint32_t flags;/// See Flags 220 /// 221 enum FourCC: char[4]{ 222 DXT1 = "DXT1", 223 DXT2 = "DXT2", 224 DXT3 = "DXT3", 225 DXT4 = "DXT4", 226 DXT5 = "DXT5", 227 DX10 = "DX10", 228 NONE = "\0\0\0\0", 229 NONE2 = "t\0\0\0", 230 } 231 FourCC four_cc;/// Four-character codes for specifying compressed or custom formats. Possible values include: DXT1, DXT2, DXT3, DXT4, or DXT5. 232 uint32_t rgb_bit_count;/// Number of bits in an RGB (possibly including alpha) format. Valid when dwFlags includes DDPF_RGB, DDPF_LUMINANCE, or DDPF_YUV. 233 uint32_t r_bit_mask;/// Red (or lumiannce or Y) mask for reading color data. For instance, given the A8R8G8B8 format, the red mask would be 0x00ff0000. 234 uint32_t g_bit_mask;/// Green (or U) mask for reading color data. For instance, given the A8R8G8B8 format, the green mask would be 0x0000ff00. 235 uint32_t b_bit_mask;/// Blue (or V) mask for reading color data. For instance, given the A8R8G8B8 format, the blue mask would be 0x000000ff. 236 uint32_t a_bit_mask;/// Alpha mask for reading alpha data. dwFlags must include DDPF_ALPHAPIXELS or DDPF_ALPHA. For instance, given the A8R8G8B8 format, the alpha mask would be 0xff000000. 237 238 string toString() const { 239 return format!"Dds.DDPF(flags=%s four_cc=%s rgb_bit_count=%d rgba_mask=%08x;%08x;%08x;%08x)"( 240 flags.flagsToString!Flags, four_cc, rgb_bit_count, r_bit_mask, g_bit_mask, b_bit_mask, a_bit_mask 241 ); 242 } 243 } 244 DDPF ddpf;/// See `DDPF` 245 /// caps values 246 enum CapsFlags{ 247 DDSCAPS_COMPLEX = 0x8,/// Optional; must be used on any file that contains more than one surface (a mipmap, a cubic environment map, or mipmapped volume texture). 248 DDSCAPS_MIPMAP = 0x400000,/// Optional; should be used for a mipmap. 249 DDSCAPS_TEXTURE = 0x1000,/// Required 250 } 251 uint32_t caps;/// See CapsFlags 252 /// caps2 values 253 enum Caps2Flags{ 254 DDSCAPS2_CUBEMAP = 0x200,/// Required for a cube map. 255 DDSCAPS2_CUBEMAP_POSITIVEX = 0x400,/// Required when these surfaces are stored in a cube map. 256 DDSCAPS2_CUBEMAP_NEGATIVEX = 0x800,/// Required when these surfaces are stored in a cube map. 257 DDSCAPS2_CUBEMAP_POSITIVEY = 0x1000,/// Required when these surfaces are stored in a cube map. 258 DDSCAPS2_CUBEMAP_NEGATIVEY = 0x2000,/// Required when these surfaces are stored in a cube map. 259 DDSCAPS2_CUBEMAP_POSITIVEZ = 0x4000,/// Required when these surfaces are stored in a cube map. 260 DDSCAPS2_CUBEMAP_NEGATIVEZ = 0x8000,/// Required when these surfaces are stored in a cube map. 261 DDSCAPS2_VOLUME = 0x200000,/// Required for a volume texture. 262 } 263 uint32_t caps2;/// See Caps2Flags 264 uint32_t[3] _reserved2;/// Unused 265 266 string toString() const { 267 return format!"Dds.Header(flags=%s size=%dx%d linear_size=%d depth=%d mip_map_count=%d caps=%s caps2=%s DDPF=%s)"( 268 flags.flagsToString!Flags, width, height, linear_size, depth, mip_map_count, caps.flagsToString!CapsFlags, caps2.flagsToString!Caps2Flags, ddpf 269 ); 270 } 271 } 272 } 273 274 275 276 unittest{ 277 enum names = [ 278 "dds_test_rgba.dds", 279 "dds_test_grayscale.dds", 280 "dds_test_rgba_dxt5.dds", 281 "PLC_MC_Auril.dds", 282 "PLC_MC_Auril_n.dds" 283 ]; 284 static foreach(i, name ; names){ 285 { 286 auto dds = new Dds(cast(ubyte[])import(name)); 287 static if(i == 0){ 288 // BGRA 289 foreach(j ; 0 .. 3){ 290 assert(dds.getPixel(0, j)[].equal([0,0,255,255]), name); 291 assert(dds.getPixel(1, j)[].equal([0,255,0,255]), name); 292 assert(dds.getPixel(2, j)[].equal([255,0,0,255]), name); 293 assert(dds.getPixel(3, j)[].equal([255,255,255,255]), name); 294 assert(dds.getPixel(4, j)[].equal([255,255,255,0]), name); 295 assert(dds.getPixel(5, j)[].equal([0,0,0,255]), name); 296 } 297 298 auto bitmap = dds.toBitmap!(ubyte[4])(); 299 foreach(y ; 0 .. dds.header.height){ 300 foreach(x ; 0 .. dds.header.width){ 301 auto p = dds.getPixel(x, y); 302 303 // BGRA => RGBA 304 assert(p[0] == bitmap[x, y][2], name); 305 assert(p[1] == bitmap[x, y][1], name); 306 assert(p[2] == bitmap[x, y][0], name); 307 assert(p[3] == bitmap[x, y][3], name); 308 } 309 } 310 } 311 else static if(i == 1){ 312 assert(dds.getPixel!ubyte(16, 16) == 0, name); 313 assert(dds.getPixel!ubyte(30, 30) == 255, name); 314 assert(dds.getPixel!ubyte(69, 80) == 0, name); 315 assert(dds.getPixel!ubyte(108, 100) == 255, name); 316 317 auto bitmap = dds.toBitmap!(ubyte[1])(); 318 foreach(y ; 0 .. dds.header.height){ 319 foreach(x ; 0 .. dds.header.width){ 320 assert(dds.getPixel!ubyte(x, y) == bitmap[x, y][0], name); 321 } 322 } 323 } 324 325 } 326 } 327 }