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 }