Quiet RNA warning
[blender-addons.git] / io_scene_fbx / encode_bin.py
blob2023a495c5f03c9c42585deefd12ee4bbaefe1cf
1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
19 # <pep8 compliant>
21 # Script copyright (C) 2013 Campbell Barton
23 try:
24 from . import data_types
25 except:
26 import data_types
28 from struct import pack
29 import array
30 import zlib
32 _BLOCK_SENTINEL_LENGTH = 13
33 _BLOCK_SENTINEL_DATA = (b'\0' * _BLOCK_SENTINEL_LENGTH)
34 _IS_BIG_ENDIAN = (__import__("sys").byteorder != 'little')
35 _HEAD_MAGIC = b'Kaydara FBX Binary\x20\x20\x00\x1a\x00'
37 # fbx has very strict CRC rules, all based on file timestamp
38 # until we figure these out, write files at a fixed time. (workaround!)
40 # Assumes: CreationTime
41 _TIME_ID = b'1970-01-01 10:00:00:000'
42 _FILE_ID = b'\x28\xb3\x2a\xeb\xb6\x24\xcc\xc2\xbf\xc8\xb0\x2a\xa9\x2b\xfc\xf1'
43 _FOOT_ID = b'\xfa\xbc\xab\x09\xd0\xc8\xd4\x66\xb1\x76\xfb\x83\x1c\xf7\x26\x7e'
45 # Awful exceptions: those "classes" of elements seem to need block sentinel even when having no children and some props.
46 _ELEMS_ID_ALWAYS_BLOCK_SENTINEL = {b"AnimationStack", b"AnimationLayer"}
49 class FBXElem:
50 __slots__ = (
51 "id",
52 "props",
53 "props_type",
54 "elems",
56 "_props_length", # combine length of props
57 "_end_offset", # byte offset from the start of the file.
60 def __init__(self, id):
61 assert(len(id) < 256) # length must fit in a uint8
62 self.id = id
63 self.props = []
64 self.props_type = bytearray()
65 self.elems = []
66 self._end_offset = -1
67 self._props_length = -1
69 def add_bool(self, data):
70 assert(isinstance(data, bool))
71 data = pack('?', data)
73 self.props_type.append(data_types.BOOL)
74 self.props.append(data)
76 def add_int16(self, data):
77 assert(isinstance(data, int))
78 data = pack('<h', data)
80 self.props_type.append(data_types.INT16)
81 self.props.append(data)
83 def add_int32(self, data):
84 assert(isinstance(data, int))
85 data = pack('<i', data)
87 self.props_type.append(data_types.INT32)
88 self.props.append(data)
90 def add_int64(self, data):
91 assert(isinstance(data, int))
92 data = pack('<q', data)
94 self.props_type.append(data_types.INT64)
95 self.props.append(data)
97 def add_float32(self, data):
98 assert(isinstance(data, float))
99 data = pack('<f', data)
101 self.props_type.append(data_types.FLOAT32)
102 self.props.append(data)
104 def add_float64(self, data):
105 assert(isinstance(data, float))
106 data = pack('<d', data)
108 self.props_type.append(data_types.FLOAT64)
109 self.props.append(data)
111 def add_bytes(self, data):
112 assert(isinstance(data, bytes))
113 data = pack('<I', len(data)) + data
115 self.props_type.append(data_types.BYTES)
116 self.props.append(data)
118 def add_string(self, data):
119 assert(isinstance(data, bytes))
120 data = pack('<I', len(data)) + data
122 self.props_type.append(data_types.STRING)
123 self.props.append(data)
125 def add_string_unicode(self, data):
126 assert(isinstance(data, str))
127 data = data.encode('utf8')
128 data = pack('<I', len(data)) + data
130 self.props_type.append(data_types.STRING)
131 self.props.append(data)
133 def _add_array_helper(self, data, array_type, prop_type):
134 assert(isinstance(data, array.array))
135 assert(data.typecode == array_type)
137 length = len(data)
139 if _IS_BIG_ENDIAN:
140 data = data[:]
141 data.byteswap()
142 data = data.tobytes()
144 # mimic behavior of fbxconverter (also common sense)
145 # we could make this configurable.
146 encoding = 0 if len(data) <= 128 else 1
147 if encoding == 0:
148 pass
149 elif encoding == 1:
150 data = zlib.compress(data, 1)
152 comp_len = len(data)
154 data = pack('<3I', length, encoding, comp_len) + data
156 self.props_type.append(prop_type)
157 self.props.append(data)
159 def add_int32_array(self, data):
160 if not isinstance(data, array.array):
161 data = array.array(data_types.ARRAY_INT32, data)
162 self._add_array_helper(data, data_types.ARRAY_INT32, data_types.INT32_ARRAY)
164 def add_int64_array(self, data):
165 if not isinstance(data, array.array):
166 data = array.array(data_types.ARRAY_INT64, data)
167 self._add_array_helper(data, data_types.ARRAY_INT64, data_types.INT64_ARRAY)
169 def add_float32_array(self, data):
170 if not isinstance(data, array.array):
171 data = array.array(data_types.ARRAY_FLOAT32, data)
172 self._add_array_helper(data, data_types.ARRAY_FLOAT32, data_types.FLOAT32_ARRAY)
174 def add_float64_array(self, data):
175 if not isinstance(data, array.array):
176 data = array.array(data_types.ARRAY_FLOAT64, data)
177 self._add_array_helper(data, data_types.ARRAY_FLOAT64, data_types.FLOAT64_ARRAY)
179 def add_bool_array(self, data):
180 if not isinstance(data, array.array):
181 data = array.array(data_types.ARRAY_BOOL, data)
182 self._add_array_helper(data, data_types.ARRAY_BOOL, data_types.BOOL_ARRAY)
184 def add_byte_array(self, data):
185 if not isinstance(data, array.array):
186 data = array.array(data_types.ARRAY_BYTE, data)
187 self._add_array_helper(data, data_types.ARRAY_BYTE, data_types.BYTE_ARRAY)
189 # -------------------------
190 # internal helper functions
192 def _calc_offsets(self, offset, is_last):
194 Call before writing, calculates fixed offsets.
196 assert(self._end_offset == -1)
197 assert(self._props_length == -1)
199 offset += 12 # 3 uints
200 offset += 1 + len(self.id) # len + idname
202 props_length = 0
203 for data in self.props:
204 # 1 byte for the prop type
205 props_length += 1 + len(data)
206 self._props_length = props_length
207 offset += props_length
209 offset = self._calc_offsets_children(offset, is_last)
211 self._end_offset = offset
212 return offset
214 def _calc_offsets_children(self, offset, is_last):
215 if self.elems:
216 elem_last = self.elems[-1]
217 for elem in self.elems:
218 offset = elem._calc_offsets(offset, (elem is elem_last))
219 offset += _BLOCK_SENTINEL_LENGTH
220 elif not self.props or self.id in _ELEMS_ID_ALWAYS_BLOCK_SENTINEL:
221 if not is_last:
222 offset += _BLOCK_SENTINEL_LENGTH
224 return offset
226 def _write(self, write, tell, is_last):
227 assert(self._end_offset != -1)
228 assert(self._props_length != -1)
230 write(pack('<3I', self._end_offset, len(self.props), self._props_length))
232 write(bytes((len(self.id),)))
233 write(self.id)
235 for i, data in enumerate(self.props):
236 write(bytes((self.props_type[i],)))
237 write(data)
239 self._write_children(write, tell, is_last)
241 if tell() != self._end_offset:
242 raise IOError("scope length not reached, "
243 "something is wrong (%d)" % (end_offset - tell()))
245 def _write_children(self, write, tell, is_last):
246 if self.elems:
247 elem_last = self.elems[-1]
248 for elem in self.elems:
249 assert(elem.id != b'')
250 elem._write(write, tell, (elem is elem_last))
251 write(_BLOCK_SENTINEL_DATA)
252 elif not self.props or self.id in _ELEMS_ID_ALWAYS_BLOCK_SENTINEL:
253 if not is_last:
254 write(_BLOCK_SENTINEL_DATA)
257 def _write_timedate_hack(elem_root):
258 # perform 2 changes
259 # - set the FileID
260 # - set the CreationTime
262 ok = 0
263 for elem in elem_root.elems:
264 if elem.id == b'FileId':
265 assert(elem.props_type[0] == b'R'[0])
266 assert(len(elem.props_type) == 1)
267 elem.props.clear()
268 elem.props_type.clear()
270 elem.add_bytes(_FILE_ID)
271 ok += 1
272 elif elem.id == b'CreationTime':
273 assert(elem.props_type[0] == b'S'[0])
274 assert(len(elem.props_type) == 1)
275 elem.props.clear()
276 elem.props_type.clear()
278 elem.add_string(_TIME_ID)
279 ok += 1
281 if ok == 2:
282 break
284 if ok != 2:
285 print("Missing fields!")
288 def write(fn, elem_root, version):
289 assert(elem_root.id == b'')
291 with open(fn, 'wb') as f:
292 write = f.write
293 tell = f.tell
295 write(_HEAD_MAGIC)
296 write(pack('<I', version))
298 # hack since we don't decode time.
299 # ideally we would _not_ modify this data.
300 _write_timedate_hack(elem_root)
302 elem_root._calc_offsets_children(tell(), False)
303 elem_root._write_children(write, tell, False)
305 write(_FOOT_ID)
306 write(b'\x00' * 4)
308 # padding for alignment (values between 1 & 16 observed)
309 # if already aligned to 16, add a full 16 bytes padding.
310 ofs = tell()
311 pad = ((ofs + 15) & ~15) - ofs
312 if pad == 0:
313 pad = 16
315 write(b'\0' * pad)
317 write(pack('<I', version))
319 # unknown magic (always the same)
320 write(b'\0' * 120)
321 write(b'\xf8\x5a\x8c\x6a\xde\xf5\xd9\x7e\xec\xe9\x0c\xe3\x75\x8f\x29\x0b')