Skinify: fix shape generation
[blender-addons.git] / io_scene_fbx / encode_bin.py
blob516221e66c7c93f21f5543c140876d516d44191e
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 # Script copyright (C) 2013 Campbell Barton
5 try:
6 from . import data_types
7 except:
8 import data_types
10 from struct import pack
11 import array
12 import zlib
14 _BLOCK_SENTINEL_LENGTH = 13
15 _BLOCK_SENTINEL_DATA = (b'\0' * _BLOCK_SENTINEL_LENGTH)
16 _IS_BIG_ENDIAN = (__import__("sys").byteorder != 'little')
17 _HEAD_MAGIC = b'Kaydara FBX Binary\x20\x20\x00\x1a\x00'
19 # fbx has very strict CRC rules, all based on file timestamp
20 # until we figure these out, write files at a fixed time. (workaround!)
22 # Assumes: CreationTime
23 _TIME_ID = b'1970-01-01 10:00:00:000'
24 _FILE_ID = b'\x28\xb3\x2a\xeb\xb6\x24\xcc\xc2\xbf\xc8\xb0\x2a\xa9\x2b\xfc\xf1'
25 _FOOT_ID = b'\xfa\xbc\xab\x09\xd0\xc8\xd4\x66\xb1\x76\xfb\x83\x1c\xf7\x26\x7e'
27 # Awful exceptions: those "classes" of elements seem to need block sentinel even when having no children and some props.
28 _ELEMS_ID_ALWAYS_BLOCK_SENTINEL = {b"AnimationStack", b"AnimationLayer"}
31 class FBXElem:
32 __slots__ = (
33 "id",
34 "props",
35 "props_type",
36 "elems",
38 "_props_length", # combine length of props
39 "_end_offset", # byte offset from the start of the file.
42 def __init__(self, id):
43 assert(len(id) < 256) # length must fit in a uint8
44 self.id = id
45 self.props = []
46 self.props_type = bytearray()
47 self.elems = []
48 self._end_offset = -1
49 self._props_length = -1
51 def add_bool(self, data):
52 assert(isinstance(data, bool))
53 data = pack('?', data)
55 self.props_type.append(data_types.BOOL)
56 self.props.append(data)
58 def add_int16(self, data):
59 assert(isinstance(data, int))
60 data = pack('<h', data)
62 self.props_type.append(data_types.INT16)
63 self.props.append(data)
65 def add_int32(self, data):
66 assert(isinstance(data, int))
67 data = pack('<i', data)
69 self.props_type.append(data_types.INT32)
70 self.props.append(data)
72 def add_int64(self, data):
73 assert(isinstance(data, int))
74 data = pack('<q', data)
76 self.props_type.append(data_types.INT64)
77 self.props.append(data)
79 def add_float32(self, data):
80 assert(isinstance(data, float))
81 data = pack('<f', data)
83 self.props_type.append(data_types.FLOAT32)
84 self.props.append(data)
86 def add_float64(self, data):
87 assert(isinstance(data, float))
88 data = pack('<d', data)
90 self.props_type.append(data_types.FLOAT64)
91 self.props.append(data)
93 def add_bytes(self, data):
94 assert(isinstance(data, bytes))
95 data = pack('<I', len(data)) + data
97 self.props_type.append(data_types.BYTES)
98 self.props.append(data)
100 def add_string(self, data):
101 assert(isinstance(data, bytes))
102 data = pack('<I', len(data)) + data
104 self.props_type.append(data_types.STRING)
105 self.props.append(data)
107 def add_string_unicode(self, data):
108 assert(isinstance(data, str))
109 data = data.encode('utf8')
110 data = pack('<I', len(data)) + data
112 self.props_type.append(data_types.STRING)
113 self.props.append(data)
115 def _add_array_helper(self, data, array_type, prop_type):
116 assert(isinstance(data, array.array))
117 assert(data.typecode == array_type)
119 length = len(data)
121 if _IS_BIG_ENDIAN:
122 data = data[:]
123 data.byteswap()
124 data = data.tobytes()
126 # mimic behavior of fbxconverter (also common sense)
127 # we could make this configurable.
128 encoding = 0 if len(data) <= 128 else 1
129 if encoding == 0:
130 pass
131 elif encoding == 1:
132 data = zlib.compress(data, 1)
134 comp_len = len(data)
136 data = pack('<3I', length, encoding, comp_len) + data
138 self.props_type.append(prop_type)
139 self.props.append(data)
141 def add_int32_array(self, data):
142 if not isinstance(data, array.array):
143 data = array.array(data_types.ARRAY_INT32, data)
144 self._add_array_helper(data, data_types.ARRAY_INT32, data_types.INT32_ARRAY)
146 def add_int64_array(self, data):
147 if not isinstance(data, array.array):
148 data = array.array(data_types.ARRAY_INT64, data)
149 self._add_array_helper(data, data_types.ARRAY_INT64, data_types.INT64_ARRAY)
151 def add_float32_array(self, data):
152 if not isinstance(data, array.array):
153 data = array.array(data_types.ARRAY_FLOAT32, data)
154 self._add_array_helper(data, data_types.ARRAY_FLOAT32, data_types.FLOAT32_ARRAY)
156 def add_float64_array(self, data):
157 if not isinstance(data, array.array):
158 data = array.array(data_types.ARRAY_FLOAT64, data)
159 self._add_array_helper(data, data_types.ARRAY_FLOAT64, data_types.FLOAT64_ARRAY)
161 def add_bool_array(self, data):
162 if not isinstance(data, array.array):
163 data = array.array(data_types.ARRAY_BOOL, data)
164 self._add_array_helper(data, data_types.ARRAY_BOOL, data_types.BOOL_ARRAY)
166 def add_byte_array(self, data):
167 if not isinstance(data, array.array):
168 data = array.array(data_types.ARRAY_BYTE, data)
169 self._add_array_helper(data, data_types.ARRAY_BYTE, data_types.BYTE_ARRAY)
171 # -------------------------
172 # internal helper functions
174 def _calc_offsets(self, offset, is_last):
176 Call before writing, calculates fixed offsets.
178 assert(self._end_offset == -1)
179 assert(self._props_length == -1)
181 offset += 12 # 3 uints
182 offset += 1 + len(self.id) # len + idname
184 props_length = 0
185 for data in self.props:
186 # 1 byte for the prop type
187 props_length += 1 + len(data)
188 self._props_length = props_length
189 offset += props_length
191 offset = self._calc_offsets_children(offset, is_last)
193 self._end_offset = offset
194 return offset
196 def _calc_offsets_children(self, offset, is_last):
197 if self.elems:
198 elem_last = self.elems[-1]
199 for elem in self.elems:
200 offset = elem._calc_offsets(offset, (elem is elem_last))
201 offset += _BLOCK_SENTINEL_LENGTH
202 elif not self.props or self.id in _ELEMS_ID_ALWAYS_BLOCK_SENTINEL:
203 if not is_last:
204 offset += _BLOCK_SENTINEL_LENGTH
206 return offset
208 def _write(self, write, tell, is_last):
209 assert(self._end_offset != -1)
210 assert(self._props_length != -1)
212 write(pack('<3I', self._end_offset, len(self.props), self._props_length))
214 write(bytes((len(self.id),)))
215 write(self.id)
217 for i, data in enumerate(self.props):
218 write(bytes((self.props_type[i],)))
219 write(data)
221 self._write_children(write, tell, is_last)
223 if tell() != self._end_offset:
224 raise IOError("scope length not reached, "
225 "something is wrong (%d)" % (end_offset - tell()))
227 def _write_children(self, write, tell, is_last):
228 if self.elems:
229 elem_last = self.elems[-1]
230 for elem in self.elems:
231 assert(elem.id != b'')
232 elem._write(write, tell, (elem is elem_last))
233 write(_BLOCK_SENTINEL_DATA)
234 elif not self.props or self.id in _ELEMS_ID_ALWAYS_BLOCK_SENTINEL:
235 if not is_last:
236 write(_BLOCK_SENTINEL_DATA)
239 def _write_timedate_hack(elem_root):
240 # perform 2 changes
241 # - set the FileID
242 # - set the CreationTime
244 ok = 0
245 for elem in elem_root.elems:
246 if elem.id == b'FileId':
247 assert(elem.props_type[0] == b'R'[0])
248 assert(len(elem.props_type) == 1)
249 elem.props.clear()
250 elem.props_type.clear()
252 elem.add_bytes(_FILE_ID)
253 ok += 1
254 elif elem.id == b'CreationTime':
255 assert(elem.props_type[0] == b'S'[0])
256 assert(len(elem.props_type) == 1)
257 elem.props.clear()
258 elem.props_type.clear()
260 elem.add_string(_TIME_ID)
261 ok += 1
263 if ok == 2:
264 break
266 if ok != 2:
267 print("Missing fields!")
270 def write(fn, elem_root, version):
271 assert(elem_root.id == b'')
273 with open(fn, 'wb') as f:
274 write = f.write
275 tell = f.tell
277 write(_HEAD_MAGIC)
278 write(pack('<I', version))
280 # hack since we don't decode time.
281 # ideally we would _not_ modify this data.
282 _write_timedate_hack(elem_root)
284 elem_root._calc_offsets_children(tell(), False)
285 elem_root._write_children(write, tell, False)
287 write(_FOOT_ID)
288 write(b'\x00' * 4)
290 # padding for alignment (values between 1 & 16 observed)
291 # if already aligned to 16, add a full 16 bytes padding.
292 ofs = tell()
293 pad = ((ofs + 15) & ~15) - ofs
294 if pad == 0:
295 pad = 16
297 write(b'\0' * pad)
299 write(pack('<I', version))
301 # unknown magic (always the same)
302 write(b'\0' * 120)
303 write(b'\xf8\x5a\x8c\x6a\xde\xf5\xd9\x7e\xec\xe9\x0c\xe3\x75\x8f\x29\x0b')