1 module dimage.tga; 2 3 import std.bitmanip; 4 static import std.stdio; 5 6 public import dimage.base; 7 8 import core.stdc..string; 9 10 /** 11 * Implements the Truevision Graphics bitmap file format (*.tga) with some extra capabilities at the cost 12 * of making them unusable in applications not implementing these features: 13 * <ul> 14 * <li>Capability of storing 1, 2, and 4 bit indexed images.</li> 15 * <li>External color map support.</li> 16 * <li>More coming soon such as more advanced compression methods.</li> 17 * </ul> 18 * Accessing developer area is fully implemented, accessing extension area is partly implemented. 19 */ 20 public class TGA : Image, ImageMetadata{ 21 protected TGAHeader header; 22 protected TGAFooter footer; 23 protected char[] imageID; 24 protected ExtArea[] extensionArea; 25 protected DevAreaTag[] developerAreaTags; 26 protected DevArea[] developerArea; 27 public uint[] scanlineTable; ///stores offset to scanlines 28 public ubyte[] postageStampImage; ///byte 0 is width, byte 1 is height 29 public ushort[] colorCorrectionTable; ///color correction table 30 /** 31 * Creates a TGA object without a footer. 32 */ 33 public this(TGAHeader header, ubyte[] imageData, ubyte[] paletteData = null, char[] imageID = null){ 34 this.header = header; 35 this.imageData = imageData; 36 this.paletteData = paletteData; 37 this.imageID = imageID; 38 } 39 /** 40 * Creates a TGA object with a footer. 41 */ 42 public this(TGAHeader header, TGAFooter footer, ubyte[] imageData, ubyte[] paletteData = null, char[] imageID = null){ 43 this.header = header; 44 this.footer = footer; 45 this.imageData = imageData; 46 this.paletteData = paletteData; 47 this.imageID = imageID; 48 } 49 /** 50 * Loads a Truevision TARGA file and creates a TGA object. 51 * FILE can be either std.stdio's file, my own implementation of a virtual file (see ziltoid1991/vfile), or any compatible solution. 52 */ 53 public static TGA load(FILE = std.stdio.File, bool loadDevArea = false, bool loadExtArea = false)(FILE file){ 54 import std.stdio; 55 ubyte[] loadRLEImageData(const ref TGAHeader header){ 56 size_t target = header.width * header.height; 57 const size_t bytedepth = (header.pixelDepth / 8); 58 target >>= header.pixelDepth < 8 ? 1 : 0; 59 target >>= header.pixelDepth < 4 ? 1 : 0; 60 target >>= header.pixelDepth < 2 ? 1 : 0; 61 ubyte[] result, dataBuffer; 62 result.reserve(header.pixelDepth >= 8 ? target * bytedepth : target); 63 switch(header.pixelDepth){ 64 case 16: 65 dataBuffer.length = 3; 66 break; 67 case 24: 68 dataBuffer.length = 4; 69 break; 70 case 32: 71 dataBuffer.length = 5; 72 break; 73 default: //all indexed type having less than 8bits of depth use the same method of RLE as 8bit ones 74 dataBuffer.length = 2; 75 while(target){ 76 file.rawRead(dataBuffer); 77 if(dataBuffer[0] & 0b1000_0000){//RLE block 78 dataBuffer[0] &= 0b0111_1111; 79 dataBuffer[0]++; 80 ubyte[] rleBlock; 81 rleBlock.length = dataBuffer[0]; 82 memset(rleBlock.ptr, dataBuffer[1], dataBuffer[0]); 83 result ~= rleBlock; 84 }else{//literal block 85 dataBuffer[0] &= 0b0111_1111; 86 ubyte[] literalBlock; 87 literalBlock.length = dataBuffer[0]; 88 file.rawRead(literalBlock); 89 result ~= dataBuffer[1] ~ literalBlock; 90 target--; 91 } 92 target -= dataBuffer[0]; 93 } 94 assert(result.length == (header.width * header.height / (header.pixelDepth / 8))); 95 return result; 96 } 97 while(target){ 98 file.rawRead(dataBuffer); 99 if(dataBuffer[0] & 0b1000_0000){//RLE block 100 dataBuffer[0] &= 0b0111_1111; 101 dataBuffer[0]++; 102 ubyte[] rleBlock; 103 rleBlock.reserve(dataBuffer[0] * bytedepth); 104 while(dataBuffer[0]){ 105 rleBlock ~= dataBuffer[1..$]; 106 dataBuffer[0]--; 107 target--; 108 } 109 result ~= rleBlock; 110 }else{//literal block 111 dataBuffer[0] &= 0b0111_1111; 112 ubyte[] literalBlock; 113 literalBlock.length = dataBuffer[0] * bytedepth; 114 file.rawRead(literalBlock); 115 result ~= dataBuffer[1] ~ literalBlock; 116 target--; 117 } 118 } 119 assert(result.length == (header.width * header.height / (header.pixelDepth / 8))); 120 return result; 121 } 122 TGAHeader headerLoad; 123 ubyte[TGAHeader.sizeof] headerBuffer; 124 file.rawRead(headerBuffer); 125 headerLoad = *(cast(TGAHeader*)(cast(void*)headerBuffer.ptr)); 126 char[] imageIDLoad; 127 imageIDLoad.length = headerLoad.idLength; 128 if(imageIDLoad.length) file.rawRead(imageIDLoad); 129 version(unittest) std.stdio.writeln(imageIDLoad); 130 ubyte[] palette; 131 palette.length = headerLoad.colorMapLength * (headerLoad.colorMapDepth / 8); 132 if(palette.length) file.rawRead(palette); 133 ubyte[] image; 134 if(headerLoad.imageType >= TGAHeader.ImageType.RLEMapped && headerLoad.imageType <= TGAHeader.ImageType.RLEGrayscale){ 135 image = loadRLEImageData(headerLoad); 136 }else{ 137 image.length = (headerLoad.width * headerLoad.height * headerLoad.pixelDepth) / 8; 138 version(unittest) std.stdio.writeln(headerLoad.toString); 139 if(image.length) file.rawRead(image); 140 } 141 static if(loadExtArea || loadDevArea){ 142 TGAFooter footerLoad; 143 file.seek(TGAFooter.sizeof * -1, SEEK_END); 144 footerLoad = file.rawRead([footerLoad]); 145 TGA result = new TGA(headerLoad, footerLoad, image, palette, imageID); 146 if(footerLoad.isValid){ 147 static if(loadDevArea){ 148 if(footerLoad.developerAreaOffset){ 149 file.seek(footerLoad.developerAreaOffset); 150 result.developerAreaTags.length = 1; 151 file.rawRead(result.developerAreaTags); 152 result.developerAreaTags.length = result.developerAreaTags[0].reserved; 153 file.rawRead(result.developerAreaTags[1..$]); 154 result.developerArea.reserve = result.developerAreaTags[0].reserved; 155 ubyte[] dataBuffer; 156 foreach(tag; result.developerAreaTags){ 157 file.seek(tag.offset); 158 dataBuffer.length = tag.fieldSize; 159 file.rawRead(dataBuffer); 160 result.developerArea ~= DevArea(dataBuffer.dup); 161 } 162 } 163 } 164 static if(loadExtArea){ 165 if(footerLoad.extensionAreaOffset){ 166 file.seek(footerLoad.extensionAreaOffset); 167 result.extensionArea.length = 1; 168 file.rawRead(result.extensionArea); 169 if(result.extensionArea[0].postageStampOffset){ 170 file.seek(result.extensionArea[0].postageStampOffset); 171 result.postageStampImage.length = 2; 172 file.rawRead(result.postageStampImage); 173 result.postageStampImage.length = 2 + result.postageStampImage[0] * result.postageStampImage[1]; 174 file.rawRead(result.postageStampImage[2..$]); 175 } 176 if(result.extensionArea[0].colorCorrectionOffset){ 177 result.colorCorrectionTable.length = 1024; 178 file.seek(result.extensionArea[0].colorCorrectionOffset); 179 file.rawRead(result.colorCorrectionTable); 180 } 181 if(result.extensionArea[0].scanlineOffset){ 182 result.scanlineTable.length = headerLoad.height; 183 file.seek(result.extensionArea[0].scanlineOffset); 184 file.rawRead(result.scanlineTable); 185 } 186 } 187 } 188 } 189 switch(headerLoad.pixelDepth){ 190 case 1: 191 result.mod = 7; 192 result.shift = 3; 193 case 2: 194 result.mod = 3; 195 result.shift = 2; 196 case 4: 197 result.mod = 1; 198 result.shift = 1; 199 default: 200 break; 201 } 202 return result; 203 }else{ 204 return new TGA(headerLoad, image, palette, imageIDLoad); 205 } 206 } 207 /** 208 * Saves the current TGA object into a Truevision TARGA file. 209 * If ignoreScanlineBounds is true, then the compression methods will ignore the scanline bounds, this disables per-line accessing, but enhances compression 210 * rates by a margin in exchange. If false, then it'll generate a scanline table. 211 */ 212 public void save(FILE = std.stdio.File, bool saveDevArea = false, bool saveExtArea = false, 213 bool ignoreScanlineBounds = false)(FILE file){ 214 import std.stdio; 215 void compressRLE(){ 216 static if(!ignoreScanlineBounds){ 217 const uint maxScanlineLength = header.width; 218 scanlineTable.length = 0; 219 scanlineTable.reserve(height); 220 } 221 switch(header.pixelDepth){ 222 case 16: 223 break; 224 case 24: 225 break; 226 case 32: 227 break; 228 default: 229 ubyte* src = imageData.ptr; 230 const ubyte* dest = src + imageData.length; 231 ubyte[] writeBuff; 232 writeBuff.length = 129; 233 static if(!ignoreScanlineBounds) 234 uint currScanlineLength; 235 while(src < dest){ 236 ubyte* currBlockBegin = src, currBlockEnd = src; 237 if(currBlockBegin[0] == currBlockBegin[1]){ //RLE block 238 ubyte blockLength = 1; 239 while(currBlockEnd[0] == currBlockEnd[1]){ 240 src++; 241 currBlockEnd++; 242 blockLength++; 243 static if(!ignoreScanlineBounds){ 244 currScanlineLength++; 245 if(currScanlineLength == maxScanlineLength){ 246 currScanlineLength = 0; 247 scanlineTable ~= cast(uint)file.tell; 248 break; 249 } 250 } 251 if(blockLength == 128 || src + 1 == dest) 252 break; 253 } 254 blockLength--; 255 blockLength |= 0b1000_0000; 256 writeBuff[0] = blockLength; 257 writeBuff[1] = currBlockBegin[0]; 258 file.rawWrite(writeBuff[0..2]); 259 }else{ //literal block 260 ubyte blockLength = 1; 261 writeBuff[1] = currBlockEnd[0]; 262 while(currBlockEnd[0] != currBlockEnd[1] && currBlockEnd[1] != currBlockEnd[2]){ //also check if next byte pair isn't RLE block 263 writeBuff[1 + blockLength] = currBlockEnd[1]; 264 src++; 265 currBlockEnd++; 266 blockLength++; 267 static if(!ignoreScanlineBounds){ 268 currScanlineLength++; 269 if(currScanlineLength == maxScanlineLength){ 270 currScanlineLength = 0; 271 break; 272 } 273 } 274 if(blockLength == 128 || src + 2 == dest) 275 break; 276 } 277 //writeBuff[1] = currBlockEnd[0]; 278 blockLength--; 279 writeBuff[0] = blockLength; 280 file.rawWrite(writeBuff[0..blockLength + 3]); 281 } 282 } 283 break; 284 } 285 } 286 //write most of the data into the file 287 file.rawWrite([header]); 288 file.rawWrite(imageID); 289 file.rawWrite(paletteData); 290 file.rawWrite(imageData); 291 static if(saveDevArea){ 292 if(developerAreaTags.length){ 293 //make all tags valid 294 uint offset = cast(uint)file.tell; 295 footer.developerAreaOffset = offset; 296 offset += cast(uint)(developerAreaTags.length * DevAreaTag.sizeof); 297 developerAreaTags[0].reserved = cast(ushort)developerAreaTags.length; 298 for(int i; i < developerAreaTags.length; i++){ 299 developerAreaTags[i].offset = offset; 300 developerAreaTags[i].fieldSize = developerArea[i].data.length; 301 offset += developerArea[i].data.length; 302 } 303 file.rawWrite(developerAreaTags); 304 foreach(d; developerArea){ 305 file.rawWrite(d.data); 306 } 307 } 308 } 309 static if(saveExtArea){ 310 if(extensionArea.length){ 311 uint offset = cast(uint)file.tell; 312 footer.extensionAreaOffset = offset; 313 offset += cast(uint)ExtArea.sizeof; 314 if(colorCorrectionTable.length){ 315 extensionArea[0].colorCorrectionOffset = offset; 316 offset += cast(uint)(colorCorrectionTable.length * ushort.sizeof); 317 }else{ 318 extensionArea[0].colorCorrectionOffset = 0; 319 } 320 if(postageStampImage.length){ 321 extensionArea[0].postageStampOffset = offset; 322 offset += cast(uint)(postageStampImage.length * ubyte.sizeof); 323 }else{ 324 extensionArea[0].postageStampOffset = 0; 325 } 326 if(scanlineTable.length){ 327 extensionArea[0].scanlineOffset = offset; 328 //offset += cast(uint)(scanlineTable.length * uint.sizeof); 329 }else{ 330 extensionArea[0].scanlineOffset = 0; 331 } 332 file.rawWrite(extensionArea); 333 assert(file.tell == footer.extensionAreaOffset); 334 if (colorCorrectionTable.length){ 335 file.rawWrite(colorCorrectionTable); 336 assert(file.tell == extensionArea[0].colorCorrectionOffset); 337 } 338 if (postageStampImage.length){ 339 file.rawWrite(postageStampImage); 340 assert(file.tell == extensionArea[0].postageStampImage); 341 } 342 if (scanlineTable.length){ 343 file.rawWrite(scanlineTable); 344 assert(file.tell == extensionArea[0].scanlineTable); 345 } 346 } 347 } 348 static if(saveExtArea || saveDevArea){ 349 file.rawWrite([footer]); 350 } 351 } 352 override ushort width() @nogc @safe @property const{ 353 return header.width; 354 } 355 override ushort height() @nogc @safe @property const{ 356 return header.height; 357 } 358 override bool isIndexed() @nogc @safe @property const{ 359 return header.colorMapType != TGAHeader.ColorMapType.NoColorMapPresent; 360 } 361 override ubyte getBitdepth() @nogc @safe @property const{ 362 return header.pixelDepth; 363 } 364 override ubyte getPaletteBitdepth() @nogc @safe @property const{ 365 return header.colorMapDepth; 366 } 367 override PixelFormat getPixelFormat() @nogc @safe @property const{ 368 if(header.pixelDepth == 16 && header.colorMapType == TGAHeader.ColorMapType.NoColorMapPresent){ 369 return PixelFormat.RGBA5551; 370 }else{ 371 return PixelFormat.Undefined; 372 } 373 } 374 /** 375 * Returns the pixel order for bitdepths less than 8. Almost excusively used for indexed bitmaps. 376 * Returns null if ordering not needed. 377 */ 378 override public ubyte[] getPixelOrder() @safe @property const{ 379 switch(header.pixelDepth){ 380 case 1: return pixelOrder1BitBE.dup; 381 case 2: return pixelOrder2BitBE.dup; 382 case 4: return pixelOrder4BitBE.dup; 383 default: return null; 384 } 385 } 386 /** 387 * Returns which pixel how much needs to be shifted right after a byteread. 388 */ 389 override public ubyte[] getPixelOrderBitshift() @safe @property const{ 390 switch(header.pixelDepth){ 391 case 1: return pixelShift1BitBE.dup; 392 case 2: return pixelShift2BitBE.dup; 393 case 4: return pixelShift4BitBE.dup; 394 default: return null; 395 } 396 } 397 public string getID(){ 398 return cast(string)imageID; 399 } 400 public string getAuthor(){ 401 if(extensionArea.length) 402 return extensionArea[0].authorName; 403 return null; 404 } 405 public string getComment(){ 406 if(extensionArea.length) 407 return extensionArea[0].authorComments; 408 return null; 409 } 410 public string getJobName(){ 411 if(extensionArea.length) 412 return extensionArea[0].jobName; 413 return null; 414 } 415 public string getSoftwareInfo(){ 416 if(extensionArea.length) 417 return extensionArea[0].softwareID; 418 return null; 419 } 420 public string getSoftwareVersion(){ 421 import std.conv : to; 422 if(extensionArea.length) 423 return to!string(extensionArea[0].softwareVersNum / 100) ~ "." ~ to!string((extensionArea[0].softwareVersNum % 100) 424 / 10) ~ "." ~ to!string(extensionArea[0].softwareVersNum % 10) ~ extensionArea[0].softwareVersChar; 425 return null; 426 } 427 public void setID(string val){ 428 if(val.length > 255) 429 throw new Exception("ID is too long"); 430 imageID = val.dup; 431 header.idLength = cast(ubyte)val.length; 432 } 433 public void setAuthor(string val){ 434 if(val.length > 41) 435 throw new Exception("Author name is too long"); 436 memset(extensionArea[0].authorName.ptr, 0, extensionArea[0].authorName.length); 437 memcpy(extensionArea[0].authorName.ptr, val.ptr, val.length); 438 } 439 public void setComment(string val){ 440 if(val.length > 324) 441 throw new Exception("Comment is too long"); 442 memset(extensionArea[0].authorComments.ptr, 0, extensionArea[0].authorComments.length); 443 memcpy(extensionArea[0].authorComments.ptr, val.ptr, val.length); 444 } 445 public void setJobName(string val){ 446 if(val.length > 41) 447 throw new Exception("Jobname is too long"); 448 memset(extensionArea[0].jobName.ptr, 0, extensionArea[0].jobName.length); 449 memcpy(extensionArea[0].jobName.ptr, val.ptr, val.length); 450 } 451 public void setSoftwareInfo(string val){ 452 if(val.length > 41) 453 throw new Exception("SoftwareID is too long"); 454 memset(extensionArea[0].softwareID.ptr, 0, extensionArea[0].softwareID.length); 455 memcpy(extensionArea[0].softwareID.ptr, val.ptr, val.length); 456 } 457 public void setSoftwareVersion(string val){ 458 459 } 460 /** 461 * Adds extension area for the file. 462 */ 463 public void createExtensionArea(){ 464 extensionArea.length = 1; 465 } 466 /** 467 * Returns the developer area info for the field. 468 */ 469 public DevAreaTag getDevAreaInfo(size_t n){ 470 return developerAreaTags[n]; 471 } 472 /** 473 * Returns the embedded field. 474 */ 475 public DevArea getEmbeddedData(size_t n){ 476 return developerArea[n]; 477 } 478 /** 479 * Creates an embedded field within the TGA file. 480 */ 481 public void addEmbeddedData(ushort ID, ubyte[] data){ 482 developerAreaTags ~= DevAreaTag(0, ID, 0, cast(uint)data.length); 483 developerArea ~= DevArea(data); 484 } 485 /** 486 * Returns the image type. 487 */ 488 public ubyte getImageType() const @nogc @property @safe nothrow{ 489 return header.imageType; 490 } 491 /** 492 * Returns the header as a reference type. 493 */ 494 public ref TGAHeader getHeader() @nogc @safe nothrow{ 495 return header; 496 } 497 } 498 /** 499 * Implements Truevision Graphics bitmap header. 500 */ 501 public struct TGAHeader { 502 align(1) : 503 /** 504 * Defines the type of the color map. 505 */ 506 public enum ColorMapType{ 507 NoColorMapPresent = 0, 508 ColorMapPresent = 1, 509 /** 510 * In this case, the palette is stored in a *.pal file, colorMapLength specifies the lenght of the filename, and the usual colorMap field instead stores the filename. 511 */ 512 ExtColorMap = 128, 513 } 514 /** 515 * Defines the type of the image. 516 */ 517 enum ImageType : ubyte { 518 NoData = 0, 519 UncompressedMapped = 1, 520 UncompressedTrueColor = 2, 521 UncompressedGrayscale = 3, 522 RLEMapped = 9, 523 RLETrueColor = 10, 524 RLEGrayscale = 11, 525 } 526 ubyte idLength; /// length in bytes 527 ubyte colorMapType; /// See ColorMapType enumerator 528 ubyte imageType; /// See ImageType enumerator 529 ushort colorMapOffset; /// index of first actual map entry 530 ushort colorMapLength; /// number of total entries (incl. skipped) 531 ubyte colorMapDepth; /// bits per pixel (entry) 532 ushort xOrigin; /// X origin of the image on the screen 533 ushort yOrigin; /// Y origin of the image on the screen 534 ushort width; /// Image width 535 ushort height; /// Image height 536 ubyte pixelDepth; /// bits per pixel 537 //imageDescriptor: 538 mixin(bitfields!( 539 ubyte, "alphaChannelBits", 4, 540 bool , "rightSideOrigin", 1, 541 bool , "topOrigin", 1, 542 ubyte, "reserved", 2, 543 )); 544 public string toString(){ 545 import std.conv : to; 546 return 547 "idLength:" ~ to!string(idLength) ~ "\n" ~ 548 "colorMapType:" ~ to!string(colorMapType) ~ "\n" ~ 549 "imageType:" ~ to!string(imageType) ~ "\n" ~ 550 "colorMapOffset:" ~ to!string(colorMapOffset) ~ "\n" ~ 551 "colorMapLength:" ~ to!string(colorMapLength) ~ "\n" ~ 552 "colorMapDepth:" ~ to!string(colorMapDepth) ~ "\n" ~ 553 "xOrigin:" ~ to!string(xOrigin) ~ "\n" ~ 554 "yOrigin:" ~ to!string(yOrigin) ~ "\n" ~ 555 "width:" ~ to!string(width) ~ "\n" ~ 556 "height:" ~ to!string(height) ~ "\n" ~ 557 "pixelDepth:" ~ to!string(pixelDepth); 558 } 559 } 560 /** 561 * Implements Truevision Graphics bitmap footer, which is used to indicate the locations of extra fields. 562 */ 563 struct TGAFooter { 564 align(1) : 565 uint extensionAreaOffset; /// offset of the extensionArea, zero if doesn't exist 566 uint developerAreaOffset; /// offset of the developerArea, zero if doesn't exist 567 char[16] signature = "TRUEVISION-XFILE"; /// if equals with "TRUEVISION-XFILE", it's the new format 568 char reserved = '.'; /// should be always a dot 569 ubyte terminator; /// terminates the file, always null 570 ///Returns true if it's a valid TGA footer 571 @property bool isValid(){ 572 return signature == "TRUEVISION-XFILE"; 573 } 574 } 575 /** 576 * Contains extended data, mostly metadata. 577 */ 578 struct ExtArea{ 579 /** 580 * Stores attributes about the alpha channel. 581 */ 582 enum Attributes : ubyte{ 583 NoAlpha = 0, 584 UndefinedAlphaCanBeIgnored = 1, 585 UndefinedAlphaMustBePreserved = 2, 586 UsefulAlpha = 4, 587 PreMultipliedAlpha = 5 588 } 589 align(1) : 590 ushort size = cast(ushort)ExtArea.sizeof; ///size of this field (should be ExtArea.sizeof) 591 char[41] authorName; ///Name of the author 592 char[324] authorComments; ///Stores author comments 593 /** 594 * Stores the datetime in the following format 595 * 0: Year 596 * 1: Month 597 * 2: Day 598 * 3: Hour 599 * 4: Minute 600 * 5: Second 601 */ 602 ushort[6] dateTimeStamp; 603 char[41] jobName; ///Name of the job 604 /** 605 * Time of the job in the following format: 606 * 0: Hours 607 * 1: Minutes 608 * 2: Seconds 609 */ 610 ushort[3] jobTime; 611 char[41] softwareID; ///Stores the name of the software 612 ushort softwareVersNum; ///Stores the version of the software in a decimal system in the following format: 000.0.0 613 char softwareVersChar; ///Stores the version of the software 614 ubyte[4] keyColor; ///Key color, mostly used for greenscreening and similar thing 615 ushort pixelWidth; ///Pixel width ratio 616 ushort pixelHeight; ///Pixel height ratio 617 ushort gammaNumerator; ///Gamma correction 618 ushort gammaDenominator; ///Gamma correction 619 uint colorCorrectionOffset; ///Color correction field. The library cannot process this information. 620 uint postageStampOffset; ///Thumbnail image offset 621 uint scanlineOffset; ///Fast access to scanline offsets. The library can create it and load it, but doesn't use it since it decompresses RLE images upon load 622 ubyte attributes; ///Information on the alpha channel 623 } 624 /** 625 * Identifies the embedded data. 626 */ 627 struct DevAreaTag{ 628 ushort reserved; /// number of tags in the beginning 629 /** 630 * Identifier tag of the developer area field. 631 * Supposedly the range of 32768 - 65535 is reserved by Truevision, however there are no information on whenever it was 632 * used by them or not. 633 */ 634 ushort tag; 635 uint offset; /// offset into file 636 uint fieldSize; /// field size in bytes 637 } 638 /** 639 * Represents embedded data within the developer area 640 */ 641 struct DevArea{ 642 ubyte[] data; 643 644 /** 645 * Returns the data as a certain type (preferrably struct) if available 646 */ 647 T get(T)(){ 648 if(T.sizeof == data.length){ 649 return cast(T)(cast(void[])data); 650 } 651 } 652 } 653 654 unittest{ 655 import std.conv : to; 656 import vfile; 657 void compareImages(Image a, Image b){ 658 assert(a.width == b.width); 659 assert(a.height == b.height); 660 //Check if the data in the two are identical 661 for(ushort y; y < a.height; y++){ 662 for(ushort x; x < a.width; x++){ 663 assert(a.readPixel(x,y) == b.readPixel(x,y), "Error at position (" ~ to!string(x) ~ "," ~ to!string(y) ~ ")!"); 664 } 665 } 666 } 667 assert(TGAHeader.sizeof == 18); 668 ubyte[] tempStream; 669 //test 8 bit RLE load for 8 bit greyscale and indexed 670 std.stdio.File greyscaleUncFile = std.stdio.File("test/tga/grey_8.tga"); 671 std.stdio.writeln("Loading ", greyscaleUncFile.name); 672 TGA greyscaleUnc = TGA.load(greyscaleUncFile); 673 std.stdio.writeln("File `", greyscaleUncFile.name, "` successfully loaded"); 674 std.stdio.File greyscaleRLEFile = std.stdio.File("test/tga/grey_8_rle.tga"); 675 std.stdio.writeln("Loading ", greyscaleRLEFile.name); 676 TGA greyscaleRLE = TGA.load(greyscaleRLEFile); 677 std.stdio.writeln("File `", greyscaleRLEFile.name, "` successfully loaded"); 678 compareImages(greyscaleUnc, greyscaleRLE); 679 //store the uncompressed one as a VFile in the memory using RLE, then restore it and check if it's working. 680 greyscaleUnc.getHeader.imageType = TGAHeader.ImageType.RLEGrayscale; 681 //VFile virtualFile = VFile(tempStream); 682 std.stdio.File virtualFile = std.stdio.File("test/tga/grey_8_rle_gen.tga", "wb"); 683 greyscaleUnc.save(virtualFile); 684 std.stdio.writeln("Save to virtual file was successful"); 685 greyscaleRLE = TGA.load(virtualFile); 686 std.stdio.writeln("Load from virtual file was successful"); 687 compareImages(greyscaleUnc, greyscaleRLE); 688 }