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 < = > < index > value < / index > 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 }