From e7c52a29b990bfd805d100e9d029a8e284254553 Mon Sep 17 00:00:00 2001 From: Campbell Barton Date: Tue, 14 Oct 2014 09:02:39 +0200 Subject: [PATCH] initial blendfile --- blendfile.py | 574 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 574 insertions(+) create mode 100644 blendfile.py diff --git a/blendfile.py b/blendfile.py new file mode 100644 index 0000000..44502f3 --- /dev/null +++ b/blendfile.py @@ -0,0 +1,574 @@ +# ***** 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# ***** END GPL LICENCE BLOCK ***** +# +# (c) 2009, At Mind B.V. - Jeroen Bakker + +# 06-10-2009: +# jbakker - adding support for python 3.0 +# 26-10-2009: +# jbakker - adding caching of the SDNA records. +# jbakker - adding caching for file block lookup +# jbakker - increased performance for readstring +# 27-10-2009: +# jbakker - remove FileBlockHeader class (reducing memory print, +# increasing performance) +# 28-10-2009: +# jbakker - reduce far-calls by joining setfield with encode and +# getfield with decode +# 02-11-2009: +# jbakker - python 3 compatibility added + + +###################################################### +# Importing modules +###################################################### +import os +import struct +import logging +import gzip +import tempfile +import sys + +log = logging.getLogger("blendfile") +FILE_BUFFER_SIZE=1024*1024 +###################################################### +# module global routines +###################################################### +# read routines +# open a filename +# determine if the file is compressed +# and returns a handle +def openBlendFile(filename, access="rb"): + """Opens a blend file for reading or writing pending on the access + supports 2 kind of blend files. Uncompressed and compressed. + Known issue: does not support packaged blend files + """ + handle = open(filename, access) + magic = ReadString(handle, 7) + if magic == "BLENDER": + log.debug("normal blendfile detected") + handle.seek(0, os.SEEK_SET) + res = BlendFile(handle) + res.compressed=False + res.originalfilename=filename + return res + else: + log.debug("gzip blendfile detected?") + handle.close() + log.debug("decompressing started") + fs = gzip.open(filename, "rb") + handle = tempfile.TemporaryFile() + data = fs.read(FILE_BUFFER_SIZE) + while data: + handle.write(data) + data = fs.read(FILE_BUFFER_SIZE) + log.debug("decompressing finished") + fs.close() + log.debug("resetting decompressed file") + handle.seek(os.SEEK_SET, 0) + res = BlendFile(handle) + res.compressed=True + res.originalfilename=filename + return res + +def closeBlendFile(afile): + """close the blend file + writes the blend file to disk if changes has happened""" + handle = afile.handle + if afile.compressed: + log.debug("close compressed blend file") + handle.seek(os.SEEK_SET, 0) + log.debug("compressing started") + fs = gzip.open(afile.originalfilename, "wb") + data = handle.read(FILE_BUFFER_SIZE) + while data: + fs.write(data) + data = handle.read(FILE_BUFFER_SIZE) + fs.close() + log.debug("compressing finished") + + handle.close() + +###################################################### +# Write a string to the file. +###################################################### +def WriteString(handle, astring, fieldlen): + stringw="" + if len(astring) >= fieldlen: + stringw=astring[0:fieldlen] + else: + stringw=astring+'\0' + handle.write(stringw.encode()) + +###################################################### +# ReadString reads a String of given length from a file handle +###################################################### +STRING=[] +for i in range(0, 2048): + STRING.append(struct.Struct(str(i)+"s")) + +def ReadString(handle, length): + st = STRING[length] + return st.unpack(handle.read(st.size))[0].decode("iso-8859-1") + +###################################################### +# ReadString0 reads a zero terminating String from a file handle +###################################################### +ZEROTESTER = 0 +if sys.version_info < (3, 0): + ZEROTESTER="\0" + +def ReadString0(data, offset): + add = 0 + + while data[offset+add]!=ZEROTESTER: + add+=1 + if add < len(STRING): + st = STRING[add] + S=st.unpack_from(data, offset)[0].decode("iso-8859-1") + else: + S=struct.Struct(str(add)+"s").unpack_from(data, offset)[0].decode("iso-8859-1") + return S + +###################################################### +# ReadUShort reads an unsigned short from a file handle +###################################################### +USHORT=[struct.Struct("H")] +def ReadUShort(handle, fileheader): + us = USHORT[fileheader.LittleEndiannessIndex] + return us.unpack(handle.read(us.size))[0] + +###################################################### +# ReadUInt reads an unsigned integer from a file handle +###################################################### +UINT=[struct.Struct("I")] +def ReadUInt(handle, fileheader): + us = UINT[fileheader.LittleEndiannessIndex] + return us.unpack(handle.read(us.size))[0] + +def ReadInt(handle, fileheader): + return struct.unpack(fileheader.StructPre+"i", handle.read(4))[0] +def ReadFloat(handle, fileheader): + return struct.unpack(fileheader.StructPre+"f", handle.read(4))[0] + +SSHORT=[struct.Struct("h")] +def ReadShort(handle, fileheader): + us = SSHORT[fileheader.LittleEndiannessIndex] + return us.unpack(handle.read(us.size))[0] + +ULONG=[struct.Struct("Q")] +def ReadULong(handle, fileheader): + us = ULONG[fileheader.LittleEndiannessIndex] + return us.unpack(handle.read(us.size))[0] + + +###################################################### +# ReadPointer reads an pointerfrom a file handle +# the pointersize is given by the header (BlendFileHeader) +###################################################### +def ReadPointer(handle, header): + if header.PointerSize == 4: + us = UINT[header.LittleEndiannessIndex] + return us.unpack(handle.read(us.size))[0] + if header.PointerSize == 8: + us = ULONG[header.LittleEndiannessIndex] + return us.unpack(handle.read(us.size))[0] + +###################################################### +# Allign alligns the filehandle on 4 bytes +###################################################### +def Allign(offset): + trim = offset % 4 + if trim != 0: + offset = offset + (4-trim) + return offset + +###################################################### +# module classes +###################################################### + +###################################################### +# BlendFile +# - Header (BlendFileHeader) +# - Blocks (FileBlockHeader) +# - Catalog (DNACatalog) +###################################################### +class BlendFile: + + def __init__(self, handle): + log.debug("initializing reading blend-file") + self.handle=handle + self.Header = BlendFileHeader(handle) + self.BlockHeaderStruct = self.Header.CreateBlockHeaderStruct() + self.Blocks = [] + self.CodeIndex = {} + + aBlock = BlendFileBlock(handle, self) + while aBlock.Code != "ENDB": + if aBlock.Code == "DNA1": + self.Catalog = DNACatalog(self.Header, aBlock, handle) + else: + handle.read(aBlock.Size) +# handle.seek(aBlock.Size, os.SEEK_CUR) does not work with py3.0! + + self.Blocks.append(aBlock) + + if aBlock.Code not in self.CodeIndex: + self.CodeIndex[aBlock.Code] = [] + self.CodeIndex[aBlock.Code].append(aBlock) + + aBlock = BlendFileBlock(handle, self) + self.Modified=False + self.Blocks.append(aBlock) + + def FindBlendFileBlocksWithCode(self, code): + if len(code) == 2: + code = code + if code not in self.CodeIndex: + return [] + return self.CodeIndex[code] + + def FindBlendFileBlockWithOffset(self, offset): + for block in self.Blocks: + if block.OldAddress == offset: + return block; + return None; + + def close(self): + if not self.Modified: + self.handle.close() + else: + closeBlendFile(self) + +###################################################### +# BlendFileBlock +# File=BlendFile +# Header=FileBlockHeader +###################################################### +class BlendFileBlock: + def __init__(self, handle, afile): + self.File = afile + header = afile.Header + + bytes = handle.read(afile.BlockHeaderStruct.size) + #header size can be 8, 20, or 24 bytes long + #8: old blend files ENDB block (exception) + #20: normal headers 32 bit platform + #24: normal headers 64 bit platform + if len(bytes)>15: + + blockheader = afile.BlockHeaderStruct.unpack(bytes) + self.Code = blockheader[0].decode().split("\0")[0] + if self.Code!="ENDB": + self.Size = blockheader[1] + self.OldAddress = blockheader[2] + self.SDNAIndex = blockheader[3] + self.Count = blockheader[4] + self.FileOffset = handle.tell() + else: + self.Size = 0 + self.OldAddress = 0 + self.SDNAIndex = 0 + self.Count = 0 + self.FileOffset = 0 + else: + blockheader = OLDBLOCK.unpack(bytes) + self.Code = blockheader[0].decode().split("\0")[0] + self.Size = 0 + self.OldAddress = 0 + self.SDNAIndex = 0 + self.Count = 0 + self.FileOffset = 0 + + def Get(self, path): + dnaIndex = self.SDNAIndex + dnaStruct = self.File.Catalog.Structs[dnaIndex] + self.File.handle.seek(self.FileOffset, os.SEEK_SET) + return dnaStruct.GetField(self.File.Header, self.File.handle, path) + + def Set(self, path, value): + dnaIndex = self.SDNAIndex + dnaStruct = self.File.Catalog.Structs[dnaIndex] + self.File.handle.seek(self.FileOffset, os.SEEK_SET) + self.File.Modified=True + return dnaStruct.SetField(self.File.Header, self.File.handle, path, value) + +###################################################### +# BlendFileHeader allocates the first 12 bytes of a blend file +# it contains information about the hardware architecture +# Magic = str +# PointerSize = int +# LittleEndianness = bool +# Version = int +###################################################### +BLOCKHEADERSTRUCT={} +BLOCKHEADERSTRUCT["<4"] = struct.Struct("<4sIIII") +BLOCKHEADERSTRUCT[">4"] = struct.Struct(">4sIIII") +BLOCKHEADERSTRUCT["<8"] = struct.Struct("<4sIQII") +BLOCKHEADERSTRUCT[">8"] = struct.Struct(">4sIQII") +FILEHEADER = struct.Struct("7s1s1s3s") +OLDBLOCK=struct.Struct("4sI") +class BlendFileHeader: + def __init__(self, handle): + log.debug("reading blend-file-header") + values = FILEHEADER.unpack(handle.read(FILEHEADER.size)) + self.Magic = values[0] + tPointerSize = values[1].decode() + if tPointerSize=="-": + self.PointerSize=8 + elif tPointerSize=="_": + self.PointerSize=4 + tEndianness = values[2].decode() + if tEndianness=="v": + self.LittleEndianness=True + self.StructPre="<" + self.LittleEndiannessIndex=0 + elif tEndianness=="V": + self.LittleEndianness=False + self.LittleEndiannessIndex=1 + self.StructPre=">" + + tVersion = values[3].decode() + self.Version = int(tVersion) + + def CreateBlockHeaderStruct(self): + return BLOCKHEADERSTRUCT[self.StructPre+str(self.PointerSize)] + +###################################################### +# DNACatalog is a catalog of all information in the DNA1 file-block +# +# Header=None +# Names=None +# Types=None +# Structs=None +###################################################### +class DNACatalog: + + def __init__(self, header, block, handle): + log.debug("building DNA catalog") + shortstruct = USHORT[header.LittleEndiannessIndex] + shortstruct2 = struct.Struct(str(USHORT[header.LittleEndiannessIndex].format.decode()+'H')) + intstruct = UINT[header.LittleEndiannessIndex] + data = handle.read(block.Size) + self.Names=[] + self.Types=[] + self.Structs=[] + + offset = 8; + numberOfNames = intstruct.unpack_from(data, offset)[0] + offset += 4 + + log.debug("building #"+str(numberOfNames)+" names") + for i in range(numberOfNames): + tName = ReadString0(data, offset) + offset = offset + len(tName) + 1 + self.Names.append(DNAName(tName)) + + offset = Allign(offset) + offset += 4 + numberOfTypes = intstruct.unpack_from(data, offset)[0] + offset += 4 + log.debug("building #"+str(numberOfTypes)+" types") + for i in range(numberOfTypes): + tType = ReadString0(data, offset) + self.Types.append([tType, 0, None]) + offset += len(tType)+1 + + offset = Allign(offset) + offset += 4 + log.debug("building #"+str(numberOfTypes)+" type-lengths") + for i in range(numberOfTypes): + tLen = shortstruct.unpack_from(data, offset)[0] + offset = offset + 2 + self.Types[i][1] = tLen + + offset = Allign(offset) + offset += 4 + + numberOfStructures = intstruct.unpack_from(data, offset)[0] + offset += 4 + log.debug("building #"+str(numberOfStructures)+" structures") + for structureIndex in range(numberOfStructures): + d = shortstruct2.unpack_from(data, offset) + tType = d[0] + offset += 4 + Type = self.Types[tType] + structure = DNAStructure(Type) + self.Structs.append(structure) + + numberOfFields = d[1] + + for fieldIndex in range(numberOfFields): + d2 = shortstruct2.unpack_from(data, offset) + fTypeIndex = d2[0] + fNameIndex = d2[1] + offset += 4 + fType = self.Types[fTypeIndex] + fName = self.Names[fNameIndex] + if fName.IsPointer or fName.IsMethodPointer: + fsize = header.PointerSize*fName.ArraySize + else: + fsize = fType[1]*fName.ArraySize + structure.Fields.append([fType, fName, fsize]) + +###################################################### +# DNAName is a C-type name stored in the DNA +# Name=str +###################################################### +class DNAName: + + def __init__(self, aName): + self.Name = aName + self.ShortName = self.DetermineShortName() + self.IsPointer = self.DetermineIsPointer() + self.IsMethodPointer = self.DetermineIsMethodPointer() + self.ArraySize = self.DetermineArraySize() + + def AsReference(self, parent): + if parent == None: + Result = "" + else: + Result = parent+"." + + Result = Result + self.ShortName + return Result + + def DetermineShortName(self): + Result = self.Name; + Result = Result.replace("*", "") + Result = Result.replace("(", "") + Result = Result.replace(")", "") + Index = Result.find("[") + if Index != -1: + Result = Result[0:Index] + self._SN = Result + return Result + + def DetermineIsPointer(self): + return self.Name.find("*")>-1 + + def DetermineIsMethodPointer(self): + return self.Name.find("(*")>-1 + + def DetermineArraySize(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 + +###################################################### +# DNAType is a C-type structure stored in the DNA +# +# Type=DNAType +# Fields=[DNAField] +###################################################### +class DNAStructure: + + def __init__(self, aType): + self.Type = aType + aType[2] = 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: + fname = field[1] + if fname.ShortName == name: + handle.seek(offset, os.SEEK_CUR) + ftype = field[0] + if len(rest) == 0: + + if fname.IsPointer: + return ReadPointer(handle, header) + elif ftype[0]=="int": + return ReadInt(handle, header) + elif ftype[0]=="short": + return ReadShort(handle, header) + elif ftype[0]=="float": + return ReadFloat(handle, header) + elif ftype[0]=="char": + return ReadString(handle, fname.ArraySize) + else: + return ftype[2].GetField(header, handle, rest) + + else: + offset += field[2] + + return None + + def SetField(self, header, handle, path, value): + splitted = path.partition(".") + name = splitted[0] + rest = splitted[2] + offset = 0; + for field in self.Fields: + fname = field[1] + if fname.ShortName == name: + handle.seek(offset, os.SEEK_CUR) + ftype = field[0] + if len(rest)==0: + if ftype[0]=="char": + return WriteString(handle, value, fname.ArraySize) + else: + return ftype[2].SetField(header, handle, rest, value) + else: + offset += field[2] + + return None + + + +###################################################### +# DNAField is a coupled DNAType and DNAName +# Type=DNAType +# Name=DNAName +###################################################### +class DNAField: + + 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 + +#determine the relative production location of a blender path.basename +def blendPath2AbsolutePath(productionFile, blenderPath): + productionFileDir=os.path.dirname(productionFile) + if blenderPath.startswith("//"): + relpath=blenderPath[2:] + abspath = os.path.join(productionFileDir, relpath) + return abspath + + + return blenderPath + +