migrated NDOF code from soc-2010-merwin, SpaceNavigator now works on Mac blender
[blender.git] / doc / blender_file_format / BlendFileReader.py
1 #! /usr/bin/env python3
2
3 # ***** BEGIN GPL LICENSE BLOCK *****
4 #
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.
9 #
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.
14 #
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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
18 #
19 # ***** END GPL LICENCE BLOCK *****
20
21 ######################################################
22 # Importing modules
23 ######################################################
24
25 import os
26 import struct
27 import gzip
28 import tempfile
29
30 import logging
31 log = logging.getLogger("BlendFileReader")
32
33 ######################################################
34 # module global routines
35 ######################################################
36
37 def ReadString(handle, length):
38     '''
39     ReadString reads a String of given length or a zero terminating String
40     from a file handle
41     '''
42     if length != 0:
43         return handle.read(length).decode()
44     else:
45         # length == 0 means we want a zero terminating string
46         result = ""
47         s = ReadString(handle, 1)
48         while s!="\0":
49             result += s
50             s = ReadString(handle, 1)
51         return result
52
53
54 def Read(type, handle, fileheader):
55     '''
56     Reads the chosen type from a file handle
57     '''
58     def unpacked_bytes(type_char, size):
59         return struct.unpack(fileheader.StructPre + type_char, handle.read(size))[0]
60     
61     if type == 'ushort':
62         return unpacked_bytes("H", 2)   # unsigned short
63     elif type == 'short':
64         return unpacked_bytes("h", 2)   # short
65     elif type == 'uint':
66         return unpacked_bytes("I", 4)   # unsigned int
67     elif type == 'int':
68         return unpacked_bytes("i", 4)   # int
69     elif type == 'float':
70         return unpacked_bytes("f", 4)   # float
71     elif type == 'ulong':
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)
79
80
81 def openBlendFile(filename):
82     '''
83     Open a filename, determine if the file is compressed and returns a handle
84     '''
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)
90         return handle
91     else:
92         log.debug("gzip blendfile detected?")
93         handle.close()
94         log.debug("decompressing started")
95         fs = gzip.open(filename, "rb")
96         handle = tempfile.TemporaryFile()
97         data = fs.read(1024*1024) 
98         while data: 
99             handle.write(data) 
100             data = fs.read(1024*1024) 
101         log.debug("decompressing finished")
102         fs.close()
103         log.debug("resetting decompressed file")
104         handle.seek(0, os.SEEK_SET)
105         return handle
106
107
108 def Align(handle):
109     '''
110     Aligns the filehandle on 4 bytes
111     '''
112     offset = handle.tell()
113     trim = offset % 4
114     if trim != 0:
115         handle.seek(4-trim, os.SEEK_CUR)
116
117
118 ######################################################
119 # module classes
120 ######################################################
121
122 class BlendFile:
123     '''
124     Reads a blendfile and store the header, all the fileblocks, and catalogue 
125     structs foound in the DNA fileblock
126     
127     - BlendFile.Header  (BlendFileHeader instance)
128     - BlendFile.Blocks  (list of BlendFileBlock instances)
129     - BlendFile.Catalog (DNACatalog instance)
130     '''
131     
132     def __init__(self, handle):
133         log.debug("initializing reading blend-file")
134         self.Header = BlendFileHeader(handle)
135         self.Blocks = []
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
142             else:
143                 fileblock.Header.skip(handle)
144             
145             self.Blocks.append(fileblock)
146             fileblock = BlendFileBlock(handle, self)
147         
148         # appending last fileblock, "ENDB"
149         self.Blocks.append(fileblock)
150     
151     # seems unused?
152     """
153     def FindBlendFileBlocksWithCode(self, code):
154         #result = []
155         #for block in self.Blocks:
156             #if block.Header.Code.startswith(code) or block.Header.Code.endswith(code):
157                 #result.append(block)
158         #return result
159     """
160
161
162 class BlendFileHeader:
163     '''
164     BlendFileHeader allocates the first 12 bytes of a blend file.
165     It contains information about the hardware architecture.
166     Header example: BLENDER_v254
167     
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)
173     '''
174     
175     def __init__(self, handle):
176         log.debug("reading blend-file-header")
177         
178         self.Magic = ReadString(handle, 7)
179         log.debug(self.Magic)
180         
181         pointersize = ReadString(handle, 1)
182         log.debug(pointersize)
183         if pointersize == "-":
184             self.PointerSize = 8
185         if pointersize == "_":
186             self.PointerSize = 4
187                     
188         endianness = ReadString(handle, 1)
189         log.debug(endianness)
190         if endianness == "v":
191             self.LittleEndianness = True
192             self.StructPre = "<"
193         if endianness == "V":
194             self.LittleEndianness = False
195             self.StructPre = ">"
196         
197         version = ReadString(handle, 3)
198         log.debug(version)
199         self.Version = int(version)
200         
201         log.debug("{0} {1} {2} {3}".format(self.Magic, self.PointerSize, self.LittleEndianness, version))
202
203
204 class BlendFileBlock:
205     '''
206     BlendFileBlock.File     (BlendFile)
207     BlendFileBlock.Header   (FileBlockHeader)
208     '''
209     
210     def __init__(self, handle, blendfile):
211         self.File = blendfile
212         self.Header = FileBlockHeader(handle, blendfile.Header)
213         
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)
221
222
223 class FileBlockHeader:
224     '''
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)
227
228     Code        (str)
229     Size        (int)
230     OldAddress  (pointer)
231     SDNAIndex   (int)
232     Count       (int)
233     FileOffset  (= file pointer of datablock)
234     '''
235     
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()
244         else:
245             self.Size = Read('uint', handle, fileheader)
246             self.OldAddress = 0
247             self.SDNAIndex = 0
248             self.Count = 0
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))
252
253     def skip(self, handle):
254         handle.read(self.Size)
255
256
257 class DNACatalog:
258     '''
259     DNACatalog is a catalog of all information in the DNA1 file-block
260     
261     Header = None
262     Names = None
263     Types = None
264     Structs = None
265     '''
266     
267     def __init__(self, fileheader, handle):
268         log.debug("building DNA catalog")
269         self.Names=[]
270         self.Types=[]
271         self.Structs=[]
272         self.Header = fileheader
273         
274         SDNA = ReadString(handle, 4)
275         
276         # names
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))
283         Align(handle)
284
285         # types
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))
292         Align(handle)
293
294         # type lengths
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
300         Align(handle)
301
302         # structs
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)
311
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))
319
320
321 class DNAName:
322     '''
323     DNAName is a C-type name stored in the DNA.
324     
325     Name = str
326     '''
327     
328     def __init__(self, name):
329         self.Name = name
330         
331     def AsReference(self, parent):
332         if parent == None:
333             result = ""
334         else:
335             result = parent+"."
336             
337         result = result + self.ShortName()
338         return result
339
340     def ShortName(self):
341         result = self.Name;
342         result = result.replace("*", "")
343         result = result.replace("(", "")
344         result = result.replace(")", "")
345         Index = result.find("[")
346         if Index != -1:
347             result = result[0:Index]
348         return result
349         
350     def IsPointer(self):
351         return self.Name.find("*")>-1
352
353     def IsMethodPointer(self):
354         return self.Name.find("(*")>-1
355
356     def ArraySize(self):
357         result = 1
358         Temp = self.Name
359         Index = Temp.find("[")
360
361         while Index != -1:
362             Index2 = Temp.find("]")
363             result*=int(Temp[Index+1:Index2])
364             Temp = Temp[Index2+1:]
365             Index = Temp.find("[")
366         
367         return result
368
369
370 class DNAType:
371     '''
372     DNAType is a C-type stored in the DNA
373
374     Name = str
375     Size = int
376     Structure = DNAStructure
377     '''
378     
379     def __init__(self, aName):
380         self.Name = aName
381         self.Structure=None
382
383
384 class DNAStructure:
385     '''
386     DNAType is a C-type structure stored in the DNA
387     
388     Type = DNAType
389     Fields = [DNAField]
390     '''
391     
392     def __init__(self, aType):
393         self.Type = aType
394         self.Type.Structure = self
395         self.Fields=[]
396         
397     def GetField(self, header, handle, path):
398         splitted = path.partition(".")
399         name = splitted[0]
400         rest = splitted[2]
401         offset = 0;
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)
407             else:
408                 offset += field.Size(header)
409
410         log.debug("error did not find "+path)
411         return None
412
413
414 class DNAField:
415     '''
416     DNAField is a coupled DNAType and DNAName.
417     
418     Type = DNAType
419     Name = DNAName
420     '''
421
422     def __init__(self, aType, aName):
423         self.Type = aType
424         self.Name = aName
425         
426     def Size(self, header):
427         if self.Name.IsPointer() or self.Name.IsMethodPointer():
428             return header.PointerSize*self.Name.ArraySize()
429         else:
430             return self.Type.Size*self.Name.ArraySize()
431
432     def DecodeField(self, header, handle, path):
433         if 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())
444         else:
445             return self.Type.Structure.GetField(header, handle, path)
446