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 = 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 }