Update for rename: constraint_orientation -> orient_type
[blender-addons.git] / io_scene_fbx / fbx2json.py
blobc5d47ade9b5fee7b7b3f473cdd8bec706852dad7
1 #!/usr/bin/env python3
2 # ##### BEGIN GPL LICENSE BLOCK #####
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software Foundation,
16 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # ##### END GPL LICENSE BLOCK #####
20 # <pep8 compliant>
22 # Script copyright (C) 2006-2012, assimp team
23 # Script copyright (C) 2013 Blender Foundation
25 """
26 Usage
27 =====
29 fbx2json [FILES]...
31 This script will write a JSON file for each FBX argument given.
34 Output
35 ======
37 The JSON data is formatted into a list of nested lists of 4 items:
39 ``[id, [data, ...], "data_types", [subtree, ...]]``
41 Where each list may be empty, and the items in
42 the subtree are formatted the same way.
44 data_types is a string, aligned with data that spesifies a type
45 for each property.
47 The types are as follows:
49 * 'Y': - INT16
50 * 'C': - BOOL
51 * 'I': - INT32
52 * 'F': - FLOAT32
53 * 'D': - FLOAT64
54 * 'L': - INT64
55 * 'R': - BYTES
56 * 'S': - STRING
57 * 'f': - FLOAT32_ARRAY
58 * 'i': - INT32_ARRAY
59 * 'd': - FLOAT64_ARRAY
60 * 'l': - INT64_ARRAY
61 * 'b': - BOOL ARRAY
62 * 'c': - BYTE ARRAY
64 Note that key:value pairs aren't used since the id's are not
65 ensured to be unique.
66 """
69 # ----------------------------------------------------------------------------
70 # FBX Binary Parser
72 from struct import unpack
73 import array
74 import zlib
76 # at the end of each nested block, there is a NUL record to indicate
77 # that the sub-scope exists (i.e. to distinguish between P: and P : {})
78 _BLOCK_SENTINEL_LENGTH = ...
79 _BLOCK_SENTINEL_DATA = ...
80 read_fbx_elem_uint = ...
81 _IS_BIG_ENDIAN = (__import__("sys").byteorder != 'little')
82 _HEAD_MAGIC = b'Kaydara FBX Binary\x20\x20\x00\x1a\x00'
83 from collections import namedtuple
84 FBXElem = namedtuple("FBXElem", ("id", "props", "props_type", "elems"))
85 del namedtuple
88 def read_uint(read):
89 return unpack(b'<I', read(4))[0]
92 def read_uint64(read):
93 return unpack(b'<Q', read(8))[0]
96 def read_ubyte(read):
97 return unpack(b'B', read(1))[0]
100 def read_string_ubyte(read):
101 size = read_ubyte(read)
102 data = read(size)
103 return data
106 def unpack_array(read, array_type, array_stride, array_byteswap):
107 length = read_uint(read)
108 encoding = read_uint(read)
109 comp_len = read_uint(read)
111 data = read(comp_len)
113 if encoding == 0:
114 pass
115 elif encoding == 1:
116 data = zlib.decompress(data)
118 assert(length * array_stride == len(data))
120 data_array = array.array(array_type, data)
121 if array_byteswap and _IS_BIG_ENDIAN:
122 data_array.byteswap()
123 return data_array
126 read_data_dict = {
127 b'Y'[0]: lambda read: unpack(b'<h', read(2))[0], # 16 bit int
128 b'C'[0]: lambda read: unpack(b'?', read(1))[0], # 1 bit bool (yes/no)
129 b'I'[0]: lambda read: unpack(b'<i', read(4))[0], # 32 bit int
130 b'F'[0]: lambda read: unpack(b'<f', read(4))[0], # 32 bit float
131 b'D'[0]: lambda read: unpack(b'<d', read(8))[0], # 64 bit float
132 b'L'[0]: lambda read: unpack(b'<q', read(8))[0], # 64 bit int
133 b'R'[0]: lambda read: read(read_uint(read)), # binary data
134 b'S'[0]: lambda read: read(read_uint(read)), # string data
135 b'f'[0]: lambda read: unpack_array(read, 'f', 4, False), # array (float)
136 b'i'[0]: lambda read: unpack_array(read, 'i', 4, True), # array (int)
137 b'd'[0]: lambda read: unpack_array(read, 'd', 8, False), # array (double)
138 b'l'[0]: lambda read: unpack_array(read, 'q', 8, True), # array (long)
139 b'b'[0]: lambda read: unpack_array(read, 'b', 1, False), # array (bool)
140 b'c'[0]: lambda read: unpack_array(read, 'B', 1, False), # array (ubyte)
144 # FBX 7500 (aka FBX2016) introduces incompatible changes at binary level:
145 # * The NULL block marking end of nested stuff switches from 13 bytes long to 25 bytes long.
146 # * The FBX element metadata (end_offset, prop_count and prop_length) switch from uint32 to uint64.
147 def init_version(fbx_version):
148 global _BLOCK_SENTINEL_LENGTH, _BLOCK_SENTINEL_DATA, read_fbx_elem_uint
150 assert(_BLOCK_SENTINEL_LENGTH == ...)
151 assert(_BLOCK_SENTINEL_DATA == ...)
153 if fbx_version < 7500:
154 _BLOCK_SENTINEL_LENGTH = 13
155 read_fbx_elem_uint = read_uint
156 else:
157 _BLOCK_SENTINEL_LENGTH = 25
158 read_fbx_elem_uint = read_uint64
159 _BLOCK_SENTINEL_DATA = (b'\0' * _BLOCK_SENTINEL_LENGTH)
162 def read_elem(read, tell, use_namedtuple):
163 # [0] the offset at which this block ends
164 # [1] the number of properties in the scope
165 # [2] the length of the property list
166 end_offset = read_fbx_elem_uint(read)
167 if end_offset == 0:
168 return None
170 prop_count = read_fbx_elem_uint(read)
171 prop_length = read_fbx_elem_uint(read)
173 elem_id = read_string_ubyte(read) # elem name of the scope/key
174 elem_props_type = bytearray(prop_count) # elem property types
175 elem_props_data = [None] * prop_count # elem properties (if any)
176 elem_subtree = [] # elem children (if any)
178 for i in range(prop_count):
179 data_type = read(1)[0]
180 elem_props_data[i] = read_data_dict[data_type](read)
181 elem_props_type[i] = data_type
183 if tell() < end_offset:
184 while tell() < (end_offset - _BLOCK_SENTINEL_LENGTH):
185 elem_subtree.append(read_elem(read, tell, use_namedtuple))
187 if read(_BLOCK_SENTINEL_LENGTH) != _BLOCK_SENTINEL_DATA:
188 raise IOError("failed to read nested block sentinel, "
189 "expected all bytes to be 0")
191 if tell() != end_offset:
192 raise IOError("scope length not reached, something is wrong")
194 args = (elem_id, elem_props_data, elem_props_type, elem_subtree)
195 return FBXElem(*args) if use_namedtuple else args
198 def parse_version(fn):
200 Return the FBX version,
201 if the file isn't a binary FBX return zero.
203 with open(fn, 'rb') as f:
204 read = f.read
206 if read(len(_HEAD_MAGIC)) != _HEAD_MAGIC:
207 return 0
209 return read_uint(read)
212 def parse(fn, use_namedtuple=True):
213 root_elems = []
215 with open(fn, 'rb') as f:
216 read = f.read
217 tell = f.tell
219 if read(len(_HEAD_MAGIC)) != _HEAD_MAGIC:
220 raise IOError("Invalid header")
222 fbx_version = read_uint(read)
223 init_version(fbx_version)
225 while True:
226 elem = read_elem(read, tell, use_namedtuple)
227 if elem is None:
228 break
229 root_elems.append(elem)
231 args = (b'', [], bytearray(0), root_elems)
232 return FBXElem(*args) if use_namedtuple else args, fbx_version
235 # ----------------------------------------------------------------------------
236 # Inline Modules
238 # pyfbx.data_types
239 data_types = type(array)("data_types")
240 data_types.__dict__.update(
241 dict(
242 INT16 = b'Y'[0],
243 BOOL = b'C'[0],
244 INT32 = b'I'[0],
245 FLOAT32 = b'F'[0],
246 FLOAT64 = b'D'[0],
247 INT64 = b'L'[0],
248 BYTES = b'R'[0],
249 STRING = b'S'[0],
250 FLOAT32_ARRAY = b'f'[0],
251 INT32_ARRAY = b'i'[0],
252 FLOAT64_ARRAY = b'd'[0],
253 INT64_ARRAY = b'l'[0],
254 BOOL_ARRAY = b'b'[0],
255 BYTE_ARRAY = b'c'[0],
258 # pyfbx.parse_bin
259 parse_bin = type(array)("parse_bin")
260 parse_bin.__dict__.update(
261 dict(
262 parse = parse
266 # ----------------------------------------------------------------------------
267 # JSON Converter
268 # from pyfbx import parse_bin, data_types
269 import json
270 import array
273 def fbx2json_property_as_string(prop, prop_type):
274 if prop_type == data_types.STRING:
275 prop_str = prop.decode('utf-8')
276 prop_str = prop_str.replace('\x00\x01', '::')
277 return json.dumps(prop_str)
278 else:
279 prop_py_type = type(prop)
280 if prop_py_type == bytes:
281 return json.dumps(repr(prop)[2:-1])
282 elif prop_py_type == bool:
283 return json.dumps(prop)
284 elif prop_py_type == array.array:
285 return repr(list(prop))
287 return repr(prop)
290 def fbx2json_properties_as_string(fbx_elem):
291 return ", ".join(fbx2json_property_as_string(*prop_item)
292 for prop_item in zip(fbx_elem.props,
293 fbx_elem.props_type))
296 def fbx2json_recurse(fw, fbx_elem, ident, is_last):
297 fbx_elem_id = fbx_elem.id.decode('utf-8')
298 fw('%s["%s", ' % (ident, fbx_elem_id))
299 fw('[%s], ' % fbx2json_properties_as_string(fbx_elem))
300 fw('"%s", ' % (fbx_elem.props_type.decode('ascii')))
302 fw('[')
303 if fbx_elem.elems:
304 fw('\n')
305 ident_sub = ident + " "
306 for fbx_elem_sub in fbx_elem.elems:
307 fbx2json_recurse(fw, fbx_elem_sub, ident_sub,
308 fbx_elem_sub is fbx_elem.elems[-1])
309 fw(']')
311 fw(']%s' % ('' if is_last else ',\n'))
314 def fbx2json(fn):
315 import os
317 fn_json = "%s.json" % os.path.splitext(fn)[0]
318 print("Writing: %r " % fn_json, end="")
319 fbx_root_elem, fbx_version = parse(fn, use_namedtuple=True)
320 print("(Version %d) ..." % fbx_version)
322 with open(fn_json, 'w', encoding="ascii", errors='xmlcharrefreplace') as f:
323 fw = f.write
324 fw('[\n')
325 ident_sub = " "
326 for fbx_elem_sub in fbx_root_elem.elems:
327 fbx2json_recurse(f.write, fbx_elem_sub, ident_sub,
328 fbx_elem_sub is fbx_root_elem.elems[-1])
329 fw(']\n')
332 # ----------------------------------------------------------------------------
333 # Command Line
335 def main():
336 import sys
338 if "--help" in sys.argv:
339 print(__doc__)
340 return
342 for arg in sys.argv[1:]:
343 try:
344 fbx2json(arg)
345 except:
346 print("Failed to convert %r, error:" % arg)
348 import traceback
349 traceback.print_exc()
352 if __name__ == "__main__":
353 main()