1# Copyright 2018-2019 The glTF-Blender-IO authors.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import struct
16import numpy as np
17
18from ..com.gltf2_io import Accessor
19from ..com.gltf2_io_constants import ComponentType, DataType
20
21
22class BinaryData():
23    """Binary reader."""
24    def __new__(cls, *args, **kwargs):
25        raise RuntimeError("%s should not be instantiated" % cls)
26
27    # Note that this function is not used in Blender importer, but is kept in
28    # Source code to be used in any pipeline that want to manage gltf/glb file in python
29    @staticmethod
30    def get_binary_from_accessor(gltf, accessor_idx):
31        """Get binary from accessor."""
32        accessor = gltf.data.accessors[accessor_idx]
33        if accessor.buffer_view is None:
34            return None
35
36        data = BinaryData.get_buffer_view(gltf, accessor.buffer_view)
37
38        accessor_offset = accessor.byte_offset
39        if accessor_offset is None:
40            accessor_offset = 0
41
42        return data[accessor_offset:]
43
44    @staticmethod
45    def get_buffer_view(gltf, buffer_view_idx):
46        """Get binary data for buffer view."""
47        buffer_view = gltf.data.buffer_views[buffer_view_idx]
48
49        if buffer_view.buffer in gltf.buffers.keys():
50            buffer = gltf.buffers[buffer_view.buffer]
51        else:
52            # load buffer
53            gltf.load_buffer(buffer_view.buffer)
54            buffer = gltf.buffers[buffer_view.buffer]
55
56        byte_offset = buffer_view.byte_offset
57        if byte_offset is None:
58            byte_offset = 0
59
60        return buffer[byte_offset:byte_offset + buffer_view.byte_length]
61
62    @staticmethod
63    def get_data_from_accessor(gltf, accessor_idx, cache=False):
64        """Get data from accessor."""
65        if accessor_idx in gltf.accessor_cache:
66            return gltf.accessor_cache[accessor_idx]
67
68        data = BinaryData.decode_accessor(gltf, accessor_idx).tolist()
69
70        if cache:
71            gltf.accessor_cache[accessor_idx] = data
72
73        return data
74
75    @staticmethod
76    def decode_accessor(gltf, accessor_idx, cache=False):
77        """Decodes accessor to 2D numpy array (count x num_components)."""
78        if accessor_idx in gltf.decode_accessor_cache:
79            return gltf.accessor_cache[accessor_idx]
80
81        accessor = gltf.data.accessors[accessor_idx]
82        array = BinaryData.decode_accessor_obj(gltf, accessor)
83
84        if cache:
85            gltf.accessor_cache[accessor_idx] = array
86            # Prevent accidentally modifying cached arrays
87            array.flags.writeable = False
88
89        return array
90
91    @staticmethod
92    def decode_accessor_obj(gltf, accessor):
93        # MAT2/3 have special alignment requirements that aren't handled. But it
94        # doesn't matter because nothing uses them.
95        assert accessor.type not in ['MAT2', 'MAT3']
96
97        dtype = ComponentType.to_numpy_dtype(accessor.component_type)
98        component_nb = DataType.num_elements(accessor.type)
99
100        if accessor.buffer_view is not None:
101            bufferView = gltf.data.buffer_views[accessor.buffer_view]
102            buffer_data = BinaryData.get_buffer_view(gltf, accessor.buffer_view)
103
104            accessor_offset = accessor.byte_offset or 0
105            buffer_data = buffer_data[accessor_offset:]
106
107            bytes_per_elem = dtype(1).nbytes
108            default_stride = bytes_per_elem * component_nb
109            stride = bufferView.byte_stride or default_stride
110
111            if stride == default_stride:
112                array = np.frombuffer(
113                    buffer_data,
114                    dtype=np.dtype(dtype).newbyteorder('<'),
115                    count=accessor.count * component_nb,
116                )
117                array = array.reshape(accessor.count, component_nb)
118
119            else:
120                # The data looks like
121                #   XXXppXXXppXXXppXXX
122                # where X are the components and p are padding.
123                # One XXXpp group is one stride's worth of data.
124                assert stride % bytes_per_elem == 0
125                elems_per_stride = stride // bytes_per_elem
126                num_elems = (accessor.count - 1) * elems_per_stride + component_nb
127
128                array = np.frombuffer(
129                    buffer_data,
130                    dtype=np.dtype(dtype).newbyteorder('<'),
131                    count=num_elems,
132                )
133                assert array.strides[0] == bytes_per_elem
134                array = np.lib.stride_tricks.as_strided(
135                    array,
136                    shape=(accessor.count, component_nb),
137                    strides=(stride, bytes_per_elem),
138                )
139
140        else:
141            # No buffer view; initialize to zeros
142            array = np.zeros((accessor.count, component_nb), dtype=dtype)
143
144        if accessor.sparse:
145            sparse_indices_obj = Accessor.from_dict({
146                'count': accessor.sparse.count,
147                'bufferView': accessor.sparse.indices.buffer_view,
148                'byteOffset': accessor.sparse.indices.byte_offset or 0,
149                'componentType': accessor.sparse.indices.component_type,
150                'type': 'SCALAR',
151            })
152            sparse_indices = BinaryData.decode_accessor_obj(gltf, sparse_indices_obj)
153            sparse_indices = sparse_indices.reshape(len(sparse_indices))
154
155            sparse_values_obj = Accessor.from_dict({
156                'count': accessor.sparse.count,
157                'bufferView': accessor.sparse.values.buffer_view,
158                'byteOffset': accessor.sparse.values.byte_offset or 0,
159                'componentType': accessor.component_type,
160                'type': accessor.type,
161            })
162            sparse_values = BinaryData.decode_accessor_obj(gltf, sparse_values_obj)
163
164            if not array.flags.writeable:
165                array = array.copy()
166            array[sparse_indices] = sparse_values
167
168        # Normalization
169        if accessor.normalized:
170            if accessor.component_type == 5120:  # int8
171                array = np.maximum(-1.0, array / 127.0)
172            elif accessor.component_type == 5121:  # uint8
173                array = array / 255.0
174            elif accessor.component_type == 5122:  # int16
175                array = np.maximum(-1.0, array / 32767.0)
176            elif accessor.component_type == 5123:  # uint16
177                array = array / 65535.0
178
179            array = array.astype(np.float32, copy=False)
180
181        return array
182
183    @staticmethod
184    def get_image_data(gltf, img_idx):
185        """Get data from image."""
186        pyimage = gltf.data.images[img_idx]
187
188        assert not (
189            pyimage.uri is not None and
190            pyimage.buffer_view is not None
191        )
192
193        if pyimage.uri is not None:
194            return gltf.load_uri(pyimage.uri)
195        if pyimage.buffer_view is not None:
196            return BinaryData.get_buffer_view(gltf, pyimage.buffer_view)
197        return None
198