1 /* 2 Inochi2D Puppet file format 3 4 Copyright © 2020, Inochi2D Project 5 Distributed under the 2-Clause BSD License, see LICENSE file. 6 7 Authors: Luna Nielsen 8 */ 9 module inochi2d.fmt; 10 import inochi2d.fmt.binfmt; 11 public import inochi2d.fmt.serialize; 12 import inochi2d.integration; 13 import inochi2d.core; 14 import std.bitmanip : nativeToBigEndian; 15 import std.exception; 16 import std.path; 17 import std.format; 18 import imagefmt; 19 import inochi2d.fmt.io; 20 21 private bool isLoadingINP_ = false; 22 23 /** 24 Gets whether the current loading state is set to INP loading 25 */ 26 bool inIsINPMode() { 27 return isLoadingINP_; 28 } 29 30 /** 31 Loads a puppet from a file 32 */ 33 T inLoadPuppet(T = Puppet)(string file) if (is(T : Puppet)) { 34 try { 35 import std.file : read; 36 ubyte[] buffer = cast(ubyte[])read(file); 37 38 switch(extension(file)) { 39 40 case ".inp": 41 enforce(inVerifyMagicBytes(buffer), "Invalid data format for INP puppet"); 42 return inLoadINPPuppet!T(buffer); 43 44 case ".inx": 45 enforce(inVerifyMagicBytes(buffer), "Invalid data format for Inochi Creator INX"); 46 return inLoadINPPuppet!T(buffer); 47 48 default: 49 throw new Exception("Invalid file format of %s at path %s".format(extension(file), file)); 50 } 51 } catch(Exception ex) { 52 inEndTextureLoading!false(); 53 throw ex; 54 } 55 } 56 57 /** 58 Loads a puppet from memory 59 */ 60 Puppet inLoadPuppetFromMemory(ubyte[] data) { 61 return deserialize!Puppet(cast(string)data); 62 } 63 64 /** 65 Loads a JSON based puppet 66 */ 67 Puppet inLoadJSONPuppet(string data) { 68 isLoadingINP_ = false; 69 return inLoadJsonDataFromMemory!Puppet(data); 70 } 71 72 /** 73 Loads a INP based puppet 74 */ 75 T inLoadINPPuppet(T = Puppet)(ubyte[] buffer) if (is(T : Puppet)) { 76 size_t bufferOffset = 0; 77 isLoadingINP_ = true; 78 79 enforce(inVerifyMagicBytes(buffer), "Invalid data format for INP puppet"); 80 bufferOffset += 8; // Magic bytes are 8 bytes 81 82 // Find the puppet data 83 uint puppetDataLength; 84 inInterpretDataFromBuffer(buffer[bufferOffset..bufferOffset+=4], puppetDataLength); 85 86 string puppetData = cast(string)buffer[bufferOffset..bufferOffset+=puppetDataLength]; 87 88 enforce(inVerifySection(buffer[bufferOffset..bufferOffset+=8], TEX_SECTION), "Expected Texture Blob section, got nothing!"); 89 90 // Load textures in to memory 91 version (InDoesRender) { 92 inBeginTextureLoading(); 93 94 // Get amount of slots 95 uint slotCount; 96 inInterpretDataFromBuffer(buffer[bufferOffset..bufferOffset+=4], slotCount); 97 98 Texture[] slots; 99 foreach(i; 0..slotCount) { 100 101 uint textureLength; 102 inInterpretDataFromBuffer(buffer[bufferOffset..bufferOffset+=4], textureLength); 103 104 ubyte textureType = buffer[bufferOffset++]; 105 if (textureLength == 0) { 106 inAddTextureBinary(ShallowTexture([], 0, 0, 4)); 107 } else inAddTextureBinary(ShallowTexture(buffer[bufferOffset..bufferOffset+=textureLength])); 108 109 // Readd to puppet so that stuff doesn't break if we re-save the puppet 110 slots ~= inGetLatestTexture(); 111 } 112 113 T puppet = inLoadJsonDataFromMemory!T(puppetData); 114 puppet.textureSlots = slots; 115 puppet.updateTextureState(); 116 inEndTextureLoading(); 117 } else version(InRenderless) { 118 inCurrentPuppetTextureSlots.length = 0; 119 120 // Get amount of slots 121 uint slotCount; 122 inInterpretDataFromBuffer(buffer[bufferOffset..bufferOffset+=4], slotCount); 123 foreach(i; 0..slotCount) { 124 125 uint textureLength; 126 inInterpretDataFromBuffer(buffer[bufferOffset..bufferOffset+=4], textureLength); 127 128 ubyte textureType = buffer[bufferOffset++]; 129 if (textureLength == 0) { 130 continue; 131 } else inCurrentPuppetTextureSlots ~= TextureBlob(textureType, buffer[bufferOffset..bufferOffset+=textureLength]); 132 } 133 134 T puppet = inLoadJsonDataFromMemory!T(puppetData); 135 } 136 137 if (buffer.length >= bufferOffset + 8 && inVerifySection(buffer[bufferOffset..bufferOffset+=8], EXT_SECTION)) { 138 uint sectionCount; 139 inInterpretDataFromBuffer(buffer[bufferOffset..bufferOffset+=4], sectionCount); 140 141 foreach(section; 0..sectionCount) { 142 import std.json : parseJSON; 143 144 // Get name of payload/vendor extended data 145 uint sectionNameLength; 146 inInterpretDataFromBuffer(buffer[bufferOffset..bufferOffset+=4], sectionNameLength); 147 string sectionName = cast(string)buffer[bufferOffset..bufferOffset+=sectionNameLength]; 148 149 // Get length of data 150 uint payloadLength; 151 inInterpretDataFromBuffer(buffer[bufferOffset..bufferOffset+=4], payloadLength); 152 153 // Load the vendor JSON data in to the extData section of the puppet 154 ubyte[] payload = buffer[bufferOffset..bufferOffset+=payloadLength]; 155 puppet.extData[sectionName] = payload; 156 } 157 } 158 159 // We're done! 160 return puppet; 161 } 162 163 /** 164 Only write changed EXT section portions to puppet file 165 */ 166 void inWriteINPExtensions(Puppet p, string file) { 167 import std.stdio : File; 168 import stdfile = std.file; 169 size_t extSectionStart, extSectionEnd; 170 bool foundExtSection; 171 File f = File(file, "rb"); 172 173 // Verify that we're in an INP file 174 enforce(inVerifyMagicBytes(f.read(MAGIC_BYTES.length)), "Invalid data format for INP puppet"); 175 176 // Read puppet payload 177 uint puppetSectionLength = f.readValue!uint; 178 f.skip(puppetSectionLength); 179 180 // Verify texture section magic bytes 181 enforce(inVerifySection(f.read(TEX_SECTION.length), TEX_SECTION), "Expected Texture Blob section, got nothing!"); 182 183 uint slotCount = f.readValue!uint; 184 foreach(slot; 0..slotCount) { 185 uint length = f.readValue!uint; 186 f.skip(length+1); 187 } 188 189 // Only do this if there is an extended section here 190 if (inVerifySection(f.peek(EXT_SECTION.length), EXT_SECTION)) { 191 foundExtSection = true; 192 193 extSectionStart = f.tell(); 194 f.skip(EXT_SECTION.length); 195 196 uint payloadCount = f.readValue!uint; 197 foreach(pc; 0..payloadCount) { 198 199 uint nameLength = f.readValue!uint; 200 f.skip(nameLength); 201 202 uint payloadLength = f.readValue!uint; 203 f.skip(payloadLength); 204 } 205 extSectionEnd = f.tell(); 206 } 207 f.close(); 208 209 ubyte[] fdata = cast(ubyte[])stdfile.read(file); 210 ubyte[] app = fdata; 211 if (foundExtSection) { 212 // If the extended section was found, reuse it. 213 app = fdata[0..extSectionStart]; 214 ubyte[] end = fdata[extSectionEnd..$]; 215 216 // Don't waste bytes on empty EXT data sections 217 if (p.extData.length > 0) { 218 // Begin extended section 219 app ~= EXT_SECTION; 220 app ~= nativeToBigEndian(cast(uint)p.extData.length)[0..4]; 221 222 foreach(name, payload; p.extData) { 223 224 // Write payload name and its length 225 app ~= nativeToBigEndian(cast(uint)name.length)[0..4]; 226 app ~= cast(ubyte[])name; 227 228 // Write payload length and payload 229 app ~= nativeToBigEndian(cast(uint)payload.length)[0..4]; 230 app ~= payload; 231 232 } 233 } 234 235 app ~= end; 236 237 } else { 238 // Otherwise, make a new one 239 240 // Don't waste bytes on empty EXT data sections 241 if (p.extData.length > 0) { 242 // Begin extended section 243 app ~= EXT_SECTION; 244 app ~= nativeToBigEndian(cast(uint)p.extData.length)[0..4]; 245 246 foreach(name, payload; p.extData) { 247 248 // Write payload name and its length 249 app ~= nativeToBigEndian(cast(uint)name.length)[0..4]; 250 app ~= cast(ubyte[])name; 251 252 // Write payload length and payload 253 app ~= nativeToBigEndian(cast(uint)payload.length)[0..4]; 254 app ~= payload; 255 256 } 257 } 258 } 259 260 // write our final file out 261 stdfile.write(file, app); 262 } 263 264 /** 265 Writes out a model to memory 266 */ 267 ubyte[] inWriteINPPuppetMemory(Puppet p) { 268 import inochi2d.ver : IN_VERSION; 269 import std.range : appender; 270 import std.json : JSONValue; 271 272 isLoadingINP_ = true; 273 auto app = appender!(ubyte[]); 274 275 // Write the current used Inochi2D version to the version_ meta tag. 276 p.meta.version_ = IN_VERSION; 277 string puppetJson = inToJson(p); 278 279 app ~= MAGIC_BYTES; 280 app ~= nativeToBigEndian(cast(uint)puppetJson.length)[0..4]; 281 app ~= cast(ubyte[])puppetJson; 282 283 // Begin texture section 284 app ~= TEX_SECTION; 285 app ~= nativeToBigEndian(cast(uint)p.textureSlots.length)[0..4]; 286 foreach(texture; p.textureSlots) { 287 int e; 288 ubyte[] tex = write_image_mem(IF_TGA, texture.width, texture.height, texture.getTextureData(), texture.channels, e); 289 app ~= nativeToBigEndian(cast(uint)tex.length)[0..4]; 290 app ~= (cast(ubyte)IN_TEX_TGA); 291 app ~= (tex); 292 } 293 294 // Don't waste bytes on empty EXT data sections 295 if (p.extData.length > 0) { 296 // Begin extended section 297 app ~= EXT_SECTION; 298 app ~= nativeToBigEndian(cast(uint)p.extData.length)[0..4]; 299 300 foreach(name, payload; p.extData) { 301 302 // Write payload name and its length 303 app ~= nativeToBigEndian(cast(uint)name.length)[0..4]; 304 app ~= cast(ubyte[])name; 305 306 // Write payload length and payload 307 app ~= nativeToBigEndian(cast(uint)payload.length)[0..4]; 308 app ~= payload; 309 310 } 311 } 312 313 return app.data; 314 } 315 316 /** 317 Writes Inochi2D puppet to file 318 */ 319 void inWriteINPPuppet(Puppet p, string file) { 320 import std.file : write; 321 322 // Write it out to file 323 write(file, inWriteINPPuppetMemory(p)); 324 } 325 326 enum IN_TEX_PNG = 0u; /// PNG encoded Inochi2D texture 327 enum IN_TEX_TGA = 1u; /// TGA encoded Inochi2D texture 328 enum IN_TEX_BC7 = 2u; /// BC7 encoded Inochi2D texture 329 330 /** 331 Writes a puppet to file 332 */ 333 void inWriteJSONPuppet(Puppet p, string file) { 334 import std.file : write; 335 isLoadingINP_ = false; 336 write(file, inToJson(p)); 337 }