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 }