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