Split igorbinarywave.py off of Hooke into its own package.
[igor.git] / igor / binarywave.py
1 # Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
2 #
3 # This file is part of Hooke.
4 #
5 # Hooke is free software: you can redistribute it and/or modify it
6 # under the terms of the GNU Lesser General Public License as
7 # published by the Free Software Foundation, either version 3 of the
8 # License, or (at your option) any later version.
9 #
10 # Hooke is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12 # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General
13 # Public License for more details.
14 #
15 # You should have received a copy of the GNU Lesser General Public
16 # License along with Hooke.  If not, see
17 # <http://www.gnu.org/licenses/>.
18
19 "Read IGOR Binary Wave files into Numpy arrays."
20
21 # Based on WaveMetric's Technical Note 003, "Igor Binary Format"
22 #   ftp://ftp.wavemetrics.net/IgorPro/Technical_Notes/TN003.zip
23 # From ftp://ftp.wavemetrics.net/IgorPro/Technical_Notes/TN000.txt
24 #   We place no restrictions on copying Technical Notes, with the
25 #   exception that you cannot resell them. So read, enjoy, and
26 #   share. We hope IGOR Technical Notes will provide you with lots of
27 #   valuable information while you are developing IGOR applications.
28
29 import array
30 import struct
31 import sys
32 import types
33
34 import numpy
35
36
37 class Field (object):
38     """Represent a Structure field.
39
40     See Also
41     --------
42     Structure
43     """
44     def __init__(self, format, name, default=None, help=None, count=1):
45         self.format = format # See the struct documentation
46         self.name = name
47         self.default = None
48         self.help = help
49         self.count = count
50         self.total_count = numpy.prod(count)
51
52 class Structure (struct.Struct):
53     """Represent a C structure.
54
55     A convenient wrapper around struct.Struct that uses Fields and
56     adds dict-handling methods for transparent name assignment.
57
58     See Also
59     --------
60     Field
61
62     Examples
63     --------
64
65     Represent the C structure::
66
67         struct thing {
68           short version;
69           long size[3];
70         }
71
72     As
73
74     >>> from pprint import pprint
75     >>> thing = Structure(name='thing',
76     ...     fields=[Field('h', 'version'), Field('l', 'size', count=3)])
77     >>> thing.set_byte_order('>')
78     >>> b = array.array('b', range(2+4*3))
79     >>> d = thing.unpack_dict_from(buffer=b)
80     >>> pprint(d)
81     {'size': array([ 33752069, 101124105, 168496141]), 'version': 1}
82     >>> [hex(x) for x in d['size']]
83     ['0x2030405L', '0x6070809L', '0xa0b0c0dL']
84
85     You can even get fancy with multi-dimensional arrays.
86
87     >>> thing = Structure(name='thing',
88     ...     fields=[Field('h', 'version'), Field('l', 'size', count=(3,2))])
89     >>> thing.set_byte_order('>')
90     >>> b = array.array('b', range(2+4*3*2))
91     >>> d = thing.unpack_dict_from(buffer=b)
92     >>> d['size'].shape
93     (3, 2)
94     >>> pprint(d)
95     {'size': array([[ 33752069, 101124105],
96            [168496141, 235868177],
97            [303240213, 370612249]]),
98      'version': 1}
99     """
100     def __init__(self, name, fields, byte_order='='):
101         # '=' for native byte order, standard size and alignment
102         # See http://docs.python.org/library/struct for details
103         self.name = name
104         self.fields = fields
105         self.set_byte_order(byte_order)
106
107     def __str__(self):
108         return self.name
109
110     def set_byte_order(self, byte_order):
111         """Allow changing the format byte_order on the fly.
112         """
113         if (hasattr(self, 'format') and self.format != None
114             and self.format.startswith(byte_order)):
115             return  # no need to change anything
116         format = []
117         for field in self.fields:
118             format.extend([field.format]*field.total_count)
119         struct.Struct.__init__(self, format=byte_order+''.join(format).replace('P', 'L'))
120
121     def _flatten_args(self, args):
122         # handle Field.count > 0
123         flat_args = []
124         for a,f in zip(args, self.fields):
125             if f.total_count > 1:
126                 flat_args.extend(a)
127             else:
128                 flat_args.append(a)
129         return flat_args
130
131     def _unflatten_args(self, args):
132         # handle Field.count > 0
133         unflat_args = []
134         i = 0
135         for f in self.fields:
136             if f.total_count > 1:
137                 data = numpy.array(args[i:i+f.total_count])
138                 data = data.reshape(f.count)
139                 unflat_args.append(data)
140             else:
141                 unflat_args.append(args[i])
142             i += f.total_count
143         return unflat_args
144         
145     def pack(self, *args):
146         return struct.Struct.pack(self, *self._flatten_args(args))
147
148     def pack_into(self, buffer, offset, *args):
149         return struct.Struct.pack_into(self, buffer, offset,
150                                        *self._flatten_args(args))
151
152     def _clean_dict(self, dict):
153         for f in self.fields:
154             if f.name not in dict:
155                 if f.default != None:
156                     dict[f.name] = f.default
157                 else:
158                     raise ValueError('%s field not set for %s'
159                                      % f.name, self.__class__.__name__)
160         return dict
161
162     def pack_dict(self, dict):
163         dict = self._clean_dict(dict)
164         return self.pack(*[dict[f.name] for f in self.fields])
165
166     def pack_dict_into(self, buffer, offset, dict={}):
167         dict = self._clean_dict(dict)
168         return self.pack_into(buffer, offset,
169                               *[dict[f.name] for f in self.fields])
170
171     def unpack(self, string):
172         return self._unflatten_args(struct.Struct.unpack(self, string))
173
174     def unpack_from(self, buffer, offset=0):
175         return self._unflatten_args(
176             struct.Struct.unpack_from(self, buffer, offset))
177
178     def unpack_dict(self, string):
179         return dict(zip([f.name for f in self.fields],
180                         self.unpack(string)))
181
182     def unpack_dict_from(self, buffer, offset=0):
183         return dict(zip([f.name for f in self.fields],
184                         self.unpack_from(buffer, offset)))
185
186
187 # Numpy doesn't support complex integers by default, see
188 #   http://mail.python.org/pipermail/python-dev/2002-April/022408.html
189 #   http://mail.scipy.org/pipermail/numpy-discussion/2007-October/029447.html
190 # So we roll our own types.  See
191 #   http://docs.scipy.org/doc/numpy/user/basics.rec.html
192 #   http://docs.scipy.org/doc/numpy/reference/generated/numpy.dtype.html
193 complexInt8 = numpy.dtype([('real', numpy.int8), ('imag', numpy.int8)])
194 complexInt16 = numpy.dtype([('real', numpy.int16), ('imag', numpy.int16)])
195 complexInt32 = numpy.dtype([('real', numpy.int32), ('imag', numpy.int32)])
196 complexUInt8 = numpy.dtype([('real', numpy.uint8), ('imag', numpy.uint8)])
197 complexUInt16 = numpy.dtype([('real', numpy.uint16), ('imag', numpy.uint16)])
198 complexUInt32 = numpy.dtype([('real', numpy.uint32), ('imag', numpy.uint32)])
199
200
201 # Begin IGOR constants and typedefs from IgorBin.h
202
203 # From IgorMath.h
204 TYPE_TABLE = {       # (key: integer flag, value: numpy dtype)
205     0:None,          # Text wave, not handled in ReadWave.c
206     1:numpy.complex, # NT_CMPLX, makes number complex.
207     2:numpy.float32, # NT_FP32, 32 bit fp numbers.
208     3:numpy.complex64,
209     4:numpy.float64, # NT_FP64, 64 bit fp numbers.
210     5:numpy.complex128,
211     8:numpy.int8,    # NT_I8, 8 bit signed integer. Requires Igor Pro
212                      # 2.0 or later.
213     9:complexInt8,
214     0x10:numpy.int16,# NT_I16, 16 bit integer numbers. Requires Igor
215                      # Pro 2.0 or later.
216     0x11:complexInt16,
217     0x20:numpy.int32,# NT_I32, 32 bit integer numbers. Requires Igor
218                      # Pro 2.0 or later.
219     0x21:complexInt32,
220 #   0x40:None,       # NT_UNSIGNED, Makes above signed integers
221 #                    # unsigned. Requires Igor Pro 3.0 or later.
222     0x48:numpy.uint8,
223     0x49:complexUInt8,
224     0x50:numpy.uint16,
225     0x51:complexUInt16,
226     0x60:numpy.uint32,
227     0x61:complexUInt32,
228 }
229
230 # From wave.h
231 MAXDIMS = 4
232
233 # From binary.h
234 BinHeaderCommon = Structure(  # WTK: this one is mine.
235     name='BinHeaderCommon',
236     fields=[
237         Field('h', 'version', help='Version number for backwards compatibility.'),
238         ])
239
240 BinHeader1 = Structure(
241     name='BinHeader1',
242     fields=[
243         Field('h', 'version', help='Version number for backwards compatibility.'),
244         Field('l', 'wfmSize', help='The size of the WaveHeader2 data structure plus the wave data plus 16 bytes of padding.'),
245         Field('h', 'checksum', help='Checksum over this header and the wave header.'),
246         ])
247
248 BinHeader2 = Structure(
249     name='BinHeader2',
250     fields=[
251         Field('h', 'version', help='Version number for backwards compatibility.'),
252         Field('l', 'wfmSize', help='The size of the WaveHeader2 data structure plus the wave data plus 16 bytes of padding.'),
253         Field('l', 'noteSize', help='The size of the note text.'),
254         Field('l', 'pictSize', default=0, help='Reserved. Write zero. Ignore on read.'),
255         Field('h', 'checksum', help='Checksum over this header and the wave header.'),
256         ])
257
258 BinHeader3 = Structure(
259     name='BinHeader3',
260     fields=[
261         Field('h', 'version', help='Version number for backwards compatibility.'),
262         Field('h', 'wfmSize', help='The size of the WaveHeader2 data structure plus the wave data plus 16 bytes of padding.'),
263         Field('l', 'noteSize', help='The size of the note text.'),
264         Field('l', 'formulaSize', help='The size of the dependency formula, if any.'),
265         Field('l', 'pictSize', default=0, help='Reserved. Write zero. Ignore on read.'),
266         Field('h', 'checksum', help='Checksum over this header and the wave header.'),
267         ])
268
269 BinHeader5 = Structure(
270     name='BinHeader5',
271     fields=[
272         Field('h', 'version', help='Version number for backwards compatibility.'),
273         Field('h', 'checksum', help='Checksum over this header and the wave header.'),
274         Field('l', 'wfmSize', help='The size of the WaveHeader5 data structure plus the wave data.'),
275         Field('l', 'formulaSize', help='The size of the dependency formula, if any.'),
276         Field('l', 'noteSize', help='The size of the note text.'),
277         Field('l', 'dataEUnitsSize', help='The size of optional extended data units.'),
278         Field('l', 'dimEUnitsSize', help='The size of optional extended dimension units.', count=MAXDIMS),
279         Field('l', 'dimLabelsSize', help='The size of optional dimension labels.', count=MAXDIMS),
280         Field('l', 'sIndicesSize', help='The size of string indicies if this is a text wave.'),
281         Field('l', 'optionsSize1', default=0, help='Reserved. Write zero. Ignore on read.'),
282         Field('l', 'optionsSize2', default=0, help='Reserved. Write zero. Ignore on read.'),
283         ])
284
285
286 # From wave.h
287 MAX_WAVE_NAME2 = 18 # Maximum length of wave name in version 1 and 2
288                     # files. Does not include the trailing null.
289 MAX_WAVE_NAME5 = 31 # Maximum length of wave name in version 5
290                     # files. Does not include the trailing null.
291 MAX_UNIT_CHARS = 3
292
293 # Header to an array of waveform data.
294
295 WaveHeader2 = Structure(
296     name='WaveHeader2',
297     fields=[
298         Field('h', 'type', help='See types (e.g. NT_FP64) above. Zero for text waves.'),
299         Field('P', 'next', default=0, help='Used in memory only. Write zero. Ignore on read.'),
300         Field('c', 'bname', help='Name of wave plus trailing null.', count=MAX_WAVE_NAME2+2),
301         Field('h', 'whVersion', default=0, help='Write 0. Ignore on read.'),
302         Field('h', 'srcFldr', default=0, help='Used in memory only. Write zero. Ignore on read.'),
303         Field('P', 'fileName', default=0, help='Used in memory only. Write zero. Ignore on read.'),
304         Field('c', 'dataUnits', default=0, help='Natural data units go here - null if none.', count=MAX_UNIT_CHARS+1),
305         Field('c', 'xUnits', default=0, help='Natural x-axis units go here - null if none.', count=MAX_UNIT_CHARS+1),
306         Field('l', 'npnts', help='Number of data points in wave.'),
307         Field('h', 'aModified', default=0, help='Used in memory only. Write zero. Ignore on read.'),
308         Field('d', 'hsA', help='X value for point p = hsA*p + hsB'),
309         Field('d', 'hsB', help='X value for point p = hsA*p + hsB'),
310         Field('h', 'wModified', default=0, help='Used in memory only. Write zero. Ignore on read.'),
311         Field('h', 'swModified', default=0, help='Used in memory only. Write zero. Ignore on read.'),
312         Field('h', 'fsValid', help='True if full scale values have meaning.'),
313         Field('d', 'topFullScale', help='The min full scale value for wave.'), # sic, 'min' should probably be 'max'
314         Field('d', 'botFullScale', help='The min full scale value for wave.'),
315         Field('c', 'useBits', default=0, help='Used in memory only. Write zero. Ignore on read.'),
316         Field('c', 'kindBits', default=0, help='Reserved. Write zero. Ignore on read.'),
317         Field('P', 'formula', default=0, help='Used in memory only. Write zero. Ignore on read.'),
318         Field('l', 'depID', default=0, help='Used in memory only. Write zero. Ignore on read.'),
319         Field('L', 'creationDate', help='DateTime of creation.  Not used in version 1 files.'),
320         Field('c', 'wUnused', default=0, help='Reserved. Write zero. Ignore on read.', count=2),
321         Field('L', 'modDate', help='DateTime of last modification.'),
322         Field('P', 'waveNoteH', help='Used in memory only. Write zero. Ignore on read.'),
323         Field('f', 'wData', help='The start of the array of waveform data.', count=4),
324         ])
325
326 WaveHeader5 = Structure(
327     name='WaveHeader5',
328     fields=[
329         Field('P', 'next', help='link to next wave in linked list.'),
330         Field('L', 'creationDate', help='DateTime of creation.'),
331         Field('L', 'modDate', help='DateTime of last modification.'),
332         Field('l', 'npnts', help='Total number of points (multiply dimensions up to first zero).'),
333         Field('h', 'type', help='See types (e.g. NT_FP64) above. Zero for text waves.'),
334         Field('h', 'dLock', default=0, help='Reserved. Write zero. Ignore on read.'),
335         Field('c', 'whpad1', default=0, help='Reserved. Write zero. Ignore on read.', count=6),
336         Field('h', 'whVersion', default=1, help='Write 1. Ignore on read.'),
337         Field('c', 'bname', help='Name of wave plus trailing null.', count=MAX_WAVE_NAME5+1),
338         Field('l', 'whpad2', default=0, help='Reserved. Write zero. Ignore on read.'),
339         Field('P', 'dFolder', default=0, help='Used in memory only. Write zero. Ignore on read.'),
340         # Dimensioning info. [0] == rows, [1] == cols etc
341         Field('l', 'nDim', help='Number of of items in a dimension -- 0 means no data.', count=MAXDIMS),
342         Field('d', 'sfA', help='Index value for element e of dimension d = sfA[d]*e + sfB[d].', count=MAXDIMS),
343         Field('d', 'sfB', help='Index value for element e of dimension d = sfA[d]*e + sfB[d].', count=MAXDIMS),
344         # SI units
345         Field('c', 'dataUnits', default=0, help='Natural data units go here - null if none.', count=MAX_UNIT_CHARS+1),
346         Field('c', 'dimUnits', default=0, help='Natural dimension units go here - null if none.', count=(MAXDIMS, MAX_UNIT_CHARS+1)),
347         Field('h', 'fsValid', help='TRUE if full scale values have meaning.'),
348         Field('h', 'whpad3', default=0, help='Reserved. Write zero. Ignore on read.'),
349         Field('d', 'topFullScale', help='The max and max full scale value for wave'), # sic, probably "max and min"
350         Field('d', 'botFullScale', help='The max and max full scale value for wave.'), # sic, probably "max and min"
351         Field('P', 'dataEUnits', default=0, help='Used in memory only. Write zero. Ignore on read.'),
352         Field('P', 'dimEUnits', default=0, help='Used in memory only. Write zero.  Ignore on read.', count=MAXDIMS),
353         Field('P', 'dimLabels', default=0, help='Used in memory only. Write zero.  Ignore on read.', count=MAXDIMS),
354         Field('P', 'waveNoteH', default=0, help='Used in memory only. Write zero. Ignore on read.'),
355         Field('l', 'whUnused', default=0, help='Reserved. Write zero. Ignore on read.', count=16),
356         # The following stuff is considered private to Igor.
357         Field('h', 'aModified', default=0, help='Used in memory only. Write zero. Ignore on read.'),
358         Field('h', 'wModified', default=0, help='Used in memory only. Write zero. Ignore on read.'),
359         Field('h', 'swModified', default=0, help='Used in memory only. Write zero. Ignore on read.'),
360         Field('c', 'useBits', default=0, help='Used in memory only. Write zero. Ignore on read.'),
361         Field('c', 'kindBits', default=0, help='Reserved. Write zero. Ignore on read.'),
362         Field('P', 'formula', default=0, help='Used in memory only. Write zero. Ignore on read.'),
363         Field('l', 'depID', default=0, help='Used in memory only. Write zero. Ignore on read.'),
364         Field('h', 'whpad4', default=0, help='Reserved. Write zero. Ignore on read.'),
365         Field('h', 'srcFldr', default=0, help='Used in memory only. Write zero. Ignore on read.'),
366         Field('P', 'fileName', default=0, help='Used in memory only. Write zero. Ignore on read.'),
367         Field('P', 'sIndices', default=0, help='Used in memory only. Write zero. Ignore on read.'),
368         Field('f', 'wData', help='The start of the array of data.  Must be 64 bit aligned.', count=1),
369         ])
370
371 # End IGOR constants and typedefs from IgorBin.h
372
373 # Begin functions from ReadWave.c
374
375 def need_to_reorder_bytes(version):
376     # If the low order byte of the version field of the BinHeader
377     # structure is zero then the file is from a platform that uses
378     # different byte-ordering and therefore all data will need to be
379     # reordered.
380     return version & 0xFF == 0
381
382 def byte_order(needToReorderBytes):
383     little_endian = sys.byteorder == 'little'
384     if needToReorderBytes:
385         little_endian = not little_endian
386     if little_endian:
387         return '<'  # little-endian
388     return '>'  # big-endian    
389
390 def version_structs(version, byte_order):
391     if version == 1:
392         bin = BinHeader1
393         wave = WaveHeader2
394     elif version == 2:
395         bin = BinHeader2
396         wave = WaveHeader2
397     elif version == 3:
398         bin = BinHeader3
399         wave = WaveHeader2
400     elif version == 5:
401         bin = BinHeader5
402         wave = WaveHeader5
403     else:
404         raise ValueError('This does not appear to be a valid Igor binary wave file. The version field = %d.\n', version);
405     checkSumSize = bin.size + wave.size
406     if version == 5:
407         checkSumSize -= 4  # Version 5 checksum does not include the wData field.
408     bin.set_byte_order(byte_order)
409     wave.set_byte_order(byte_order)
410     return (bin, wave, checkSumSize)
411
412 def checksum(buffer, byte_order, oldcksum, numbytes):
413     x = numpy.ndarray(
414         (numbytes/2,), # 2 bytes to a short -- ignore trailing odd byte
415         dtype=numpy.dtype(byte_order+'h'),
416         buffer=buffer)
417     oldcksum += x.sum()
418     if oldcksum > 2**31:  # fake the C implementation's int rollover
419         oldcksum %= 2**32
420         if oldcksum > 2**31:
421             oldcksum -= 2**31
422     return oldcksum & 0xffff
423
424 # Translated from ReadWave()
425 def loadibw(filename, strict=True):
426     if hasattr(filename, 'read'):
427         f = filename  # filename is actually a stream object
428     else:
429         f = open(filename, 'rb')
430     try:
431         b = buffer(f.read(BinHeaderCommon.size))
432         version = BinHeaderCommon.unpack_dict_from(b)['version']
433         needToReorderBytes = need_to_reorder_bytes(version)
434         byteOrder = byte_order(needToReorderBytes)
435         
436         if needToReorderBytes:
437             BinHeaderCommon.set_byte_order(byteOrder)
438             version = BinHeaderCommon.unpack_dict_from(b)['version']
439         bin_struct,wave_struct,checkSumSize = version_structs(version, byteOrder)
440
441         b = buffer(b + f.read(bin_struct.size + wave_struct.size - BinHeaderCommon.size))
442         c = checksum(b, byteOrder, 0, checkSumSize)
443         if c != 0:
444             raise ValueError('Error in checksum - should be 0, is %d.  This does not appear to be a valid Igor binary wave file.' % c)
445         bin_info = bin_struct.unpack_dict_from(b)
446         wave_info = wave_struct.unpack_dict_from(b, offset=bin_struct.size)
447         if wave_info['type'] == 0:
448             raise NotImplementedError('Text wave')
449         if version in [1,2,3]:
450             tail = 16  # 16 = size of wData field in WaveHeader2 structure
451             waveDataSize = bin_info['wfmSize'] - wave_struct.size
452             # =  bin_info['wfmSize']-16 - (wave_struct.size - tail)
453         else:
454             assert version == 5, version
455             tail = 4  # 4 = size of wData field in WaveHeader5 structure
456             waveDataSize = bin_info['wfmSize'] - (wave_struct.size - tail)
457         # dtype() wrapping to avoid numpy.generic and
458         # getset_descriptor issues with the builtin Numpy types
459         # (e.g. int32).  It has no effect on our local complex
460         # integers.
461         t = numpy.dtype(TYPE_TABLE[wave_info['type']])
462         assert waveDataSize == wave_info['npnts'] * t.itemsize, \
463             ('%d, %d, %d, %s' % (waveDataSize, wave_info['npnts'], t.itemsize, t))
464         tail_data = array.array('f', b[-tail:])
465         data_b = buffer(buffer(tail_data) + f.read(waveDataSize-tail))
466         if version == 5:
467             shape = [n for n in wave_info['nDim'] if n > 0]
468         else:
469             shape = (wave_info['npnts'],)
470         data = numpy.ndarray(
471             shape=shape,
472             dtype=t.newbyteorder(byteOrder),
473             buffer=data_b,
474             order='F',
475             )
476
477         if version == 1:
478             pass  # No post-data information
479         elif version == 2:
480             # Post-data info:
481             #   * 16 bytes of padding
482             #   * Optional wave note data
483             pad_b = buffer(f.read(16))  # skip the padding
484             if max(pad_b) != 0:
485                 if strict:
486                     assert max(pad_b) == 0, pad_b
487                 else:
488                     print sys.stderr, 'warning: post-data padding not zero: %s.' % pad_b
489             bin_info['note'] = str(f.read(bin_info['noteSize'])).strip()
490         elif version == 3:
491             # Post-data info:
492             #   * 16 bytes of padding
493             #   * Optional wave note data
494             #   * Optional wave dependency formula
495             """Excerpted from TN003:
496
497             A wave has a dependency formula if it has been bound by a
498             statement such as "wave0 := sin(x)". In this example, the
499             dependency formula is "sin(x)". The formula is stored with
500             no trailing null byte.
501             """
502             pad_b = buffer(f.read(16))  # skip the padding
503             if max(pad_b) != 0:
504                 if strict:
505                     assert max(pad_b) == 0, pad_b
506                 else:
507                     print sys.stderr, 'warning: post-data padding not zero: %s.' % pad_b
508             bin_info['note'] = str(f.read(bin_info['noteSize'])).strip()
509             bin_info['formula'] = str(f.read(bin_info['formulaSize'])).strip()
510         elif version == 5:
511             # Post-data info:
512             #   * Optional wave dependency formula
513             #   * Optional wave note data
514             #   * Optional extended data units data
515             #   * Optional extended dimension units data
516             #   * Optional dimension label data
517             #   * String indices used for text waves only
518             """Excerpted from TN003:
519
520             dataUnits - Present in versions 1, 2, 3, 5. The dataUnits
521               field stores the units for the data represented by the
522               wave. It is a C string terminated with a null
523               character. This field supports units of 0 to 3 bytes. In
524               version 1, 2 and 3 files, longer units can not be
525               represented. In version 5 files, longer units can be
526               stored using the optional extended data units section of
527               the file.
528
529             xUnits - Present in versions 1, 2, 3. The xUnits field
530               stores the X units for a wave. It is a C string
531               terminated with a null character.  This field supports
532               units of 0 to 3 bytes. In version 1, 2 and 3 files,
533               longer units can not be represented.
534
535             dimUnits - Present in version 5 only. This field is an
536               array of 4 strings, one for each possible wave
537               dimension. Each string supports units of 0 to 3
538               bytes. Longer units can be stored using the optional
539               extended dimension units section of the file.
540             """
541             bin_info['formula'] = str(f.read(bin_info['formulaSize'])).strip()
542             bin_info['note'] = str(f.read(bin_info['noteSize'])).strip()
543             bin_info['dataEUnits'] = str(f.read(bin_info['dataEUnitsSize'])).strip()
544             bin_info['dimEUnits'] = [
545                 str(f.read(size)).strip() for size in bin_info['dimEUnitsSize']]
546             bin_info['dimLabels'] = []
547             for size in bin_info['dimLabelsSize']:
548                 labels = str(f.read(size)).split(chr(0)) # split null-delimited strings
549                 bin_info['dimLabels'].append([L for L in labels if len(L) > 0])
550             if wave_info['type'] == 0:  # text wave
551                 bin_info['sIndices'] = f.read(bin_info['sIndicesSize'])
552
553     finally:
554         if not hasattr(filename, 'read'):
555             f.close()
556
557     return data, bin_info, wave_info
558
559
560 def saveibw(filename):
561     raise NotImplementedError