# ***** BEGIN LICENSE BLOCK *****
# Version: MPL 1.1/GPL 2.0/LGPL 2.1
#
# The contents of this file are subject to the Mozilla Public License Version
# 1.1 (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
# http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
# for the specific language governing rights and limitations under the
# License.
#
# The Original Code is the Python Computer Graphics Kit.
#
# The Initial Developer of the Original Code is Matthias Baas.
# Portions created by the Initial Developer are Copyright (C) 2004
# the Initial Developer. All Rights Reserved.
#
# Contributor(s):
#
# Alternatively, the contents of this file may be used under the terms of
# either the GNU General Public License Version 2 or later (the "GPL"), or
# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
# in which case the provisions of the GPL or the LGPL are applicable instead
# of those above. If you wish to allow use of your version of this file only
# under the terms of either the GPL or the LGPL, and not to allow others to
# use your version of this file under the terms of the MPL, indicate your
# decision by deleting the provisions above and replace them with the notice
# and other provisions required by the GPL or the LGPL. If you do not delete
# the provisions above, a recipient may use your version of this file under
# the terms of any one of the MPL, the GPL or the LGPL.
#
# ***** END LICENSE BLOCK *****
# $Id$
"""This module contains the MBReader base class to parse Maya binary files (~IFF files).
"""
import struct, os.path
class Chunk:
"""Chunk class.
This class stores information about a chunk. It is passed to the handler
methods which can also use this class to read the actual chunk data.
The class has the following attributes:
- tag: The chunk name (four characters)
- size: The size in bytes of the data part of the chunk
- pos: The absolute position of the data part within the input file
- parent: The GroupChunk object of the parent chunk
- depth: The depth of the node (i.e. how deep it is nested). The root
has a depth of 0.
The binary chunk data can be read by using the read() method. You
can specify the number of bytes to read or read the entire data
at once. It is not possible to read data that lies outside this
chunk.
"""
def __init__(self, file, parent, tag, size, pos, depth):
"""Constructor.
"""
# The file handle that currently points to the start of the chunk data
self.file = file
# The parent chunk object
self.parent = parent
# The chunk name (four characters)
self.tag = tag
# The chunk size in bytes (only the data part)
self.size = size
# The absolute position (of the data) within the input file
self.pos = pos
# The depth of the node (i.e. how deep it is nested)
self.depth = depth
# The number of bytes read so far
self._bytesRead = 0
def __str__(self):
return "<Chunk %s at pos %d (%d bytes)>"%(self.tag, self.pos, self.size)
def chunkPath(self):
"""Return the full path to this chunk.
The result is a concatenation of all chunk names that lead to this chunk.
"""
name = "%s"%(self.tag)
if self.parent is None:
return name
else:
return self.parent.chunkPath()+".%s"%name
def read(self, bytes=-1):
"""Read the specified number of bytes from the chunk.
If bytes is -1 the entire chunk data is read.
"""
if self.file is None:
raise RuntimeError, "This chunk is not active anymore"
maxbytes = self.size-self._bytesRead
if bytes<0:
bytes = maxbytes
else:
bytes = min(bytes, maxbytes)
self._bytesRead += bytes
return self.file.read(bytes)
class GroupChunk(Chunk):
"""Specialized group chunk class.
In addition to the Chunk class this class has an attribute "type"
that contains the group type (the first four characters of the data part).
"""
def __init__(self, file, parent, tag, size, pos, type, depth):
Chunk.__init__(self, file=file, parent=parent, tag=tag, size=size, pos=pos, depth=depth)
# The group type
self.type = type
# Start with 4 because the type was already read
self._bytesRead = 4
def __str__(self):
return "<GroupChunk %s (%s) at pos %d (%d bytes)>"%(self.tag, self.type, self.pos, self.size)
def chunkPath(self):
"""Return the full path to this chunk.
"""
name = "%s[%s]"%(self.tag, self.type)
if self.parent is None:
return name
else:
return self.parent.chunkPath()+".%s"%name
class MBReader:
"""Read Maya IFF files and call an appropriate handler for each chunk.
This is the base class for a .mb reader class. Derived classes can implement
the chunk handler methods onBeginGroup(), onEndGroup() and onDataChunk().
These handlers receive a Chunk (or GroupChunk) object as input that
contains information about the current chunk and that can also be used
to read the actual chunk data.
"""
def __init__(self):
pass
def abort(self):
"""Aborts reading the current file.
This method can be called in handler functions to abort reading
the file.
"""
self._abortFlag = True
def onBeginGroup(self, chunk):
"""Callback that is called whenever a new group tag begins.
chunk is a GroupChunk object containing information about the group chunk.
"""
pass
# print "BEGIN", chunk
def onEndGroup(self, chunk):
"""Callback that is called whenever a group goes out of scope.
chunk is a GroupChunk object containing information about the group chunk
(it is the same instance that was passed to onBeginGroup()).
"""
pass
# print "END", chunk
def onDataChunk(self, chunk):
"""Callback that is called for each data chunk.
chunk is a Chunk object that contains information about the chunk
and that can be used to read the actual chunk data.
"""
pass
# print " ", chunk
def read(self, file):
"""Read the binary file.
This method reads all chunks sequentially and invokes appropriate
callback methods.
file is a file-like object or the name of a file.
"""
if isinstance(file, basestring):
self.filename = file
file = open(file, "rb")
else:
self.filename = getattr(file, "name", "?")
# Check if this actually is a Maya file
# (and that it starts with a group tag)
header = file.read(12)
file.seek(0)
if len(header)!=12 or header[0:4]!="FOR4" or header[8:12]!="Maya":
raise ValueError, 'The file "%s" is not a Maya binary file.'%self.filename
self._file = file
self._abortFlag = False
# The current byte position inside the file
pos = 0
# A stack with alignment values. Each group tag pushes a new value
# which is popped when the group goes out of scope
alignments = []
# A stack with the currently open group chunks. The items are
# 2-tuples (endpos, groupchunk).
pendingGroups = []
# The current depth of the chunks
depth = 0
while not self._abortFlag:
tag,size = self.readChunkHeader()
if tag==None:
break
pos += 8
if self.isGroupChunk(tag):
# Group chunk...
type = file.read(4)
if len(pendingGroups)==0:
parent = None
else:
parent = pendingGroups[-1][1]
chunk = GroupChunk(file=file, parent=parent, tag=tag, size=size, pos=pos, type=type, depth=depth)
self.onBeginGroup(chunk)
av = self.alignmentValue(tag)
alignments.append(av)
end = pos+self.paddedSize(size, av)
if len(pendingGroups)>0 and end>pendingGroups[-1][0]:
raise ValueError, 'Chunk %s at position %s in file "%s" has an invalid size (%d) that goes beyond its contained group chunk.'%(tag,pos-8,os.path.basename(self.filename),size)
pendingGroups.append((end, chunk))
pos += 4
depth += 1
else:
# Data chunk...
chunk = Chunk(file=file, parent=pendingGroups[-1][1], tag=tag, size=size, pos=pos, depth=depth)
self.onDataChunk(chunk)
pos += self.paddedSize(size, alignments[-1])
# Check which groups are to be closed...
while len(pendingGroups)>0 and pos>=pendingGroups[-1][0]:
end,chunk = pendingGroups.pop()
self.onEndGroup(chunk)
depth -= 1
# Seek to the next chunk position. This is done here (even though it
# wouldn't be necessary in some cases) so that the callbacks have
# no chance to mess with the file handle and bring the reader
# out of sync.
file.seek(pos)
def readChunkHeader(self):
"""Read the tag and size of the next chunk.
Returns a tuple (tag, size) where tag is the four character
chunk name and size is an integer containing the size of the
data part of the chunk.
Returns None,None if the end of the file has been reached.
Throws an exception when an incomplete tag/size was read.
"""
header = self._file.read(8)
if len(header)==0:
return None,None
if len(header)!=8:
raise ValueError, 'Premature end of file "%s" (chunk tag & size expected)'%os.path.basename(self.filename)
return (header[:4], struct.unpack(">L", header[4:])[0])
def isGroupChunk(self, tag):
"""Check if the given tag refers to a group chunk.
tag is the chunk name. Returns True when tag is the name
of a group chunk.
"""
return tag in ["FORM", "CAT ", "LIST", "PROP",
"FOR4", "CAT4", "LIS4", "PRO4",
"FOR8", "CAT8", "LIS8", "PRO8"]
def alignmentValue(self, tag):
"""Return the alignment value for a group chunk.
Returns 2, 4 or 8.
"""
if tag in ["FORM", "CAT ", "LIST", "PROP"]:
return 2
elif tag in ["FOR4", "CAT4", "LIS4", "PRO4"]:
return 4
elif tag in ["FOR8", "CAT8", "LIS8", "PRO8"]:
return 8
else:
return 2
def paddedSize(self, size, alignment):
"""Return the padded size that is aligned to the given value.
size is an arbitrary chunk size and alignment an integer
containing the alignment value (2,4,8). If size is already
aligned it is returned unchanged, otherwise an appropriate
number of padding bytes is added and the aligned size is
returned.
"""
# Padding required?
if size%alignment!=0:
padding = alignment-size%alignment
size += padding
return size
def dump(self, buf):
"""Helper method to do a hex dump of chunk data.
buf is a string containing the data to dump.
"""
offset = 0
while len(buf)>0:
data = buf[:16]
buf = buf[16:]
s = "%04x: "%offset
s += " ".join(map(lambda x: "%02x"%ord(x), data))
s += (55-len(s))*' '
for c in data:
if ord(c)<32:
c = '.'
s += c
print s
offset += 16
if __name__=="__main__":
import sys
class MBDumper(MBReader):
def onBeginGroup(self, chunk):
print "GRP BEGIN", chunk
def onEndGroup(self, chunk):
print "GRP END ", chunk
def onDataChunk(self, chunk):
print "CHUNK ",chunk
rd = MBDumper()
rd.read(open(sys.argv[1], "rb"))
|