1 /*
2  * Copyright (C) 2015-2017, by Laszlo Szeremi under the Boost license.
3  *
4  * Pixel Perfect Engine, extbmp module
5  */
6 
7 module PixelPerfectEngine.extbmp.extbmp;
8 
9 import std.xml;
10 import std.bitmanip;
11 import std.stdio;
12 import std.zlib;
13 import std.conv;
14 import std.file;
15 
16 public import PixelPerfectEngine.extbmp.animation;
17 /**
18  * Proprietary image format for the engine. Mainly created to get around the lack of 16bit indexed image formats. 
19  * Stores most data in XML, binary is stored after the document. Most data is little endian, future versions might be able to specify big endian data in the binary, but due to
20  * the main targets (x86 and ARM) are mainly little endian, it's unlikely.
21  */
22 public class ExtendibleBitmap{
23 	public AnimationData[string] animData;	///Stores animation data. See documentation of AnimationData for more information.
24 	private void[] rawData, rawData0;		///The binary workfield.
25 	private string filename;				///Stores the current filename.
26 	private string[string] metaData;		///Stores metadata. Serialized as: [index] = value &lt = &gt &lt index &gt value &lt / index &gt
27 	private ReplaceData[string] dataReplacer;	///Datareplacers for the indexed 8bit bitmaps.
28 	private size_t[string] paletteOffset;	///The starting point of the palette.
29 	private size_t[string] paletteLength;	///The length of the palette.
30 	private int headerLength;		///The length of the header in bytes.
31 	private uint flags;				///See ExtBMPFlags for details.
32 	public string[] bitmapID;		///The ID of the bitmap. Used to identify the bitmaps in the file.
33 	public string[] bitdepth;		///Bitdepth of the given bitmap. Editing this might make the bitmap unusable.
34 	public string[] format;			///Format of the given bitmap. Editing this might make the bitmap unusable or cause graphic corruption.
35 	public string[] paletteMode;	///Palette of the given bitmap. Null if unindexed, default or the palette's name otherwise.
36 	private size_t[] offset;		///The starting point of the bitmap in the binary field.
37 	private int[] iX;			///The X size of the bitmap.
38 	private int[] iY;			///The X size of the bitmap.
39 	private size_t[] length;		///The size of the bitmap in the binary field in bytes.
40 	//private ushort[string] paletteOffset;
41 	/// Standard constructor for empty files.
42 	this(){
43 
44 	}
45 	/// Loads file from a binary.
46 	this(void[] data){
47 		rawData = data;
48 		flags = *cast(uint*)rawData.ptr;
49 		headerLength = *cast(int*)rawData.ptr + 4;
50 		if((flags & ExtBMPFlags.CompressionMethodNull) == ExtBMPFlags.CompressionMethodNull){
51 			headerLoad();
52 		}else if((flags & ExtBMPFlags.CompressionMethodZLIB) == ExtBMPFlags.CompressionMethodZLIB){
53 			rawData0 = uncompress(rawData[8..rawData.length]);
54 			rawData.length = 8;
55 			rawData ~= rawData0;
56 			rawData0.length = 0;
57 			headerLoad();
58 		}
59 		rawData0 = rawData[(9 + headerLength)..rawData.length];
60 		rawData.length = 0;
61 	}
62 	/// Loads file from file.
63 	this(string filename){
64 		this.filename = filename;
65 		loadFile();
66 	}
67 	///
68 	public @property string dir(){
69 		return filename;
70 	}
71 	/// Sets the filename 
72 	public void setFileName(string s){
73 		filename = s;
74 	}
75 	/// Loads the file specified in field "filename"
76 	public void loadFile(){
77 		//writeln(filename);
78 		try{
79 			rawData = std.file.read(filename);
80 			flags = *cast(uint*)rawData.ptr;
81 			headerLength = *cast(int*)(rawData.ptr + 4);
82 			//if((flags & ExtBMPFlags.CompressionMethodNull) == ExtBMPFlags.CompressionMethodNull){
83 			headerLoad();
84 			/*}else if((flags & ExtBMPFlags.CompressionMethodZLIB) == ExtBMPFlags.CompressionMethodZLIB){
85 				rawData0 = uncompress(rawData[8..rawData.length-1]);
86 				rawData.length = 8;
87 				rawData ~= rawData0;
88 				rawData0.length = 0;
89 				headerLoad();
90 			}*/
91 			
92 			if(rawData.length > 8 + headerLength){
93 				rawData0 = rawData[8 + headerLength..rawData.length];
94 			}
95 			rawData.length = 0;
96 			//writeln(cast(string)rawData0);
97 		}catch(Exception e){
98 			writeln(e.toString);
99 		}
100 	}
101 	/// Saves the file to the place specified in field "filename".
102 	public void saveFile(){
103 		saveFile(filename);
104 	}
105 	/// Saves the file to the given location.
106 	public void saveFile(string f){
107 		//writeln(f);
108 		try{
109 			rawData.length=8;
110 			*cast(uint*)rawData.ptr = flags;
111 			headerSave();
112 			
113 			*cast(int*)(rawData.ptr+4) = headerLength;
114 			rawData ~= rawData0;
115 			//File file = File(f, "w");
116 			//file.rawWrite(rawData);
117 
118 			std.file.write(f, rawData);
119 		}catch(Exception e){
120 			writeln(e.toString);
121 		}
122 		rawData.length = 0;
123 	}
124 	/// Deserializes the header data.
125 	private void headerLoad(){
126 		string s = cast(string)rawData[8..8 + headerLength];
127 		//writeln(s);
128 		Document d = new Document(s);
129 		foreach(Element e1; d.elements){
130 			if(e1.tag.name == "MetaData"){
131 				//writeln("MetaData found");
132 				foreach(Element e2; e1.elements){
133 					metaData[e2.tag.name] = e2.text;
134 				}
135 			}else if(e1.tag.name == "Bitmap"){
136 				//writeln("Bitmap found");
137 				bitmapID ~= e1.tag.attr["ID"];
138 				offset ~= to!int(e1.tag.attr["offset"]);
139 				iX ~= to!int(e1.tag.attr["sizeX"]);
140 				iY ~= to!int(e1.tag.attr["sizeY"]);
141 				length ~= to!int(e1.tag.attr["length"]);
142 				bitdepth ~= e1.tag.attr["bitDepth"];
143 				format ~= e1.tag.attr.get("format","");
144 				paletteMode ~= e1.tag.attr.get("paletteMode","");
145 				if(e1.tag.attr.get("format","") == "upconv"){
146 					dataReplacer[e1.tag.attr["ID"]] = new ReplaceData();
147 					foreach(Element e2; e1.elements){
148 						if(e1.tag.name == "ColorSwap"){
149 							dataReplacer[e1.tag.attr["ID"]].addReplaceAttr(to!ubyte(e2.tag.attr["from"]), to!ushort(e2.tag.attr["to"]));
150 						}
151 					}
152 				}
153 			}else if(e1.tag.name == "Palette"){
154 				//writeln("Palette found");
155 				//palettes[e1.tag.attr["ID"]] = PaletteData();
156 				paletteLength[e1.tag.attr["ID"]] = to!int(e1.tag.attr["length"]);
157 				//palettes[e1.tag.attr["ID"]].format = e1.tag.attr.get("format","");
158 				paletteOffset[e1.tag.attr["ID"]] = to!int(e1.tag.attr["offset"]);
159 			}else if(e1.tag.name == "AnimData"){
160 				animData[e1.tag.attr["ID"]] = AnimationData();
161 				foreach(Element e2; e1.elements){
162 					animData[e1.tag.attr["ID"]].addFrame(e2.tag.attr["ID"], to!int(e2.tag.attr["length"]));
163 				}
164 			}
165 		}
166 	}
167 	/// Serializes the header data.
168 	private void headerSave(){
169 		auto doc = new Document(new Tag("HEADER"));
170 		auto e0 = new Element("MetaData");
171 		foreach(string s; metaData.byKey()){
172 			e0 ~= new Element(s, metaData[s]);
173 		}
174 
175 		doc ~= e0;
176 		for(int i; i < bitmapID.length; i++){
177 			auto e1 = new Element("Bitmap");
178 			e1.tag.attr["ID"] = bitmapID[i];
179 			e1.tag.attr["offset"] = to!string(offset[i]);
180 			e1.tag.attr["sizeX"] = to!string(iX[i]);
181 			e1.tag.attr["sizeY"] = to!string(iY[i]);
182 			e1.tag.attr["bitDepth"] = bitdepth[i];
183 			e1.tag.attr["length"] = to!string(length[i]);
184 			if(format[i] != ""){
185 				e1.tag.attr["format"] = format[i];
186 			}
187 			/*if(paletteMode[i] != ""){
188 				e1.tag.attr["paletteMode"] = paletteMode[i];
189 			}*/
190 			if(dataReplacer.get(bitmapID[i], null) !is null){
191 				for(int j; j < dataReplacer[bitmapID[i]].src.length; j++){
192 					auto e2 = new Element("ColorSwap");
193 					e2.tag.attr["from"] = to!string(dataReplacer[bitmapID[i]].src[j]);
194 					e2.tag.attr["to"] = to!string(dataReplacer[bitmapID[i]].dest[j]);
195 					e1 ~= e2;
196 				}
197 			}
198 			doc ~= e1;
199 		}
200 
201 		foreach(string s; paletteLength.byKey()){
202 			auto e1 = new Element("Palette");
203 			e1.tag.attr["ID"] = s;
204 			e1.tag.attr["length"] = to!string(paletteLength[s]);
205 			e1.tag.attr["offset"] = to!string(paletteOffset[s]);
206 			/*if(palettes[s].format != "")
207 				e1.tag.attr["format"] = palettes[s].format;*/
208 			doc ~= e1;
209 		}
210 
211 		foreach(string s; animData.byKey()){
212 			auto e1 = new Element("AnimData");
213 			e1.tag.attr["ID"] = s;
214 			for(int i; i < animData[s].duration.length; i++){
215 				auto e2 = new Element("Frame");
216 				e2.tag.attr["ID"] = animData[s].ID[i];
217 				e2.tag.attr["length"] = to!string(animData[s].duration[i]);
218 				e1 ~= e2;
219 			}
220 			doc ~= e1;
221 		}
222 		string h = doc.toString();
223 		headerLength = h.length;
224 		//writeln(h);
225 		writeln(headerLength);
226 		rawData ~= cast(void[])h;
227 	}
228 	/// Returns the first instance of the ID.
229 	public int searchForID(string ID){
230 		for(int i; i < bitmapID.length; i++){
231 			if(bitmapID[i] == ID){
232 				return i;
233 			}
234 		}
235 		return -1;
236 	}
237 	public deprecated string[] getIDs(){
238 		return bitmapID;
239 	}
240 	/// Adds a bitmap to the file (any supported formats).
241 	public void addBitmap(void[] data, int x, int y, string bitDepth, string ID, string format = null, string palette = null){
242 		int o = rawData0.length;
243 		rawData0 ~= data;
244 		offset ~= o;
245 		iX ~= x;
246 		iY ~= y;
247 		bitmapID ~= ID;
248 		bitdepth ~= bitDepth;
249 		length ~= data.length;
250 		this.format ~= format;
251 		paletteMode ~= palette;
252 	}
253 	/// Adds a bitmap to the file (16bit).
254 	public void addBitmap(ushort[] data, int x, int y, string bitDepth, string ID, string format = null, string palette = null){
255 		int o = rawData0.length;
256 		rawData0 ~= cast(void[])data;
257 		offset ~= o;
258 		iX ~= x;
259 		iY ~= y;
260 		bitmapID ~= ID;
261 		bitdepth ~= bitDepth;
262 		length ~= data.length * 2;
263 		this.format ~= format;
264 	}
265 	/// Adds a bitmap to the file (4bit, 8bit or 32bit).
266 	public void addBitmap(ubyte[] data, int x, int y, string bitDepth, string ID, string format = null, string palette = null, ReplaceData rd = null){
267 		int o = rawData0.length;
268 		rawData0 ~= cast(void[])data;
269 		offset ~= o;
270 		iX ~= x;
271 		iY ~= y;
272 		bitmapID ~= ID;
273 		bitdepth ~= bitDepth;
274 		length ~= data.length;
275 		this.format ~= format;
276 
277 	}
278 	/// Adds a palette to the file (32bit only, ARGB).
279 	public void addPalette(void[] data, string ID){
280 		if(paletteLength.get(ID, -1)==-1){
281 			paletteOffset[ID] = rawData0.length;
282 			rawData0 ~= data;
283 			paletteLength[ID] = data.length;
284 		}else{
285 
286 		}
287 		writeln(cast(ubyte[])data);
288 	}
289 	/// Removes the palette with the given ID.
290 	public void removePalette(string ID){
291 		removeRangeFromBinary(paletteOffset[ID],paletteLength[ID]);
292 		paletteLength.remove(ID);
293 		paletteOffset.remove(ID);
294 	}
295 	/// Gets the bitmap with the given ID (all formats).
296 	public void[] getBitmap(string ID){
297 		int pitch;
298 		int n = searchForID(ID);
299 		switch(bitdepth[n]){
300 			case "1bit": pitch = 1; break;
301 			case "4bit": pitch = 4; break;
302 			case "8bit": pitch = 8; break;
303 			case "16bit": pitch = 16; break;
304 			case "32bit": pitch = 32; break;
305 			default: break;
306 		}
307 
308 		int l = iX[n]*iY[n];
309 
310 		if(pitch == 1){
311 			BitArray ba = BitArray(rawData0[offset[n]..offset[n]+l], l/8);
312 			return cast(void[])ba;
313 		}else if(pitch == 4){
314 			l/=2;
315 		}else{
316 			l/=pitch/8;
317 		}
318 		return rawData0[offset[n]..offset[n]+l];
319 	}
320 	public void[] getBitmapRaw(int n){
321 		int pitch;
322 		//int n = searchForID(ID);
323 		switch(bitdepth[n]){
324 			case "1bit": pitch = 1; break;
325 			case "4bit": pitch = 4; break;
326 			case "8bit": pitch = 8; break;
327 			case "16bit": pitch = 16; break;
328 			case "32bit": pitch = 32; break;
329 			default: break;
330 		}
331 		
332 		int l = iX[n]*iY[n]*(pitch/8);
333 		if(pitch == 1){
334 			BitArray ba = BitArray(rawData0[offset[n]..offset[n]+l], l);
335 			return cast(void[])ba;
336 		}
337 		return rawData0[offset[n]..offset[n]+l];
338 	}
339 	public ubyte[] getBitmap(int n){
340 		int pitch;
341 		//int n = searchForID(ID);
342 		/*switch(bitdepth[n]){
343 			case "1bit": pitch = 1; break;
344 			case "8bit": pitch = 8; break;
345 			case "16bit": pitch = 16; break;
346 			case "32bit": pitch = 32; break;
347 			default: break;
348 		}*/
349 
350 		//int l = (iX[n]*iY[n]*pitch)/8;
351 		/*if(pitch == 1){
352 			BitArray ba = BitArray(rawData0[offset[n]..offset[n]+l], l);
353 			return cast(void[])ba;
354 		}*/
355 		writeln(offset[n],',',offset[n]+length[n]);
356 		return cast(ubyte[])(rawData0[offset[n]..offset[n]+length[n]]);
357 	}
358 	/// Gets the bitmap with the given ID (8bit).
359 	public ubyte[] get8bitBitmap(string ID){
360 		int n = searchForID(ID);
361 		//int l = iX[n]*iY[n];
362 		return cast(ubyte[])rawData0[offset[n]..offset[n]+length[n]];
363 	}
364 	/// Gets the bitmap with the given ID (16bit or 8bit Huffman encoded).
365 	public ushort[] get16bitBitmap(string ID){
366 		int n = searchForID(ID);
367 		//int l = iX[n]*iY[n];
368 		ushort[] d;
369 		if(bitdepth[n] == "16bit"){
370 
371 		}else{
372 			if(dataReplacer.get(ID,null) is null){
373 				for(int i ; i < length[n]; i++){
374 					d ~= *cast(ubyte*)(rawData0.ptr + offset[n] + i);
375 				}
376 			}else{
377 				d = dataReplacer[ID].decodeBitmap(cast(ubyte[])rawData0[offset[n]..offset[n]+length[n]]);
378 			}
379 		}
380 		return d;
381 	}
382 	/// Removes the bitmap from the file by ID.
383 	public void removeBitmap(string ID){
384 		import std.algorithm.mutation;
385 		int i = searchForID(ID);
386 		removeRangeFromBinary(offset[i],length[i]);
387 		offset = remove(offset, i);
388 		length = remove(length, i);
389 		paletteMode = remove(paletteMode, i);
390 		bitdepth = remove(bitdepth, i);
391 		format = remove(format, i);
392 		bitmapID = remove(bitmapID, i);
393 		iX = remove(iX, i);
394 		iY = remove(iY, i);
395 	}
396 	/// Removes the bitmap from the file by index.
397 	public void removeBitmap(int i){
398 		import std.algorithm.mutation;
399 		removeRangeFromBinary(offset[i],length[i]);
400 		offset = remove(offset, i);
401 		length = remove(length, i);
402 		paletteMode = remove(paletteMode, i);
403 		bitdepth = remove(bitdepth, i);
404 		format = remove(format, i);
405 		bitmapID = remove(bitmapID, i);
406 		iX = remove(iX, i);
407 		iY = remove(iY, i);
408 	}
409 	/// Returns the palette with the given ID.
410 	public void[] getPalette(string ID){
411 		if(paletteLength.get(ID, -1) == -1){
412 			ID = "default";
413 		}
414 		return rawData0[paletteOffset[ID]..(paletteOffset[ID]+paletteLength[ID])];
415 	}
416 	/// Returns the palette for the bitmap if exists.
417 	public string getPaletteMode(string ID){
418 		return paletteMode[searchForID(ID)];
419 	}
420 	/// Returns the X size by ID.
421 	public int getXsize(string ID){
422 		return iX[searchForID(ID)];
423 	}
424 	/// Returns the X size by number.
425 	public int getXsize(int i){
426 		return iX[i];
427 	}
428 	/// Returns the Y size by ID.
429 	public int getYsize(string ID){
430 		return iY[searchForID(ID)];
431 	}
432 	/// Returns the X size by number.
433 	public int getYsize(int i){
434 		return iY[i];
435 	}
436 	/// Returns the bitdepth of the image.
437 	public string getBitDepth(string ID){
438 		return bitdepth[searchForID(ID)];
439 	}
440 	/// Returns the pixel format of the image.
441 	public string getFormat(string ID){
442 		return format[searchForID(ID)];
443 	}
444 	/// Returns true if file doesn't contain any images.
445 	public bool isEmpty(){
446 		return (bitmapID.length == 0);
447 	}
448 	/// Removes a given range from the binary field.
449 	private void removeRangeFromBinary(size_t offset, size_t length){
450 		if(length == 0){
451 			return;
452 		}
453 		if(offset == 0){
454 			rawData0 = rawData0[length..rawData0.length];
455 		}else if(offset + length == rawData0.length){
456 			rawData0 = rawData0[0..offset];
457 		}else{
458 			rawData0 = rawData0[0..offset] ~ rawData0[(offset+length)..rawData0.length];
459 		}
460 		foreach(string s ; paletteOffset.byKey){
461 			if(paletteOffset[s] > offset){
462 				paletteOffset[s] -= length;
463 			}
464 		}
465 		for (int i ; i < bitmapID.length ; i++){
466 			if(this.offset[i] > offset){
467 				this.offset[i] -= length;
468 			}
469 		}
470 	}
471 }
472 /**
473 * Does a Huffman encoding/decoding to convert between 8bit and 16bit.
474 */
475 
476 public class ReplaceData{
477 	ubyte[] src;
478 	ushort[] dest;
479 	this(){
480 		
481 	}
482 	/// Adds a new replaceattribute
483 	void addReplaceAttr(ubyte f, ushort t){
484 		this.src ~= f;
485 		this.dest ~= t;
486 	}
487 	/// Decodes bitmap with the preprogrammed dictionary.
488 	ushort[] decodeBitmap(ubyte[] data){
489 		ushort[] result;
490 		result.length = data.length;
491 		for(int i; i < data.length; i++){
492 			result[i] = lookupForDecoding(data[i]);
493 		}
494 		return result;
495 	}
496 	/// Decodes bitmap with the preprogrammed dictionary.
497 	ubyte[] encodeBitmap(ushort[] data){
498 		ubyte[] result;
499 		result.length = data.length;
500 		for(int i; i < data.length; i++){
501 			result[i] = lookupForEncoding(data[i]);
502 		}
503 		return result;
504 	}
505 
506 	private ushort lookupForDecoding(ubyte b){
507 		for(int i; i < src.length; i++){
508 			if(src[i] == b){
509 				return dest[i];
510 			}
511 		}
512 		return b;
513 	}
514 
515 	private ubyte lookupForEncoding(ushort s){
516 		for(int i; i < src.length; i++){
517 			if(dest[i] == s){
518 				return src[i];
519 			}
520 		}
521 		return to!ubyte(s);
522 	}
523 	
524 }
525 
526 public enum ExtBMPFlags : uint{
527 	CompressionMethodNull	=	1,		///No compression.
528 	CompressionMethodZLIB	=	2,		///Compression using DEFLATE.
529 	CompressionMethodLZMA	=	3,		///Compression using Lempel-Zif-Markov algorithm.
530 	CompressionMethodLZHAM	=	4,
531 	LongHeader              =   16,		///For headers over 2 gigabyte.
532 	LongFile                =   32		///For files over 2 gigabyte.
533 	/*ZLIBCompressionLevel0 = 16,
534 	ZLIBCompressionLevel1 = 32,
535 	ZLIBCompressionLevel2 = 48,
536 	ZLIBCompressionLevel3 = 64,
537 	ZLIBCompressionLevel4 = 80,
538 	ZLIBCompressionLevel5 = 96,
539 	ZLIBCompressionLevel6 = 112,
540 	ZLIBCompressionLevel7 = 128,
541 	ZLIBCompressionLevel8 = 144,
542 	ZLIBCompressionLevel9 = 160,*/
543 }