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.core; 13 import std.bitmanip; 14 import std.exception; 15 import std.path; 16 import std.file; 17 import std.format; 18 import imagefmt; 19 20 private bool isLoadingINP_ = false; 21 22 /** 23 Gets whether the current loading state is set to INP loading 24 */ 25 bool inIsINPMode() { 26 return isLoadingINP_; 27 } 28 29 /** 30 Loads a puppet from a file 31 */ 32 Puppet inLoadPuppet(string file) { 33 ubyte[] buffer = cast(ubyte[])read(file); 34 35 switch(extension(file)) { 36 37 case ".json": 38 enforce(!inVerifyMagicBytes(buffer), "Tried loading INP format as JSON format, rename file to .inp extension"); 39 return inLoadJSONPuppet(cast(string)buffer); 40 41 case ".inp": 42 enforce(inVerifyMagicBytes(buffer), "Invalid data format for INP puppet"); 43 return inLoadINPPuppet(buffer); 44 45 case ".inx": 46 enforce(inVerifyMagicBytes(buffer), "Invalid data format for Inochi Creator INX"); 47 return inLoadINPPuppet(buffer); 48 49 default: 50 throw new Exception("Invalid file format of %s at path %s".format(extension(file), file)); 51 } 52 } 53 54 /** 55 Loads a puppet from memory 56 */ 57 Puppet inLoadPuppetFromMemory(ubyte[] data) { 58 return deserialize!Puppet(cast(string)data); 59 } 60 61 /** 62 Loads a JSON based puppet 63 */ 64 Puppet inLoadJSONPuppet(string data) { 65 isLoadingINP_ = false; 66 return inLoadJsonDataFromMemory!Puppet(data); 67 } 68 69 /** 70 Loads a INP based puppet 71 */ 72 Puppet inLoadINPPuppet(ubyte[] buffer) { 73 size_t bufferOffset = 0; 74 isLoadingINP_ = true; 75 76 enforce(inVerifyMagicBytes(buffer), "Invalid data format for INP puppet"); 77 bufferOffset += 8; // Magic bytes are 8 bytes 78 79 // Find the puppet data 80 uint puppetDataLength; 81 inInterpretDataFromBuffer(buffer[bufferOffset..bufferOffset+=4], puppetDataLength); 82 83 string puppetData = cast(string)buffer[bufferOffset..bufferOffset+=puppetDataLength]; 84 85 enforce(inVerifySection(buffer[bufferOffset..bufferOffset+=8], TEX_SECTION), "Expected Texture Blob section, got nothing!"); 86 87 // Load textures in to memory 88 inBeginTextureLoading(); 89 90 // Get amount of slots 91 uint slotCount; 92 inInterpretDataFromBuffer(buffer[bufferOffset..bufferOffset+=4], slotCount); 93 94 Texture[] slots; 95 foreach(i; 0..slotCount) { 96 97 uint textureLength; 98 inInterpretDataFromBuffer(buffer[bufferOffset..bufferOffset+=4], textureLength); 99 100 ubyte textureType = buffer[bufferOffset++]; 101 if (textureLength == 0) { 102 inAddTextureBinary(ShallowTexture([], 0, 0, 4)); 103 } else inAddTextureBinary(ShallowTexture(buffer[bufferOffset..bufferOffset+=textureLength])); 104 105 // Readd to puppet so that stuff doesn't break if we re-save the puppet 106 slots ~= inGetLatestTexture(); 107 } 108 109 Puppet puppet = inLoadJsonDataFromMemory!Puppet(puppetData); 110 puppet.textureSlots = slots; 111 112 puppet.updateTextureState(); 113 114 inEndTextureLoading(); 115 116 if (inVerifySection(buffer[bufferOffset..bufferOffset+=8], EXT_SECTION)) { 117 uint sectionCount; 118 inInterpretDataFromBuffer(buffer[bufferOffset..bufferOffset+=4], sectionCount); 119 120 foreach(section; 0..sectionCount) { 121 import std.json : parseJSON; 122 123 // Get name of payload/vendor extended data 124 uint sectionNameLength; 125 inInterpretDataFromBuffer(buffer[bufferOffset..bufferOffset+=4], sectionNameLength); 126 string sectionName = cast(string)buffer[bufferOffset..bufferOffset+=sectionNameLength]; 127 128 // Get length of data 129 uint payloadLength; 130 inInterpretDataFromBuffer(buffer[bufferOffset..bufferOffset+=4], payloadLength); 131 132 // Load the vendor JSON data in to the extData section of the puppet 133 string payload = cast(string)buffer[bufferOffset..bufferOffset+=payloadLength]; 134 puppet.extData[sectionName] = parseJSON(payload); 135 } 136 } 137 138 // We're done! 139 return puppet; 140 } 141 142 /** 143 Writes Inochi2D puppet to file 144 */ 145 void inWriteINPPuppet(Puppet p, string file) { 146 import inochi2d.ver : IN_VERSION; 147 import std.range : appender; 148 import std.json : JSONValue; 149 150 isLoadingINP_ = true; 151 auto app = appender!(ubyte[]); 152 153 // Write the current used Inochi2D version to the version_ meta tag. 154 p.meta.version_ = IN_VERSION; 155 string puppetJson = inToJson(p); 156 157 app ~= MAGIC_BYTES; 158 app ~= nativeToBigEndian(cast(uint)puppetJson.length)[0..4]; 159 app ~= cast(ubyte[])puppetJson; 160 161 // Begin texture section 162 app ~= TEX_SECTION; 163 app ~= nativeToBigEndian(cast(uint)p.textureSlots.length)[0..4]; 164 foreach(texture; p.textureSlots) { 165 int e; 166 ubyte[] tex = write_image_mem(IF_TGA, texture.width, texture.height, texture.getTextureData(), 4, e); 167 app ~= nativeToBigEndian(cast(uint)tex.length)[0..4]; 168 app ~= (cast(ubyte)IN_TEX_TGA); 169 app ~= (tex); 170 } 171 172 // Begin extended section 173 app ~= EXT_SECTION; 174 app ~= nativeToBigEndian(cast(uint)p.extData.length)[0..4]; 175 176 foreach(name, payload; p.extData) { 177 178 // Write payload name and its length 179 app ~= nativeToBigEndian(cast(uint)name.length)[0..4]; 180 app ~= cast(ubyte[])name; 181 182 // Write payload length and payload 183 string payloadText = payload.toString; 184 app ~= nativeToBigEndian(cast(uint)payloadText.length)[0..4]; 185 app ~= cast(ubyte[])payloadText; 186 187 } 188 189 190 // Write it out to file 191 write(file, app.data); 192 } 193 194 enum IN_TEX_PNG = 0u; /// PNG encoded Inochi2D texture 195 enum IN_TEX_TGA = 1u; /// TGA encoded Inochi2D texture 196 enum IN_TEX_BC7 = 2u; /// BC7 encoded Inochi2D texture 197 198 /** 199 Writes a puppet to file 200 */ 201 void inWriteJSONPuppet(Puppet p, string file) { 202 isLoadingINP_ = false; 203 write(file, inToJson(p)); 204 }