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 }