1 /// DirectDraw Surfaces (DDS files)
2 module nwnlibd.bitmap;
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 
15 alias BitmapGrayscale = Bitmap!(ubyte[1]);
16 alias BitmapGrayscaleA = Bitmap!(ubyte[2]);
17 alias BitmapRGB = Bitmap!(ubyte[3]);
18 alias BitmapRGBA = Bitmap!(ubyte[4]);
19 
20 /// 2D bitmap. Each pixel is coded with a fixed number of bytes.
21 ///
22 /// Pixel sizes:
23 ///   ubyte[1] => grayscale
24 ///   ubyte[2] => grayscale + alpha
25 ///   ubyte[3] => RGB
26 ///   ubyte[4] => RGBA
27 struct Bitmap(Pixel = ubyte[4]) {
28 	static assert(isStaticArray!Pixel || isIntegral!Pixel, "Bitmap can only contain a static array or a single integers");
29 
30 	this(size_t width, size_t height){
31 		this.width = width;
32 		this.height = height;
33 		pixels.length = width * height;
34 	}
35 
36 	/// Access a given pixel with Bitmap[x, y]
37 	ref inout(Pixel) opIndex(in size_t x, in size_t y) inout {
38 		return pixels[y * width + x];
39 	}
40 
41 	///// Rotate the image by increments of 90° counter clockwise
42 	//void rotate(int rot) {
43 	//	rot = ((abs(rot / 4) + 1) * 4 + rot) % 4;
44 
45 	//	auto newPixels = new Pixel[pixels.length];
46 
47 	//	final switch(rot){
48 	//		case 0:
49 	//			return;
50 	//		case 1:
51 	//			foreach(y ; 0 .. height)
52 	//				foreach(x ; 0 .. width)
53 	//					newPixels[y * width + x] = this[]
54 
55 
56 	//	}
57 
58 	//}
59 
60 	/// Serialize to a BMP file. Alpha data is lost.
61 	ubyte[] toBMP(){
62 		import std.outbuffer;
63 		auto buf = new OutBuffer();
64 
65 		//header
66 		buf.write(cast(char[2])"BM");
67 		buf.write(cast(uint32_t)0);//will be filled later
68 		buf.write(cast(uint16_t)0);
69 		buf.write(cast(uint16_t)0);
70 		buf.write(cast(uint32_t)(54));
71 		assert(buf.offset == 14);
72 
73 		//DIB header
74 		buf.write(cast(uint32_t)40);
75 		buf.write(cast(int32_t)width);
76 		buf.write(cast(int32_t)height);//height
77 		buf.write(cast(uint16_t)1);
78 		buf.write(cast(uint16_t)24);//bits per pixel
79 		buf.write(cast(uint32_t)0);
80 		buf.write(cast(uint32_t)0);//size of the pixel array, 0 = auto
81 		buf.write(cast(uint32_t)500);//dpi
82 		buf.write(cast(uint32_t)500);//dpi
83 		buf.write(cast(uint32_t)(0));//colors in color palette
84 		buf.write(cast(uint32_t)0);
85 		assert(buf.offset == 54);
86 
87 		ubyte[] padding;
88 		padding.length = ((width * 3 + 3) / 4) * 4 - width * 3;
89 		padding[] = 0;
90 
91 		foreach_reverse(y ; 0 .. height){
92 			foreach(x ; 0 .. width){
93 				static if(isStaticArray!Pixel){
94 					static if     (Pixel.sizeof == 1 || Pixel.sizeof == 2)
95 						buf.write([this[x, y][0], this[x, y][0], this[x, y][0]]);
96 					else static if(Pixel.sizeof >= 3)
97 						buf.write(this[x, y][0 .. 3]);
98 				}
99 				else
100 					buf.write(this[x, y]);
101 			}
102 			buf.write(padding);
103 		}
104 
105 		// re-write file size
106 		const oldOffset = buf.offset;
107 		buf.offset = 2;
108 		buf.write(cast(uint32_t)oldOffset);
109 		buf.offset = oldOffset;
110 
111 		return buf.toBytes();
112 	}
113 
114 	/// Serialize to a PNG file
115 	ubyte[] toPNG(){
116 		import std.zlib: compress, crc32;
117 		import std.outbuffer: OutBuffer;
118 		import std.bitmanip;
119 		//import std.digest.crc: crc32Of;
120 
121 		auto buf = new OutBuffer();
122 
123 		void writeChunk(T...)(char[4] type, T data){
124 			template ChunkLen(T...){
125 				uint32_t ChunkLen(T data){
126 					uint32_t len = 0;
127 					static foreach(d ; data){
128 						static if(isDynamicArray!(typeof(d)))
129 							len += ForeachType!(typeof(d)).sizeof * d.length;
130 						else
131 							len += d.sizeof;
132 					}
133 					return len;
134 				}
135 			}
136 			buf.write(ChunkLen(data).nativeToBigEndian);
137 			const crcStart = buf.offset;
138 			buf.write(type);
139 			static foreach(d ; data)
140 				buf.write(d);
141 			buf.write(crc32(0, buf.toBytes()[crcStart .. buf.offset]).nativeToBigEndian);
142 		}
143 
144 		buf.write("\x89PNG\r\n\x1A\n");
145 
146 		static if(isStaticArray!Pixel)
147 			ubyte depth = ForeachType!Pixel.sizeof * 8;
148 		else
149 			ubyte depth = 8;
150 		ubyte colorType;
151 		switch(Pixel.sizeof){
152 			case 1: colorType = 0b0000; break; // Grayscale
153 			case 2: colorType = 0b0100; break; // Grayscale + A
154 			case 3: colorType = 0b0010; break; // RGB
155 			case 4: colorType = 0b0110; break; // RGBA
156 			default: assert(0, format!"Cannot convert %d-bits colors to PNG"(Pixel.sizeof * 8));
157 		}
158 
159 
160 		writeChunk("IHDR",
161 			width.to!uint32_t.nativeToBigEndian,
162 			height.to!uint32_t.nativeToBigEndian,
163 			depth,// depth
164 			cast(ubyte)colorType,// color type
165 			cast(ubyte)0,// compression
166 			cast(ubyte)0,// filter
167 			cast(ubyte)0,// interlace
168 		);
169 
170 
171 		auto uncompIdat = new OutBuffer();
172 		foreach(y ; 0 .. height){
173 			uncompIdat.write(cast(ubyte)0);
174 			uncompIdat.write(cast(ubyte[])(pixels[y * width .. (y + 1) * width]));
175 		}
176 
177 		writeChunk("IDAT",
178 			uncompIdat.toBytes().compress(0)
179 		);
180 
181 
182 		writeChunk("IEND");
183 
184 		return buf.toBytes();
185 	}
186 
187 
188 	size_t width, height;
189 	Pixel[] pixels;
190 }
191 
192 unittest{
193 	import std.file;
194 	auto bmp = BitmapRGBA(8, 8);
195 
196 	ubyte[4] GetColor(size_t i){
197 		switch(i % 8){
198 			case 0: return [255, 0,   0,   255];
199 			case 1: return [255, 255, 0,   255];
200 			case 2: return [0,   255, 0,   255];
201 			case 3: return [0,   255, 255, 255];
202 			case 4: return [0,   0,   255, 255];
203 			case 5: return [255, 0,   255, 255];
204 			case 6: return [0,   0,   0,   255];
205 			case 7: return [255, 255, 255, 255];
206 			default: assert(0);
207 		}
208 	}
209 
210 	foreach(i ; 0 .. 8){
211 		bmp[i, i] = GetColor(i);
212 		bmp[i, 0] = GetColor(i);
213 	}
214 
215 	std.file.write("test.png", bmp.toPNG());
216 }