Merge branch 'blender2.7'
[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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 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
38 def ReadString(handle, length):
39     '''
40     ReadString reads a String of given length or a zero terminating String
41     from a file handle
42     '''
43     if length != 0:
44         return handle.read(length).decode()
45     else:
46         # length == 0 means we want a zero terminating string
47         result = ""
48         s = ReadString(handle, 1)
49         while s != "\0":
50             result += s
51             s = ReadString(handle, 1)
52         return result
53
54
55 def Read(type, handle, fileheader):
56     '''
57     Reads the chosen type from a file handle
58     '''
59     def unpacked_bytes(type_char, size):
60         return struct.unpack(fileheader.StructPre + type_char, handle.read(size))[0]
61
62     if type == 'ushort':
63         return unpacked_bytes("H", 2)   # unsigned short
64     elif type == 'short':
65         return unpacked_bytes("h", 2)   # short
66     elif type == 'uint':
67         return unpacked_bytes("I", 4)   # unsigned int
68     elif type == 'int':
69         return unpacked_bytes("i", 4)   # int
70     elif type == 'float':
71         return unpacked_bytes("f", 4)   # float
72     elif type == 'ulong':
73         return unpacked_bytes("Q", 8)   # unsigned long
74     elif type == 'pointer':
75         # The pointersize is given by the header (BlendFileHeader).
76         if fileheader.PointerSize == 4:
77             return Read('uint', handle, fileheader)
78         if fileheader.PointerSize == 8:
79             return Read('ulong', handle, fileheader)
80
81
82 def openBlendFile(filename):
83     '''
84     Open a filename, determine if the file is compressed and returns a handle
85     '''
86     handle = open(filename, 'rb')
87     magic = ReadString(handle, 7)
88     if magic in ("BLENDER", "BULLETf"):
89         log.debug("normal blendfile detected")
90         handle.seek(0, os.SEEK_SET)
91         return handle
92     else:
93         log.debug("gzip blendfile detected?")
94         handle.close()
95         log.debug("decompressing started")
96         fs = gzip.open(filename, "rb")
97         handle = tempfile.TemporaryFile()
98         data = fs.read(1024 * 1024)
99         while data:
100             handle.write(data)
101             data = fs.read(1024 * 1024)
102         log.debug("decompressing finished")
103         fs.close()
104         log.debug("resetting decompressed file")
105         handle.seek(0, os.SEEK_SET)
106         return handle
107
108
109 def Align(handle):
110     '''
111     Aligns the filehandle on 4 bytes
112     '''
113     offset = handle.tell()
114     trim = offset % 4
115     if trim != 0:
116         handle.seek(4 - trim, os.SEEK_CUR)
117
118
119 ######################################################
120 # module classes
121 ######################################################
122
123 class BlendFile:
124     '''
125     Reads a blendfile and store the header, all the fileblocks, and catalogue
126     structs foound in the DNA fileblock
127
128     - BlendFile.Header  (BlendFileHeader instance)
129     - BlendFile.Blocks  (list of BlendFileBlock instances)
130     - BlendFile.Catalog (DNACatalog instance)
131     '''
132
133     def __init__(self, handle):
134         log.debug("initializing reading blend-file")
135         self.Header = BlendFileHeader(handle)
136         self.Blocks = []
137         fileblock = BlendFileBlock(handle, self)
138         found_dna_block = False
139         while not found_dna_block:
140             if fileblock.Header.Code in ("DNA1", "SDNA"):
141                 self.Catalog = DNACatalog(self.Header, handle)
142                 found_dna_block = True
143             else:
144                 fileblock.Header.skip(handle)
145
146             self.Blocks.append(fileblock)
147             fileblock = BlendFileBlock(handle, self)
148
149         # appending last fileblock, "ENDB"
150         self.Blocks.append(fileblock)
151
152     # seems unused?
153     """
154     def FindBlendFileBlocksWithCode(self, code):
155         #result = []
156         #for block in self.Blocks:
157             #if block.Header.Code.startswith(code) or block.Header.Code.endswith(code):
158                 #result.append(block)
159         #return result
160     """
161
162
163 class BlendFileHeader:
164     '''
165     BlendFileHeader allocates the first 12 bytes of a blend file.
166     It contains information about the hardware architecture.
167     Header example: BLENDER_v254
168
169     BlendFileHeader.Magic             (str)
170     BlendFileHeader.PointerSize       (int)
171     BlendFileHeader.LittleEndianness  (bool)
172     BlendFileHeader.StructPre         (str)   see http://docs.python.org/py3k/library/struct.html#byte-order-size-and-alignment
173     BlendFileHeader.Version           (int)
174     '''
175
176     def __init__(self, handle):
177         log.debug("reading blend-file-header")
178
179         self.Magic = ReadString(handle, 7)
180         log.debug(self.Magic)
181
182         pointersize = ReadString(handle, 1)
183         log.debug(pointersize)
184         if pointersize == "-":
185             self.PointerSize = 8
186         if pointersize == "_":
187             self.PointerSize = 4
188
189         endianness = ReadString(handle, 1)
190         log.debug(endianness)
191         if endianness == "v":
192             self.LittleEndianness = True
193             self.StructPre = "<"
194         if endianness == "V":
195             self.LittleEndianness = False
196             self.StructPre = ">"
197
198         version = ReadString(handle, 3)
199         log.debug(version)
200         self.Version = int(version)
201
202         log.debug("{0} {1} {2} {3}".format(self.Magic, self.PointerSize, self.LittleEndianness, version))
203
204
205 class BlendFileBlock:
206     '''
207     BlendFileBlock.File     (BlendFile)
208     BlendFileBlock.Header   (FileBlockHeader)
209     '''
210
211     def __init__(self, handle, blendfile):
212         self.File = blendfile
213         self.Header = FileBlockHeader(handle, blendfile.Header)
214
215     def Get(self, handle, path):
216         log.debug("find dna structure")
217         dnaIndex = self.Header.SDNAIndex
218         dnaStruct = self.File.Catalog.Structs[dnaIndex]
219         log.debug("found " + dnaStruct.Type.Name)
220         handle.seek(self.Header.FileOffset, os.SEEK_SET)
221         return dnaStruct.GetField(self.File.Header, handle, path)
222
223
224 class FileBlockHeader:
225     '''
226     FileBlockHeader contains the information in a file-block-header.
227     The class is needed for searching to the correct file-block (containing Code: DNA1)
228
229     Code        (str)
230     Size        (int)
231     OldAddress  (pointer)
232     SDNAIndex   (int)
233     Count       (int)
234     FileOffset  (= file pointer of datablock)
235     '''
236
237     def __init__(self, handle, fileheader):
238         self.Code = ReadString(handle, 4).strip()
239         if self.Code != "ENDB":
240             self.Size = Read('uint', handle, fileheader)
241             self.OldAddress = Read('pointer', handle, fileheader)
242             self.SDNAIndex = Read('uint', handle, fileheader)
243             self.Count = Read('uint', handle, fileheader)
244             self.FileOffset = handle.tell()
245         else:
246             self.Size = Read('uint', handle, fileheader)
247             self.OldAddress = 0
248             self.SDNAIndex = 0
249             self.Count = 0
250             self.FileOffset = handle.tell()
251         #self.Code += ' ' * (4 - len(self.Code))
252         log.debug("found blend-file-block-fileheader {0} {1}".format(self.Code, self.FileOffset))
253
254     def skip(self, handle):
255         handle.read(self.Size)
256
257
258 class DNACatalog:
259     '''
260     DNACatalog is a catalog of all information in the DNA1 file-block
261
262     Header = None
263     Names = None
264     Types = None
265     Structs = None
266     '''
267
268     def __init__(self, fileheader, handle):
269         log.debug("building DNA catalog")
270         self.Names = []
271         self.Types = []
272         self.Structs = []
273         self.Header = fileheader
274
275         SDNA = ReadString(handle, 4)
276
277         # names
278         NAME = ReadString(handle, 4)
279         numberOfNames = Read('uint', handle, fileheader)
280         log.debug("building #{0} names".format(numberOfNames))
281         for i in range(numberOfNames):
282             name = ReadString(handle, 0)
283             self.Names.append(DNAName(name))
284         Align(handle)
285
286         # types
287         TYPE = ReadString(handle, 4)
288         numberOfTypes = Read('uint', handle, fileheader)
289         log.debug("building #{0} types".format(numberOfTypes))
290         for i in range(numberOfTypes):
291             type = ReadString(handle, 0)
292             self.Types.append(DNAType(type))
293         Align(handle)
294
295         # type lengths
296         TLEN = ReadString(handle, 4)
297         log.debug("building #{0} type-lengths".format(numberOfTypes))
298         for i in range(numberOfTypes):
299             length = Read('ushort', handle, fileheader)
300             self.Types[i].Size = length
301         Align(handle)
302
303         # structs
304         STRC = ReadString(handle, 4)
305         numberOfStructures = Read('uint', handle, fileheader)
306         log.debug("building #{0} structures".format(numberOfStructures))
307         for structureIndex in range(numberOfStructures):
308             type = Read('ushort', handle, fileheader)
309             Type = self.Types[type]
310             structure = DNAStructure(Type)
311             self.Structs.append(structure)
312
313             numberOfFields = Read('ushort', handle, fileheader)
314             for fieldIndex in range(numberOfFields):
315                 fTypeIndex = Read('ushort', handle, fileheader)
316                 fNameIndex = Read('ushort', handle, fileheader)
317                 fType = self.Types[fTypeIndex]
318                 fName = self.Names[fNameIndex]
319                 structure.Fields.append(DNAField(fType, fName))
320
321
322 class DNAName:
323     '''
324     DNAName is a C-type name stored in the DNA.
325
326     Name = str
327     '''
328
329     def __init__(self, name):
330         self.Name = name
331
332     def AsReference(self, parent):
333         if parent is None:
334             result = ""
335         else:
336             result = parent + "."
337
338         result = result + self.ShortName()
339         return result
340
341     def ShortName(self):
342         result = self.Name
343         result = result.replace("*", "")
344         result = result.replace("(", "")
345         result = result.replace(")", "")
346         Index = result.find("[")
347         if Index != -1:
348             result = result[0:Index]
349         return result
350
351     def IsPointer(self):
352         return self.Name.find("*") > -1
353
354     def IsMethodPointer(self):
355         return self.Name.find("(*") > -1
356
357     def ArraySize(self):
358         result = 1
359         Temp = self.Name
360         Index = Temp.find("[")
361
362         while Index != -1:
363             Index2 = Temp.find("]")
364             result *= int(Temp[Index + 1:Index2])
365             Temp = Temp[Index2 + 1:]
366             Index = Temp.find("[")
367
368         return result
369
370
371 class DNAType:
372     '''
373     DNAType is a C-type stored in the DNA
374
375     Name = str
376     Size = int
377     Structure = DNAStructure
378     '''
379
380     def __init__(self, aName):
381         self.Name = aName
382         self.Structure = None
383
384
385 class DNAStructure:
386     '''
387     DNAType is a C-type structure stored in the DNA
388
389     Type = DNAType
390     Fields = [DNAField]
391     '''
392
393     def __init__(self, aType):
394         self.Type = aType
395         self.Type.Structure = self
396         self.Fields = []
397
398     def GetField(self, header, handle, path):
399         splitted = path.partition(".")
400         name = splitted[0]
401         rest = splitted[2]
402         offset = 0
403         for field in self.Fields:
404             if field.Name.ShortName() == name:
405                 log.debug("found " + name + "@" + str(offset))
406                 handle.seek(offset, os.SEEK_CUR)
407                 return field.DecodeField(header, handle, rest)
408             else:
409                 offset += field.Size(header)
410
411         log.debug("error did not find " + path)
412         return None
413
414
415 class DNAField:
416     '''
417     DNAField is a coupled DNAType and DNAName.
418
419     Type = DNAType
420     Name = DNAName
421     '''
422
423     def __init__(self, aType, aName):
424         self.Type = aType
425         self.Name = aName
426
427     def Size(self, header):
428         if self.Name.IsPointer() or self.Name.IsMethodPointer():
429             return header.PointerSize * self.Name.ArraySize()
430         else:
431             return self.Type.Size * self.Name.ArraySize()
432
433     def DecodeField(self, header, handle, path):
434         if path == "":
435             if self.Name.IsPointer():
436                 return Read('pointer', handle, header)
437             if self.Type.Name == "int":
438                 return Read('int', handle, header)
439             if self.Type.Name == "short":
440                 return Read('short', handle, header)
441             if self.Type.Name == "float":
442                 return Read('float', handle, header)
443             if self.Type.Name == "char":
444                 return ReadString(handle, self.Name.ArraySize())
445         else:
446             return self.Type.Structure.GetField(header, handle, path)