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