1 #! /usr/bin/env python3
3 # ***** BEGIN GPL LICENSE BLOCK *****
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software Foundation,
17 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 # ***** END GPL LICENCE BLOCK *****
21 ######################################################
23 ######################################################
31 log = logging.getLogger("BlendFileReader")
33 ######################################################
34 # module global routines
35 ######################################################
37 def ReadString(handle, length):
39 ReadString reads a String of given length or a zero terminating String
43 return handle.read(length).decode()
45 # length == 0 means we want a zero terminating string
47 s = ReadString(handle, 1)
50 s = ReadString(handle, 1)
54 def Read(type, handle, fileheader):
56 Reads the chosen type from a file handle
58 def unpacked_bytes(type_char, size):
59 return struct.unpack(fileheader.StructPre + type_char, handle.read(size))[0]
62 return unpacked_bytes("H", 2) # unsigned short
64 return unpacked_bytes("h", 2) # short
66 return unpacked_bytes("I", 4) # unsigned int
68 return unpacked_bytes("i", 4) # int
70 return unpacked_bytes("f", 4) # float
72 return unpacked_bytes("Q", 8) # unsigned long
73 elif type == 'pointer':
74 # The pointersize is given by the header (BlendFileHeader).
75 if fileheader.PointerSize == 4:
76 return Read('uint', handle, fileheader)
77 if fileheader.PointerSize == 8:
78 return Read('ulong', handle, fileheader)
81 def openBlendFile(filename):
83 Open a filename, determine if the file is compressed and returns a handle
85 handle = open(filename, 'rb')
86 magic = ReadString(handle, 7)
87 if magic in ("BLENDER", "BULLETf"):
88 log.debug("normal blendfile detected")
89 handle.seek(0, os.SEEK_SET)
92 log.debug("gzip blendfile detected?")
94 log.debug("decompressing started")
95 fs = gzip.open(filename, "rb")
96 handle = tempfile.TemporaryFile()
97 data = fs.read(1024*1024)
100 data = fs.read(1024*1024)
101 log.debug("decompressing finished")
103 log.debug("resetting decompressed file")
104 handle.seek(0, os.SEEK_SET)
110 Aligns the filehandle on 4 bytes
112 offset = handle.tell()
115 handle.seek(4-trim, os.SEEK_CUR)
118 ######################################################
120 ######################################################
124 Reads a blendfile and store the header, all the fileblocks, and catalogue
125 structs foound in the DNA fileblock
127 - BlendFile.Header (BlendFileHeader instance)
128 - BlendFile.Blocks (list of BlendFileBlock instances)
129 - BlendFile.Catalog (DNACatalog instance)
132 def __init__(self, handle):
133 log.debug("initializing reading blend-file")
134 self.Header = BlendFileHeader(handle)
136 fileblock = BlendFileBlock(handle, self)
137 found_dna_block = False
138 while not found_dna_block:
139 if fileblock.Header.Code in ("DNA1", "SDNA"):
140 self.Catalog = DNACatalog(self.Header, handle)
141 found_dna_block = True
143 fileblock.Header.skip(handle)
145 self.Blocks.append(fileblock)
146 fileblock = BlendFileBlock(handle, self)
148 # appending last fileblock, "ENDB"
149 self.Blocks.append(fileblock)
153 def FindBlendFileBlocksWithCode(self, code):
155 #for block in self.Blocks:
156 #if block.Header.Code.startswith(code) or block.Header.Code.endswith(code):
157 #result.append(block)
162 class BlendFileHeader:
164 BlendFileHeader allocates the first 12 bytes of a blend file.
165 It contains information about the hardware architecture.
166 Header example: BLENDER_v254
168 BlendFileHeader.Magic (str)
169 BlendFileHeader.PointerSize (int)
170 BlendFileHeader.LittleEndianness (bool)
171 BlendFileHeader.StructPre (str) see http://docs.python.org/py3k/library/struct.html#byte-order-size-and-alignment
172 BlendFileHeader.Version (int)
175 def __init__(self, handle):
176 log.debug("reading blend-file-header")
178 self.Magic = ReadString(handle, 7)
179 log.debug(self.Magic)
181 pointersize = ReadString(handle, 1)
182 log.debug(pointersize)
183 if pointersize == "-":
185 if pointersize == "_":
188 endianness = ReadString(handle, 1)
189 log.debug(endianness)
190 if endianness == "v":
191 self.LittleEndianness = True
193 if endianness == "V":
194 self.LittleEndianness = False
197 version = ReadString(handle, 3)
199 self.Version = int(version)
201 log.debug("{0} {1} {2} {3}".format(self.Magic, self.PointerSize, self.LittleEndianness, version))
204 class BlendFileBlock:
206 BlendFileBlock.File (BlendFile)
207 BlendFileBlock.Header (FileBlockHeader)
210 def __init__(self, handle, blendfile):
211 self.File = blendfile
212 self.Header = FileBlockHeader(handle, blendfile.Header)
214 def Get(self, handle, path):
215 log.debug("find dna structure")
216 dnaIndex = self.Header.SDNAIndex
217 dnaStruct = self.File.Catalog.Structs[dnaIndex]
218 log.debug("found " + dnaStruct.Type.Name)
219 handle.seek(self.Header.FileOffset, os.SEEK_SET)
220 return dnaStruct.GetField(self.File.Header, handle, path)
223 class FileBlockHeader:
225 FileBlockHeader contains the information in a file-block-header.
226 The class is needed for searching to the correct file-block (containing Code: DNA1)
233 FileOffset (= file pointer of datablock)
236 def __init__(self, handle, fileheader):
237 self.Code = ReadString(handle, 4).strip()
238 if self.Code != "ENDB":
239 self.Size = Read('uint', handle, fileheader)
240 self.OldAddress = Read('pointer', handle, fileheader)
241 self.SDNAIndex = Read('uint', handle, fileheader)
242 self.Count = Read('uint', handle, fileheader)
243 self.FileOffset = handle.tell()
245 self.Size = Read('uint', handle, fileheader)
249 self.FileOffset = handle.tell()
250 #self.Code += ' ' * (4 - len(self.Code))
251 log.debug("found blend-file-block-fileheader {0} {1}".format(self.Code, self.FileOffset))
253 def skip(self, handle):
254 handle.read(self.Size)
259 DNACatalog is a catalog of all information in the DNA1 file-block
267 def __init__(self, fileheader, handle):
268 log.debug("building DNA catalog")
272 self.Header = fileheader
274 SDNA = ReadString(handle, 4)
277 NAME = ReadString(handle, 4)
278 numberOfNames = Read('uint', handle, fileheader)
279 log.debug("building #{0} names".format(numberOfNames))
280 for i in range(numberOfNames):
281 name = ReadString(handle,0)
282 self.Names.append(DNAName(name))
286 TYPE = ReadString(handle, 4)
287 numberOfTypes = Read('uint', handle, fileheader)
288 log.debug("building #{0} types".format(numberOfTypes))
289 for i in range(numberOfTypes):
290 type = ReadString(handle,0)
291 self.Types.append(DNAType(type))
295 TLEN = ReadString(handle, 4)
296 log.debug("building #{0} type-lengths".format(numberOfTypes))
297 for i in range(numberOfTypes):
298 length = Read('ushort', handle, fileheader)
299 self.Types[i].Size = length
303 STRC = ReadString(handle, 4)
304 numberOfStructures = Read('uint', handle, fileheader)
305 log.debug("building #{0} structures".format(numberOfStructures))
306 for structureIndex in range(numberOfStructures):
307 type = Read('ushort', handle, fileheader)
308 Type = self.Types[type]
309 structure = DNAStructure(Type)
310 self.Structs.append(structure)
312 numberOfFields = Read('ushort', handle, fileheader)
313 for fieldIndex in range(numberOfFields):
314 fTypeIndex = Read('ushort', handle, fileheader)
315 fNameIndex = Read('ushort', handle, fileheader)
316 fType = self.Types[fTypeIndex]
317 fName = self.Names[fNameIndex]
318 structure.Fields.append(DNAField(fType, fName))
323 DNAName is a C-type name stored in the DNA.
328 def __init__(self, name):
331 def AsReference(self, parent):
337 result = result + self.ShortName()
342 result = result.replace("*", "")
343 result = result.replace("(", "")
344 result = result.replace(")", "")
345 Index = result.find("[")
347 result = result[0:Index]
351 return self.Name.find("*")>-1
353 def IsMethodPointer(self):
354 return self.Name.find("(*")>-1
359 Index = Temp.find("[")
362 Index2 = Temp.find("]")
363 result*=int(Temp[Index+1:Index2])
364 Temp = Temp[Index2+1:]
365 Index = Temp.find("[")
372 DNAType is a C-type stored in the DNA
376 Structure = DNAStructure
379 def __init__(self, aName):
386 DNAType is a C-type structure stored in the DNA
392 def __init__(self, aType):
394 self.Type.Structure = self
397 def GetField(self, header, handle, path):
398 splitted = path.partition(".")
402 for field in self.Fields:
403 if field.Name.ShortName() == name:
404 log.debug("found "+name+"@"+str(offset))
405 handle.seek(offset, os.SEEK_CUR)
406 return field.DecodeField(header, handle, rest)
408 offset += field.Size(header)
410 log.debug("error did not find "+path)
416 DNAField is a coupled DNAType and DNAName.
422 def __init__(self, aType, aName):
426 def Size(self, header):
427 if self.Name.IsPointer() or self.Name.IsMethodPointer():
428 return header.PointerSize*self.Name.ArraySize()
430 return self.Type.Size*self.Name.ArraySize()
432 def DecodeField(self, header, handle, path):
434 if self.Name.IsPointer():
435 return Read('pointer', handle, header)
436 if self.Type.Name=="int":
437 return Read('int', handle, header)
438 if self.Type.Name=="short":
439 return Read('short', handle, header)
440 if self.Type.Name=="float":
441 return Read('float', handle, header)
442 if self.Type.Name=="char":
443 return ReadString(handle, self.Name.ArraySize())
445 return self.Type.Structure.GetField(header, handle, path)