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 }