1 """Simple class to read IFF chunks.
2 
3 An IFF chunk (used in formats such as AIFF, TIFF, RMFF (RealMedia File
4 Format)) has the following structure:
5 
6 +----------------+
7 | ID (4 bytes)   |
8 +----------------+
9 | size (4 bytes) |
10 +----------------+
11 | data           |
12 | ...            |
13 +----------------+
14 
15 The ID is a 4-byte string which identifies the type of chunk.
16 
17 The size field (a 32-bit value, encoded using big-endian byte order)
18 gives the size of the whole chunk, including the 8-byte header.
19 
20 Usually an IFF-type file consists of one or more chunks.  The proposed
21 usage of the Chunk class defined here is to instantiate an instance at
22 the start of each chunk and read from the instance until it reaches
23 the end, after which a new instance can be instantiated.  At the end
24 of the file, creating a new instance will fail with an EOFError
25 exception.
26 
27 Usage:
28 while True:
29     try:
30         chunk = Chunk(file)
31     except EOFError:
32         break
33     chunktype = chunk.getname()
34     while True:
35         data = chunk.read(nbytes)
36         if not data:
37             pass
38         # do something with data
39 
40 The interface is file-like.  The implemented methods are:
41 read, close, seek, tell, isatty.
42 Extra methods are: skip() (called by close, skips to the end of the chunk),
43 getname() (returns the name (ID) of the chunk)
44 
45 The __init__ method has one required argument, a file-like object
46 (including a chunk instance), and one optional argument, a flag which
47 specifies whether or not chunks are aligned on 2-byte boundaries.  The
48 default is 1, i.e. aligned.
49 """
50 
51 class Chunk:
52     def __init__(self, file, align=True, bigendian=True, inclheader=False):
53         import struct
54         self.closed = False
55         self.align = align      # whether to align to word (2-byte) boundaries
56         if bigendian:
57             strflag = '>'
58         else:
59             strflag = '<'
60         self.file = file
61         self.chunkname = file.read(4)
62         if len(self.chunkname) < 4:
63             raise EOFError
64         try:
65             self.chunksize = struct.unpack(strflag+'L', file.read(4))[0]
66         except struct.error:
67             raise EOFError
68         if inclheader:
69             self.chunksize = self.chunksize - 8 # subtract header
70         self.size_read = 0
71         try:
72             self.offset = self.file.tell()
73         except (AttributeError, IOError):
74             self.seekable = False
75         else:
76             self.seekable = True
77 
78     def getname(self):
79         """Return the name (ID) of the current chunk."""
80         return self.chunkname
81 
82     def getsize(self):
83         """Return the size of the current chunk."""
84         return self.chunksize
85 
86     def close(self):
87         if not self.closed:
88             try:
89                 self.skip()
90             finally:
91                 self.closed = True
92 
93     def isatty(self):
94         if self.closed:
95             raise ValueError, "I/O operation on closed file"
96         return False
97 
98     def seek(self, pos, whence=0):
99         """Seek to specified position into the chunk.
100         Default position is 0 (start of chunk).
101         If the file is not seekable, this will result in an error.
102         """
103 
104         if self.closed:
105             raise ValueError, "I/O operation on closed file"
106         if not self.seekable:
107             raise IOError, "cannot seek"
108         if whence == 1:
109             pos = pos + self.size_read
110         elif whence == 2:
111             pos = pos + self.chunksize
112         if pos < 0 or pos > self.chunksize:
113             raise RuntimeError
114         self.file.seek(self.offset + pos, 0)
115         self.size_read = pos
116 
117     def tell(self):
118         if self.closed:
119             raise ValueError, "I/O operation on closed file"
120         return self.size_read
121 
122     def read(self, size=-1):
123         """Read at most size bytes from the chunk.
124         If size is omitted or negative, read until the end
125         of the chunk.
126         """
127 
128         if self.closed:
129             raise ValueError, "I/O operation on closed file"
130         if self.size_read >= self.chunksize:
131             return ''
132         if size < 0:
133             size = self.chunksize - self.size_read
134         if size > self.chunksize - self.size_read:
135             size = self.chunksize - self.size_read
136         data = self.file.read(size)
137         self.size_read = self.size_read + len(data)
138         if self.size_read == self.chunksize and \
139            self.align and \
140            (self.chunksize & 1):
141             dummy = self.file.read(1)
142             self.size_read = self.size_read + len(dummy)
143         return data
144 
145     def skip(self):
146         """Skip the rest of the chunk.
147         If you are not interested in the contents of the chunk,
148         this method should be called so that the file points to
149         the start of the next chunk.
150         """
151 
152         if self.closed:
153             raise ValueError, "I/O operation on closed file"
154         if self.seekable:
155             try:
156                 n = self.chunksize - self.size_read
157                 # maybe fix alignment
158                 if self.align and (self.chunksize & 1):
159                     n = n + 1
160                 self.file.seek(n, 1)
161                 self.size_read = self.size_read + n
162                 return
163             except IOError:
164                 pass
165         while self.size_read < self.chunksize:
166             n = min(8192, self.chunksize - self.size_read)
167             dummy = self.read(n)
168             if not dummy:
169                 raise EOFError
170