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 }