e955c94ed3
Listing the "Blender Foundation" as copyright holder implied the Blender Foundation holds copyright to files which may include work from many developers. While keeping copyright on headers makes sense for isolated libraries, Blender's own code may be refactored or moved between files in a way that makes the per file copyright holders less meaningful. Copyright references to the "Blender Foundation" have been replaced with "Blender Authors", with the exception of `./extern/` since these this contains libraries which are more isolated, any changed to license headers there can be handled on a case-by-case basis. Some directories in `./intern/` have also been excluded: - `./intern/cycles/` it's own `AUTHORS` file is planned. - `./intern/opensubdiv/`. An "AUTHORS" file has been added, using the chromium projects authors file as a template. Design task: #110784 Ref !110783.
432 lines
13 KiB
Python
432 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
# SPDX-FileCopyrightText: 2010-2023 Blender Authors
|
|
#
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
######################################################
|
|
# 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 found 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)
|