Fix #104672: FBX Byte type unsupported
[blender-addons.git] / io_scene_fbx / parse_fbx.py
blobfd4868fbe6d70b911a4429c1001cb3790b458429
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 # Script copyright (C) 2006-2012, assimp team
4 # Script copyright (C) 2013 Blender Foundation
6 __all__ = (
7 "parse",
8 "data_types",
9 "parse_version",
10 "FBXElem",
13 from struct import unpack
14 import array
15 import zlib
17 from . import data_types
19 # at the end of each nested block, there is a NUL record to indicate
20 # that the sub-scope exists (i.e. to distinguish between P: and P : {})
21 _BLOCK_SENTINEL_LENGTH = ...
22 _BLOCK_SENTINEL_DATA = ...
23 read_fbx_elem_uint = ...
24 _IS_BIG_ENDIAN = (__import__("sys").byteorder != 'little')
25 _HEAD_MAGIC = b'Kaydara FBX Binary\x20\x20\x00\x1a\x00'
26 from collections import namedtuple
27 FBXElem = namedtuple("FBXElem", ("id", "props", "props_type", "elems"))
28 del namedtuple
31 def read_uint(read):
32 return unpack(b'<I', read(4))[0]
35 def read_uint64(read):
36 return unpack(b'<Q', read(8))[0]
39 def read_ubyte(read):
40 return unpack(b'B', read(1))[0]
43 def read_string_ubyte(read):
44 size = read_ubyte(read)
45 data = read(size)
46 return data
49 def unpack_array(read, array_type, array_stride, array_byteswap):
50 length = read_uint(read)
51 encoding = read_uint(read)
52 comp_len = read_uint(read)
54 data = read(comp_len)
56 if encoding == 0:
57 pass
58 elif encoding == 1:
59 data = zlib.decompress(data)
61 assert(length * array_stride == len(data))
63 data_array = array.array(array_type, data)
64 if array_byteswap and _IS_BIG_ENDIAN:
65 data_array.byteswap()
66 return data_array
69 read_data_dict = {
70 b'Z'[0]: lambda read: unpack(b'<b', read(1))[0], # byte
71 b'Y'[0]: lambda read: unpack(b'<h', read(2))[0], # 16 bit int
72 b'C'[0]: lambda read: unpack(b'?', read(1))[0], # 1 bit bool (yes/no)
73 b'I'[0]: lambda read: unpack(b'<i', read(4))[0], # 32 bit int
74 b'F'[0]: lambda read: unpack(b'<f', read(4))[0], # 32 bit float
75 b'D'[0]: lambda read: unpack(b'<d', read(8))[0], # 64 bit float
76 b'L'[0]: lambda read: unpack(b'<q', read(8))[0], # 64 bit int
77 b'R'[0]: lambda read: read(read_uint(read)), # binary data
78 b'S'[0]: lambda read: read(read_uint(read)), # string data
79 b'f'[0]: lambda read: unpack_array(read, data_types.ARRAY_FLOAT32, 4, False), # array (float)
80 b'i'[0]: lambda read: unpack_array(read, data_types.ARRAY_INT32, 4, True), # array (int)
81 b'd'[0]: lambda read: unpack_array(read, data_types.ARRAY_FLOAT64, 8, False), # array (double)
82 b'l'[0]: lambda read: unpack_array(read, data_types.ARRAY_INT64, 8, True), # array (long)
83 b'b'[0]: lambda read: unpack_array(read, data_types.ARRAY_BOOL, 1, False), # array (bool)
84 b'c'[0]: lambda read: unpack_array(read, data_types.ARRAY_BYTE, 1, False), # array (ubyte)
88 # FBX 7500 (aka FBX2016) introduces incompatible changes at binary level:
89 # * The NULL block marking end of nested stuff switches from 13 bytes long to 25 bytes long.
90 # * The FBX element metadata (end_offset, prop_count and prop_length) switch from uint32 to uint64.
91 def init_version(fbx_version):
92 global _BLOCK_SENTINEL_LENGTH, _BLOCK_SENTINEL_DATA, read_fbx_elem_uint
94 _BLOCK_SENTINEL_LENGTH = ...
95 _BLOCK_SENTINEL_DATA = ...
96 read_fbx_elem_uint = ...
98 if fbx_version < 7500:
99 _BLOCK_SENTINEL_LENGTH = 13
100 read_fbx_elem_uint = read_uint
101 else:
102 _BLOCK_SENTINEL_LENGTH = 25
103 read_fbx_elem_uint = read_uint64
104 _BLOCK_SENTINEL_DATA = (b'\0' * _BLOCK_SENTINEL_LENGTH)
107 def read_elem(read, tell, use_namedtuple):
108 # [0] the offset at which this block ends
109 # [1] the number of properties in the scope
110 # [2] the length of the property list
111 end_offset = read_fbx_elem_uint(read)
112 if end_offset == 0:
113 return None
115 prop_count = read_fbx_elem_uint(read)
116 prop_length = read_fbx_elem_uint(read)
118 elem_id = read_string_ubyte(read) # elem name of the scope/key
119 elem_props_type = bytearray(prop_count) # elem property types
120 elem_props_data = [None] * prop_count # elem properties (if any)
121 elem_subtree = [] # elem children (if any)
123 for i in range(prop_count):
124 data_type = read(1)[0]
125 elem_props_data[i] = read_data_dict[data_type](read)
126 elem_props_type[i] = data_type
128 if tell() < end_offset:
129 while tell() < (end_offset - _BLOCK_SENTINEL_LENGTH):
130 elem_subtree.append(read_elem(read, tell, use_namedtuple))
132 if read(_BLOCK_SENTINEL_LENGTH) != _BLOCK_SENTINEL_DATA:
133 raise IOError("failed to read nested block sentinel, "
134 "expected all bytes to be 0")
136 if tell() != end_offset:
137 raise IOError("scope length not reached, something is wrong")
139 args = (elem_id, elem_props_data, elem_props_type, elem_subtree)
140 return FBXElem(*args) if use_namedtuple else args
143 def parse_version(fn):
145 Return the FBX version,
146 if the file isn't a binary FBX return zero.
148 with open(fn, 'rb') as f:
149 read = f.read
151 if read(len(_HEAD_MAGIC)) != _HEAD_MAGIC:
152 return 0
154 return read_uint(read)
157 def parse(fn, use_namedtuple=True):
158 root_elems = []
160 with open(fn, 'rb') as f:
161 read = f.read
162 tell = f.tell
164 if read(len(_HEAD_MAGIC)) != _HEAD_MAGIC:
165 raise IOError("Invalid header")
167 fbx_version = read_uint(read)
168 init_version(fbx_version)
170 while True:
171 elem = read_elem(read, tell, use_namedtuple)
172 if elem is None:
173 break
174 root_elems.append(elem)
176 args = (b'', [], bytearray(0), root_elems)
177 return FBXElem(*args) if use_namedtuple else args, fbx_version