diff options
Diffstat (limited to 'doc/blender_file_format/BlendFileReader.py')
-rw-r--r-- | doc/blender_file_format/BlendFileReader.py | 446 |
1 files changed, 446 insertions, 0 deletions
diff --git a/doc/blender_file_format/BlendFileReader.py b/doc/blender_file_format/BlendFileReader.py new file mode 100644 index 00000000000..313c8c7ff5d --- /dev/null +++ b/doc/blender_file_format/BlendFileReader.py @@ -0,0 +1,446 @@ +#! /usr/bin/env python3 + +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ***** END GPL LICENCE BLOCK ***** + +###################################################### +# Importing modules +###################################################### + +import os +import struct +import gzip +import tempfile + +import logging +log = logging.getLogger("BlendFileReader") + +###################################################### +# module global routines +###################################################### + +def ReadString(handle, length): + ''' + ReadString reads a String of given length or a zero terminating String + from a file handle + ''' + if length != 0: + return handle.read(length).decode() + else: + # length == 0 means we want a zero terminating string + result = "" + s = ReadString(handle, 1) + while s!="\0": + result += s + s = ReadString(handle, 1) + return result + + +def Read(type, handle, fileheader): + ''' + Reads the chosen type from a file handle + ''' + def unpacked_bytes(type_char, size): + return struct.unpack(fileheader.StructPre + type_char, handle.read(size))[0] + + if type == 'ushort': + return unpacked_bytes("H", 2) # unsigned short + elif type == 'short': + return unpacked_bytes("h", 2) # short + elif type == 'uint': + return unpacked_bytes("I", 4) # unsigned int + elif type == 'int': + return unpacked_bytes("i", 4) # int + elif type == 'float': + return unpacked_bytes("f", 4) # float + elif type == 'ulong': + return unpacked_bytes("Q", 8) # unsigned long + elif type == 'pointer': + # The pointersize is given by the header (BlendFileHeader). + if fileheader.PointerSize == 4: + return Read('uint', handle, fileheader) + if fileheader.PointerSize == 8: + return Read('ulong', handle, fileheader) + + +def openBlendFile(filename): + ''' + Open a filename, determine if the file is compressed and returns a handle + ''' + handle = open(filename, 'rb') + magic = ReadString(handle, 7) + if magic in ("BLENDER", "BULLETf"): + log.debug("normal blendfile detected") + handle.seek(0, os.SEEK_SET) + return handle + else: + log.debug("gzip blendfile detected?") + handle.close() + log.debug("decompressing started") + fs = gzip.open(filename, "rb") + handle = tempfile.TemporaryFile() + data = fs.read(1024*1024) + while data: + handle.write(data) + data = fs.read(1024*1024) + log.debug("decompressing finished") + fs.close() + log.debug("resetting decompressed file") + handle.seek(0, os.SEEK_SET) + return handle + + +def Align(handle): + ''' + Aligns the filehandle on 4 bytes + ''' + offset = handle.tell() + trim = offset % 4 + if trim != 0: + handle.seek(4-trim, os.SEEK_CUR) + + +###################################################### +# module classes +###################################################### + +class BlendFile: + ''' + Reads a blendfile and store the header, all the fileblocks, and catalogue + structs foound in the DNA fileblock + + - BlendFile.Header (BlendFileHeader instance) + - BlendFile.Blocks (list of BlendFileBlock instances) + - BlendFile.Catalog (DNACatalog instance) + ''' + + def __init__(self, handle): + log.debug("initializing reading blend-file") + self.Header = BlendFileHeader(handle) + self.Blocks = [] + fileblock = BlendFileBlock(handle, self) + found_dna_block = False + while not found_dna_block: + if fileblock.Header.Code in ("DNA1", "SDNA"): + self.Catalog = DNACatalog(self.Header, handle) + found_dna_block = True + else: + fileblock.Header.skip(handle) + + self.Blocks.append(fileblock) + fileblock = BlendFileBlock(handle, self) + + # appending last fileblock, "ENDB" + self.Blocks.append(fileblock) + + # seems unused? + """ + def FindBlendFileBlocksWithCode(self, code): + #result = [] + #for block in self.Blocks: + #if block.Header.Code.startswith(code) or block.Header.Code.endswith(code): + #result.append(block) + #return result + """ + + +class BlendFileHeader: + ''' + BlendFileHeader allocates the first 12 bytes of a blend file. + It contains information about the hardware architecture. + Header example: BLENDER_v254 + + BlendFileHeader.Magic (str) + BlendFileHeader.PointerSize (int) + BlendFileHeader.LittleEndianness (bool) + BlendFileHeader.StructPre (str) see http://docs.python.org/py3k/library/struct.html#byte-order-size-and-alignment + BlendFileHeader.Version (int) + ''' + + def __init__(self, handle): + log.debug("reading blend-file-header") + + self.Magic = ReadString(handle, 7) + log.debug(self.Magic) + + pointersize = ReadString(handle, 1) + log.debug(pointersize) + if pointersize == "-": + self.PointerSize = 8 + if pointersize == "_": + self.PointerSize = 4 + + endianness = ReadString(handle, 1) + log.debug(endianness) + if endianness == "v": + self.LittleEndianness = True + self.StructPre = "<" + if endianness == "V": + self.LittleEndianness = False + self.StructPre = ">" + + version = ReadString(handle, 3) + log.debug(version) + self.Version = int(version) + + log.debug("{0} {1} {2} {3}".format(self.Magic, self.PointerSize, self.LittleEndianness, version)) + + +class BlendFileBlock: + ''' + BlendFileBlock.File (BlendFile) + BlendFileBlock.Header (FileBlockHeader) + ''' + + def __init__(self, handle, blendfile): + self.File = blendfile + self.Header = FileBlockHeader(handle, blendfile.Header) + + def Get(self, handle, path): + log.debug("find dna structure") + dnaIndex = self.Header.SDNAIndex + dnaStruct = self.File.Catalog.Structs[dnaIndex] + log.debug("found " + dnaStruct.Type.Name) + handle.seek(self.Header.FileOffset, os.SEEK_SET) + return dnaStruct.GetField(self.File.Header, handle, path) + + +class FileBlockHeader: + ''' + FileBlockHeader contains the information in a file-block-header. + The class is needed for searching to the correct file-block (containing Code: DNA1) + + Code (str) + Size (int) + OldAddress (pointer) + SDNAIndex (int) + Count (int) + FileOffset (= file pointer of datablock) + ''' + + def __init__(self, handle, fileheader): + self.Code = ReadString(handle, 4).strip() + if self.Code != "ENDB": + self.Size = Read('uint', handle, fileheader) + self.OldAddress = Read('pointer', handle, fileheader) + self.SDNAIndex = Read('uint', handle, fileheader) + self.Count = Read('uint', handle, fileheader) + self.FileOffset = handle.tell() + else: + self.Size = Read('uint', handle, fileheader) + self.OldAddress = 0 + self.SDNAIndex = 0 + self.Count = 0 + self.FileOffset = handle.tell() + #self.Code += ' ' * (4 - len(self.Code)) + log.debug("found blend-file-block-fileheader {0} {1}".format(self.Code, self.FileOffset)) + + def skip(self, handle): + handle.read(self.Size) + + +class DNACatalog: + ''' + DNACatalog is a catalog of all information in the DNA1 file-block + + Header = None + Names = None + Types = None + Structs = None + ''' + + def __init__(self, fileheader, handle): + log.debug("building DNA catalog") + self.Names=[] + self.Types=[] + self.Structs=[] + self.Header = fileheader + + SDNA = ReadString(handle, 4) + + # names + NAME = ReadString(handle, 4) + numberOfNames = Read('uint', handle, fileheader) + log.debug("building #{0} names".format(numberOfNames)) + for i in range(numberOfNames): + name = ReadString(handle,0) + self.Names.append(DNAName(name)) + Align(handle) + + # types + TYPE = ReadString(handle, 4) + numberOfTypes = Read('uint', handle, fileheader) + log.debug("building #{0} types".format(numberOfTypes)) + for i in range(numberOfTypes): + type = ReadString(handle,0) + self.Types.append(DNAType(type)) + Align(handle) + + # type lengths + TLEN = ReadString(handle, 4) + log.debug("building #{0} type-lengths".format(numberOfTypes)) + for i in range(numberOfTypes): + length = Read('ushort', handle, fileheader) + self.Types[i].Size = length + Align(handle) + + # structs + STRC = ReadString(handle, 4) + numberOfStructures = Read('uint', handle, fileheader) + log.debug("building #{0} structures".format(numberOfStructures)) + for structureIndex in range(numberOfStructures): + type = Read('ushort', handle, fileheader) + Type = self.Types[type] + structure = DNAStructure(Type) + self.Structs.append(structure) + + numberOfFields = Read('ushort', handle, fileheader) + for fieldIndex in range(numberOfFields): + fTypeIndex = Read('ushort', handle, fileheader) + fNameIndex = Read('ushort', handle, fileheader) + fType = self.Types[fTypeIndex] + fName = self.Names[fNameIndex] + structure.Fields.append(DNAField(fType, fName)) + + +class DNAName: + ''' + DNAName is a C-type name stored in the DNA. + + Name = str + ''' + + def __init__(self, name): + self.Name = name + + def AsReference(self, parent): + if parent == None: + result = "" + else: + result = parent+"." + + result = result + self.ShortName() + return result + + def ShortName(self): + result = self.Name; + result = result.replace("*", "") + result = result.replace("(", "") + result = result.replace(")", "") + Index = result.find("[") + if Index != -1: + result = result[0:Index] + return result + + def IsPointer(self): + return self.Name.find("*")>-1 + + def IsMethodPointer(self): + return self.Name.find("(*")>-1 + + def ArraySize(self): + result = 1 + Temp = self.Name + Index = Temp.find("[") + + while Index != -1: + Index2 = Temp.find("]") + result*=int(Temp[Index+1:Index2]) + Temp = Temp[Index2+1:] + Index = Temp.find("[") + + return result + + +class DNAType: + ''' + DNAType is a C-type stored in the DNA + + Name = str + Size = int + Structure = DNAStructure + ''' + + def __init__(self, aName): + self.Name = aName + self.Structure=None + + +class DNAStructure: + ''' + DNAType is a C-type structure stored in the DNA + + Type = DNAType + Fields = [DNAField] + ''' + + def __init__(self, aType): + self.Type = aType + self.Type.Structure = self + self.Fields=[] + + def GetField(self, header, handle, path): + splitted = path.partition(".") + name = splitted[0] + rest = splitted[2] + offset = 0; + for field in self.Fields: + if field.Name.ShortName() == name: + log.debug("found "+name+"@"+str(offset)) + handle.seek(offset, os.SEEK_CUR) + return field.DecodeField(header, handle, rest) + else: + offset += field.Size(header) + + log.debug("error did not find "+path) + return None + + +class DNAField: + ''' + DNAField is a coupled DNAType and DNAName. + + Type = DNAType + Name = DNAName + ''' + + def __init__(self, aType, aName): + self.Type = aType + self.Name = aName + + def Size(self, header): + if self.Name.IsPointer() or self.Name.IsMethodPointer(): + return header.PointerSize*self.Name.ArraySize() + else: + return self.Type.Size*self.Name.ArraySize() + + def DecodeField(self, header, handle, path): + if path == "": + if self.Name.IsPointer(): + return Read('pointer', handle, header) + if self.Type.Name=="int": + return Read('int', handle, header) + if self.Type.Name=="short": + return Read('short', handle, header) + if self.Type.Name=="float": + return Read('float', handle, header) + if self.Type.Name=="char": + return ReadString(handle, self.Name.ArraySize()) + else: + return self.Type.Structure.GetField(header, handle, path) + |