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 #####
21 # Script copyright (C) 2013 Campbell Barton
24 from . import data_types
28 from struct
import pack
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"}
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
64 self
.props_type
= bytearray()
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
)
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
150 data
= zlib
.compress(data
, 1)
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
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
214 def _calc_offsets_children(self
, offset
, is_last
):
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
:
222 offset
+= _BLOCK_SENTINEL_LENGTH
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),)))
235 for i
, data
in enumerate(self
.props
):
236 write(bytes((self
.props_type
[i
],)))
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
):
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
:
254 write(_BLOCK_SENTINEL_DATA
)
257 def _write_timedate_hack(elem_root
):
260 # - set the CreationTime
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)
268 elem
.props_type
.clear()
270 elem
.add_bytes(_FILE_ID
)
272 elif elem
.id == b
'CreationTime':
273 assert(elem
.props_type
[0] == b
'S'[0])
274 assert(len(elem
.props_type
) == 1)
276 elem
.props_type
.clear()
278 elem
.add_string(_TIME_ID
)
285 print("Missing fields!")
288 def write(fn
, elem_root
, version
):
289 assert(elem_root
.id == b
'')
291 with
open(fn
, 'wb') as f
:
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)
308 # padding for alignment (values between 1 & 16 observed)
309 # if already aligned to 16, add a full 16 bytes padding.
311 pad
= ((ofs
+ 15) & ~
15) - ofs
317 write(pack('<I', version
))
319 # unknown magic (always the same)
321 write(b
'\xf8\x5a\x8c\x6a\xde\xf5\xd9\x7e\xec\xe9\x0c\xe3\x75\x8f\x29\x0b')