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 }