Skip to content

Commit ef9b1b3

Browse files
committed
ENH: First pass at MZ3 image
1 parent 204a48c commit ef9b1b3

File tree

1 file changed

+177
-0
lines changed

1 file changed

+177
-0
lines changed

nibabel/surfice.py

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import io
2+
import struct
3+
import gzip
4+
import numpy as np
5+
from .wrapstruct import LabeledWrapStruct
6+
from .dataobj_images import DataobjImage
7+
from .arrayproxy import ArrayProxy
8+
9+
10+
header_dtd = [
11+
('magic', 'S2'), # 0; 0x5a4d (little endian) == "MZ"
12+
('attr', 'u2'), # 2; Attributes bitfield reporting stored data
13+
('nface', 'u4'), # 4; Number of faces
14+
('nvert', 'u4'), # 8; Number of vertices
15+
('nskip', 'u4'), # 12; Number of bytes to skip (for future header extensions)
16+
]
17+
header_dtype = np.dtype(header_dtd)
18+
19+
20+
class MZ3Header(LabeledWrapStruct):
21+
template_dtype = header_dtype
22+
compression = False
23+
24+
@classmethod
25+
def from_header(klass, header=None, check=True):
26+
if type(header) == klass:
27+
obj = header.copy()
28+
if check:
29+
obj.check_fix()
30+
return obj
31+
32+
def copy(self):
33+
ret = super().copy()
34+
ret.compression = self.compression
35+
ret._nscalar = self._nscalar
36+
return ret
37+
38+
@classmethod
39+
def from_fileobj(klass, fileobj, endianness=None, check=True):
40+
raw_str = fileobj.read(klass.template_dtype.itemsize)
41+
compression = raw_str[:2] == b'\x1f\x8b'
42+
if compression:
43+
fileobj.seek(0)
44+
with gzip.open(fileobj, 'rb') as fobj:
45+
raw_str = fobj.read(klass.template_dtype.itemsize)
46+
47+
hdr = klass(raw_str, endianness, check)
48+
hdr.compression = compression
49+
hdr._nscalar = hdr._calculate_nscalar(fileobj)
50+
return hdr
51+
52+
def get_data_offset(self):
53+
_, attr, nface, nvert, nskip = self._structarr.tolist()
54+
55+
isface = attr & 1 != 0
56+
isvert = attr & 2 != 0
57+
isrgba = attr & 4 != 0
58+
return 16 + nskip + isface * nface * 12 + isvert * nvert * 12 + isrgba * nvert * 12
59+
60+
def _calculate_nscalar(self, fileobj):
61+
_, attr, nface, nvert, nskip = self._structarr.tolist()
62+
63+
isscalar = attr & 8 != 0
64+
isdouble = attr & 16 != 0
65+
base_size = self.get_data_offset()
66+
67+
nscalar = 0
68+
if isscalar or isdouble:
69+
factor = nvert * (4 if isscalar else 8)
70+
ret = fileobj.tell()
71+
if self.compression:
72+
fileobj.seek(-4, 2)
73+
full_size_mod_4gb = struct.unpack('I', fileobj.read(4))[0]
74+
full_size = full_size_mod_4gb
75+
nscalar, remainder = divmod(full_size - base_size, factor)
76+
for _ in range(5):
77+
full_size += (1 << 32)
78+
nscalar, remainder = divmod(full_size - base_size, factor)
79+
if remainder == 0:
80+
break
81+
else:
82+
fileobj.seek(0)
83+
with gzip.open(fileobj, 'rb') as fobj:
84+
fobj.seek(0, 2)
85+
full_size = fobj.tell()
86+
nscalar, remainder = divmod(full_size - base_size, factor)
87+
if remainder:
88+
raise ValueError("Apparent file size failure")
89+
else:
90+
fileobj.seek(0, 2)
91+
full_size = fileobj.tell()
92+
nscalar, remainder = divmod(full_size - base_size, factor)
93+
if remainder:
94+
raise ValueError("Apparent file size failure")
95+
fileobj.seek(ret)
96+
return nscalar
97+
98+
@classmethod
99+
def guessed_endian(klass, mapping):
100+
return '<'
101+
102+
@classmethod
103+
def default_structarr(klass, endianness=None):
104+
if endianness is not None and endian_codes[endianness] != '<':
105+
raise ValueError('MZ3Header must always be little endian')
106+
structarr = super().default_structarr(endianness=endianness)
107+
structarr['magic'] = b"MZ"
108+
return structarr
109+
110+
@classmethod
111+
def may_contain_header(klass, binaryblock):
112+
if len(binaryblock) < 16:
113+
return False
114+
115+
# May be gzipped without changing extension
116+
if binaryblock[:2] == b'\x1f\x8b':
117+
with gzip.open(io.BytesIO(binaryblock), 'rb') as fobj:
118+
binaryblock = fobj.read(16)
119+
120+
hdr_struct = np.ndarray(shape=(), dtype=klass.template_dtype, buffer=binaryblock[:16])
121+
return hdr_struct['magic'] == b'MZ'
122+
123+
def get_data_dtype(self):
124+
if self._structarr['attr'] & 8:
125+
return np.dtype('<f4')
126+
elif self._structarr['attr'] & 16:
127+
return np.dtype('<f8')
128+
129+
def set_data_dtype(self, datatype):
130+
if np.dtype(datatype).byteorder == ">":
131+
raise ValueError("Cannot set type to big-endian")
132+
dt = np.dtype(datatype).newbyteorder("<")
133+
134+
if dt == np.dtype('<f8'):
135+
self._structarr['attr'] |= 0b00010000
136+
elif dt == np.dtype('<f4'):
137+
self._structarr['attr'] &= 0b11101111
138+
else:
139+
raise ValueError(f"Cannot set dtype: {datatype}")
140+
141+
def get_data_shape(self):
142+
base_shape = (int(self._structarr['nvert']),)
143+
if self._nscalar == 0:
144+
return ()
145+
elif self._nscalar == 1:
146+
return base_shape
147+
else:
148+
return base_shape + (self._nscalar,)
149+
150+
151+
class MZ3Image(DataobjImage):
152+
header_class = MZ3Header
153+
valid_exts = ('.mz3',)
154+
files_types = (('image', '.mz3'),)
155+
156+
ImageArrayProxy = ArrayProxy
157+
158+
@classmethod
159+
def from_file_map(klass, file_map, *, mmap=True, keep_file_open=None):
160+
if mmap not in (True, False, 'c', 'r'):
161+
raise ValueError("mmap should be one of {True, False, 'c', 'r'}")
162+
fh = file_map['image']
163+
with fh.get_prepare_fileobj(mode='rb') as fileobj:
164+
header = klass.header_class.from_fileobj(fileobj)
165+
print(header)
166+
167+
data_dtype = header.get_data_dtype()
168+
if data_dtype:
169+
spec = (header.get_data_shape(), data_dtype, header.get_data_offset())
170+
dataobj = klass.ImageArrayProxy(fh.filename, spec, mmap=mmap,
171+
keep_file_open=keep_file_open,
172+
compression="gz" if header.compression else None)
173+
else:
174+
dataobj = np.array((), dtype="<f4")
175+
176+
img = klass(dataobj, header=header)
177+
return img

0 commit comments

Comments
 (0)