blender/doc/blender_file_format/BlendFileReader.py
Campbell Barton e955c94ed3 License Headers: Set copyright to "Blender Authors", add AUTHORS
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.
2023-08-16 00:20:26 +10:00

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)