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 = cast(int)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,
242 			string palette = null){
243 		size_t o = rawData0.length;
244 		rawData0 ~= data;
245 		offset ~= o;
246 		iX ~= x;
247 		iY ~= y;
248 		bitmapID ~= ID;
249 		bitdepth ~= bitDepth;
250 		length ~= data.length;
251 		this.format ~= format;
252 		paletteMode ~= palette;
253 	}
254 	/// Adds a bitmap to the file (16bit).
255 	public void addBitmap(ushort[] data, int x, int y, string bitDepth, string ID, string format = null,
256 			string palette = null){
257 		const size_t o = rawData0.length;
258 		rawData0 ~= cast(void[])data;
259 		offset ~= o;
260 		iX ~= x;
261 		iY ~= y;
262 		bitmapID ~= ID;
263 		bitdepth ~= bitDepth;
264 		length ~= data.length * 2;
265 		this.format ~= format;
266 		paletteMode ~= palette;
267 	}
268 	/// Adds a bitmap to the file (4bit, 8bit or 32bit).
269 	/*public void addBitmap(ubyte[] data, int x, int y, string bitDepth, string ID, string format = null,
270 			string palette = null){
271 		const size_t o = rawData0.length;
272 		rawData0 ~= cast(void[])data;
273 		offset ~= o;
274 		iX ~= x;
275 		iY ~= y;
276 		bitmapID ~= ID;
277 		bitdepth ~= bitDepth;
278 		length ~= data.length;
279 		this.format ~= format;
280 		paletteMode ~= palette;
281 	}*/
282 	/// Adds a palette to the file (32bit only, ARGB).
283 	public void addPalette(void[] data, string ID){
284 		if(paletteLength.get(ID, -1)==-1){
285 			paletteOffset[ID] = rawData0.length;
286 			rawData0 ~= data;
287 			paletteLength[ID] = data.length;
288 		}else{
289 
290 		}
291 		//writeln(cast(ubyte[])data);
292 	}
293 	/// Removes the palette with the given ID.
294 	public void removePalette(string ID){
295 		removeRangeFromBinary(paletteOffset[ID],paletteLength[ID]);
296 		paletteLength.remove(ID);
297 		paletteOffset.remove(ID);
298 	}
299 	/// Gets the bitmap with the given ID (all formats).
300 	public void[] getBitmap(string ID){
301 		int pitch;
302 		int n = searchForID(ID);
303 		switch(bitdepth[n]){
304 			case "1bit": pitch = 1; break;
305 			case "4bit": pitch = 4; break;
306 			case "8bit": pitch = 8; break;
307 			case "16bit": pitch = 16; break;
308 			case "32bit": pitch = 32; break;
309 			default: break;
310 		}
311 
312 		int l = iX[n]*iY[n];
313 
314 		if(pitch == 1){
315 			BitArray ba = BitArray(rawData0[offset[n]..offset[n]+l], l/8);
316 			return cast(void[])ba;
317 		}else if(pitch == 4){
318 			l/=2;
319 		}else{
320 			l/=pitch/8;
321 		}
322 		return rawData0[offset[n]..offset[n]+l];
323 	}
324 	public void[] getBitmapRaw(int n){
325 		int pitch;
326 		//int n = searchForID(ID);
327 		switch(bitdepth[n]){
328 			case "1bit": pitch = 1; break;
329 			case "4bit": pitch = 4; break;
330 			case "8bit": pitch = 8; break;
331 			case "16bit": pitch = 16; break;
332 			case "32bit": pitch = 32; break;
333 			default: break;
334 		}
335 
336 		int l = iX[n]*iY[n]*(pitch/8);
337 		if(pitch == 1){
338 			BitArray ba = BitArray(rawData0[offset[n]..offset[n]+l], l);
339 			return cast(void[])ba;
340 		}
341 		return rawData0[offset[n]..offset[n]+l];
342 	}
343 	public ubyte[] getBitmap(int n){
344 		int pitch;
345 		//int n = searchForID(ID);
346 		/*switch(bitdepth[n]){
347 			case "1bit": pitch = 1; break;
348 			case "8bit": pitch = 8; break;
349 			case "16bit": pitch = 16; break;
350 			case "32bit": pitch = 32; break;
351 			default: break;
352 		}*/
353 
354 		//int l = (iX[n]*iY[n]*pitch)/8;
355 		/*if(pitch == 1){
356 			BitArray ba = BitArray(rawData0[offset[n]..offset[n]+l], l);
357 			return cast(void[])ba;
358 		}*/
359 		writeln(offset[n],',',offset[n]+length[n]);
360 		return cast(ubyte[])(rawData0[offset[n]..offset[n]+length[n]]);
361 	}
362 	/// Gets the bitmap with the given ID (8bit).
363 	public ubyte[] get8bitBitmap(string ID){
364 		int n = searchForID(ID);
365 		//int l = iX[n]*iY[n];
366 		return cast(ubyte[])rawData0[offset[n]..offset[n]+length[n]];
367 	}
368 	/// Gets the bitmap with the given ID (16bit or 8bit Huffman encoded).
369 	public ushort[] get16bitBitmap(string ID){
370 		int n = searchForID(ID);
371 		//int l = iX[n]*iY[n];
372 		ushort[] d;
373 		if(bitdepth[n] == "16bit"){
374 
375 		}else{
376 			if(dataReplacer.get(ID,null) is null){
377 				for(int i ; i < length[n]; i++){
378 					d ~= *cast(ubyte*)(rawData0.ptr + offset[n] + i);
379 				}
380 			}else{
381 				d = dataReplacer[ID].decodeBitmap(cast(ubyte[])rawData0[offset[n]..offset[n]+length[n]]);
382 			}
383 		}
384 		return d;
385 	}
386 	/// Removes the bitmap from the file by ID.
387 	public void removeBitmap(string ID){
388 		import std.algorithm.mutation;
389 		int i = searchForID(ID);
390 		removeRangeFromBinary(offset[i],length[i]);
391 		offset = remove(offset, i);
392 		length = remove(length, i);
393 		paletteMode = remove(paletteMode, i);
394 		bitdepth = remove(bitdepth, i);
395 		format = remove(format, i);
396 		bitmapID = remove(bitmapID, i);
397 		iX = remove(iX, i);
398 		iY = remove(iY, i);
399 	}
400 	/// Removes the bitmap from the file by index.
401 	public void removeBitmap(int i){
402 		import std.algorithm.mutation;
403 		removeRangeFromBinary(offset[i],length[i]);
404 		offset = remove(offset, i);
405 		length = remove(length, i);
406 		paletteMode = remove(paletteMode, i);
407 		bitdepth = remove(bitdepth, i);
408 		format = remove(format, i);
409 		bitmapID = remove(bitmapID, i);
410 		iX = remove(iX, i);
411 		iY = remove(iY, i);
412 	}
413 	/// Returns the palette with the given ID.
414 	public void[] getPalette(string ID){
415 		if(paletteLength.get(ID, -1) == -1){
416 			ID = "default";
417 		}
418 		return rawData0[paletteOffset[ID]..(paletteOffset[ID]+paletteLength[ID])];
419 	}
420 	/// Returns the palette for the bitmap if exists.
421 	public string getPaletteMode(string ID){
422 		return paletteMode[searchForID(ID)];
423 	}
424 	/// Returns the X size by ID.
425 	public int getXsize(string ID){
426 		return iX[searchForID(ID)];
427 	}
428 	/// Returns the X size by number.
429 	public int getXsize(int i){
430 		return iX[i];
431 	}
432 	/// Returns the Y size by ID.
433 	public int getYsize(string ID){
434 		return iY[searchForID(ID)];
435 	}
436 	/// Returns the X size by number.
437 	public int getYsize(int i){
438 		return iY[i];
439 	}
440 	/// Returns the bitdepth of the image.
441 	public string getBitDepth(string ID){
442 		return bitdepth[searchForID(ID)];
443 	}
444 	/// Returns the pixel format of the image.
445 	public string getFormat(string ID){
446 		return format[searchForID(ID)];
447 	}
448 	/// Returns true if file doesn't contain any images.
449 	public bool isEmpty(){
450 		return (bitmapID.length == 0);
451 	}
452 	/// Removes a given range from the binary field.
453 	private void removeRangeFromBinary(size_t offset, size_t length){
454 		if(length == 0){
455 			return;
456 		}
457 		if(offset == 0){
458 			rawData0 = rawData0[length..rawData0.length];
459 		}else if(offset + length == rawData0.length){
460 			rawData0 = rawData0[0..offset];
461 		}else{
462 			rawData0 = rawData0[0..offset] ~ rawData0[(offset+length)..rawData0.length];
463 		}
464 		foreach(string s ; paletteOffset.byKey){
465 			if(paletteOffset[s] > offset){
466 				paletteOffset[s] -= length;
467 			}
468 		}
469 		for (int i ; i < bitmapID.length ; i++){
470 			if(this.offset[i] > offset){
471 				this.offset[i] -= length;
472 			}
473 		}
474 	}
475 }
476 /**
477 * Does a Huffman encoding/decoding to convert between 8bit and 16bit.
478 */
479 
480 public class ReplaceData{
481 	ubyte[] src;
482 	ushort[] dest;
483 	this(){
484 
485 	}
486 	/// Adds a new replaceattribute
487 	void addReplaceAttr(ubyte f, ushort t){
488 		this.src ~= f;
489 		this.dest ~= t;
490 	}
491 	/// Decodes bitmap with the preprogrammed dictionary.
492 	ushort[] decodeBitmap(ubyte[] data){
493 		ushort[] result;
494 		result.length = data.length;
495 		for(int i; i < data.length; i++){
496 			result[i] = lookupForDecoding(data[i]);
497 		}
498 		return result;
499 	}
500 	/// Decodes bitmap with the preprogrammed dictionary.
501 	ubyte[] encodeBitmap(ushort[] data){
502 		ubyte[] result;
503 		result.length = data.length;
504 		for(int i; i < data.length; i++){
505 			result[i] = lookupForEncoding(data[i]);
506 		}
507 		return result;
508 	}
509 
510 	private ushort lookupForDecoding(ubyte b){
511 		for(int i; i < src.length; i++){
512 			if(src[i] == b){
513 				return dest[i];
514 			}
515 		}
516 		return b;
517 	}
518 
519 	private ubyte lookupForEncoding(ushort s){
520 		for(int i; i < src.length; i++){
521 			if(dest[i] == s){
522 				return src[i];
523 			}
524 		}
525 		return to!ubyte(s);
526 	}
527 
528 }
529 
530 public enum ExtBMPFlags : uint{
531 	CompressionMethodNull	=	1,		///No compression.
532 	CompressionMethodZLIB	=	2,		///Compression using DEFLATE.
533 	CompressionMethodLZMA	=	3,		///Compression using Lempel-Zif-Markov algorithm.
534 	CompressionMethodLZHAM	=	4,
535 	LongHeader              =   16,		///For headers over 2 gigabyte.
536 	LongFile                =   32		///For files over 2 gigabyte.
537 	/*ZLIBCompressionLevel0 = 16,
538 	ZLIBCompressionLevel1 = 32,
539 	ZLIBCompressionLevel2 = 48,
540 	ZLIBCompressionLevel3 = 64,
541 	ZLIBCompressionLevel4 = 80,
542 	ZLIBCompressionLevel5 = 96,
543 	ZLIBCompressionLevel6 = 112,
544 	ZLIBCompressionLevel7 = 128,
545 	ZLIBCompressionLevel8 = 144,
546 	ZLIBCompressionLevel9 = 160,*/
547 }