1 # SPDX-FileCopyrightText: 2006-2012 assimp team
2 # SPDX-FileCopyrightText: 2013 Blender Foundation
4 # SPDX-License-Identifier: GPL-2.0-or-later
13 from struct
import unpack
16 from io
import BytesIO
18 from . import data_types
20 # at the end of each nested block, there is a NUL record to indicate
21 # that the sub-scope exists (i.e. to distinguish between P: and P : {})
22 _BLOCK_SENTINEL_LENGTH
= ...
23 _BLOCK_SENTINEL_DATA
= ...
24 read_fbx_elem_start
= ...
25 _IS_BIG_ENDIAN
= (__import__("sys").byteorder
!= 'little')
26 _HEAD_MAGIC
= b
'Kaydara FBX Binary\x20\x20\x00\x1a\x00'
27 from collections
import namedtuple
28 FBXElem
= namedtuple("FBXElem", ("id", "props", "props_type", "elems"))
33 return unpack(b
'<I', read(4))[0]
37 return unpack(b
'B', read(1))[0]
40 def read_string_ubyte(read
):
41 size
= read_ubyte(read
)
46 def read_array_params(read
):
47 return unpack(b
'<III', read(12))
50 def read_elem_start32(read
):
51 end_offset
, prop_count
, _prop_length
, elem_id_size
= unpack(b
'<IIIB', read(13))
52 elem_id
= read(elem_id_size
) if elem_id_size
else b
""
53 return end_offset
, prop_count
, elem_id
56 def read_elem_start64(read
):
57 end_offset
, prop_count
, _prop_length
, elem_id_size
= unpack(b
'<QQQB', read(25))
58 elem_id
= read(elem_id_size
) if elem_id_size
else b
""
59 return end_offset
, prop_count
, elem_id
62 def unpack_array(read
, array_type
, array_stride
, array_byteswap
):
63 length
, encoding
, comp_len
= read_array_params(read
)
70 data
= zlib
.decompress(data
)
72 assert(length
* array_stride
== len(data
))
74 data_array
= array
.array(array_type
, data
)
75 if array_byteswap
and _IS_BIG_ENDIAN
:
81 b
'Z'[0]: lambda read
: unpack(b
'<b', read(1))[0], # byte
82 b
'Y'[0]: lambda read
: unpack(b
'<h', read(2))[0], # 16 bit int
83 b
'C'[0]: lambda read
: unpack(b
'?', read(1))[0], # 1 bit bool (yes/no)
84 b
'I'[0]: lambda read
: unpack(b
'<i', read(4))[0], # 32 bit int
85 b
'F'[0]: lambda read
: unpack(b
'<f', read(4))[0], # 32 bit float
86 b
'D'[0]: lambda read
: unpack(b
'<d', read(8))[0], # 64 bit float
87 b
'L'[0]: lambda read
: unpack(b
'<q', read(8))[0], # 64 bit int
88 b
'R'[0]: lambda read
: read(read_uint(read
)), # binary data
89 b
'S'[0]: lambda read
: read(read_uint(read
)), # string data
90 b
'f'[0]: lambda read
: unpack_array(read
, data_types
.ARRAY_FLOAT32
, 4, False), # array (float)
91 b
'i'[0]: lambda read
: unpack_array(read
, data_types
.ARRAY_INT32
, 4, True), # array (int)
92 b
'd'[0]: lambda read
: unpack_array(read
, data_types
.ARRAY_FLOAT64
, 8, False), # array (double)
93 b
'l'[0]: lambda read
: unpack_array(read
, data_types
.ARRAY_INT64
, 8, True), # array (long)
94 b
'b'[0]: lambda read
: unpack_array(read
, data_types
.ARRAY_BOOL
, 1, False), # array (bool)
95 b
'c'[0]: lambda read
: unpack_array(read
, data_types
.ARRAY_BYTE
, 1, False), # array (ubyte)
99 # FBX 7500 (aka FBX2016) introduces incompatible changes at binary level:
100 # * The NULL block marking end of nested stuff switches from 13 bytes long to 25 bytes long.
101 # * The FBX element metadata (end_offset, prop_count and prop_length) switch from uint32 to uint64.
102 def init_version(fbx_version
):
103 global _BLOCK_SENTINEL_LENGTH
, _BLOCK_SENTINEL_DATA
, read_fbx_elem_start
105 _BLOCK_SENTINEL_LENGTH
= ...
106 _BLOCK_SENTINEL_DATA
= ...
108 if fbx_version
< 7500:
109 _BLOCK_SENTINEL_LENGTH
= 13
110 read_fbx_elem_start
= read_elem_start32
112 _BLOCK_SENTINEL_LENGTH
= 25
113 read_fbx_elem_start
= read_elem_start64
114 _BLOCK_SENTINEL_DATA
= (b
'\0' * _BLOCK_SENTINEL_LENGTH
)
117 def read_elem(read
, tell
, use_namedtuple
, tell_file_offset
=0):
118 # [0] the offset at which this block ends
119 # [1] the number of properties in the scope
120 # [2] the length of the property list
121 # [3] elem name length
122 # [4] elem name of the scope/key
123 # read_fbx_elem_start does not return [2] because we don't use it and does not return [3] because it is only used to
125 end_offset
, prop_count
, elem_id
= read_fbx_elem_start(read
)
129 elem_props_type
= bytearray(prop_count
) # elem property types
130 elem_props_data
= [None] * prop_count
# elem properties (if any)
131 elem_subtree
= [] # elem children (if any)
133 for i
in range(prop_count
):
134 data_type
= read(1)[0]
135 elem_props_data
[i
] = read_data_dict
[data_type
](read
)
136 elem_props_type
[i
] = data_type
139 local_end_offset
= end_offset
- tell_file_offset
141 if pos
< local_end_offset
:
142 # The default BufferedReader used when `open()`-ing files in 'rb' mode has to get the raw stream position from
143 # the OS every time its tell() function is called. This is about 10 times slower than the tell() function of
144 # BytesIO objects, so reading chunks of bytes from the file into memory at once and exposing them through
145 # BytesIO can give better performance. We know the total size of each element's subtree so can read entire
146 # subtrees into memory at a time.
147 # The "Objects" element's subtree, however, usually makes up most of the file, so we specifically avoid reading
148 # all its sub-elements into memory at once to reduce memory requirements at the cost of slightly worse
149 # performance when memory is not a concern.
150 # If we're currently reading directly from the opened file, then tell_file_offset will be zero.
151 if tell_file_offset
== 0 and elem_id
!= b
"Objects":
152 block_bytes_remaining
= local_end_offset
- pos
154 # Read the entire subtree
155 sub_elem_bytes
= read(block_bytes_remaining
)
156 num_bytes_read
= len(sub_elem_bytes
)
157 if num_bytes_read
!= block_bytes_remaining
:
158 raise IOError("failed to read complete nested block, expected %i bytes, but only got %i"
159 % (block_bytes_remaining
, num_bytes_read
))
161 # BytesIO provides IO API for reading bytes in memory, so we can use the same code as reading bytes directly
163 f
= BytesIO(sub_elem_bytes
)
166 # The new `tell` function starts at zero and is offset by `pos` bytes from the start of the file.
168 tell_file_offset
= pos
169 sub_tree_end
= block_bytes_remaining
- _BLOCK_SENTINEL_LENGTH
171 # The `tell` function is unchanged, so starts at the value returned by `tell()`, which is still `pos`
172 # because no reads have been made since then.
174 sub_tree_end
= local_end_offset
- _BLOCK_SENTINEL_LENGTH
176 sub_pos
= start_sub_pos
177 while sub_pos
< sub_tree_end
:
178 elem_subtree
.append(read_elem(read
, tell
, use_namedtuple
, tell_file_offset
))
181 # At the end of each subtree there should be a sentinel (an empty element with all bytes set to zero).
182 if read(_BLOCK_SENTINEL_LENGTH
) != _BLOCK_SENTINEL_DATA
:
183 raise IOError("failed to read nested block sentinel, "
184 "expected all bytes to be 0")
186 # Update `pos` for the number of bytes that have been read.
187 pos
+= (sub_pos
- start_sub_pos
) + _BLOCK_SENTINEL_LENGTH
189 if pos
!= local_end_offset
:
190 raise IOError("scope length not reached, something is wrong")
192 args
= (elem_id
, elem_props_data
, elem_props_type
, elem_subtree
)
193 return FBXElem(*args
) if use_namedtuple
else args
196 def parse_version(fn
):
198 Return the FBX version,
199 if the file isn't a binary FBX return zero.
201 with
open(fn
, 'rb') as f
:
204 if read(len(_HEAD_MAGIC
)) != _HEAD_MAGIC
:
207 return read_uint(read
)
210 def parse(fn
, use_namedtuple
=True):
213 with
open(fn
, 'rb') as f
:
217 if read(len(_HEAD_MAGIC
)) != _HEAD_MAGIC
:
218 raise IOError("Invalid header")
220 fbx_version
= read_uint(read
)
221 init_version(fbx_version
)
224 elem
= read_elem(read
, tell
, use_namedtuple
)
227 root_elems
.append(elem
)
229 args
= (b
'', [], bytearray(0), root_elems
)
230 return FBXElem(*args
) if use_namedtuple
else args
, fbx_version