1 /*
2  * Copyright (C) 2015-2019, by Laszlo Szeremi under the Boost license.
3  *
4  * Pixel Perfect Engine, image file extensions module.
5  */
6 module pixelperfectengine.graphics.extensions;
7 
8 import pixelperfectengine.system.etc;
9 
10 /**
11  * Implements a custom sprite sheet module with variable sprite sizes.
12  * Size should be ushort for TGA, and uint for PNG.
13  */
14 public class ObjectSheet (Size = ushort) {
15 	static enum char[4] IDENTIFIER_PNG = "sHIT";	///Identifier for PNG ancillary chunk
16 	static enum ushort	IDENTIFIER_TGA = 0xFF01;	///Identifier for TGA developer extension
17 	/**
18 	 * Defines the header of an ObjectSheet in the bytestream
19 	 */
20 	align(1) public struct Header {
21 	align(1):
22 		uint	id;			///Identification if there's multiple of object sheets in a single file
23 		ushort	nOfIndexes;	///Number of indexes in a single instance
24 		ubyte	nameLength;	///Length of the name of this instance, can be zero
25 	}
26 	//memory initiated header
27 	uint		id;			///ID of this sheet
28 	protected string	_name;	///Name with overflow protection
29 	/**
30 	 * Defines a single index
31 	 */
32 	align(1) public struct Index (bool MemoryResident = true) {
33 	align(1):
34 		uint	id;			///ID of this object
35 		Size	x;			///Top-left origin point of the object (X)
36 		Size	y;			///Top-left origin point of the object (Y)
37 		ubyte	width;		///The width of this object
38 		ubyte	height;		///The height of this object
39 		byte	displayOffsetX;	///Describes the offset compared to the average when the object needs to be displayed.
40 		byte	displayOffsetY;	///Describes the offset compared to the average when the object needs to be displayed.
41 		static if(MemoryResident){
42 			private string _name;	///Name with character overflow protection
43 			///Default ctor
44 			this (uint id, Size x, Size y, ubyte width, ubyte height, byte displayOffsetX, byte displayOffsetY, string _name) 
45 					@safe pure {
46 				this.id = id;
47 				this.x = x;
48 				this.y = y;
49 				this.width = width;
50 				this.height = height;
51 				this.displayOffsetX = displayOffsetX;
52 				this.displayOffsetY = displayOffsetY;
53 				this._name = _name;
54 			}
55 			///Converts from a non memory resident type
56 			this (Index!false src) @safe pure {
57 				id = src.id;
58 				x = src.x;
59 				y = src.y;
60 				width = src.width;
61 				height = src.height;
62 				displayOffsetX = src.displayOffsetX;
63 				displayOffsetY = src.displayOffsetY;
64 			}
65 			///Returns the name of the object
66 			@property string name () @safe pure nothrow {
67 				return _name;
68 			}
69 			///Sets the name of the object if input's length is less or equal than 255
70 			@property string name (string val) @safe pure nothrow {
71 				if(val.length <= ubyte.max){
72 					_name = val;
73 				}
74 				return _name;
75 			}
76 		}else{
77 			ubyte nameLength;		///Length of next field
78 			///Convert from a memory resident type
79 			this (Index src) {
80 				id = src.id;
81 				x = src.x;
82 				y = src.y;
83 				width = src.width;
84 				height = src.height;
85 				displayOffsetX = src.displayOffsetX;
86 				displayOffsetY = src.displayOffsetY;
87 				nameLength = name.length;
88 			}
89 		}
90 	}
91 	Index!(true)[]		objects;	///Stores each index as a separate entity.
92 	/**
93 	 * Serializes itself from a bytestream.
94 	 */
95 	public this(ubyte[] source) @safe pure {
96 		const Header h = reinterpretGet!Header(source[0..Header.sizeof]);
97 		source = source[Header.sizeof..$];
98 		id = h.id;
99 		objects.length = h.nOfIndexes;
100 		if(h.nameLength){
101 			name = reinterpretCast!char(source[0..h.nameLength]).idup;
102 			source = source[h.nameLength..$];
103 		}
104 		for(ushort i ; i < objects.length ; i++){
105 			const Index!false index = reinterpretGet!(Index!(false))(source[0..(Index!(false)).sizeof]);
106 			source = source[(Index!(false)).sizeof..$];
107 			objects[i] = Index!(true)(index);
108 			if (index.nameLength) {
109 				objects[i].name = reinterpretCast!char(source[0..index.nameLength]).idup;
110 				source = source[index.nameLength..$];
111 			}
112 		}
113 	}
114 	/**
115 	 * Creates a new instance from scratch
116 	 */
117 	public this(string _name, uint id){
118 		this._name = _name;
119 		this.id = id;
120 	}
121 	///Returns the name of the object
122 	@property string name() @safe pure nothrow {
123 		return _name;
124 	}
125 	///Sets the name of the object if input's length is less or equal than 255
126 	@property string name(string val) @safe pure nothrow {
127 		if(val.length <= ubyte.max){
128 			_name = val;
129 		}
130 		return _name;
131 	}
132 	/**
133 	 * Serializes itself to bytestream.
134 	 */
135 	public ubyte[] serialize() @safe pure {
136 		ubyte[] result;
137 		result ~= toStream(Header(id, cast(uint)objects.length), cast(ubyte)_name.length);
138 		result ~= reinterpretCast!ubyte(_name.dup);
139 		foreach (i ; objects) {
140 			result ~= toStream(Index!(false)(i));
141 			if (i.name.length)
142 				result ~= reinterpretCast!ubyte(i.name.dup);
143 		}
144 		return result;
145 	}
146 }
147 /**
148  * Implements Adaptive Framerate Animation data, that can be embedded into regular bitmaps.
149  * Works with either tile or object sheet extensions.
150  */
151 public class AdaptiveFramerateAnimationData {
152 	/**
153 	 * Header for an adaptive framerate animation
154 	 */
155 	align(1) public struct Header (bool MemoryResident = true) {
156 	align(1):
157 		uint	id;		///Identifier of the animation
158 		static if (!MemoryResident)
159 			uint	frames;	///Number of frames associated with this animation
160 		uint	source;	///Identifier of the object source. f tile extension is used instead of the sheet one, set it to zero.
161 		static if (!MemoryResident) {
162 			ubyte	nameLength;	///Length of the namefield
163 			///Default constructor
164 			this (uint id, uint frames, uint source, ubyte nameLength){
165 				this.id = id;
166 				this.frames = frames;
167 				this.source = source;
168 				this.nameLength = nameLength;
169 			}
170 			///Converts from memory-resident header
171 			this (Header!true val) {
172 				id = val.id;
173 				nameLength = cast(ubyte)val.name.length;
174 			}
175 		} else {
176 			private string	_name;	///Name of the animation
177 			///Converts from serialized header
178 			this (Header!false val) {
179 				id = val.id;
180 				source = val.source;
181 			}
182 			///Returns the name of the object
183 			@property string name() @safe pure nothrow {
184 				return _name;
185 			}
186 			///Sets the name of the object if input's length is less or equal than 255
187 			@property string name(string val) @trusted pure nothrow {
188 				void _helper() @system pure nothrow {
189 					_name = val;
190 				}
191 				if(val.length <= ubyte.max){
192 					_helper;
193 				}
194 				return _name;
195 			}
196 		}
197 	}
198 	/**
199 	 * Each index represents an animation frame
200 	 */
201 	align(1) public struct Index {
202 	align(1):
203 		uint	sourceID;	///Identifier of the frame source.
204 		uint	hold;		///Duration of the frame in hundreds of microseconds.
205 		byte	offsetX;	///Horizontal offset of the frame.
206 		byte	offsetY;	///Vertical offset of the frame.
207 	}
208 	Header!(true) header;		///Header of this instance.
209 	Index[] frames;		///Frames for each animation.
210 	/**
211 	 * Serializes itself from a bytestream.
212 	 */
213 	public this (ubyte[] stream) @safe pure {
214 		const Header!false h = reinterpretGet!(Header!false)(stream[0..(Header!false).sizeof]);
215 		stream = stream[(Header!false).sizeof..$];
216 		header = Header!true(h);
217 		header.name = reinterpretCast!char(stream[0..h.nameLength]).idup;
218 		stream = stream[h.nameLength..$];
219 		//rest should be composed of equal sized indexes
220 		frames = reinterpretCast!Index(stream);
221 		if (frames.length != h.frames) {
222 			throw new Exception ("Stream error!");
223 		}
224 	}
225 	/**
226 	 * Creates a new instance from scratch.
227 	 */
228 	public this (string _name, uint id, uint source) {
229 		header.name = _name;
230 		header.id = id;
231 		header.source = source;
232 	}
233 	/**
234 	 * Serializes the object into a bytestream.
235 	 */
236 	public ubyte[] serialize () @safe pure {
237 		ubyte[] result;
238 		result ~= reinterpretAsArray!(ubyte)(Header!false(header.id, cast(uint)frames.length, header.source, 
239 				cast(ubyte)header.name.length));
240 		result ~= reinterpretCast!ubyte(frames);
241 		return result;
242 	}
243 }
244 // test if all templates compile as they should, then test serialization functions
245 unittest {
246 	import std.random;
247 
248 	//auto rnd = rndGen;
249 
250 	ObjectSheet!ushort testObj = new ObjectSheet!ushort("Test", 0);
251 	for(int i ; i < 10 ; i++)
252 		testObj.objects ~= ObjectSheet!(ushort).Index(uniform!uint(), uniform!ushort(), uniform!ushort(), uniform!ubyte(),
253 				uniform!ubyte() ,uniform!byte(), uniform!ubyte());
254 	ubyte[] datastream = testObj.serialize();
255 	ObjectSheet!ushort secondTestObj = new ObjectSheet!ushort(datastream);
256 	for(int i ; i < 10 ; i++)
257 		assert(testObj.objects[i] == secondTestObj.objects[i]);
258 }
259 unittest {
260 	import std.random;
261 
262 	//auto rnd = rndGen;
263 
264 	ObjectSheet!uint testObj = new ObjectSheet!uint("Test", 0);
265 	for(int i ; i < 10 ; i++)
266 		testObj.objects ~= ObjectSheet!(uint).Index(uniform!uint(), uniform!ushort(), uniform!ushort(), uniform!ubyte(),
267 				uniform!ubyte() ,uniform!byte(), uniform!ubyte());
268 	ubyte[] datastream = testObj.serialize();
269 	ObjectSheet!uint secondTestObj = new ObjectSheet!uint(datastream);
270 	for(int i ; i < 10 ; i++)
271 		assert(testObj.objects[i] == secondTestObj.objects[i]);
272 }
273 unittest {
274 	import std.random;
275 
276 	AdaptiveFramerateAnimationData testObj = new AdaptiveFramerateAnimationData("Test", 0, 0);
277 	for(int i ; i < 10 ; i++)
278 		testObj.frames ~= AdaptiveFramerateAnimationData.Index(uniform!uint(), uniform!uint(), uniform!byte(), 
279 				uniform!byte());
280 	ubyte[] datastream = testObj.serialize();
281 	AdaptiveFramerateAnimationData secondTestObj = new AdaptiveFramerateAnimationData(datastream);
282 	for(int i ; i < 10 ; i++)
283 		assert(testObj.frames[i] == secondTestObj.frames[i]);
284 }