packed.py 7.45 KB
Newer Older
1
2
# Copyright

3
"Read IGOR Packed Experiment files files into records."
4

5
from . import LOG as _LOG
6
7
from .struct import Structure as _Structure
from .struct import Field as _Field
8
9
10
11
12
from .util import byte_order as _byte_order
from .util import need_to_reorder_bytes as _need_to_reorder_bytes
from .record import RECORD_TYPE as _RECORD_TYPE
from .record.base import UnknownRecord as _UnknownRecord
from .record.base import UnusedRecord as _UnusedRecord
13
14
15
16
from .record.folder import FolderStartRecord as _FolderStartRecord
from .record.folder import FolderEndRecord as _FolderEndRecord
from .record.variables import VariablesRecord as _VariablesRecord
from .record.wave import WaveRecord as _WaveRecord
17
18


19
# From PTN003:
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# Igor writes other kinds of records in a packed experiment file, for
# storing things like pictures, page setup records, and miscellaneous
# settings.  The format for these records is quite complex and is not
# described in PTN003.  If you are writing a program to read packed
# files, you must skip any record with a record type that is not
# listed above.

PackedFileRecordHeader = _Structure(
    name='PackedFileRecordHeader',
    fields=[
        _Field('H', 'recordType', help='Record type plus superceded flag.'),
        _Field('h', 'version', help='Version information depends on the type of record.'),
        _Field('l', 'numDataBytes', help='Number of data bytes in the record following this record header.'),
        ])

#CR_STR = '\x15'  (\r)

PACKEDRECTYPE_MASK = 0x7FFF  # Record type = (recordType & PACKEDREC_TYPE_MASK)
SUPERCEDED_MASK = 0x8000  # Bit is set if the record is superceded by
                          # a later record in the packed file.


def load(filename, strict=True, ignore_unknown=True):
43
    _LOG.debug('loading a packed experiment file from {}'.format(filename))
44
45
46
47
48
    records = []
    if hasattr(filename, 'read'):
        f = filename  # filename is actually a stream object
    else:
        f = open(filename, 'rb')
49
50
    byte_order = None
    initial_byte_order = '='
51
52
    try:
        while True:
53
54
            PackedFileRecordHeader.byte_order = initial_byte_order
            PackedFileRecordHeader.setup()
55
56
57
            b = buffer(f.read(PackedFileRecordHeader.size))
            if not b:
                break
58
59
60
61
            if len(b) < PackedFileRecordHeader.size:
                raise ValueError(
                    ('not enough data for the next record header ({} < {})'
                     ).format(len(b), PackedFileRecordHeader.size))
62
            _LOG.debug('reading a new packed experiment file record')
63
            header = PackedFileRecordHeader.unpack_from(b)
64
65
66
            if header['version'] and not byte_order:
                need_to_reorder = _need_to_reorder_bytes(header['version'])
                byte_order = initial_byte_order = _byte_order(need_to_reorder)
67
68
69
                _LOG.debug(
                    'get byte order from version: {} (reorder? {})'.format(
                        byte_order, need_to_reorder))
70
                if need_to_reorder:
71
72
                    PackedFileRecordHeader.byte_order = byte_order
                    PackedFileRecordHeader.setup()
73
                    header = PackedFileRecordHeader.unpack_from(b)
74
75
                    _LOG.debug(
                        'reordered version: {}'.format(header['version']))
76
            data = buffer(f.read(header['numDataBytes']))
77
78
79
80
            if len(data) < header['numDataBytes']:
                raise ValueError(
                    ('not enough data for the next record ({} < {})'
                     ).format(len(b), header['numDataBytes']))
81
82
            record_type = _RECORD_TYPE.get(
                header['recordType'] & PACKEDRECTYPE_MASK, _UnknownRecord)
83
84
            _LOG.debug('the new record has type {} ({}).'.format(
                    record_type, header['recordType']))
85
            if record_type in [_UnknownRecord, _UnusedRecord
86
87
88
                               ] and not ignore_unknown:
                raise KeyError('unkown record type {}'.format(
                        header['recordType']))
89
            records.append(record_type(header, data, byte_order=byte_order))
90
    finally:
91
92
        _LOG.debug('finished loading {} records from {}'.format(
                len(records), filename))
93
94
95
        if not hasattr(filename, 'read'):
            f.close()

96
97
98
99
100
    filesystem = _build_filesystem(records)

    return (records, filesystem)

def _build_filesystem(records):
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
    # From PTN003:
    """The name must be a valid Igor data folder name. See Object
    Names in the Igor Reference help file for name rules.

    When Igor Pro reads the data folder start record, it creates a new
    data folder with the specified name. Any subsequent variable, wave
    or data folder start records cause Igor to create data objects in
    this new data folder, until Igor Pro reads a corresponding data
    folder end record."""
    # From the Igor Manual, chapter 2, section 8, page II-123
    # http://www.wavemetrics.net/doc/igorman/II-08%20Data%20Folders.pdf
    """Like the Macintosh file system, Igor Pro's data folders use the
    colon character (:) to separate components of a path to an
    object. This is analogous to Unix which uses / and Windows which
    uses \. (Reminder: Igor's data folders exist wholly in memory
    while an experiment is open. It is not a disk file system!)

    A data folder named "root" always exists and contains all other
    data folders.
    """
    # From the Igor Manual, chapter 4, page IV-2
    # http://www.wavemetrics.net/doc/igorman/IV-01%20Commands.pdf
    """For waves and data folders only, you can also use "liberal"
    names. Liberal names can include almost any character, including
    spaces and dots (see Liberal Object Names on page III-415 for
    details).
    """
    # From the Igor Manual, chapter 3, section 16, page III-416
    # http://www.wavemetrics.net/doc/igorman/III-16%20Miscellany.pdf
    """Liberal names have the same rules as standard names except you
    may use any character except control characters and the following:

      " ' : ;
    """
    filesystem = {'root': {}}
    dir_stack = [('root', filesystem['root'])]
    for record in records:
        cwd = dir_stack[-1][-1]
        if isinstance(record, _FolderStartRecord):
            name = record.null_terminated_text
            cwd[name] = {}
            dir_stack.append((name, cwd[name]))
        elif isinstance(record, _FolderEndRecord):
            dir_stack.pop()
        elif isinstance(record, (_VariablesRecord, _WaveRecord)):
            if isinstance(record, _VariablesRecord):
147
148
149
150
151
152
153
154
155
156
157
                sys_vars = record.variables['variables']['sysVars'].keys()
                for filename,value in record.namespace.items():
                    if len(dir_stack) > 1 and filename in sys_vars:
                        # From PTN003:
                        """When reading a packed file, any system
                        variables encountered while the current data
                        folder is not the root should be ignored.
                        """
                        continue
                    _check_filename(dir_stack, filename)
                    cwd[filename] = value
158
            else:  # WaveRecord
159
                filename = record.wave['wave']['wave_header']['bname']
160
                _check_filename(dir_stack, filename)
161
                cwd[filename] = record
162
    return filesystem
163

164
165
166
167
168
def _check_filename(dir_stack, filename):
    cwd = dir_stack[-1][-1]
    if filename in cwd:
        raise ValueError('collision on name {} in {}'.format(
                filename, ':'.join(d for d,cwd in dir_stack)))