1 # SPDX-License-Identifier: GPL-2.0-or-later
3 # Script copyright (C) 2013 Campbell Barton
6 from . import data_types
10 from struct
import pack
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"}
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
46 self
.props_type
= bytearray()
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
)
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
132 data
= zlib
.compress(data
, 1)
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
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
196 def _calc_offsets_children(self
, offset
, is_last
):
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
:
204 offset
+= _BLOCK_SENTINEL_LENGTH
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),)))
217 for i
, data
in enumerate(self
.props
):
218 write(bytes((self
.props_type
[i
],)))
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
):
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
:
236 write(_BLOCK_SENTINEL_DATA
)
239 def _write_timedate_hack(elem_root
):
242 # - set the CreationTime
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)
250 elem
.props_type
.clear()
252 elem
.add_bytes(_FILE_ID
)
254 elif elem
.id == b
'CreationTime':
255 assert(elem
.props_type
[0] == b
'S'[0])
256 assert(len(elem
.props_type
) == 1)
258 elem
.props_type
.clear()
260 elem
.add_string(_TIME_ID
)
267 print("Missing fields!")
270 def write(fn
, elem_root
, version
):
271 assert(elem_root
.id == b
'')
273 with
open(fn
, 'wb') as f
:
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)
290 # padding for alignment (values between 1 & 16 observed)
291 # if already aligned to 16, add a full 16 bytes padding.
293 pad
= ((ofs
+ 15) & ~
15) - ofs
299 write(pack('<I', version
))
301 # unknown magic (always the same)
303 write(b
'\xf8\x5a\x8c\x6a\xde\xf5\xd9\x7e\xec\xe9\x0c\xe3\x75\x8f\x29\x0b')