tornavis/doc/blender_file_format/BlendFileReader.py

447 lines
13 KiB
Python

#!/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 is 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)